"use client"; import { ComposerAddAttachment, ComposerAttachments, UserMessageAttachments, } from "@/components/assistant-ui/attachment"; import { MarkdownText } from "@/components/assistant-ui/markdown-text"; import { DotMatrix } from "@/components/assistant-ui/dot-matrix"; import { MessageTiming } from "@/components/assistant-ui/message-timing"; import { ToolFallback } from "@/components/assistant-ui/tool-fallback"; import { ToolGroupContent, ToolGroupRoot, ToolGroupTrigger, } from "@/components/assistant-ui/tool-group"; import { ThreadList, ThreadListItems, ThreadListNew, ThreadListRoot, } from "@/components/assistant-ui/thread-list"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { Reasoning, ReasoningContent, ReasoningRoot, ReasoningText, ReasoningTrigger, } from "@/components/assistant-ui/reasoning"; import { Button } from "@/components/ui/radix/button"; import { cn } from "@/lib/utils"; import icon from "@/public/favicon/icon.svg"; import { ComposerQuotePreview, QuoteBlock, SelectionToolbar, } from "@/components/assistant-ui/quote"; import { ComposerTriggerPopover } from "@/components/assistant-ui/composer-trigger-popover"; import { DirectiveText } from "@/components/assistant-ui/directive-text"; import { ActionBarMorePrimitive, ActionBarPrimitive, AuiIf, type AssistantState, BranchPickerPrimitive, ComposerPrimitive, ErrorPrimitive, groupPartByType, MessagePrimitive, ThreadPrimitive, unstable_useMentionAdapter, unstable_useSlashCommandAdapter, useAui, useAuiState, type Unstable_SlashCommand, } from "@assistant-ui/react"; import { ArrowDownIcon, ArrowUpIcon, ChartColumnIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, CloudSunIcon, CodeXmlIcon, CopyIcon, DownloadIcon, FileTextIcon, GlobeIcon, HelpCircleIcon, LanguagesIcon, LightbulbIcon, MenuIcon, MicIcon, MoreHorizontalIcon, PanelLeftIcon, PencilIcon, PencilLineIcon, RefreshCwIcon, ShareIcon, SlashIcon, SquareIcon, WrenchIcon, } from "lucide-react"; import { LexicalComposerInput, type DirectiveChipProps, } from "@assistant-ui/react-lexical"; import Image from "next/image"; import { useState, type FC, type ReactNode } from "react"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/radix/sheet"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/radix/tooltip"; import { ModelSelector } from "@/components/assistant-ui/model-selector"; import { docsModelOptions } from "@/components/docs/assistant/docs-model-options"; import { DEFAULT_MODEL_ID } from "@/constants/model"; const Logo: FC = () => { return (
logo assistant-ui
); }; const Sidebar: FC<{ collapsed?: boolean }> = ({ collapsed }) => { return ( ); }; const MobileSidebar: FC = () => { return (
); }; const models = docsModelOptions(); const ModelPicker: FC = () => { return ( ); }; const ThreadTitle: FC = () => { const title = useAuiState( (s) => s.threads.threadItems.find((t) => t.id === s.threads.mainThreadId)?.title, ); return ( {title ?? "New Chat"} ); }; const Header: FC<{ sidebarCollapsed: boolean; onToggleSidebar: () => void; }> = ({ sidebarCollapsed, onToggleSidebar }) => { return (
); }; // Startup exposes a loading placeholder thread; treat it as a new chat so // the composer mounts centered. Loads after startup keep the docked layout. const isNewChatView = (s: AssistantState) => s.thread.messages.length === 0 && (!s.thread.isLoading || s.threads.isLoading); const Thread: FC = () => { const isEmpty = useAuiState(isNewChatView); return (
{({ message }) => { if (message.composer.isEditing) return ; if (message.role === "user") return ; return ; }}
s.composer.isEmpty}>
); }; const ThreadScrollToBottom: FC = () => { return ( ); }; const ThreadWelcome: FC = () => { return (

How can I help you today?

); }; type SuggestionGroup = { label: string; icon: ReactNode; options: { label: string; prompt: string }[]; }; const SUGGESTION_GROUPS: SuggestionGroup[] = [ { label: "Weather", icon: , options: [ { label: "in San Francisco", prompt: "What's the weather in San Francisco?", }, { label: "in Singapore", prompt: "What's the weather in Singapore?" }, { label: "in Tokyo", prompt: "What's the weather in Tokyo?" }, { label: "in London", prompt: "What's the weather in London?" }, ], }, { label: "Code", icon: , options: [ { label: "explain React hooks", prompt: "Explain React hooks like useState and useEffect", }, { label: "write a debounce function", prompt: "Write a debounce function in TypeScript", }, { label: "review a useEffect cleanup", prompt: "Show me the right way to clean up a subscription in useEffect", }, ], }, { label: "Write", icon: , options: [ { label: "a birthday card message", prompt: "Help me write a birthday card message for a friend in the notepad", }, { label: "a product announcement", prompt: "Draft a short product announcement for a new dark mode", }, { label: "release notes", prompt: "Write release notes for a bugfix release of a React component library", }, { label: "a PR description", prompt: "Write a pull request description for a change that adds keyboard shortcuts", }, ], }, { label: "Analyze", icon: , options: [ { label: "React vs Vue vs Svelte", prompt: "Compare React, Vue, and Svelte in a table", }, { label: "GDP of US, China, Japan", prompt: "Compare the GDP of the United States, China, and Japan in a table", }, { label: "pros and cons of SSR", prompt: "What are the pros and cons of server-side rendering?", }, ], }, { label: "Brainstorm", icon: , options: [ { label: "side project ideas", prompt: "Brainstorm five side project ideas for a React developer", }, { label: "names for a dev tool", prompt: "Brainstorm names for a developer tools startup", }, { label: "talk topics", prompt: "Brainstorm talk topics for a React meetup", }, ], }, ]; const suggestionChipClass = "aui-thread-welcome-suggestion text-foreground hover:bg-muted border-border/60 h-auto gap-1.5 rounded-full border px-3.5 py-1.5 text-sm font-normal whitespace-nowrap transition-colors [&_svg]:size-4"; const ThreadSuggestions: FC = () => { const aui = useAui(); const [expandedLabel, setExpandedLabel] = useState(null); const expandedGroup = SUGGESTION_GROUPS.find( (group) => group.label === expandedLabel, ); const sendPrompt = (prompt: string) => { if (aui.thread().getState().isRunning) return; aui.thread().append({ content: [{ type: "text", text: prompt }], runConfig: aui.composer().getState().runConfig, }); }; return (
{SUGGESTION_GROUPS.map((group) => ( ))}
{expandedGroup && (
{expandedGroup.options.map((option) => ( ))}
)}
); }; const slashCommands: readonly Unstable_SlashCommand[] = [ { id: "summarize", description: "Summarize the conversation", icon: "FileText", execute: () => console.log("[base example] /summarize invoked"), }, { id: "translate", description: "Translate text to another language", icon: "Languages", execute: () => console.log("[base example] /translate invoked"), }, { id: "search", description: "Search the web for information", icon: "Globe", execute: () => console.log("[base example] /search invoked"), }, { id: "help", description: "List available commands", icon: "HelpCircle", execute: () => console.log("[base example] /help invoked"), }, ]; const slashIconMap: Record> = { FileText: FileTextIcon, Languages: LanguagesIcon, Globe: GlobeIcon, HelpCircle: HelpCircleIcon, }; function DirectiveChip(props: DirectiveChipProps) { const { directiveId, directiveType, label } = props; const showWrench = directiveType !== "command"; return ( {showWrench && ( )} {label} ); } const Composer: FC = () => { const mention = unstable_useMentionAdapter({ fallbackIcon: WrenchIcon }); const slash = unstable_useSlashCommandAdapter({ commands: slashCommands, iconMap: slashIconMap, fallbackIcon: SlashIcon, }); return (
); }; const ComposerAction: FC = () => { return (
s.thread.capabilities.dictation}> s.composer.dictation == null}> s.composer.dictation != null}> !s.thread.isRunning}> s.thread.isRunning}>
); }; const MessageError: FC = () => { return ( ); }; const AssistantWorkingIndicator: FC = () => { const isEmpty = useAuiState((s) => s.message.content.length === 0); if (isEmpty) { return ( Connecting ); } return ( {"●"} ); }; const AssistantMessage: FC = () => { // reserves space for action bar and compensates with `-mb` for consistent msg spacing // keeps hovered action bar from shifting layout (autohide doesn't support absolute positioning well) // for pt-[n] use -mb-[n + 6] & min-h-[n + 6] to preserve compensation const ACTION_BAR_PT = "pt-1.5"; const ACTION_BAR_HEIGHT = `-mb-7.5 min-h-7.5 ${ACTION_BAR_PT}`; return (
{({ part, children }) => { switch (part.type) { case "group-chainOfThought": return
{children}
; case "group-tool": return ( {children} ); case "group-reasoning": { const running = part.status.type === "running"; return ( {children} ); } case "text": return ; case "reasoning": return ; case "tool-call": return part.toolUI ?? ; case "indicator": return ; case "data": return part.dataRendererUI; default: return null; } }}
); }; const AssistantActionBar: FC = () => { return ( s.message.isCopied}> !s.message.isCopied}> Export as Markdown ); }; const UserMessage: FC = () => { return (
{(quote) => }
); }; const UserActionBar: FC = () => { return ( ); }; const EditComposer: FC = () => { return (
); }; const BranchPicker: FC = ({ className, ...rest }) => { return ( / ); }; export const Base: FC = () => { const [sidebarCollapsed, setSidebarCollapsed] = useState(false); return (
setSidebarCollapsed(!sidebarCollapsed)} />
); };