Claude
Skills
Sign in
Back

ai-artifacts

Included with Lifetime
$97 forever

Rich artifact panel for AI chat — code, HTML, Mermaid diagrams, tables, and markdown render in a resizable side panel. Use this skill when the user says "add artifacts", "add code preview", "add side panel", "artifact panel", or "rich output".

Image & Video

What this skill does


# AI Artifacts

Experience layer that lets the AI create rich artifacts (syntax-highlighted code, sandboxed HTML, Mermaid diagrams, markdown, and tables) rendered in a resizable side panel alongside the chat.

Artifacts are created via tool calls (`createArtifact`, `updateArtifact`) and stored in React state scoped to the current session. Clicking an artifact reference in a chat message opens the panel and scrolls to that artifact.

## Prerequisites

- Next.js app with `src/` directory and App Router
- `ai-core` skill installed (provides `getModel()`)
- `ai-chat` skill installed (provides chat UI, route pipeline, message renderer)
- `ai-tools` skill installed (provides tool calling framework and `tool-invocation` rendering)

## Installation

```bash
bun add mermaid
```

## What Gets Created

```
src/
├── lib/
│   └── ai/
│       └── tools/
│           └── artifact.ts            # createArtifact + updateArtifact tool definitions
└── components/
    └── ai/
        ├── artifact-panel.tsx         # Resizable right-side panel with artifact list
        └── artifact-renderers.tsx     # Renderers: code, HTML iframe, Mermaid, markdown, table
```

## What Gets Modified

```
src/
├── app/
│   └── (app)/
│       └── chat/
│           └── page.tsx               # Split layout — chat left, artifact panel right
└── components/
    └── ai/
        └── message.tsx                # Artifact refs as clickable cards inside tool-invocation case
```

## Setup Steps

### Step 1: Create `src/lib/ai/tools/artifact.ts`

```typescript
import { tool } from "ai";
import { z } from "zod";

export const artifactTypeSchema = z.enum([
  "code",
  "html",
  "mermaid",
  "table",
  "markdown",
]);

export type ArtifactType = z.infer<typeof artifactTypeSchema>;

export type Artifact = {
  id: string;
  type: ArtifactType;
  title: string;
  content: string;
  language?: string;
  versions: string[];
  createdAt: Date;
};

export const createArtifact = tool({
  description:
    "Create a rich artifact (code, HTML page, Mermaid diagram, table, or markdown document) that renders in the side panel. Use this for any substantial content the user might want to copy, preview, or iterate on.",
  inputSchema: z.object({
    type: artifactTypeSchema.describe(
      "The artifact type: 'code' for source code with syntax highlighting, 'html' for rendered HTML pages, 'mermaid' for diagrams, 'table' for structured data, 'markdown' for formatted documents"
    ),
    title: z
      .string()
      .describe("A short descriptive title for the artifact"),
    content: z
      .string()
      .describe("The full content of the artifact"),
    language: z
      .string()
      .optional()
      .describe(
        "Programming language for syntax highlighting (only used when type is 'code'). Examples: 'typescript', 'python', 'rust', 'sql'"
      ),
  }),
  execute: async ({ type, title, content, language }) => {
    const id = crypto.randomUUID();
    return {
      id,
      type,
      title,
      content,
      language,
      _artifact: true,
    };
  },
});

export const updateArtifact = tool({
  description:
    "Update an existing artifact with new content. Creates a new version so the user can see the change. Use this when the user asks to modify, fix, or improve an existing artifact.",
  inputSchema: z.object({
    id: z
      .string()
      .describe("The ID of the artifact to update"),
    content: z
      .string()
      .describe("The complete updated content (replaces the previous version)"),
    title: z
      .string()
      .optional()
      .describe("Optional new title for the artifact"),
  }),
  execute: async ({ id, content, title }) => {
    return {
      id,
      content,
      title,
      _artifact: true,
      _update: true,
    };
  },
});

export const artifactTools = {
  createArtifact,
  updateArtifact,
} as const;
```

### Step 2: Create `src/components/ai/artifact-renderers.tsx`

```tsx
"use client";

import { useEffect, useRef, useState } from "react";
import dynamic from "next/dynamic";
import { CodeBlock } from "@/components/ai/code-block";
import { Markdown } from "@/components/ai/markdown";

type ArtifactType = "code" | "html" | "mermaid" | "table" | "markdown";

type RendererProps = {
  content: string;
  language?: string;
};

function CodeRenderer({ content, language }: RendererProps) {
  return (
    <div className="overflow-auto rounded-lg border">
      <CodeBlock code={content} language={language ?? "text"} />
    </div>
  );
}

function HtmlRenderer({ content }: RendererProps) {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const [height, setHeight] = useState(400);

  useEffect(() => {
    const iframe = iframeRef.current;
    if (!iframe) return;

    const doc = iframe.contentDocument;
    if (!doc) return;

    doc.open();
    doc.write(content);
    doc.close();

    const resizeObserver = new ResizeObserver(() => {
      const body = doc.body;
      if (body) {
        setHeight(Math.min(body.scrollHeight + 20, 800));
      }
    });

    if (doc.body) {
      resizeObserver.observe(doc.body);
    }

    return () => resizeObserver.disconnect();
  }, [content]);

  return (
    <iframe
      ref={iframeRef}
      title="HTML Preview"
      sandbox="allow-scripts allow-same-origin"
      className="w-full rounded-lg border bg-white"
      style={{ height }}
    />
  );
}

function MermaidRenderer({ content }: RendererProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [svg, setSvg] = useState<string>("");
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function renderDiagram() {
      try {
        const mermaid = (await import("mermaid")).default;
        mermaid.initialize({
          startOnLoad: false,
          theme: "default",
          securityLevel: "loose",
        });
        const id = `mermaid-${crypto.randomUUID()}`;
        const { svg: rendered } = await mermaid.render(id, content);
        if (!cancelled) {
          setSvg(rendered);
          setError(null);
        }
      } catch (err) {
        if (!cancelled) {
          setError(
            err instanceof Error ? err.message : "Failed to render diagram"
          );
        }
      }
    }

    renderDiagram();
    return () => {
      cancelled = true;
    };
  }, [content]);

  if (error) {
    return (
      <div className="rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700">
        <p className="font-medium">Mermaid rendering error</p>
        <p className="mt-1">{error}</p>
        <pre className="mt-2 overflow-auto rounded bg-red-100 p-2 text-xs">
          {content}
        </pre>
      </div>
    );
  }

  return (
    <div
      ref={containerRef}
      className="flex items-center justify-center overflow-auto rounded-lg border bg-white p-4"
      dangerouslySetInnerHTML={{ __html: svg }}
    />
  );
}

type TableRow = Record<string, string | number | boolean>;

function TableRenderer({ content }: RendererProps) {
  let data: { headers: string[]; rows: TableRow[] };

  try {
    const parsed: unknown = JSON.parse(content);
    if (
      typeof parsed === "object" &&
      parsed !== null &&
      "headers" in parsed &&
      "rows" in parsed
    ) {
      const obj = parsed as { headers: string[]; rows: TableRow[] };
      data = { headers: obj.headers, rows: obj.rows };
    } else if (Array.isArray(parsed) && parsed.length > 0) {
      const rows = parsed as TableRow[];
      const headers = Object.keys(rows[0]);
      data = { headers, rows };
    } else {
      throw new Error("Invalid table format");
    }
  } catch {
    return (
      <div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4 text-sm text-yellow-700">
        <p className="font-medium">Could not parse table data</p>
        <pre className="mt-2 overflow-auto rounded bg-yellow-100 p-2 text-xs">
          {content}
        </pre>
      </div>
    );
  }

  return (
    <
Files: 1
Size: 30.5 KB
Complexity: 39/100
Category: Image & Video

Related in Image & Video