Artifacts: side panel with live preview, targeted edits, and version history (#2303)
* Add Artifacts: side panel with live preview, targeted edits, and version history
Claude-style artifacts for chat-ui. The model emits <artifact> tags (taught
via an injected system prompt, ENABLE_ARTIFACTS=false to opt out); the client
renders them as cards in chat and a dedicated side panel next to the
conversation.
- Streaming-safe parser (src/lib/utils/artifacts.ts): partially received
tags stay hidden, open artifacts grow token by token, and every operation
folds into a versioned registry derived from the visible message path.
- Targeted edits: type="update" blocks carry old_str/new_str find/replace
pairs applied to the latest version, so small changes don't re-emit the
whole artifact; bigger changes rewrite under the same identifier. Each
create/update/rewrite becomes a navigable version (v1, v2, ...).
- Panel UI: Preview/Code tabs, copy/download/fullscreen actions, version
stepper, drag-to-resize on desktop, fullscreen overlay on mobile,
auto-open while streaming (code view pinned to bottom, preview disabled
until complete), runtime-error capture with one-click "ask to fix".
- Live preview types: html and svg in a sandboxed iframe (reusing the
hardened srcdoc builder extracted from HtmlPreviewModal), react via
Babel standalone + React 18 + Tailwind Play CDN, mermaid via CDN ESM,
markdown rendered natively; code gets syntax highlighting only.
- ChatMessage strips artifact blocks from prose (and clipboard copies) and
renders cards inline; ChatWindow hosts the panel beside the chat column
with pointer-events handling that keeps the z-[-1] chat area clickable.
Verified end-to-end against a mock OpenAI endpoint with Playwright:
create + streaming auto-open, live preview interactivity, old_str/new_str
update to v2, version navigation, Esc/close/reopen, download filename,
and the mobile overlay.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Harden artifacts against real-model output: doubled brackets, fuzzy edits, think rehearsals
Live testing on HuggingChat (Kimi-K2.6 via the agentic route) surfaced four
failure modes the mock-based tests missed:
- Kimi consistently emits doubled opening brackets (<<artifact, <<old_str).
The old_str/new_str pair regex stopped matching, so "make it green" became
three silent no-op versions labeled "Edited" while the button stayed blue,
with stray < characters leaking into the prose. All tag matchers now
consume a run of brackets, and a closed update that parses zero pairs is
surfaced as a failed edit instead of silently doing nothing.
- Models mis-copy indentation in old_str (observed live: the model burned a
whole thinking block counting spaces, failed anyway, then retried).
applyArtifactUpdate now falls back to whitespace-tolerant matching, exact
match first.
- Models rehearse artifact tags inside <think> blocks; the registry parsed
them as real operations, creating phantom versions. collectArtifacts now
strips reasoning before parsing, mirroring ChatMessage.
- One update block per find/replace pair cluttered the chat with a card per
pair. Consecutive ops on the same artifact now collapse into the last
card, and the system prompt asks for a single update block per reply.
Also: only the card whose version the panel displays gets the active ring,
and the version stepper label spacing is fixed.
Verified end to end in Chrome against the live dev server: the previously
broken conversation now renders one clean card and applies the green edit;
create/rewrite/update cycles, mermaid and react artifacts, streaming
auto-open, version navigation and failed-edit warnings all checked against
real Kimi-K2.6 generations.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Expose artifacts as a global user setting, enabled by default
Adds an Artifacts toggle to Application Settings (global, not per-model).
The setting gates the system prompt injection server-side, combined with
the instance-wide ENABLE_ARTIFACTS env flag; existing artifacts in old
conversations keep rendering even when the toggle is off.
- Settings type + DEFAULT_SETTINGS: artifactsEnabled (default true)
- Both settings endpoints (zod schemas) and the GET response expose it
- TextGenerationContext carries the user value from the conversation
endpoint into textGeneration, where it gates injectArtifactsPrompt
- Application Settings page gets the Switch, styled like its neighbors
Verified live in Chrome against real models: toggle off, "draw a blue
button in html" returns a plain markdown code block (0 artifact cards,
no panel) across two separate conversations; toggle back on, the same
prompt produces an artifact card with the side panel streaming, and the
model's reasoning explicitly references the artifact instructions again.
Setting persistence confirmed through the API between page loads.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Polish artifact panel and cards from live design feedback
- Panel width: cap at 896px (was 1024) and never wider than
calc(100dvw - 700px), so the chat column stays readable on large
screens; double-clicking the resize handle resets to the default width.
- Code tab now inherits the exact .prose pre styling chat code blocks
use (white + Atom One Light in light mode, gray-900 + Atom One Dark in
dark mode) instead of forcing a dark background with light-theme
tokens. The pre's own 0.5px border is dropped (!border-0) since the
panel provides its frame, fixing the doubled borders; the streaming
fade gradient follows the light/dark background.
- Artifact cards: drop the chevron icon, dim the dark-mode active ring
to 30% opacity, and remove transition-colors so theme switches don't
animate the card.
- Panel header gets z-10 so the copy button's "Copied" tooltip isn't
painted over by the body.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Make artifact panel resize proportional: each pane keeps 20%, drag up to 80%
Replace the fixed pixel caps (896px max, viewport-minus-700 guard) with
proportional bounds on the chat/panel split: the panel can be dragged
anywhere between 20% and 80% of the shared area, so the same floor
protects both panes symmetrically. The drag clamp uses the live container
width, so the handle tracks the pointer 1:1 with no dead zone at the
bounds. Stored width keeps loose absolute sanity bounds only.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Give artifact cards more vertical room when surrounded by text
Cards keep the tight my-2 at the start/end of a message, but get 1rem of
separation on the side that touches prose ([.prose+&] above, has-[+.prose]
below), matching the spacing pattern used for tool blocks.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Add word-wrap toggle to the code view; drop "Click to open" from cards
The wrap button sits with the other header actions and only shows on the
Code tab; the preference persists in localStorage. Card subtitles now read
just "Web app ยท v2" since the whole card is clickable anyway.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Default artifact panel to 65% of the split
When the user hasn't resized, the panel takes 65% of the chat/panel area
(stored width remains null until a drag persists one). Double-clicking the
resize handle now returns to this proportional default instead of a fixed
pixel width.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Default artifact panel split to 70/30
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Stop the white flash when opening artifact previews in dark mode
The preview iframe was hard-coded bg-white, which flashed before the
embedded document painted its own (often dark) background. The iframe
backing now follows the panel theme. SVG previews get an explicit white
document canvas since they're usually drawn for light backgrounds.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Move the share button into the chat column so the artifact panel doesn't cover it
It was absolutely positioned at the layout's top right, which the panel
overlaps when open. It now anchors to the top right of the chat column,
staying visible (and correctly placed) whether or not artifacts are open.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Give artifact cards more right padding than left
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Match artifact panel borders to the nav sidebar color (gray-100)
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Make artifacts opt-in per model via supportsArtifacts in MODELS overrides
Replaces the global artifactsEnabled user setting and the ENABLE_ARTIFACTS
env kill switch: the artifacts system prompt is now injected only when the
conversation's model carries supportsArtifacts: true, configured per model
through the MODELS JSON5 overrides (same mechanism as supportsTools /
supportsReasoning) and off by default.
- modelConfig gains supportsArtifacts (default false); overridable per entry
- textGeneration gates injectArtifactsPrompt on ctx.model.supportsArtifacts
- Removed: the Application Settings toggle, artifactsEnabled across Settings
types / both settings endpoints / store / layout types / generation ctx,
and the ENABLE_ARTIFACTS config key (.env documents the new mechanism)
- Artifact rendering (cards/panel) is untouched so old conversations keep
displaying their artifacts; stale artifactsEnabled values in settings
documents are simply ignored
For Omni/router conversations the flag applies to the selected model entity
(e.g. flag "omni" itself): the prompt is built before routing, so routed
targets can't be gated individually.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Expose artifacts on the model settings page with a per-model user override
Adds a fourth capability row ("Artifacts") to /settings/<model> next to
Tool calling / Multimodal / Reasoning effort, backed by a new
artifactsOverrides record in user settings (same plumbing as
toolsOverrides). The override wins over the model's supportsArtifacts
config flag in both directions: users can enable artifacts on an
unflagged model or disable them on a flagged one.
Unlike the provider-determined capabilities, the switch stays editable
on HuggingChat and the override is not stripped by the settings
endpoints. supportsArtifacts is now serialized to the client (Model
type, both models endpoints, GETModelsResponse) so the toggle reflects
the config default.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Add BETA badge to the Artifacts row on the model settings page
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Drop tracking-wide from the BETA badge
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Enable artifacts by default on selected models (omni + 7) in dev/prod configs
Sets supportsArtifacts: true on omni, GLM-5.1, MiniMax-M2.7, Kimi-K2.6,
DeepSeek-V4-Pro, Nemotron-3-Ultra, Qwen3.6-35B-A3B and Qwen3.6-27B in
chart/env/{dev,prod}.yaml. The router alias (omni) is built after the
MODELS overrides merge, so models.ts now applies a matching override
entry to the alias too, letting it carry capability flags like any
other model.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Float the code wrap toggle over the code view so it doesn't shift the header tabs
* Fix artifact edge cases found in live browser testing
- Collapse doubled brackets on an artifact's leading content tag
(Kimi-K2.6 emits "<<svg ..."), which rendered a stray "<" in the
preview and polluted the code view / downloads. The fuzzy old_str
matcher is bracket-run tolerant the same way, so updates quoting the
raw "<<svg" still match the normalized content.
- Clear the panel's highlighted code when the target version disappears,
instead of holding the previous artifact's code while a new artifact's
first tokens stream in.
- Debounce the close-on-missing-artifact effect (300ms) so transient
registry gaps during end-of-generation invalidation don't close the
panel; real removals (branch switch, edits) still close it.
- Give the React preview scaffold the same white canvas as SVG/Mermaid:
unstyled components were invisible (default black text) on the dark
panel background in dark mode.
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
* Simplify artifacts code: flatten fallbacks, drop redundant casts and IIFE
Behavior-preserving cleanups across the artifacts feature:
- ChatMessage: replace the nested-ternary each-key with the equivalent
`${unit.kind}-${unitIndex}` template
- ArtifactCard: flatten the title fallback chain (create ops always carry
a title, so the kind branch was redundant) and rename the onclick
helper to openInPanel so it no longer shadows the DOM attribute name
- artifacts.ts: single-pass map in stripArtifacts (the filter only
existed for type narrowing the map redid) and a named fileExtension
helper instead of an IIFE in artifactFileName
- models.ts: use the file's existing `void id` convention in the alias
override merge instead of an eslint-disable comment
- settings page: drop the double cast, BackendModel already types
supportsArtifacts (matches the supportsReasoning getter style)
- artifactsContext: explicit void return type on setArtifactsContext
https://claude.ai/code/session_01DaEvzheeeFNXEd66t4135U
---------
Co-authored-by: Claude <noreply@anthropic.com>