Files
llama-swap/ui-svelte/src/lib/markdown.ts
T
Benson Wong 20738f3623 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
2026-01-31 22:49:13 -08:00

85 lines
2.5 KiB
TypeScript

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> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};
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>`;
}
}