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 */}
+
+ {editText}
+
+
+ {/* 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;
}