Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions docs/content/docs/chat/artifacts.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
---
title: Artifacts
description: Add side-panel content that opens from inline previews in chat.
---

Artifacts let a component render a compact inline preview inside the chat message and expand into a full side panel when clicked. Use them for code viewers, document previews, embedded frames, or any content that benefits from a larger canvas.

```tsx
import { defineComponent } from "@openuidev/react-lang";
import { Artifact } from "@openuidev/react-ui";
import { z } from "zod";

const ArtifactCodeBlock = defineComponent({
name: "ArtifactCodeBlock",
props: z.object({
language: z.string(),
title: z.string(),
codeString: z.string(),
}),
description: "Code block that opens in the artifact side panel",
component: Artifact({
title: (props) => props.title,
preview: (props, { open, isActive }) => (
<CodeChip title={props.title} language={props.language} onClick={open} isActive={isActive} />
),
panel: (props) => (
<SyntaxHighlighter language={props.language}>{props.codeString}</SyntaxHighlighter>
),
}),
});
```

## How it works

An artifact component has two parts:

- **Preview** — a compact element rendered inline in the chat message. It receives an `open` callback to activate the side panel.
- **Panel** — the full content rendered inside `ArtifactPanel`, portaled into the `ArtifactPortalTarget` in your layout. Only one panel is visible at a time.

`Artifact()` is a factory function that wires these together. It generates a `ComponentRenderer` that handles ID generation, artifact state, and panel portaling internally. Pass the result as the `component` field of `defineComponent`.

## `Artifact()` config

```ts
import { Artifact } from "@openuidev/react-ui";

Artifact({
title, // string | (props) => string
preview, // (props, controls) => ReactNode
panel, // (props, controls) => ReactNode
panelProps, // optional — className, errorFallback, header
});
```

| Option | Type | Description |
|--------|------|-------------|
| `title` | `string \| (props: P) => string` | Panel header title. Static string or derived from props. |
| `preview` | `(props: P, controls: ArtifactControls) => ReactNode` | Inline preview rendered in the chat message. |
| `panel` | `(props: P, controls: ArtifactControls) => ReactNode` | Content rendered inside the side panel. |
| `panelProps` | `{ className?, errorFallback?, header? }` | Optional overrides forwarded to `ArtifactPanel`. |

Both `preview` and `panel` receive the full Zod-inferred props as the first argument and `ArtifactControls` as the second.

## `ArtifactControls`

The controls object passed to `preview` and `panel` render functions.

```ts
interface ArtifactControls {
isActive: boolean; // whether this artifact's panel is currently open
open: () => void; // activate this artifact
close: () => void; // deactivate this artifact
toggle: () => void; // toggle open/close
}
```

The preview typically uses `open` and `isActive` to show a click-to-expand button. The panel can use `close` to render a dismiss button inside the panel body.

## Layout setup

Built-in layouts (`FullScreen`, `Copilot`, `BottomTray`) mount `ArtifactPortalTarget` automatically. Artifact panels render into this target with no extra setup.

If you build a custom layout with the headless hooks, mount one `ArtifactPortalTarget` in your layout where the panel should appear.

```tsx
import { ArtifactPortalTarget } from "@openuidev/react-ui";

function Layout() {
return (
<div className="flex h-screen">
<main className="flex-1">{/* chat area */}</main>
<ArtifactPortalTarget className="w-[480px]" />
</div>
);
}
```

Only one `ArtifactPortalTarget` should be mounted at a time. All artifact panels portal into this single element.

## Headless hooks

For custom layouts or advanced control, use the artifact hooks from `@openuidev/react-headless`.

### `useArtifact(id)`

Binds a component to a specific artifact by ID. Returns activation state and actions.

```ts
import { useArtifact } from "@openuidev/react-headless";

const { isActive, open, close, toggle } = useArtifact(artifactId);
```

### `useActiveArtifact()`

Returns global artifact state — whether any artifact is open, and a close action. Use this in layout components that resize or show overlays when any artifact is active.

```ts
import { useActiveArtifact } from "@openuidev/react-headless";

const { isArtifactActive, activeArtifactId, closeArtifact } = useActiveArtifact();
```

Both hooks require a `ChatProvider` ancestor in the component tree.

## Manual wiring

If `Artifact()` does not fit your use case, wire the pieces directly. This is the escape hatch for full control.

```tsx
import { defineComponent } from "@openuidev/react-lang";
import { ArtifactPanel } from "@openuidev/react-ui";
import { useArtifact } from "@openuidev/react-headless";
import { useId } from "react";

const CustomArtifact = defineComponent({
name: "CustomArtifact",
props: CustomSchema,
description: "Artifact with full manual control",
component: ({ props }) => {
const artifactId = useId();
const { isActive, open, close } = useArtifact(artifactId);

return (
<>
<button onClick={open}>{isActive ? "Viewing" : "Open"}</button>
<ArtifactPanel artifactId={artifactId} title="Custom">
<div>{/* panel content */}</div>
</ArtifactPanel>
</>
);
},
});
```

`ArtifactPanel` accepts `artifactId`, `title`, `children`, `className`, `errorFallback`, and `header` (boolean or custom ReactNode). It renders nothing when the artifact is inactive.

## Related guides

<Cards>
<Card title="Defining Components" href="/docs/openui-lang/defining-components">
Create custom openui-lang components with `defineComponent`.
</Card>
<Card title="Custom UI Guide" href="/docs/chat/custom-ui-guide">
Build a fully custom chat UI with headless hooks.
</Card>
<Card title="Headless Hooks" href="/docs/chat/hooks">
Full reference for all headless hooks.
</Card>
<Card title="Theming" href="/docs/chat/theming">
Adjust colors, mode, and theme overrides.
</Card>
</Cards>
1 change: 1 addition & 0 deletions docs/content/docs/chat/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"copilot",
"fullscreen",
"bottom-tray",
"artifacts",
"---Configurations---",
"connecting",
"persistence",
Expand Down
39 changes: 39 additions & 0 deletions examples/openui-artifact-demo/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Node
node_modules
.pnpm-store
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Next.js
.next
out

# Git
.git
.gitignore

# Logs
logs
*.log

# Env files
.env
.env.*
!.env.example

# OS files
.DS_Store
Thumbs.db

# Build / cache
dist
build
.turbo
.cache
coverage

# Editor
.vscode
.idea
41 changes: 41 additions & 0 deletions examples/openui-artifact-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
73 changes: 73 additions & 0 deletions examples/openui-artifact-demo/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# syntax=docker/dockerfile:1.7
# --------------------------------------------------
# Build stage
# --------------------------------------------------
FROM node:20-alpine AS builder

WORKDIR /app

RUN apk add --no-cache libc6-compat
ARG PNPM_VERSION=9.12.0
RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate

ENV NEXT_TELEMETRY_DISABLED=1

COPY pnpm-workspace.yaml package.json pnpm-lock.yaml tsconfig.json ./

COPY packages/openui-cli/package.json ./packages/openui-cli/
COPY packages/react-ui/package.json ./packages/react-ui/
COPY packages/react-headless/package.json ./packages/react-headless/
COPY packages/react-lang/package.json ./packages/react-lang/
COPY examples/openui-chat/package.json ./examples/openui-chat/

RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --ignore-scripts

COPY packages/openui-cli ./packages/openui-cli
COPY packages/react-ui ./packages/react-ui
COPY packages/react-headless ./packages/react-headless
COPY packages/react-lang ./packages/react-lang
COPY examples/openui-chat ./examples/openui-chat

RUN pnpm --filter @openuidev/cli build
RUN pnpm --filter @openuidev/react-ui build
RUN pnpm --filter @openuidev/react-headless build
RUN pnpm --filter @openuidev/react-lang build

WORKDIR /app/examples/openui-chat
RUN node /app/packages/openui-cli/dist/index.js generate src/library.ts --out src/generated/system-prompt.txt \
&& pnpm build



# --------------------------------------------------
# Runtime stage
# --------------------------------------------------
FROM node:20-alpine AS runner

WORKDIR /app

RUN apk add --no-cache libc6-compat

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000 HOSTNAME=0.0.0.0

RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
USER nextjs

# Copy full standalone output to avoid brittle partial-copy assumptions
COPY --from=builder --chown=nextjs:nodejs /app/examples/openui-chat/.next/standalone ./

# Static assets expected by Next at runtime
COPY --from=builder --chown=nextjs:nodejs /app/examples/openui-chat/.next/static ./examples/openui-chat/.next/static

# If your app has a public directory, include this line
# COPY --from=builder --chown=nextjs:nodejs /app/examples/openui-chat/public ./examples/openui-chat/public

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:3000').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"

CMD ["node", "examples/openui-chat/server.js"]
46 changes: 46 additions & 0 deletions examples/openui-artifact-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# OpenUI Artifact Demo

A demo application showcasing the OpenUI artifact system for displaying generated code in a resizable side panel.

## Features

- **Artifact Code Blocks**: AI-generated code appears as compact previews in chat
- **Side Panel**: Click "View Code" to open the full code in a resizable artifact panel
- **Syntax Highlighting**: Full Prism-based syntax highlighting in the artifact panel
- **Multiple Artifacts**: Multiple code blocks per conversation, one active at a time
- **Copy to Clipboard**: One-click code copying from the artifact panel

## Getting Started

```bash
# Install dependencies (from repo root)
pnpm install

# Generate the system prompt
pnpm --filter openui-artifact-demo generate:prompt

# Start the development server
pnpm --filter openui-artifact-demo dev
```

Set your OpenAI API key:
```bash
export OPENAI_API_KEY=your-key-here
```

## How It Works

This example extends the standard OpenUI chat library with a custom `ArtifactCodeBlock` component that integrates with the OpenUI artifact system:

1. User asks for code (e.g., "Build me a React login form")
2. AI generates a response using `ArtifactCodeBlock` components
3. Each code block shows an inline preview in the chat
4. Clicking "View Code" opens the full code in the artifact side panel
5. The panel is resizable and supports syntax highlighting + copy

## Architecture

- `src/components/ArtifactCodeBlock/` — Custom genui component with inline preview and artifact panel view
- `src/library.ts` — Extended component library with ArtifactCodeBlock
- `src/app/page.tsx` — Main page using FullScreen layout
- `src/app/api/chat/route.ts` — API route for OpenAI streaming
Loading
Loading