# Artifact

Artifact [#artifact]

The **Artifact** is a Claude-style docked side-panel/canvas: a vertical surface with a header (title + description), an action toolbar (copy / download / refresh / close), and a scrollable content region. It renders generated output — code, a markdown doc, an SVG, raw HTML — *beside* the chat instead of inline in the message stream.

It is composed from sub-parts: `Artifact` (root) → `ArtifactHeader` → `ArtifactTitle` / `ArtifactDescription` and `ArtifactActions` → `ArtifactAction` / `ArtifactClose`, plus `ArtifactContent` for the scrollable body.

**Bring your own content.** The shell is purely presentational — it renders whatever content you pass in, from any backend. A local language model is a recommended (optional) producer: drive it with [`useGenerateText()`](https://localmode.dev/docs/react) / [`useGenerateObject()`](https://localmode.dev/docs/react) and there is &#x2A;*no server, no sandbox, no remote execution.** Copy writes to the clipboard, download produces a real `Blob`, and refresh re-runs your generation — all in-browser.

**When to use it:** show a generated file/document in a dedicated canvas that the user can copy, download, or regenerate, separate from the conversational stream.

Preview [#preview]

```tsx
'use client';

/**
 * @file artifact-demo.tsx
 * @description Demo for the Artifact docked-canvas shell, used by the docs live
 * preview. Drives the canvas content from a REAL local `useGenerateText` run
 * (no mock): click "Generate" to download/load a small local model and render
 * its output in the docked panel. The copy / download / refresh / close toolbar
 * actions all operate entirely client-side.
 */

import { useState } from 'react';
import { useGenerateText } from '@localmode/react';
import { transformers } from '@localmode/transformers';
import {
  Artifact,
  ArtifactHeader,
  ArtifactTitle,
  ArtifactDescription,
  ArtifactActions,
  ArtifactAction,
  ArtifactClose,
  ArtifactContent,
} from '@/components/artifact';

const PROMPT =
  'Write a short TypeScript function `slugify(input: string): string`. Reply with only the code.';

export default function ArtifactDemo() {
  const [open, setOpen] = useState(true);
  const model = transformers.languageModel('onnx-community/granite-4.0-350m-ONNX-web');
  const { data, isLoading, error, execute } = useGenerateText({
    model,
    maxTokens: 200,
  });

  const content = data?.text ?? '';

  return (
    <div className="flex w-full min-w-0 max-w-md flex-col gap-3">
      <div className="flex items-center gap-2">
        <button
          type="button"
          onClick={() => execute(PROMPT)}
          disabled={isLoading}
          className="inline-flex h-8 items-center rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50"
        >
          {isLoading ? 'Generating…' : 'Generate'}
        </button>
        {!open && (
          <button
            type="button"
            onClick={() => setOpen(true)}
            className="inline-flex h-8 items-center rounded-md border border-border px-3 text-sm focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
          >
            Reopen canvas
          </button>
        )}
      </div>

      <Artifact open={open} className="h-80 w-full min-w-0 max-w-md">
        <ArtifactHeader>
          <div className="min-w-0">
            <ArtifactTitle>slugify.ts</ArtifactTitle>
            <ArtifactDescription>
              Generated by a local model (transformers · Granite 4.0 350M)
            </ArtifactDescription>
          </div>
          <ArtifactActions>
            <ArtifactAction content={content} label="Copy" />
            <ArtifactAction
              content={content}
              fileName="slugify.ts"
              label="Download"
            />
            <ArtifactAction onClick={() => execute(PROMPT)} label="Refresh" />
            <ArtifactClose onClick={() => setOpen(false)} />
          </ArtifactActions>
        </ArtifactHeader>
        <ArtifactContent>
          {error ? (
            <p className="text-sm text-destructive">{error.message}</p>
          ) : content ? (
            <pre>{content}</pre>
          ) : (
            <p className="text-sm text-muted-foreground">
              {isLoading
                ? 'Loading the local model and generating…'
                : 'Click Generate to fill the canvas from a local model.'}
            </p>
          )}
        </ArtifactContent>
      </Artifact>
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/artifacts/artifact
```

Dependencies [#dependencies]

* **Data source:** the canvas renders whatever content you pass it — works with any backend. Recommended LocalMode producers: `useGenerateText()` / `useGenerateObject()` from `@localmode/react` (optional).
* `@localmode/ui/lib/browser-utils` — the copy-owned `downloadBlob` helper for artifact export (installed automatically as a registry dependency)
* `lucide-react` — toolbar icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)

Files installed [#files-installed]

* `artifact.tsx` — the canvas shell and all sub-parts
* `lib/browser-utils.ts` — the copy-owned file helpers (if not already present)
* `lib/utils.ts` — the `cn()` helper (if not already present)

Props [#props]

**Artifact**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | `true` | When false, the canvas is hidden (returns `null`). Lets the host toggle the docked panel open/closed. Defaults to `true`. |

ArtifactAction [#artifactaction]

**ArtifactAction**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | — | **Required.** Accessible label for the icon button (also used as the tooltip). |
| `icon` | `ReactNode` | — | Icon to render. Defaults are wired by convention: pass `content` to make a copy/download action, or `onClick` for a custom (e.g. refresh) action. |
| `content` | `string` | — | String content the action operates on. When provided without `fileName`, clicking copies it to the clipboard. When provided with `fileName`, clicking downloads it as a real `Blob`. Both happen entirely client-side. |
| `fileName` | `string` | — | When set (with `content`), the action downloads `content` as this file. |
| `mimeType` | `string` | `"text/plain"` | MIME type for the downloaded blob. |

ArtifactClose [#artifactclose]

**ArtifactClose**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | `"Close"` | Accessible label. |

Examples [#examples]

Drive the canvas from a local model [#drive-the-canvas-from-a-local-model]

```tsx
import { useGenerateText } from '@localmode/react';
import { transformers } from '@localmode/transformers';
import {
  Artifact,
  ArtifactHeader,
  ArtifactTitle,
  ArtifactDescription,
  ArtifactActions,
  ArtifactAction,
  ArtifactClose,
  ArtifactContent,
} from '@/components/artifacts/artifact';

export function GeneratedCodeCanvas() {
  const model = transformers.languageModel('onnx-community/granite-4.0-350m-ONNX-web');
  const { data, isLoading, execute } = useGenerateText({ model, maxTokens: 200 });
  const code = data?.text ?? '';

  return (
    <Artifact className="h-96 w-[28rem]">
      <ArtifactHeader>
        <div>
          <ArtifactTitle>generated.ts</ArtifactTitle>
          <ArtifactDescription>From a local model run</ArtifactDescription>
        </div>
        <ArtifactActions>
          {/* content only → copy to clipboard */}
          <ArtifactAction content={code} label="Copy" />
          {/* content + fileName → download a Blob */}
          <ArtifactAction content={code} fileName="generated.ts" label="Download" />
          {/* onClick only → custom action (re-run) */}
          <ArtifactAction onClick={() => execute('Write a slugify function')} label="Refresh" />
          <ArtifactClose onClick={close} />
        </ArtifactActions>
      </ArtifactHeader>
      <ArtifactContent>
        {isLoading ? <p>Generating…</p> : <pre>{code}</pre>}
      </ArtifactContent>
    </Artifact>
  );
}
```

Toggle the panel open/closed [#toggle-the-panel-openclosed]

```tsx
<Artifact open={isOpen}>{/* … */}</Artifact>
```

When `open` is `false`, the canvas renders `null`. Wire `ArtifactClose`'s `onClick` to your state setter to hide it.

Toolbar action behaviors [#toolbar-action-behaviors]

`ArtifactAction` picks a built-in behavior from its props — all client-side:

| Props                    | Behavior                                                  |
| ------------------------ | --------------------------------------------------------- |
| `content` only           | Copy `content` to the clipboard (shows a transient check) |
| `content` + `fileName`   | Download `content` as a `Blob` named `fileName`           |
| `onClick` (no `content`) | Run your callback (e.g. refresh re-runs generation)       |

Local-first boundary [#local-first-boundary]

This is a **local canvas**. It does not embed a Sandbox, a Web Preview iframe, a Terminal, Test Results, a Commit/Package-Info panel, a File Tree, or Env Vars — those members of external artifact catalogs require remote code execution, deploys, or CI/VCS tooling and are deliberately out of scope (no server / no API key / no telemetry). See [Out of scope](#out-of-scope) below.

Out of scope [#out-of-scope]

The following server/IDE-centric artifact members from external AI-UI libraries are **refused** in LocalMode because they reintroduce a server/cloud dependency:

* **Sandbox** / **Terminal** — remote/cloud code execution.
* **Web Preview** — deployed-app iframe.
* **Test Results** / **Stack Trace** / **Commit** / **Package Info** / **File Tree** / **Env Vars** — CI/VCS/IDE dev tooling.

A live **JSXPreview** (generative-UI: rendering model-emitted JSX/JSON) is **deferred, not refused** — it is local-first-capable and high-interest, but rendering arbitrary model-emitted UI carries arbitrary-code-execution risk and needs a safe allowlist/sandbox design first. Tracked as a future change.

Customization [#customization]

The shell is styled entirely with shadcn/ui CSS-variable utilities (`bg-card`, `text-card-foreground`, `border-border`, `bg-primary`, `hover:bg-accent`), so it inherits your theme. Because you own the file, swap the `lucide-react` icons, adjust the toolbar layout, or render markdown/SVG/HTML inside `ArtifactContent` however you like.