Safetensors
GGUF
Turkish
llama
Llama-3
instruct
finetune
chatml
gpt4
synthetic data
distillation
function calling
json mode
axolotl
roleplaying
chat
Instructions to use tda45/TdAI with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- llama-cpp-python
How to use tda45/TdAI with llama-cpp-python:
# !pip install llama-cpp-python from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="tda45/TdAI", filename="llama.cpp/models/ggml-vocab-aquila.gguf", )
output = llm( "Once upon a time,", max_tokens=512, echo=True ) print(output)
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use tda45/TdAI with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf tda45/TdAI # Run inference directly in the terminal: llama cli -hf tda45/TdAI
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf tda45/TdAI # Run inference directly in the terminal: llama cli -hf tda45/TdAI
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf tda45/TdAI # Run inference directly in the terminal: ./llama-cli -hf tda45/TdAI
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf tda45/TdAI # Run inference directly in the terminal: ./build/bin/llama-cli -hf tda45/TdAI
Use Docker
docker model run hf.co/tda45/TdAI
- LM Studio
- Jan
- Ollama
How to use tda45/TdAI with Ollama:
ollama run hf.co/tda45/TdAI
- Unsloth Studio
How to use tda45/TdAI with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for tda45/TdAI to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for tda45/TdAI to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for tda45/TdAI to start chatting
- Atomic Chat new
- Docker Model Runner
How to use tda45/TdAI with Docker Model Runner:
docker model run hf.co/tda45/TdAI
- Lemonade
How to use tda45/TdAI with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull tda45/TdAI
Run and chat with the model
lemonade run user.TdAI-{{QUANT_TAG}}List all available models
lemonade list
TdAI / llama.cpp /tools /ui /src /lib /components /app /content /MarkdownContent /MarkdownContent.svelte
| <script lang="ts"> | |
| import { remark } from 'remark'; | |
| import remarkBreaks from 'remark-breaks'; | |
| import remarkGfm from 'remark-gfm'; | |
| import remarkMath from 'remark-math'; | |
| import rehypeHighlight from 'rehype-highlight'; | |
| import { all as lowlightAll } from 'lowlight'; | |
| import remarkRehype from 'remark-rehype'; | |
| import rehypeKatex from 'rehype-katex'; | |
| import rehypeStringify from 'rehype-stringify'; | |
| import type { Root as HastRoot, RootContent as HastRootContent } from 'hast'; | |
| import type { Root as MdastRoot } from 'mdast'; | |
| import { browser } from '$app/environment'; | |
| import { onDestroy, tick } from 'svelte'; | |
| import { SvelteMap } from 'svelte/reactivity'; | |
| import { rehypeRestoreTableHtml } from './plugins/rehype/table-html-restorer'; | |
| import { rehypeEnhanceLinks } from './plugins/rehype/enhance-links'; | |
| import { rehypeEnhanceCodeBlocks } from './plugins/rehype/enhance-code-blocks'; | |
| import { rehypeEnhanceMermaidBlocks } from './plugins/rehype/enhance-mermaid-blocks'; | |
| import { rehypeMermaidPre } from './plugins/rehype/mermaid-pre'; | |
| import { rehypeSvgPre } from './plugins/rehype/svg-pre'; | |
| import { rehypeEnhanceSvgBlocks } from './plugins/rehype/enhance-svg-blocks'; | |
| import { rehypeResolveAttachmentImages } from './plugins/rehype/resolve-attachment-images'; | |
| import { rehypeRtlSupport } from './plugins/rehype/rehype-rtl-support'; | |
| import { remarkLiteralHtml } from './plugins/remark/literal-html'; | |
| import { | |
| getHastNodeId, | |
| getMdastNodeHash, | |
| isAppendMode, | |
| getCodeInfoFromTarget | |
| } from './markdown-utils'; | |
| import { | |
| preprocessLaTeX, | |
| getImageErrorFallbackHtml, | |
| copyCodeToClipboard, | |
| copyToClipboard | |
| } from '$lib/utils'; | |
| import { | |
| IMAGE_NOT_ERROR_BOUND_SELECTOR, | |
| DATA_ERROR_BOUND_ATTR, | |
| DATA_ERROR_HANDLED_ATTR, | |
| BOOL_TRUE_STRING, | |
| SETTINGS_KEYS, | |
| CODE_BLOCK_HEADER_CLASS, | |
| MERMAID_WRAPPER_CLASS, | |
| MERMAID_BLOCK_CLASS, | |
| MERMAID_LANGUAGE, | |
| MERMAID_SYNTAX_ATTR, | |
| MERMAID_RENDERED_ATTR, | |
| SVG_WRAPPER_CLASS, | |
| SVG_BLOCK_CLASS, | |
| SVG_LANGUAGE, | |
| XML_LANGUAGE, | |
| SVG_TAG_PREFIX, | |
| SVG_SOURCE_ATTR, | |
| SVG_RENDERED_ATTR, | |
| SVG_INLINE_SHADOW_STYLE, | |
| TOGGLE_SOURCE_BTN_CLASS, | |
| DIAGRAM_VIEW_MODE_ATTR, | |
| DIAGRAM_VIEW_RENDERED, | |
| DIAGRAM_VIEW_SOURCE | |
| } from '$lib/constants'; | |
| import { ColorMode, UrlProtocol } from '$lib/enums'; | |
| import { FileTypeText } from '$lib/enums/files.enums'; | |
| import { highlightCode, detectIncompleteCodeBlock, type IncompleteCodeBlock } from '$lib/utils'; | |
| import { sanitizeSvg } from '$lib/utils/sanitize-svg'; | |
| import { mountSvgShadow } from '$lib/utils/svg-shadow'; | |
| import '$styles/katex-custom.scss'; | |
| import githubDarkCss from 'highlight.js/styles/github-dark.css?inline'; | |
| import githubLightCss from 'highlight.js/styles/github.css?inline'; | |
| import { mode } from 'mode-watcher'; | |
| import { | |
| CodeBlockActions, | |
| DialogCodePreview, | |
| DialogMermaidPreview, | |
| ActionIconCopyToClipboard | |
| } from '$lib/components/app'; | |
| import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte'; | |
| import type { DatabaseMessageExtra } from '$lib/types/database'; | |
| import { config } from '$lib/stores/settings.svelte'; | |
| import { fadeInView } from '$lib/actions/fade-in-view.svelte'; | |
| interface Props { | |
| attachments?: DatabaseMessageExtra[]; | |
| content: string; | |
| class?: string; | |
| disableMath?: boolean; | |
| } | |
| interface MarkdownBlock { | |
| id: string; | |
| html: string; | |
| contentHash?: string; | |
| } | |
| let { content, attachments, class: className = '', disableMath = false }: Props = $props(); | |
| let containerRef = $state<HTMLDivElement>(); | |
| let renderedBlocks = $state<MarkdownBlock[]>([]); | |
| let unstableBlockHtml = $state(''); | |
| let incompleteCodeBlock = $state<IncompleteCodeBlock | null>(null); | |
| const streamingSvgCode = $derived.by(() => { | |
| const block = incompleteCodeBlock; | |
| if (!block) return null; | |
| if (block.language === SVG_LANGUAGE) return block.code; | |
| if (block.language === XML_LANGUAGE && block.code.trimStart().startsWith(SVG_TAG_PREFIX)) | |
| return block.code; | |
| return null; | |
| }); | |
| const liveSvgHtml = $derived(streamingSvgCode !== null ? sanitizeSvg(streamingSvgCode) : ''); | |
| let previewDialogOpen = $state(false); | |
| let previewCode = $state(''); | |
| let previewLanguage = $state('text'); | |
| let mermaidPreviewOpen = $state(false); | |
| let mermaidPreviewSvgHtml = $state(''); | |
| let svgPreviewLive = $state(false); | |
| let streamingSvgHost = $state<HTMLDivElement | null>(null); | |
| // While the zoom dialog is open on a streaming svg, mirror the live render into it | |
| $effect(() => { | |
| if (svgPreviewLive && liveSvgHtml) mermaidPreviewSvgHtml = liveSvgHtml; | |
| }); | |
| // Mount the streaming svg into its shadow host on every chunk so it renders live | |
| $effect(() => { | |
| if (streamingSvgHost) mountSvgShadow(streamingSvgHost, liveSvgHtml, SVG_INLINE_SHADOW_STYLE); | |
| }); | |
| let streamingCodeScrollContainer = $state<HTMLDivElement>(); | |
| // Auto-scroll controller for streaming code block content | |
| const streamingAutoScroll = createAutoScrollController(); | |
| let pendingMarkdown: string | null = null; | |
| let isProcessing = false; | |
| // Per-instance transform cache, avoids re-transforming stable blocks during streaming | |
| // Garbage collected when component is destroyed (on conversation change) | |
| const transformCache = new SvelteMap<string, string>(); | |
| let previousContent = ''; | |
| const themeStyleId = `highlight-theme-${(window.idxThemeStyle = (window.idxThemeStyle ?? 0) + 1)}`; | |
| let processor = $derived(() => { | |
| void attachments; | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| let proc: any = remark().use(remarkGfm); // GitHub Flavored Markdown | |
| if (!disableMath) { | |
| proc = proc.use(remarkMath); // Parse $inline$ and $$block$$ math | |
| } | |
| proc = proc | |
| .use(remarkBreaks) // Convert line breaks to <br> | |
| .use(remarkLiteralHtml) // Treat raw HTML as literal text with preserved indentation | |
| .use(remarkRehype); // Convert Markdown AST to rehype | |
| if (!disableMath) { | |
| proc = proc.use(rehypeKatex); // Render math using KaTeX | |
| } | |
| return proc | |
| .use(rehypeHighlight, { | |
| languages: lowlightAll, | |
| aliases: { [FileTypeText.XML]: [FileTypeText.SVELTE, FileTypeText.VUE] } | |
| }) // Add syntax highlighting | |
| .use(rehypeRestoreTableHtml) // Restore limited HTML (e.g., <br>, <ul>) inside Markdown tables | |
| .use(rehypeEnhanceLinks) // Add target="_blank" to links | |
| .use(rehypeMermaidPre) // Convert mermaid blocks to <pre class="mermaid"> | |
| .use(rehypeSvgPre) // Convert svg blocks to <pre class="svg-block"> | |
| .use(rehypeEnhanceCodeBlocks) // Wrap code blocks with header and actions | |
| .use(rehypeEnhanceMermaidBlocks) // Wrap mermaid blocks with header and actions | |
| .use(rehypeEnhanceSvgBlocks) // Wrap svg blocks with header and actions | |
| .use(rehypeResolveAttachmentImages, { attachments }) | |
| .use(rehypeRtlSupport) // Add bidirectional text support | |
| .use(rehypeStringify, { allowDangerousHtml: true }); // Convert to HTML string | |
| }); | |
| /** | |
| * Removes click event listeners from copy and preview buttons. | |
| * Called on component destroy. | |
| */ | |
| function cleanupEventListeners() { | |
| if (!containerRef) return; | |
| const copyButtons = containerRef.querySelectorAll<HTMLButtonElement>('.copy-code-btn'); | |
| const previewButtons = containerRef.querySelectorAll<HTMLButtonElement>('.preview-code-btn'); | |
| for (const button of copyButtons) { | |
| button.removeEventListener('click', handleCopyClick); | |
| } | |
| for (const button of previewButtons) { | |
| button.removeEventListener('click', handlePreviewClick); | |
| } | |
| } | |
| /** | |
| * Removes this component's highlight.js theme style from the document head. | |
| * Called on component destroy to clean up injected styles. | |
| */ | |
| function cleanupHighlightTheme() { | |
| if (!browser) return; | |
| const existingTheme = document.getElementById(themeStyleId); | |
| existingTheme?.remove(); | |
| } | |
| /** | |
| * Loads the appropriate highlight.js theme based on dark/light mode. | |
| * Injects a scoped style element into the document head. | |
| * @param isDark - Whether to load the dark theme (true) or light theme (false) | |
| */ | |
| function loadHighlightTheme(isDark: boolean) { | |
| if (!browser) return; | |
| const existingTheme = document.getElementById(themeStyleId); | |
| existingTheme?.remove(); | |
| const style = document.createElement('style'); | |
| style.id = themeStyleId; | |
| style.textContent = isDark ? githubDarkCss : githubLightCss; | |
| document.head.appendChild(style); | |
| } | |
| /** | |
| * Transforms a single MDAST node to HTML string with caching. | |
| /** | |
| * Transforms a single MDAST node to HTML string with caching. | |
| * Runs the full remark/rehype plugin pipeline (GFM, math, syntax highlighting, etc.) | |
| * on an isolated single-node tree, then stringifies the resulting HAST to HTML. | |
| * Results are cached by node position hash for streaming performance. | |
| * @param processorInstance - The remark/rehype processor instance | |
| * @param node - The MDAST node to transform | |
| * @param index - Node index for hash fallback | |
| * @returns Object containing the HTML string and cache hash | |
| */ | |
| async function transformMdastNode( | |
| processorInstance: ReturnType<typeof processor>, | |
| node: unknown, | |
| index: number | |
| ): Promise<{ html: string; hash: string }> { | |
| const hash = getMdastNodeHash(node, index); | |
| const cached = transformCache.get(hash); | |
| if (cached) { | |
| return { html: cached, hash }; | |
| } | |
| const singleNodeRoot = { type: 'root', children: [node] }; | |
| const transformedRoot = (await processorInstance.run(singleNodeRoot as MdastRoot)) as HastRoot; | |
| const html = processorInstance.stringify(transformedRoot); | |
| transformCache.set(hash, html); | |
| return { html, hash }; | |
| } | |
| /** | |
| * Handles click events on copy buttons within code blocks. | |
| * Copies the raw code content to the clipboard. | |
| * @param event - The click event from the copy button | |
| */ | |
| async function handleCopyClick(event: Event) { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| const target = event.currentTarget as HTMLButtonElement | null; | |
| if (!target) { | |
| return; | |
| } | |
| const info = getCodeInfoFromTarget(target); | |
| if (!info) { | |
| return; | |
| } | |
| try { | |
| await copyCodeToClipboard(info.rawCode); | |
| } catch (error) { | |
| console.error('Failed to copy code:', error); | |
| } | |
| } | |
| /** | |
| * Handles preview dialog open state changes. | |
| * Clears preview content when dialog is closed. | |
| * @param open - Whether the dialog is being opened or closed | |
| */ | |
| function handlePreviewDialogOpenChange(open: boolean) { | |
| previewDialogOpen = open; | |
| if (!open) { | |
| previewCode = ''; | |
| previewLanguage = 'text'; | |
| } | |
| } | |
| /** | |
| * Handles click events on preview buttons within HTML code blocks. | |
| * Opens a preview dialog with the rendered HTML content. | |
| * @param event - The click event from the preview button | |
| */ | |
| function handlePreviewClick(event: Event) { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| const target = event.currentTarget as HTMLButtonElement | null; | |
| if (!target) { | |
| return; | |
| } | |
| const info = getCodeInfoFromTarget(target); | |
| if (!info) { | |
| return; | |
| } | |
| previewCode = info.rawCode; | |
| previewLanguage = info.language; | |
| previewDialogOpen = true; | |
| } | |
| /** | |
| * Processes markdown content into stable and unstable HTML blocks. | |
| * Uses incremental rendering: stable blocks are cached, unstable block is re-rendered. | |
| * Incomplete code blocks are rendered using SyntaxHighlightedCode to maintain interactivity. | |
| * @param markdown - The raw markdown string to process | |
| */ | |
| async function processMarkdown(markdown: string) { | |
| // Early exit if content unchanged (can happen with rapid coalescing) | |
| if (markdown === previousContent) { | |
| return; | |
| } | |
| if (!markdown) { | |
| renderedBlocks = []; | |
| unstableBlockHtml = ''; | |
| incompleteCodeBlock = null; | |
| previousContent = ''; | |
| return; | |
| } | |
| // Check for incomplete code block at the end of content | |
| const incompleteBlock = detectIncompleteCodeBlock(markdown); | |
| if (incompleteBlock) { | |
| // Process only the prefix (content before the incomplete code block) | |
| const prefixMarkdown = markdown.slice(0, incompleteBlock.openingIndex); | |
| if (prefixMarkdown.trim()) { | |
| const normalizedPrefix = preprocessLaTeX(prefixMarkdown); | |
| const processorInstance = processor(); | |
| const ast = processorInstance.parse(normalizedPrefix) as MdastRoot; | |
| const mdastChildren = (ast as { children?: unknown[] }).children ?? []; | |
| const nextBlocks: MarkdownBlock[] = []; | |
| // Check if we're in append mode for cache reuse | |
| const appendMode = isAppendMode(prefixMarkdown, previousContent); | |
| const previousBlockCount = appendMode ? renderedBlocks.length : 0; | |
| // All prefix blocks are now stable since code block is separate | |
| for (let index = 0; index < mdastChildren.length; index++) { | |
| const child = mdastChildren[index]; | |
| // In append mode, reuse previous blocks if unchanged | |
| if (appendMode && index < previousBlockCount) { | |
| const prevBlock = renderedBlocks[index]; | |
| const currentHash = getMdastNodeHash(child, index); | |
| if (prevBlock?.contentHash === currentHash) { | |
| nextBlocks.push(prevBlock); | |
| continue; | |
| } | |
| } | |
| // Transform this block (with caching) | |
| const { html, hash } = await transformMdastNode(processorInstance, child, index); | |
| const id = getHastNodeId( | |
| { position: (child as { position?: unknown }).position } as HastRootContent, | |
| index | |
| ); | |
| nextBlocks.push({ id, html, contentHash: hash }); | |
| } | |
| renderedBlocks = nextBlocks; | |
| } else { | |
| renderedBlocks = []; | |
| } | |
| previousContent = prefixMarkdown; | |
| unstableBlockHtml = ''; | |
| incompleteCodeBlock = incompleteBlock; | |
| return; | |
| } | |
| // No incomplete code block - use standard processing | |
| incompleteCodeBlock = null; | |
| const normalized = preprocessLaTeX(markdown); | |
| const processorInstance = processor(); | |
| const ast = processorInstance.parse(normalized) as MdastRoot; | |
| const mdastChildren = (ast as { children?: unknown[] }).children ?? []; | |
| const stableCount = Math.max(mdastChildren.length - 1, 0); | |
| const nextBlocks: MarkdownBlock[] = []; | |
| // Check if we're in append mode for cache reuse | |
| const appendMode = isAppendMode(markdown, previousContent); | |
| const previousBlockCount = appendMode ? renderedBlocks.length : 0; | |
| for (let index = 0; index < stableCount; index++) { | |
| const child = mdastChildren[index]; | |
| // In append mode, reuse previous blocks if unchanged | |
| if (appendMode && index < previousBlockCount) { | |
| const prevBlock = renderedBlocks[index]; | |
| const currentHash = getMdastNodeHash(child, index); | |
| if (prevBlock?.contentHash === currentHash) { | |
| nextBlocks.push(prevBlock); | |
| continue; | |
| } | |
| } | |
| // Transform this block (with caching) | |
| const { html, hash } = await transformMdastNode(processorInstance, child, index); | |
| const id = getHastNodeId( | |
| { position: (child as { position?: unknown }).position } as HastRootContent, | |
| index | |
| ); | |
| nextBlocks.push({ id, html, contentHash: hash }); | |
| } | |
| let unstableHtml = ''; | |
| if (mdastChildren.length > stableCount) { | |
| const unstableChild = mdastChildren[stableCount]; | |
| const singleNodeRoot = { type: 'root', children: [unstableChild] }; | |
| const transformedRoot = (await processorInstance.run( | |
| singleNodeRoot as MdastRoot | |
| )) as HastRoot; | |
| unstableHtml = processorInstance.stringify(transformedRoot); | |
| } | |
| renderedBlocks = nextBlocks; | |
| previousContent = markdown; | |
| await tick(); // Force DOM sync before updating unstable HTML block | |
| unstableBlockHtml = unstableHtml; | |
| } | |
| /** | |
| * Attaches click event listeners to copy and preview buttons in code blocks. | |
| * Uses data-listener-bound attribute to prevent duplicate bindings. | |
| */ | |
| function setupCodeBlockActions() { | |
| if (!containerRef) return; | |
| const wrappers = containerRef.querySelectorAll<HTMLElement>('.code-block-wrapper'); | |
| for (const wrapper of wrappers) { | |
| const copyButton = wrapper.querySelector<HTMLButtonElement>('.copy-code-btn'); | |
| const previewButton = wrapper.querySelector<HTMLButtonElement>('.preview-code-btn'); | |
| if (copyButton && copyButton.dataset.listenerBound !== 'true') { | |
| copyButton.dataset.listenerBound = 'true'; | |
| copyButton.addEventListener('click', handleCopyClick); | |
| } | |
| if (previewButton && previewButton.dataset.listenerBound !== 'true') { | |
| previewButton.dataset.listenerBound = 'true'; | |
| previewButton.addEventListener('click', handlePreviewClick); | |
| } | |
| } | |
| } | |
| /** | |
| * Attaches error handlers to images to show fallback UI when loading fails (e.g., CORS). | |
| * Uses data-error-bound attribute to prevent duplicate bindings. | |
| */ | |
| function setupImageErrorHandlers() { | |
| if (!containerRef) return; | |
| const images = containerRef.querySelectorAll<HTMLImageElement>(IMAGE_NOT_ERROR_BOUND_SELECTOR); | |
| for (const img of images) { | |
| img.dataset[DATA_ERROR_BOUND_ATTR] = BOOL_TRUE_STRING; | |
| img.addEventListener('error', handleImageError); | |
| } | |
| } | |
| /** | |
| * Opens the mermaid diagram in a full-screen preview dialog with zoom/pan support. | |
| * Also handles copy and preview button clicks for mermaid blocks. | |
| * Uses event delegation: a single handler on the container. | |
| */ | |
| async function handleMermaidClick(event: MouseEvent) { | |
| const target = event.target as HTMLElement; | |
| // Toggle a diagram block between its rendered view and its source view. | |
| // Shared by mermaid and svg, css drives the visibility from the wrapper mode. | |
| const toggleBtn = target.closest(`.${TOGGLE_SOURCE_BTN_CLASS}`); | |
| if (toggleBtn) { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| const wrapper = toggleBtn.closest(`.${MERMAID_WRAPPER_CLASS}, .${SVG_WRAPPER_CLASS}`); | |
| if (!wrapper) return; | |
| const isSource = wrapper.getAttribute(DIAGRAM_VIEW_MODE_ATTR) === DIAGRAM_VIEW_SOURCE; | |
| const next = isSource ? DIAGRAM_VIEW_RENDERED : DIAGRAM_VIEW_SOURCE; | |
| wrapper.setAttribute(DIAGRAM_VIEW_MODE_ATTR, next); | |
| toggleBtn.setAttribute('aria-pressed', String(!isSource)); | |
| return; | |
| } | |
| // Check if clicking on copy or preview button in mermaid block | |
| const copyBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .copy-code-btn`); | |
| const previewBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .preview-code-btn`); | |
| if (copyBtn || previewBtn) { | |
| const wrapper = target.closest(`.${MERMAID_WRAPPER_CLASS}`); | |
| if (!wrapper) return; | |
| const preElement = wrapper.querySelector<HTMLElement>( | |
| `pre.${MERMAID_BLOCK_CLASS}[${MERMAID_SYNTAX_ATTR}]` | |
| ); | |
| if (!preElement) return; | |
| const mermaidSyntax = preElement.getAttribute(MERMAID_SYNTAX_ATTR) ?? ''; | |
| if (copyBtn) { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| try { | |
| await copyToClipboard(mermaidSyntax); | |
| } catch (error) { | |
| console.error('Failed to copy mermaid syntax:', error); | |
| } | |
| return; | |
| } | |
| if (previewBtn) { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| const svg = preElement.querySelector('svg'); | |
| if (!svg) return; | |
| mermaidPreviewSvgHtml = svg.outerHTML; | |
| svgPreviewLive = false; | |
| mermaidPreviewOpen = true; | |
| return; | |
| } | |
| } | |
| // Check if clicking on copy or preview button in svg block | |
| const svgCopyBtn = target.closest(`.${SVG_WRAPPER_CLASS} .copy-code-btn`); | |
| const svgPreviewBtn = target.closest(`.${SVG_WRAPPER_CLASS} .preview-code-btn`); | |
| if (svgCopyBtn || svgPreviewBtn) { | |
| const wrapper = target.closest(`.${SVG_WRAPPER_CLASS}`); | |
| if (!wrapper) return; | |
| const preElement = wrapper.querySelector<HTMLElement>( | |
| `pre.${SVG_BLOCK_CLASS}[${SVG_SOURCE_ATTR}]` | |
| ); | |
| if (!preElement) return; | |
| if (svgCopyBtn) { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| try { | |
| await copyToClipboard(preElement.getAttribute(SVG_SOURCE_ATTR) ?? ''); | |
| } catch (error) { | |
| console.error('Failed to copy svg source:', error); | |
| } | |
| return; | |
| } | |
| if (svgPreviewBtn) { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| mermaidPreviewSvgHtml = sanitizeSvg(preElement.getAttribute(SVG_SOURCE_ATTR) ?? ''); | |
| svgPreviewLive = false; | |
| mermaidPreviewOpen = true; | |
| return; | |
| } | |
| } | |
| // A click on the header chrome targets the action buttons, never the | |
| // diagram. Guard so a header click can not fall through to the click to | |
| // zoom branches below, whatever the scroll position or stacking. | |
| if (target.closest(`.${CODE_BLOCK_HEADER_CLASS}`)) return; | |
| // Open preview when clicking the svg block itself. A final block carries its | |
| // source, a streaming block does not and is mirrored live into the dialog. | |
| const svgEl = target.closest(`.${SVG_BLOCK_CLASS}`); | |
| if (svgEl) { | |
| const source = svgEl.getAttribute(SVG_SOURCE_ATTR); | |
| if (source !== null) { | |
| mermaidPreviewSvgHtml = sanitizeSvg(source); | |
| svgPreviewLive = false; | |
| } else { | |
| svgPreviewLive = true; | |
| } | |
| mermaidPreviewOpen = true; | |
| return; | |
| } | |
| // Otherwise, open preview when clicking on the mermaid diagram itself | |
| const mermaidEl = target.closest(`.${MERMAID_BLOCK_CLASS}`); | |
| if (!mermaidEl) return; | |
| const svg = mermaidEl.querySelector('svg'); | |
| if (!svg) return; | |
| mermaidPreviewSvgHtml = svg.outerHTML; | |
| svgPreviewLive = false; | |
| mermaidPreviewOpen = true; | |
| } | |
| /** | |
| * Handles mermaid preview dialog open state changes. | |
| * Cleans up SVG content when dialog is closed. | |
| */ | |
| function handleMermaidPreviewOpenChange(open: boolean) { | |
| mermaidPreviewOpen = open; | |
| if (!open) { | |
| mermaidPreviewSvgHtml = ''; | |
| svgPreviewLive = false; | |
| } | |
| } | |
| /** | |
| * Renders mermaid diagrams that haven't been rendered yet. | |
| * Called after each markdown content update. | |
| * Marks nodes immediately to prevent duplicate renders during streaming. | |
| * Reads mode.current before await to ensure reactive tracking. | |
| */ | |
| async function renderMermaidDiagrams() { | |
| if (!containerRef) return; | |
| const nodes = containerRef.querySelectorAll( | |
| `pre.${MERMAID_BLOCK_CLASS}:not([${MERMAID_RENDERED_ATTR}])` | |
| ); | |
| if (nodes.length === 0) return; | |
| // Mark nodes immediately to prevent duplicate renders if called again during streaming. | |
| // This avoids needing a guard that would block node discovery. | |
| nodes.forEach((node) => node.setAttribute(MERMAID_RENDERED_ATTR, 'true')); | |
| // Read mode before await so Svelte tracks it reactively. | |
| const isDark = mode.current === ColorMode.DARK; | |
| // lazy load the mermaid dependecy only when needed to reduce bundle size. | |
| const { default: mermaid } = await import('mermaid'); | |
| mermaid.initialize({ | |
| startOnLoad: false, | |
| theme: isDark ? 'dark' : 'default', | |
| securityLevel: 'strict', | |
| flowchart: { | |
| useMaxWidth: false, | |
| htmlLabels: true | |
| }, | |
| sequence: { | |
| useMaxWidth: false | |
| }, | |
| gantt: { | |
| useMaxWidth: false | |
| } | |
| }); | |
| try { | |
| await mermaid.run({ | |
| nodes: Array.from(nodes) as unknown as NodeListOf<HTMLElement> | |
| }); | |
| } catch (error) { | |
| console.error('Failed to render mermaid diagram:', error); | |
| } | |
| } | |
| /** | |
| * Renders svg diagrams that haven't been rendered yet. | |
| * Sanitizes the source before injecting and marks each node so it renders once. | |
| * An empty sanitize result keeps the raw source as escaped text. | |
| */ | |
| function renderSvgDiagrams() { | |
| if (!containerRef) return; | |
| const nodes = containerRef.querySelectorAll<HTMLElement>( | |
| `pre.${SVG_BLOCK_CLASS}:not([${SVG_RENDERED_ATTR}])` | |
| ); | |
| if (nodes.length === 0) return; | |
| nodes.forEach((node) => { | |
| node.setAttribute(SVG_RENDERED_ATTR, 'true'); | |
| const source = node.getAttribute(SVG_SOURCE_ATTR) ?? node.textContent ?? ''; | |
| const clean = sanitizeSvg(source); | |
| if (clean) { | |
| node.textContent = ''; | |
| const host = document.createElement('div'); | |
| node.appendChild(host); | |
| mountSvgShadow(host, clean, SVG_INLINE_SHADOW_STYLE); | |
| } | |
| }); | |
| } | |
| /** | |
| * Handles image load errors by replacing the image with a fallback UI. | |
| * Shows a placeholder with a link to open the image in a new tab. | |
| */ | |
| function handleImageError(event: Event) { | |
| const img = event.target as HTMLImageElement; | |
| if (!img || !img.src) return; | |
| // Don't handle data URLs or already-handled images | |
| if ( | |
| img.src.startsWith(UrlProtocol.DATA) || | |
| img.dataset[DATA_ERROR_HANDLED_ATTR] === BOOL_TRUE_STRING | |
| ) | |
| return; | |
| img.dataset[DATA_ERROR_HANDLED_ATTR] = BOOL_TRUE_STRING; | |
| const src = img.src; | |
| // Create fallback element | |
| const fallback = document.createElement('div'); | |
| fallback.className = 'image-load-error'; | |
| fallback.innerHTML = getImageErrorFallbackHtml(src); | |
| // Replace image with fallback | |
| img.parentNode?.replaceChild(fallback, img); | |
| } | |
| /** | |
| * Queues markdown for processing with coalescing support. | |
| * Only processes the latest markdown when multiple updates arrive quickly. | |
| * Uses requestAnimationFrame to yield to browser paint between batches. | |
| * @param markdown - The markdown content to render | |
| */ | |
| async function updateRenderedBlocks(markdown: string) { | |
| pendingMarkdown = markdown; | |
| if (isProcessing) { | |
| return; | |
| } | |
| isProcessing = true; | |
| try { | |
| while (pendingMarkdown !== null) { | |
| const nextMarkdown = pendingMarkdown; | |
| pendingMarkdown = null; | |
| await processMarkdown(nextMarkdown); | |
| // Yield to browser for paint. During this, new chunks coalesce | |
| // into pendingMarkdown, so we always render the latest state. | |
| if (pendingMarkdown !== null) { | |
| await new Promise((resolve) => requestAnimationFrame(resolve)); | |
| } | |
| } | |
| } catch (error) { | |
| console.error('Failed to process markdown:', error); | |
| renderedBlocks = []; | |
| unstableBlockHtml = markdown.replace(/\n/g, '<br>'); | |
| } finally { | |
| isProcessing = false; | |
| } | |
| } | |
| $effect(() => { | |
| const currentMode = mode.current; | |
| const isDark = currentMode === ColorMode.DARK; | |
| loadHighlightTheme(isDark); | |
| }); | |
| $effect(() => { | |
| updateRenderedBlocks(content); | |
| }); | |
| $effect(() => { | |
| const hasRenderedBlocks = renderedBlocks.length > 0; | |
| const hasUnstableBlock = Boolean(unstableBlockHtml); | |
| if ((hasRenderedBlocks || hasUnstableBlock) && containerRef) { | |
| setupCodeBlockActions(); | |
| setupImageErrorHandlers(); | |
| renderMermaidDiagrams(); | |
| renderSvgDiagrams(); | |
| } | |
| }); | |
| // Auto-scroll for streaming code block | |
| $effect(() => { | |
| streamingAutoScroll.setContainer(streamingCodeScrollContainer); | |
| }); | |
| $effect(() => { | |
| streamingAutoScroll.updateInterval(incompleteCodeBlock !== null); | |
| }); | |
| onDestroy(() => { | |
| cleanupEventListeners(); | |
| cleanupHighlightTheme(); | |
| streamingAutoScroll.destroy(); | |
| }); | |
| </script> | |
| <!-- svelte-ignore a11y_click_events_have_key_events --> | |
| <!-- svelte-ignore a11y_no_static_element_interactions --> | |
| <div | |
| bind:this={containerRef} | |
| onclick={handleMermaidClick} | |
| class="markdown-content {className}{config()[SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS] | |
| ? ' full-height-code-blocks' | |
| : ''}" | |
| > | |
| {#each renderedBlocks as block (block.id)} | |
| <div class="markdown-block" data-block-id={block.id} use:fadeInView={{ skipIfVisible: true }}> | |
| {@html block.html} | |
| </div> | |
| {/each} | |
| {#if unstableBlockHtml} | |
| <div class="markdown-block markdown-block--unstable" data-block-id="unstable"> | |
| <!-- eslint-disable-next-line no-at-html-tags --> | |
| {@html unstableBlockHtml} | |
| </div> | |
| {/if} | |
| {#if incompleteCodeBlock} | |
| {#if incompleteCodeBlock.language === MERMAID_LANGUAGE} | |
| <div class="mermaid-block-wrapper streaming-mermaid-block"> | |
| <div class="code-block-header"> | |
| <span class="code-language">mermaid</span> | |
| <div class="code-block-actions"> | |
| <ActionIconCopyToClipboard | |
| text={incompleteCodeBlock.code} | |
| canCopy={false} | |
| ariaLabel="Diagram incomplete" | |
| /> | |
| </div> | |
| </div> | |
| <div class="mermaid-loading-placeholder"> | |
| <span class="mermaid-loading-text">Generating diagram...</span> | |
| </div> | |
| </div> | |
| {:else if streamingSvgCode !== null} | |
| <div class="svg-block-wrapper streaming-svg-block"> | |
| <div class="code-block-header"> | |
| <span class="code-language">svg</span> | |
| <div class="code-block-actions"> | |
| <ActionIconCopyToClipboard | |
| text={incompleteCodeBlock.code} | |
| canCopy={false} | |
| ariaLabel="Diagram incomplete" | |
| /> | |
| </div> | |
| </div> | |
| {#if liveSvgHtml} | |
| <div class="svg-scroll-container"> | |
| <div class={SVG_BLOCK_CLASS}> | |
| <div bind:this={streamingSvgHost}></div> | |
| </div> | |
| </div> | |
| {:else} | |
| <div class="mermaid-loading-placeholder"> | |
| <span class="mermaid-loading-text">Rendering svg...</span> | |
| </div> | |
| {/if} | |
| </div> | |
| {:else} | |
| <div class="code-block-wrapper streaming-code-block relative"> | |
| <div class="code-block-header"> | |
| <span class="code-language">{incompleteCodeBlock.language || 'text'}</span> | |
| <CodeBlockActions | |
| code={incompleteCodeBlock.code} | |
| language={incompleteCodeBlock.language || 'text'} | |
| disabled | |
| onPreview={(code, lang) => { | |
| previewCode = code; | |
| previewLanguage = lang; | |
| previewDialogOpen = true; | |
| }} | |
| /> | |
| </div> | |
| <div | |
| bind:this={streamingCodeScrollContainer} | |
| class="streaming-code-scroll-container" | |
| onscroll={() => streamingAutoScroll.handleScroll()} | |
| > | |
| <pre class="streaming-code-pre"><code | |
| class="hljs language-{incompleteCodeBlock.language || 'text'}" | |
| >{@html highlightCode( | |
| incompleteCodeBlock.code, | |
| incompleteCodeBlock.language || 'text' | |
| )}</code | |
| ></pre> | |
| </div> | |
| </div> | |
| {/if} | |
| {/if} | |
| </div> | |
| <DialogCodePreview | |
| open={previewDialogOpen} | |
| code={previewCode} | |
| language={previewLanguage} | |
| onOpenChange={handlePreviewDialogOpenChange} | |
| /> | |
| <DialogMermaidPreview | |
| open={mermaidPreviewOpen} | |
| svgHtml={mermaidPreviewSvgHtml} | |
| onOpenChange={handleMermaidPreviewOpenChange} | |
| /> | |
| <style> | |
| @import './markdown-content.css'; | |
| </style> | |