diff --git a/frontend/src/lib/components/chat/ChatInput.svelte b/frontend/src/lib/components/chat/ChatInput.svelte index 3aa453b..3d9804c 100644 --- a/frontend/src/lib/components/chat/ChatInput.svelte +++ b/frontend/src/lib/components/chat/ChatInput.svelte @@ -113,7 +113,7 @@ }); -
+
{#if isVisionModel} - + {#if isVisionModel && pendingImages.length > 0} -
- +
+ -
+
{#if showStopButton}
- -

- Press Enter to send, - Shift+Enter for new line + +

+ Enter send + · + Shift+Enter new line {#if isVisionModel} - | Vision model: paste or drag images + · + images supported {/if}

diff --git a/frontend/src/lib/components/chat/ChatWindow.svelte b/frontend/src/lib/components/chat/ChatWindow.svelte index 161e00a..bc7edfb 100644 --- a/frontend/src/lib/components/chat/ChatWindow.svelte +++ b/frontend/src/lib/components/chat/ChatWindow.svelte @@ -615,7 +615,7 @@ } -
+
{#if hasMessages}
{/if} -
- - + +
+ +
- - {#if hasMessages} -
- +
+ + + + + {#if hasMessages} +
+ +
+ {/if} + +
+
- {/if} - -
-
diff --git a/frontend/src/lib/components/chat/CodeBlock.svelte b/frontend/src/lib/components/chat/CodeBlock.svelte index 9595761..3c14240 100644 --- a/frontend/src/lib/components/chat/CodeBlock.svelte +++ b/frontend/src/lib/components/chat/CodeBlock.svelte @@ -14,9 +14,11 @@ language?: string; /** Whether to show the run button for executable code */ showRunButton?: boolean; + /** Skip syntax highlighting during streaming to prevent layout shifts */ + isStreaming?: boolean; } - const { code, language = 'text', showRunButton = true }: Props = $props(); + const { code, language = 'text', showRunButton = true, isStreaming = false }: Props = $props(); // State for highlighted HTML and copy feedback let highlightedHtml = $state(''); @@ -190,17 +192,26 @@ } // Highlight code when component mounts or code/language changes + // Skip highlighting during streaming to prevent layout shifts $effect(() => { // Access reactive dependencies - const _ = [code, language]; + const _ = [code, language, isStreaming]; + + if (isStreaming) { + // During streaming, just show plain code + isLoading = true; + return; + } + + // Only highlight when not streaming highlightCode(); }); -
+
{language}
@@ -270,47 +281,53 @@
- -
+ +
{#if isLoading} -
{code}
+
{code}
{:else} -
- {@html highlightedHtml} -
+ {@html highlightedHtml} {/if}
{#if showOutput && (isExecuting || executionResult)} -
+
-
+
- Output + + + + + Output + {#if isExecuting} - + - Running... + Running {:else if executionResult} - - {executionResult.duration}ms - {#if executionResult.status === 'success'} - Success - {:else if executionResult.status === 'error'} - Error + Completed + {:else} + + + + + Error + {/if} + {executionResult.duration}ms {/if}
-
+
{#if executionResult?.outputs.length} -
{#each executionResult.outputs as output}{output.content}
-{/each}
+
{#each executionResult.outputs as output}{output.content}{/each}
{:else if !isExecuting} -

No output

+

No output

{/if}
@@ -331,33 +347,36 @@
diff --git a/frontend/src/lib/components/chat/EmptyState.svelte b/frontend/src/lib/components/chat/EmptyState.svelte index fb50176..6a6acb2 100644 --- a/frontend/src/lib/components/chat/EmptyState.svelte +++ b/frontend/src/lib/components/chat/EmptyState.svelte @@ -1,32 +1,95 @@
- +
- +
-

+

{#if hasModel} Start a conversation {:else} @@ -34,68 +97,88 @@ {/if}

-

+

{#if hasModel && selectedModel} - You're chatting with {selectedModel.name}. - Type a message below to begin. + Chatting with {selectedModel.name} {:else} - Please select a model from the sidebar to start chatting. + Select a model from the sidebar to start chatting {/if}

- + {#if hasModel} -
+
{@render SuggestionCard({ + type: "question", icon: "lightbulb", title: "Ask a question", description: "Get explanations on any topic" })} {@render SuggestionCard({ + type: "code", icon: "code", title: "Write code", - description: "Generate or debug code snippets" + description: "Generate or debug code" })} {@render SuggestionCard({ + type: "content", icon: "pencil", title: "Create content", - description: "Draft emails, articles, or stories" + description: "Draft emails or articles" })} {@render SuggestionCard({ + type: "conversation", icon: "chat", title: "Have a conversation", - description: "Discuss ideas and get feedback" + description: "Discuss ideas and brainstorm" })}
+ + + {#if activeQuickPrompt} +

+ + + + + {activeQuickPrompt} mode active + +

+ {/if} {/if}
-{#snippet SuggestionCard(props: { icon: string; title: string; description: string })} -
selectPrompt(props.type)} + class="flex items-start gap-3 rounded-xl border p-3 text-left transition-all {active + ? 'border-violet-500/50 bg-violet-500/10' + : 'border-slate-800/50 bg-slate-800/30 hover:border-slate-700 hover:bg-slate-800/60'}" > -
+
{#if props.icon === 'lightbulb'} - + {:else if props.icon === 'code'} - + {:else if props.icon === 'pencil'} - + {:else if props.icon === 'chat'} - + {/if}
-

{props.title}

-

{props.description}

+

{props.title}

+

{props.description}

-
+ {/snippet} diff --git a/frontend/src/lib/components/chat/MessageContent.svelte b/frontend/src/lib/components/chat/MessageContent.svelte index 3d1143c..6d02cf1 100644 --- a/frontend/src/lib/components/chat/MessageContent.svelte +++ b/frontend/src/lib/components/chat/MessageContent.svelte @@ -14,9 +14,10 @@ interface Props { content: string; images?: string[]; + isStreaming?: boolean; } - const { content, images }: Props = $props(); + const { content, images, isStreaming = false }: Props = $props(); // Pattern to find fenced code blocks const CODE_BLOCK_PATTERN = /```(\w+)?\n([\s\S]*?)```/g; @@ -259,6 +260,7 @@ {#if part.showPreview} diff --git a/frontend/src/lib/components/chat/MessageItem.svelte b/frontend/src/lib/components/chat/MessageItem.svelte index 47cb5be..3078be3 100644 --- a/frontend/src/lib/components/chat/MessageItem.svelte +++ b/frontend/src/lib/components/chat/MessageItem.svelte @@ -110,9 +110,9 @@ {#if isAssistant} {#if isToolMessage} - + {:else} - + {/if} @@ -150,10 +150,10 @@
{#if isEditing} @@ -188,6 +188,7 @@ {/if} @@ -238,7 +239,7 @@ {#if isUser} diff --git a/frontend/src/lib/components/layout/SidenavHeader.svelte b/frontend/src/lib/components/layout/SidenavHeader.svelte index 45cdbc6..dbd1968 100644 --- a/frontend/src/lib/components/layout/SidenavHeader.svelte +++ b/frontend/src/lib/components/layout/SidenavHeader.svelte @@ -4,7 +4,7 @@ * Contains the app logo/title and "New Chat" button */ import { goto } from '$app/navigation'; - import { uiState, chatState } from '$lib/stores'; + import { uiState, chatState, promptsState } from '$lib/stores'; /** * Handle new chat - reset state and navigate @@ -12,6 +12,7 @@ function handleNewChat(event: MouseEvent) { event.preventDefault(); chatState.reset(); + promptsState.clearTemporaryPrompt(); goto('/'); } diff --git a/frontend/src/lib/stores/prompts.svelte.ts b/frontend/src/lib/stores/prompts.svelte.ts index 435d0a9..ab0daf3 100644 --- a/frontend/src/lib/stores/prompts.svelte.ts +++ b/frontend/src/lib/stores/prompts.svelte.ts @@ -42,6 +42,9 @@ class PromptsState { /** Currently selected prompt for new chats (null = no system prompt) */ activePromptId = $state(null); + /** Temporary prompt content for session (overrides activePromptId when set) */ + temporaryPrompt = $state<{ name: string; content: string } | null>(null); + /** Loading state */ isLoading = $state(false); @@ -52,8 +55,12 @@ class PromptsState { private _readyPromise: Promise | null = null; private _readyResolve: (() => void) | null = null; - /** Derived: active prompt content */ - get activePrompt(): Prompt | null { + /** Derived: active prompt content (temporary takes precedence) */ + get activePrompt(): Prompt | { name: string; content: string } | null { + // Temporary prompt takes precedence + if (this.temporaryPrompt) { + return this.temporaryPrompt; + } if (!this.activePromptId) return null; return this.prompts.find(p => p.id === this.activePromptId) ?? null; } @@ -254,6 +261,23 @@ class PromptsState { */ setActive(id: string | null): void { this.activePromptId = id; + // Clear temporary prompt when explicitly setting active + this.temporaryPrompt = null; + } + + /** + * Set a temporary prompt for the current session (overrides stored prompts) + * Use this for quick-start prompts from the suggestion cards + */ + setTemporaryPrompt(name: string, content: string): void { + this.temporaryPrompt = { name, content }; + } + + /** + * Clear the temporary prompt + */ + clearTemporaryPrompt(): void { + this.temporaryPrompt = null; } /**