From 3e521440588ef8a881ae82c584292f72c86171c0 Mon Sep 17 00:00:00 2001 From: Benson Wong Date: Sun, 15 Feb 2026 11:00:44 -0800 Subject: [PATCH] ui-svelte: incremental rendering of chat messages in the Playground (#520) add incremental rendering to Playground > Chat --- .../playground/ChatInterface.svelte | 45 ++- .../components/playground/ChatMessage.svelte | 30 +- ui-svelte/src/lib/markdown.test.ts | 265 +++++++++++++++++- ui-svelte/src/lib/markdown.ts | 178 +++++++++++- 4 files changed, 502 insertions(+), 16 deletions(-) diff --git a/ui-svelte/src/components/playground/ChatInterface.svelte b/ui-svelte/src/components/playground/ChatInterface.svelte index 4b2ba99..752d7e6 100644 --- a/ui-svelte/src/components/playground/ChatInterface.svelte +++ b/ui-svelte/src/components/playground/ChatInterface.svelte @@ -11,7 +11,16 @@ const systemPromptStore = persistentStore("playground-system-prompt", ""); const temperatureStore = persistentStore("playground-temperature", 0.7); - let messages = $state([]); + function loadMessages(): ChatMessage[] { + try { + const saved = localStorage.getItem("playground-messages"); + return saved ? JSON.parse(saved) : []; + } catch { + return []; + } + } + + let messages = $state(loadMessages()); let userInput = $state(""); let isStreaming = $state(false); let isReasoning = $state(false); @@ -24,21 +33,48 @@ let imageError = $state(null); let hasModels = $derived($models.some((m) => !m.unlisted)); + let userScrolledUp = $state(false); - // Auto-scroll when messages change + function handleMessagesScroll() { + if (!messagesContainer) return; + const { scrollTop, scrollHeight, clientHeight } = messagesContainer; + // Consider "at bottom" if within 40px of the bottom + userScrolledUp = scrollHeight - scrollTop - clientHeight > 40; + } + + // Auto-scroll when messages change — skip if user scrolled up $effect(() => { - if (messages.length > 0 && messagesContainer) { + if (messages.length > 0 && messagesContainer && !userScrolledUp) { messagesContainer.scrollTo({ top: messagesContainer.scrollHeight, - behavior: "smooth", + behavior: isStreaming ? "instant" : "smooth", }); } }); + // Persist messages to localStorage (throttled to once per 2s) + let lastSaveTime = 0; + $effect(() => { + const json = JSON.stringify(messages); + const elapsed = Date.now() - lastSaveTime; + const save = () => { + try { localStorage.setItem("playground-messages", json); } catch {} + lastSaveTime = Date.now(); + }; + if (elapsed >= 2000) { + save(); + return; + } + const timer = setTimeout(save, 2000 - elapsed); + return () => clearTimeout(timer); + }); + async function sendMessage() { const trimmedInput = userInput.trim(); if ((!trimmedInput && attachedImages.length === 0) || !$selectedModelStore || isStreaming) return; + userScrolledUp = false; + // Build message content (multimodal if images attached) let content: string | ContentPart[]; if (attachedImages.length > 0) { @@ -321,6 +357,7 @@
{#if messages.length === 0}
diff --git a/ui-svelte/src/components/playground/ChatMessage.svelte b/ui-svelte/src/components/playground/ChatMessage.svelte index 1f57382..2bbb5fc 100644 --- a/ui-svelte/src/components/playground/ChatMessage.svelte +++ b/ui-svelte/src/components/playground/ChatMessage.svelte @@ -1,5 +1,6 @@