"use client"; import { ArrowUpIcon, CalendarIcon, ChevronDownIcon, FileTextIcon, FolderIcon, ImageIcon, LinkIcon, type LucideIcon, MailIcon, MicIcon, PlusIcon, ShieldCheckIcon, ShieldIcon, SquareKanbanIcon, XIcon, } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type KeyboardEvent, useEffect, useId, useLayoutEffect, useRef, useState } from "react"; import { Menu, MenuGroup, MenuGroupLabel, MenuItem, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger, } from "@/components/ui/menu"; import { cn } from "@/lib/utils"; export interface PromptBarModel { id: string; name: string; hint?: string; } export interface PromptBarApp { id: string; name: string; icon: LucideIcon; tint?: string; } export interface PromptBarAttachment { id: string; name: string; kind: "file" | "image" | "link"; } export interface PromptBarSendPayload { text: string; model: string; approval: boolean; apps: string[]; attachments: PromptBarAttachment[]; } export interface PromptBarProps { placeholder?: string; models?: PromptBarModel[]; defaultModel?: string; apps?: PromptBarApp[]; defaultApps?: string[]; defaultApproval?: boolean; transcript?: string; onSend?: (payload: PromptBarSendPayload) => void; className?: string; } const defaultModels: PromptBarModel[] = [ { id: "lumen-3", name: "Lumen 3", hint: "Balanced" }, { id: "lumen-3-swift", name: "Lumen 3 Swift", hint: "Fastest" }, { id: "atlas-pro", name: "Atlas Pro", hint: "Deep reasoning" }, ]; const defaultApps: PromptBarApp[] = [ { id: "calendar", name: "Calendar", icon: CalendarIcon, tint: "text-sky-600 dark:text-sky-400" }, { id: "mail", name: "Mail", icon: MailIcon, tint: "text-rose-600 dark:text-rose-400" }, { id: "files", name: "Files", icon: FolderIcon, tint: "text-amber-600 dark:text-amber-400" }, { id: "tracker", name: "Tracker", icon: SquareKanbanIcon, tint: "text-violet-600 dark:text-violet-400" }, ]; const fakeFiles: Record = { file: ["q3-brief.pdf", "offsite-notes.md", "budget-v2.xlsx"], image: ["moodboard.png", "whiteboard.jpg"], link: ["pinecrest.io/pricing", "docs.halyard.dev"], }; const kindIcon = { file: FileTextIcon, image: ImageIcon, link: LinkIcon } as const; const spring = { type: "spring", stiffness: 480, damping: 36 } as const; const easeOut = [0.23, 1, 0.32, 1] as const; const press = "transition-[color,background-color,border-color,scale] duration-150 ease-out active:scale-[0.97]"; const menuMotion = "transition-[scale,opacity] duration-150 ease-out data-ending-style:scale-95 data-starting-style:scale-95 data-ending-style:opacity-0 data-starting-style:opacity-0"; export function PromptBar({ placeholder = "Ask anything or attach a file…", models = defaultModels, defaultModel, apps = defaultApps, defaultApps: initialApps = ["calendar", "mail"], defaultApproval = true, transcript = "Find a 30 minute slot with Priya next week and draft the invite.", onSend, className, }: PromptBarProps) { const reduce = useReducedMotion(); const inputId = useId(); const textareaRef = useRef(null); const [text, setText] = useState(""); const [model, setModel] = useState(defaultModel ?? models[0]?.id ?? ""); const [approval, setApproval] = useState(defaultApproval); const [active, setActive] = useState(initialApps); const [attachments, setAttachments] = useState([]); const [recording, setRecording] = useState(false); const [sent, setSent] = useState(0); const counter = useRef(0); const canSend = text.trim().length > 0 && !recording; const current = models.find((m) => m.id === model); useLayoutEffect(() => { const el = textareaRef.current; if (!el) return; el.style.height = "0px"; el.style.height = `${Math.min(el.scrollHeight, 168)}px`; }, [text]); const attach = (kind: PromptBarAttachment["kind"]) => { const pool = fakeFiles[kind]; const name = pool[counter.current % pool.length] ?? kind; counter.current += 1; setAttachments((a) => [...a, { id: `${kind}-${counter.current}`, name, kind }]); }; const send = () => { if (!canSend) return; onSend?.({ text: text.trim(), model, approval, apps: active, attachments }); setText(""); setAttachments([]); setSent((s) => s + 1); }; const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); send(); } }; const stopRecording = (keep: boolean) => { setRecording(false); if (keep) { setText((t) => (t ? `${t.trimEnd()} ${transcript}` : transcript)); requestAnimationFrame(() => textareaRef.current?.focus()); } }; const toolBtn = cn( "inline-flex h-8 shrink-0 items-center gap-1.5 rounded-full px-2.5 text-muted-foreground text-xs outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring data-popup-open:bg-accent data-popup-open:text-foreground", press, ); return (
{attachments.length > 0 && ( {attachments.map((a) => { const Icon = kindIcon[a.kind]; return ( {a.name} ); })} )}