proxy,ui-svelte: replace old UI with svelte+playground
Replace the legacy React UI with the new Svelte-based one. Introduce a Playground in the UI to quickly test out text, image, text to speech and speech to text models behind llama-swap.
Key Changes
New Svelte UI (ui-svelte/)
- Multi-tab Playground with Chat, Image Generation, Audio Transcription, and Speech interfaces
- Chat: message editing/regeneration, markdown rendering with LaTeX math support, image attachments, code syntax highlighting
- Image: size selector, download/fullscreen viewing
- Audio: transcription with peer support
- Speech: voice caching with manual refresh, download button
- Responsive mobile layout with collapsible navigation
- XSS fixes and accessibility improvements
Proxy Improvements
- Add gzip/brotli compression for UI static assets (proxy/ui_compress.go)
- Add GET /v1/audio/voices?model={model} endpoint for voice listing
- Add peer support for /v1/audio/transcriptions
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { unified } from "unified";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
import rehypeStringify from "rehype-stringify";
|
||||
import hljs from "highlight.js";
|
||||
import { visit } from "unist-util-visit";
|
||||
import type { Element, Root } from "hast";
|
||||
|
||||
// Custom plugin to highlight code blocks with highlight.js
|
||||
function rehypeHighlight() {
|
||||
return (tree: Root) => {
|
||||
visit(tree, "element", (node: Element) => {
|
||||
if (node.tagName === "code" && node.properties) {
|
||||
const className = node.properties.className;
|
||||
const classes = Array.isArray(className)
|
||||
? className.filter((c): c is string => typeof c === "string")
|
||||
: [];
|
||||
const lang = classes
|
||||
.find((c) => c.startsWith("language-"))
|
||||
?.replace("language-", "");
|
||||
|
||||
const text = node.children
|
||||
.filter((child): child is { type: "text"; value: string } => child.type === "text")
|
||||
.map((child) => child.value)
|
||||
.join("");
|
||||
|
||||
if (text) {
|
||||
const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
|
||||
const highlighted = hljs.highlight(text, { language }).value;
|
||||
|
||||
// Replace the text node with raw HTML
|
||||
node.properties.className = [
|
||||
"hljs",
|
||||
`language-${language}`,
|
||||
...classes.filter((c) => !c.startsWith("language-")),
|
||||
];
|
||||
// Use type assertion since we're modifying the tree structure
|
||||
(node.children as unknown) = [
|
||||
{ type: "raw", value: highlighted },
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function escapeHtml(text: string): string {
|
||||
const htmlEntities: Record<string, string> = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
};
|
||||
return text.replace(/[&<>"']/g, (char) => htmlEntities[char]);
|
||||
}
|
||||
|
||||
// Create the unified processor
|
||||
const processor = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkMath)
|
||||
.use(remarkRehype, { allowDangerousHtml: true })
|
||||
.use(rehypeKatex)
|
||||
.use(rehypeHighlight)
|
||||
.use(rehypeStringify, { allowDangerousHtml: true });
|
||||
|
||||
export function renderMarkdown(content: string): string {
|
||||
if (!content) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const result = processor.processSync(content);
|
||||
return String(result);
|
||||
} catch {
|
||||
// Fallback to escaped plain text if markdown parsing fails
|
||||
return `<p>${escapeHtml(content)}</p>`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user