mirror of
https://github.com/temetro/temetro.git
synced 2026-08-27 19:06:52 +00:00
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:
@@ -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'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 = 'admin'
|
||||
</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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user