"use client"; import { ComposerPrimitive, useAui, useAuiState } from "@assistant-ui/react"; import { ArrowUpIcon, Globe, Paperclip, SquareIcon } from "lucide-react"; import { type FC, useCallback, useRef, useState } from "react"; import { toast } from "sonner"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useAnonymousMode } from "@/contexts/anonymous-mode"; import { useLoginGate } from "@/contexts/login-gate"; import { BACKEND_URL } from "@/lib/env-config"; import { cn } from "@/lib/utils"; const ANON_ALLOWED_EXTENSIONS = new Set([ ".md", ".markdown", ".txt", ".text", ".json", ".jsonl", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf", ".xml", ".css", ".scss", ".py", ".js", ".jsx", ".ts", ".tsx", ".java", ".kt", ".go", ".rs", ".rb", ".php", ".c", ".h", ".cpp", ".hpp", ".cs", ".swift", ".sh", ".sql", ".log", ".rst", ".tex", ".vue", ".svelte", ".astro", ".tf", ".proto", ".csv", ".tsv", ".html", ".htm", ".xhtml", ]); const ACCEPT_EXTENSIONS = Array.from(ANON_ALLOWED_EXTENSIONS).join(","); export const FreeComposer: FC = () => { const aui = useAui(); const isRunning = useAuiState(({ thread }) => thread.isRunning); const isEmpty = useAuiState(({ thread }) => thread.isEmpty); const { gate } = useLoginGate(); const anonMode = useAnonymousMode(); const [text, setText] = useState(""); const [webSearchEnabled, setWebSearchEnabled] = useState(true); const fileInputRef = useRef(null); const hasUploadedDoc = anonMode.isAnonymous && anonMode.uploadedDoc !== null; const handleTextChange = useCallback( (e: React.ChangeEvent) => { setText(e.target.value); aui.composer().setText(e.target.value); }, [aui] ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "/" && text === "") { e.preventDefault(); gate("use saved prompts"); return; } if (e.key === "@") { e.preventDefault(); gate("mention documents"); return; } if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); if (text.trim()) { aui.composer().send(); setText(""); } } }, [text, aui, gate] ); const handleUploadClick = useCallback(() => { if (hasUploadedDoc) { gate("upload more documents"); return; } fileInputRef.current?.click(); }, [hasUploadedDoc, gate]); const handleFileChange = useCallback( async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; e.target.value = ""; const ext = `.${file.name.split(".").pop()?.toLowerCase()}`; if (!ANON_ALLOWED_EXTENSIONS.has(ext)) { gate("upload PDFs, Word documents, images, and more"); return; } try { const formData = new FormData(); formData.append("file", file); const res = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/upload`, { method: "POST", credentials: "include", body: formData, }); if (res.status === 409) { gate("upload more documents"); return; } if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.detail || `Upload failed: ${res.status}`); } const data = await res.json(); if (anonMode.isAnonymous) { anonMode.setUploadedDoc({ filename: data.filename, sizeBytes: data.size_bytes, }); } toast.success(`Uploaded "${data.filename}"`); } catch (err) { console.error("Upload failed:", err); toast.error(err instanceof Error ? err.message : "Upload failed"); } }, [gate, anonMode] ); return ( {hasUploadedDoc && anonMode.isAnonymous && (
{anonMode.uploadedDoc?.filename} (1/1)
)}