studio: add SVG preview, fix streaming bug and model selector state (#4354)

- Add SVG preview rendering below code blocks using safe data URI
  in <img> tag. Includes sanitization to block script/event handlers.
- Fix GGUF streaming crash: cache response.iter_text() iterator
  instead of creating a new one on every loop iteration.
- Fix model selector showing "Select model..." after auto-load by
  re-reading store state after setCheckpoint before setParams.
- Remove unused warmupToastShown variable (TS6133 build error).
- Change default suggestion to "Draw an SVG of a cute sloth".
This commit is contained in:
Daniel Han 2026-03-17 02:34:05 -07:00 committed by GitHub
commit 37fe04f7bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 46 additions and 13 deletions

View file

@ -1277,12 +1277,13 @@ class LlamaCppBackend:
the next token. Without this, iter_text() blocks until the next
chunk arrives and cancellation can take many seconds on large models.
"""
text_iter = response.iter_text()
while True:
if cancel_event is not None and cancel_event.is_set():
response.close()
return
try:
chunk = next(response.iter_text())
chunk = next(text_iter)
yield chunk
except StopIteration:
return

View file

@ -64,6 +64,7 @@ function getCodeFilename(language: string | null) {
ts: "ts",
tsx: "tsx",
typescript: "ts",
svg: "svg",
yaml: "yml",
yml: "yml",
};
@ -76,6 +77,33 @@ function getCodeFilename(language: string | null) {
return `snippet.${ext}`;
}
function isSvgFence(codeFence: CodeFence): boolean {
const lang = codeFence.language?.toLowerCase() ?? "";
if (lang === "svg") return true;
if ((lang === "xml" || lang === "html") && codeFence.source.trimStart().startsWith("<svg")) return true;
return false;
}
const UNSAFE_SVG_RE = /<script[\s>]|on\w+\s*=|javascript:|<foreignObject[\s>]|<iframe[\s>]|<embed[\s>]|<object[\s>]/i;
function sanitizeSvg(source: string): string | null {
if (UNSAFE_SVG_RE.test(source)) return null;
return source;
}
function SvgPreview({ source }: { source: string }) {
const dataUri = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`;
return (
<div className="mt-2 flex justify-center rounded-lg border border-border bg-white p-4 dark:bg-neutral-100">
<img
src={dataUri}
alt="SVG preview"
style={{ maxWidth: "100%", maxHeight: 512 }}
/>
</div>
);
}
function downloadTextFile(filename: string, text: string): void {
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
@ -207,15 +235,19 @@ function StreamdownBlock(props: BlockProps) {
}
if (codeFence) {
const svgSource = !props.isIncomplete && isSvgFence(codeFence) ? sanitizeSvg(codeFence.source) : null;
return (
<div className="relative isolate">
<Block {...props} />
<CodeBlockActions
disabled={props.isIncomplete}
language={codeFence.language}
source={codeFence.source}
/>
</div>
<>
<div className="relative isolate">
<Block {...props} />
<CodeBlockActions
disabled={props.isIncomplete}
language={codeFence.language}
source={codeFence.source}
/>
</div>
{svgSource && <SvgPreview source={svgSource} />}
</>
);
}

View file

@ -232,8 +232,8 @@ async function autoLoadSmallestModel(): Promise<boolean> {
gguf_variant: variant.quant,
trust_remote_code: false,
});
useChatRuntimeStore.getState().setCheckpoint(repo.repo_id, variant.quant);
const store = useChatRuntimeStore.getState();
store.setCheckpoint(repo.repo_id, variant.quant);
store.setParams({ ...store.params, maxTokens: loadResp.context_length ?? 131072 });
// Add model to store so the selector shows the name
const autoModel: ChatModelSummary = {
@ -282,8 +282,8 @@ async function autoLoadSmallestModel(): Promise<boolean> {
gguf_variant: null,
trust_remote_code: false,
});
useChatRuntimeStore.getState().setCheckpoint(repo.repo_id);
const store = useChatRuntimeStore.getState();
store.setCheckpoint(repo.repo_id);
store.setParams({ ...store.params, maxTokens: 4096 });
const sfModel: ChatModelSummary = {
id: repo.repo_id,
@ -319,8 +319,8 @@ async function autoLoadSmallestModel(): Promise<boolean> {
gguf_variant: "UD-Q4_K_XL",
trust_remote_code: false,
});
useChatRuntimeStore.getState().setCheckpoint("unsloth/Qwen3.5-4B-GGUF", "UD-Q4_K_XL");
const store = useChatRuntimeStore.getState();
store.setCheckpoint("unsloth/Qwen3.5-4B-GGUF", "UD-Q4_K_XL");
store.setParams({ ...store.params, maxTokens: loadResp.context_length ?? 131072 });
const defaultModel: ChatModelSummary = {
id: "unsloth/Qwen3.5-4B-GGUF",

View file

@ -31,7 +31,7 @@ import { useChatRuntimeStore } from "./stores/chat-runtime-store";
import type { MessageRecord, ModelType } from "./types";
const DEFAULT_SUGGESTIONS = [
"Draw an ASCII art of a cute sloth",
"Draw an SVG of a cute sloth",
"Solve the integral of x²·sin(x) step by step",
"Write a Python function that finds the longest palindrome in a string",
"Format a comparison of 3 databases as a markdown table with pros and cons",