From f5d383ec94b5cedd881d1a1a2ff7befef99af823 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 26 Aug 2025 04:49:54 +0200 Subject: [PATCH] Add user editing UI improvements and bubble styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented an editing state that visually highlights the user message bubble with an accent border and shadow. • Updated the message render logic to handle an CSS class and overlay a textarea that perfectly aligns with the bubble using a hidden shadow element. • Re‑worked the and styles to apply the accent border, box‑shadow, and transition effects. • Adjusted the textarea styling (, ) so it inherits the bubble’s look, respects whitespace, and automatically resizes without extra padding or borders. • Added a hidden to keep the bubble width/height consistent while editing. • Included new button styling for expandable user messages. • Minor cleanup of redundant padding, margin, and color definitions in the styles. • Updated the JSX to conditionally render the overlay textarea only during editing, ensuring a smooth UX. • Added appropriate ARIA attributes and cursor styles for clarity. --- src/App.jsx | 132 ++++++++++++++++++++++++++++++------------------- src/styles.css | 107 +++++++++++++++++++++++++++++++++------ 2 files changed, 173 insertions(+), 66 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 4a6beba..ec59370 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -146,6 +146,7 @@ export default function App() { setEditingMessageIndex(index); setEditText(content || ''); } + function cancelEditMessage() { setEditingMessageIndex(null); setEditText(''); @@ -153,11 +154,21 @@ export default function App() { async function commitEditMessage(index) { const original = (messages[index]?.content || '').trim(); - const next = (editText || '').trim(); + const nextRaw = editText ?? ''; + const next = nextRaw.trim(); + + // NEW: If empty after trimming, cancel edit (revert to original) + if (next.length === 0) { + cancelEditMessage(); + return; + } + + // If nothing changed, cancel edit if (next === original) { cancelEditMessage(); return; } + const sessionId = activeSessionId; if (!sessionId) return; @@ -166,7 +177,6 @@ export default function App() { prev.map(s => { if (s.session_id !== sessionId) return s; const old = s.messages || []; - // keep up to index (inclusive) and update that item const updated = old.slice(0, index + 1).map((m, j) => j === index ? { ...m, content: next } : m ); @@ -174,11 +184,12 @@ export default function App() { }) ); + // Exit edit mode immediately setEditingMessageIndex(null); setEditText(''); // ⬇️ Scroll the chat frame to the bottom after the DOM updates - requestAnimationFrame(() => scrollToBottom('auto', sessionId)); // use 'smooth' if you prefer + requestAnimationFrame(() => scrollToBottom('auto', sessionId)); try { const resp = await fetch(`${ollamaApiUrl}/sessions/${sessionId}/messages/${index}`, { @@ -1140,54 +1151,73 @@ export default function App() {
- {messages.map((m, i) => ( -
- {m.role === 'assistant' ? ( -
- - {!isSending && ( -
- - -
- )} -
- ) : ( -
- {editingMessageIndex === i ? ( - setEditText(e.target.value)} - onBlur={cancelEditMessage} - onKeyDown={(e) => { - if (e.key === 'Escape') { e.preventDefault(); cancelEditMessage(); } - if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); commitEditMessage(i); } - }} - autoFocus - maxRows={13} - /> - ) : ( -
{m.content}
- )} - {!isSending && ( -
- - -
- )} -
- )} -
-))} + {messages.map((m, i) => { + const isEditingThis = m.role === 'user' && editingMessageIndex === i; + return ( +
+ {m.role === 'assistant' ? ( +
+ + {!isSending && ( +
+ + +
+ )} +
+ ) : ( +
+ {isEditingThis ? ( +
+ {/* Shadow defines bubble width/height based on current text */} + + + {/* Overlay textarea fills the bubble exactly */} + setEditText(e.target.value)} + onBlur={cancelEditMessage} + onKeyDown={(e) => { + if (e.key === 'Escape') { e.preventDefault(); cancelEditMessage(); } + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); commitEditMessage(i); } + }} + autoFocus + minRows={1} + /> +
+ ) : ( +
{m.content}
+ )} + {!isSending && !isEditingThis && ( +
+ + +
+ )} +
+ )} +
+ ); + })}
{/* New message tip (active chat only) */} diff --git a/src/styles.css b/src/styles.css index f1740c4..d8767c2 100644 --- a/src/styles.css +++ b/src/styles.css @@ -320,18 +320,46 @@ textarea.input { flex-grow: 1; /* Allow textarea to take up available space */ } +/* /Users/giers/Heimgeist/src/styles.css */ +/* User message bubble */ .msg.user { background: var(--user-msg-bg); - margin-left: auto; /* Right-align user messages */ + margin-left: auto; /* Right-align user messages */ max-width: 80%; border: 1px solid var(--border); margin-right: 5px; margin-bottom: 15px; + transition: border-color .15s ease, box-shadow .15s ease; /* NEW */ } + +.msg.user.editing { + border-color: var(--accent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent); +} + +/* Render user messages as plain text (no markdown), preserving line breaks and wrapping */ .msg.user .msg-content--user { white-space: pre-wrap; /* keep newlines */ overflow-wrap: anywhere; /* wrap long tokens/URLs */ word-break: break-word; /* fallback */ + position: relative; +} + +/* Expand link inside the user bubble */ +.msg.user .user-msg-expand { + margin-top: 8px; + padding: 0; + background: transparent; + border: none; + color: var(--accent); + font-weight: 700; /* bold as requested */ + cursor: pointer; + text-align: left; /* align with text content */ +} + +.msg.user .user-msg-expand:hover { + opacity: .9; + text-decoration: underline; } .msg.assistant { @@ -793,22 +821,71 @@ input:checked + .slider:before { .msg-content { /* container for assistant/user rendered markdown */ } +.msg.user .msg-content--user.editing { + /* Bubble (.msg.user) already provides padding/border/max-width/rounded corners. */ + /* Keep this wrapper neutral — no extra padding or borders here. */ +} /* Editing textarea styled like user bubble */ +/* Editing textarea should be visually indistinguishable from the user bubble */ .edit-message-input { - background: var(--user-msg-bg); - border: 1px solid var(--border); - padding: 12px 14px; - border-radius: 12px; - margin-left: auto; - margin-right: 5px; - margin-bottom: 15px; - max-width: 80%; - width: 100%; - color: var(--text); - font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Inter, Helvetica, Arial; - line-height: 1.5; - resize: none; - overflow-y: auto; display: block; + width: 100%; + height: 100%; /* autosize will override with inline height; harmless */ + background: transparent; /* let the bubble show through */ + border: 0; /* bubble has the border */ + padding: 0; /* bubble provides padding */ + margin: 0; + + color: inherit; + font: inherit; + line-height: inherit; + letter-spacing: inherit; + + white-space: pre-wrap; /* keep newlines like the view mode */ + overflow-wrap: anywhere; /* wrap long tokens/URLs */ + word-break: break-word; + + resize: none; /* autosize controls height */ + overflow: hidden; /* smooth growth */ + outline: none; +} +.edit-message-input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent); +} + +/* Shadow stays in flow to set the bubble's width & height, but remains invisible */ +.user-edit-shadow { + visibility: hidden; + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; +} + +/* Overlayed textarea fills exactly the content box of the bubble */ +.edit-message-input.edit-overlay { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + + background: transparent; /* bubble provides bg */ + border: 0; /* bubble provides border */ + padding: 0; /* bubble provides padding */ + margin: 0; + + color: inherit; + font: inherit; + line-height: inherit; + letter-spacing: inherit; + + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; + + resize: none; /* autosize controls height if needed */ + overflow: hidden; + outline: none; + box-shadow: none; }