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:
parent
33dc47da72
commit
37fe04f7bf
4 changed files with 46 additions and 13 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue