"use client"; import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport, type UIMessage } from "ai"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react"; import { ChatMessage } from "./chat-message"; import { FilePickerModal, type SelectedFile, } from "./file-picker-modal"; import { ChatEditor, type ChatEditorHandle } from "./tiptap/chat-editor"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "./ui/dropdown-menu"; import { UnicodeSpinner } from "./unicode-spinner"; // ── Attachment types & helpers ── type AttachedFile = { id: string; name: string; path: string; /** True while the file is still uploading to the server. */ uploading?: boolean; /** Local blob URL for instant preview before upload completes. */ localUrl?: string; }; function getFileCategory( name: string, ): "image" | "video" | "audio" | "pdf" | "code" | "document" | "other" { const ext = name.split(".").pop()?.toLowerCase() ?? ""; if ( [ "jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico", "tiff", "heic", ].includes(ext) ) {return "image";} if (["mp4", "webm", "mov", "avi", "mkv", "flv"].includes(ext)) {return "video";} if (["mp3", "wav", "ogg", "aac", "flac", "m4a"].includes(ext)) {return "audio";} if (ext === "pdf") {return "pdf";} if ( [ "js", "ts", "tsx", "jsx", "py", "rb", "go", "rs", "java", "cpp", "c", "h", "css", "html", "json", "yaml", "yml", "toml", "md", "sh", "bash", "sql", "swift", "kt", ].includes(ext) ) {return "code";} if ( [ "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "rtf", "csv", "pages", "numbers", "key", ].includes(ext) ) {return "document";} return "other"; } function shortenPath(path: string): string { return path .replace(/^\/Users\/[^/]+/, "~") .replace(/^\/home\/[^/]+/, "~") .replace(/^[A-Z]:\\Users\\[^\\]+/, "~"); } const categoryMeta: Record = { image: { bg: "rgba(16, 185, 129, 0.12)", fg: "#10b981" }, video: { bg: "rgba(139, 92, 246, 0.12)", fg: "#8b5cf6" }, audio: { bg: "rgba(245, 158, 11, 0.12)", fg: "#f59e0b" }, pdf: { bg: "rgba(239, 68, 68, 0.12)", fg: "#ef4444" }, code: { bg: "rgba(59, 130, 246, 0.12)", fg: "#3b82f6" }, document: { bg: "rgba(107, 114, 128, 0.12)", fg: "#6b7280" }, other: { bg: "rgba(107, 114, 128, 0.08)", fg: "#9ca3af" }, }; function FileTypeIcon({ category }: { category: string }) { const props = { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" as const, strokeLinejoin: "round" as const, }; switch (category) { case "image": return ( ); case "video": return ( ); case "audio": return ( ); case "pdf": return ( ); case "code": return ( ); case "document": return ( ); default: return ( ); } } function QueueItem({ msg, idx, onEdit, onSendNow, onRemove, }: { msg: QueuedMessage; idx: number; onEdit: (id: string, text: string) => void; onSendNow: (id: string) => void; onRemove: (id: string) => void; }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(msg.text); const inputRef = useRef(null); const autoResize = () => { const el = inputRef.current; if (!el) {return;} el.style.height = "auto"; el.style.height = `${el.scrollHeight}px`; }; useEffect(() => { if (editing) { inputRef.current?.focus(); const len = inputRef.current?.value.length ?? 0; inputRef.current?.setSelectionRange(len, len); autoResize(); } }, [editing]); const commitEdit = () => { const trimmed = draft.trim(); if (trimmed && trimmed !== msg.text) { onEdit(msg.id, trimmed); } else { setDraft(msg.text); } setEditing(false); }; return (
0 ? "border-t" : ""}`} style={idx > 0 ? { borderColor: "var(--color-border)" } : undefined} > {idx + 1} {editing ? (