AI suite

The Phase 1 components that turn the conversation primitive into a full chat experience: a status-aware prompt-input, collapsible reasoning, message actions, and prompt suggestions. The demo below wires them all together — try a suggestion, then watch the reasoning stream before the answer.

Pick a suggestion or type below to see the AI suite in action.

Install

npx shadcn@latest add @weaveworm/prompt-input
npx shadcn@latest add @weaveworm/reasoning
npx shadcn@latest add @weaveworm/actions
npx shadcn@latest add @weaveworm/suggestions
npx shadcn@latest add @weaveworm/tool
npx shadcn@latest add @weaveworm/sources
npx shadcn@latest add @weaveworm/branch
npx shadcn@latest add @weaveworm/task
npx shadcn@latest add @weaveworm/image
npx shadcn@latest add @weaveworm/web-preview
npx shadcn@latest add @weaveworm/artifact

Prompt input

A composable <form> composer. The textarea grows with its content and submits on Enter (Shift+Enter for a newline); the submit button reflects the generation status — send, spinner, stop, or error.

import {
  PromptInput,
  PromptInputTextarea,
  PromptInputToolbar,
  PromptInputTools,
  PromptInputSubmit,
} from "@/components/ai/prompt-input";

<PromptInput onSubmit={handleSubmit}>
  <PromptInputTextarea value={input} onChange={onChange} />
  <PromptInputToolbar>
    <PromptInputTools>{/* model picker, attachments… */}</PromptInputTools>
    <PromptInputSubmit status={status} />
  </PromptInputToolbar>
</PromptInput>

Reasoning

Pass isStreaming and the block auto-expands while the model thinks, times the thought, then collapses — or control open yourself.

import { Reasoning, ReasoningTrigger, ReasoningContent } from "@/components/ai/reasoning";

<Reasoning isStreaming={isStreaming}>
  <ReasoningTrigger />
  <ReasoningContent>{reasoningText}</ReasoningContent>
</Reasoning>

Tool

A collapsible card for a single tool call. Pass the state from the Vercel AI SDK's ToolUIPart and the header badge tracks it — pending, running, completed, or error. Drop in ToolConfirmation to gate a call behind human-in-the-loop approval.

Input
{
  "service": "web",
  "ref": "main@a1b2c3d"
}

Deploy web (main@a1b2c3d) to production?

import {
  Tool,
  ToolHeader,
  ToolContent,
  ToolInput,
  ToolOutput,
  ToolConfirmation,
} from "@/components/ai/tool";

<Tool state={part.state}>
  <ToolHeader name={part.type} />
  <ToolContent>
    <ToolInput input={part.input} />
    {needsApproval ? (
      <ToolConfirmation onApprove={approve} onReject={reject} />
    ) : (
      <ToolOutput output={part.output} errorText={part.errorText} />
    )}
  </ToolContent>
</Tool>

Sources

Ground an answer in its citations: drop InlineCitation markers into the text (they show the source in a tooltip), then list everything in a collapsible Sources block underneath. Map a Vercel AI SDK source-url part straight onto <Source>.

Weaveworm builds its primitives on Base UI1, themes them with Tailwind v4 tokens2, and distributes everything through a shadcn-compatible registry3.

import {
  Sources,
  SourcesTrigger,
  SourcesContent,
  Source,
  InlineCitation,
} from "@/components/ai/sources";

<p>
  The answer text<InlineCitation index={1} href={sources[0].url} title={sources[0].title} />.
</p>

<Sources>
  <SourcesTrigger count={sources.length} />
  <SourcesContent>
    {sources.map((s) => (
      <Source key={s.url} href={s.url} title={s.title} />
    ))}
  </SourcesContent>
</Sources>

Branch

When a prompt is edited or an answer regenerated, you get more than one version of a turn. Wrap the alternatives in BranchMessages — only the active one renders — and BranchSelector pages through them with wrap-around prev/next controls and a 2 / 3 indicator. Use it controlled (value + onValueChange) or let it manage its own state.

Sure — Weaveworm is an open-source design system for building AI experiences.

import {
  Branch,
  BranchMessages,
  BranchSelector,
  BranchPrevious,
  BranchPage,
  BranchNext,
} from "@/components/ai/branch";

<Branch defaultValue={versions.length - 1}>
  <BranchMessages>
    {versions.map((text) => (
      <Response key={text}>{text}</Response>
    ))}
  </BranchMessages>
  <BranchSelector>
    <BranchPrevious />
    <BranchPage />
    <BranchNext />
  </BranchSelector>
</Branch>

Task

Show what an agent is doing — a collapsible trace of its steps along a single timeline. Pass status and the block auto-expands while the work is in-progress, then collapses once it's completed. Drop TaskItemFile chips into a step to reference the files it touched.

Searched the registry for the existing AI component pattern
Created branch.tsx with the navigation controls
Registered it in registry.json and rebuilt the registry
Ran lint and typecheck — all green
import {
  Task,
  TaskTrigger,
  TaskContent,
  TaskItem,
  TaskItemFile,
} from "@/components/ai/task";

<Task status={status}>
  <TaskTrigger title="Refactoring the auth flow" />
  <TaskContent>
    {steps.map((step) => (
      <TaskItem key={step.id}>{step.text}</TaskItem>
    ))}
    <TaskItem>
      Edited <TaskItemFile name="auth.ts" />
    </TaskItem>
  </TaskContent>
</Task>

Image

Render a model-generated image. Pass a src, or hand it the raw base64 + mediaType straight from the Vercel AI SDK's experimental_generateImage and it builds the data URL. A skeleton holds the space while the bitmap decodes, with a graceful fallback if it fails.

A generated abstract gradient
import { Image } from "@/components/ai/image";

<Image base64={file.base64} mediaType={file.mediaType} alt="Generated logo" />

Web preview

Preview a site the agent generated or linked to, inside faux-browser chrome. Drop a WebPreviewNavigation (back / forward / reload + an editable address bar) above a sandboxed WebPreviewBody iframe. Point the body at a src URL or hand it generated HTML via srcDoc.

import {
  WebPreview,
  WebPreviewNavigation,
  WebPreviewBody,
} from "@/components/ai/web-preview";

<WebPreview defaultUrl="https://example.com" className="h-96">
  <WebPreviewNavigation />
  <WebPreviewBody />
</WebPreview>

Artifact

Lift a discrete piece of generated content — a document, a code file, a chart — out of the message stream into its own framed panel. Pair an ArtifactHeader (title, description, and an ArtifactActions row of icon buttons) above an ArtifactContent body that holds the rendered output.

greet.ts
TypeScript · 3 lines
export function greet(name: string) {
  return `Hello, ${name}!`;
}
import {
  Artifact,
  ArtifactHeader,
  ArtifactTitleGroup,
  ArtifactTitle,
  ArtifactDescription,
  ArtifactActions,
  ArtifactAction,
  ArtifactContent,
} from "@/components/ai/artifact";

<Artifact>
  <ArtifactHeader>
    <ArtifactTitleGroup>
      <ArtifactTitle>{file.name}</ArtifactTitle>
      <ArtifactDescription>{file.language}</ArtifactDescription>
    </ArtifactTitleGroup>
    <ArtifactActions>
      <ArtifactAction label="Copy" onClick={copy}>
        <Copy />
      </ArtifactAction>
    </ArtifactActions>
  </ArtifactHeader>
  <ArtifactContent>{/* code, doc, preview… */}</ArtifactContent>
</Artifact>