e30e75b5d4
Code Quality / Oxlint + Oxfmt (push) Waiting to run
Code Quality / Template Sync (push) Waiting to run
Code Quality / Build Changed Packages (push) Waiting to run
Code Quality / Test Changed Packages (push) Waiting to run
Deploy Expo Example / Deploy Production (push) Waiting to run
Deploy Ink Example / Deploy Production (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.12) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Waiting to run
Deploy Shadcn Registry / Deploy Production (push) Waiting to run
Template Metrics / LOC + Bundle Size (push) Waiting to run
Changesets / Create Version PR (push) Has been cancelled
985 lines
33 KiB
TypeScript
985 lines
33 KiB
TypeScript
"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 (
|
|
<div className="flex items-center gap-2 px-2 text-sm font-medium">
|
|
<Image
|
|
src={icon}
|
|
alt="logo"
|
|
className="size-5 dark:hue-rotate-180 dark:invert"
|
|
/>
|
|
<span className="text-foreground/90">assistant-ui</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const Sidebar: FC<{ collapsed?: boolean }> = ({ collapsed }) => {
|
|
return (
|
|
<aside
|
|
className={cn(
|
|
"flex h-full flex-col overflow-hidden transition-all duration-200",
|
|
collapsed ? "w-12" : "w-65",
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"mt-2 flex h-12 shrink-0 items-center transition-[padding] duration-200",
|
|
collapsed ? "px-3.5" : "px-6",
|
|
)}
|
|
>
|
|
<Image
|
|
src={icon}
|
|
alt="logo"
|
|
className="size-5 shrink-0 dark:hue-rotate-180 dark:invert"
|
|
/>
|
|
<span
|
|
className={cn(
|
|
"text-foreground/90 ml-2 text-sm font-medium whitespace-nowrap transition-opacity duration-200",
|
|
collapsed && "opacity-0",
|
|
)}
|
|
>
|
|
assistant-ui
|
|
</span>
|
|
</div>
|
|
<ThreadListRoot
|
|
className={cn(
|
|
"relative flex-1 overflow-y-auto transition-[padding,width] duration-200",
|
|
collapsed ? "w-12 px-2 pt-1" : "w-65 p-3",
|
|
)}
|
|
>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<ThreadListNew
|
|
className={cn(
|
|
"overflow-hidden transition-all duration-200",
|
|
collapsed
|
|
? "w-8 gap-0 px-2 has-[>svg]:px-2"
|
|
: "w-full gap-2 px-2.5 has-[>svg]:px-2.5",
|
|
)}
|
|
labelClassName={cn(
|
|
"overflow-hidden transition-all duration-200",
|
|
collapsed ? "max-w-0 opacity-0" : "max-w-24 opacity-100",
|
|
)}
|
|
/>
|
|
</TooltipTrigger>
|
|
{collapsed && (
|
|
<TooltipContent side="right">New Thread</TooltipContent>
|
|
)}
|
|
</Tooltip>
|
|
<ThreadListItems
|
|
aria-hidden={collapsed}
|
|
inert={collapsed}
|
|
className={cn(
|
|
"transition-[opacity,transform] duration-150",
|
|
collapsed
|
|
? "pointer-events-none opacity-0 delay-50"
|
|
: "translate-x-0 opacity-100",
|
|
)}
|
|
/>
|
|
</ThreadListRoot>
|
|
</aside>
|
|
);
|
|
};
|
|
|
|
const MobileSidebar: FC = () => {
|
|
return (
|
|
<Sheet>
|
|
<SheetTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-8 shrink-0 md:hidden"
|
|
>
|
|
<MenuIcon className="size-4" />
|
|
<span className="sr-only">Toggle menu</span>
|
|
</Button>
|
|
</SheetTrigger>
|
|
<SheetContent side="left" className="flex w-70 flex-col p-0">
|
|
<div className="flex h-12 shrink-0 items-center px-4">
|
|
<Logo />
|
|
</div>
|
|
<div className="relative flex-1 overflow-y-auto p-3">
|
|
<ThreadList />
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
};
|
|
|
|
const models = docsModelOptions();
|
|
|
|
const ModelPicker: FC = () => {
|
|
return (
|
|
<ModelSelector
|
|
models={models}
|
|
defaultValue={DEFAULT_MODEL_ID}
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 rounded-full"
|
|
/>
|
|
);
|
|
};
|
|
|
|
const ThreadTitle: FC = () => {
|
|
const title = useAuiState(
|
|
(s) =>
|
|
s.threads.threadItems.find((t) => t.id === s.threads.mainThreadId)?.title,
|
|
);
|
|
|
|
return (
|
|
<span className="min-w-0 truncate text-sm font-medium">
|
|
{title ?? "New Chat"}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const Header: FC<{
|
|
sidebarCollapsed: boolean;
|
|
onToggleSidebar: () => void;
|
|
}> = ({ sidebarCollapsed, onToggleSidebar }) => {
|
|
return (
|
|
<header className="flex h-12 shrink-0 items-center gap-2 px-4">
|
|
<MobileSidebar />
|
|
<TooltipIconButton
|
|
variant="ghost"
|
|
size="icon"
|
|
tooltip={sidebarCollapsed ? "Show sidebar" : "Hide sidebar"}
|
|
side="bottom"
|
|
onClick={onToggleSidebar}
|
|
className="hidden size-8 md:flex"
|
|
>
|
|
<PanelLeftIcon className="size-4" />
|
|
</TooltipIconButton>
|
|
<ThreadTitle />
|
|
<TooltipIconButton
|
|
variant="ghost"
|
|
size="icon"
|
|
tooltip="Share"
|
|
side="bottom"
|
|
disabled
|
|
className="ml-auto size-8"
|
|
>
|
|
<ShareIcon className="size-4" />
|
|
</TooltipIconButton>
|
|
</header>
|
|
);
|
|
};
|
|
|
|
// 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 (
|
|
<ThreadPrimitive.Root
|
|
className="aui-root aui-thread-root bg-background @container flex h-full flex-col"
|
|
style={{
|
|
["--thread-max-width" as string]: "44rem",
|
|
["--composer-bg" as string]:
|
|
"color-mix(in oklab, var(--color-muted) 30%, var(--color-background))",
|
|
["--composer-radius" as string]: "1.5rem",
|
|
["--composer-padding" as string]: "8px",
|
|
}}
|
|
>
|
|
<ThreadPrimitive.Viewport
|
|
turnAnchor="top"
|
|
data-slot="aui_thread-viewport"
|
|
className={cn(
|
|
"relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4",
|
|
isEmpty && "justify-center",
|
|
)}
|
|
>
|
|
<AuiIf condition={isNewChatView}>
|
|
<ThreadWelcome />
|
|
</AuiIf>
|
|
|
|
<div
|
|
data-slot="aui_message-group"
|
|
className="mb-14 flex flex-col gap-y-6 empty:hidden"
|
|
>
|
|
<ThreadPrimitive.Messages>
|
|
{({ message }) => {
|
|
if (message.composer.isEditing) return <EditComposer />;
|
|
if (message.role === "user") return <UserMessage />;
|
|
return <AssistantMessage />;
|
|
}}
|
|
</ThreadPrimitive.Messages>
|
|
</div>
|
|
|
|
<ThreadPrimitive.ViewportFooter
|
|
className={cn(
|
|
"aui-thread-viewport-footer bg-background mx-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible pb-4 md:pb-6",
|
|
!isEmpty && "sticky bottom-0 mt-auto rounded-t-(--composer-radius)",
|
|
)}
|
|
>
|
|
<ThreadScrollToBottom />
|
|
<Composer />
|
|
<AuiIf condition={isNewChatView}>
|
|
<div className="aui-thread-welcome-suggestions-shell min-h-19">
|
|
<AuiIf condition={(s) => s.composer.isEmpty}>
|
|
<ThreadSuggestions />
|
|
</AuiIf>
|
|
</div>
|
|
</AuiIf>
|
|
</ThreadPrimitive.ViewportFooter>
|
|
</ThreadPrimitive.Viewport>
|
|
|
|
<SelectionToolbar />
|
|
</ThreadPrimitive.Root>
|
|
);
|
|
};
|
|
|
|
const ThreadScrollToBottom: FC = () => {
|
|
return (
|
|
<ThreadPrimitive.ScrollToBottom asChild>
|
|
<TooltipIconButton
|
|
tooltip="Scroll to bottom"
|
|
variant="outline"
|
|
className="aui-thread-scroll-to-bottom dark:border-border dark:bg-background dark:hover:bg-accent absolute -top-12 z-10 self-center rounded-full p-4 disabled:invisible"
|
|
>
|
|
<ArrowDownIcon />
|
|
</TooltipIconButton>
|
|
</ThreadPrimitive.ScrollToBottom>
|
|
);
|
|
};
|
|
|
|
const ThreadWelcome: FC = () => {
|
|
return (
|
|
<div className="aui-thread-welcome-root mx-auto mb-6 flex w-full max-w-(--thread-max-width) flex-col items-center px-4 text-center">
|
|
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-2xl font-semibold duration-200">
|
|
How can I help you today?
|
|
</h1>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type SuggestionGroup = {
|
|
label: string;
|
|
icon: ReactNode;
|
|
options: { label: string; prompt: string }[];
|
|
};
|
|
|
|
const SUGGESTION_GROUPS: SuggestionGroup[] = [
|
|
{
|
|
label: "Weather",
|
|
icon: <CloudSunIcon />,
|
|
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: <CodeXmlIcon />,
|
|
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: <PencilLineIcon />,
|
|
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: <ChartColumnIcon />,
|
|
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: <LightbulbIcon />,
|
|
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<string | null>(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 (
|
|
<div className="aui-thread-welcome-suggestions flex w-full flex-col gap-2 px-4">
|
|
<div className="w-full scrollbar-none overflow-x-auto">
|
|
<div className="mx-auto flex w-max items-center gap-2">
|
|
{SUGGESTION_GROUPS.map((group) => (
|
|
<Button
|
|
key={group.label}
|
|
variant="ghost"
|
|
className={cn(
|
|
suggestionChipClass,
|
|
group.label === expandedLabel && "bg-muted",
|
|
)}
|
|
onClick={() =>
|
|
setExpandedLabel(
|
|
group.label === expandedLabel ? null : group.label,
|
|
)
|
|
}
|
|
>
|
|
{group.icon}
|
|
{group.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
{expandedGroup && (
|
|
<div
|
|
key={expandedGroup.label}
|
|
className="fade-in slide-in-from-top-1 animate-in w-full scrollbar-none overflow-x-auto duration-200"
|
|
>
|
|
<div className="mx-auto flex w-max items-center gap-2">
|
|
{expandedGroup.options.map((option) => (
|
|
<Button
|
|
key={option.label}
|
|
variant="ghost"
|
|
className={suggestionChipClass}
|
|
onClick={() => sendPrompt(option.prompt)}
|
|
>
|
|
{option.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
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<string, FC<{ className?: string }>> = {
|
|
FileText: FileTextIcon,
|
|
Languages: LanguagesIcon,
|
|
Globe: GlobeIcon,
|
|
HelpCircle: HelpCircleIcon,
|
|
};
|
|
|
|
function DirectiveChip(props: DirectiveChipProps) {
|
|
const { directiveId, directiveType, label } = props;
|
|
const showWrench = directiveType !== "command";
|
|
return (
|
|
<span
|
|
className="aui-directive-chip"
|
|
data-directive-type={directiveType}
|
|
data-directive-id={directiveId}
|
|
>
|
|
{showWrench && (
|
|
<span className="aui-directive-chip-icon">
|
|
<WrenchIcon className="size-3" />
|
|
</span>
|
|
)}
|
|
<span className="aui-directive-chip-label">{label}</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
const Composer: FC = () => {
|
|
const mention = unstable_useMentionAdapter({ fallbackIcon: WrenchIcon });
|
|
const slash = unstable_useSlashCommandAdapter({
|
|
commands: slashCommands,
|
|
iconMap: slashIconMap,
|
|
fallbackIcon: SlashIcon,
|
|
});
|
|
|
|
return (
|
|
<ComposerPrimitive.Unstable_TriggerPopoverRoot>
|
|
<ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col">
|
|
<ComposerPrimitive.AttachmentDropzone asChild>
|
|
<div
|
|
data-slot="aui_composer-shell"
|
|
className="border-border/60 data-[dragging=true]:border-ring focus-within:border-border dark:border-muted-foreground/15 dark:focus-within:border-muted-foreground/30 flex w-full flex-col gap-2 rounded-(--composer-radius) border bg-(--composer-bg) p-(--composer-padding) shadow-[0_4px_16px_-8px_rgba(0,0,0,0.08),0_1px_2px_rgba(0,0,0,0.04)] transition-[border-color,box-shadow] focus-within:shadow-[0_6px_24px_-8px_rgba(0,0,0,0.12),0_1px_2px_rgba(0,0,0,0.05)] data-[dragging=true]:border-dashed data-[dragging=true]:bg-[color-mix(in_oklab,var(--color-accent)_50%,var(--color-background))] dark:shadow-none"
|
|
>
|
|
<ComposerQuotePreview />
|
|
<ComposerAttachments />
|
|
<LexicalComposerInput
|
|
directiveChip={DirectiveChip}
|
|
placeholder="Send a message... (@ to mention, / for commands)"
|
|
className="aui-composer-input [&_.aui-lexical-placeholder]:text-muted-foreground/80 relative max-h-32 min-h-10 w-full resize-none bg-transparent px-2.5 py-1 text-base outline-none [&_.aui-directive-chip]:inline-flex [&_.aui-directive-chip]:items-baseline [&_.aui-directive-chip]:gap-1 [&_.aui-directive-chip]:rounded-md [&_.aui-directive-chip]:bg-blue-100 [&_.aui-directive-chip]:px-1.5 [&_.aui-directive-chip]:py-0.5 [&_.aui-directive-chip]:text-[13px] [&_.aui-directive-chip]:leading-none [&_.aui-directive-chip]:font-medium [&_.aui-directive-chip]:text-blue-700 dark:[&_.aui-directive-chip]:bg-blue-900/50 dark:[&_.aui-directive-chip]:text-blue-300 [&_.aui-directive-chip-icon]:self-center [&_.aui-lexical-input]:min-h-lh [&_.aui-lexical-input]:outline-none [&_.aui-lexical-placeholder]:pointer-events-none [&_.aui-lexical-placeholder]:absolute [&_.aui-lexical-placeholder]:top-0 [&_.aui-lexical-placeholder]:right-0 [&_.aui-lexical-placeholder]:left-0 [&_.aui-lexical-placeholder]:truncate [&_.aui-lexical-placeholder]:px-2.5 [&_.aui-lexical-placeholder]:py-1"
|
|
/>
|
|
<ComposerAction />
|
|
</div>
|
|
</ComposerPrimitive.AttachmentDropzone>
|
|
|
|
<ComposerTriggerPopover char="@" {...mention} />
|
|
|
|
<ComposerTriggerPopover
|
|
char="/"
|
|
{...slash}
|
|
emptyItemsLabel="No matching commands"
|
|
/>
|
|
</ComposerPrimitive.Root>
|
|
</ComposerPrimitive.Unstable_TriggerPopoverRoot>
|
|
);
|
|
};
|
|
|
|
const ComposerAction: FC = () => {
|
|
return (
|
|
<div className="aui-composer-action-wrapper relative flex items-center justify-between">
|
|
<div className="flex items-center gap-1">
|
|
<ComposerAddAttachment />
|
|
<ModelPicker />
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<AuiIf condition={(s) => s.thread.capabilities.dictation}>
|
|
<AuiIf condition={(s) => s.composer.dictation == null}>
|
|
<ComposerPrimitive.Dictate asChild>
|
|
<TooltipIconButton
|
|
tooltip="Voice input"
|
|
side="bottom"
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="aui-composer-dictate size-7 rounded-full"
|
|
aria-label="Start voice input"
|
|
>
|
|
<MicIcon className="aui-composer-dictate-icon size-4" />
|
|
</TooltipIconButton>
|
|
</ComposerPrimitive.Dictate>
|
|
</AuiIf>
|
|
<AuiIf condition={(s) => s.composer.dictation != null}>
|
|
<ComposerPrimitive.StopDictation asChild>
|
|
<TooltipIconButton
|
|
tooltip="Stop dictation"
|
|
side="bottom"
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="aui-composer-stop-dictation text-destructive size-7 rounded-full"
|
|
aria-label="Stop voice input"
|
|
>
|
|
<SquareIcon className="aui-composer-stop-dictation-icon size-3.5 animate-pulse fill-current" />
|
|
</TooltipIconButton>
|
|
</ComposerPrimitive.StopDictation>
|
|
</AuiIf>
|
|
</AuiIf>
|
|
<AuiIf condition={(s) => !s.thread.isRunning}>
|
|
<ComposerPrimitive.Send asChild>
|
|
<TooltipIconButton
|
|
tooltip="Send message"
|
|
side="bottom"
|
|
type="button"
|
|
variant="default"
|
|
size="icon"
|
|
className="aui-composer-send size-7 rounded-full"
|
|
aria-label="Send message"
|
|
>
|
|
<ArrowUpIcon className="aui-composer-send-icon size-4.5" />
|
|
</TooltipIconButton>
|
|
</ComposerPrimitive.Send>
|
|
</AuiIf>
|
|
<AuiIf condition={(s) => s.thread.isRunning}>
|
|
<ComposerPrimitive.Cancel asChild>
|
|
<Button
|
|
type="button"
|
|
variant="default"
|
|
size="icon"
|
|
className="aui-composer-cancel size-7 rounded-full"
|
|
aria-label="Stop generating"
|
|
>
|
|
<SquareIcon className="aui-composer-cancel-icon size-3.5 fill-current" />
|
|
</Button>
|
|
</ComposerPrimitive.Cancel>
|
|
</AuiIf>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const MessageError: FC = () => {
|
|
return (
|
|
<MessagePrimitive.Error>
|
|
<ErrorPrimitive.Root className="aui-message-error-root border-destructive bg-destructive/10 text-destructive dark:bg-destructive/5 mt-2 rounded-md border p-3 text-sm dark:text-red-200">
|
|
<ErrorPrimitive.Message className="aui-message-error-message line-clamp-2" />
|
|
</ErrorPrimitive.Root>
|
|
</MessagePrimitive.Error>
|
|
);
|
|
};
|
|
|
|
const AssistantWorkingIndicator: FC = () => {
|
|
const isEmpty = useAuiState((s) => s.message.content.length === 0);
|
|
if (isEmpty) {
|
|
return (
|
|
<span
|
|
data-slot="aui_assistant-message-indicator"
|
|
className="text-muted-foreground inline-flex items-center gap-2 align-middle"
|
|
>
|
|
<DotMatrix state="connecting" aria-hidden />
|
|
<span className="text-sm">Connecting</span>
|
|
</span>
|
|
);
|
|
}
|
|
return (
|
|
<span
|
|
data-slot="aui_assistant-message-indicator"
|
|
className="animate-pulse font-sans"
|
|
aria-label="Assistant is working"
|
|
>
|
|
{"●"}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<MessagePrimitive.Root
|
|
data-slot="aui_assistant-message-root"
|
|
data-role="assistant"
|
|
className="fade-in slide-in-from-bottom-1 animate-in relative mx-auto w-full max-w-(--thread-max-width) duration-150"
|
|
>
|
|
<div
|
|
data-slot="aui_assistant-message-content"
|
|
className="text-foreground px-2 leading-relaxed wrap-break-word"
|
|
>
|
|
<MessagePrimitive.GroupedParts
|
|
groupBy={groupPartByType({
|
|
reasoning: ["group-chainOfThought", "group-reasoning"],
|
|
"tool-call": ["group-chainOfThought", "group-tool"],
|
|
"standalone-tool-call": [],
|
|
})}
|
|
>
|
|
{({ part, children }) => {
|
|
switch (part.type) {
|
|
case "group-chainOfThought":
|
|
return <div data-slot="aui_chain-of-thought">{children}</div>;
|
|
case "group-tool":
|
|
return (
|
|
<ToolGroupRoot variant="ghost">
|
|
<ToolGroupTrigger
|
|
count={part.indices.length}
|
|
active={part.status.type === "running"}
|
|
/>
|
|
<ToolGroupContent>{children}</ToolGroupContent>
|
|
</ToolGroupRoot>
|
|
);
|
|
case "group-reasoning": {
|
|
const running = part.status.type === "running";
|
|
return (
|
|
<ReasoningRoot defaultOpen={running}>
|
|
<ReasoningTrigger active={running} />
|
|
<ReasoningContent aria-busy={running}>
|
|
<ReasoningText>{children}</ReasoningText>
|
|
</ReasoningContent>
|
|
</ReasoningRoot>
|
|
);
|
|
}
|
|
case "text":
|
|
return <MarkdownText />;
|
|
case "reasoning":
|
|
return <Reasoning {...part} />;
|
|
case "tool-call":
|
|
return part.toolUI ?? <ToolFallback {...part} />;
|
|
case "indicator":
|
|
return <AssistantWorkingIndicator />;
|
|
case "data":
|
|
return part.dataRendererUI;
|
|
default:
|
|
return null;
|
|
}
|
|
}}
|
|
</MessagePrimitive.GroupedParts>
|
|
<MessageError />
|
|
</div>
|
|
|
|
<div
|
|
data-slot="aui_assistant-message-footer"
|
|
className={cn("ml-2 flex items-center", ACTION_BAR_HEIGHT)}
|
|
>
|
|
<BranchPicker />
|
|
<AssistantActionBar />
|
|
</div>
|
|
</MessagePrimitive.Root>
|
|
);
|
|
};
|
|
|
|
const AssistantActionBar: FC = () => {
|
|
return (
|
|
<ActionBarPrimitive.Root
|
|
hideWhenRunning
|
|
autohide="not-last"
|
|
className="aui-assistant-action-bar-root text-muted-foreground animate-in fade-in col-start-3 row-start-2 -ml-1 flex gap-1 duration-200"
|
|
>
|
|
<ActionBarPrimitive.Copy asChild>
|
|
<TooltipIconButton tooltip="Copy">
|
|
<AuiIf condition={(s) => s.message.isCopied}>
|
|
<CheckIcon className="animate-in zoom-in-50 fade-in duration-200 ease-out" />
|
|
</AuiIf>
|
|
<AuiIf condition={(s) => !s.message.isCopied}>
|
|
<CopyIcon className="animate-in zoom-in-75 fade-in duration-150" />
|
|
</AuiIf>
|
|
</TooltipIconButton>
|
|
</ActionBarPrimitive.Copy>
|
|
<ActionBarPrimitive.Reload asChild>
|
|
<TooltipIconButton tooltip="Refresh">
|
|
<RefreshCwIcon />
|
|
</TooltipIconButton>
|
|
</ActionBarPrimitive.Reload>
|
|
<ActionBarMorePrimitive.Root>
|
|
<ActionBarMorePrimitive.Trigger asChild>
|
|
<TooltipIconButton
|
|
tooltip="More"
|
|
className="data-[state=open]:bg-accent"
|
|
>
|
|
<MoreHorizontalIcon />
|
|
</TooltipIconButton>
|
|
</ActionBarMorePrimitive.Trigger>
|
|
<ActionBarMorePrimitive.Content
|
|
side="bottom"
|
|
align="start"
|
|
sideOffset={6}
|
|
className="aui-action-bar-more-content bg-popover/95 text-popover-foreground data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:animate-out data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-xl border p-1.5 shadow-lg backdrop-blur-sm"
|
|
>
|
|
<ActionBarPrimitive.ExportMarkdown asChild>
|
|
<ActionBarMorePrimitive.Item className="aui-action-bar-more-item hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground flex cursor-pointer items-center gap-2 rounded-lg px-2.5 py-1.5 text-sm outline-none select-none">
|
|
<DownloadIcon className="size-4" />
|
|
Export as Markdown
|
|
</ActionBarMorePrimitive.Item>
|
|
</ActionBarPrimitive.ExportMarkdown>
|
|
</ActionBarMorePrimitive.Content>
|
|
</ActionBarMorePrimitive.Root>
|
|
<MessageTiming />
|
|
</ActionBarPrimitive.Root>
|
|
);
|
|
};
|
|
|
|
const UserMessage: FC = () => {
|
|
return (
|
|
<MessagePrimitive.Root
|
|
data-slot="aui_user-message-root"
|
|
data-role="user"
|
|
className="fade-in slide-in-from-bottom-1 animate-in mx-auto grid w-full max-w-(--thread-max-width) auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 duration-150 [&:where(>*)]:col-start-2"
|
|
>
|
|
<UserMessageAttachments />
|
|
|
|
<div className="aui-user-message-content-wrapper relative col-start-2 min-w-0">
|
|
<div className="aui-user-message-content peer bg-muted text-foreground rounded-xl px-4 py-2 wrap-break-word empty:hidden">
|
|
<MessagePrimitive.Quote>
|
|
{(quote) => <QuoteBlock {...quote} />}
|
|
</MessagePrimitive.Quote>
|
|
<MessagePrimitive.Parts components={{ Text: DirectiveText }} />
|
|
</div>
|
|
<div className="aui-user-action-bar-wrapper absolute top-1/2 left-0 -translate-x-full -translate-y-1/2 pr-2 peer-empty:hidden">
|
|
<UserActionBar />
|
|
</div>
|
|
</div>
|
|
|
|
<BranchPicker
|
|
data-slot="aui_user-branch-picker"
|
|
className="col-span-full col-start-1 row-start-3 -mr-1 justify-end"
|
|
/>
|
|
</MessagePrimitive.Root>
|
|
);
|
|
};
|
|
|
|
const UserActionBar: FC = () => {
|
|
return (
|
|
<ActionBarPrimitive.Root
|
|
hideWhenRunning
|
|
autohide="not-last"
|
|
className="aui-user-action-bar-root flex flex-col items-end"
|
|
>
|
|
<ActionBarPrimitive.Edit asChild>
|
|
<TooltipIconButton tooltip="Edit" className="aui-user-action-edit">
|
|
<PencilIcon />
|
|
</TooltipIconButton>
|
|
</ActionBarPrimitive.Edit>
|
|
</ActionBarPrimitive.Root>
|
|
);
|
|
};
|
|
|
|
const EditComposer: FC = () => {
|
|
return (
|
|
<MessagePrimitive.Root
|
|
data-slot="aui_edit-composer-wrapper"
|
|
className="mx-auto flex w-full max-w-(--thread-max-width) flex-col px-2"
|
|
>
|
|
<ComposerPrimitive.Unstable_TriggerPopoverRoot>
|
|
<ComposerPrimitive.Root className="aui-edit-composer-root border-border/60 dark:border-muted-foreground/15 ml-auto flex w-full max-w-[85%] flex-col rounded-(--composer-radius) border bg-(--composer-bg) shadow-[0_4px_16px_-8px_rgba(0,0,0,0.08),0_1px_2px_rgba(0,0,0,0.04)] dark:shadow-none">
|
|
<LexicalComposerInput
|
|
directiveChip={DirectiveChip}
|
|
autoFocus
|
|
className="aui-edit-composer-input text-foreground min-h-14 w-full resize-none bg-transparent px-4 pt-3 pb-1 text-base outline-none [&_.aui-directive-chip]:inline-flex [&_.aui-directive-chip]:items-baseline [&_.aui-directive-chip]:gap-1 [&_.aui-directive-chip]:rounded-md [&_.aui-directive-chip]:bg-blue-100 [&_.aui-directive-chip]:px-1.5 [&_.aui-directive-chip]:py-0.5 [&_.aui-directive-chip]:text-[13px] [&_.aui-directive-chip]:leading-none [&_.aui-directive-chip]:font-medium [&_.aui-directive-chip]:text-blue-700 dark:[&_.aui-directive-chip]:bg-blue-900/50 dark:[&_.aui-directive-chip]:text-blue-300 [&_.aui-directive-chip-icon]:self-center [&_.aui-lexical-input]:min-h-lh [&_.aui-lexical-input]:outline-none"
|
|
/>
|
|
<div className="aui-edit-composer-footer mx-2.5 mb-2.5 flex items-center gap-1.5 self-end">
|
|
<ComposerPrimitive.Cancel asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 rounded-full px-3.5"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</ComposerPrimitive.Cancel>
|
|
<ComposerPrimitive.Send asChild>
|
|
<Button size="sm" className="h-8 rounded-full px-3.5">
|
|
Update
|
|
</Button>
|
|
</ComposerPrimitive.Send>
|
|
</div>
|
|
</ComposerPrimitive.Root>
|
|
</ComposerPrimitive.Unstable_TriggerPopoverRoot>
|
|
</MessagePrimitive.Root>
|
|
);
|
|
};
|
|
|
|
const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
|
|
className,
|
|
...rest
|
|
}) => {
|
|
return (
|
|
<BranchPickerPrimitive.Root
|
|
hideWhenSingleBranch
|
|
className={cn(
|
|
"aui-branch-picker-root text-muted-foreground mr-2 -ml-2 inline-flex items-center text-xs",
|
|
className,
|
|
)}
|
|
{...rest}
|
|
>
|
|
<BranchPickerPrimitive.Previous asChild>
|
|
<TooltipIconButton tooltip="Previous">
|
|
<ChevronLeftIcon />
|
|
</TooltipIconButton>
|
|
</BranchPickerPrimitive.Previous>
|
|
<span className="aui-branch-picker-state font-medium">
|
|
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
|
|
</span>
|
|
<BranchPickerPrimitive.Next asChild>
|
|
<TooltipIconButton tooltip="Next">
|
|
<ChevronRightIcon />
|
|
</TooltipIconButton>
|
|
</BranchPickerPrimitive.Next>
|
|
</BranchPickerPrimitive.Root>
|
|
);
|
|
};
|
|
|
|
export const Base: FC = () => {
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
|
|
return (
|
|
<div className="bg-muted/30 flex h-full w-full">
|
|
<div className="hidden md:block">
|
|
<Sidebar collapsed={sidebarCollapsed} />
|
|
</div>
|
|
<div className="flex flex-1 flex-col overflow-hidden p-2 md:pl-0">
|
|
<div className="bg-background flex flex-1 flex-col overflow-hidden rounded-lg">
|
|
<Header
|
|
sidebarCollapsed={sidebarCollapsed}
|
|
onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
/>
|
|
<main className="flex-1 overflow-hidden">
|
|
<Thread />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|