@aparte/plugin-artifacts 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/binary-file.d.ts +7 -0
- package/dist/binary-file.d.ts.map +1 -0
- package/dist/card.d.ts +6 -0
- package/dist/card.d.ts.map +1 -0
- package/dist/custom-elements.json +1184 -0
- package/dist/highlight.d.ts +17 -0
- package/dist/highlight.d.ts.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +675 -0
- package/dist/index.js.map +1 -0
- package/dist/index.node.d.ts +32 -0
- package/dist/index.node.d.ts.map +1 -0
- package/dist/index.node.js +30 -0
- package/dist/index.node.js.map +1 -0
- package/dist/kinds.d.ts +19 -0
- package/dist/kinds.d.ts.map +1 -0
- package/dist/options.d.ts +72 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/preview-document-xP-Ne-F0.js +177 -0
- package/dist/preview-document-xP-Ne-F0.js.map +1 -0
- package/dist/preview-document.d.ts +27 -0
- package/dist/preview-document.d.ts.map +1 -0
- package/dist/segment.d.ts +66 -0
- package/dist/segment.d.ts.map +1 -0
- package/dist/shared.d.ts +29 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/tool.d.ts +31 -0
- package/dist/tool.d.ts.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/shared.ts","../src/highlight.ts","../src/options.ts","../src/binary-file.ts","../src/artifact.css?raw","../src/card.ts","../src/index.ts"],"sourcesContent":["/**\n * The leaves the artifact card and the binary-file path BOTH use.\n *\n * Scoped by a call-site census, not by intuition. The obvious-looking candidates are\n * not here: `BINARY_FILE_KINDS` reads as shared and is used only by the card (which\n * consults it to delegate), `PREVIEWABLE_KINDS`, `languageForKind` and\n * `downloadTextArtifact` are card-only, and `FILE_ICON_LABEL` and `formatBytes` are\n * binary-only. A first draft of this module took all of them and would have been a\n * grab-bag named for one of its three jobs.\n *\n * What is genuinely shared is small: fence stripping (which the plain code renderer\n * needs too), the kind→label map, and the throttle pair behind the debounced\n * re-highlight.\n */\n\n\n/**\n * Strip leading/trailing markdown code fences (``` or ~~~, optional lang tag).\n * Also strips any content that appears after the closing fence (small models\n * frequently duplicate lines after the closing ``` block).\n *\n * Char-based scanner — no regex.\n */\nexport function stripCodeFences(content: string): string {\n let s = content;\n\n // Opening fence: leading ``` or ~~~ (3+) optionally followed by language tag,\n // then a newline. Walk the start of the string only.\n if (s.startsWith('```') || s.startsWith('~~~')) {\n const fenceChar = s[0];\n let i = 0;\n while (i < s.length && s[i] === fenceChar) i++;\n // Skip language tag chars (anything until newline)\n while (i < s.length && s[i] !== '\\n') i++;\n // Skip the newline itself if present\n if (i < s.length && s[i] === '\\n') i++;\n s = s.slice(i);\n }\n\n // Closing fence: scan forward to find a line that is exclusively `````/`~~~`+\n // optionally followed by trailing whitespace; cut there + everything after.\n const closeAt = findClosingFence(s);\n if (closeAt !== -1) s = s.slice(0, closeAt);\n\n return s.trim();\n}\n\n/** Find the byte offset where a closing fence line begins, or -1 if none. */\nfunction findClosingFence(s: string): number {\n let i = 0;\n while (i < s.length) {\n // Find start of next line\n const lineStart = i;\n // Skip leading whitespace on this line (indentation)\n let k = lineStart;\n while (k < s.length && (s[k] === ' ' || s[k] === '\\t')) k++;\n if (k < s.length && (s[k] === '`' || s[k] === '~')) {\n const fenceChar = s[k];\n let runs = 0;\n while (k < s.length && s[k] === fenceChar) { runs++; k++; }\n if (runs >= 3) {\n // The rest of the line should be whitespace only — otherwise\n // it's not a closing fence (e.g. inline backtick text).\n let onlyWs = true;\n while (k < s.length && s[k] !== '\\n') {\n if (s[k] !== ' ' && s[k] !== '\\t' && s[k] !== '\\r') { onlyWs = false; break; }\n k++;\n }\n if (onlyWs) {\n // Cut at start of this line; if a `\\n` precedes, drop it too\n let cut = lineStart;\n if (cut > 0 && s[cut - 1] === '\\n') cut--;\n return cut;\n }\n }\n }\n // Advance to next line\n while (i < s.length && s[i] !== '\\n') i++;\n if (i < s.length) i++;\n }\n return -1;\n}\n\nexport function labelForKind(kind: string): string {\n switch (kind) {\n case 'react': return 'React component';\n case 'html': return 'HTML document';\n case 'svg': return 'SVG image';\n case 'js': return 'JavaScript snippet';\n case 'css': return 'CSS stylesheet';\n case 'json': return 'JSON document';\n case 'markdown': return 'Markdown document';\n case 'csv': return 'CSV table';\n case 'text': return 'Text file';\n case 'python': return 'Python script';\n case 'typescript': return 'TypeScript file';\n case 'bash': return 'Bash script';\n case 'sql': return 'SQL query';\n case 'pdf': return 'PDF generator';\n case 'xlsx': return 'Excel generator';\n case 'docx': return 'Word generator';\n default: return 'Artifact';\n }\n}\n\n/** Cap a segmentId→timestamp throttle map so a long session can't grow it without\n * bound (one entry per streamed segment). Values are timestamps (tiny), so the cap\n * is generous; delete-then-set refreshes recency, evict the oldest key when full.\n * Evicting a stale entry costs at most one extra highlight/dispatch — never\n * incorrect, since both debounce windows are far shorter than the cap horizon. */\n\n","/**\n * Progressive syntax highlighting for a code pane that is STILL STREAMING.\n *\n * The previous shape — `codeEl.textContent = content` on every token, plus a\n * debounced full re-highlight — made the pane flicker between plain and coloured:\n * assigning `textContent` destroys the highlighter's `<span>`s, so each token\n * erased the colours the last debounce had painted, and the reader saw plain text\n * most of the time with a coloured frame every 400ms. The debounce was not the\n * problem; rewriting the whole block on every token was.\n *\n * So the pane is split at the last newline:\n *\n * <pre><code> …highlighted COMPLETE lines… <span data-aparte-tail>partial line</span>\n *\n * A token only rewrites the tail, which costs one text assignment and leaves the\n * coloured prefix untouched. The prefix advances at most once per\n * `HIGHLIGHT_DEBOUNCE_MS`, and only when a new line has actually completed —\n * highlighting a half-written line is also what made the colours wrong as well as\n * flickering, because an unterminated string or brace re-tokenises everything\n * after it.\n *\n * `data-aparte-hl-len` on the pane is the boundary, and the DOM is deliberately\n * the source of truth for it rather than a module-level map: it makes the value\n * monotonic across a slow highlight that resolves out of order, and it cannot go\n * stale when the pane is rebuilt underneath us.\n */\n// Copied from core's `renderers/highlight-stream.ts` (the only other reader is the\n// code segment's own renderer): two readers is not yet a layer to export.\nimport { contextConfig, escapeHtml } from '@aparte/core';\n\n/** One highlight per pane per this window. Shiki costs 50-100ms per call. */\nconst HIGHLIGHT_DEBOUNCE_MS = 400;\n/** Bound on the throttle bookkeeping, so a long session cannot grow it forever. */\nconst MAX_THROTTLE_ENTRIES = 256;\n\nconst _lastHighlightAt = new Map<string, number>();\n\n/**\n * Record `id`'s last-seen time in a bounded, insertion-ordered map.\n *\n * Shared with the artifact's event-dispatch throttle, which is why it is generic\n * over the map rather than closing over one.\n */\nexport function markThrottle(map: Map<string, number>, id: string, at: number): void {\n map.delete(id);\n if (map.size >= MAX_THROTTLE_ENTRIES) {\n const oldest = map.keys().next().value;\n if (oldest !== undefined) map.delete(oldest);\n }\n map.set(id, at);\n}\n\n/** Where the coloured prefix ends. Absent or unparseable means \"nothing coloured\". */\nfunction highlightedLen(pane: HTMLElement): number {\n const n = Number(pane.dataset.aparteHlLen);\n return Number.isFinite(n) && n > 0 ? n : 0;\n}\n\n/**\n * Paint `content` into `paneSelector` while it streams.\n *\n * Call it on every token: the cheap half runs every time, the highlight is\n * throttled internally. The caller still runs one final, whole-content highlight\n * when the segment settles — this function deliberately never colours the last\n * line, so the settle pass is what completes it.\n */\nexport function streamHighlight(\n element: HTMLElement,\n paneSelector: string,\n content: string,\n lang: string,\n segId: string,\n): void {\n const pane = element.querySelector<HTMLElement>(paneSelector);\n if (!pane) return;\n\n const tail = pane.querySelector<HTMLElement>('[data-aparte-tail]');\n const hlLen = highlightedLen(pane);\n\n // ── every token: move the tail, and nothing else ──────────────────────\n if (tail && hlLen <= content.length) {\n tail.textContent = content.slice(hlLen);\n } else {\n // No coloured prefix yet, or the pane was rebuilt under us (a settle\n // highlight replaces the whole thing). Plain text, and forget the\n // boundary — self-healing beats a reset call at three call sites.\n delete pane.dataset.aparteHlLen;\n const codeEl = pane.querySelector('code');\n if (codeEl) codeEl.textContent = content;\n else pane.innerHTML = `<pre><code class=\"language-${escapeHtml(lang || 'text')}\">${escapeHtml(content)}</code></pre>`;\n }\n\n // ── throttled: advance the coloured prefix by whole lines ─────────────\n const cut = content.lastIndexOf('\\n') + 1;\n if (cut <= hlLen) return;\n const now = Date.now();\n if (now - (_lastHighlightAt.get(segId) ?? 0) < HIGHLIGHT_DEBOUNCE_MS) return;\n markThrottle(_lastHighlightAt, segId, now);\n\n // Resolved from the element, not the ambient config: this lands late, and by\n // then the render-time config is gone.\n void contextConfig(element).highlightCode(content.slice(0, cut), lang).then(html => {\n const live = element.querySelector<HTMLElement>(paneSelector);\n if (!live) return;\n // Monotonic. Two highlights can be in flight; the older one carries the\n // SHORTER prefix, and letting it land would visibly rewind the pane.\n if (cut <= highlightedLen(live)) return;\n live.innerHTML = html;\n const codeEl = live.querySelector('code') ?? live;\n const span = document.createElement('span');\n span.dataset.aparteTail = '';\n // The captured tail may already be one token behind; the next token fixes\n // it, and there is always a next one or a settle pass.\n span.textContent = content.slice(cut);\n codeEl.appendChild(span);\n live.dataset.aparteHlLen = String(cut);\n }).catch(() => { /* best-effort: a failed highlight degrades silently */ });\n}\n","/**\n * What the app decided at `setupArtifacts()`, read back by the card at render time.\n *\n * The card is resolved through core's ambient config (`contextConfig()`), not through\n * a closure, so the settings live beside the config they were given for: one entry per\n * `AparteConfig`, the global one by default. A chat with its own config that never\n * called `setupArtifacts` reads the global settings, which is what its renderers do\n * too.\n */\nimport { aparteGlobalConfig, type AparteConfig } from '@aparte/core';\nimport type { ArtifactSegment } from './segment.js';\nimport type { ArtifactToolOptions } from './tool.js';\n\n/** Builds the `srcdoc` of the sandboxed preview frame for a previewable kind. */\nexport type ArtifactPreviewBuilder = (kind: string, body: string, title: string) => string;\n\n/** The bytes a binary artifact (pdf, xlsx, docx) resolved to. */\nexport interface ArtifactBinary {\n /** The file. */\n buffer: BlobPart;\n /** Its MIME type, for the download. */\n mime: string;\n /** The name the download gets. */\n filename: string;\n /**\n * Optional HTML rendering of the file (a spreadsheet as a table, a PDF's text),\n * shown in the card's preview pane after sanitisation. Absent: the pane says so.\n */\n previewHtml?: string | null;\n}\n\n/**\n * Turn a binary artifact's source (the JS the model wrote to produce a workbook, a\n * PDF, a document) into bytes. Core owns no sandbox and no file generator: this is the\n * app's, and without it a binary artifact shows its source with no download and no\n * preview — declared nowhere, offered nowhere (ratified decision #8).\n */\nexport type ArtifactBinaryResolver = (artifact: ArtifactSegment) => Promise<ArtifactBinary>;\n\nexport interface ArtifactRenderOptions {\n /**\n * The Preview tab for previewable kinds (html, react, svg, js, css). `true`\n * (default) mounts a sandboxed frame on a gesture with the built-in document\n * builder; a function replaces the builder; `false` offers no preview at all.\n */\n preview?: boolean | ArtifactPreviewBuilder;\n /** See {@link ArtifactBinaryResolver}. */\n onBinary?: ArtifactBinaryResolver;\n}\n\nconst settings = new WeakMap<AparteConfig, ArtifactRenderOptions>();\n\nexport function setRenderOptions(config: AparteConfig, options: ArtifactRenderOptions): void {\n settings.set(config, options);\n}\n\n/** The options for this config, else the global config's, else the defaults. */\nexport function renderOptions(config: AparteConfig): ArtifactRenderOptions {\n return settings.get(config) ?? settings.get(aparteGlobalConfig) ?? {};\n}\n\n/** For tests and a teardown: forget what a config was told. */\nexport function clearRenderOptions(config: AparteConfig): void {\n settings.delete(config);\n}\n\n/**\n * Everything `setupArtifacts()` accepts — the tool's options, the card's, and the tag.\n *\n * ONE declaration, here, because there are two `setupArtifacts()`: the browser barrel's\n * and the node one. Each used to declare its own `ArtifactsSetupOptions`, and they were\n * not the same shape — the node copy omitted `ArtifactRenderOptions`, so `preview` and\n * `onBinary` were type errors against the SSR entry while being valid against the\n * browser one. A consumer typing a shared setup object got a different contract\n * depending on which condition resolved, from a name that reads as one thing.\n *\n * The server ignores `preview` and `onBinary` (it registers no renderer), and that is\n * correct: the same options object is meant to be written once and passed on both\n * sides. An option nobody reads there is inert, whereas a type error there is a wall.\n */\nexport interface ArtifactsSetupOptions extends ArtifactToolOptions, ArtifactRenderOptions {\n /**\n * The tag recognised in the prose — `<artifact …>…</artifact>` by default. `false`\n * registers no grammar: only the tool produces artifacts then.\n */\n tag?: string | false;\n}\n","/**\n * The binary-artifact path: pdf, xlsx, docx.\n *\n * A different LIFETIME from the card, and that is the seam. The card renders content\n * that arrives in the stream and is complete when the stream ends. This path renders a\n * file that something ELSE produces from that content — a sandbox that runs the JS the\n * model wrote and hands back a workbook, a PDF, a document. That something is the\n * app's (`onBinary` in `setupArtifacts`): the plugin knows how to ask and how to show\n * the answer, not how to make it.\n *\n * This file used to be a protocol — three window events, a redownload/rehydrate\n * pair of host handlers, a debounce against a sandbox that was \"busy\", and the name of\n * a service that existed in no package. What is left is a function call and its two\n * outcomes: the bytes, or an error shown in the card.\n */\nimport { escapeHtml, escapeAttr, contextConfig } from '@aparte/core';\nimport type { ArtifactSegment } from './segment.js';\nimport { stripCodeFences, labelForKind } from './shared.js';\nimport { streamHighlight } from './highlight.js';\nimport { renderOptions, type ArtifactBinary } from './options.js';\n\nconst FILE_ICON_LABEL: Record<string, string> = {\n xlsx: 'XLS',\n pdf: 'PDF',\n docx: 'DOC',\n};\n\n/**\n * Bytes already produced, by segment id — so a re-mount (a branch switch, a\n * conversation toggle) shows the file again without asking the app to make it twice.\n * Bounded: each entry holds a whole file.\n */\nconst produced = new Map<string, ArtifactBinary>();\nconst MAX_PRODUCED = 24;\n/** One generation in flight per segment, whatever re-mounts meanwhile. */\nconst inFlight = new Map<string, Promise<ArtifactBinary>>();\n\nfunction remember(id: string, bin: ArtifactBinary): void {\n produced.delete(id);\n if (produced.size >= MAX_PRODUCED) {\n const oldest = produced.keys().next().value;\n if (oldest !== undefined) produced.delete(oldest);\n }\n produced.set(id, bin);\n}\n\n/** For tests: forget every produced file and every generation in flight. */\nexport function resetBinaryArtifacts(): void {\n produced.clear();\n inFlight.clear();\n}\n\nexport function renderBinaryFileArtifact(segment: ArtifactSegment, kind: string): string {\n const cfg = contextConfig();\n const title = segment.title?.trim() || labelForKind(kind);\n const iconLabel = FILE_ICON_LABEL[kind] ?? kind.toUpperCase();\n const isStreaming = !!segment.isStreaming;\n const canProduce = typeof renderOptions(cfg).onBinary === 'function';\n const downloadLabel = escapeHtml(cfg.t('download'));\n const done = produced.get(segment.id);\n\n // Already produced (a re-mount): the file, at once.\n if (done && !isStreaming) {\n const preview = previewMarkup(done, kind); // safe-text: the app's previewHtml through the config's sanitizer, or an escapeHtml'd sentence — markup on purpose, as swapToPreview() sets it\n return `\n <div class=\"aparte-segment aparte-card aparte-segment-artifact-file\"\n data-segment-id=\"${escapeHtml(segment.id)}\"\n data-artifact-type=\"${escapeHtml(kind)}\"\n data-state=\"ready\">\n <div class=\"aparte-art-file__card\">\n <div class=\"aparte-art-file__icon\" data-kind=\"${escapeHtml(kind)}\">${escapeHtml(iconLabel)}</div>\n <div class=\"aparte-art-file__meta\">\n <div class=\"aparte-art-file__meta-name\" data-role=\"file-name\">${escapeHtml(done.filename)}</div>\n <div class=\"aparte-art-file__meta-sub\" data-role=\"file-sub\">${escapeHtml(formatBytes(byteLength(done.buffer)))} · ${escapeHtml(kind.toUpperCase())}</div>\n </div>\n <div class=\"aparte-art-file__actions\">\n <button type=\"button\" class=\"aparte-btn aparte-btn--primary aparte-btn--solid aparte-art-file__btn aparte-art-file__btn--primary\" data-action=\"download\">${downloadLabel}</button>\n </div>\n </div>\n <div class=\"aparte-art-file__body\">\n <div class=\"aparte-art-file__code-pane\" data-role=\"code-pane\" hidden>\n <pre><code class=\"language-js\"></code></pre>\n </div>\n <div class=\"aparte-art-file__preview-pane\" data-role=\"preview-pane\">${preview}</div>\n </div>\n </div>\n `;\n }\n\n const cleanContent = stripCodeFences(segment.content || '');\n // Streaming: the model is still writing the source. Settled with a producer: the\n // file is being made. Settled without one: the source is all there is to show.\n const subText = isStreaming ? cfg.t('generating') : canProduce ? cfg.t('rebuildingPreview') : kind.toUpperCase();\n return `\n <div class=\"aparte-segment aparte-card aparte-segment-artifact-file\"\n data-segment-id=\"${escapeHtml(segment.id)}\"\n data-artifact-type=\"${escapeHtml(kind)}\"\n data-state=\"${escapeAttr(isStreaming ? 'streaming' : canProduce ? 'compiling' : 'source')}\">\n <div class=\"aparte-art-file__card\">\n <div class=\"aparte-art-file__icon\" data-kind=\"${escapeHtml(kind)}\">${escapeHtml(iconLabel)}</div>\n <div class=\"aparte-art-file__meta\">\n <div class=\"aparte-art-file__meta-name\" data-role=\"file-name\">${escapeHtml(title)}</div>\n <div class=\"aparte-art-file__meta-sub\" data-role=\"file-sub\">${escapeHtml(subText)}</div>\n </div>\n <div class=\"aparte-art-file__actions\">\n ${canProduce ? `<button type=\"button\" class=\"aparte-btn aparte-btn--primary aparte-btn--solid aparte-art-file__btn aparte-art-file__btn--primary\" data-action=\"download\" disabled>${downloadLabel}</button>` : ''}\n </div>\n </div>\n <div class=\"aparte-art-file__body\">\n <div class=\"aparte-art-file__code-pane\" data-role=\"code-pane\">\n <pre><code class=\"language-js\">${escapeHtml(cleanContent)}</code></pre>\n </div>\n <div class=\"aparte-art-file__preview-pane\" data-role=\"preview-pane\" hidden></div>\n </div>\n </div>\n `;\n}\n\nexport function setupBinaryFileArtifact(element: HTMLElement, segment: ArtifactSegment, kind: string): void {\n if (element.dataset['aparteInit'] !== 'true') {\n element.dataset['aparteInit'] = 'true';\n element.addEventListener('click', (ev) => {\n const target = ev.target as HTMLElement;\n const action = target.closest<HTMLElement>('[data-action]')?.getAttribute('data-action');\n if (action !== 'download') return;\n const bin = produced.get(segment.id);\n if (bin) downloadBinary(bin);\n });\n }\n if (segment.isStreaming) return;\n settle(element, segment, kind);\n}\n\nexport function updateBinaryFileArtifact(element: HTMLElement, segment: ArtifactSegment, isStreaming: boolean): void {\n const state = element.getAttribute('data-state');\n if (state === 'ready' || state === 'error') return;\n\n const cleanContent = stripCodeFences(segment.content || '');\n if (isStreaming) {\n streamHighlight(element, '[data-role=\"code-pane\"]', cleanContent, 'js', segment.id);\n return;\n }\n const codeEl = element.querySelector<HTMLElement>('[data-role=\"code-pane\"] code');\n if (codeEl) codeEl.textContent = cleanContent;\n if (state === 'streaming') settle(element, segment, kind(element));\n}\n\n/**\n * The source is final. Highlight it, and — when the app can — ask for the file.\n * One request per segment whatever re-mounts meanwhile; the answer is remembered so a\n * later mount shows the file without asking again.\n */\nfunction settle(element: HTMLElement, segment: ArtifactSegment, kind: string): void {\n const cfg = contextConfig(element);\n const cleanContent = stripCodeFences(segment.content || '');\n const wrapper = element.querySelector<HTMLElement>('[data-role=\"code-pane\"]');\n if (wrapper) {\n void cfg.highlightCode(cleanContent, 'js').then(html => { wrapper.innerHTML = html; })\n .catch(() => { /* best-effort: a failed highlight degrades silently */ });\n }\n\n const already = produced.get(segment.id);\n if (already) { swapToPreview(element, already, kind); return; }\n\n const resolve = renderOptions(cfg).onBinary;\n if (!resolve) { element.setAttribute('data-state', 'source'); return; }\n element.setAttribute('data-state', 'compiling');\n\n let job = inFlight.get(segment.id);\n if (!job) {\n job = resolve({ ...segment, content: cleanContent });\n inFlight.set(segment.id, job);\n // Both branches, or the bookkeeping chain itself rejects unhandled on failure.\n const done = (): void => { inFlight.delete(segment.id); };\n job.then(done, done);\n }\n void job.then((bin) => {\n remember(segment.id, bin);\n if (element.isConnected) swapToPreview(element, bin, kind);\n }).catch((err: unknown) => {\n if (element.isConnected) showError(element, err instanceof Error ? err.message : String(err));\n });\n}\n\nfunction kind(element: HTMLElement): string {\n return (element.getAttribute('data-artifact-type') || '').toLowerCase();\n}\n\nfunction previewMarkup(bin: ArtifactBinary, kind: string): string {\n return bin.previewHtml\n ? contextConfig().sanitizeHtml(bin.previewHtml)\n : `<div class=\"aparte-art-file__preview-empty\">${escapeHtml(contextConfig().t('previewPending'))} ${escapeHtml(kind)}</div>`;\n}\n\nfunction swapToPreview(element: HTMLElement, bin: ArtifactBinary, kind: string): void {\n element.setAttribute('data-state', 'ready');\n const codePane = element.querySelector<HTMLElement>('[data-role=\"code-pane\"]');\n if (codePane) codePane.hidden = true;\n const preview = element.querySelector<HTMLElement>('[data-role=\"preview-pane\"]');\n if (preview) {\n // `previewHtml` comes from the app and is built from file bytes the model's code\n // produced, so it goes through the sanitizer like every other innerHTML here.\n preview.innerHTML = previewMarkup(bin, kind);\n preview.hidden = false;\n }\n const nameEl = element.querySelector<HTMLElement>('[data-role=\"file-name\"]');\n if (nameEl) nameEl.textContent = bin.filename;\n const sub = element.querySelector<HTMLElement>('[data-role=\"file-sub\"]');\n if (sub) sub.textContent = `${formatBytes(byteLength(bin.buffer))} · ${kind.toUpperCase()}`;\n const dlBtn = element.querySelector<HTMLButtonElement>('[data-action=\"download\"]');\n if (dlBtn) dlBtn.disabled = false;\n}\n\nfunction downloadBinary(bin: ArtifactBinary): void {\n const blob = new Blob([bin.buffer], { type: bin.mime });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = bin.filename;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n}\n\nfunction byteLength(part: BlobPart): number {\n if (typeof part === 'string') return new TextEncoder().encode(part).length;\n if (part instanceof Blob) return part.size;\n return (part as ArrayBufferView | ArrayBuffer).byteLength;\n}\n\nfunction formatBytes(n: number): string {\n if (n < 1024) return `${n} B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;\n return `${(n / (1024 * 1024)).toFixed(2)} MB`;\n}\n\nfunction showError(element: HTMLElement, errorMsg: string): void {\n if (element.getAttribute('data-state') === 'ready') return;\n element.setAttribute('data-state', 'error');\n const cfg = contextConfig(element);\n const sub = element.querySelector<HTMLElement>('[data-role=\"file-sub\"]');\n if (sub) sub.textContent = cfg.t('sandboxError');\n const body = element.querySelector<HTMLElement>('.aparte-art-file__body');\n if (body) {\n // The first line of a stack is the actionable one.\n const short = (errorMsg.split('\\n')[0] ?? '').slice(0, 240);\n body.innerHTML = `\n <div class=\"aparte-art-file__error\">\n <div class=\"aparte-art-file__error-title\">${escapeHtml(cfg.t('sandboxError'))}</div>\n <div class=\"aparte-output aparte-art-file__error-msg\">${escapeHtml(short)}</div>\n <div class=\"aparte-art-file__error-hint\">${escapeHtml(cfg.t('sandboxErrorHint'))}</div>\n </div>\n `;\n }\n}\n","export default \"/*\\n * @aparte/plugin-artifacts — the artifact card and the artifact file preview.\\n *\\n * Injected once per document through the renderer's `getStyles()`, the seam core\\n * keeps for a renderer that is not core's. It reads core's tokens (`--aparte-space-*`,\\n * `--aparte-surface-*`, `--aparte-code-bg`…) and declares its own below, on the two\\n * roots it draws, so a consumer overrides them the way they override any other:\\n * `.aparte-segment-artifact-card { --aparte-art-paper-bg: … }`.\\n */\\n.aparte-segment-artifact-card,\\n.aparte-segment-artifact-file {\\n /* Paper. An artifact preview is a DOCUMENT shown inside the chat — a spreadsheet,\\n a rendered page — so it stays light whatever the app theme is, the way a PDF\\n viewer shows a white page in a dark editor. */\\n --aparte-art-paper-bg: #fff;\\n --aparte-art-paper-text: #1f2937;\\n --aparte-art-paper-row-alt: #f9fafb;\\n --aparte-art-paper-head-bg: #f3f4f6;\\n --aparte-art-paper-head-text: #111827;\\n --aparte-art-paper-border: rgba(0, 0, 0, 0.1);\\n /* File-type tiles. Brand colours, so they are literals on purpose — but named,\\n because an app with its own file-type palette has nowhere else to put it. The\\n lettering is fixed like the tiles: a themed ink would go near-black in dark\\n mode, on a dark green tile. */\\n --aparte-art-file-icon-bg: linear-gradient(135deg, #1d6f42, #0f5132);\\n --aparte-art-file-icon-color: #fff;\\n --aparte-art-file-icon-bg-pdf: linear-gradient(135deg, #c0392b, #7d1f17);\\n --aparte-art-file-icon-bg-docx: linear-gradient(135deg, #1e5288, #0f3060);\\n --aparte-art-file-error-msg-bg: rgba(0, 0, 0, 0.04);\\n /* Sizes. */\\n --aparte-art-card-header-min-height: 36px;\\n --aparte-art-card-pulse-size: 8px;\\n --aparte-art-card-btn-size: 28px;\\n --aparte-art-file-icon-size: 40px;\\n --aparte-art-file-preview-padding: 20px;\\n --aparte-art-file-error-padding-inline: 18px;\\n}\\n/* Not the paper — that stays light on purpose. This is a code block inside the\\n error panel, which does follow the theme. */\\n[data-aparte-theme=\\\"dark\\\"] .aparte-segment-artifact-file {\\n --aparte-art-file-error-msg-bg: rgba(255, 255, 255, 0.06);\\n}\\n/* Artifact card */\\n/* The shell is `.aparte-card`; what stays here is what a card in a TRANSCRIPT needs\\n and a card in general does not — the vertical rhythm between messages, and `font:\\n inherit` so a segment never picks up a host's form-control font. */\\n.aparte-segment-artifact-card,\\n.aparte-segment-artifact-file {\\n margin: var(--aparte-space-4) 0;\\n font: inherit;\\n}\\n.aparte-art-card__header {\\n display: flex; align-items: center; justify-content: space-between;\\n padding: var(--aparte-space-4) var(--aparte-space-5);\\n border-bottom: var(--aparte-border-width) solid var(--aparte-border);\\n background: var(--aparte-surface-2);\\n min-height: var(--aparte-art-card-header-min-height);\\n}\\n.aparte-art-card__title-block { display: flex; align-items: center; gap: var(--aparte-space-4); min-width: 0; }\\n/* An outline `.aparte-badge`. What stays is the lettering: a language tag reads as a\\n code marker, so it is upper-cased and tracked out — which a badge in general is not. */\\n.aparte-art-card__kind {\\n text-transform: uppercase;\\n letter-spacing: 0.04em;\\n --aparte-badge-font-size: var(--aparte-font-size-xs);\\n --aparte-badge-radius: var(--aparte-radius-sm);\\n}\\n.aparte-art-card__title {\\n font-size: var(--aparte-font-size-lg); font-weight: var(--aparte-font-weight-medium);\\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\\n}\\n/* The geometry is `.aparte-dot`; a streaming card breathes on the accent rather than\\n the status colour, and larger, because it marks a whole card and not a line. */\\n.aparte-art-card__pulse {\\n --aparte-status-dot-size: var(--aparte-art-card-pulse-size);\\n --aparte-status-color: var(--aparte-accent);\\n}\\n.aparte-art-card__actions { display: flex; gap: var(--aparte-space-2); }\\n.aparte-art-card__btn {\\n /* Same as .aparte-action-btn: declare the size, let the recipe draw. */\\n --aparte-btn-size: var(--aparte-art-card-btn-size);\\n}\\n.aparte-art-card__btn:hover:not(:disabled) {\\n background: var(--aparte-surface-hover);\\n border-color: var(--aparte-border);\\n color: var(--aparte-text);\\n}\\n.aparte-art-card__btn:disabled { opacity: var(--aparte-disabled-opacity); cursor: not-allowed; }\\n.aparte-art-card__tabs {\\n /* justify-content and the padding are DECLARED, not left to a default.\\n Core is light DOM on purpose - no shadow root, no ::part(), any selector\\n reaches in - and the corollary is that a component must state what its\\n layout depends on, because an undeclared property has nothing to override\\n a host rule with. A consuming page with a bare nav selector setting\\n justify-content: space-between and padding-top (this library's own docs\\n site had exactly that) otherwise pushes these two tabs to opposite ends of\\n the card and pads the row out.\\n\\n flex-end, and Code first in the DOM. The card OPENS on Code - mounting the\\n preview would execute model-authored code with no gesture (ratified\\n decision #8) - and a selected tab sitting second reads backwards. Right,\\n because the header above puts the artifact's identity on the left and its\\n copy/download buttons on the right, so this keeps every control in one\\n column. DOM order is also keyboard order, so the tab a reader lands on\\n first is the one already showing. */\\n display: flex; justify-content: flex-end; align-items: stretch; gap: var(--aparte-space-1);\\n padding: var(--aparte-space-2) var(--aparte-space-4) 0;\\n border-bottom: var(--aparte-border-width) solid var(--aparte-border);\\n background: var(--aparte-surface-2);\\n}\\n/* The tabs are `.aparte-tabs--underline`, like the elicitation panel's steps — one\\n tab in this library, not one per component. Only the row's own business stays. */\\n.aparte-art-card__tabs button { font-size: var(--aparte-font-size-sm); }\\n/* The card's heights, as variables with ONE owner each.\\n They were four hardcoded numbers in the only part of this card that did not\\n use a variable - everything else here already reads var(--aparte-code-bg) and\\n friends - and two of them had to agree while a third contradicted a fourth:\\n the code pane repeated the body's 600px, and the \\\"press Preview\\\" placeholder\\n was 120px tall inside a body whose min-height said 80, so that minimum applied\\n to nothing.\\n The frame stays a FIXED height rather than an aspect ratio, which is what\\n embeds of arbitrary HTML actually do - CodeSandbox documents 500px, StackBlitz\\n takes a height parameter - because a frame with an opaque origin cannot be\\n measured and a 16/10 ratio on a wide card is enormous. The vh cap is the part\\n that was missing: a fixed 480px should not own a phone screen.\\n Each default lives in its read, as var(--x, literal), the way every other\\n value in this file already does - not in a declaration block on top of the\\n fallbacks, which would be two owners of one number again. It also means the\\n docs' CSS-variable generator finds them: its sweep looks for reads that\\n carry a fallback, so a read without one is a public knob nobody documents. */\\n.aparte-art-card__body {\\n position: relative;\\n /* The placeholder is the tallest thing this box can hold while empty, so it\\n IS the minimum - one number instead of two that disagreed. */\\n min-height: var(--aparte-artifact-pending-height, 120px);\\n max-height: var(--aparte-artifact-body-max, 600px);\\n overflow: hidden;\\n}\\n.aparte-art-card__pane { display: none; height: 100%; }\\n.aparte-segment-artifact-card[data-tab=\\\"code\\\"] .aparte-art-card__pane[data-pane=\\\"code\\\"] { display: block; }\\n.aparte-segment-artifact-card[data-tab=\\\"preview\\\"] .aparte-art-card__pane[data-pane=\\\"preview\\\"] { display: block; }\\n.aparte-art-card__pane[data-pane=\\\"code\\\"] {\\n /* The body already caps this; repeating the number was the second owner. */\\n max-height: var(--aparte-artifact-body-max, 600px); overflow: auto;\\n}\\n.aparte-art-card__frame {\\n display: block;\\n width: 100%;\\n height: min(var(--aparte-artifact-frame-height, 480px), var(--aparte-artifact-frame-max, 70vh));\\n border: 0;\\n background: var(--aparte-art-paper-bg);\\n}\\n.aparte-art-card__pending {\\n display: flex; align-items: center; justify-content: center;\\n height: var(--aparte-artifact-pending-height, 120px);\\n color: var(--aparte-text-muted);\\n font-size: var(--aparte-font-size-lg);\\n font-style: italic;\\n}\\n/* ── Binary file artifact (xlsx/pdf/docx) ──────────────────── */\\n.aparte-art-file__card {\\n display: flex; align-items: center; gap: var(--aparte-space-6);\\n padding: var(--aparte-space-6) var(--aparte-space-7);\\n background: var(--aparte-surface-2);\\n border-bottom: var(--aparte-border-width) solid var(--aparte-border);\\n}\\n.aparte-art-file__body {\\n position: relative;\\n}\\n.aparte-art-file__code-pane {\\n max-height: var(--aparte-artifact-file-code-max, 360px); overflow: auto;\\n background: var(--aparte-code-bg);\\n}\\n.aparte-art-file__preview-pane {\\n max-height: var(--aparte-artifact-file-preview-max, 460px);\\n overflow: auto;\\n background: var(--aparte-art-paper-bg);\\n /* Preview is a document view — force light scheme regardless of\\n the app theme, with a dark text colour so cells stay readable. */\\n color: var(--aparte-art-paper-text);\\n}\\n.aparte-art-file__icon {\\n width: var(--aparte-art-file-icon-size); height: var(--aparte-art-file-icon-size);\\n border-radius: var(--aparte-radius-lg);\\n display: flex; align-items: center; justify-content: center;\\n background: var(--aparte-art-file-icon-bg);\\n color: var(--aparte-art-file-icon-color);\\n font-weight: var(--aparte-font-weight-bold);\\n font-size: var(--aparte-font-size-sm);\\n letter-spacing: 0.04em;\\n flex-shrink: 0;\\n}\\n.aparte-art-file__icon[data-kind=\\\"pdf\\\"] { background: var(--aparte-art-file-icon-bg-pdf); }\\n.aparte-art-file__icon[data-kind=\\\"docx\\\"] { background: var(--aparte-art-file-icon-bg-docx); }\\n.aparte-art-file__meta { flex: 1 1 auto; min-width: 0; }\\n.aparte-art-file__meta-name {\\n font-weight: var(--aparte-font-weight-semibold); font-size: var(--aparte-font-size-base);\\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\\n}\\n.aparte-art-file__meta-sub {\\n font-size: var(--aparte-font-size-sm);\\n color: var(--aparte-text-muted);\\n}\\n.aparte-art-file__actions { display: flex; gap: var(--aparte-space-3); flex-shrink: 0; }\\n/* The chrome is `.aparte-btn`; every one of these is a `--primary --solid`, so the\\n fill comes from the recipe. Its own measurements stay: a labelled button in a card\\n is roomier than an icon in a row. */\\n.aparte-art-file__btn {\\n padding: var(--aparte-space-3) var(--aparte-space-5);\\n font-size: var(--aparte-font-size-md);\\n}\\n/* NOTHING here repaints the button, and the comment above is why. These seven lines\\n re-declared `background`, `color`, `border-color` and a hover the recipe already\\n paints — a migration leftover that contradicted the sentence directly above it.\\n\\n Five of them were inert duplicates. The sixth was not: `color: var(--aparte-text-inverse)`\\n overrode `--aparte-btn-on-intent`, which the recipe derives from the fill. Measured in a\\n browser on the built stylesheet: 3.54:1 in the light theme against the recipe's 5.27 —\\n an AA failure on a visible button label, in the default theme. It was also the last\\n place in `styles/` forcing `--aparte-text-inverse` as ink on a coloured fill; badge and\\n field had already stopped.\\n\\n The consumer cost was the sharper one: the one-attribute rebrand this library documents\\n re-derives the ink on every other solid-primary button and, here alone, kept a token\\n bound to core's own palette. `.aparte-art-file__btn:hover` went with them — at 0,2,0 it\\n was always beaten by `.aparte-btn--solid:hover:not(:disabled)` at 0,3,0, so it never\\n applied either. */\\n/* Hide the download button until the sandbox has produced the buffer.\\n Avoids confusing the user with a disabled-but-styled-primary button\\n during streaming / compiling. */\\n.aparte-segment-artifact-file:not([data-state=\\\"ready\\\"]) .aparte-art-file__btn[data-action=\\\"download\\\"] {\\n display: none;\\n}\\n.aparte-art-file__preview-pane th,\\n.aparte-art-file__preview-pane td {\\n border: var(--aparte-border-width) solid var(--aparte-art-paper-border);\\n padding: var(--aparte-space-3) var(--aparte-space-5);\\n text-align: left;\\n white-space: nowrap;\\n color: var(--aparte-art-paper-text);\\n background: var(--aparte-art-paper-bg);\\n}\\n.aparte-art-file__preview-pane tr:nth-child(odd) td { background: var(--aparte-art-paper-row-alt); }\\n.aparte-art-file__preview-pane tr:first-child td {\\n background: var(--aparte-art-paper-head-bg);\\n font-weight: var(--aparte-font-weight-semibold);\\n position: sticky; top: 0;\\n color: var(--aparte-art-paper-head-text);\\n}\\n.aparte-art-file__preview-empty {\\n padding: var(--aparte-art-file-preview-padding); text-align: center;\\n color: var(--aparte-text-muted);\\n font-size: var(--aparte-font-size-lg); font-style: italic;\\n}\\n.aparte-segment-artifact-file[data-state=\\\"error\\\"] .aparte-art-file__icon {\\n background: var(--aparte-art-file-icon-bg-pdf);\\n}\\n.aparte-art-file__error {\\n padding: var(--aparte-space-8) var(--aparte-art-file-error-padding-inline);\\n background: var(--aparte-error-bg);\\n border-top: var(--aparte-border-width) solid var(--aparte-error-border);\\n color: var(--aparte-text);\\n}\\n.aparte-art-file__error-title {\\n font-weight: var(--aparte-font-weight-semibold);\\n font-size: var(--aparte-font-size-lg);\\n margin-bottom: var(--aparte-space-3);\\n color: var(--aparte-error-title);\\n}\\n/* A computed value shown back, which is `.aparte-output`. What stays is that this\\n one is an ERROR — tinted text on its own quiet ground — and that a raw message has\\n no spaces to wrap on, so it must be allowed to break mid-word. */\\n.aparte-art-file__error-msg {\\n display: block;\\n padding: var(--aparte-space-3) var(--aparte-space-5);\\n margin-bottom: var(--aparte-space-4);\\n background: var(--aparte-art-file-error-msg-bg);\\n color: var(--aparte-error-text);\\n word-break: break-word;\\n}\\n.aparte-art-file__error-hint {\\n font-size: var(--aparte-font-size-sm);\\n color: var(--aparte-text-muted);\\n font-style: italic;\\n}\\n/* ── The card's code and table panes ─────────────────────────\\n These three lived in core's prose sheet, styling this card's DOM from a sheet\\n that knew nothing else about it. */\\n.aparte-art-card__pane[data-pane=\\\"code\\\"] pre {\\n margin: 0; padding: var(--aparte-space-6);\\n font-size: var(--aparte-font-size-md);\\n background: var(--aparte-code-bg);\\n}\\n.aparte-art-file__code-pane pre {\\n margin: 0; padding: var(--aparte-space-6);\\n font-size: var(--aparte-font-size-md);\\n font-family: var(--aparte-code-font-family);\\n}\\n.aparte-art-file__preview-pane table {\\n border-collapse: collapse;\\n width: 100%;\\n font-size: var(--aparte-font-size-md);\\n font-family: inherit;\\n}\\n\"","/**\n * The artifact card: the inline Code/Preview panel a streamed artifact renders as.\n *\n * One thing, at length — and it used to be twice this size, half of it a `getStyles()`\n * block, which is why it kept `segment-renderers.ts` at 1900 lines while that file's own\n * banners described it as a renderer registry plus nine small renderers. It is that now,\n * and the CSS has since gone to `styles/segment/artifact.css` with every other built-in's.\n *\n * The card owns the previewable path. When a segment's kind is binary (pdf/xlsx/docx)\n * it DELEGATES to `./binary-file.ts` — which is why `BINARY_FILE_KINDS` lives\n * here despite the name: the card is what consults it to decide to hand over. A\n * call-site census settled that; grouping it as \"shared\" was the first draft's mistake.\n *\n * The preview frame is deliberately not built at render time. It is mounted only when\n * the user presses Preview (`mountPreviewFrame`), because a previewable artifact is\n * model-authored code and mounting it unasked executes it — ratified decision #8\n * applied to a tier-(c) affordance. The document it mounts comes from\n * `./preview-document.ts`, or from the consumer's own builder.\n */\nimport { escapeHtml, escapeAttr, copyText, contextConfig } from '@aparte/core';\nimport type { AparteSegmentRenderer } from '@aparte/core';\nimport { ARTIFACT_SEGMENT_TYPE, type ArtifactSegment } from './segment.js';\nimport { deriveArtifactKind } from './kinds.js';\nimport { stripCodeFences, labelForKind } from './shared.js';\nimport { streamHighlight } from './highlight.js';\nimport { PREVIEW_CSP, buildSafePreviewDocument } from './preview-document.js';\nimport { renderOptions } from './options.js';\nimport {\n renderBinaryFileArtifact,\n setupBinaryFileArtifact,\n updateBinaryFileArtifact,\n} from './binary-file.js';\nimport artifactStyles from './artifact.css?raw';\n\n/** The segment shape the card draws — exported for a consumer's own tests and renderers. */\nexport type AparteArtifactSegment = ArtifactSegment;\n\nconst PREVIEWABLE_KINDS: ReadonlySet<string> = new Set(['react', 'html', 'svg', 'js', 'css']);\n\n/** Binary file kinds: their source is implementation noise; the app produces the file. */\nconst BINARY_FILE_KINDS: ReadonlySet<string> = new Set(['pdf', 'xlsx', 'docx']);\n\n/** Whether the Preview tab exists at all, per the app's `preview` option. */\nfunction previewEnabled(): boolean {\n return renderOptions(contextConfig()).preview !== false;\n}\n\nexport const artifactRenderer: AparteSegmentRenderer<ArtifactSegment> = {\n type: ARTIFACT_SEGMENT_TYPE,\n render: (segment: ArtifactSegment) => {\n const kind = (segment.artifactType || 'unknown').toLowerCase();\n // Binary file kinds (xlsx/pdf/docx) follow a separate track: the generated JS\n // is implementation noise, so the file card shows progress, then the file.\n if (BINARY_FILE_KINDS.has(kind)) {\n return renderBinaryFileArtifact(segment, kind);\n }\n const title = segment.title?.trim() || labelForKind(kind);\n const displayLang = languageForKind(kind);\n const isStreaming = !!segment.isStreaming;\n const previewable = PREVIEWABLE_KINDS.has(kind) && previewEnabled();\n // `t()`, not `getLocale().x ?? 'X'`: `t()` already falls back to\n // APARTE_DEFAULT_LOCALE, so the English lives in ONE place instead of being\n // re-typed at each call site — and `setLocale()` REPLACES rather than merges,\n // which makes a partial locale the normal case rather than an edge one.\n // `download` is title AND aria-label: they disagreed on the button next door,\n // whose title went through `t('copy')` while its aria-label said \"Copy\" in\n // every language.\n const cfg = contextConfig();\n const downloadLabel = cfg.t('download');\n const previewLabel = cfg.t('preview');\n const codeLabel = cfg.t('code');\n const isBinary = BINARY_FILE_KINDS.has(kind);\n const cleanContent = stripCodeFences(segment.content || '');\n /*\n * Ids, so the tabs can POINT at their panels.\n *\n * The card announced `role=\"tablist\"` with `role=\"tab\"` buttons and shipped none\n * of the pattern's obligations: no `aria-controls`, no `role=\"tabpanel\"`, no ids\n * and no arrow keys. A role is a promise about behaviour — declaring tablist and\n * then behaving like two ordinary buttons tells a screen-reader user to expect a\n * relationship and a keyboard model that are not there, which is worse than the\n * plain buttons it actually was.\n *\n * Scoped to the segment id because a transcript holds many cards, and duplicate\n * ids would make `aria-controls` point at whichever one parsed first.\n *\n * Escaped HERE rather than inside an id-building helper: `check:attr-escaping`\n * follows a local produced by an escaper and cannot see through a function, and\n * teaching it to trust the helper's NAME is precisely the hole that guard was\n * just tightened to close.\n */\n const cardId = escapeAttr(segment.id);\n // The card ALWAYS opens on the code tab, and the preview frame is not built\n // here at all — it is mounted only when the user presses Preview\n // (`mountPreviewFrame`, called from the tab handler).\n //\n // It used to open on Preview for any artifact that was not streaming, i.e.\n // every render of a completed one — so reloading a persisted conversation\n // executed the model's JS with no gesture. Defaulting the tab is not enough\n // on its own either: a `display:none` iframe still loads and still runs\n // scripts, so the frame has to be ABSENT, not hidden.\n //\n // Ratified decision #8, applied to a tier-(c) affordance: content the app\n // did not produce does not get to act on its own.\n\n return `\n <div class=\"aparte-segment aparte-card aparte-segment-artifact-card\"\n data-segment-id=\"${escapeHtml(segment.id)}\"\n data-artifact-type=\"${escapeHtml(kind)}\"\n data-streaming=\"${isStreaming ? 'true' : 'false'}\"\n data-tab=\"code\"\n data-previewable=\"${previewable ? 'true' : 'false'}\"\n data-binary=\"${isBinary ? 'true' : 'false'}\">\n <header class=\"aparte-art-card__header\">\n <div class=\"aparte-art-card__title-block\">\n <span class=\"aparte-badge aparte-badge--outline aparte-art-card__kind\" data-kind=\"${escapeHtml(kind)}\">${escapeHtml(displayLang)}</span>\n <span class=\"aparte-art-card__title\">${escapeHtml(title)}</span>\n ${isStreaming ? `<span class=\"aparte-dot aparte-art-card__pulse\" role=\"img\" aria-label=\"${escapeAttr(cfg.t('generating'))}\"></span>` : ''}\n </div>\n <div class=\"aparte-art-card__actions\">\n <button type=\"button\" class=\"aparte-btn aparte-btn--icon aparte-art-card__btn\" data-action=\"copy\" title=\"${escapeAttr(contextConfig().t('copy'))}\" aria-label=\"${escapeAttr(contextConfig().t('copy'))}\">\n ${contextConfig().getIcon('copy')}\n </button>\n <button type=\"button\" class=\"aparte-btn aparte-btn--icon aparte-art-card__btn\" data-action=\"download\" title=\"${escapeAttr(downloadLabel)}\" aria-label=\"${escapeAttr(downloadLabel)}\" ${isStreaming ? 'disabled' : ''}>\n ${contextConfig().getIcon('download')}\n </button>\n </div>\n </header>\n <nav class=\"aparte-tabs aparte-tabs--underline aparte-art-card__tabs\" role=\"tablist\">\n <button type=\"button\" class=\"aparte-tabs__tab\" role=\"tab\" id=\"aparte-art-${cardId}-tab-code\" aria-controls=\"aparte-art-${cardId}-pane-code\" aria-selected=\"true\" tabindex=\"0\" data-tab-target=\"code\">${escapeHtml(codeLabel)}</button>\n ${previewable ? `<button type=\"button\" class=\"aparte-tabs__tab\" role=\"tab\" id=\"aparte-art-${cardId}-tab-preview\" aria-controls=\"aparte-art-${cardId}-pane-preview\" aria-selected=\"false\" tabindex=\"-1\" data-tab-target=\"preview\" ${isStreaming ? 'disabled' : ''}>${escapeHtml(previewLabel)}</button>` : ''}\n </nav>\n <div class=\"aparte-art-card__body\">\n <div class=\"aparte-art-card__pane\" role=\"tabpanel\" id=\"aparte-art-${cardId}-pane-code\" aria-labelledby=\"aparte-art-${cardId}-tab-code\" tabindex=\"0\" data-pane=\"code\">\n <div class=\"aparte-code-content-wrapper\">\n <pre><code class=\"language-${escapeHtml(displayLang)}\">${escapeHtml(cleanContent)}</code></pre>\n </div>\n </div>\n ${previewable ? `\n <div class=\"aparte-art-card__pane\" role=\"tabpanel\" id=\"aparte-art-${cardId}-pane-preview\" aria-labelledby=\"aparte-art-${cardId}-tab-preview\" tabindex=\"0\" data-pane=\"preview\">\n <div class=\"aparte-art-card__pending\">${escapeHtml(cfg.t('previewPending'))}</div>\n </div>\n ` : ''}\n </div>\n </div>\n `;\n },\n /**\n * Every string this card shows: the copy button's tooltip, glyph and accessible\n * name, the download button's title and label, and the two tab names. They were\n * all hardcoded literals until they got locale keys — including the copy button's\n * `aria-label`, which said \"Copy\" while its own `title` one attribute away already\n * went through `t('copy')`, so a French reader got a French tooltip and an English\n * announcement.\n *\n * Nothing here touches the tab state or the preview pane: a mounted iframe is\n * running model-authored code, and re-rendering this card is exactly what the\n * hook exists to avoid.\n */\n relabel: (element: HTMLElement) => {\n const cfg = contextConfig();\n const copyBtn = element.querySelector('.aparte-art-card__btn[data-action=\"copy\"]');\n if (copyBtn) {\n copyBtn.setAttribute('title', cfg.t('copy'));\n copyBtn.setAttribute('aria-label', cfg.t('copy'));\n copyBtn.innerHTML = cfg.getIcon('copy');\n }\n const dl = element.querySelector('.aparte-art-card__btn[data-action=\"download\"]');\n if (dl) {\n const label = cfg.t('download');\n dl.setAttribute('title', label);\n dl.setAttribute('aria-label', label);\n }\n // Text only, and never `aria-selected` or `data-tab`: which pane is open is\n // the reader's state, not the locale's. A relabel that touched it would close\n // a preview somebody had opened.\n const previewTab = element.querySelector('[data-tab-target=\"preview\"]');\n if (previewTab) previewTab.textContent = cfg.t('preview');\n const codeTab = element.querySelector('[data-tab-target=\"code\"]');\n if (codeTab) codeTab.textContent = cfg.t('code');\n },\n setup: (element: HTMLElement, segment: AparteArtifactSegment) => {\n latestSegment.set(element, segment);\n const kind = (segment.artifactType || '').toLowerCase();\n if (BINARY_FILE_KINDS.has(kind)) {\n setupBinaryFileArtifact(element, segment, kind);\n return;\n }\n // Async highlight on the code pane\n const wrapper = element.querySelector('.aparte-code-content-wrapper');\n if (wrapper) {\n const displayLang = languageForKind(kind);\n const cleanContent = stripCodeFences(segment.content || '');\n void contextConfig().highlightCode(cleanContent, displayLang).then(html => {\n wrapper.innerHTML = html;\n }).catch(() => { /* best-effort: a failed highlight degrades silently */ });\n }\n\n /*\n * Tab switching — and, for Preview, the one place the frame is created.\n *\n * A `tablist` is a SINGLE tab stop with arrow keys inside it, not N tab stops.\n * This shipped as two ordinary buttons under a tablist role: Tab landed on each\n * in turn and the arrows did nothing, so the role promised a keyboard model the\n * card did not have. The roving `tabindex` and the arrow handling below are that\n * model; `aria-controls` / `role=\"tabpanel\"` in the markup are the other half.\n */\n const tabs = [...element.querySelectorAll<HTMLButtonElement>('[data-tab-target]')];\n const select = (btn: HTMLButtonElement, focus: boolean): void => {\n const target = btn.getAttribute('data-tab-target');\n if (!target || btn.disabled) return;\n if (target === 'preview') mountPreviewFrame(element, segment);\n element.setAttribute('data-tab', target);\n for (const b of tabs) {\n const on = b === btn;\n b.setAttribute('aria-selected', on ? 'true' : 'false');\n b.tabIndex = on ? 0 : -1;\n }\n if (focus) btn.focus();\n };\n for (const [i, btn] of tabs.entries()) {\n btn.addEventListener('click', () => select(btn, false));\n btn.addEventListener('keydown', (e) => {\n // Home/End as well as the arrows: both are in the pattern, and on a\n // two-tab list they are the fastest way to the one you are not on.\n const step = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;\n let next = -1;\n if (step) next = (i + step + tabs.length) % tabs.length;\n else if (e.key === 'Home') next = 0;\n else if (e.key === 'End') next = tabs.length - 1;\n if (next < 0) return;\n e.preventDefault();\n // Skip a disabled tab rather than trapping focus on it: Preview is\n // disabled while the artifact still streams.\n for (let n = 0; n < tabs.length; n++) {\n const candidate = tabs[(next + n * (step || 1) + tabs.length) % tabs.length];\n if (candidate && !candidate.disabled) { select(candidate, true); return; }\n }\n });\n }\n\n // Copy\n const copyBtn = element.querySelector<HTMLButtonElement>('[data-action=\"copy\"]');\n if (copyBtn) {\n copyBtn.addEventListener('click', () => {\n // Late execution (user click) — resolve from the element.\n const code = stripCodeFences(segment.content || '');\n void copyText(code).catch(() => { /* best-effort: a failed clipboard write degrades silently */ });\n const original = copyBtn.innerHTML;\n copyBtn.innerHTML = contextConfig(copyBtn).getIcon('check');\n copyBtn.setAttribute('title', contextConfig(copyBtn).t('copied'));\n setTimeout(() => {\n copyBtn.innerHTML = original;\n copyBtn.setAttribute('title', contextConfig(copyBtn).t('copy'));\n }, 1500);\n });\n }\n\n // Download — a text artifact downloads from its own content. (A binary one\n // never reaches here: its setup returned above, and its file card owns the\n // button.) The LATEST segment, not the one this closure captured.\n const dlBtn = element.querySelector<HTMLButtonElement>('[data-action=\"download\"]');\n if (dlBtn) {\n dlBtn.addEventListener('click', () => {\n if (dlBtn.disabled) return;\n downloadTextArtifact(latestSegment.get(element) ?? segment);\n });\n }\n },\n update: (element: HTMLElement, segment: AparteArtifactSegment) => {\n // Keep the click handler's view of the artifact current, and throw away a\n // frame that is now showing stale content so the next press rebuilds it.\n const previous = latestSegment.get(element);\n latestSegment.set(element, segment);\n if (previous && previous.content !== segment.content) {\n element.querySelector('.aparte-art-card__pane[data-pane=\"preview\"] iframe')?.remove();\n }\n const isStreaming = !!segment.isStreaming;\n const kind = (segment.artifactType || '').toLowerCase();\n if (BINARY_FILE_KINDS.has(kind)) {\n updateBinaryFileArtifact(element, segment, isStreaming);\n return;\n }\n const wasStreaming = element.getAttribute('data-streaming') === 'true';\n const cleanContent = stripCodeFences(segment.content || '');\n\n // 1. Live-update the code pane during streaming.\n //\n // One call owns both halves — the plain tail every token and the coloured\n // prefix on a throttle. Setting `textContent` here as well is what made the\n // pane flicker: it erased the highlighter's spans on every token.\n if (isStreaming) {\n const segId = element.getAttribute('data-segment-id') ?? segment.id;\n streamHighlight(element, '.aparte-code-content-wrapper', cleanContent, languageForKind(kind), segId);\n } else {\n const codeEl = element.querySelector('.aparte-code-content-wrapper code');\n if (codeEl) {\n codeEl.textContent = cleanContent;\n } else {\n const wrapper = element.querySelector('.aparte-code-content-wrapper');\n if (wrapper) {\n const displayLang = languageForKind(kind);\n wrapper.innerHTML = `<pre><code class=\"language-${escapeHtml(displayLang)}\">${escapeHtml(cleanContent)}</code></pre>`;\n }\n }\n }\n\n // 2. On stream-completion: highlight + build preview iframe + auto-switch\n if (wasStreaming && !isStreaming) {\n element.setAttribute('data-streaming', 'false');\n\n // The pulse says \"the model is still writing this\". `render()` painted it\n // and nothing ever took it away, so a finished document kept claiming to be\n // in flight — forever, at 1.2s a cycle. Unnoticed until the artifact demo\n // actually streamed: the indicator had never been exercised.\n element.querySelector('.aparte-art-card__pulse')?.remove();\n\n // Re-run syntax highlight now that content is final\n const wrapper = element.querySelector('.aparte-code-content-wrapper');\n if (wrapper) {\n const displayLang = languageForKind(kind);\n void contextConfig().highlightCode(cleanContent, displayLang).then(html => {\n wrapper.innerHTML = html;\n }).catch(() => { /* best-effort: a failed highlight degrades silently */ });\n }\n\n // Enable previously-disabled buttons (download, preview tab)\n element.querySelectorAll<HTMLButtonElement>('button[disabled]').forEach(b => {\n b.disabled = false;\n });\n\n // Nothing to build and nothing to switch: enabling the Preview button\n // above is the whole of it. The frame is mounted by the tab handler, on\n // a real user press — see the note at `initialTab`.\n }\n },\n // The card's sheet, injected once per document by core — the seam a renderer that\n // is not core's has onto the page. It is a real `.css` file, inlined at build.\n getStyles: () => artifactStyles,\n};\n\nfunction languageForKind(kind: string): string {\n if (kind === 'react') return 'jsx';\n if (kind === 'markdown') return 'md';\n if (kind === 'pdf' || kind === 'xlsx' || kind === 'docx') return 'js';\n return kind || 'text';\n}\n\nfunction downloadTextArtifact(segment: AparteArtifactSegment): void {\n const content = stripCodeFences(segment.content || '');\n const kind = (segment.artifactType || '').toLowerCase();\n const ext = ({\n react: 'jsx', html: 'html', svg: 'svg', js: 'js', css: 'css',\n json: 'json', markdown: 'md', csv: 'csv', text: 'txt',\n python: 'py', typescript: 'ts', bash: 'sh', sql: 'sql',\n } as Record<string, string>)[kind] ?? 'txt';\n const baseTitle = (segment.title ?? labelForKind(kind)).trim();\n const safeBase = slugifyForFilename(baseTitle) || 'artifact';\n const filename = `${safeBase}.${ext}`;\n const mime = segment.mimeType || 'text/plain';\n const blob = new Blob([content], { type: mime });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n}\n\nfunction slugifyForFilename(text: string): string {\n const lower = text.trim().toLowerCase();\n let out = '';\n let prevDash = false;\n for (let i = 0; i < lower.length && out.length < 40; i++) {\n const ch = lower[i]!;\n const isAlnum = (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9');\n if (isAlnum) { out += ch; prevDash = false; continue; }\n if (!prevDash && out.length > 0) { out += '-'; prevDash = true; }\n }\n if (out.endsWith('-')) out = out.slice(0, -1);\n return out;\n}\n\n/**\n * The LATEST segment for a mounted artifact card.\n *\n * `setup()` runs once and closes over the segment it was handed; the bubble builds a\n * fresh object on every `updateSegment`, so that closure freezes the artifact as it\n * was when it was ADDED — which for anything streamed is `content: ''`. Pressing\n * Preview then ran an empty document. Gesture-gating the frame moved the read from\n * `update()` (which always had the current segment) into a closure that did not, and\n * the test written with it never streamed, so it could not see this.\n */\nconst latestSegment = new WeakMap<HTMLElement, AparteArtifactSegment>();\n\nfunction mountPreviewFrame(element: HTMLElement, fallback: AparteArtifactSegment): void {\n const pane = element.querySelector('.aparte-art-card__pane[data-pane=\"preview\"]');\n if (!pane || pane.querySelector('iframe')) return;\n\n // The latest segment, not the one the closure captured — see `latestSegment`.\n const segment = latestSegment.get(element) ?? fallback;\n // Lower-cased like every other reader of the kind in this file: an app-built\n // segment with `artifactType: 'HTML'` rendered an enabled Preview tab whose press\n // did nothing, because `PREVIEWABLE_KINDS.has('HTML')` is false.\n const kind = (segment.artifactType || deriveArtifactKind(segment.mimeType ?? '', 'text')).toLowerCase();\n if (!PREVIEWABLE_KINDS.has(kind) || !previewEnabled()) return;\n\n const title = segment.title?.trim() || labelForKind(kind);\n // The app's builder when it gave one (`preview: (kind, body, title) => srcdoc`),\n // else the built-in document with its CSP.\n const option = renderOptions(contextConfig(element)).preview;\n const build = typeof option === 'function' ? option : buildSafePreviewDocument;\n const srcdoc = build(kind, stripCodeFences(segment.content || ''), title);\n\n /*\n * `sandbox=\"allow-scripts\"` and nothing else: no `allow-same-origin` (opaque\n * origin — the frame cannot read this page, its storage, or the key), no\n * `allow-forms` (it cannot POST out), no `allow-top-navigation` (it cannot move\n * the tab), no `allow-popups`.\n *\n * It CAN navigate itself, which no sandbox token and no CSP directive prevents —\n * see the note on PREVIEW_CSP. `referrerpolicy=\"no-referrer\"` at least keeps the\n * host URL out of that request.\n */\n pane.innerHTML = `<iframe class=\"aparte-art-card__frame\"`\n + ` sandbox=\"allow-scripts\"`\n + ` csp=\"${escapeAttr(PREVIEW_CSP)}\"`\n + ` referrerpolicy=\"no-referrer\" loading=\"lazy\"`\n + ` title=\"${escapeAttr(title)}\" srcdoc=\"${escapeAttr(srcdoc)}\"></iframe>`;\n}\n","/**\n * @aparte/plugin-artifacts\n *\n * An artifact is a document the model produces — a page, a component, a script, a\n * spreadsheet — and nothing a model does by nature: it is a convention an app teaches\n * it. This package is that convention, end to end, for aparté:\n *\n * - a real `create_artifact` **tool** the model calls (`./tool.ts`), whose structured\n * result is the document;\n * - a **tool renderer** that draws that result as the Code/Preview card;\n * - the `<artifact …>…</artifact>` **block grammar**, for a model that writes one in\n * its prose, registered on core's parser (`registerStreamBlock`);\n * - the **segment renderer** for the segment that grammar produces — the same card.\n *\n * One implementation, four registrations, all made by `setupArtifacts()`.\n *\n * Usage:\n * import { setupArtifacts } from '@aparte/plugin-artifacts';\n * setupArtifacts();\n */\nimport { aparteGlobalConfig, registerSegmentRenderer, unregisterSegmentRenderer, type AparteConfig, type AparteToolRenderer } from '@aparte/core';\nimport { createArtifactTool, artifactHandler } from './tool.js';\nimport { artifactRenderer } from './card.js';\nimport { artifactBlock, artifactFromToolCall, ARTIFACT_TAG, ARTIFACT_SEGMENT_TYPE } from './segment.js';\nimport { setRenderOptions, clearRenderOptions, type ArtifactsSetupOptions } from './options.js';\n\n/**\n * Register the tool, its renderer, the block grammar and the segment renderer on\n * `config` (the global config by default). Call once at application startup;\n * returns a function that unregisters all four.\n */\nexport function setupArtifacts(options: ArtifactsSetupOptions = {}, config: AparteConfig = aparteGlobalConfig): () => void {\n const tool = createArtifactTool(options);\n setRenderOptions(config, { preview: options.preview, onBinary: options.onBinary });\n\n config.registerTool(tool, artifactHandler);\n // The card, on the tool's result: the same renderer the segment gets, adapted to\n // the tool-call segment it is handed. `update` and `relabel` are forwarded, so a\n // preview a reader mounted survives the result landing.\n const toolRenderer: AparteToolRenderer = {\n render: (segment) => artifactRenderer.render(artifactFromToolCall(segment)),\n setup: (element, segment) => artifactRenderer.setup?.(element, artifactFromToolCall(segment)),\n update: (element, segment) => artifactRenderer.update?.(element, artifactFromToolCall(segment)),\n relabel: (element, segment) => artifactRenderer.relabel?.(element, artifactFromToolCall(segment)),\n getStyles: () => artifactRenderer.getStyles?.() ?? '',\n };\n config.registerToolRenderer(tool.name, toolRenderer);\n\n registerSegmentRenderer(artifactRenderer, config);\n const tag = options.tag === undefined ? ARTIFACT_TAG : options.tag;\n if (tag) config.registerStreamBlock(artifactBlock(tag));\n\n return () => {\n config.unregisterTool(tool.name);\n config.unregisterToolRenderer(tool.name);\n unregisterSegmentRenderer(ARTIFACT_SEGMENT_TYPE, config);\n if (tag) config.unregisterStreamBlock(tag);\n clearRenderOptions(config);\n };\n}\n\nexport { createArtifactTool, artifactHandler, ARTIFACT_SYSTEM_PROMPT } from './tool.js';\nexport type { ArtifactToolOptions } from './tool.js';\nexport { artifactRenderer } from './card.js';\nexport type { AparteArtifactSegment } from './card.js';\nexport { artifactBlock, artifactSegment, artifactFromToolCall, ARTIFACT_TAG, ARTIFACT_SEGMENT_TYPE } from './segment.js';\nexport type { ArtifactSegment, ArtifactInput } from './segment.js';\nexport { deriveArtifactKind } from './kinds.js';\nexport { buildSafePreviewDocument, PREVIEW_CSP } from './preview-document.js';\nexport type { ArtifactRenderOptions, ArtifactPreviewBuilder, ArtifactBinary, ArtifactBinaryResolver, ArtifactsSetupOptions } from './options.js';\nexport type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';\n"],"names":["kind","e"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@aparte/plugin-artifacts` — the DOM-free entry, for Node and SSR.
|
|
3
|
+
*
|
|
4
|
+
* The browser barrel carries the card, which builds DOM and reads a stylesheet; an
|
|
5
|
+
* SSR build that evaluates the import on the server needs neither. What is here is
|
|
6
|
+
* what a server can legitimately use: the tool, its handler, the block grammar (the
|
|
7
|
+
* parser runs anywhere), the preview document builder and the types. Calling
|
|
8
|
+
* `setupArtifacts()` here registers the tool and the grammar without a renderer, which
|
|
9
|
+
* is the correct outcome: nothing is being rendered there.
|
|
10
|
+
*
|
|
11
|
+
* `buildSafePreviewDocument` and `PREVIEW_CSP` are pure string work over `escapeHtml` /
|
|
12
|
+
* `escapeAttr`, both of which core's own node barrel carries — they were missing here
|
|
13
|
+
* for no reason but the omission, and the consequence was not a missing feature but a
|
|
14
|
+
* hard `SyntaxError: does not provide an export named 'buildSafePreviewDocument'` the
|
|
15
|
+
* moment an SSR build evaluated the import. `ArtifactsSetupOptions` is imported from
|
|
16
|
+
* `./options.js` rather than declared again: this file used to declare a SECOND
|
|
17
|
+
* interface of that name, without the render half, so the same name meant two shapes
|
|
18
|
+
* depending on which export condition resolved.
|
|
19
|
+
*/
|
|
20
|
+
import { type AparteConfig } from '@aparte/core';
|
|
21
|
+
import type { ArtifactsSetupOptions } from './options.js';
|
|
22
|
+
/** Register the tool, its handler and the grammar on the server. No renderer: it builds DOM. */
|
|
23
|
+
export declare function setupArtifacts(options?: ArtifactsSetupOptions, config?: AparteConfig): () => void;
|
|
24
|
+
export { createArtifactTool, artifactHandler, ARTIFACT_SYSTEM_PROMPT } from './tool.js';
|
|
25
|
+
export type { ArtifactToolOptions } from './tool.js';
|
|
26
|
+
export { artifactBlock, artifactSegment, artifactFromToolCall, ARTIFACT_TAG, ARTIFACT_SEGMENT_TYPE } from './segment.js';
|
|
27
|
+
export type { ArtifactSegment, ArtifactInput } from './segment.js';
|
|
28
|
+
export { deriveArtifactKind } from './kinds.js';
|
|
29
|
+
export { buildSafePreviewDocument, PREVIEW_CSP } from './preview-document.js';
|
|
30
|
+
export type { ArtifactRenderOptions, ArtifactPreviewBuilder, ArtifactBinary, ArtifactBinaryResolver, ArtifactsSetupOptions } from './options.js';
|
|
31
|
+
export type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';
|
|
32
|
+
//# sourceMappingURL=index.node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.node.d.ts","sourceRoot":"","sources":["../src/index.node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAGrE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1D,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,EAAE,MAAM,GAAE,YAAiC,GAAG,MAAM,IAAI,CAWzH;AAED,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACxF,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACzH,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC9E,YAAY,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,cAAc,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACjJ,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { aparteGlobalConfig } from "@aparte/core";
|
|
2
|
+
import { c as createArtifactTool, a as artifactHandler, A as ARTIFACT_TAG, b as artifactBlock } from "./preview-document-xP-Ne-F0.js";
|
|
3
|
+
import { d, e, P, f, g, h, i } from "./preview-document-xP-Ne-F0.js";
|
|
4
|
+
function setupArtifacts(options = {}, config = aparteGlobalConfig) {
|
|
5
|
+
const tool = createArtifactTool(options);
|
|
6
|
+
config.registerTool(tool, artifactHandler);
|
|
7
|
+
config.registerToolRenderer(tool.name, { render: () => "" });
|
|
8
|
+
const tag = options.tag === void 0 ? ARTIFACT_TAG : options.tag;
|
|
9
|
+
if (tag) config.registerStreamBlock(artifactBlock(tag));
|
|
10
|
+
return () => {
|
|
11
|
+
config.unregisterTool(tool.name);
|
|
12
|
+
config.unregisterToolRenderer(tool.name);
|
|
13
|
+
if (tag) config.unregisterStreamBlock(tag);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
d as ARTIFACT_SEGMENT_TYPE,
|
|
18
|
+
e as ARTIFACT_SYSTEM_PROMPT,
|
|
19
|
+
ARTIFACT_TAG,
|
|
20
|
+
P as PREVIEW_CSP,
|
|
21
|
+
artifactBlock,
|
|
22
|
+
f as artifactFromToolCall,
|
|
23
|
+
artifactHandler,
|
|
24
|
+
g as artifactSegment,
|
|
25
|
+
h as buildSafePreviewDocument,
|
|
26
|
+
createArtifactTool,
|
|
27
|
+
i as deriveArtifactKind,
|
|
28
|
+
setupArtifacts
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=index.node.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.node.js","sources":["../src/index.node.ts"],"sourcesContent":["/**\n * `@aparte/plugin-artifacts` — the DOM-free entry, for Node and SSR.\n *\n * The browser barrel carries the card, which builds DOM and reads a stylesheet; an\n * SSR build that evaluates the import on the server needs neither. What is here is\n * what a server can legitimately use: the tool, its handler, the block grammar (the\n * parser runs anywhere), the preview document builder and the types. Calling\n * `setupArtifacts()` here registers the tool and the grammar without a renderer, which\n * is the correct outcome: nothing is being rendered there.\n *\n * `buildSafePreviewDocument` and `PREVIEW_CSP` are pure string work over `escapeHtml` /\n * `escapeAttr`, both of which core's own node barrel carries — they were missing here\n * for no reason but the omission, and the consequence was not a missing feature but a\n * hard `SyntaxError: does not provide an export named 'buildSafePreviewDocument'` the\n * moment an SSR build evaluated the import. `ArtifactsSetupOptions` is imported from\n * `./options.js` rather than declared again: this file used to declare a SECOND\n * interface of that name, without the render half, so the same name meant two shapes\n * depending on which export condition resolved.\n */\nimport { aparteGlobalConfig, type AparteConfig } from '@aparte/core';\nimport { createArtifactTool, artifactHandler } from './tool.js';\nimport { artifactBlock, ARTIFACT_TAG } from './segment.js';\nimport type { ArtifactsSetupOptions } from './options.js';\n\n/** Register the tool, its handler and the grammar on the server. No renderer: it builds DOM. */\nexport function setupArtifacts(options: ArtifactsSetupOptions = {}, config: AparteConfig = aparteGlobalConfig): () => void {\n const tool = createArtifactTool(options);\n config.registerTool(tool, artifactHandler);\n config.registerToolRenderer(tool.name, { render: () => '' });\n const tag = options.tag === undefined ? ARTIFACT_TAG : options.tag;\n if (tag) config.registerStreamBlock(artifactBlock(tag));\n return () => {\n config.unregisterTool(tool.name);\n config.unregisterToolRenderer(tool.name);\n if (tag) config.unregisterStreamBlock(tag);\n };\n}\n\nexport { createArtifactTool, artifactHandler, ARTIFACT_SYSTEM_PROMPT } from './tool.js';\nexport type { ArtifactToolOptions } from './tool.js';\nexport { artifactBlock, artifactSegment, artifactFromToolCall, ARTIFACT_TAG, ARTIFACT_SEGMENT_TYPE } from './segment.js';\nexport type { ArtifactSegment, ArtifactInput } from './segment.js';\nexport { deriveArtifactKind } from './kinds.js';\nexport { buildSafePreviewDocument, PREVIEW_CSP } from './preview-document.js';\nexport type { ArtifactRenderOptions, ArtifactPreviewBuilder, ArtifactBinary, ArtifactBinaryResolver, ArtifactsSetupOptions } from './options.js';\nexport type { AparteTool, AparteToolHandler, AparteToolCall, AparteToolResult } from '@aparte/core';\n"],"names":[],"mappings":";;;AAyBO,SAAS,eAAe,UAAiC,IAAI,SAAuB,oBAAgC;AACvH,QAAM,OAAO,mBAAmB,OAAO;AACvC,SAAO,aAAa,MAAM,eAAe;AACzC,SAAO,qBAAqB,KAAK,MAAM,EAAE,QAAQ,MAAM,IAAI;AAC3D,QAAM,MAAM,QAAQ,QAAQ,SAAY,eAAe,QAAQ;AAC/D,MAAI,IAAK,QAAO,oBAAoB,cAAc,GAAG,CAAC;AACtD,SAAO,MAAM;AACT,WAAO,eAAe,KAAK,IAAI;AAC/B,WAAO,uBAAuB,KAAK,IAAI;AACvC,QAAI,IAAK,QAAO,sBAAsB,GAAG;AAAA,EAC7C;AACJ;"}
|
package/dist/kinds.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map an artifact MIME type to a renderer kind — `'react'`, `'html'`, `'js'`, `'css'`,
|
|
3
|
+
* `'svg'`, `'json'`, `'markdown'`, `'csv'`, `'text'` — or `fallback` (default
|
|
4
|
+
* `'unknown'`) for anything else. Anthropic's `application/vnd.ant.*` namespace maps to
|
|
5
|
+
* its suffix; exact standard MIMEs are matched first, then a substring rescue for
|
|
6
|
+
* parameterised or vendor variants (`text/html; charset=utf-8`, `application/ld+json`).
|
|
7
|
+
*
|
|
8
|
+
* THE implementation, and the plugin's: it used to live in `@aparte/engine` and be
|
|
9
|
+
* re-exported by `@aparte/core`, because the engine's built-in `create_artifact` and
|
|
10
|
+
* core's parser both needed it. Neither does any more — an artifact is a plugin's
|
|
11
|
+
* convention, so the function that names its kinds is the plugin's too.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* deriveArtifactKind('application/vnd.ant.react') // 'react'
|
|
15
|
+
* deriveArtifactKind('text/html; charset=utf-8') // 'html'
|
|
16
|
+
* deriveArtifactKind('font/woff2', 'text') // 'text' (fallback)
|
|
17
|
+
*/
|
|
18
|
+
export declare function deriveArtifactKind(mimeType: string, fallback?: string): string;
|
|
19
|
+
//# sourceMappingURL=kinds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kinds.d.ts","sourceRoot":"","sources":["../src/kinds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,SAAY,GAAG,MAAM,CA0BjF"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the app decided at `setupArtifacts()`, read back by the card at render time.
|
|
3
|
+
*
|
|
4
|
+
* The card is resolved through core's ambient config (`contextConfig()`), not through
|
|
5
|
+
* a closure, so the settings live beside the config they were given for: one entry per
|
|
6
|
+
* `AparteConfig`, the global one by default. A chat with its own config that never
|
|
7
|
+
* called `setupArtifacts` reads the global settings, which is what its renderers do
|
|
8
|
+
* too.
|
|
9
|
+
*/
|
|
10
|
+
import { type AparteConfig } from '@aparte/core';
|
|
11
|
+
import type { ArtifactSegment } from './segment.js';
|
|
12
|
+
import type { ArtifactToolOptions } from './tool.js';
|
|
13
|
+
/** Builds the `srcdoc` of the sandboxed preview frame for a previewable kind. */
|
|
14
|
+
export type ArtifactPreviewBuilder = (kind: string, body: string, title: string) => string;
|
|
15
|
+
/** The bytes a binary artifact (pdf, xlsx, docx) resolved to. */
|
|
16
|
+
export interface ArtifactBinary {
|
|
17
|
+
/** The file. */
|
|
18
|
+
buffer: BlobPart;
|
|
19
|
+
/** Its MIME type, for the download. */
|
|
20
|
+
mime: string;
|
|
21
|
+
/** The name the download gets. */
|
|
22
|
+
filename: string;
|
|
23
|
+
/**
|
|
24
|
+
* Optional HTML rendering of the file (a spreadsheet as a table, a PDF's text),
|
|
25
|
+
* shown in the card's preview pane after sanitisation. Absent: the pane says so.
|
|
26
|
+
*/
|
|
27
|
+
previewHtml?: string | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Turn a binary artifact's source (the JS the model wrote to produce a workbook, a
|
|
31
|
+
* PDF, a document) into bytes. Core owns no sandbox and no file generator: this is the
|
|
32
|
+
* app's, and without it a binary artifact shows its source with no download and no
|
|
33
|
+
* preview — declared nowhere, offered nowhere (ratified decision #8).
|
|
34
|
+
*/
|
|
35
|
+
export type ArtifactBinaryResolver = (artifact: ArtifactSegment) => Promise<ArtifactBinary>;
|
|
36
|
+
export interface ArtifactRenderOptions {
|
|
37
|
+
/**
|
|
38
|
+
* The Preview tab for previewable kinds (html, react, svg, js, css). `true`
|
|
39
|
+
* (default) mounts a sandboxed frame on a gesture with the built-in document
|
|
40
|
+
* builder; a function replaces the builder; `false` offers no preview at all.
|
|
41
|
+
*/
|
|
42
|
+
preview?: boolean | ArtifactPreviewBuilder;
|
|
43
|
+
/** See {@link ArtifactBinaryResolver}. */
|
|
44
|
+
onBinary?: ArtifactBinaryResolver;
|
|
45
|
+
}
|
|
46
|
+
export declare function setRenderOptions(config: AparteConfig, options: ArtifactRenderOptions): void;
|
|
47
|
+
/** The options for this config, else the global config's, else the defaults. */
|
|
48
|
+
export declare function renderOptions(config: AparteConfig): ArtifactRenderOptions;
|
|
49
|
+
/** For tests and a teardown: forget what a config was told. */
|
|
50
|
+
export declare function clearRenderOptions(config: AparteConfig): void;
|
|
51
|
+
/**
|
|
52
|
+
* Everything `setupArtifacts()` accepts — the tool's options, the card's, and the tag.
|
|
53
|
+
*
|
|
54
|
+
* ONE declaration, here, because there are two `setupArtifacts()`: the browser barrel's
|
|
55
|
+
* and the node one. Each used to declare its own `ArtifactsSetupOptions`, and they were
|
|
56
|
+
* not the same shape — the node copy omitted `ArtifactRenderOptions`, so `preview` and
|
|
57
|
+
* `onBinary` were type errors against the SSR entry while being valid against the
|
|
58
|
+
* browser one. A consumer typing a shared setup object got a different contract
|
|
59
|
+
* depending on which condition resolved, from a name that reads as one thing.
|
|
60
|
+
*
|
|
61
|
+
* The server ignores `preview` and `onBinary` (it registers no renderer), and that is
|
|
62
|
+
* correct: the same options object is meant to be written once and passed on both
|
|
63
|
+
* sides. An option nobody reads there is inert, whereas a type error there is a wall.
|
|
64
|
+
*/
|
|
65
|
+
export interface ArtifactsSetupOptions extends ArtifactToolOptions, ArtifactRenderOptions {
|
|
66
|
+
/**
|
|
67
|
+
* The tag recognised in the prose — `<artifact …>…</artifact>` by default. `false`
|
|
68
|
+
* registers no grammar: only the tool produces artifacts then.
|
|
69
|
+
*/
|
|
70
|
+
tag?: string | false;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAErD,iFAAiF;AACjF,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;AAE3F,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC3B,gBAAgB;IAChB,MAAM,EAAE,QAAQ,CAAC;IACjB,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,QAAQ,EAAE,eAAe,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AAE5F,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC3C,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CACrC;AAID,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAE3F;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,qBAAqB,CAEzE;AAED,+DAA+D;AAC/D,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAE7D;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,qBAAsB,SAAQ,mBAAmB,EAAE,qBAAqB;IACrF;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACxB"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { escapeAttr, escapeHtml } from "@aparte/core";
|
|
2
|
+
const ARTIFACT_SYSTEM_PROMPT = "When the user asks for a document, a page, a component, a diagram or a file — anything they will keep, edit or run rather than read once — produce it with the create_artifact tool, with a MIME type that names what it is, rather than pasting it into your reply. Keep your reply short: say what you made, not what it contains.";
|
|
3
|
+
function createArtifactTool(options = {}) {
|
|
4
|
+
const tool = {
|
|
5
|
+
name: options.name ?? "create_artifact",
|
|
6
|
+
description: "Create a self-contained document the user can keep, edit, run or download: a page, a component, a script, a stylesheet, an SVG, a JSON document, a Markdown note, a CSV. Give it a MIME type (text/html, application/vnd.ant.react, image/svg+xml, text/markdown, application/json, text/csv, application/javascript, text/css) and a short title.",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
mimeType: { type: "string", description: "The MIME type of the document, e.g. text/html or application/vnd.ant.react." },
|
|
11
|
+
title: { type: "string", description: "A short human title for the document." },
|
|
12
|
+
content: { type: "string", description: "The complete document." }
|
|
13
|
+
},
|
|
14
|
+
required: ["mimeType", "content"]
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
if (options.systemPrompt !== false) tool.systemPrompt = options.systemPrompt ?? ARTIFACT_SYSTEM_PROMPT;
|
|
18
|
+
return tool;
|
|
19
|
+
}
|
|
20
|
+
async function artifactHandler(call) {
|
|
21
|
+
const input = call.input ?? {};
|
|
22
|
+
const mimeType = typeof input.mimeType === "string" && input.mimeType.trim() ? input.mimeType.trim() : "text/plain";
|
|
23
|
+
const title = typeof input.title === "string" ? input.title.trim() : "";
|
|
24
|
+
const content = typeof input.content === "string" ? input.content : "";
|
|
25
|
+
const structured = { mimeType, content, ...title ? { title } : {} };
|
|
26
|
+
return {
|
|
27
|
+
toolCallId: call.id,
|
|
28
|
+
content: `Artifact created${title ? `: ${title}` : ""} (${mimeType}, ${content.length} characters). It is shown to the user; do not repeat its content.`,
|
|
29
|
+
structuredContent: structured
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function deriveArtifactKind(mimeType, fallback = "unknown") {
|
|
33
|
+
const m = (mimeType || "").toLowerCase().trim();
|
|
34
|
+
const ant = m.match(/^application\/vnd\.ant\.([a-z0-9-]+)/);
|
|
35
|
+
if (ant) return ant[1];
|
|
36
|
+
if (m === "text/html" || m === "application/xhtml+xml") return "html";
|
|
37
|
+
if (m === "application/javascript" || m === "text/javascript") return "js";
|
|
38
|
+
if (m === "text/css") return "css";
|
|
39
|
+
if (m === "image/svg+xml") return "svg";
|
|
40
|
+
if (m === "application/json") return "json";
|
|
41
|
+
if (m === "text/markdown") return "markdown";
|
|
42
|
+
if (m === "text/csv") return "csv";
|
|
43
|
+
if (m === "text/plain") return "text";
|
|
44
|
+
if (m === "application/pdf") return "pdf";
|
|
45
|
+
if (m === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") return "xlsx";
|
|
46
|
+
if (m === "application/vnd.openxmlformats-officedocument.wordprocessingml.document") return "docx";
|
|
47
|
+
if (m.includes("react")) return "react";
|
|
48
|
+
if (m.includes("html")) return "html";
|
|
49
|
+
if (m.includes("javascript")) return "js";
|
|
50
|
+
if (m.includes("css")) return "css";
|
|
51
|
+
if (m.includes("svg")) return "svg";
|
|
52
|
+
if (m.includes("json")) return "json";
|
|
53
|
+
if (m.includes("csv")) return "csv";
|
|
54
|
+
if (m.includes("markdown")) return "markdown";
|
|
55
|
+
return fallback;
|
|
56
|
+
}
|
|
57
|
+
const ARTIFACT_SEGMENT_TYPE = "artifact";
|
|
58
|
+
const ARTIFACT_TAG = "artifact";
|
|
59
|
+
function artifactSegment(id, input) {
|
|
60
|
+
const mimeType = typeof input.mimeType === "string" && input.mimeType.trim() ? input.mimeType.trim() : "text/plain";
|
|
61
|
+
return {
|
|
62
|
+
id,
|
|
63
|
+
type: ARTIFACT_SEGMENT_TYPE,
|
|
64
|
+
mimeType,
|
|
65
|
+
artifactType: deriveArtifactKind(mimeType),
|
|
66
|
+
title: typeof input.title === "string" && input.title.trim() ? input.title.trim() : void 0,
|
|
67
|
+
content: typeof input.content === "string" ? input.content : ""
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function artifactBlock(tag = ARTIFACT_TAG) {
|
|
71
|
+
return {
|
|
72
|
+
tag,
|
|
73
|
+
toSegment: ({ attrs, id }) => artifactSegment(id, {
|
|
74
|
+
mimeType: attrs["mimeType"] ?? attrs["mimetype"] ?? attrs["type"],
|
|
75
|
+
title: attrs["title"],
|
|
76
|
+
content: ""
|
|
77
|
+
})
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function artifactFromToolCall(segment) {
|
|
81
|
+
const structured = segment.structuredResult;
|
|
82
|
+
const source = structured && typeof structured === "object" ? structured : segment.toolCall?.input ?? {};
|
|
83
|
+
const art = artifactSegment(segment.id, source);
|
|
84
|
+
art.isStreaming = false;
|
|
85
|
+
return art;
|
|
86
|
+
}
|
|
87
|
+
const PREVIEW_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:";
|
|
88
|
+
function withMetaCsp(doc) {
|
|
89
|
+
const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttr(PREVIEW_CSP)}">`;
|
|
90
|
+
const doctype = doc.match(/^\s*<!doctype[^>]*>/i);
|
|
91
|
+
const at = doctype ? doctype[0].length : 0;
|
|
92
|
+
return `${doc.slice(0, at)}<head>${meta}</head>${doc.slice(at)}`;
|
|
93
|
+
}
|
|
94
|
+
function buildSafePreviewDocument(kind, body, title) {
|
|
95
|
+
return withMetaCsp(buildPreviewBody(kind, body, title));
|
|
96
|
+
}
|
|
97
|
+
function buildPreviewBody(kind, body, title) {
|
|
98
|
+
switch (kind) {
|
|
99
|
+
case "html": {
|
|
100
|
+
if (startsWithDoctype(body)) return body;
|
|
101
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>${escapeAttr(title)}</title></head><body>${body}</body></html>`;
|
|
102
|
+
}
|
|
103
|
+
// An SVG with only a `viewBox` — the recommended, responsive form, and the one
|
|
104
|
+
// a model writes most often — has NO intrinsic size. As a flex item its cross
|
|
105
|
+
// size then collapses to zero and the frame is blank: the preview worked for an
|
|
106
|
+
// SVG that declared `width`/`height` and silently showed nothing for the
|
|
107
|
+
// idiomatic one. Reported from the landing the moment its demo payload became a
|
|
108
|
+
// real file instead of a hand-written chart with dimensions on it.
|
|
109
|
+
//
|
|
110
|
+
// The rule is narrowed by attribute selector rather than applied to every SVG,
|
|
111
|
+
// so an SVG that DOES state its size keeps the size it asked for.
|
|
112
|
+
case "svg":
|
|
113
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
|
|
114
|
+
<style>html,body{margin:0;height:100%;display:flex;align-items:center;justify-content:center;background:#fff}svg{max-width:90%;max-height:90%}svg:not([width]):not([height]){width:90%;height:90%}</style>
|
|
115
|
+
</head><body>${body}</body></html>`;
|
|
116
|
+
case "js": {
|
|
117
|
+
const safeBody = escapeClosingScriptTag(body);
|
|
118
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
|
|
119
|
+
<style>body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;padding:1rem;background:#fff;color:#0f172a}</style>
|
|
120
|
+
</head><body><div id="root"></div><script>
|
|
121
|
+
try { ${safeBody}
|
|
122
|
+
} catch (e) { document.getElementById('root').innerHTML = '<pre style="color:#b91c1c">' + (e && e.stack || e) + '</pre>'; }
|
|
123
|
+
<\/script></body></html>`;
|
|
124
|
+
}
|
|
125
|
+
case "css":
|
|
126
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
|
|
127
|
+
<style>${body}</style></head><body>
|
|
128
|
+
<div class="demo">
|
|
129
|
+
<h1>Heading</h1>
|
|
130
|
+
<p>Paragraph with a <a href="#">link</a> and <strong>strong</strong> text.</p>
|
|
131
|
+
<button>Button</button>
|
|
132
|
+
<input placeholder="Input"/>
|
|
133
|
+
<ul><li>One</li><li>Two</li><li>Three</li></ul>
|
|
134
|
+
</div></body></html>`;
|
|
135
|
+
default:
|
|
136
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
|
|
137
|
+
<style>body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;padding:1rem;background:#fff;color:#0f172a}pre{white-space:pre-wrap;word-break:break-word;margin:0}</style>
|
|
138
|
+
</head><body><pre>${escapeHtml(body)}</pre></body></html>`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function startsWithDoctype(s) {
|
|
142
|
+
let i = 0;
|
|
143
|
+
while (i < s.length && (s[i] === " " || s[i] === " " || s[i] === "\n" || s[i] === "\r")) i++;
|
|
144
|
+
const probe = s.slice(i, i + 9).toLowerCase();
|
|
145
|
+
return probe === "<!doctype";
|
|
146
|
+
}
|
|
147
|
+
function escapeClosingScriptTag(body) {
|
|
148
|
+
let out = "";
|
|
149
|
+
let i = 0;
|
|
150
|
+
while (i < body.length) {
|
|
151
|
+
if (body[i] === "<" && body.slice(i, i + 8).toLowerCase() === "<\/script") {
|
|
152
|
+
const next = body[i + 8];
|
|
153
|
+
if (next === void 0 || next === "/" || next === ">" || /\s/.test(next)) {
|
|
154
|
+
out += "<\\/script";
|
|
155
|
+
i += 8;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
out += body[i];
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
ARTIFACT_TAG as A,
|
|
166
|
+
PREVIEW_CSP as P,
|
|
167
|
+
artifactHandler as a,
|
|
168
|
+
artifactBlock as b,
|
|
169
|
+
createArtifactTool as c,
|
|
170
|
+
ARTIFACT_SEGMENT_TYPE as d,
|
|
171
|
+
ARTIFACT_SYSTEM_PROMPT as e,
|
|
172
|
+
artifactFromToolCall as f,
|
|
173
|
+
artifactSegment as g,
|
|
174
|
+
buildSafePreviewDocument as h,
|
|
175
|
+
deriveArtifactKind as i
|
|
176
|
+
};
|
|
177
|
+
//# sourceMappingURL=preview-document-xP-Ne-F0.js.map
|