diff --git a/grid-agent-gui/frontend/src/components/ChatInterface.svelte b/grid-agent-gui/frontend/src/components/ChatInterface.svelte index 5f279fe..f28e20f 100644 --- a/grid-agent-gui/frontend/src/components/ChatInterface.svelte +++ b/grid-agent-gui/frontend/src/components/ChatInterface.svelte @@ -32,6 +32,7 @@ let showErrorModal = false; let showSettings = false; let showDocs = false; + let showClearModal = false; let errorMessage = ""; let isExporting = false; let isAborting = false; @@ -235,6 +236,102 @@ errorMessage = ""; } + function handleClear() { + showClearModal = true; + } + + function clearConversation() { + showClearModal = false; + messagesStore.set([]); + } + + async function saveAndClear() { + showClearModal = false; + await handleExport(); + messagesStore.set([]); + } + + function editMessage(index: number, newContent: string) { + // First update the message content and timestamp + messagesStore.update((msgs) => { + const newMsgs = [...msgs]; + if (newMsgs[index] && newMsgs[index].role === "user") { + newMsgs[index] = { ...newMsgs[index], content: newContent, timestamp: new Date().toISOString() }; + } + return newMsgs; + }); + + // If this user message has an agent response immediately following, retrigger AI + const messages = $messagesStore; + const nextIndex = index + 1; + if (messages[index].role === "user" && nextIndex < messages.length && messages[nextIndex].role === "agent") { + // Remove the agent response + messagesStore.update((msgs) => { + return msgs.filter((_, i) => i !== nextIndex); + }); + + // Add placeholder for new response + const requestID = `req_${Date.now()}_${Math.random()}`; + const placeholderMsg = { + role: "agent", + content: "", + timestamp: new Date().toISOString(), + requestID: requestID, + steps: [ + { + progressText: "🤔 Processing your request...", + exportPrefix: "", + content: "Processing your request...", + output: "", + error: "", + }, + ], + }; + messagesStore.update((msgs) => [...msgs, placeholderMsg]); + + // Send the edited message directly + (async () => { + isSending = true; + currentRequestID = requestID; + try { + const response = await SendMessage(newContent, requestID); + messagesStore.update((msgs) => { + const idx = msgs.findIndex((m) => m.requestID === response.requestID); + if (idx !== -1) { + const newMsgs = [...msgs]; + newMsgs[idx] = response; + return newMsgs; + } + return [...msgs, response]; // Fallback + }); + } catch (error) { + console.error("Failed to send message:", error); + messagesStore.update((msgs) => { + const idx = msgs.findIndex((m) => m.requestID === requestID); + if (idx !== -1) { + const existingMsg = msgs[idx]; + const steps = (existingMsg.steps || []).filter( + (s) => !s.progressText?.includes("🤔 Processing"), + ); + steps.push({ + progressText: "❌ Error", + exportPrefix: "", + content: "", + output: "", + error: String(error), + }); + msgs[idx] = { ...existingMsg, content: error, steps }; + } + return [...msgs]; + }); + } finally { + isSending = false; + currentRequestID = ""; + } + })(); + } + } + function toggleSettings() { showSettings = !showSettings; } @@ -612,6 +709,25 @@ > {/if} + + + + + + +{/if} + (showSettings = false)} /> (showDocs = false)} /> @@ -970,6 +1101,15 @@ background: rgba(239, 68, 68, 0.1); } + .clear-btn { + color: var(--text-secondary); + } + + .clear-btn:hover { + color: var(--error); + background: rgba(239, 68, 68, 0.1); + } + /* Floating Input Area */ .input-area { padding: 1.5rem; diff --git a/grid-agent-gui/frontend/src/components/ChatMessage.svelte b/grid-agent-gui/frontend/src/components/ChatMessage.svelte index ed5cbe5..79c77d2 100644 --- a/grid-agent-gui/frontend/src/components/ChatMessage.svelte +++ b/grid-agent-gui/frontend/src/components/ChatMessage.svelte @@ -6,6 +6,8 @@ import { marked } from "marked"; import DOMPurify from "dompurify"; import { BrowserOpenURL } from "../../wailsjs/runtime/runtime.js"; + import { createEventDispatcher } from "svelte"; + const dispatch = createEventDispatcher(); // Step types const STEP_TYPE_TOOL = "tool"; @@ -32,6 +34,9 @@ error?: string; }; + let isEditing = false; + let editContent = message.content; + const isUser = message.role === "user"; let showSteps = false; @@ -104,6 +109,34 @@ BrowserOpenURL(link.href); } } + + function startEditing() { + if (isUser) { + isEditing = true; + editContent = message.content; + } + } + + function saveEdit() { + if (isUser && editContent.trim()) { + dispatch('edit', { content: editContent.trim() }); + isEditing = false; + } + } + + function cancelEdit() { + isEditing = false; + editContent = message.content; + } + + function handleKeydown(event: KeyboardEvent) { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + saveEdit(); + } else if (event.key === 'Escape') { + cancelEdit(); + } + }
@@ -118,9 +151,44 @@
{#if message.content} -
- {@html renderMarkdown(message.content)} -
+ {#if isEditing} +
+ +
+ + +
+
+ {:else} +
+
+ {@html renderMarkdown(message.content)} +
+ {#if isUser} + + {/if} +
+ {/if} {:else if message.steps && message.steps.length > 0}
@@ -543,4 +611,113 @@ font-weight: 700; color: inherit; /* inherit ensures it looks good in user bubbles too */ } + + /* Edit functionality styling */ + .message-content { + position: relative; + } + + .edit-trigger { + position: absolute; + bottom: 0.5rem; + right: 0.5rem; + background: transparent; + border: none; + cursor: pointer; + font-size: 1rem; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; + padding: 0.25rem; + border-radius: 0.25rem; + color: var(--text-secondary); + } + + .message-content:hover .edit-trigger { + opacity: 1; + pointer-events: auto; + background: var(--bg-tertiary); + color: var(--text-primary); + } + + .edit-container { + display: flex; + flex-direction: column; + gap: 0.75rem; + } + + .edit-textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--border); + border-radius: 0.5rem; + background: var(--bg-primary); + color: var(--text-primary); + font-family: inherit; + font-size: 0.95rem; + resize: vertical; + min-height: 3rem; + outline: none; + transition: border-color 0.2s; + } + + .edit-textarea:focus { + border-color: var(--accent); + } + + .user .edit-textarea { + background: rgba(255, 255, 255, 0.1); + color: white; + } + + .agent .edit-textarea { + background: var(--bg-primary); + color: var(--text-primary); + } + + .edit-actions { + display: flex; + gap: 0.5rem; + justify-content: flex-end; + } + + .edit-btn { + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + border: none; + } + + .save-btn { + background: var(--accent); + color: white; + } + + .save-btn:hover { + background: var(--accent-hover); + } + + .cancel-btn { + background: transparent; + color: var(--text-secondary); + border: 1px solid var(--border); + } + + .cancel-btn:hover { + background: var(--bg-tertiary); + color: var(--text-primary); + } + + .user .cancel-btn { + color: rgba(255, 255, 255, 0.8); + border-color: rgba(255, 255, 255, 0.3); + } + + .user .cancel-btn:hover { + background: rgba(255, 255, 255, 0.1); + color: white; + }