"use client"; import { AnimatePresence, LayoutGroup, MotionConfig, motion, useReducedMotion } from "motion/react"; import { ArrowLeftIcon, ArrowUpIcon, ArrowUpRightIcon, BotIcon, CalendarDaysIcon, CheckIcon, CodeXmlIcon, CopyIcon, EllipsisIcon, FeatherIcon, FileSpreadsheetIcon, GlobeIcon, HardDriveIcon, HouseIcon, LibraryIcon, LoaderCircleIcon, LogOutIcon, type LucideIcon, MailIcon, MenuIcon, NotebookPenIcon, PanelsTopLeftIcon, PaperclipIcon, PlusIcon, ReceiptTextIcon, RotateCcwIcon, SearchIcon, SquareIcon, SettingsIcon, SparklesIcon, SquareKanbanIcon, SquarePenIcon, TelescopeIcon, ThumbsUpIcon, XIcon, } from "lucide-react"; import { type KeyboardEvent, type ReactNode, type RefObject, useEffect, useId, useMemo, useRef, useState, } from "react"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; import { Kbd } from "@/components/ui/kbd"; import { Menu, MenuItem, MenuPopup, MenuSeparator, MenuTrigger } from "@/components/ui/menu"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; export type AssistantTone = "orange" | "teal" | "violet" | "amber" | "info" | "success" | "warning"; export interface AssistantChat { id: string; title: string; time: string; projectId?: string; } export interface AssistantProject { id: string; name: string; tone: AssistantTone; } export interface AssistantApp { id: string; name: string; detail: string; icon: LucideIcon; tone: AssistantTone; connected: boolean; } export interface AssistantSuggestion { id: string; title: string; description: string; prompt: string; icon: LucideIcon; tone: AssistantTone; } export interface AssistantTool { id: string; label: string; icon: LucideIcon; } export interface AssistantModel { value: string; label: string; } export interface AssistantStartProps { productName?: string; userName?: string; userInitials?: string; plan?: string; greeting?: string; chats?: AssistantChat[]; projects?: AssistantProject[]; apps?: AssistantApp[]; suggestions?: AssistantSuggestion[]; tools?: AssistantTool[]; models?: AssistantModel[]; placeholders?: string[]; className?: string; } const tileTone: Record = { orange: "bg-orange-500/12 text-orange-700 dark:bg-orange-400/14 dark:text-orange-300", teal: "bg-teal-500/12 text-teal-700 dark:bg-teal-400/14 dark:text-teal-300", violet: "bg-violet-500/12 text-violet-700 dark:bg-violet-400/14 dark:text-violet-300", amber: "bg-amber-500/12 text-amber-700 dark:bg-amber-400/14 dark:text-amber-300", info: "bg-info/12 text-info-foreground", success: "bg-success/12 text-success-foreground", warning: "bg-warning/16 text-warning-foreground", }; const dotTone: Record = { orange: "bg-orange-500", teal: "bg-teal-500", violet: "bg-violet-500", amber: "bg-amber-500", info: "bg-info", success: "bg-success", warning: "bg-warning", }; const easeOut = [0.23, 1, 0.32, 1] as const; const spring = { type: "spring", duration: 0.3, bounce: 0 } as const; const softSpring = { type: "spring", duration: 0.4, bounce: 0 } as const; const fade = { duration: 0.2, ease: easeOut } as const; const defaultProjects: AssistantProject[] = [ { id: "launch", name: "Aurora launch", tone: "orange" }, { id: "renewals", name: "Q3 renewals", tone: "teal" }, { id: "hiring", name: "Design hiring", tone: "info" }, ]; const defaultChats: AssistantChat[] = [ { id: "c1", title: "Summarise the pricing page feedback", time: "2m", projectId: "launch" }, { id: "c2", title: "Which renewals slip past October?", time: "1h", projectId: "renewals" }, { id: "c3", title: "Draft the Aurora press note", time: "3h", projectId: "launch" }, { id: "c4", title: "Portfolio review rubric for juniors", time: "Yesterday", projectId: "hiring" }, { id: "c5", title: "Rewrite onboarding email in a warmer tone", time: "Mon" }, { id: "c6", title: "Compare churn by plan tier", time: "Sep 28", projectId: "renewals" }, ]; const defaultApps: AssistantApp[] = [ { id: "mail", name: "Postbox", detail: "1,204 threads", icon: MailIcon, tone: "info", connected: true }, { id: "cal", name: "Daybook", detail: "This week", icon: CalendarDaysIcon, tone: "orange", connected: true }, { id: "drive", name: "Vault", detail: "382 files", icon: HardDriveIcon, tone: "teal", connected: true }, { id: "tracker", name: "Linewise", detail: "64 open issues", icon: SquareKanbanIcon, tone: "amber", connected: true }, { id: "ledger", name: "Tally", detail: "Not connected", icon: ReceiptTextIcon, tone: "success", connected: false }, ]; const defaultSuggestions: AssistantSuggestion[] = [ { id: "s1", title: "Catch me up on this week", description: "Threads, meetings and decisions you missed", prompt: "Catch me up on everything that happened this week across mail and calendar, grouped by project.", icon: NotebookPenIcon, tone: "orange", }, { id: "s2", title: "Find renewals at risk", description: "Cross-check the tracker with recent emails", prompt: "Which Q3 renewals look at risk? Cross-check open issues in Linewise with the latest customer emails.", icon: TelescopeIcon, tone: "teal", }, { id: "s3", title: "Brief from a spreadsheet", description: "Upload a CSV and get a one-page summary", prompt: "Turn the attached spreadsheet into a one-page brief with the three numbers leadership should care about.", icon: FileSpreadsheetIcon, tone: "info", }, { id: "s4", title: "Prototype a landing hero", description: "Open a canvas with copy and layout ideas", prompt: "Sketch three hero sections for the Aurora launch page, each with a headline, subline and CTA.", icon: PanelsTopLeftIcon, tone: "amber", }, ]; const defaultTools: AssistantTool[] = [ { id: "web", label: "Search", icon: GlobeIcon }, { id: "research", label: "Deep research", icon: TelescopeIcon }, { id: "code", label: "Code", icon: CodeXmlIcon }, { id: "canvas", label: "Canvas", icon: PanelsTopLeftIcon }, ]; const defaultModels: AssistantModel[] = [ { value: "wren-pro", label: "Wren 3 Pro" }, { value: "wren-flash", label: "Wren 3 Flash" }, { value: "wren-reason", label: "Wren 3 Reason" }, ]; const defaultPlaceholders = [ "Ask anything, or @mention an app…", "Plan my Thursday around the launch review", "What did customers say about the new pricing?", "Draft a reply to the Halcyon contract thread", ]; interface Message { id: string; role: "user" | "assistant"; text: string; full?: string; steps?: string[]; stopped?: boolean; } function buildReply(apps: AssistantApp[], files: string[], tools: string[]): { text: string; steps: string[] } { const steps: string[] = []; for (const app of apps) steps.push(`Read ${app.name} · ${app.detail.toLowerCase()}`); if (tools.includes("web")) steps.push("Searched the web · 6 sources"); if (tools.includes("research")) steps.push("Ran deep research · 4 passes"); for (const file of files) steps.push(`Opened ${file}`); if (steps.length === 0) steps.push("Thought for 3 seconds"); const sources = [...apps.map((a) => a.name), ...files]; const from = sources.length === 0 ? "" : ` from ${sources.length === 1 ? sources[0] : `${sources.slice(0, -1).join(", ")} and ${sources.at(-1)}`}`; const text = [ `Here's a first pass${from}.`, "Three things stand out. The Halcyon and Brightwater threads both mention the October price change, so I'd treat those as one conversation. The launch review moved to Thursday, which leaves a single day to land the press note. And two open Linewise issues are blocking the same customer, which is worth a direct reply rather than a ticket update.", "Want me to draft the replies, block time on Thursday, or turn this into a checklist for the team?", ].join("\n\n"); return { text, steps }; } function OrbFilter({ id, blur }: { id: string; blur: number }) { return ( ); } function GooOrb({ className, busy = false, size = 48 }: { className?: string; busy?: boolean; size?: number }) { const id = `orb-${useId().replace(/:/g, "")}`; const reduce = useReducedMotion(); const k = size / 48; const loop = (duration: number, x: number[], y: number[]) => reduce ? undefined : { x: x.map((v) => v * k), y: y.map((v) => v * k), transition: { duration, repeat: Infinity, ease: "easeInOut" as const }, }; const blob = "absolute rounded-full"; return ( ); } function SendButton({ disabled, streaming, onSend, onStop, }: { disabled: boolean; streaming: boolean; onSend: () => void; onStop: () => void; }) { return ( ); } interface PromptBoxProps { value: string; onChange: (v: string) => void; onSubmit: () => void; streaming: boolean; onStop: () => void; tools: AssistantTool[]; activeTools: string[]; onToggleTool: (id: string) => void; context: AssistantApp[]; onRemoveContext: (id: string) => void; files: string[]; onAttach: () => void; onRemoveFile: (name: string) => void; models: AssistantModel[]; model: string; onModel: (v: string) => void; placeholders: string[]; compact?: boolean; textareaRef: RefObject; } function PromptBox({ value, onChange, onSubmit, streaming, onStop, tools, activeTools, onToggleTool, context, onRemoveContext, files, onAttach, onRemoveFile, models, model, onModel, placeholders, compact, textareaRef, }: PromptBoxProps) { const reduce = useReducedMotion(); const [ph, setPh] = useState(0); const inputId = useId(); useEffect(() => { if (reduce || value || placeholders.length < 2) return; const t = setInterval(() => setPh((p) => (p + 1) % placeholders.length), 3200); return () => clearInterval(t); }, [reduce, value, placeholders.length]); const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); if (value.trim()) onSubmit(); } }; const chips = [ ...context.map((app) => ({ key: `app-${app.id}`, label: app.name, icon: app.icon, tone: app.tone, remove: () => onRemoveContext(app.id) })), ...files.map((f) => ({ key: `file-${f}`, label: f, icon: FileSpreadsheetIcon, tone: "success" as AssistantTone, remove: () => onRemoveFile(f) })), ]; return ( {chips.length > 0 && (
{chips.map((c) => ( {c.label} ))}
)}