feat: AI-added records save with placeholders + "Added by AI" provenance

Stop blocking AI imports/proposals on missing non-critical fields. Records
the chat agent drafts now save with safe placeholders, auto-generated file
numbers, and a source="ai" marker that surfaces an "Added by AI" badge so a
clinician can review/edit them later.

Backend:
- add `source` (manual|ai) column to patients/appointments/prescriptions
  (migration 0014) + canonical types, services, validation schemas
- relax patient/appointment validation: empty file number allowed, demographic
  + type/provider/initials fall back to placeholders (initials derived from name)
- patients.generateFileNumber() auto-assigns an MRN when one is missing
- proposeAppointment accepts a name when no file number resolves; AI commits +
  /api/ai/import stamp source="ai"

Frontend:
- `source` on Appointment/Patient/Prescription types; AI commits send source="ai"
- reusable <AiBadge> shown on the Patients table/detail and prescriptions list

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-14 19:27:17 +03:00
parent 67249be67b
commit 929bec8f31
258 changed files with 28490 additions and 681 deletions
@@ -0,0 +1,61 @@
"use client";
import {
Agent,
AgentContent,
AgentHeader,
AgentInstructions,
AgentOutput,
AgentTool,
AgentTools,
} from "@/components/ai-elements/agent";
import { z } from "zod";
const webSearchTool = {
description: "Search the web for information",
inputSchema: z.object({
query: z.string().describe("The search query"),
}),
};
const readUrlTool = {
description: "Read and parse a URL",
inputSchema: z.object({
url: z.string().url().describe("The URL to read"),
}),
};
const summarizeTool = {
description: "Summarize text into key points",
inputSchema: z.object({
maxPoints: z.number().optional().describe("Maximum number of key points"),
text: z.string().describe("The text to summarize"),
}),
};
const outputSchema = `z.object({
sentiment: z.enum(['positive', 'negative', 'neutral']),
score: z.number(),
summary: z.string(),
})`;
const Example = () => (
<Agent>
<AgentHeader model="openai/gpt-5.2-pro" name="Research Assistant" />
<AgentContent>
<AgentInstructions>
You are a helpful research assistant. Your job is to search the web for
information and summarize findings for the user. Always cite your
sources and provide accurate, up-to-date information.
</AgentInstructions>
<AgentTools type="multiple">
<AgentTool tool={webSearchTool} value="web_search" />
<AgentTool tool={readUrlTool} value="read_url" />
<AgentTool tool={summarizeTool} value="summarize" />
</AgentTools>
<AgentOutput schema={outputSchema} />
</AgentContent>
</Agent>
);
export default Example;
@@ -0,0 +1,127 @@
"use client";
import {
Artifact,
ArtifactAction,
ArtifactActions,
ArtifactContent,
ArtifactDescription,
ArtifactHeader,
ArtifactTitle,
} from "@/components/ai-elements/artifact";
import { CodeBlock } from "@/components/ai-elements/code-block";
import {
CopyIcon,
DownloadIcon,
PlayIcon,
RefreshCwIcon,
ShareIcon,
} from "lucide-react";
const handleRun = () => {
console.log("Run");
};
const handleCopy = () => {
console.log("Copy");
};
const handleRegenerate = () => {
console.log("Regenerate");
};
const handleDownload = () => {
console.log("Download");
};
const handleShare = () => {
console.log("Share");
};
const code = `# Dijkstra's Algorithm implementation
import heapq
def dijkstra(graph, start):
distances = {node: float('inf') for node in graph}
distances[start] = 0
heap = [(0, start)]
visited = set()
while heap:
current_distance, current_node = heapq.heappop(heap)
if current_node in visited:
continue
visited.add(current_node)
for neighbor, weight in graph[current_node].items():
distance = current_distance + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(heap, (distance, neighbor))
return distances
# Example graph
graph = {
'A': {'B': 1, 'C': 4},
'B': {'A': 1, 'C': 2, 'D': 5},
'C': {'A': 4, 'B': 2, 'D': 1},
'D': {'B': 5, 'C': 1}
}
print(dijkstra(graph, 'A'))`;
const Example = () => (
<Artifact>
<ArtifactHeader>
<div>
<ArtifactTitle>Dijkstra&apos;s Algorithm Implementation</ArtifactTitle>
<ArtifactDescription>Updated 1 minute ago</ArtifactDescription>
</div>
<div className="flex items-center gap-2">
<ArtifactActions>
<ArtifactAction
icon={PlayIcon}
label="Run"
onClick={handleRun}
tooltip="Run code"
/>
<ArtifactAction
icon={CopyIcon}
label="Copy"
onClick={handleCopy}
tooltip="Copy to clipboard"
/>
<ArtifactAction
icon={RefreshCwIcon}
label="Regenerate"
onClick={handleRegenerate}
tooltip="Regenerate content"
/>
<ArtifactAction
icon={DownloadIcon}
label="Download"
onClick={handleDownload}
tooltip="Download file"
/>
<ArtifactAction
icon={ShareIcon}
label="Share"
onClick={handleShare}
tooltip="Share artifact"
/>
</ArtifactActions>
</div>
</ArtifactHeader>
<ArtifactContent className="p-0">
<CodeBlock
className="border-none"
code={code}
language="python"
showLineNumbers
/>
</ArtifactContent>
</Artifact>
);
export default Example;
@@ -0,0 +1,128 @@
"use client";
import {
Attachment,
AttachmentHoverCard,
AttachmentHoverCardContent,
AttachmentHoverCardTrigger,
AttachmentInfo,
AttachmentPreview,
AttachmentRemove,
Attachments,
getAttachmentLabel,
getMediaCategory,
} from "@/components/ai-elements/attachments";
import { nanoid } from "nanoid";
import { memo, useCallback, useState } from "react";
const initialAttachments = [
{
filename: "mountain-landscape.jpg",
id: nanoid(),
mediaType: "image/jpeg",
type: "file" as const,
url: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=400&fit=crop",
},
{
filename: "quarterly-report.pdf",
id: nanoid(),
mediaType: "application/pdf",
type: "file" as const,
url: "",
},
{
id: nanoid(),
mediaType: "text/html",
title: "React Documentation",
type: "source-document" as const,
url: "https://react.dev",
},
{
filename: "podcast-episode.mp3",
id: nanoid(),
mediaType: "audio/mp3",
type: "file" as const,
url: "",
},
];
interface AttachmentItemProps {
attachment: (typeof initialAttachments)[0];
onRemove: (id: string) => void;
}
const AttachmentItem = memo(({ attachment, onRemove }: AttachmentItemProps) => {
const handleRemove = useCallback(
() => onRemove(attachment.id),
[onRemove, attachment.id]
);
const mediaCategory = getMediaCategory(attachment);
const label = getAttachmentLabel(attachment);
return (
<AttachmentHoverCard key={attachment.id}>
<AttachmentHoverCardTrigger asChild>
<Attachment data={attachment} onRemove={handleRemove}>
<div className="relative size-5 shrink-0">
<div className="absolute inset-0 transition-opacity group-hover:opacity-0">
<AttachmentPreview />
</div>
<AttachmentRemove className="absolute inset-0" />
</div>
<AttachmentInfo />
</Attachment>
</AttachmentHoverCardTrigger>
<AttachmentHoverCardContent>
<div className="space-y-3">
{mediaCategory === "image" &&
attachment.type === "file" &&
attachment.url && (
<div className="flex max-h-96 w-80 items-center justify-center overflow-hidden rounded-md border">
<img
alt={label}
className="max-h-full max-w-full object-contain"
height={384}
src={attachment.url}
width={320}
/>
</div>
)}
<div className="space-y-1 px-0.5">
<h4 className="font-semibold text-sm leading-none">{label}</h4>
{attachment.mediaType && (
<p className="font-mono text-muted-foreground text-xs">
{attachment.mediaType}
</p>
)}
</div>
</div>
</AttachmentHoverCardContent>
</AttachmentHoverCard>
);
});
AttachmentItem.displayName = "AttachmentItem";
const Example = () => {
const [attachments, setAttachments] = useState(initialAttachments);
const handleRemove = useCallback((id: string) => {
setAttachments((prev) => prev.filter((a) => a.id !== id));
}, []);
return (
<div className="flex items-center justify-center p-8">
<Attachments variant="inline">
{attachments.map((attachment) => (
<AttachmentItem
attachment={attachment}
key={attachment.id}
onRemove={handleRemove}
/>
))}
</Attachments>
</div>
);
};
export default Example;
@@ -0,0 +1,95 @@
"use client";
import {
Attachment,
AttachmentInfo,
AttachmentPreview,
AttachmentRemove,
Attachments,
} from "@/components/ai-elements/attachments";
import { nanoid } from "nanoid";
import { memo, useCallback, useState } from "react";
const initialAttachments = [
{
filename: "mountain-landscape.jpg",
id: nanoid(),
mediaType: "image/jpeg",
type: "file" as const,
url: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=400&fit=crop",
},
{
filename: "quarterly-report-2024.pdf",
id: nanoid(),
mediaType: "application/pdf",
type: "file" as const,
url: "",
},
{
filename: "product-demo.mp4",
id: nanoid(),
mediaType: "video/mp4",
type: "file" as const,
url: "",
},
{
filename: "api-reference",
id: nanoid(),
mediaType: "text/html",
title: "API Documentation",
type: "source-document" as const,
url: "https://docs.example.com/api",
},
{
filename: "meeting-recording.mp3",
id: nanoid(),
mediaType: "audio/mpeg",
type: "file" as const,
url: "",
},
];
interface AttachmentItemProps {
attachment: (typeof initialAttachments)[0];
onRemove: (id: string) => void;
}
const AttachmentItem = memo(({ attachment, onRemove }: AttachmentItemProps) => {
const handleRemove = useCallback(
() => onRemove(attachment.id),
[onRemove, attachment.id]
);
return (
<Attachment data={attachment} key={attachment.id} onRemove={handleRemove}>
<AttachmentPreview />
<AttachmentInfo showMediaType />
<AttachmentRemove />
</Attachment>
);
});
AttachmentItem.displayName = "AttachmentItem";
const Example = () => {
const [attachments, setAttachments] = useState(initialAttachments);
const handleRemove = useCallback((id: string) => {
setAttachments((prev) => prev.filter((a) => a.id !== id));
}, []);
return (
<div className="flex items-center justify-center p-8">
<Attachments className="w-full max-w-md" variant="list">
{attachments.map((attachment) => (
<AttachmentItem
attachment={attachment}
key={attachment.id}
onRemove={handleRemove}
/>
))}
</Attachments>
</div>
);
};
export default Example;
@@ -0,0 +1,85 @@
"use client";
import {
Attachment,
AttachmentPreview,
AttachmentRemove,
Attachments,
} from "@/components/ai-elements/attachments";
import { nanoid } from "nanoid";
import { memo, useCallback, useState } from "react";
const initialAttachments = [
{
filename: "mountain-landscape.jpg",
id: nanoid(),
mediaType: "image/jpeg",
type: "file" as const,
url: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=400&fit=crop",
},
{
filename: "ocean-sunset.jpg",
id: nanoid(),
mediaType: "image/jpeg",
type: "file" as const,
url: "https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?w=400&h=400&fit=crop",
},
{
filename: "document.pdf",
id: nanoid(),
mediaType: "application/pdf",
type: "file" as const,
url: "",
},
{
filename: "video.mp4",
id: nanoid(),
mediaType: "video/mp4",
type: "file" as const,
url: "",
},
];
interface AttachmentItemProps {
attachment: (typeof initialAttachments)[0];
onRemove: (id: string) => void;
}
const AttachmentItem = memo(({ attachment, onRemove }: AttachmentItemProps) => {
const handleRemove = useCallback(
() => onRemove(attachment.id),
[onRemove, attachment.id]
);
return (
<Attachment data={attachment} onRemove={handleRemove}>
<AttachmentPreview />
<AttachmentRemove />
</Attachment>
);
});
AttachmentItem.displayName = "AttachmentItem";
const Example = () => {
const [attachments, setAttachments] = useState(initialAttachments);
const handleRemove = useCallback((id: string) => {
setAttachments((prev) => prev.filter((a) => a.id !== id));
}, []);
return (
<div className="flex items-center justify-center p-8">
<Attachments variant="grid">
{attachments.map((attachment) => (
<AttachmentItem
attachment={attachment}
key={attachment.id}
onRemove={handleRemove}
/>
))}
</Attachments>
</div>
);
};
export default Example;
@@ -0,0 +1,35 @@
"use client";
import {
AudioPlayer,
AudioPlayerControlBar,
AudioPlayerDurationDisplay,
AudioPlayerElement,
AudioPlayerMuteButton,
AudioPlayerPlayButton,
AudioPlayerSeekBackwardButton,
AudioPlayerSeekForwardButton,
AudioPlayerTimeDisplay,
AudioPlayerTimeRange,
AudioPlayerVolumeRange,
} from "@/components/ai-elements/audio-player";
const Example = () => (
<div className="flex size-full items-center justify-center">
<AudioPlayer>
<AudioPlayerElement src="https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/ElevenLabs_2025-11-10T22_07_46_Hayden_pvc_sp108_s50_sb75_se0_b_m2.mp3" />
<AudioPlayerControlBar>
<AudioPlayerPlayButton />
<AudioPlayerSeekBackwardButton seekOffset={10} />
<AudioPlayerSeekForwardButton seekOffset={10} />
<AudioPlayerTimeDisplay />
<AudioPlayerTimeRange />
<AudioPlayerDurationDisplay />
<AudioPlayerMuteButton />
<AudioPlayerVolumeRange />
</AudioPlayerControlBar>
</AudioPlayer>
</div>
);
export default Example;
@@ -0,0 +1,68 @@
"use client";
import {
AudioPlayer,
AudioPlayerControlBar,
AudioPlayerDurationDisplay,
AudioPlayerElement,
AudioPlayerMuteButton,
AudioPlayerPlayButton,
AudioPlayerSeekBackwardButton,
AudioPlayerSeekForwardButton,
AudioPlayerTimeDisplay,
AudioPlayerTimeRange,
AudioPlayerVolumeRange,
} from "@/components/ai-elements/audio-player";
import type { Experimental_SpeechResult as SpeechResult } from "ai";
import { useEffect, useState } from "react";
const Example = () => {
const [data, setData] = useState<SpeechResult["audio"] | null>(null);
useEffect(() => {
const fetchData = async () => {
const response = await fetch(
"https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/ElevenLabs_2025-11-10T22_07_46_Hayden_pvc_sp108_s50_sb75_se0_b_m2.mp3"
);
const arrayBuffer = await response.arrayBuffer();
const base64 = Buffer.from(arrayBuffer).toString("base64");
const newData: SpeechResult["audio"] = {
base64,
format: "mp3",
mediaType: "audio/mpeg",
uint8Array: new Uint8Array(arrayBuffer),
};
setData(newData);
};
if (!data) {
fetchData();
}
}, [data]);
if (!data) {
return <div>Loading...</div>;
}
return (
<div className="flex size-full items-center justify-center">
<AudioPlayer>
<AudioPlayerElement data={data} />
<AudioPlayerControlBar>
<AudioPlayerPlayButton />
<AudioPlayerSeekBackwardButton seekOffset={10} />
<AudioPlayerSeekForwardButton seekOffset={10} />
<AudioPlayerTimeDisplay />
<AudioPlayerTimeRange />
<AudioPlayerDurationDisplay />
<AudioPlayerMuteButton />
<AudioPlayerVolumeRange />
</AudioPlayerControlBar>
</AudioPlayer>
</div>
);
};
export default Example;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,110 @@
"use client";
import {
Checkpoint,
CheckpointIcon,
CheckpointTrigger,
} from "@/components/ai-elements/checkpoint";
import { Conversation, ConversationContent } from "@/components/ai-elements/conversation";
import {
Message,
MessageContent,
MessageResponse,
} from "@/components/ai-elements/message";
import { nanoid } from "nanoid";
import { Fragment, memo, useCallback, useState } from "react";
interface MessageType {
id: string;
role: "user" | "assistant";
content: string;
}
const initialMessages: MessageType[] = [
{
content: "What is React?",
id: nanoid(),
role: "user",
},
{
content:
"React is a JavaScript library for building user interfaces. It was developed by Facebook and is now maintained by Meta and a community of developers.",
id: nanoid(),
role: "assistant",
},
{
content: "How does component state work?",
id: nanoid(),
role: "user",
},
];
interface CheckpointItemProps {
checkpoint: { messageCount: number; timestamp: Date };
onRestore: (messageCount: number) => void;
}
const CheckpointItem = memo(
({ checkpoint, onRestore }: CheckpointItemProps) => {
const handleClick = useCallback(
() => onRestore(checkpoint.messageCount),
[onRestore, checkpoint.messageCount]
);
return (
<Checkpoint>
<CheckpointIcon />
<CheckpointTrigger
onClick={handleClick}
tooltip="Restores workspace and chat to this point"
>
Restore checkpoint
</CheckpointTrigger>
</Checkpoint>
);
}
);
CheckpointItem.displayName = "CheckpointItem";
const Example = () => {
const [messages, setMessages] = useState<MessageType[]>(initialMessages);
const [checkpoints] = useState([
{ messageCount: 2, timestamp: new Date(Date.now() - 3_600_000) },
]);
const handleRestore = useCallback((messageCount: number) => {
setMessages(initialMessages.slice(0, messageCount));
}, []);
return (
<div className="flex size-full flex-col rounded-lg border p-6">
<Conversation>
<ConversationContent>
{messages.map((message, index) => {
const checkpoint = checkpoints.find(
(cp) => cp.messageCount === index + 1
);
return (
<Fragment key={message.id}>
<Message from={message.role}>
<MessageContent>
<MessageResponse>{message.content}</MessageResponse>
</MessageContent>
</Message>
{checkpoint && (
<CheckpointItem
checkpoint={checkpoint}
onRestore={handleRestore}
/>
)}
</Fragment>
);
})}
</ConversationContent>
</Conversation>
</div>
);
};
export default Example;
@@ -0,0 +1,46 @@
"use client";
import {
CodeBlock,
CodeBlockActions,
CodeBlockCopyButton,
CodeBlockFilename,
CodeBlockHeader,
CodeBlockTitle,
} from "@/components/ai-elements/code-block";
import { FileIcon } from "lucide-react";
const handleCopy = () => {
console.log("Copied code to clipboard");
};
const handleCopyError = () => {
console.error("Failed to copy code to clipboard");
};
const code = `function MyComponent(props) {
return (
<div>
<h1>Hello, {props.name}!</h1>
<p>This is an example React component.</p>
</div>
);
}`;
const Example = () => (
<div className="dark">
<CodeBlock code={code} language="jsx">
<CodeBlockHeader>
<CodeBlockTitle>
<FileIcon size={14} />
<CodeBlockFilename>MyComponent.jsx</CodeBlockFilename>
</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton onCopy={handleCopy} onError={handleCopyError} />
</CodeBlockActions>
</CodeBlockHeader>
</CodeBlock>
</div>
);
export default Example;
@@ -0,0 +1,120 @@
"use client";
import {
CodeBlock,
CodeBlockActions,
CodeBlockCopyButton,
CodeBlockFilename,
CodeBlockHeader,
CodeBlockLanguageSelector,
CodeBlockLanguageSelectorContent,
CodeBlockLanguageSelectorItem,
CodeBlockLanguageSelectorTrigger,
CodeBlockLanguageSelectorValue,
CodeBlockTitle,
} from "@/components/ai-elements/code-block";
import { FileIcon } from "lucide-react";
import { useCallback, useState } from "react";
import type { BundledLanguage } from "shiki";
const codeExamples = {
go: {
code: `package main
import "fmt"
func greet(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
func main() {
fmt.Println(greet("World"))
}`,
filename: "greet.go",
},
python: {
code: `def greet(name: str) -> str:
return f"Hello, {name}!"
print(greet("World"))`,
filename: "greet.py",
},
rust: {
code: `fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
fn main() {
println!("{}", greet("World"));
}`,
filename: "greet.rs",
},
typescript: {
code: `function greet(name: string): string {
return \`Hello, \${name}!\`;
}
console.log(greet("World"));`,
filename: "greet.ts",
},
} as const;
type Language = keyof typeof codeExamples;
const languages: { value: Language; label: string }[] = [
{ label: "TypeScript", value: "typescript" },
{ label: "Python", value: "python" },
{ label: "Rust", value: "rust" },
{ label: "Go", value: "go" },
];
const handleCopy = () => {
console.log("Copied code to clipboard");
};
const handleCopyError = () => {
console.error("Failed to copy code to clipboard");
};
const Example = () => {
const [language, setLanguage] = useState<Language>("typescript");
const { code, filename } = codeExamples[language];
const handleLanguageChange = useCallback((value: string) => {
setLanguage(value as Language);
}, []);
return (
<CodeBlock code={code} language={language as BundledLanguage}>
<CodeBlockHeader>
<CodeBlockTitle>
<FileIcon size={14} />
<CodeBlockFilename>{filename}</CodeBlockFilename>
</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockLanguageSelector
onValueChange={handleLanguageChange}
value={language}
>
<CodeBlockLanguageSelectorTrigger>
<CodeBlockLanguageSelectorValue />
</CodeBlockLanguageSelectorTrigger>
<CodeBlockLanguageSelectorContent>
{languages.map((lang) => (
<CodeBlockLanguageSelectorItem
key={lang.value}
value={lang.value}
>
{lang.label}
</CodeBlockLanguageSelectorItem>
))}
</CodeBlockLanguageSelectorContent>
</CodeBlockLanguageSelector>
<CodeBlockCopyButton onCopy={handleCopy} onError={handleCopyError} />
</CodeBlockActions>
</CodeBlockHeader>
</CodeBlock>
);
};
export default Example;
@@ -0,0 +1,94 @@
"use client";
import {
Commit,
CommitActions,
CommitAuthor,
CommitAuthorAvatar,
CommitContent,
CommitCopyButton,
CommitFile,
CommitFileAdditions,
CommitFileChanges,
CommitFileDeletions,
CommitFileIcon,
CommitFileInfo,
CommitFilePath,
CommitFileStatus,
CommitFiles,
CommitHash,
CommitHeader,
CommitInfo,
CommitMessage,
CommitMetadata,
CommitSeparator,
CommitTimestamp,
} from "@/components/ai-elements/commit";
const handleCopy = () => {
console.log("Copied hash!");
};
const hash = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0";
const timestamp = new Date(Date.now() - 1000 * 60 * 60 * 2);
const files = [
{
additions: 150,
deletions: 0,
path: "src/auth/login.tsx",
status: "added" as const,
},
{
additions: 45,
deletions: 0,
path: "src/auth/logout.tsx",
status: "added" as const,
},
{
additions: 23,
deletions: 8,
path: "src/lib/session.ts",
status: "modified" as const,
},
];
const Example = () => (
<Commit>
<CommitHeader>
<CommitAuthor>
<CommitAuthorAvatar initials="HB" />
</CommitAuthor>
<CommitInfo>
<CommitMessage>feat: Add user authentication flow</CommitMessage>
<CommitMetadata>
<CommitHash>{hash.slice(0, 7)}</CommitHash>
<CommitSeparator />
<CommitTimestamp date={timestamp} />
</CommitMetadata>
</CommitInfo>
<CommitActions>
<CommitCopyButton hash={hash} onCopy={handleCopy} />
</CommitActions>
</CommitHeader>
<CommitContent>
<CommitFiles>
{files.map((file) => (
<CommitFile key={file.path}>
<CommitFileInfo>
<CommitFileStatus status={file.status} />
<CommitFileIcon />
<CommitFilePath>{file.path}</CommitFilePath>
</CommitFileInfo>
<CommitFileChanges>
<CommitFileAdditions count={file.additions} />
<CommitFileDeletions count={file.deletions} />
</CommitFileChanges>
</CommitFile>
))}
</CommitFiles>
</CommitContent>
</Commit>
);
export default Example;
@@ -0,0 +1,40 @@
"use client";
import {
Confirmation,
ConfirmationAccepted,
ConfirmationRejected,
ConfirmationRequest,
ConfirmationTitle,
} from "@/components/ai-elements/confirmation";
import { CheckIcon, XIcon } from "lucide-react";
import { nanoid } from "nanoid";
const Example = () => (
<div className="w-full max-w-2xl">
<Confirmation
approval={{ approved: true, id: nanoid() }}
state="approval-responded"
>
<ConfirmationTitle>
<ConfirmationRequest>
This tool wants to delete the file{" "}
<code className="rounded bg-muted px-1.5 py-0.5 text-sm">
/tmp/example.txt
</code>
. Do you approve this action?
</ConfirmationRequest>
<ConfirmationAccepted>
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
<span>You approved this tool execution</span>
</ConfirmationAccepted>
<ConfirmationRejected>
<XIcon className="size-4 text-destructive" />
<span>You rejected this tool execution</span>
</ConfirmationRejected>
</ConfirmationTitle>
</Confirmation>
</div>
);
export default Example;
@@ -0,0 +1,40 @@
"use client";
import {
Confirmation,
ConfirmationAccepted,
ConfirmationRejected,
ConfirmationRequest,
ConfirmationTitle,
} from "@/components/ai-elements/confirmation";
import { CheckIcon, XIcon } from "lucide-react";
import { nanoid } from "nanoid";
const Example = () => (
<div className="w-full max-w-2xl">
<Confirmation
approval={{ approved: false, id: nanoid() }}
state="output-denied"
>
<ConfirmationTitle>
<ConfirmationRequest>
This tool wants to delete the file{" "}
<code className="rounded bg-muted px-1.5 py-0.5 text-sm">
/tmp/example.txt
</code>
. Do you approve this action?
</ConfirmationRequest>
<ConfirmationAccepted>
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
<span>You approved this tool execution</span>
</ConfirmationAccepted>
<ConfirmationRejected>
<XIcon className="size-4 text-destructive" />
<span>You rejected this tool execution</span>
</ConfirmationRejected>
</ConfirmationTitle>
</Confirmation>
</div>
);
export default Example;
@@ -0,0 +1,54 @@
"use client";
import {
Confirmation,
ConfirmationAccepted,
ConfirmationAction,
ConfirmationActions,
ConfirmationRejected,
ConfirmationRequest,
ConfirmationTitle,
} from "@/components/ai-elements/confirmation";
import { CheckIcon, XIcon } from "lucide-react";
import { nanoid } from "nanoid";
const handleReject = () => {
// In production, call respondToConfirmationRequest with approved: false
};
const handleApprove = () => {
// In production, call respondToConfirmationRequest with approved: true
};
const Example = () => (
<div className="w-full max-w-2xl">
<Confirmation approval={{ id: nanoid() }} state="approval-requested">
<ConfirmationTitle>
<ConfirmationRequest>
This tool wants to execute a query on the production database:
<code className="mt-2 block rounded bg-muted p-2 text-sm">
SELECT * FROM users WHERE role = &apos;admin&apos;
</code>
</ConfirmationRequest>
<ConfirmationAccepted>
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
<span>You approved this tool execution</span>
</ConfirmationAccepted>
<ConfirmationRejected>
<XIcon className="size-4 text-destructive" />
<span>You rejected this tool execution</span>
</ConfirmationRejected>
</ConfirmationTitle>
<ConfirmationActions>
<ConfirmationAction onClick={handleReject} variant="outline">
Reject
</ConfirmationAction>
<ConfirmationAction onClick={handleApprove} variant="default">
Approve
</ConfirmationAction>
</ConfirmationActions>
</Confirmation>
</div>
);
export default Example;
@@ -0,0 +1,55 @@
"use client";
import {
Confirmation,
ConfirmationAccepted,
ConfirmationAction,
ConfirmationActions,
ConfirmationRejected,
ConfirmationRequest,
ConfirmationTitle,
} from "@/components/ai-elements/confirmation";
import { CheckIcon, XIcon } from "lucide-react";
import { nanoid } from "nanoid";
const handleReject = () => {
// In production, call respondToConfirmationRequest with approved: false
};
const handleApprove = () => {
// In production, call respondToConfirmationRequest with approved: true
};
const Example = () => (
<div className="w-full max-w-2xl">
<Confirmation approval={{ id: nanoid() }} state="approval-requested">
<ConfirmationTitle>
<ConfirmationRequest>
This tool wants to delete the file{" "}
<code className="inline rounded bg-muted px-1.5 py-0.5 text-sm">
/tmp/example.txt
</code>
. Do you approve this action?
</ConfirmationRequest>
<ConfirmationAccepted>
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
<span>You approved this tool execution</span>
</ConfirmationAccepted>
<ConfirmationRejected>
<XIcon className="size-4 text-destructive" />
<span>You rejected this tool execution</span>
</ConfirmationRejected>
</ConfirmationTitle>
<ConfirmationActions>
<ConfirmationAction onClick={handleReject} variant="outline">
Reject
</ConfirmationAction>
<ConfirmationAction onClick={handleApprove} variant="default">
Approve
</ConfirmationAction>
</ConfirmationActions>
</Confirmation>
</div>
);
export default Example;
@@ -0,0 +1,45 @@
"use client";
import {
Context,
ContextCacheUsage,
ContextContent,
ContextContentBody,
ContextContentFooter,
ContextContentHeader,
ContextInputUsage,
ContextOutputUsage,
ContextReasoningUsage,
ContextTrigger,
} from "@/components/ai-elements/context";
const Example = () => (
<div className="flex items-center justify-center p-8">
<Context
maxTokens={128_000}
modelId="openai:gpt-5"
usage={{
cachedInputTokens: 0,
inputTokens: 32_000,
outputTokens: 8000,
reasoningTokens: 0,
totalTokens: 40_000,
}}
usedTokens={40_000}
>
<ContextTrigger />
<ContextContent>
<ContextContentHeader />
<ContextContentBody>
<ContextInputUsage />
<ContextOutputUsage />
<ContextReasoningUsage />
<ContextCacheUsage />
</ContextContentBody>
<ContextContentFooter />
</ContextContent>
</Context>
</div>
);
export default Example;
@@ -0,0 +1,177 @@
"use client";
import {
Conversation,
ConversationContent,
ConversationDownload,
ConversationEmptyState,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import { Message, MessageContent } from "@/components/ai-elements/message";
import { MessageSquareIcon } from "lucide-react";
import { nanoid } from "nanoid";
import { useEffect, useState } from "react";
const messages: {
key: string;
content: string;
role: "user" | "assistant";
}[] = [
{
content: "Hello, how are you?",
key: nanoid(),
role: "user",
},
{
content: "I'm good, thank you! How can I assist you today?",
key: nanoid(),
role: "assistant",
},
{
content: "I'm looking for information about your services.",
key: nanoid(),
role: "user",
},
{
content:
"Sure! We offer a variety of AI solutions. What are you interested in?",
key: nanoid(),
role: "assistant",
},
{
content: "I'm interested in natural language processing tools.",
key: nanoid(),
role: "user",
},
{
content: "Great choice! We have several NLP APIs. Would you like a demo?",
key: nanoid(),
role: "assistant",
},
{
content: "Yes, a demo would be helpful.",
key: nanoid(),
role: "user",
},
{
content: "Alright, I can show you a sentiment analysis example. Ready?",
key: nanoid(),
role: "assistant",
},
{
content: "Yes, please proceed.",
key: nanoid(),
role: "user",
},
{
content: "Here is a sample: 'I love this product!' → Positive sentiment.",
key: nanoid(),
role: "assistant",
},
{
content: "Impressive! Can it handle multiple languages?",
key: nanoid(),
role: "user",
},
{
content: "Absolutely, our models support over 20 languages.",
key: nanoid(),
role: "assistant",
},
{
content: "How do I get started with the API?",
key: nanoid(),
role: "user",
},
{
content: "You can sign up on our website and get an API key instantly.",
key: nanoid(),
role: "assistant",
},
{
content: "Is there a free trial available?",
key: nanoid(),
role: "user",
},
{
content: "Yes, we offer a 14-day free trial with full access.",
key: nanoid(),
role: "assistant",
},
{
content: "What kind of support do you provide?",
key: nanoid(),
role: "user",
},
{
content: "We provide 24/7 chat and email support for all users.",
key: nanoid(),
role: "assistant",
},
{
content: "Thank you for the information!",
key: nanoid(),
role: "user",
},
{
content: "You're welcome! Let me know if you have any more questions.",
key: nanoid(),
role: "assistant",
},
];
const Example = () => {
const [visibleMessages, setVisibleMessages] = useState<
{
key: string;
content: string;
role: "user" | "assistant";
}[]
>([]);
useEffect(() => {
let currentIndex = 0;
const interval = setInterval(() => {
if (currentIndex < messages.length && messages[currentIndex]) {
const currentMessage = messages[currentIndex];
setVisibleMessages((prev) => [
...prev,
{
content: currentMessage.content,
key: currentMessage.key,
role: currentMessage.role,
},
]);
currentIndex += 1;
} else {
clearInterval(interval);
}
}, 500);
return () => clearInterval(interval);
}, []);
return (
<Conversation className="relative size-full">
<ConversationContent>
{visibleMessages.length === 0 ? (
<ConversationEmptyState
description="Messages will appear here as the conversation progresses."
icon={<MessageSquareIcon className="size-6" />}
title="Start a conversation"
/>
) : (
visibleMessages.map(({ key, content, role }) => (
<Message from={role} key={key}>
<MessageContent>{content}</MessageContent>
</Message>
))
)}
</ConversationContent>
<ConversationDownload messages={visibleMessages} />
<ConversationScrollButton />
</Conversation>
);
};
export default Example;
@@ -0,0 +1,62 @@
"use client";
import {
EnvironmentVariable,
EnvironmentVariableCopyButton,
EnvironmentVariableGroup,
EnvironmentVariableName,
EnvironmentVariableRequired,
EnvironmentVariables,
EnvironmentVariablesContent,
EnvironmentVariablesHeader,
EnvironmentVariablesTitle,
EnvironmentVariablesToggle,
EnvironmentVariableValue,
} from "@/components/ai-elements/environment-variables";
const variables = [
{
name: "DATABASE_URL",
required: true,
value: "postgresql://localhost:5432/mydb",
},
{ name: "API_KEY", required: true, value: "sk-1234567890abcdef" },
{ name: "NODE_ENV", required: false, value: "production" },
{ name: "PORT", required: false, value: "3000" },
];
const handleCopy = () => {
console.log("Copied!");
};
const Example = () => (
<EnvironmentVariables defaultShowValues={false}>
<EnvironmentVariablesHeader>
<EnvironmentVariablesTitle />
<EnvironmentVariablesToggle />
</EnvironmentVariablesHeader>
<EnvironmentVariablesContent>
{variables.map((variable) => (
<EnvironmentVariable
key={variable.name}
name={variable.name}
value={variable.value}
>
<EnvironmentVariableGroup>
<EnvironmentVariableName />
{variable.required && <EnvironmentVariableRequired />}
</EnvironmentVariableGroup>
<EnvironmentVariableGroup>
<EnvironmentVariableValue />
<EnvironmentVariableCopyButton
copyFormat="export"
onCopy={handleCopy}
/>
</EnvironmentVariableGroup>
</EnvironmentVariable>
))}
</EnvironmentVariablesContent>
</EnvironmentVariables>
);
export default Example;
@@ -0,0 +1,18 @@
"use client";
import {
FileTree,
FileTreeFile,
FileTreeFolder,
} from "@/components/ai-elements/file-tree";
const Example = () => (
<FileTree>
<FileTreeFolder name="src" path="src">
<FileTreeFile name="index.ts" path="src/index.ts" />
</FileTreeFolder>
<FileTreeFile name="package.json" path="package.json" />
</FileTree>
);
export default Example;
@@ -0,0 +1,21 @@
"use client";
import {
FileTree,
FileTreeFile,
FileTreeFolder,
} from "@/components/ai-elements/file-tree";
const Example = () => (
<FileTree defaultExpanded={new Set(["src", "src/components"])}>
<FileTreeFolder name="src" path="src">
<FileTreeFolder name="components" path="src/components">
<FileTreeFile name="button.tsx" path="src/components/button.tsx" />
<FileTreeFile name="input.tsx" path="src/components/input.tsx" />
</FileTreeFolder>
<FileTreeFile name="index.ts" path="src/index.ts" />
</FileTreeFolder>
</FileTree>
);
export default Example;
@@ -0,0 +1,24 @@
"use client";
import {
FileTree,
FileTreeFile,
FileTreeFolder,
} from "@/components/ai-elements/file-tree";
import { useState } from "react";
const Example = () => {
const [selectedPath, setSelectedPath] = useState<string>();
return (
<FileTree onSelect={setSelectedPath} selectedPath={selectedPath}>
<FileTreeFolder name="src" path="src">
<FileTreeFile name="app.tsx" path="src/app.tsx" />
<FileTreeFile name="index.ts" path="src/index.ts" />
</FileTreeFolder>
<FileTreeFile name="package.json" path="package.json" />
</FileTree>
);
};
export default Example;
@@ -0,0 +1,42 @@
"use client";
import {
FileTree,
FileTreeFile,
FileTreeFolder,
} from "@/components/ai-elements/file-tree";
import { useState } from "react";
const Example = () => {
const [selectedPath, setSelectedPath] = useState<string | undefined>();
return (
<FileTree
defaultExpanded={new Set(["src", "src/components"])}
onSelect={setSelectedPath}
selectedPath={selectedPath}
>
<FileTreeFolder name="src" path="src">
<FileTreeFolder name="components" path="src/components">
<FileTreeFile name="button.tsx" path="src/components/button.tsx" />
<FileTreeFile name="input.tsx" path="src/components/input.tsx" />
<FileTreeFile name="modal.tsx" path="src/components/modal.tsx" />
</FileTreeFolder>
<FileTreeFolder name="hooks" path="src/hooks">
<FileTreeFile name="use-auth.ts" path="src/hooks/use-auth.ts" />
<FileTreeFile name="use-theme.ts" path="src/hooks/use-theme.ts" />
</FileTreeFolder>
<FileTreeFolder name="lib" path="src/lib">
<FileTreeFile name="utils.ts" path="src/lib/utils.ts" />
</FileTreeFolder>
<FileTreeFile name="app.tsx" path="src/app.tsx" />
<FileTreeFile name="main.tsx" path="src/main.tsx" />
</FileTreeFolder>
<FileTreeFile name="package.json" path="package.json" />
<FileTreeFile name="tsconfig.json" path="tsconfig.json" />
<FileTreeFile name="README.md" path="README.md" />
</FileTree>
);
};
export default Example;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,97 @@
"use client";
import {
InlineCitation,
InlineCitationCard,
InlineCitationCardBody,
InlineCitationCardTrigger,
InlineCitationCarousel,
InlineCitationCarouselContent,
InlineCitationCarouselHeader,
InlineCitationCarouselIndex,
InlineCitationCarouselItem,
InlineCitationCarouselNext,
InlineCitationCarouselPrev,
InlineCitationSource,
InlineCitationText,
} from "@/components/ai-elements/inline-citation";
const citation = {
sources: [
{
description:
"A comprehensive study on the recent developments in natural language processing technologies and their applications.",
title: "Advances in Natural Language Processing",
url: "https://example.com/nlp-advances",
},
{
description:
"An overview of the most significant machine learning breakthroughs in the past year.",
title: "Breakthroughs in Machine Learning",
url: "https://mlnews.org/breakthroughs",
},
{
description:
"A report on how artificial intelligence is transforming healthcare and diagnostics.",
title: "AI in Healthcare: Current Trends",
url: "https://healthai.com/trends",
},
{
description:
"A discussion on the ethical considerations and challenges in the development of AI.",
title: "Ethics of Artificial Intelligence",
url: "https://aiethics.org/overview",
},
{
description:
"Insights into the technical challenges and solutions for scaling deep learning architectures.",
title: "Scaling Deep Learning Models",
url: "https://deeplearninghub.com/scaling-models",
},
{
description:
"A summary of the latest benchmarks and evaluation metrics for natural language understanding systems.",
title: "Natural Language Understanding Benchmarks",
url: "https://nlubenchmarks.com/latest",
},
],
text: "The technology continues to evolve rapidly, with new breakthroughs being announced regularly",
};
const Example = () => (
<p className="text-sm leading-relaxed">
According to recent studies, artificial intelligence has shown remarkable
progress in natural language processing.{" "}
<InlineCitation>
<InlineCitationText>{citation.text}</InlineCitationText>
<InlineCitationCard>
<InlineCitationCardTrigger
sources={citation.sources.map((source) => source.url)}
/>
<InlineCitationCardBody>
<InlineCitationCarousel>
<InlineCitationCarouselHeader>
<InlineCitationCarouselPrev />
<InlineCitationCarouselNext />
<InlineCitationCarouselIndex />
</InlineCitationCarouselHeader>
<InlineCitationCarouselContent>
{citation.sources.map((source) => (
<InlineCitationCarouselItem key={source.url}>
<InlineCitationSource
description={source.description}
title={source.title}
url={source.url}
/>
</InlineCitationCarouselItem>
))}
</InlineCitationCarouselContent>
</InlineCitationCarousel>
</InlineCitationCardBody>
</InlineCitationCard>
</InlineCitation>
.
</p>
);
export default Example;
@@ -0,0 +1,104 @@
"use client";
import {
JSXPreview,
JSXPreviewContent,
JSXPreviewError,
} from "@/components/ai-elements/jsx-preview";
import { Button } from "@/components/ui/button";
import { useCallback, useEffect, useRef, useState } from "react";
const handleError = (error: Error) => {
console.log("JSX Parse Error:", error);
};
const fullJsx = `<div className="rounded-lg border bg-card p-6 shadow-sm">
<div className="flex items-center gap-4 mb-4">
<div className="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
<span className="text-primary text-xl font-bold">AI</span>
</div>
<div>
<h2 className="text-lg font-semibold">AI-Generated Component</h2>
<p className="text-sm text-muted-foreground">Rendered from JSX string</p>
</div>
</div>
<div className="space-y-3">
<p className="text-sm">This component was dynamically rendered from a JSX string. The JSXPreview component supports streaming mode, automatically closing unclosed tags as content arrives.</p>
<div className="flex gap-2">
<span className="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">React</span>
<span className="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800">Streaming</span>
<span className="inline-flex items-center rounded-full bg-purple-100 px-2.5 py-0.5 text-xs font-medium text-purple-800">Dynamic</span>
</div>
</div>
<div className="mt-4 pt-4 border-t">
<div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Generated just now</span>
<button className="px-3 py-1.5 text-sm bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors">
Learn more
</button>
</div>
</div>
</div>`;
const Example = () => {
const [streamedJsx, setStreamedJsx] = useState(fullJsx);
const [isStreaming, setIsStreaming] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const simulateStreaming = useCallback(() => {
setIsStreaming(true);
setStreamedJsx("");
let index = 0;
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
intervalRef.current = setInterval(() => {
if (index < fullJsx.length) {
setStreamedJsx(fullJsx.slice(0, index + 15));
index += 15;
} else {
setIsStreaming(false);
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
}, 30);
}, []);
useEffect(
() => () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
},
[]
);
return (
<div className="space-y-4">
<Button
disabled={isStreaming}
onClick={simulateStreaming}
size="sm"
variant="outline"
>
{isStreaming ? "Streaming..." : "Simulate Streaming"}
</Button>
<JSXPreview
className="min-h-[200px]"
isStreaming={isStreaming}
jsx={streamedJsx}
onError={handleError}
>
<JSXPreviewContent />
<JSXPreviewError className="mt-2" />
</JSXPreview>
</div>
);
};
export default Example;
@@ -0,0 +1,365 @@
"use client";
import {
Attachment,
AttachmentPreview,
AttachmentRemove,
Attachments,
} from "@/components/ai-elements/attachments";
import {
Message,
MessageAction,
MessageActions,
MessageBranch,
MessageBranchContent,
MessageBranchNext,
MessageBranchPage,
MessageBranchPrevious,
MessageBranchSelector,
MessageContent,
MessageResponse,
MessageToolbar,
} from "@/components/ai-elements/message";
import {
CopyIcon,
RefreshCcwIcon,
ThumbsDownIcon,
ThumbsUpIcon,
} from "lucide-react";
import { nanoid } from "nanoid";
import { memo, useCallback, useState } from "react";
const messages: {
key: string;
from: "user" | "assistant";
versions?: { id: string; content: string }[];
content?: string;
attachments?: {
id: string;
type: "file";
url: string;
mediaType?: string;
filename?: string;
}[];
}[] = [
{
attachments: [
{
filename: "palace-of-fine-arts.jpg",
id: nanoid(),
mediaType: "image/jpeg",
type: "file",
url: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=400&fit=crop",
},
{
filename: "react-hooks-guide.pdf",
id: nanoid(),
mediaType: "application/pdf",
type: "file",
url: "",
},
],
content: "How do React hooks work and when should I use them?",
from: "user",
key: nanoid(),
},
{
from: "assistant",
key: nanoid(),
versions: [
{
content: `# React Hooks Guide
React hooks are functions that let you "hook into" React state and lifecycle features from function components. Here's what you need to know:
## Core Hooks
### useState
Adds state to functional components:
\`\`\`jsx
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
\`\`\`
### useEffect
Handles side effects (data fetching, subscriptions, DOM updates):
\`\`\`jsx
useEffect(() => {
document.title = \`You clicked \${count} times\`;
// Cleanup function (optional)
return () => {
document.title = 'React App';
};
}, [count]); // Dependency array
\`\`\`
## When to Use Hooks
- ✅ **Function components** - Hooks only work in function components
- ✅ **Replacing class components** - Modern React favors hooks over classes
- ✅ **Sharing stateful logic** - Create custom hooks to reuse logic
- ❌ **Class components** - Use lifecycle methods instead
## Rules of Hooks
1. Only call hooks at the **top level** (not inside loops, conditions, or nested functions)
2. Only call hooks from **React functions** (components or custom hooks)
Would you like to explore more advanced hooks like \`useCallback\` or \`useMemo\`?`,
id: nanoid(),
},
{
content: `React hooks are special functions that let you use React features in function components. The most common ones are:
- **useState** - for managing component state
- **useEffect** - for side effects like data fetching
- **useContext** - for consuming context values
- **useRef** - for accessing DOM elements
Here's a simple example:
\`\`\`jsx
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
\`\`\`
Which specific hook would you like to learn more about?`,
id: nanoid(),
},
{
content: `Absolutely! React hooks are a powerful feature introduced in React 16.8. They solve several problems:
## Key Benefits
1. **Simpler code** - No need for class components
2. **Reusable logic** - Extract stateful logic into custom hooks
3. **Better organization** - Group related code together
## Most Popular Hooks
| Hook | Purpose |
|------|---------|
| useState | Add state to components |
| useEffect | Handle side effects |
| useContext | Access context values |
| useReducer | Complex state logic |
| useCallback | Memoize functions |
| useMemo | Memoize values |
The beauty of hooks is that they let you reuse stateful logic without changing your component hierarchy. Want to dive into a specific hook?`,
id: nanoid(),
},
],
},
];
const handleCopy = (content: string) => {
navigator.clipboard.writeText(content);
};
const handleRetry = () => {
console.log("Retrying...");
};
interface LikeActionProps {
messageKey: string;
isLiked: boolean;
onToggle: (key: string) => void;
}
const LikeAction = memo(
({ messageKey, isLiked, onToggle }: LikeActionProps) => {
const handleClick = useCallback(
() => onToggle(messageKey),
[messageKey, onToggle]
);
return (
<MessageAction
label="Like"
onClick={handleClick}
tooltip="Like this response"
>
<ThumbsUpIcon
className="size-4"
fill={isLiked ? "currentColor" : "none"}
/>
</MessageAction>
);
}
);
LikeAction.displayName = "LikeAction";
interface DislikeActionProps {
messageKey: string;
isDisliked: boolean;
onToggle: (key: string) => void;
}
const DislikeAction = memo(
({ messageKey, isDisliked, onToggle }: DislikeActionProps) => {
const handleClick = useCallback(
() => onToggle(messageKey),
[messageKey, onToggle]
);
return (
<MessageAction
label="Dislike"
onClick={handleClick}
tooltip="Dislike this response"
>
<ThumbsDownIcon
className="size-4"
fill={isDisliked ? "currentColor" : "none"}
/>
</MessageAction>
);
}
);
DislikeAction.displayName = "DislikeAction";
interface CopyActionProps {
content: string;
}
const CopyAction = memo(({ content }: CopyActionProps) => {
const handleClick = useCallback(() => handleCopy(content), [content]);
return (
<MessageAction
label="Copy"
onClick={handleClick}
tooltip="Copy to clipboard"
>
<CopyIcon className="size-4" />
</MessageAction>
);
});
CopyAction.displayName = "CopyAction";
const Example = () => {
const [liked, setLiked] = useState<Record<string, boolean>>({});
const [disliked, setDisliked] = useState<Record<string, boolean>>({});
const handleToggleLike = useCallback((key: string) => {
setLiked((prev) => ({ ...prev, [key]: !prev[key] }));
}, []);
const handleToggleDislike = useCallback((key: string) => {
setDisliked((prev) => ({ ...prev, [key]: !prev[key] }));
}, []);
return (
<div className="flex flex-col gap-4">
{/* biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Demo component with complex rendering logic */}
{messages.map((message) => (
<Message from={message.from} key={message.key}>
{message.versions?.length && message.versions.length > 1 ? (
<MessageBranch defaultBranch={0} key={message.key}>
<MessageBranchContent>
{message.versions?.map((version) => (
<MessageContent key={version.id}>
<MessageResponse>{version.content}</MessageResponse>
</MessageContent>
))}
</MessageBranchContent>
{message.from === "assistant" && (
<MessageToolbar>
<MessageBranchSelector>
<MessageBranchPrevious />
<MessageBranchPage />
<MessageBranchNext />
</MessageBranchSelector>
<MessageActions>
<MessageAction
label="Retry"
onClick={handleRetry}
tooltip="Regenerate response"
>
<RefreshCcwIcon className="size-4" />
</MessageAction>
<LikeAction
isLiked={liked[message.key] ?? false}
messageKey={message.key}
onToggle={handleToggleLike}
/>
<DislikeAction
isDisliked={disliked[message.key] ?? false}
messageKey={message.key}
onToggle={handleToggleDislike}
/>
<CopyAction
content={
message.versions?.find((v) => v.id)?.content || ""
}
/>
</MessageActions>
</MessageToolbar>
)}
</MessageBranch>
) : (
<div key={message.key}>
{message.attachments && message.attachments.length > 0 && (
<Attachments className="mb-2" variant="grid">
{message.attachments.map((attachment) => (
<Attachment data={attachment} key={attachment.id}>
<AttachmentPreview />
<AttachmentRemove />
</Attachment>
))}
</Attachments>
)}
<MessageContent>
{message.from === "assistant" ? (
<MessageResponse>{message.content}</MessageResponse>
) : (
message.content
)}
</MessageContent>
{message.from === "assistant" && message.versions && (
<MessageActions>
<MessageAction
label="Retry"
onClick={handleRetry}
tooltip="Regenerate response"
>
<RefreshCcwIcon className="size-4" />
</MessageAction>
<LikeAction
isLiked={liked[message.key] ?? false}
messageKey={message.key}
onToggle={handleToggleLike}
/>
<DislikeAction
isDisliked={disliked[message.key] ?? false}
messageKey={message.key}
onToggle={handleToggleDislike}
/>
<CopyAction content={message.content || ""} />
</MessageActions>
)}
</div>
)}
</Message>
))}
</div>
);
};
export default Example;
@@ -0,0 +1,49 @@
"use client";
import {
MicSelector,
MicSelectorContent,
MicSelectorEmpty,
MicSelectorInput,
MicSelectorItem,
MicSelectorLabel,
MicSelectorList,
MicSelectorTrigger,
MicSelectorValue,
} from "@/components/ai-elements/mic-selector";
const handleOpenChange = (open: boolean) => {
console.log("MicSelector is open?", open);
};
const handleValueChange = (newValue: string) => {
console.log("MicSelector value:", newValue);
};
const Example = () => (
<div className="flex size-full flex-col items-center justify-center gap-4">
<MicSelector
onOpenChange={handleOpenChange}
onValueChange={handleValueChange}
>
<MicSelectorTrigger className="w-full max-w-sm">
<MicSelectorValue />
</MicSelectorTrigger>
<MicSelectorContent>
<MicSelectorInput />
<MicSelectorEmpty />
<MicSelectorList>
{(devices) =>
devices.map((device) => (
<MicSelectorItem key={device.deviceId} value={device.deviceId}>
<MicSelectorLabel device={device} />
</MicSelectorItem>
))
}
</MicSelectorList>
</MicSelectorContent>
</MicSelector>
</div>
);
export default Example;
@@ -0,0 +1,365 @@
"use client";
import {
ModelSelector,
ModelSelectorContent,
ModelSelectorEmpty,
ModelSelectorGroup,
ModelSelectorInput,
ModelSelectorItem,
ModelSelectorList,
ModelSelectorLogo,
ModelSelectorLogoGroup,
ModelSelectorName,
ModelSelectorTrigger,
} from "@/components/ai-elements/model-selector";
import { Button } from "@/components/ui/button";
import { CheckIcon } from "lucide-react";
import { memo, useCallback, useState } from "react";
const models = [
{
chef: "OpenAI",
chefSlug: "openai",
id: "gpt-4o",
name: "GPT-4o",
providers: ["openai", "azure"],
},
{
chef: "OpenAI",
chefSlug: "openai",
id: "gpt-4o-mini",
name: "GPT-4o Mini",
providers: ["openai", "azure"],
},
{
chef: "OpenAI",
chefSlug: "openai",
id: "o1",
name: "o1",
providers: ["openai", "azure"],
},
{
chef: "OpenAI",
chefSlug: "openai",
id: "o1-mini",
name: "o1 Mini",
providers: ["openai", "azure"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-opus-4-20250514",
name: "Claude 4 Opus",
providers: ["anthropic", "azure", "google-vertex", "amazon-bedrock"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-sonnet-4-20250514",
name: "Claude 4 Sonnet",
providers: ["anthropic", "azure", "google-vertex", "amazon-bedrock"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-3.5-sonnet",
name: "Claude 3.5 Sonnet",
providers: ["anthropic", "azure", "google-vertex", "amazon-bedrock"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-3.5-haiku",
name: "Claude 3.5 Haiku",
providers: ["anthropic", "azure", "google-vertex", "amazon-bedrock"],
},
{
chef: "Google",
chefSlug: "google",
id: "gemini-2.0-flash-exp",
name: "Gemini 2.0 Flash",
providers: ["google", "google-vertex"],
},
{
chef: "Google",
chefSlug: "google",
id: "gemini-1.5-pro",
name: "Gemini 1.5 Pro",
providers: ["google", "google-vertex"],
},
{
chef: "Google",
chefSlug: "google",
id: "gemini-1.5-flash",
name: "Gemini 1.5 Flash",
providers: ["google", "google-vertex"],
},
{
chef: "Meta",
chefSlug: "llama",
id: "llama-3.3-70b",
name: "Llama 3.3 70B",
providers: ["groq", "togetherai", "amazon-bedrock"],
},
{
chef: "Meta",
chefSlug: "llama",
id: "llama-3.1-405b",
name: "Llama 3.1 405B",
providers: ["togetherai", "amazon-bedrock"],
},
{
chef: "Meta",
chefSlug: "llama",
id: "llama-3.1-70b",
name: "Llama 3.1 70B",
providers: ["groq", "togetherai", "amazon-bedrock"],
},
{
chef: "Meta",
chefSlug: "llama",
id: "llama-3.1-8b",
name: "Llama 3.1 8B",
providers: ["groq", "togetherai"],
},
{
chef: "DeepSeek",
chefSlug: "deepseek",
id: "deepseek-r1",
name: "DeepSeek R1",
providers: ["deepseek", "openrouter"],
},
{
chef: "DeepSeek",
chefSlug: "deepseek",
id: "deepseek-v3",
name: "DeepSeek V3",
providers: ["deepseek", "openrouter"],
},
{
chef: "DeepSeek",
chefSlug: "deepseek",
id: "deepseek-coder-v2",
name: "DeepSeek Coder V2",
providers: ["deepseek", "openrouter"],
},
{
chef: "Mistral AI",
chefSlug: "mistral",
id: "mistral-large",
name: "Mistral Large",
providers: ["mistral", "azure"],
},
{
chef: "Mistral AI",
chefSlug: "mistral",
id: "mistral-small",
name: "Mistral Small",
providers: ["mistral", "azure"],
},
{
chef: "Mistral AI",
chefSlug: "mistral",
id: "codestral",
name: "Codestral",
providers: ["mistral"],
},
{
chef: "Alibaba",
chefSlug: "alibaba",
id: "qwen-2.5-72b",
name: "Qwen 2.5 72B",
providers: ["alibaba", "openrouter"],
},
{
chef: "Alibaba",
chefSlug: "alibaba",
id: "qwen-2.5-coder-32b",
name: "Qwen 2.5 Coder 32B",
providers: ["alibaba", "openrouter"],
},
{
chef: "Alibaba",
chefSlug: "alibaba",
id: "qwen-max",
name: "Qwen Max",
providers: ["alibaba"],
},
{
chef: "Cohere",
chefSlug: "cohere",
id: "command-r-plus",
name: "Command R+",
providers: ["cohere", "azure", "amazon-bedrock"],
},
{
chef: "Cohere",
chefSlug: "cohere",
id: "command-r",
name: "Command R",
providers: ["cohere", "azure", "amazon-bedrock"],
},
{
chef: "xAI",
chefSlug: "xai",
id: "grok-3",
name: "Grok 3",
providers: ["xai"],
},
{
chef: "xAI",
chefSlug: "xai",
id: "grok-2-1212",
name: "Grok 2 1212",
providers: ["xai"],
},
{
chef: "xAI",
chefSlug: "xai",
id: "grok-vision",
name: "Grok Vision",
providers: ["xai"],
},
{
chef: "Moonshot AI",
chefSlug: "moonshotai",
id: "moonshot-v1-128k",
name: "Moonshot v1 128K",
providers: ["moonshotai"],
},
{
chef: "Moonshot AI",
chefSlug: "moonshotai",
id: "moonshot-v1-32k",
name: "Moonshot v1 32K",
providers: ["moonshotai"],
},
{
chef: "Perplexity",
chefSlug: "perplexity",
id: "sonar-pro",
name: "Sonar Pro",
providers: ["perplexity"],
},
{
chef: "Perplexity",
chefSlug: "perplexity",
id: "sonar",
name: "Sonar",
providers: ["perplexity"],
},
{
chef: "Vercel",
chefSlug: "v0",
id: "v0-chat",
name: "v0 Chat",
providers: ["vercel"],
},
{
chef: "Amazon",
chefSlug: "amazon-bedrock",
id: "nova-pro",
name: "Nova Pro",
providers: ["amazon-bedrock"],
},
{
chef: "Amazon",
chefSlug: "amazon-bedrock",
id: "nova-lite",
name: "Nova Lite",
providers: ["amazon-bedrock"],
},
{
chef: "Amazon",
chefSlug: "amazon-bedrock",
id: "nova-micro",
name: "Nova Micro",
providers: ["amazon-bedrock"],
},
];
interface ModelItemProps {
model: (typeof models)[0];
selectedModel: string;
onSelect: (id: string) => void;
}
const ModelItem = memo(({ model, selectedModel, onSelect }: ModelItemProps) => {
const handleSelect = useCallback(
() => onSelect(model.id),
[onSelect, model.id]
);
return (
<ModelSelectorItem key={model.id} onSelect={handleSelect} value={model.id}>
<ModelSelectorLogo provider={model.chefSlug} />
<ModelSelectorName>{model.name}</ModelSelectorName>
<ModelSelectorLogoGroup>
{model.providers.map((provider) => (
<ModelSelectorLogo key={provider} provider={provider} />
))}
</ModelSelectorLogoGroup>
{selectedModel === model.id ? (
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</ModelSelectorItem>
);
});
ModelItem.displayName = "ModelItem";
const Example = () => {
const [open, setOpen] = useState(false);
const [selectedModel, setSelectedModel] = useState<string>("gpt-4o");
const handleModelSelect = useCallback((id: string) => {
setSelectedModel(id);
setOpen(false);
}, []);
const selectedModelData = models.find((model) => model.id === selectedModel);
// Get unique chefs in order of appearance
const chefs = [...new Set(models.map((model) => model.chef))];
return (
<div className="flex size-full items-center justify-center p-8">
<ModelSelector onOpenChange={setOpen} open={open}>
<ModelSelectorTrigger asChild>
<Button className="w-[200px] justify-between" variant="outline">
{selectedModelData?.chefSlug && (
<ModelSelectorLogo provider={selectedModelData.chefSlug} />
)}
{selectedModelData?.name && (
<ModelSelectorName>{selectedModelData.name}</ModelSelectorName>
)}
</Button>
</ModelSelectorTrigger>
<ModelSelectorContent>
<ModelSelectorInput placeholder="Search models..." />
<ModelSelectorList>
<ModelSelectorEmpty>No models found.</ModelSelectorEmpty>
{chefs.map((chef) => (
<ModelSelectorGroup heading={chef} key={chef}>
{models
.filter((model) => model.chef === chef)
.map((model) => (
<ModelItem
key={model.id}
model={model}
onSelect={handleModelSelect}
selectedModel={selectedModel}
/>
))}
</ModelSelectorGroup>
))}
</ModelSelectorList>
</ModelSelectorContent>
</ModelSelector>
</div>
);
};
export default Example;
@@ -0,0 +1,33 @@
"use client";
import {
OpenIn,
OpenInChatGPT,
OpenInClaude,
OpenInContent,
OpenInCursor,
OpenInScira,
OpenInT3,
OpenInTrigger,
OpenInv0,
} from "@/components/ai-elements/open-in-chat";
const Example = () => {
const sampleQuery = "How can I implement authentication in Next.js?";
return (
<OpenIn>
<OpenInTrigger />
<OpenInContent>
<OpenInChatGPT query={sampleQuery} />
<OpenInClaude query={sampleQuery} />
<OpenInCursor query={sampleQuery} />
<OpenInT3 query={sampleQuery} />
<OpenInScira query={sampleQuery} />
<OpenInv0 query={sampleQuery} />
</OpenInContent>
</OpenIn>
);
};
export default Example;
@@ -0,0 +1,51 @@
"use client";
import {
PackageInfo,
PackageInfoChangeType,
PackageInfoContent,
PackageInfoDependencies,
PackageInfoDependency,
PackageInfoDescription,
PackageInfoHeader,
PackageInfoName,
PackageInfoVersion,
} from "@/components/ai-elements/package-info";
const Example = () => (
<div className="flex flex-col gap-4">
<PackageInfo
changeType="major"
currentVersion="18.2.0"
name="react"
newVersion="19.0.0"
>
<PackageInfoHeader>
<PackageInfoName />
<PackageInfoChangeType />
</PackageInfoHeader>
<PackageInfoVersion />
<PackageInfoDescription>
A JavaScript library for building user interfaces.
</PackageInfoDescription>
<PackageInfoContent>
<PackageInfoDependencies>
<PackageInfoDependency name="react-dom" version="^19.0.0" />
<PackageInfoDependency name="scheduler" version="^0.24.0" />
</PackageInfoDependencies>
</PackageInfoContent>
</PackageInfo>
<PackageInfo changeType="added" name="lodash">
<PackageInfoHeader>
<PackageInfoName />
<PackageInfoChangeType />
</PackageInfoHeader>
<PackageInfoVersion />
</PackageInfo>
<PackageInfo changeType="removed" currentVersion="2.29.4" name="moment" />
</div>
);
export default Example;
@@ -0,0 +1,110 @@
"use client";
import type { PersonaState } from "@/components/ai-elements/persona";
import { Persona } from "@/components/ai-elements/persona";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { LucideIcon } from "lucide-react";
import {
BrainIcon,
CircleIcon,
EyeClosedIcon,
MegaphoneIcon,
MicIcon,
} from "lucide-react";
import { memo, useCallback, useState } from "react";
const states: {
state: PersonaState;
icon: LucideIcon;
label: string;
}[] = [
{
icon: CircleIcon,
label: "Idle",
state: "idle",
},
{
icon: MicIcon,
label: "Listening",
state: "listening",
},
{
icon: BrainIcon,
label: "Thinking",
state: "thinking",
},
{
icon: MegaphoneIcon,
label: "Speaking",
state: "speaking",
},
{
icon: EyeClosedIcon,
label: "Asleep",
state: "asleep",
},
];
interface StateButtonProps {
state: (typeof states)[0];
currentState: PersonaState;
onStateChange: (state: PersonaState) => void;
}
const StateButton = memo(
({ state, currentState, onStateChange }: StateButtonProps) => {
const handleClick = useCallback(
() => onStateChange(state.state),
[onStateChange, state.state]
);
return (
<Tooltip key={state.state}>
<TooltipTrigger asChild>
<Button
onClick={handleClick}
size="icon-sm"
variant={currentState === state.state ? "default" : "outline"}
>
<state.icon className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{state.label}</TooltipContent>
</Tooltip>
);
}
);
StateButton.displayName = "StateButton";
const Example = () => {
const [currentState, setCurrentState] = useState<PersonaState>("idle");
const handleStateChange = useCallback((state: PersonaState) => {
setCurrentState(state);
}, []);
return (
<div className="flex size-full flex-col items-center justify-center gap-4">
<Persona className="size-32" state={currentState} variant="command" />
<ButtonGroup orientation="horizontal">
{states.map((state) => (
<StateButton
currentState={currentState}
key={state.state}
onStateChange={handleStateChange}
state={state}
/>
))}
</ButtonGroup>
</div>
);
};
export default Example;
@@ -0,0 +1,110 @@
"use client";
import type { PersonaState } from "@/components/ai-elements/persona";
import { Persona } from "@/components/ai-elements/persona";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { LucideIcon } from "lucide-react";
import {
BrainIcon,
CircleIcon,
EyeClosedIcon,
MegaphoneIcon,
MicIcon,
} from "lucide-react";
import { memo, useCallback, useState } from "react";
const states: {
state: PersonaState;
icon: LucideIcon;
label: string;
}[] = [
{
icon: CircleIcon,
label: "Idle",
state: "idle",
},
{
icon: MicIcon,
label: "Listening",
state: "listening",
},
{
icon: BrainIcon,
label: "Thinking",
state: "thinking",
},
{
icon: MegaphoneIcon,
label: "Speaking",
state: "speaking",
},
{
icon: EyeClosedIcon,
label: "Asleep",
state: "asleep",
},
];
interface StateButtonProps {
state: (typeof states)[0];
currentState: PersonaState;
onStateChange: (state: PersonaState) => void;
}
const StateButton = memo(
({ state, currentState, onStateChange }: StateButtonProps) => {
const handleClick = useCallback(
() => onStateChange(state.state),
[onStateChange, state.state]
);
return (
<Tooltip key={state.state}>
<TooltipTrigger asChild>
<Button
onClick={handleClick}
size="icon-sm"
variant={currentState === state.state ? "default" : "outline"}
>
<state.icon className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{state.label}</TooltipContent>
</Tooltip>
);
}
);
StateButton.displayName = "StateButton";
const Example = () => {
const [currentState, setCurrentState] = useState<PersonaState>("idle");
const handleStateChange = useCallback((state: PersonaState) => {
setCurrentState(state);
}, []);
return (
<div className="flex size-full flex-col items-center justify-center gap-4">
<Persona className="size-32" state={currentState} variant="glint" />
<ButtonGroup orientation="horizontal">
{states.map((state) => (
<StateButton
currentState={currentState}
key={state.state}
onStateChange={handleStateChange}
state={state}
/>
))}
</ButtonGroup>
</div>
);
};
export default Example;
@@ -0,0 +1,110 @@
"use client";
import type { PersonaState } from "@/components/ai-elements/persona";
import { Persona } from "@/components/ai-elements/persona";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { LucideIcon } from "lucide-react";
import {
BrainIcon,
CircleIcon,
EyeClosedIcon,
MegaphoneIcon,
MicIcon,
} from "lucide-react";
import { memo, useCallback, useState } from "react";
const states: {
state: PersonaState;
icon: LucideIcon;
label: string;
}[] = [
{
icon: CircleIcon,
label: "Idle",
state: "idle",
},
{
icon: MicIcon,
label: "Listening",
state: "listening",
},
{
icon: BrainIcon,
label: "Thinking",
state: "thinking",
},
{
icon: MegaphoneIcon,
label: "Speaking",
state: "speaking",
},
{
icon: EyeClosedIcon,
label: "Asleep",
state: "asleep",
},
];
interface StateButtonProps {
state: (typeof states)[0];
currentState: PersonaState;
onStateChange: (state: PersonaState) => void;
}
const StateButton = memo(
({ state, currentState, onStateChange }: StateButtonProps) => {
const handleClick = useCallback(
() => onStateChange(state.state),
[onStateChange, state.state]
);
return (
<Tooltip key={state.state}>
<TooltipTrigger asChild>
<Button
onClick={handleClick}
size="icon-sm"
variant={currentState === state.state ? "default" : "outline"}
>
<state.icon className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{state.label}</TooltipContent>
</Tooltip>
);
}
);
StateButton.displayName = "StateButton";
const Example = () => {
const [currentState, setCurrentState] = useState<PersonaState>("idle");
const handleStateChange = useCallback((state: PersonaState) => {
setCurrentState(state);
}, []);
return (
<div className="flex size-full flex-col items-center justify-center gap-4">
<Persona className="size-32" state={currentState} variant="halo" />
<ButtonGroup orientation="horizontal">
{states.map((state) => (
<StateButton
currentState={currentState}
key={state.state}
onStateChange={handleStateChange}
state={state}
/>
))}
</ButtonGroup>
</div>
);
};
export default Example;
@@ -0,0 +1,110 @@
"use client";
import type { PersonaState } from "@/components/ai-elements/persona";
import { Persona } from "@/components/ai-elements/persona";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { LucideIcon } from "lucide-react";
import {
BrainIcon,
CircleIcon,
EyeClosedIcon,
MegaphoneIcon,
MicIcon,
} from "lucide-react";
import { memo, useCallback, useState } from "react";
const states: {
state: PersonaState;
icon: LucideIcon;
label: string;
}[] = [
{
icon: CircleIcon,
label: "Idle",
state: "idle",
},
{
icon: MicIcon,
label: "Listening",
state: "listening",
},
{
icon: BrainIcon,
label: "Thinking",
state: "thinking",
},
{
icon: MegaphoneIcon,
label: "Speaking",
state: "speaking",
},
{
icon: EyeClosedIcon,
label: "Asleep",
state: "asleep",
},
];
interface StateButtonProps {
state: (typeof states)[0];
currentState: PersonaState;
onStateChange: (state: PersonaState) => void;
}
const StateButton = memo(
({ state, currentState, onStateChange }: StateButtonProps) => {
const handleClick = useCallback(
() => onStateChange(state.state),
[onStateChange, state.state]
);
return (
<Tooltip key={state.state}>
<TooltipTrigger asChild>
<Button
onClick={handleClick}
size="icon-sm"
variant={currentState === state.state ? "default" : "outline"}
>
<state.icon className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{state.label}</TooltipContent>
</Tooltip>
);
}
);
StateButton.displayName = "StateButton";
const Example = () => {
const [currentState, setCurrentState] = useState<PersonaState>("idle");
const handleStateChange = useCallback((state: PersonaState) => {
setCurrentState(state);
}, []);
return (
<div className="flex size-full flex-col items-center justify-center gap-4">
<Persona className="size-32" state={currentState} variant="mana" />
<ButtonGroup orientation="horizontal">
{states.map((state) => (
<StateButton
currentState={currentState}
key={state.state}
onStateChange={handleStateChange}
state={state}
/>
))}
</ButtonGroup>
</div>
);
};
export default Example;
@@ -0,0 +1,110 @@
"use client";
import type { PersonaState } from "@/components/ai-elements/persona";
import { Persona } from "@/components/ai-elements/persona";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { LucideIcon } from "lucide-react";
import {
BrainIcon,
CircleIcon,
EyeClosedIcon,
MegaphoneIcon,
MicIcon,
} from "lucide-react";
import { memo, useCallback, useState } from "react";
const states: {
state: PersonaState;
icon: LucideIcon;
label: string;
}[] = [
{
icon: CircleIcon,
label: "Idle",
state: "idle",
},
{
icon: MicIcon,
label: "Listening",
state: "listening",
},
{
icon: BrainIcon,
label: "Thinking",
state: "thinking",
},
{
icon: MegaphoneIcon,
label: "Speaking",
state: "speaking",
},
{
icon: EyeClosedIcon,
label: "Asleep",
state: "asleep",
},
];
interface StateButtonProps {
state: (typeof states)[0];
currentState: PersonaState;
onStateChange: (state: PersonaState) => void;
}
const StateButton = memo(
({ state, currentState, onStateChange }: StateButtonProps) => {
const handleClick = useCallback(
() => onStateChange(state.state),
[onStateChange, state.state]
);
return (
<Tooltip key={state.state}>
<TooltipTrigger asChild>
<Button
onClick={handleClick}
size="icon-sm"
variant={currentState === state.state ? "default" : "outline"}
>
<state.icon className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{state.label}</TooltipContent>
</Tooltip>
);
}
);
StateButton.displayName = "StateButton";
const Example = () => {
const [currentState, setCurrentState] = useState<PersonaState>("idle");
const handleStateChange = useCallback((state: PersonaState) => {
setCurrentState(state);
}, []);
return (
<div className="flex size-full flex-col items-center justify-center gap-4">
<Persona className="size-32" state={currentState} variant="obsidian" />
<ButtonGroup orientation="horizontal">
{states.map((state) => (
<StateButton
currentState={currentState}
key={state.state}
onStateChange={handleStateChange}
state={state}
/>
))}
</ButtonGroup>
</div>
);
};
export default Example;
@@ -0,0 +1,110 @@
"use client";
import type { PersonaState } from "@/components/ai-elements/persona";
import { Persona } from "@/components/ai-elements/persona";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { LucideIcon } from "lucide-react";
import {
BrainIcon,
CircleIcon,
EyeClosedIcon,
MegaphoneIcon,
MicIcon,
} from "lucide-react";
import { memo, useCallback, useState } from "react";
interface StateButtonProps {
state: { state: PersonaState; icon: LucideIcon; label: string };
currentState: PersonaState;
onStateChange: (state: PersonaState) => void;
}
const StateButton = memo(
({ state, currentState, onStateChange }: StateButtonProps) => {
const handleClick = useCallback(
() => onStateChange(state.state),
[onStateChange, state.state]
);
return (
<Tooltip key={state.state}>
<TooltipTrigger asChild>
<Button
onClick={handleClick}
size="icon-sm"
variant={currentState === state.state ? "default" : "outline"}
>
<state.icon className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{state.label}</TooltipContent>
</Tooltip>
);
}
);
StateButton.displayName = "StateButton";
const states: {
state: PersonaState;
icon: LucideIcon;
label: string;
}[] = [
{
icon: CircleIcon,
label: "Idle",
state: "idle",
},
{
icon: MicIcon,
label: "Listening",
state: "listening",
},
{
icon: BrainIcon,
label: "Thinking",
state: "thinking",
},
{
icon: MegaphoneIcon,
label: "Speaking",
state: "speaking",
},
{
icon: EyeClosedIcon,
label: "Asleep",
state: "asleep",
},
];
const Example = () => {
const [currentState, setCurrentState] = useState<PersonaState>("idle");
const handleStateChange = useCallback((state: PersonaState) => {
setCurrentState(state);
}, []);
return (
<div className="flex size-full flex-col items-center justify-center gap-4">
<Persona className="size-32" state={currentState} variant="opal" />
<ButtonGroup orientation="horizontal">
{states.map((state) => (
<StateButton
currentState={currentState}
key={state.state}
onStateChange={handleStateChange}
state={state}
/>
))}
</ButtonGroup>
</div>
);
};
export default Example;
@@ -0,0 +1,65 @@
"use client";
import {
Plan,
PlanAction,
PlanContent,
PlanDescription,
PlanFooter,
PlanHeader,
PlanTitle,
PlanTrigger,
} from "@/components/ai-elements/plan";
import { Button } from "@/components/ui/button";
import { FileText } from "lucide-react";
const Example = () => (
<Plan defaultOpen={false}>
<PlanHeader>
<div>
<div className="mb-4 flex items-center gap-2">
<FileText className="size-4" />
<PlanTitle>Rewrite AI Elements to SolidJS</PlanTitle>
</div>
<PlanDescription>
Rewrite the AI Elements component library from React to SolidJS while
maintaining compatibility with existing React-based shadcn/ui
components using solid-js/compat, updating all 29 components and their
test suite.
</PlanDescription>
</div>
<PlanTrigger />
</PlanHeader>
<PlanContent>
<div className="space-y-4 text-sm">
<div>
<h3 className="mb-2 font-semibold">Overview</h3>
<p>
This plan outlines the migration strategy for converting the AI
Elements library from React to SolidJS, ensuring compatibility and
maintaining existing functionality.
</p>
</div>
<div>
<h3 className="mb-2 font-semibold">Key Steps</h3>
<ul className="list-inside list-disc space-y-1">
<li>Set up SolidJS project structure</li>
<li>Install solid-js/compat for React compatibility</li>
<li>Migrate components one by one</li>
<li>Update test suite for each component</li>
<li>Verify compatibility with shadcn/ui</li>
</ul>
</div>
</div>
</PlanContent>
<PlanFooter className="justify-end">
<PlanAction>
<Button size="sm">
Build <kbd className="font-mono"></kbd>
</Button>
</PlanAction>
</PlanFooter>
</Plan>
);
export default Example;
@@ -0,0 +1,502 @@
"use client";
import type { AttachmentData } from "@/components/ai-elements/attachments";
import {
Attachment,
AttachmentInfo,
AttachmentPreview,
AttachmentRemove,
Attachments,
} from "@/components/ai-elements/attachments";
import {
ModelSelector,
ModelSelectorContent,
ModelSelectorEmpty,
ModelSelectorGroup,
ModelSelectorInput,
ModelSelectorItem,
ModelSelectorList,
ModelSelectorLogo,
ModelSelectorLogoGroup,
ModelSelectorName,
ModelSelectorTrigger,
} from "@/components/ai-elements/model-selector";
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
import {
PromptInput,
PromptInputBody,
PromptInputButton,
PromptInputCommand,
PromptInputCommandEmpty,
PromptInputCommandGroup,
PromptInputCommandInput,
PromptInputCommandItem,
PromptInputCommandList,
PromptInputCommandSeparator,
PromptInputFooter,
PromptInputHeader,
PromptInputHoverCard,
PromptInputHoverCardContent,
PromptInputHoverCardTrigger,
PromptInputProvider,
PromptInputSubmit,
PromptInputTab,
PromptInputTabBody,
PromptInputTabItem,
PromptInputTabLabel,
PromptInputTextarea,
PromptInputTools,
usePromptInputAttachments,
usePromptInputReferencedSources,
} from "@/components/ai-elements/prompt-input";
import { Button } from "@/components/ui/button";
import type { SourceDocumentUIPart } from "ai";
import {
AtSignIcon,
CheckIcon,
FilesIcon,
GlobeIcon,
ImageIcon,
RulerIcon,
} from "lucide-react";
import { memo, useCallback, useState } from "react";
const models = [
{
chef: "OpenAI",
chefSlug: "openai",
id: "gpt-4o",
name: "GPT-4o",
providers: ["openai", "azure"],
},
{
chef: "OpenAI",
chefSlug: "openai",
id: "gpt-4o-mini",
name: "GPT-4o Mini",
providers: ["openai", "azure"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-opus-4-20250514",
name: "Claude 4 Opus",
providers: ["anthropic", "azure", "google", "amazon-bedrock"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-sonnet-4-20250514",
name: "Claude 4 Sonnet",
providers: ["anthropic", "azure", "google", "amazon-bedrock"],
},
{
chef: "Google",
chefSlug: "google",
id: "gemini-2.0-flash-exp",
name: "Gemini 2.0 Flash",
providers: ["google"],
},
];
const SUBMITTING_TIMEOUT = 200;
const STREAMING_TIMEOUT = 2000;
interface AttachmentItemProps {
attachment: AttachmentData;
onRemove: (id: string) => void;
}
const AttachmentItem = memo(({ attachment, onRemove }: AttachmentItemProps) => {
const handleRemove = useCallback(
() => onRemove(attachment.id),
[onRemove, attachment.id]
);
return (
<Attachment data={attachment} key={attachment.id} onRemove={handleRemove}>
<AttachmentPreview />
<AttachmentRemove />
</Attachment>
);
});
AttachmentItem.displayName = "AttachmentItem";
interface SourceItemProps {
source: AttachmentData;
onRemove: (id: string) => void;
}
const SourceItem = memo(({ source, onRemove }: SourceItemProps) => {
const handleRemove = useCallback(
() => onRemove(source.id),
[onRemove, source.id]
);
return (
<Attachment data={source} key={source.id} onRemove={handleRemove}>
<AttachmentPreview />
<AttachmentInfo />
<AttachmentRemove />
</Attachment>
);
});
SourceItem.displayName = "SourceItem";
interface ModelItemProps {
m: (typeof models)[0];
selectedModel: string;
onSelect: (id: string) => void;
}
const ModelItem = memo(({ m, selectedModel, onSelect }: ModelItemProps) => {
const handleSelect = useCallback(() => onSelect(m.id), [onSelect, m.id]);
return (
<ModelSelectorItem key={m.id} onSelect={handleSelect} value={m.id}>
<ModelSelectorLogo provider={m.chefSlug} />
<ModelSelectorName>{m.name}</ModelSelectorName>
<ModelSelectorLogoGroup>
{m.providers.map((provider) => (
<ModelSelectorLogo key={provider} provider={provider} />
))}
</ModelSelectorLogoGroup>
{selectedModel === m.id ? (
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</ModelSelectorItem>
);
});
ModelItem.displayName = "ModelItem";
interface SourceCommandItemProps {
source: SourceDocumentUIPart;
onAdd: (source: SourceDocumentUIPart) => void;
}
const SourceCommandItem = memo(({ source, onAdd }: SourceCommandItemProps) => {
const handleSelect = useCallback(() => onAdd(source), [onAdd, source]);
return (
<PromptInputCommandItem
key={`${source.filename}-${source.title}`}
onSelect={handleSelect}
>
<GlobeIcon className="text-primary" />
<div className="flex flex-col">
<span className="font-medium text-sm">{source.title}</span>
<span className="text-muted-foreground text-xs">{source.filename}</span>
</div>
</PromptInputCommandItem>
);
});
SourceCommandItem.displayName = "SourceCommandItem";
const sampleSources: SourceDocumentUIPart[] = [
{
filename: "packages/elements/src",
mediaType: "text/plain",
sourceId: "1",
title: "prompt-input.tsx",
type: "source-document",
},
{
filename: "apps/test/app/examples",
mediaType: "text/plain",
sourceId: "2",
title: "queue.tsx",
type: "source-document",
},
{
filename: "packages/elements/src",
mediaType: "text/plain",
sourceId: "3",
title: "queue.tsx",
type: "source-document",
},
];
const sampleTabs = {
active: [{ path: "packages/elements/src/task-queue-panel.tsx" }],
recents: [
{ path: "apps/test/app/examples/task-queue-panel.tsx" },
{ path: "apps/test/app/page.tsx" },
{ path: "packages/elements/src/task.tsx" },
{ path: "apps/test/app/examples/prompt-input.tsx" },
{ path: "packages/elements/src/queue.tsx" },
{ path: "apps/test/app/examples/queue.tsx" },
],
};
const PromptInputAttachmentsDisplay = () => {
const attachments = usePromptInputAttachments();
const handleRemove = useCallback(
(id: string) => attachments.remove(id),
[attachments]
);
if (attachments.files.length === 0) {
return null;
}
return (
<Attachments variant="inline">
{attachments.files.map((attachment) => (
<AttachmentItem
attachment={attachment}
key={attachment.id}
onRemove={handleRemove}
/>
))}
</Attachments>
);
};
const PromptInputReferencedSourcesDisplay = () => {
const refs = usePromptInputReferencedSources();
const handleRemove = useCallback((id: string) => refs.remove(id), [refs]);
if (refs.sources.length === 0) {
return null;
}
return (
<Attachments variant="inline">
{refs.sources.map((source) => (
<SourceItem
key={source.id}
onRemove={handleRemove}
source={source as AttachmentData}
/>
))}
</Attachments>
);
};
const SampleFilesMenu = () => {
const refs = usePromptInputReferencedSources();
const handleAdd = useCallback(
(source: SourceDocumentUIPart) => refs.add(source),
[refs]
);
return (
<PromptInputCommand>
<PromptInputCommandInput
className="border-none focus-visible:ring-0"
placeholder="Add files, folders, docs..."
/>
<PromptInputCommandList>
<PromptInputCommandEmpty className="p-3 text-muted-foreground text-sm">
No results found.
</PromptInputCommandEmpty>
<PromptInputCommandGroup heading="Added">
<PromptInputCommandItem>
<GlobeIcon />
<span>Active Tabs</span>
<span className="ml-auto text-muted-foreground"></span>
</PromptInputCommandItem>
</PromptInputCommandGroup>
<PromptInputCommandSeparator />
<PromptInputCommandGroup heading="Other Files">
{sampleSources
.filter(
(source) =>
!refs.sources.some(
(s) =>
s.title === source.title && s.filename === source.filename
)
)
.map((source) => (
<SourceCommandItem
key={`${source.filename}-${source.title}`}
onAdd={handleAdd}
source={source}
/>
))}
</PromptInputCommandGroup>
</PromptInputCommandList>
</PromptInputCommand>
);
};
const Example = () => {
const [model, setModel] = useState<string>(models[0].id);
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
const [status, setStatus] = useState<
"submitted" | "streaming" | "ready" | "error"
>("ready");
const selectedModelData = models.find((m) => m.id === model);
const handleModelSelect = useCallback((id: string) => {
setModel(id);
setModelSelectorOpen(false);
}, []);
const handleSubmit = useCallback((message: PromptInputMessage) => {
const hasText = Boolean(message.text);
const hasAttachments = Boolean(message.files?.length);
if (!(hasText || hasAttachments)) {
return;
}
setStatus("submitted");
setTimeout(() => {
setStatus("streaming");
}, SUBMITTING_TIMEOUT);
setTimeout(() => {
setStatus("ready");
}, STREAMING_TIMEOUT);
}, []);
return (
<div className="flex size-full flex-col justify-end">
<PromptInputProvider>
<PromptInput globalDrop multiple onSubmit={handleSubmit}>
<PromptInputHeader>
<PromptInputHoverCard>
<PromptInputHoverCardTrigger>
<PromptInputButton
className="h-8!"
size="icon-sm"
variant="outline"
>
<AtSignIcon className="text-muted-foreground" size={12} />
</PromptInputButton>
</PromptInputHoverCardTrigger>
<PromptInputHoverCardContent className="w-[400px] p-0">
<SampleFilesMenu />
</PromptInputHoverCardContent>
</PromptInputHoverCard>
<PromptInputHoverCard>
<PromptInputHoverCardTrigger>
<PromptInputButton size="sm" variant="outline">
<RulerIcon className="text-muted-foreground" size={12} />
<span>1</span>
</PromptInputButton>
</PromptInputHoverCardTrigger>
<PromptInputHoverCardContent className="divide-y overflow-hidden p-0">
<div className="space-y-2 p-3">
<p className="font-medium text-muted-foreground text-sm">
Attached Project Rules
</p>
<p className="ml-4 text-muted-foreground text-sm">
Always Apply:
</p>
<p className="ml-8 text-sm">ultracite.mdc</p>
</div>
<p className="bg-sidebar px-4 py-3 text-muted-foreground text-sm">
Click to manage
</p>
</PromptInputHoverCardContent>
</PromptInputHoverCard>
<PromptInputHoverCard>
<PromptInputHoverCardTrigger>
<PromptInputButton size="sm" variant="outline">
<FilesIcon className="text-muted-foreground" size={12} />
<span>1 Tab</span>
</PromptInputButton>
</PromptInputHoverCardTrigger>
<PromptInputHoverCardContent className="w-[300px] space-y-4 px-0 py-3">
<PromptInputTab>
<PromptInputTabLabel>Active Tabs</PromptInputTabLabel>
<PromptInputTabBody>
{sampleTabs.active.map((tab) => (
<PromptInputTabItem key={tab.path}>
<GlobeIcon className="text-primary" size={16} />
<span className="truncate" dir="rtl">
{tab.path}
</span>
</PromptInputTabItem>
))}
</PromptInputTabBody>
</PromptInputTab>
<PromptInputTab>
<PromptInputTabLabel>Recents</PromptInputTabLabel>
<PromptInputTabBody>
{sampleTabs.recents.map((tab) => (
<PromptInputTabItem key={tab.path}>
<GlobeIcon className="text-primary" size={16} />
<span className="truncate" dir="rtl">
{tab.path}
</span>
</PromptInputTabItem>
))}
</PromptInputTabBody>
</PromptInputTab>
<div className="border-t px-3 pt-2 text-muted-foreground text-xs">
Only file paths are included
</div>
</PromptInputHoverCardContent>
</PromptInputHoverCard>
<PromptInputAttachmentsDisplay />
<PromptInputReferencedSourcesDisplay />
</PromptInputHeader>
<PromptInputBody>
<PromptInputTextarea placeholder="Plan, search, build anything" />
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<ModelSelector
onOpenChange={setModelSelectorOpen}
open={modelSelectorOpen}
>
<ModelSelectorTrigger asChild>
<PromptInputButton>
{selectedModelData?.chefSlug && (
<ModelSelectorLogo
provider={selectedModelData.chefSlug}
/>
)}
{selectedModelData?.name && (
<ModelSelectorName>
{selectedModelData.name}
</ModelSelectorName>
)}
</PromptInputButton>
</ModelSelectorTrigger>
<ModelSelectorContent>
<ModelSelectorInput placeholder="Search models..." />
<ModelSelectorList>
<ModelSelectorEmpty>No models found.</ModelSelectorEmpty>
{["OpenAI", "Anthropic", "Google"].map((chef) => (
<ModelSelectorGroup heading={chef} key={chef}>
{models
.filter((m) => m.chef === chef)
.map((m) => (
<ModelItem
key={m.id}
m={m}
onSelect={handleModelSelect}
selectedModel={model}
/>
))}
</ModelSelectorGroup>
))}
</ModelSelectorList>
</ModelSelectorContent>
</ModelSelector>
</PromptInputTools>
<div className="flex items-center gap-2">
<Button size="icon-sm" variant="ghost">
<ImageIcon className="text-muted-foreground" size={16} />
</Button>
<PromptInputSubmit className="!h-8" status={status} />
</div>
</PromptInputFooter>
</PromptInput>
</PromptInputProvider>
</div>
);
};
export default Example;
@@ -0,0 +1,44 @@
"use client";
import {
PromptInput,
PromptInputBody,
PromptInputButton,
PromptInputFooter,
PromptInputSubmit,
PromptInputTextarea,
PromptInputTools,
} from "@/components/ai-elements/prompt-input";
import { GlobeIcon, MicIcon, PaperclipIcon } from "lucide-react";
const handleSubmit = () => {
// Handle submit
};
const Example = () => (
<PromptInput onSubmit={handleSubmit}>
<PromptInputBody>
<PromptInputTextarea />
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<PromptInputButton tooltip="Attach files">
<PaperclipIcon size={16} />
</PromptInputButton>
<PromptInputButton
tooltip={{ content: "Search the web", shortcut: "⌘K" }}
>
<GlobeIcon size={16} />
</PromptInputButton>
<PromptInputButton
tooltip={{ content: "Voice input", shortcut: "⌘M", side: "bottom" }}
>
<MicIcon size={16} />
</PromptInputButton>
</PromptInputTools>
<PromptInputSubmit />
</PromptInputFooter>
</PromptInput>
);
export default Example;
@@ -0,0 +1,267 @@
"use client";
import {
Attachment,
AttachmentPreview,
AttachmentRemove,
Attachments,
} from "@/components/ai-elements/attachments";
import {
ModelSelector,
ModelSelectorContent,
ModelSelectorEmpty,
ModelSelectorGroup,
ModelSelectorInput,
ModelSelectorItem,
ModelSelectorList,
ModelSelectorLogo,
ModelSelectorLogoGroup,
ModelSelectorName,
ModelSelectorTrigger,
} from "@/components/ai-elements/model-selector";
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
import {
PromptInput,
PromptInputActionAddAttachments,
PromptInputActionAddScreenshot,
PromptInputActionMenu,
PromptInputActionMenuContent,
PromptInputActionMenuTrigger,
PromptInputBody,
PromptInputButton,
PromptInputFooter,
PromptInputProvider,
PromptInputSubmit,
PromptInputTextarea,
PromptInputTools,
usePromptInputAttachments,
} from "@/components/ai-elements/prompt-input";
import { CheckIcon, GlobeIcon } from "lucide-react";
import { memo, useCallback, useState } from "react";
const models = [
{
chef: "OpenAI",
chefSlug: "openai",
id: "gpt-4o",
name: "GPT-4o",
providers: ["openai", "azure"],
},
{
chef: "OpenAI",
chefSlug: "openai",
id: "gpt-4o-mini",
name: "GPT-4o Mini",
providers: ["openai", "azure"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-opus-4-20250514",
name: "Claude 4 Opus",
providers: ["anthropic", "azure", "google", "amazon-bedrock"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-sonnet-4-20250514",
name: "Claude 4 Sonnet",
providers: ["anthropic", "azure", "google", "amazon-bedrock"],
},
{
chef: "Google",
chefSlug: "google",
id: "gemini-2.0-flash-exp",
name: "Gemini 2.0 Flash",
providers: ["google"],
},
];
const SUBMITTING_TIMEOUT = 200;
const STREAMING_TIMEOUT = 2000;
interface AttachmentItemProps {
attachment: {
id: string;
type: "file";
filename?: string;
mediaType?: string;
url: string;
};
onRemove: (id: string) => void;
}
const AttachmentItem = memo(({ attachment, onRemove }: AttachmentItemProps) => {
const handleRemove = useCallback(
() => onRemove(attachment.id),
[onRemove, attachment.id]
);
return (
<Attachment data={attachment} key={attachment.id} onRemove={handleRemove}>
<AttachmentPreview />
<AttachmentRemove />
</Attachment>
);
});
AttachmentItem.displayName = "AttachmentItem";
interface ModelItemProps {
m: (typeof models)[0];
selectedModel: string;
onSelect: (id: string) => void;
}
const ModelItem = memo(({ m, selectedModel, onSelect }: ModelItemProps) => {
const handleSelect = useCallback(() => onSelect(m.id), [onSelect, m.id]);
return (
<ModelSelectorItem key={m.id} onSelect={handleSelect} value={m.id}>
<ModelSelectorLogo provider={m.chefSlug} />
<ModelSelectorName>{m.name}</ModelSelectorName>
<ModelSelectorLogoGroup>
{m.providers.map((provider) => (
<ModelSelectorLogo key={provider} provider={provider} />
))}
</ModelSelectorLogoGroup>
{selectedModel === m.id ? (
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</ModelSelectorItem>
);
});
ModelItem.displayName = "ModelItem";
const PromptInputAttachmentsDisplay = () => {
const attachments = usePromptInputAttachments();
const handleRemove = useCallback(
(id: string) => attachments.remove(id),
[attachments]
);
if (attachments.files.length === 0) {
return null;
}
return (
<Attachments variant="inline">
{attachments.files.map((attachment) => (
<AttachmentItem
attachment={attachment}
key={attachment.id}
onRemove={handleRemove}
/>
))}
</Attachments>
);
};
const Example = () => {
const [model, setModel] = useState<string>(models[0].id);
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
const [status, setStatus] = useState<
"submitted" | "streaming" | "ready" | "error"
>("ready");
const selectedModelData = models.find((m) => m.id === model);
const handleModelSelect = useCallback((id: string) => {
setModel(id);
setModelSelectorOpen(false);
}, []);
const handleSubmit = useCallback((message: PromptInputMessage) => {
const hasText = Boolean(message.text);
const hasAttachments = Boolean(message.files?.length);
if (!(hasText || hasAttachments)) {
return;
}
setStatus("submitted");
// eslint-disable-next-line no-console
console.log("Submitting message:", message);
setTimeout(() => {
setStatus("streaming");
}, SUBMITTING_TIMEOUT);
setTimeout(() => {
setStatus("ready");
}, STREAMING_TIMEOUT);
}, []);
return (
<div className="size-full">
<PromptInputProvider>
<PromptInput globalDrop multiple onSubmit={handleSubmit}>
<PromptInputAttachmentsDisplay />
<PromptInputBody>
<PromptInputTextarea />
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionMenuTrigger />
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments />
<PromptInputActionAddScreenshot />
</PromptInputActionMenuContent>
</PromptInputActionMenu>
<PromptInputButton>
<GlobeIcon size={16} />
<span>Search</span>
</PromptInputButton>
<ModelSelector
onOpenChange={setModelSelectorOpen}
open={modelSelectorOpen}
>
<ModelSelectorTrigger asChild>
<PromptInputButton>
{selectedModelData?.chefSlug && (
<ModelSelectorLogo
provider={selectedModelData.chefSlug}
/>
)}
{selectedModelData?.name && (
<ModelSelectorName>
{selectedModelData.name}
</ModelSelectorName>
)}
</PromptInputButton>
</ModelSelectorTrigger>
<ModelSelectorContent>
<ModelSelectorInput placeholder="Search models..." />
<ModelSelectorList>
<ModelSelectorEmpty>No models found.</ModelSelectorEmpty>
{["OpenAI", "Anthropic", "Google"].map((chef) => (
<ModelSelectorGroup heading={chef} key={chef}>
{models
.filter((m) => m.chef === chef)
.map((m) => (
<ModelItem
key={m.id}
m={m}
onSelect={handleModelSelect}
selectedModel={model}
/>
))}
</ModelSelectorGroup>
))}
</ModelSelectorList>
</ModelSelectorContent>
</ModelSelector>
</PromptInputTools>
<PromptInputSubmit status={status} />
</PromptInputFooter>
</PromptInput>
</PromptInputProvider>
</div>
);
};
export default Example;
@@ -0,0 +1,394 @@
"use client";
import {
Attachment,
AttachmentPreview,
AttachmentRemove,
Attachments,
} from "@/components/ai-elements/attachments";
import {
ModelSelector,
ModelSelectorContent,
ModelSelectorEmpty,
ModelSelectorGroup,
ModelSelectorInput,
ModelSelectorItem,
ModelSelectorList,
ModelSelectorLogo,
ModelSelectorLogoGroup,
ModelSelectorName,
ModelSelectorTrigger,
} from "@/components/ai-elements/model-selector";
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
import {
PromptInput,
PromptInputActionAddAttachments,
PromptInputActionMenu,
PromptInputActionMenuContent,
PromptInputActionMenuTrigger,
PromptInputBody,
PromptInputButton,
PromptInputFooter,
PromptInputHeader,
PromptInputSubmit,
PromptInputTextarea,
PromptInputTools,
usePromptInputAttachments,
} from "@/components/ai-elements/prompt-input";
import type { QueueTodo } from "@/components/ai-elements/queue";
import {
Queue,
QueueItem,
QueueItemAction,
QueueItemActions,
QueueItemContent,
QueueItemDescription,
QueueItemIndicator,
QueueSection,
QueueSectionContent,
} from "@/components/ai-elements/queue";
import { CheckIcon, GlobeIcon, Trash2 } from "lucide-react";
import { memo, useCallback, useRef, useState } from "react";
const models = [
{
chef: "OpenAI",
chefSlug: "openai",
id: "gpt-4o",
name: "GPT-4o",
providers: ["openai", "azure"],
},
{
chef: "OpenAI",
chefSlug: "openai",
id: "gpt-4o-mini",
name: "GPT-4o Mini",
providers: ["openai", "azure"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-opus-4-20250514",
name: "Claude 4 Opus",
providers: ["anthropic", "azure", "google", "amazon-bedrock"],
},
{
chef: "Anthropic",
chefSlug: "anthropic",
id: "claude-sonnet-4-20250514",
name: "Claude 4 Sonnet",
providers: ["anthropic", "azure", "google", "amazon-bedrock"],
},
{
chef: "Google",
chefSlug: "google",
id: "gemini-2.0-flash-exp",
name: "Gemini 2.0 Flash",
providers: ["google"],
},
];
const SUBMITTING_TIMEOUT = 200;
const STREAMING_TIMEOUT = 2000;
interface AttachmentItemProps {
attachment: {
id: string;
type: "file";
filename?: string;
mediaType?: string;
url: string;
};
onRemove: (id: string) => void;
}
const AttachmentItem = memo(({ attachment, onRemove }: AttachmentItemProps) => {
const handleRemove = useCallback(
() => onRemove(attachment.id),
[onRemove, attachment.id]
);
return (
<Attachment data={attachment} key={attachment.id} onRemove={handleRemove}>
<AttachmentPreview />
<AttachmentRemove />
</Attachment>
);
});
AttachmentItem.displayName = "AttachmentItem";
interface TodoItemProps {
todo: QueueTodo;
onRemove: (id: string) => void;
}
const TodoItem = memo(({ todo, onRemove }: TodoItemProps) => {
const isCompleted = todo.status === "completed";
const handleRemove = useCallback(
() => onRemove(todo.id),
[onRemove, todo.id]
);
return (
<QueueItem key={todo.id}>
<div className="flex items-center gap-2">
<QueueItemIndicator completed={isCompleted} />
<QueueItemContent completed={isCompleted}>
{todo.title}
</QueueItemContent>
<QueueItemActions>
<QueueItemAction aria-label="Remove todo" onClick={handleRemove}>
<Trash2 size={12} />
</QueueItemAction>
</QueueItemActions>
</div>
{todo.description && (
<QueueItemDescription completed={isCompleted}>
{todo.description}
</QueueItemDescription>
)}
</QueueItem>
);
});
TodoItem.displayName = "TodoItem";
interface ModelItemProps {
m: (typeof models)[0];
selectedModel: string;
onSelect: (id: string) => void;
}
const ModelItem = memo(({ m, selectedModel, onSelect }: ModelItemProps) => {
const handleSelect = useCallback(() => onSelect(m.id), [onSelect, m.id]);
return (
<ModelSelectorItem key={m.id} onSelect={handleSelect} value={m.id}>
<ModelSelectorLogo provider={m.chefSlug} />
<ModelSelectorName>{m.name}</ModelSelectorName>
<ModelSelectorLogoGroup>
{m.providers.map((provider) => (
<ModelSelectorLogo key={provider} provider={provider} />
))}
</ModelSelectorLogoGroup>
{selectedModel === m.id ? (
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</ModelSelectorItem>
);
});
ModelItem.displayName = "ModelItem";
const sampleTodos: QueueTodo[] = [
{
description: "Complete the README and API docs",
id: "todo-1",
status: "completed",
title: "Write project documentation",
},
{
id: "todo-2",
status: "pending",
title: "Implement authentication",
},
{
description: "Resolve crash on settings page",
id: "todo-3",
status: "pending",
title: "Fix bug #42",
},
{
description: "Unify queue and todo state management",
id: "todo-4",
status: "pending",
title: "Refactor queue logic",
},
{
description: "Increase test coverage for hooks",
id: "todo-5",
status: "pending",
title: "Add unit tests",
},
];
const PromptInputAttachmentsDisplay = () => {
const attachments = usePromptInputAttachments();
const handleRemove = useCallback(
(id: string) => attachments.remove(id),
[attachments]
);
if (attachments.files.length === 0) {
return null;
}
return (
<Attachments variant="inline">
{attachments.files.map((attachment) => (
<AttachmentItem
attachment={attachment}
key={attachment.id}
onRemove={handleRemove}
/>
))}
</Attachments>
);
};
const Example = () => {
const [todos, setTodos] = useState(sampleTodos);
const handleRemoveTodo = useCallback((id: string) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}, []);
const [text, setText] = useState<string>("");
const [model, setModel] = useState<string>(models[0].id);
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
const [status, setStatus] = useState<
"submitted" | "streaming" | "ready" | "error"
>("ready");
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleTextChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => setText(e.target.value),
[]
);
const handleModelSelect = useCallback((id: string) => {
setModel(id);
setModelSelectorOpen(false);
}, []);
const selectedModelData = models.find((m) => m.id === model);
const stop = () => {
console.log("Stopping request...");
// Clear any pending timeouts
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
setStatus("ready");
};
const handleSubmit = useCallback(
(message: PromptInputMessage) => {
// If currently streaming or submitted, stop instead of submitting
if (status === "streaming" || status === "submitted") {
stop();
return;
}
const hasText = Boolean(message.text);
const hasAttachments = Boolean(message.files?.length);
if (!(hasText || hasAttachments)) {
return;
}
setStatus("submitted");
console.log("Submitting message:", message);
setTimeout(() => {
setStatus("streaming");
}, SUBMITTING_TIMEOUT);
timeoutRef.current = setTimeout(() => {
setStatus("ready");
timeoutRef.current = null;
}, STREAMING_TIMEOUT);
},
[status]
);
return (
<div className="flex size-full flex-col justify-end">
<Queue className="mx-auto max-h-[150px] w-[95%] overflow-y-auto rounded-b-none border-input border-b-0">
{todos.length > 0 && (
<QueueSection>
<QueueSectionContent>
<div>
{todos.map((todo) => (
<TodoItem
key={todo.id}
onRemove={handleRemoveTodo}
todo={todo}
/>
))}
</div>
</QueueSectionContent>
</QueueSection>
)}
</Queue>
<PromptInput globalDrop multiple onSubmit={handleSubmit}>
<PromptInputHeader>
<PromptInputAttachmentsDisplay />
</PromptInputHeader>
<PromptInputBody>
<PromptInputTextarea onChange={handleTextChange} value={text} />
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionMenuTrigger />
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments />
</PromptInputActionMenuContent>
</PromptInputActionMenu>
<PromptInputButton>
<GlobeIcon size={16} />
<span>Search</span>
</PromptInputButton>
<ModelSelector
onOpenChange={setModelSelectorOpen}
open={modelSelectorOpen}
>
<ModelSelectorTrigger asChild>
<PromptInputButton>
{selectedModelData?.chefSlug && (
<ModelSelectorLogo provider={selectedModelData.chefSlug} />
)}
{selectedModelData?.name && (
<ModelSelectorName>
{selectedModelData.name}
</ModelSelectorName>
)}
</PromptInputButton>
</ModelSelectorTrigger>
<ModelSelectorContent>
<ModelSelectorInput placeholder="Search models..." />
<ModelSelectorList>
<ModelSelectorEmpty>No models found.</ModelSelectorEmpty>
{["OpenAI", "Anthropic", "Google"].map((chef) => (
<ModelSelectorGroup heading={chef} key={chef}>
{models
.filter((m) => m.chef === chef)
.map((m) => (
<ModelItem
key={m.id}
m={m}
onSelect={handleModelSelect}
selectedModel={model}
/>
))}
</ModelSelectorGroup>
))}
</ModelSelectorList>
</ModelSelectorContent>
</ModelSelector>
</PromptInputTools>
<PromptInputSubmit status={status} />
</PromptInputFooter>
</PromptInput>
</div>
);
};
export default Example;
@@ -0,0 +1,295 @@
"use client";
import type { QueueMessage, QueueTodo } from "@/components/ai-elements/queue";
import {
Queue,
QueueItem,
QueueItemAction,
QueueItemActions,
QueueItemAttachment,
QueueItemContent,
QueueItemDescription,
QueueItemFile,
QueueItemImage,
QueueItemIndicator,
QueueList,
QueueSection,
QueueSectionContent,
QueueSectionLabel,
QueueSectionTrigger,
} from "@/components/ai-elements/queue";
import { ArrowUp, Trash2 } from "lucide-react";
import { memo, useCallback, useState } from "react";
const sampleMessages: QueueMessage[] = [
{
id: "msg-1",
parts: [{ text: "How do I set up the project?", type: "text" }],
},
{
id: "msg-2",
parts: [{ text: "What is the roadmap for Q4?", type: "text" }],
},
{
id: "msg-3",
parts: [
{ text: "Update the default logo to this png.", type: "text" },
{
filename: "setup-guide.png",
mediaType: "image/png",
type: "file",
url: "https://github.com/haydenbleasel.png",
},
],
},
{
id: "msg-4",
parts: [{ text: "Please generate a changelog.", type: "text" }],
},
{
id: "msg-5",
parts: [{ text: "Add dark mode support.", type: "text" }],
},
{
id: "msg-6",
parts: [{ text: "Optimize database queries.", type: "text" }],
},
{
id: "msg-7",
parts: [{ text: "Set up CI/CD pipeline.", type: "text" }],
},
];
const sampleTodos: QueueTodo[] = [
{
description: "Complete the README and API docs",
id: "todo-1",
status: "completed",
title: "Write project documentation",
},
{
id: "todo-2",
status: "pending",
title: "Implement authentication",
},
{
description: "Resolve crash on settings page",
id: "todo-3",
status: "pending",
title: "Fix bug #42",
},
{
description: "Unify queue and todo state management",
id: "todo-4",
status: "pending",
title: "Refactor queue logic",
},
{
description: "Increase test coverage for hooks",
id: "todo-5",
status: "pending",
title: "Add unit tests",
},
];
interface MessageActionsProps {
messageId: string;
onRemove: (e: React.MouseEvent, id: string) => void;
onSend: (e: React.MouseEvent, id: string) => void;
}
const MessageActions = memo(
({ messageId, onRemove, onSend }: MessageActionsProps) => {
const handleRemove = useCallback(
(e: React.MouseEvent) => onRemove(e, messageId),
[onRemove, messageId]
);
const handleSend = useCallback(
(e: React.MouseEvent) => onSend(e, messageId),
[onSend, messageId]
);
return (
<QueueItemActions>
<QueueItemAction
aria-label="Remove from queue"
onClick={handleRemove}
title="Remove from queue"
>
<Trash2 size={12} />
</QueueItemAction>
<QueueItemAction aria-label="Send now" onClick={handleSend}>
<ArrowUp size={14} />
</QueueItemAction>
</QueueItemActions>
);
}
);
MessageActions.displayName = "MessageActions";
interface TodoItemProps {
todo: QueueTodo;
onRemove: (id: string) => void;
}
const TodoItem = memo(({ todo, onRemove }: TodoItemProps) => {
const isCompleted = todo.status === "completed";
const handleRemove = useCallback(
() => onRemove(todo.id),
[onRemove, todo.id]
);
return (
<QueueItem key={todo.id}>
<div className="flex items-center gap-2">
<QueueItemIndicator completed={isCompleted} />
<QueueItemContent completed={isCompleted}>
{todo.title}
</QueueItemContent>
<QueueItemActions>
<QueueItemAction aria-label="Remove todo" onClick={handleRemove}>
<Trash2 size={12} />
</QueueItemAction>
</QueueItemActions>
</div>
{todo.description && (
<QueueItemDescription completed={isCompleted}>
{todo.description}
</QueueItemDescription>
)}
</QueueItem>
);
});
TodoItem.displayName = "TodoItem";
const Example = () => {
const [messages, setMessages] = useState(sampleMessages);
const [todos, setTodos] = useState(sampleTodos);
const handleRemoveMessage = useCallback((id: string) => {
setMessages((prev) => prev.filter((msg) => msg.id !== id));
}, []);
const handleRemoveTodo = useCallback((id: string) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}, []);
const handleSendNow = useCallback((id: string) => {
console.log("Send now:", id);
setMessages((prev) => prev.filter((msg) => msg.id !== id));
}, []);
const handleMessageRemove = useCallback(
(e: React.MouseEvent, id: string) => {
e.preventDefault();
e.stopPropagation();
handleRemoveMessage(id);
},
[handleRemoveMessage]
);
const handleMessageSend = useCallback(
(e: React.MouseEvent, id: string) => {
e.preventDefault();
e.stopPropagation();
handleSendNow(id);
},
[handleSendNow]
);
if (messages.length === 0 && todos.length === 0) {
return null;
}
return (
<Queue>
{messages.length > 0 && (
<QueueSection>
<QueueSectionTrigger>
<QueueSectionLabel count={messages.length} label="Queued" />
</QueueSectionTrigger>
<QueueSectionContent>
<QueueList>
{messages.map((message) => {
const summary = (() => {
const textParts = message.parts.filter(
(p) => p.type === "text"
);
const text = textParts
.map((p) => p.text)
.join(" ")
.trim();
return text || "(queued message)";
})();
const hasFiles = message.parts.some(
(p) => p.type === "file" && p.url
);
return (
<QueueItem key={message.id}>
<div className="flex items-center gap-2">
<QueueItemIndicator />
<QueueItemContent>{summary}</QueueItemContent>
<MessageActions
messageId={message.id}
onRemove={handleMessageRemove}
onSend={handleMessageSend}
/>
</div>
{hasFiles && (
<QueueItemAttachment>
{message.parts
.filter((p) => p.type === "file" && p.url)
.map((file) => {
if (
file.mediaType?.startsWith("image/") &&
file.url
) {
return (
<QueueItemImage
alt={file.filename || "attachment"}
key={file.url}
src={file.url}
/>
);
}
return (
<QueueItemFile key={file.url}>
{file.filename || "file"}
</QueueItemFile>
);
})}
</QueueItemAttachment>
)}
</QueueItem>
);
})}
</QueueList>
</QueueSectionContent>
</QueueSection>
)}
{todos.length > 0 && (
<QueueSection>
<QueueSectionTrigger>
<QueueSectionLabel count={todos.length} label="Todo" />
</QueueSectionTrigger>
<QueueSectionContent>
<QueueList>
{todos.map((todo) => (
<TodoItem
key={todo.id}
onRemove={handleRemoveTodo}
todo={todo}
/>
))}
</QueueList>
</QueueSectionContent>
</QueueSection>
)}
</Queue>
);
};
export default Example;
@@ -0,0 +1,71 @@
"use client";
import {
Reasoning,
ReasoningContent,
ReasoningTrigger,
} from "@/components/ai-elements/reasoning";
import { useCallback, useEffect, useState } from "react";
const reasoningSteps = [
"Let me think about this problem step by step.",
"\n\nFirst, I need to understand what the user is asking for.",
"\n\nThey want a reasoning component that opens automatically when streaming begins and closes when streaming finishes. The component should be composable and follow existing patterns in the codebase.",
"\n\nThis seems like a collapsible component with state management would be the right approach.",
].join("");
const Example = () => {
const [content, setContent] = useState("");
const [isStreaming, setIsStreaming] = useState(false);
const [currentTokenIndex, setCurrentTokenIndex] = useState(0);
const [tokens, setTokens] = useState<string[]>([]);
// Function to chunk text into fake tokens of 3-4 characters
const chunkIntoTokens = useCallback((text: string): string[] => {
const chunks: string[] = [];
let i = 0;
while (i < text.length) {
// Random size between 3-4
const chunkSize = Math.floor(Math.random() * 2) + 3;
chunks.push(text.slice(i, i + chunkSize));
i += chunkSize;
}
return chunks;
}, []);
useEffect(() => {
const tokenizedSteps = chunkIntoTokens(reasoningSteps);
setTokens(tokenizedSteps);
setContent("");
setCurrentTokenIndex(0);
setIsStreaming(true);
}, [chunkIntoTokens]);
useEffect(() => {
if (!isStreaming || currentTokenIndex >= tokens.length) {
if (isStreaming) {
setIsStreaming(false);
}
return;
}
// Faster interval since we're streaming smaller chunks
const timer = setTimeout(() => {
setContent((prev) => prev + tokens[currentTokenIndex]);
setCurrentTokenIndex((prev) => prev + 1);
}, 25);
return () => clearTimeout(timer);
}, [isStreaming, currentTokenIndex, tokens]);
return (
<div className="w-full p-4" style={{ height: "300px" }}>
<Reasoning className="w-full" isStreaming={isStreaming}>
<ReasoningTrigger />
<ReasoningContent>{content}</ReasoningContent>
</Reasoning>
</div>
);
};
export default Example;
@@ -0,0 +1,178 @@
"use client";
import { CodeBlock, CodeBlockCopyButton } from "@/components/ai-elements/code-block";
import {
Sandbox,
SandboxContent,
SandboxHeader,
SandboxTabContent,
SandboxTabs,
SandboxTabsBar,
SandboxTabsList,
SandboxTabsTrigger,
} from "@/components/ai-elements/sandbox";
import {
StackTrace,
StackTraceActions,
StackTraceContent,
StackTraceCopyButton,
StackTraceError,
StackTraceErrorMessage,
StackTraceErrorType,
StackTraceExpandButton,
StackTraceFrames,
StackTraceHeader,
} from "@/components/ai-elements/stack-trace";
import { Button } from "@/components/ui/button";
import type { ToolUIPart } from "ai";
import { memo, useCallback, useState } from "react";
const code = `import math
def calculate_primes(limit):
"""Find all prime numbers up to a given limit using Sieve of Eratosthenes."""
sieve = [True] * (limit + 1)
sieve[0] = sieve[1] = False
for i in range(2, int(math.sqrt(limit)) + 1):
if sieve[i]:
for j in range(i * i, limit + 1, i):
sieve[j] = False
return [i for i, is_prime in enumerate(sieve) if is_prime]
if __name__ == "__main__":
primes = calculate_primes(50)
print(f"Found {len(primes)} prime numbers up to 50:")
print(primes)`;
const outputs: Record<ToolUIPart["state"], string | undefined> = {
"input-available": undefined,
"input-streaming": undefined,
"output-available": `Found 15 prime numbers up to 50:
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]`,
"output-error": `TypeError: Cannot read properties of undefined (reading 'map')
at calculatePrimes (/src/utils/primes.ts:15:23)
at runCalculation (/src/components/Calculator.tsx:42:12)
at onClick (/src/components/Button.tsx:18:5)
at HTMLButtonElement.dispatch (node_modules/react-dom/cjs/react-dom.development.js:3456:9)
at node_modules/react-dom/cjs/react-dom.development.js:4245:12`,
};
const states: ToolUIPart["state"][] = [
"input-streaming",
"input-available",
"output-available",
"output-error",
];
interface StateButtonProps {
s: ToolUIPart["state"];
currentState: ToolUIPart["state"];
onStateChange: (state: ToolUIPart["state"]) => void;
}
const StateButton = memo(
({ s, currentState, onStateChange }: StateButtonProps) => {
const handleClick = useCallback(() => onStateChange(s), [onStateChange, s]);
return (
<Button
key={s}
onClick={handleClick}
size="sm"
variant={currentState === s ? "default" : "outline"}
>
{s}
</Button>
);
}
);
StateButton.displayName = "StateButton";
const Example = () => {
const [state, setState] = useState<ToolUIPart["state"]>("output-available");
const handleStateChange = useCallback((s: ToolUIPart["state"]) => {
setState(s);
}, []);
return (
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
{states.map((s) => (
<StateButton
currentState={state}
key={s}
onStateChange={handleStateChange}
s={s}
/>
))}
</div>
<Sandbox>
<SandboxHeader state={state} title="primes.py" />
<SandboxContent>
<SandboxTabs defaultValue="code">
<SandboxTabsBar>
<SandboxTabsList>
<SandboxTabsTrigger value="code">Code</SandboxTabsTrigger>
<SandboxTabsTrigger value="output">Output</SandboxTabsTrigger>
</SandboxTabsList>
</SandboxTabsBar>
<SandboxTabContent value="code">
<CodeBlock
className="border-0"
code={
state === "input-streaming" ? "# Generating code..." : code
}
language="python"
>
<CodeBlockCopyButton
className="absolute top-2 right-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
size="sm"
/>
</CodeBlock>
</SandboxTabContent>
<SandboxTabContent value="output">
{state === "output-error" ? (
<StackTrace
className="rounded-none border-0"
defaultOpen
trace={outputs[state] ?? ""}
>
<StackTraceHeader>
<StackTraceError>
<StackTraceErrorType />
<StackTraceErrorMessage />
</StackTraceError>
<StackTraceActions>
<StackTraceCopyButton />
<StackTraceExpandButton />
</StackTraceActions>
</StackTraceHeader>
<StackTraceContent>
<StackTraceFrames />
</StackTraceContent>
</StackTrace>
) : (
<CodeBlock
className="border-0"
code={outputs[state] ?? ""}
language="log"
>
<CodeBlockCopyButton
className="absolute top-2 right-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
size="sm"
/>
</CodeBlock>
)}
</SandboxTabContent>
</SandboxTabs>
</SandboxContent>
</Sandbox>
</div>
);
};
export default Example;
@@ -0,0 +1,9 @@
"use client";
import { SchemaDisplay } from "@/components/ai-elements/schema-display";
const Example = () => (
<SchemaDisplay description="List all users" method="GET" path="/api/users" />
);
export default Example;
@@ -0,0 +1,20 @@
"use client";
import { SchemaDisplay } from "@/components/ai-elements/schema-display";
const Example = () => (
<SchemaDisplay
method="POST"
path="/api/posts"
requestBody={[
{ name: "title", required: true, type: "string" },
{ name: "content", required: true, type: "string" },
]}
responseBody={[
{ name: "id", required: true, type: "string" },
{ name: "createdAt", required: true, type: "string" },
]}
/>
);
export default Example;
@@ -0,0 +1,23 @@
"use client";
import { SchemaDisplay } from "@/components/ai-elements/schema-display";
const Example = () => (
<SchemaDisplay
method="POST"
path="/api/posts"
requestBody={[
{
name: "author",
properties: [
{ name: "id", type: "string" },
{ name: "name", type: "string" },
],
type: "object",
},
{ name: "title", required: true, type: "string" },
]}
/>
);
export default Example;
@@ -0,0 +1,16 @@
"use client";
import { SchemaDisplay } from "@/components/ai-elements/schema-display";
const Example = () => (
<SchemaDisplay
method="GET"
parameters={[
{ location: "path", name: "userId", required: true, type: "string" },
{ location: "query", name: "include", type: "string" },
]}
path="/api/users/{userId}"
/>
);
export default Example;
@@ -0,0 +1,110 @@
"use client";
import {
SchemaDisplay,
SchemaDisplayContent,
SchemaDisplayDescription,
SchemaDisplayHeader,
SchemaDisplayMethod,
SchemaDisplayParameters,
SchemaDisplayPath,
SchemaDisplayRequest,
SchemaDisplayResponse,
} from "@/components/ai-elements/schema-display";
const Example = () => (
<SchemaDisplay
description="Create a new post for a specific user. Requires authentication."
method="POST"
parameters={[
{
description: "The unique identifier of the user",
location: "path",
name: "userId",
required: true,
type: "string",
},
{
description: "Save as draft instead of publishing",
location: "query",
name: "draft",
required: false,
type: "boolean",
},
]}
path="/api/users/{userId}/posts"
requestBody={[
{
description: "The post title",
name: "title",
required: true,
type: "string",
},
{
description: "The post content in markdown format",
name: "content",
required: true,
type: "string",
},
{
description: "Tags for categorization",
items: { name: "tag", type: "string" },
name: "tags",
type: "array",
},
{
description: "Additional metadata",
name: "metadata",
properties: [
{
description: "SEO optimized title",
name: "seoTitle",
type: "string",
},
{
description: "Meta description",
name: "seoDescription",
type: "string",
},
],
type: "object",
},
]}
responseBody={[
{ description: "Post ID", name: "id", required: true, type: "string" },
{ name: "title", required: true, type: "string" },
{ name: "content", required: true, type: "string" },
{
description: "ISO 8601 timestamp",
name: "createdAt",
required: true,
type: "string",
},
{
name: "author",
properties: [
{ name: "id", required: true, type: "string" },
{ name: "name", required: true, type: "string" },
{ name: "avatar", type: "string" },
],
required: true,
type: "object",
},
]}
>
<SchemaDisplayHeader>
<div className="flex items-center gap-3">
<SchemaDisplayMethod />
<SchemaDisplayPath />
</div>
</SchemaDisplayHeader>
<SchemaDisplayDescription />
<SchemaDisplayContent>
<SchemaDisplayParameters />
<SchemaDisplayRequest />
<SchemaDisplayResponse />
</SchemaDisplayContent>
</SchemaDisplay>
);
export default Example;
@@ -0,0 +1,31 @@
"use client";
import { Shimmer } from "@/components/ai-elements/shimmer";
const Example = () => (
<div className="flex flex-col gap-6 p-8">
<div className="text-center">
<p className="mb-3 text-muted-foreground text-sm">Fast (1 second)</p>
<Shimmer duration={1}>Loading quickly...</Shimmer>
</div>
<div className="text-center">
<p className="mb-3 text-muted-foreground text-sm">Default (2 seconds)</p>
<Shimmer duration={2}>Loading at normal speed...</Shimmer>
</div>
<div className="text-center">
<p className="mb-3 text-muted-foreground text-sm">Slow (4 seconds)</p>
<Shimmer duration={4}>Loading slowly...</Shimmer>
</div>
<div className="text-center">
<p className="mb-3 text-muted-foreground text-sm">
Very Slow (6 seconds)
</p>
<Shimmer duration={6}>Loading very slowly...</Shimmer>
</div>
</div>
);
export default Example;
@@ -0,0 +1,43 @@
"use client";
import { Shimmer } from "@/components/ai-elements/shimmer";
const Example = () => (
<div className="flex flex-col gap-6 p-8">
<div className="text-center">
<p className="mb-3 text-muted-foreground text-sm">
As paragraph (default)
</p>
<Shimmer as="p">This is rendered as a paragraph</Shimmer>
</div>
<div className="text-center">
<p className="mb-3 text-muted-foreground text-sm">As heading</p>
<Shimmer as="h2" className="font-bold text-2xl">
Large Heading with Shimmer
</Shimmer>
</div>
<div className="text-center">
<p className="mb-3 text-muted-foreground text-sm">As span (inline)</p>
<div>
Processing your request{" "}
<Shimmer as="span" className="inline">
with AI magic
</Shimmer>
...
</div>
</div>
<div className="text-center">
<p className="mb-3 text-muted-foreground text-sm">
As div with custom styling
</p>
<Shimmer as="div" className="font-semibold text-lg">
Custom styled shimmer text
</Shimmer>
</div>
</div>
);
export default Example;
@@ -0,0 +1,17 @@
"use client";
import { Shimmer } from "@/components/ai-elements/shimmer";
const Example = () => (
<div className="flex flex-col items-center justify-center gap-4 p-8">
<Shimmer>This text has a shimmer effect</Shimmer>
<Shimmer as="h1" className="font-bold text-4xl">
Large Heading
</Shimmer>
<Shimmer duration={3} spread={3}>
Slower shimmer with wider spread
</Shimmer>
</div>
);
export default Example;
@@ -0,0 +1,19 @@
"use client";
import {
Snippet,
SnippetAddon,
SnippetCopyButton,
SnippetInput,
} from "@/components/ai-elements/snippet";
const Example = () => (
<Snippet code="git clone https://github.com/user/repo">
<SnippetInput />
<SnippetAddon align="inline-end">
<SnippetCopyButton />
</SnippetAddon>
</Snippet>
);
export default Example;
@@ -0,0 +1,25 @@
"use client";
import {
Snippet,
SnippetAddon,
SnippetCopyButton,
SnippetInput,
SnippetText,
} from "@/components/ai-elements/snippet";
const Example = () => (
<div className="flex size-full items-center justify-center p-4">
<Snippet className="max-w-sm" code="npx ai-elements add snippet">
<SnippetAddon className="pl-1">
<SnippetText>$</SnippetText>
</SnippetAddon>
<SnippetInput />
<SnippetAddon align="inline-end" className="pr-2">
<SnippetCopyButton />
</SnippetAddon>
</Snippet>
</div>
);
export default Example;
@@ -0,0 +1,39 @@
"use client";
import {
Source,
Sources,
SourcesContent,
SourcesTrigger,
} from "@/components/ai-elements/sources";
import { ChevronDownIcon, ExternalLinkIcon } from "lucide-react";
const sources = [
{ href: "https://stripe.com/docs/api", title: "Stripe API Documentation" },
{ href: "https://docs.github.com/en/rest", title: "GitHub REST API" },
{
href: "https://docs.aws.amazon.com/sdk-for-javascript/",
title: "AWS SDK for JavaScript",
},
];
const Example = () => (
<div style={{ height: "110px" }}>
<Sources>
<SourcesTrigger count={sources.length}>
<p className="font-medium">Using {sources.length} citations</p>
<ChevronDownIcon className="size-4" />
</SourcesTrigger>
<SourcesContent>
{sources.map((source) => (
<Source href={source.href} key={source.href}>
{source.title}
<ExternalLinkIcon className="size-4" />
</Source>
))}
</SourcesContent>
</Sources>
</div>
);
export default Example;
@@ -0,0 +1,32 @@
"use client";
import {
Source,
Sources,
SourcesContent,
SourcesTrigger,
} from "@/components/ai-elements/sources";
const sources = [
{ href: "https://stripe.com/docs/api", title: "Stripe API Documentation" },
{ href: "https://docs.github.com/en/rest", title: "GitHub REST API" },
{
href: "https://docs.aws.amazon.com/sdk-for-javascript/",
title: "AWS SDK for JavaScript",
},
];
const Example = () => (
<div style={{ height: "110px" }}>
<Sources>
<SourcesTrigger count={sources.length} />
<SourcesContent>
{sources.map((source) => (
<Source href={source.href} key={source.href} title={source.title} />
))}
</SourcesContent>
</Sources>
</div>
);
export default Example;
@@ -0,0 +1,84 @@
"use client";
import { SpeechInput } from "@/components/ai-elements/speech-input";
import { useCallback, useState } from "react";
/**
* Fallback handler for browsers that don't support Web Speech API (Firefox, Safari).
* This function receives recorded audio and should send it to a transcription service.
* Example uses OpenAI Whisper API - replace with your preferred service.
*/
const handleAudioRecorded = async (audioBlob: Blob): Promise<string> => {
const formData = new FormData();
formData.append("file", audioBlob, "audio.webm");
formData.append("model", "whisper-1");
const response = await fetch(
"https://api.openai.com/v1/audio/transcriptions",
{
body: formData,
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}`,
},
method: "POST",
}
);
if (!response.ok) {
throw new Error("Transcription failed");
}
const data = await response.json();
return data.text;
};
const Example = () => {
const [transcript, setTranscript] = useState("");
const handleTranscriptionChange = useCallback((text: string) => {
setTranscript((prev) => {
const newText = prev ? `${prev} ${text}` : text;
return newText;
});
}, []);
const handleClear = useCallback(() => {
setTranscript("");
}, []);
return (
<div className="flex size-full flex-col items-center justify-center gap-4">
<div className="flex gap-2">
<SpeechInput
onAudioRecorded={handleAudioRecorded}
onTranscriptionChange={handleTranscriptionChange}
size="icon"
variant="outline"
/>
{transcript && (
<button
className="text-muted-foreground text-sm underline hover:text-foreground"
onClick={handleClear}
type="button"
>
Clear
</button>
)}
</div>
{transcript ? (
<div className="max-w-md rounded-lg border bg-card p-4 text-sm">
<p className="text-muted-foreground">
<strong>Transcript:</strong>
</p>
<p className="mt-2">{transcript}</p>
</div>
) : (
<p className="text-muted-foreground text-sm">
Click the microphone to start speaking
</p>
)}
</div>
);
};
export default Example;
@@ -0,0 +1,39 @@
"use client";
import {
StackTrace,
StackTraceActions,
StackTraceContent,
StackTraceCopyButton,
StackTraceError,
StackTraceErrorMessage,
StackTraceErrorType,
StackTraceExpandButton,
StackTraceFrames,
StackTraceHeader,
} from "@/components/ai-elements/stack-trace";
const errorString = `TypeError: Cannot read properties of undefined (reading 'map')
at UserList (/app/src/components/UserList.tsx:15:23)
at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:14985:18)
at mountIndeterminateComponent (node_modules/react-dom/cjs/react-dom.development.js:17811:13)`;
const Example = () => (
<StackTrace defaultOpen={false} trace={errorString}>
<StackTraceHeader>
<StackTraceError>
<StackTraceErrorType />
<StackTraceErrorMessage />
</StackTraceError>
<StackTraceActions>
<StackTraceCopyButton />
<StackTraceExpandButton />
</StackTraceActions>
</StackTraceHeader>
<StackTraceContent>
<StackTraceFrames />
</StackTraceContent>
</StackTrace>
);
export default Example;
@@ -0,0 +1,41 @@
"use client";
import {
StackTrace,
StackTraceActions,
StackTraceContent,
StackTraceCopyButton,
StackTraceError,
StackTraceErrorMessage,
StackTraceErrorType,
StackTraceExpandButton,
StackTraceFrames,
StackTraceHeader,
} from "@/components/ai-elements/stack-trace";
const errorString = `TypeError: Cannot read properties of undefined (reading 'map')
at UserList (/app/src/components/UserList.tsx:15:23)
at App (/app/src/App.tsx:42:5)
at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:14985:18)
at mountIndeterminateComponent (node_modules/react-dom/cjs/react-dom.development.js:17811:13)
at beginWork (node_modules/react-dom/cjs/react-dom.development.js:19049:16)`;
const Example = () => (
<StackTrace defaultOpen trace={errorString}>
<StackTraceHeader>
<StackTraceError>
<StackTraceErrorType />
<StackTraceErrorMessage />
</StackTraceError>
<StackTraceActions>
<StackTraceCopyButton />
<StackTraceExpandButton />
</StackTraceActions>
</StackTraceHeader>
<StackTraceContent>
<StackTraceFrames showInternalFrames={false} />
</StackTraceContent>
</StackTrace>
);
export default Example;
@@ -0,0 +1,58 @@
"use client";
import {
StackTrace,
StackTraceActions,
StackTraceContent,
StackTraceCopyButton,
StackTraceError,
StackTraceErrorMessage,
StackTraceErrorType,
StackTraceExpandButton,
StackTraceFrames,
StackTraceHeader,
} from "@/components/ai-elements/stack-trace";
const handleFilePathClick = (path: string, line: number, col: number) => {
console.log(`Open file: ${path}:${line}:${col}`);
};
const handleCopy = () => {
console.log("Stack trace copied");
};
const sampleStackTrace = `TypeError: Cannot read properties of undefined (reading 'map')
at UserList (/app/components/UserList.tsx:15:23)
at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:14985:18)
at mountIndeterminateComponent (node_modules/react-dom/cjs/react-dom.development.js:17811:13)
at beginWork (node_modules/react-dom/cjs/react-dom.development.js:19049:16)
at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:3945:14)
at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:3994:16)
at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:4056:31)
at beginWork$1 (node_modules/react-dom/cjs/react-dom.development.js:23964:7)
at performUnitOfWork (node_modules/react-dom/cjs/react-dom.development.js:22776:12)
at workLoopSync (node_modules/react-dom/cjs/react-dom.development.js:22707:5)`;
const Example = () => (
<StackTrace
defaultOpen
onFilePathClick={handleFilePathClick}
trace={sampleStackTrace}
>
<StackTraceHeader>
<StackTraceError>
<StackTraceErrorType />
<StackTraceErrorMessage />
</StackTraceError>
<StackTraceActions>
<StackTraceCopyButton onCopy={handleCopy} />
<StackTraceExpandButton />
</StackTraceActions>
</StackTraceHeader>
<StackTraceContent>
<StackTraceFrames />
</StackTraceContent>
</StackTrace>
);
export default Example;
@@ -0,0 +1,144 @@
"use client";
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
import {
PromptInput,
PromptInputButton,
PromptInputFooter,
PromptInputSelect,
PromptInputSelectContent,
PromptInputSelectItem,
PromptInputSelectTrigger,
PromptInputSelectValue,
PromptInputSubmit,
PromptInputTextarea,
PromptInputTools,
} from "@/components/ai-elements/prompt-input";
import { Suggestion, Suggestions } from "@/components/ai-elements/suggestion";
import { GlobeIcon, MicIcon, PlusIcon, SendIcon } from "lucide-react";
import { nanoid } from "nanoid";
import { memo, useCallback, useState } from "react";
const suggestions: { key: string; value: string }[] = [
{ key: nanoid(), value: "What are the latest trends in AI?" },
{ key: nanoid(), value: "How does machine learning work?" },
{ key: nanoid(), value: "Explain quantum computing" },
{ key: nanoid(), value: "Best practices for React development" },
{ key: nanoid(), value: "Tell me about TypeScript benefits" },
{ key: nanoid(), value: "How to optimize database queries?" },
{ key: nanoid(), value: "What is the difference between SQL and NoSQL?" },
{ key: nanoid(), value: "Explain cloud computing basics" },
];
const models = [
{ id: "gpt-4", name: "GPT-4" },
{ id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo" },
{ id: "claude-2", name: "Claude 2" },
{ id: "claude-instant", name: "Claude Instant" },
{ id: "palm-2", name: "PaLM 2" },
{ id: "llama-2-70b", name: "Llama 2 70B" },
{ id: "llama-2-13b", name: "Llama 2 13B" },
{ id: "cohere-command", name: "Command" },
{ id: "mistral-7b", name: "Mistral 7B" },
];
const handleSubmit = (message: PromptInputMessage) => {
const hasText = Boolean(message.text);
const hasAttachments = Boolean(message.files?.length);
if (!(hasText || hasAttachments)) {
return;
}
console.log("Submitted message:", message.text || "Sent with attachments");
console.log("Attached files:", message.files);
};
interface SuggestionItemProps {
suggestion: { key: string; value: string };
onSuggestionClick: (value: string) => void;
}
const SuggestionItem = memo(
({ suggestion, onSuggestionClick }: SuggestionItemProps) => {
const handleClick = useCallback(
() => onSuggestionClick(suggestion.value),
[onSuggestionClick, suggestion.value]
);
return (
<Suggestion
key={suggestion.key}
onClick={handleClick}
suggestion={suggestion.value}
/>
);
}
);
SuggestionItem.displayName = "SuggestionItem";
const Example = () => {
const [model, setModel] = useState<string>(models[0].id);
const [text, setText] = useState<string>("");
const handleSuggestionClick = useCallback((suggestion: string) => {
setText(suggestion);
}, []);
const handleTextChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => setText(e.target.value),
[]
);
return (
<div className="grid gap-4">
<Suggestions>
{suggestions.map((suggestion) => (
<SuggestionItem
key={suggestion.key}
onSuggestionClick={handleSuggestionClick}
suggestion={suggestion}
/>
))}
</Suggestions>
<PromptInput onSubmit={handleSubmit}>
<PromptInputTextarea
onChange={handleTextChange}
placeholder="Ask me about anything..."
value={text}
/>
<PromptInputFooter>
<PromptInputTools>
<PromptInputButton>
<PlusIcon size={16} />
</PromptInputButton>
<PromptInputButton>
<MicIcon size={16} />
</PromptInputButton>
<PromptInputButton>
<GlobeIcon size={16} />
<span>Search</span>
</PromptInputButton>
<PromptInputSelect onValueChange={setModel} value={model}>
<PromptInputSelectTrigger>
<PromptInputSelectValue />
</PromptInputSelectTrigger>
<PromptInputSelectContent>
{models.map((m) => (
<PromptInputSelectItem key={m.id} value={m.id}>
{m.name}
</PromptInputSelectItem>
))}
</PromptInputSelectContent>
</PromptInputSelect>
</PromptInputTools>
<PromptInputSubmit>
<SendIcon size={16} />
</PromptInputSubmit>
</PromptInputFooter>
</PromptInput>
</div>
);
};
export default Example;
@@ -0,0 +1,32 @@
"use client";
import { Suggestion, Suggestions } from "@/components/ai-elements/suggestion";
const suggestions = [
"What are the latest trends in AI?",
"How does machine learning work?",
"Explain quantum computing",
"Best practices for React development",
"Tell me about TypeScript benefits",
"How to optimize database queries?",
"What is the difference between SQL and NoSQL?",
"Explain cloud computing basics",
];
const handleSuggestionClick = (suggestion: string) => {
console.log("Selected suggestion:", suggestion);
};
const Example = () => (
<Suggestions>
{suggestions.map((suggestion) => (
<Suggestion
key={suggestion}
onClick={handleSuggestionClick}
suggestion={suggestion}
/>
))}
</Suggestions>
);
export default Example;
@@ -0,0 +1,59 @@
"use client";
import { SiReact } from "@icons-pack/react-simple-icons";
import {
Task,
TaskContent,
TaskItem,
TaskItemFile,
TaskTrigger,
} from "@/components/ai-elements/task";
import { nanoid } from "nanoid";
import type { ReactNode } from "react";
const Example = () => {
const tasks: { key: string; value: ReactNode }[] = [
{ key: nanoid(), value: 'Searching "app/page.tsx, components structure"' },
{
key: nanoid(),
value: (
<span className="inline-flex items-center gap-1" key="read-page-tsx">
Read
<TaskItemFile>
<SiReact className="size-4" color="#149ECA" />
<span>page.tsx</span>
</TaskItemFile>
</span>
),
},
{ key: nanoid(), value: "Scanning 52 files" },
{ key: nanoid(), value: "Scanning 2 files" },
{
key: nanoid(),
value: (
<span className="inline-flex items-center gap-1" key="read-layout-tsx">
Reading files
<TaskItemFile>
<SiReact className="size-4" color="#149ECA" />
<span>layout.tsx</span>
</TaskItemFile>
</span>
),
},
];
return (
<div style={{ height: "200px" }}>
<Task className="w-full">
<TaskTrigger title="Found project files" />
<TaskContent>
{tasks.map((task) => (
<TaskItem key={task.key}>{task.value}</TaskItem>
))}
</TaskContent>
</Task>
</div>
);
};
export default Example;
@@ -0,0 +1,7 @@
"use client";
import { Terminal } from "@/components/ai-elements/terminal";
const Example = () => <Terminal output="npm install complete" />;
export default Example;
@@ -0,0 +1,19 @@
"use client";
import { Terminal } from "@/components/ai-elements/terminal";
import { useCallback, useState } from "react";
const initialOutput = `\u001B[36m$\u001B[0m npm run build
Building project...
\u001B[32m✓\u001B[0m Compiled successfully
\u001B[32m✓\u001B[0m Bundle size: 124kb`;
const Example = () => {
const [output, setOutput] = useState(initialOutput);
const handleClear = useCallback(() => setOutput(""), []);
return <Terminal onClear={handleClear} output={output} />;
};
export default Example;
@@ -0,0 +1,38 @@
"use client";
import { Terminal } from "@/components/ai-elements/terminal";
import { useEffect, useState } from "react";
const lines = [
"\u001B[36m$\u001B[0m npm install",
"Installing dependencies...",
"\u001B[32m✓\u001B[0m react@19.0.0",
"\u001B[32m✓\u001B[0m typescript@5.0.0",
"\u001B[32m✓\u001B[0m vite@5.0.0",
"",
"\u001B[32mDone!\u001B[0m Installed 3 packages in 1.2s",
];
const Example = () => {
const [output, setOutput] = useState("");
const [isStreaming, setIsStreaming] = useState(true);
useEffect(() => {
let lineIndex = 0;
const interval = setInterval(() => {
if (lineIndex < lines.length) {
setOutput((prev) => prev + (prev ? "\n" : "") + lines[lineIndex]);
lineIndex += 1;
} else {
setIsStreaming(false);
clearInterval(interval);
}
}, 500);
return () => clearInterval(interval);
}, []);
return <Terminal autoScroll isStreaming={isStreaming} output={output} />;
};
export default Example;
@@ -0,0 +1,82 @@
"use client";
import {
Terminal,
TerminalActions,
TerminalClearButton,
TerminalContent,
TerminalCopyButton,
TerminalHeader,
TerminalStatus,
TerminalTitle,
} from "@/components/ai-elements/terminal";
import { useCallback, useEffect, useState } from "react";
const handleTerminalCopy = () => {
console.log("Copied!");
};
const ansiOutput = `\u001B[32m✓\u001B[0m Compiled successfully in 1.2s
\u001B[1m\u001B[34minfo\u001B[0m - Collecting page data...
\u001B[1m\u001B[34minfo\u001B[0m - Generating static pages (0/3)
\u001B[32m✓\u001B[0m Generated static pages (3/3)
\u001B[1m\u001B[33mwarn\u001B[0m - Using \u001B[1mexperimental\u001B[0m server actions
\u001B[36mRoute (app)\u001B[0m \u001B[36mSize\u001B[0m \u001B[36mFirst Load JS\u001B[0m
\u001B[37m┌ ○ /\u001B[0m \u001B[32m5.2 kB\u001B[0m \u001B[32m87.3 kB\u001B[0m
\u001B[37m├ ○ /about\u001B[0m \u001B[32m2.1 kB\u001B[0m \u001B[32m84.2 kB\u001B[0m
\u001B[37m└ ○ /contact\u001B[0m \u001B[32m3.8 kB\u001B[0m \u001B[32m85.9 kB\u001B[0m
\u001B[32m✓\u001B[0m Build completed successfully!
\u001B[90mTotal time: 3.45s\u001B[0m
`;
const Example = () => {
const [output, setOutput] = useState("");
const [isStreaming, setIsStreaming] = useState(true);
useEffect(() => {
let index = 0;
const interval = setInterval(() => {
if (index < ansiOutput.length) {
setOutput(ansiOutput.slice(0, index + 10));
index += 10;
} else {
setIsStreaming(false);
clearInterval(interval);
}
}, 20);
return () => clearInterval(interval);
}, []);
const handleClear = useCallback(() => {
setOutput("");
setIsStreaming(false);
}, []);
return (
<Terminal
autoScroll={true}
isStreaming={isStreaming}
onClear={handleClear}
output={output}
>
<TerminalHeader>
<TerminalTitle>Build Output</TerminalTitle>
<div className="flex items-center gap-1">
<TerminalStatus />
<TerminalActions>
<TerminalCopyButton onCopy={handleTerminalCopy} />
<TerminalClearButton />
</TerminalActions>
</div>
</TerminalHeader>
<TerminalContent />
</Terminal>
);
};
export default Example;
@@ -0,0 +1,27 @@
"use client";
import {
TestResults,
TestResultsDuration,
TestResultsHeader,
TestResultsSummary,
} from "@/components/ai-elements/test-results";
const Example = () => (
<TestResults
summary={{
duration: 3500,
failed: 2,
passed: 10,
skipped: 1,
total: 13,
}}
>
<TestResultsHeader>
<TestResultsSummary />
<TestResultsDuration />
</TestResultsHeader>
</TestResults>
);
export default Example;
@@ -0,0 +1,51 @@
"use client";
import {
Test,
TestError,
TestErrorMessage,
TestErrorStack,
TestResults,
TestResultsContent,
TestResultsHeader,
TestResultsSummary,
TestSuite,
TestSuiteContent,
TestSuiteName,
} from "@/components/ai-elements/test-results";
const stackTrace = ` at Object.<anonymous> (/app/src/api.test.ts:45:12)
at Module._compile (node:internal/modules/cjs/loader:1369:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1427:10)`;
const Example = () => (
<TestResults
summary={{
duration: 130,
failed: 1,
passed: 1,
skipped: 0,
total: 2,
}}
>
<TestResultsHeader>
<TestResultsSummary />
</TestResultsHeader>
<TestResultsContent>
<TestSuite defaultOpen name="API" status="failed">
<TestSuiteName />
<TestSuiteContent>
<Test duration={45} name="should fetch data" status="passed" />
<Test duration={85} name="should update" status="failed">
<TestError>
<TestErrorMessage>Expected 200, got 500</TestErrorMessage>
<TestErrorStack>{stackTrace}</TestErrorStack>
</TestError>
</Test>
</TestSuiteContent>
</TestSuite>
</TestResultsContent>
</TestResults>
);
export default Example;
@@ -0,0 +1,40 @@
"use client";
import {
Test,
TestResults,
TestResultsContent,
TestResultsHeader,
TestResultsSummary,
TestSuite,
TestSuiteContent,
TestSuiteName,
} from "@/components/ai-elements/test-results";
const Example = () => (
<TestResults
summary={{
duration: 150,
failed: 0,
passed: 3,
skipped: 0,
total: 3,
}}
>
<TestResultsHeader>
<TestResultsSummary />
</TestResultsHeader>
<TestResultsContent>
<TestSuite name="Auth" status="passed">
<TestSuiteName />
<TestSuiteContent>
<Test duration={45} name="should login" status="passed" />
<Test duration={32} name="should logout" status="passed" />
<Test duration={73} name="should refresh token" status="passed" />
</TestSuiteContent>
</TestSuite>
</TestResultsContent>
</TestResults>
);
export default Example;
@@ -0,0 +1,102 @@
"use client";
import {
Test,
TestError,
TestErrorMessage,
TestErrorStack,
TestResults,
TestResultsContent,
TestResultsDuration,
TestResultsHeader,
TestResultsProgress,
TestResultsSummary,
TestSuite,
TestSuiteContent,
TestSuiteName,
} from "@/components/ai-elements/test-results";
const Example = () => (
<TestResults
summary={{
duration: 3245,
failed: 2,
passed: 12,
skipped: 1,
total: 15,
}}
>
<TestResultsHeader>
<TestResultsSummary />
<TestResultsDuration />
</TestResultsHeader>
<div className="border-b px-4 py-3">
<TestResultsProgress />
</div>
<TestResultsContent>
<TestSuite defaultOpen={true} name="Authentication" status="passed">
<TestSuiteName />
<TestSuiteContent>
<Test
duration={45}
name="should login with valid credentials"
status="passed"
/>
<Test
duration={32}
name="should reject invalid password"
status="passed"
/>
<Test
duration={28}
name="should handle expired tokens"
status="passed"
/>
</TestSuiteContent>
</TestSuite>
<TestSuite defaultOpen={true} name="User API" status="failed">
<TestSuiteName />
<TestSuiteContent>
<Test duration={120} name="should create new user" status="passed" />
<Test duration={85} name="should update user profile" status="failed">
<TestError>
<TestErrorMessage>
Expected status 200 but received 500
</TestErrorMessage>
<TestErrorStack>
{` at Object.<anonymous> (src/user.test.ts:45:12)
at Promise.then.completed (node_modules/jest-circus/build/utils.js:391:28)`}
</TestErrorStack>
</TestError>
</Test>
<Test name="should delete user" status="skipped" />
</TestSuiteContent>
</TestSuite>
<TestSuite name="Database" status="failed">
<TestSuiteName />
<TestSuiteContent>
<Test
duration={200}
name="should connect to database"
status="passed"
/>
<Test
duration={5000}
name="should handle connection timeout"
status="failed"
>
<TestError>
<TestErrorMessage>
Connection timed out after 5000ms
</TestErrorMessage>
</TestError>
</Test>
</TestSuiteContent>
</TestSuite>
</TestResultsContent>
</TestResults>
);
export default Example;
@@ -0,0 +1,31 @@
"use client";
import { Tool, ToolContent, ToolHeader, ToolInput } from "@/components/ai-elements/tool";
import { nanoid } from "nanoid";
const toolCall = {
errorText: undefined,
input: {
prompt: "A futuristic cityscape at sunset with flying cars",
quality: "high",
resolution: "1024x1024",
style: "digital_art",
},
output: undefined,
state: "input-available" as const,
toolCallId: nanoid(),
type: "tool-image_generation" as const,
};
const Example = () => (
<div style={{ height: "500px" }}>
<Tool>
<ToolHeader state={toolCall.state} type={toolCall.type} />
<ToolContent>
<ToolInput input={toolCall.input} />
</ToolContent>
</Tool>
</div>
);
export default Example;
@@ -0,0 +1,30 @@
"use client";
import { Tool, ToolContent, ToolHeader, ToolInput } from "@/components/ai-elements/tool";
import { nanoid } from "nanoid";
const toolCall = {
errorText: undefined,
input: {
include_snippets: true,
max_results: 10,
query: "latest AI market trends 2024",
},
output: undefined,
state: "input-streaming" as const,
toolCallId: nanoid(),
type: "tool-web_search" as const,
};
const Example = () => (
<div style={{ height: "500px" }}>
<Tool>
<ToolHeader state={toolCall.state} type={toolCall.type} />
<ToolContent>
<ToolInput input={toolCall.input} />
</ToolContent>
</Tool>
</div>
);
export default Example;
@@ -0,0 +1,80 @@
"use client";
import { CodeBlock } from "@/components/ai-elements/code-block";
import {
Tool,
ToolContent,
ToolHeader,
ToolInput,
ToolOutput,
} from "@/components/ai-elements/tool";
import type { ToolUIPart } from "ai";
import { nanoid } from "nanoid";
const toolCall: ToolUIPart = {
errorText: undefined,
input: {
database: "analytics",
params: ["2024-01-01"],
query: "SELECT COUNT(*) FROM users WHERE created_at >= ?",
},
output: [
{
"Created At": "2024-01-15",
Email: "john@example.com",
Name: "John Doe",
"User ID": 1,
},
{
"Created At": "2024-01-20",
Email: "jane@example.com",
Name: "Jane Smith",
"User ID": 2,
},
{
"Created At": "2024-02-01",
Email: "bob@example.com",
Name: "Bob Wilson",
"User ID": 3,
},
{
"Created At": "2024-02-10",
Email: "alice@example.com",
Name: "Alice Brown",
"User ID": 4,
},
{
"Created At": "2024-02-15",
Email: "charlie@example.com",
Name: "Charlie Davis",
"User ID": 5,
},
],
state: "output-available" as const,
toolCallId: nanoid(),
type: "tool-database_query" as const,
};
const Example = () => (
<div style={{ height: "500px" }}>
<Tool>
<ToolHeader state={toolCall.state} type={toolCall.type} />
<ToolContent>
<ToolInput input={toolCall.input} />
{toolCall.state === "output-available" && (
<ToolOutput
errorText={toolCall.errorText}
output={
<CodeBlock
code={JSON.stringify(toolCall.output)}
language="json"
/>
}
/>
)}
</ToolContent>
</Tool>
</div>
);
export default Example;
@@ -0,0 +1,44 @@
"use client";
import {
Tool,
ToolContent,
ToolHeader,
ToolInput,
ToolOutput,
} from "@/components/ai-elements/tool";
import type { ToolUIPart } from "ai";
const toolCall: ToolUIPart = {
errorText:
"Connection timeout: The request took longer than 5000ms to complete. Please check your network connection and try again.",
input: {
headers: {
Authorization: "Bearer token123",
"Content-Type": "application/json",
},
method: "GET",
timeout: 5000,
url: "https://api.example.com/data",
},
output: undefined,
state: "output-error" as const,
toolCallId: "api_request_1",
type: "tool-api_request" as const,
};
const Example = () => (
<div style={{ height: "500px" }}>
<Tool>
<ToolHeader state={toolCall.state} type={toolCall.type} />
<ToolContent>
<ToolInput input={toolCall.input} />
{toolCall.state === "output-error" && (
<ToolOutput errorText={toolCall.errorText} output={toolCall.output} />
)}
</ToolContent>
</Tool>
</div>
);
export default Example;
+222
View File
@@ -0,0 +1,222 @@
"use client";
import {
Confirmation,
ConfirmationAccepted,
ConfirmationAction,
ConfirmationActions,
ConfirmationRejected,
ConfirmationRequest,
ConfirmationTitle,
} from "@/components/ai-elements/confirmation";
import {
Tool,
ToolContent,
ToolHeader,
ToolInput,
ToolOutput,
} from "@/components/ai-elements/tool";
import type { ToolUIPart } from "ai";
import { CheckIcon, XIcon } from "lucide-react";
import { nanoid } from "nanoid";
const handleReject = () => {
// In production, call addConfirmationResponse
};
const handleAccept = () => {
// In production, call addConfirmationResponse
};
const toolCall: ToolUIPart = {
errorText: undefined,
input: {
database: "analytics",
params: ["2024-01-01"],
query: "SELECT COUNT(*) FROM users WHERE created_at >= ?",
},
output: `| User ID | Name | Email | Created At |
|---------|------|-------|------------|
| 1 | John Doe | john@example.com | 2024-01-15 |
| 2 | Jane Smith | jane@example.com | 2024-01-20 |
| 3 | Bob Wilson | bob@example.com | 2024-02-01 |
| 4 | Alice Brown | alice@example.com | 2024-02-10 |
| 5 | Charlie Davis | charlie@example.com | 2024-02-15 |`,
state: "output-available" as const,
toolCallId: nanoid(),
type: "tool-database_query" as const,
};
const Example = () => (
<div className="space-y-4" style={{ minHeight: "1400px" }}>
{/* 1. input-streaming: Pending */}
<Tool defaultOpen>
<ToolHeader
state="input-streaming"
title="database_query"
type="tool-database_query"
/>
<ToolContent>
<ToolInput input={{}} />
</ToolContent>
</Tool>
{/* 2. approval-requested: Awaiting Approval */}
<Tool>
<ToolHeader
state={"approval-requested" as ToolUIPart["state"]}
title="database_query"
type="tool-database_query"
/>
<ToolContent>
<ToolInput input={toolCall.input} />
<Confirmation approval={{ id: nanoid() }} state="approval-requested">
<ConfirmationTitle>
<ConfirmationRequest>
This tool will execute a query on the production database.
</ConfirmationRequest>
<ConfirmationAccepted>
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
<span>Accepted</span>
</ConfirmationAccepted>
<ConfirmationRejected>
<XIcon className="size-4 text-destructive" />
<span>Rejected</span>
</ConfirmationRejected>
</ConfirmationTitle>
<ConfirmationActions>
<ConfirmationAction onClick={handleReject} variant="outline">
Reject
</ConfirmationAction>
<ConfirmationAction onClick={handleAccept} variant="default">
Accept
</ConfirmationAction>
</ConfirmationActions>
</Confirmation>
</ToolContent>
</Tool>
{/* 3. approval-responded: Responded */}
<Tool>
<ToolHeader
state={"approval-responded" as ToolUIPart["state"]}
title="database_query"
type="tool-database_query"
/>
<ToolContent>
<ToolInput input={toolCall.input} />
<Confirmation
approval={{ approved: true, id: nanoid() }}
state="approval-responded"
>
<ConfirmationTitle>
<ConfirmationRequest>
This tool will execute a query on the production database.
</ConfirmationRequest>
<ConfirmationAccepted>
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
<span>Accepted</span>
</ConfirmationAccepted>
<ConfirmationRejected>
<XIcon className="size-4 text-destructive" />
<span>Rejected</span>
</ConfirmationRejected>
</ConfirmationTitle>
</Confirmation>
</ToolContent>
</Tool>
{/* 4. input-available: Running */}
<Tool>
<ToolHeader
state="input-available"
title="database_query"
type="tool-database_query"
/>
<ToolContent>
<ToolInput input={toolCall.input} />
</ToolContent>
</Tool>
{/* 5. output-available: Completed */}
<Tool>
<ToolHeader state={toolCall.state} type={toolCall.type} />
<ToolContent>
<ToolInput input={toolCall.input} />
<Confirmation
approval={{ approved: true, id: nanoid() }}
state="output-available"
>
<ConfirmationTitle>
<ConfirmationRequest>
This tool will execute a query on the production database.
</ConfirmationRequest>
<ConfirmationAccepted>
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
<span>Accepted</span>
</ConfirmationAccepted>
<ConfirmationRejected>
<XIcon className="size-4 text-destructive" />
<span>Rejected</span>
</ConfirmationRejected>
</ConfirmationTitle>
</Confirmation>
{toolCall.state === "output-available" && (
<ToolOutput errorText={toolCall.errorText} output={toolCall.output} />
)}
</ToolContent>
</Tool>
{/* 6. output-error: Error */}
<Tool>
<ToolHeader
state="output-error"
title="database_query"
type="tool-database_query"
/>
<ToolContent>
<ToolInput input={toolCall.input} />
<ToolOutput
errorText="Connection timeout: Unable to reach database server"
output={undefined}
/>
</ToolContent>
</Tool>
{/* 7. output-denied: Denied */}
<Tool>
<ToolHeader
state={"output-denied" as ToolUIPart["state"]}
title="database_query"
type="tool-database_query"
/>
<ToolContent>
<ToolInput input={toolCall.input} />
<Confirmation
approval={{
approved: false,
id: nanoid(),
reason: "Query could impact production performance",
}}
state="output-denied"
>
<ConfirmationTitle>
<ConfirmationRequest>
This tool will execute a query on the production database.
</ConfirmationRequest>
<ConfirmationAccepted>
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
<span>Accepted</span>
</ConfirmationAccepted>
<ConfirmationRejected>
<XIcon className="size-4 text-destructive" />
<span>Rejected: Query could impact production performance</span>
</ConfirmationRejected>
</ConfirmationTitle>
</Confirmation>
</ToolContent>
</Tool>
</div>
);
export default Example;
@@ -0,0 +1,290 @@
"use client";
import {
Transcription,
TranscriptionSegment,
} from "@/components/ai-elements/transcription";
import type { Experimental_TranscriptionResult as TranscriptionResult } from "ai";
import { useCallback, useRef, useState } from "react";
const segments: TranscriptionResult["segments"] = [
{
endSecond: 0.219,
startSecond: 0.119,
text: "You",
},
{
endSecond: 0.259,
startSecond: 0.219,
text: " ",
},
{
endSecond: 0.439,
startSecond: 0.259,
text: "can",
},
{
endSecond: 0.459,
startSecond: 0.439,
text: " ",
},
{
endSecond: 0.699,
startSecond: 0.459,
text: "build",
},
{
endSecond: 0.72,
startSecond: 0.699,
text: " ",
},
{
endSecond: 0.799,
startSecond: 0.72,
text: "and",
},
{
endSecond: 0.879,
startSecond: 0.799,
text: " ",
},
{
endSecond: 1.339,
startSecond: 0.879,
text: "host",
},
{
endSecond: 1.359,
startSecond: 1.339,
text: " ",
},
{
endSecond: 1.539,
startSecond: 1.36,
text: "many",
},
{
endSecond: 1.6,
startSecond: 1.539,
text: " ",
},
{
endSecond: 1.86,
startSecond: 1.6,
text: "different",
},
{
endSecond: 1.899,
startSecond: 1.86,
text: " ",
},
{
endSecond: 2.099,
startSecond: 1.899,
text: "types",
},
{
endSecond: 2.119,
startSecond: 2.099,
text: " ",
},
{
endSecond: 2.2,
startSecond: 2.119,
text: "of",
},
{
endSecond: 2.259,
startSecond: 2.2,
text: " ",
},
{
endSecond: 2.96,
startSecond: 2.259,
text: "applications",
},
{
endSecond: 3.479,
startSecond: 2.96,
text: " ",
},
{
endSecond: 3.699,
startSecond: 3.48,
text: "from",
},
{
endSecond: 3.779,
startSecond: 3.699,
text: " ",
},
{
endSecond: 4.099,
startSecond: 3.779,
text: "static",
},
{
endSecond: 4.179,
startSecond: 4.099,
text: " ",
},
{
endSecond: 4.519,
startSecond: 4.179,
text: "sites",
},
{
endSecond: 4.539,
startSecond: 4.519,
text: " ",
},
{
endSecond: 4.759,
startSecond: 4.539,
text: "with",
},
{
endSecond: 4.799,
startSecond: 4.759,
text: " ",
},
{
endSecond: 4.939,
startSecond: 4.799,
text: "your",
},
{
endSecond: 4.96,
startSecond: 4.939,
text: " ",
},
{
endSecond: 5.219,
startSecond: 4.96,
text: "favorite",
},
{
endSecond: 5.319,
startSecond: 5.219,
text: " ",
},
{
endSecond: 5.939,
startSecond: 5.319,
text: "framework,",
},
{
endSecond: 5.96,
startSecond: 5.939,
text: " ",
},
{
endSecond: 6.519,
startSecond: 5.96,
text: "multi-tenant",
},
{
endSecond: 6.559,
startSecond: 6.519,
text: " ",
},
{
endSecond: 7.259,
startSecond: 6.559,
text: "applications",
},
{
endSecond: 7.699,
startSecond: 7.259,
text: " ",
},
{
endSecond: 7.759,
startSecond: 7.699,
text: "or",
},
{
endSecond: 7.859,
startSecond: 7.759,
text: " ",
},
{
endSecond: 8.739,
startSecond: 7.859,
text: "micro-frontends",
},
{
endSecond: 8.78,
startSecond: 8.739,
text: " ",
},
{
endSecond: 8.96,
startSecond: 8.78,
text: "to",
},
{
endSecond: 9.099,
startSecond: 8.96,
text: " ",
},
{
endSecond: 9.779,
startSecond: 9.099,
text: "AI-powered",
},
{
endSecond: 9.82,
startSecond: 9.779,
text: " ",
},
{
endSecond: 10.439,
startSecond: 9.82,
text: "agents.",
},
];
const Example = () => {
const audioRef = useRef<HTMLAudioElement>(null);
const [currentTime, setCurrentTime] = useState(0);
const handleSeek = useCallback((time: number) => {
if (audioRef.current) {
audioRef.current.currentTime = time;
}
}, []);
const handleTimeUpdate = useCallback(() => {
if (audioRef.current) {
setCurrentTime(audioRef.current.currentTime);
}
}, []);
return (
<div className="space-y-6 p-6">
{/* biome-ignore lint/a11y/useMediaCaption: "No caption needed" */}
{/* oxlint-disable-next-line eslint-plugin-jsx-a11y(media-has-caption) */}
<audio controls onTimeUpdate={handleTimeUpdate} ref={audioRef}>
<source src="https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/ElevenLabs_2025-11-10T22_10_24_Hayden_pvc_sp110_s50_sb75_se0_b_m2.mp3" />
</audio>
<Transcription
currentTime={currentTime}
onSeek={handleSeek}
segments={segments}
>
{(segment, index) => (
<TranscriptionSegment
className="text-lg"
index={index}
key={`${segment.startSecond}-${segment.endSecond}`}
segment={segment}
/>
)}
</Transcription>
</div>
);
};
export default Example;
@@ -0,0 +1,245 @@
"use client";
import {
VoiceSelector,
VoiceSelectorAccent,
VoiceSelectorAge,
VoiceSelectorBullet,
VoiceSelectorContent,
VoiceSelectorDescription,
VoiceSelectorEmpty,
VoiceSelectorGender,
VoiceSelectorInput,
VoiceSelectorItem,
VoiceSelectorList,
VoiceSelectorName,
VoiceSelectorPreview,
VoiceSelectorTrigger,
} from "@/components/ai-elements/voice-selector";
import { Button } from "@/components/ui/button";
import type { ComponentProps } from "react";
import { memo, useCallback, useRef, useState } from "react";
const voices: {
id: string;
name: string;
description: string;
gender: ComponentProps<typeof VoiceSelectorGender>["value"];
accent: ComponentProps<typeof VoiceSelectorAccent>["value"];
age: string;
previewUrl: string;
}[] = [
{
accent: "american",
age: "20-30",
description: "Energetic, Social Media Creator",
gender: "male",
id: "liam",
name: "Liam",
previewUrl:
"https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/ElevenLabs_2026-01-16T21_16_50_Liam%20-%20Energetic%2C%20Social%20Media%20Creator_pre_sp100_s50_sb75_se0_b_m2.mp3",
},
{
accent: "american",
age: "30-40",
description: "Dominant, Firm",
gender: "male",
id: "adam",
name: "Adam",
previewUrl:
"https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/ElevenLabs_2026-01-16T21_17_00_Adam%20-%20Dominant%2C%20Firm_pre_sp100_s50_sb75_se0_b_m2.mp3",
},
{
accent: "british",
age: "30-40",
description: "Clear, Engaging Educator",
gender: "female",
id: "alice",
name: "Alice",
previewUrl:
"https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/ElevenLabs_2026-01-16T21_17_09_Alice%20-%20Clear%2C%20Engaging%20Educator_pre_sp100_s50_sb75_se0_b_m2.mp3",
},
{
accent: "american",
age: "50-60",
description: "Wise, Mature, Balanced",
gender: "male",
id: "bill",
name: "Bill",
previewUrl:
"https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/ElevenLabs_2026-01-16T21_17_25_Bill%20-%20Wise%2C%20Mature%2C%20Balanced_pre_sp100_s50_sb75_se0_b_m2.mp3",
},
{
accent: "american",
age: "20-30",
description: "Playful, Bright, Warm",
gender: "female",
id: "jessica",
name: "Jessica",
previewUrl:
"https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/ElevenLabs_2026-01-16T21_17_50_Jessica%20-%20Playful%2C%20Bright%2C%20Warm_pre_sp100_s50_sb75_se0_b_m2.mp3",
},
{
accent: "british",
age: "30-40",
description: "Velvety Actress",
gender: "female",
id: "lily",
name: "Lily",
previewUrl:
"https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/ElevenLabs_2026-01-16T21_18_03_Lily%20-%20Velvety%20Actress_pre_sp100_s50_sb75_se0_b_m2.mp3",
},
];
interface VoiceItemProps {
voice: (typeof voices)[0];
playingVoice: string | null;
loadingVoice: string | null;
onSelect: (id: string) => void;
onPreview: (id: string) => void;
}
const VoiceItem = memo(
({
voice,
playingVoice,
loadingVoice,
onSelect,
onPreview,
}: VoiceItemProps) => {
const handleSelect = useCallback(
() => onSelect(voice.id),
[onSelect, voice.id]
);
const handlePreview = useCallback(
() => onPreview(voice.id),
[onPreview, voice.id]
);
return (
<VoiceSelectorItem
key={voice.id}
onSelect={handleSelect}
value={voice.id}
>
<VoiceSelectorPreview
loading={loadingVoice === voice.id}
onPlay={handlePreview}
playing={playingVoice === voice.id}
/>
<VoiceSelectorName>{voice.name}</VoiceSelectorName>
<VoiceSelectorDescription>{voice.description}</VoiceSelectorDescription>
<VoiceSelectorBullet />
<VoiceSelectorAccent value={voice.accent} />
<VoiceSelectorBullet />
<VoiceSelectorAge>{voice.age}</VoiceSelectorAge>
<VoiceSelectorBullet />
<VoiceSelectorGender value={voice.gender} />
</VoiceSelectorItem>
);
}
);
VoiceItem.displayName = "VoiceItem";
const Example = () => {
const [open, setOpen] = useState(false);
const [selectedVoice, setSelectedVoice] = useState<string | null>(null);
const [playingVoice, setPlayingVoice] = useState<string | null>(null);
const [loadingVoice, setLoadingVoice] = useState<string | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
const handleSelect = useCallback((voiceId: string) => {
setSelectedVoice(voiceId);
setOpen(false);
}, []);
const handlePreview = useCallback(
(voiceId: string) => {
const voice = voices.find((v) => v.id === voiceId);
if (!voice) {
return;
}
// If clicking the same voice that's playing, pause it
if (playingVoice === voiceId) {
audioRef.current?.pause();
setPlayingVoice(null);
return;
}
// Stop any currently playing audio
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
setLoadingVoice(voiceId);
const audio = new Audio(voice.previewUrl);
audioRef.current = audio;
audio.addEventListener("canplaythrough", () => {
setLoadingVoice(null);
setPlayingVoice(voiceId);
audio.play();
});
audio.addEventListener("ended", () => {
setPlayingVoice(null);
});
audio.addEventListener("error", () => {
setLoadingVoice(null);
setPlayingVoice(null);
});
audio.load();
},
[playingVoice]
);
const selectedVoiceData = voices.find((voice) => voice.id === selectedVoice);
return (
<div className="flex size-full flex-col items-center justify-center">
<VoiceSelector onOpenChange={setOpen} open={open}>
<VoiceSelectorTrigger asChild>
<Button className="w-full max-w-xs" variant="outline">
{selectedVoiceData ? (
<>
<VoiceSelectorName>{selectedVoiceData.name}</VoiceSelectorName>
<VoiceSelectorAccent value={selectedVoiceData.accent} />
<VoiceSelectorBullet />
<VoiceSelectorAge>{selectedVoiceData.age}</VoiceSelectorAge>
<VoiceSelectorBullet />
<VoiceSelectorGender value={selectedVoiceData.gender} />
</>
) : (
<span className="flex-1 text-left text-sm">
Select a voice...
</span>
)}
</Button>
</VoiceSelectorTrigger>
<VoiceSelectorContent className="max-w-md">
<VoiceSelectorInput placeholder="Search voices..." />
<VoiceSelectorList>
<VoiceSelectorEmpty>No voices found.</VoiceSelectorEmpty>
{voices.map((voice) => (
<VoiceItem
key={voice.id}
loadingVoice={loadingVoice}
onPreview={handlePreview}
onSelect={handleSelect}
playingVoice={playingVoice}
voice={voice}
/>
))}
</VoiceSelectorList>
</VoiceSelectorContent>
</VoiceSelector>
</div>
);
};
export default Example;
@@ -0,0 +1,115 @@
"use client";
import {
WebPreview,
WebPreviewBody,
WebPreviewConsole,
WebPreviewNavigation,
WebPreviewNavigationButton,
WebPreviewUrl,
} from "@/components/ai-elements/web-preview";
import {
ArrowLeftIcon,
ArrowRightIcon,
ExternalLinkIcon,
Maximize2Icon,
MousePointerClickIcon,
RefreshCcwIcon,
} from "lucide-react";
import { useCallback, useState } from "react";
const handleUrlChange = (url: string) => {
console.log("URL changed to:", url);
};
const handleGoBack = () => {
console.log("Go back");
};
const handleGoForward = () => {
console.log("Go forward");
};
const handleReload = () => {
console.log("Reload");
};
const handleSelect = () => {
console.log("Select");
};
const handleOpenInNewTab = () => {
console.log("Open in new tab");
};
const exampleLogs = [
{
level: "log" as const,
message: "Page loaded successfully",
timestamp: new Date(Date.now() - 10_000),
},
{
level: "warn" as const,
message: "Deprecated API usage detected",
timestamp: new Date(Date.now() - 5000),
},
{
level: "error" as const,
message: "Failed to load resource",
timestamp: new Date(),
},
];
const Example = () => {
const [_fullscreen, setFullscreen] = useState(false);
const handleToggleFullscreen = useCallback(
() => setFullscreen((prev) => !prev),
[]
);
return (
<WebPreview
defaultUrl="/"
onUrlChange={handleUrlChange}
style={{ height: "400px" }}
>
<WebPreviewNavigation>
<WebPreviewNavigationButton onClick={handleGoBack} tooltip="Go back">
<ArrowLeftIcon className="size-4" />
</WebPreviewNavigationButton>
<WebPreviewNavigationButton
onClick={handleGoForward}
tooltip="Go forward"
>
<ArrowRightIcon className="size-4" />
</WebPreviewNavigationButton>
<WebPreviewNavigationButton onClick={handleReload} tooltip="Reload">
<RefreshCcwIcon className="size-4" />
</WebPreviewNavigationButton>
<WebPreviewUrl />
<WebPreviewNavigationButton onClick={handleSelect} tooltip="Select">
<MousePointerClickIcon className="size-4" />
</WebPreviewNavigationButton>
<WebPreviewNavigationButton
onClick={handleOpenInNewTab}
tooltip="Open in new tab"
>
<ExternalLinkIcon className="size-4" />
</WebPreviewNavigationButton>
<WebPreviewNavigationButton
onClick={handleToggleFullscreen}
tooltip="Maximize"
>
<Maximize2Icon className="size-4" />
</WebPreviewNavigationButton>
</WebPreviewNavigation>
<WebPreviewBody src="https://preview-v0me-kzml7zc6fkcvbyhzrf47.vusercontent.net/" />
<WebPreviewConsole logs={exampleLogs} />
</WebPreview>
);
};
export default Example;