import { ActionBarPrimitive, AssistantIf, BranchPickerPrimitive, ComposerPrimitive, ErrorPrimitive, MessagePrimitive, ThreadPrimitive, useAssistantState, } from "@assistant-ui/react"; import { ArrowDownIcon, ArrowUpIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, DownloadIcon, Loader2, PencilIcon, RefreshCwIcon, SquareIcon, } from "lucide-react"; import { useParams } from "next/navigation"; import type { FC } from "react"; import { useRef, useState } from "react"; import { ComposerAddAttachment, ComposerAttachments, UserMessageAttachments, } from "@/components/assistant-ui/attachment"; import { MarkdownText } from "@/components/assistant-ui/markdown-text"; import { ToolFallback } from "@/components/assistant-ui/tool-fallback"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { DocumentsDataTable } from "@/components/new-chat/DocumentsDataTable"; import { Button } from "@/components/ui/button"; import type { Document } from "@/contracts/types/document.types"; import { cn } from "@/lib/utils"; export const Thread: FC = () => { return ( thread.isEmpty}> ); }; const ThreadScrollToBottom: FC = () => { return ( ); }; const ThreadWelcome: FC = () => { return (

Hello there!

How can I help you today?

); }; const SUGGESTIONS = [ { title: "What's the weather", label: "in San Francisco?", prompt: "What's the weather in San Francisco?", }, { title: "Explain React hooks", label: "like useState and useEffect", prompt: "Explain React hooks like useState and useEffect", }, ] as const; const ThreadSuggestions: FC = () => { return (
{SUGGESTIONS.map((suggestion, index) => (
))}
); }; const Composer: FC = () => { // ---- State for document mentions ---- const [allSelectedDocuments, setAllSelectedDocuments] = useState([]); const [mentionedDocuments, setMentionedDocuments] = useState([]); const [showDocumentPopover, setShowDocumentPopover] = useState(false); const [inputValue, setInputValue] = useState(""); const inputRef = useRef(null); const { search_space_id } = useParams(); const handleInputOrKeyUp = ( e: React.FormEvent | React.KeyboardEvent ) => { const textarea = e.currentTarget; const value = textarea.value; setInputValue(value); // Regex: finds all [title] occurrences const mentionRegex = /\[([^\]]+)\]/g; const titlesMentioned: string[] = []; let match; while ((match = mentionRegex.exec(value)) !== null) { titlesMentioned.push(match[1]); } // Use allSelectedDocuments to filter down for current chips setMentionedDocuments( allSelectedDocuments.filter((doc) => titlesMentioned.includes(doc.title)) ); const selectionStart = textarea.selectionStart; // Only open if the last character before the caret is exactly '@' if ( selectionStart !== null && value[selectionStart - 1] === "@" && value.length === selectionStart ) { setShowDocumentPopover(true); } else { setShowDocumentPopover(false); } }; const handleDocumentsMention = (documents: Document[]) => { // Add newly selected docs to allSelectedDocuments setAllSelectedDocuments((prev) => { const toAdd = documents.filter((doc) => !prev.find((p) => p.id === doc.id)); return [...prev, ...toAdd]; }); let newValue = inputValue; documents.forEach((doc) => { const refString = `[${doc.title}]`; if (!newValue.includes(refString)) { if (newValue.trim() !== "" && !newValue.endsWith(" ")) { newValue += " "; } newValue += refString; } }); setInputValue(newValue); // Run the chip update as well right after change const mentionRegex = /\[([^\]]+)\]/g; const titlesMentioned: string[] = []; let match; while ((match = mentionRegex.exec(newValue)) !== null) { titlesMentioned.push(match[1]); } setMentionedDocuments( allSelectedDocuments.filter((doc) => titlesMentioned.includes(doc.title)) ); }; return ( {/* -------- Input field w/ refs and handlers -------- */} {/* -------- Document mention popover (simple version) -------- */} {showDocumentPopover && (
setShowDocumentPopover(false)} initialSelectedDocuments={mentionedDocuments} viewOnly={true} />
)} {/* ---- Mention chips for selected/mentioned documents ---- */} {mentionedDocuments.length > 0 && (
{mentionedDocuments.map((doc) => ( {doc.title} ))}
)}
); }; const ComposerAction: FC = () => { // Check if any attachments are still being processed (running AND progress < 100) // When progress is 100, processing is done but waiting for send() const hasProcessingAttachments = useAssistantState(({ composer }) => composer.attachments?.some((att) => { const status = att.status; if (status?.type !== "running") return false; const progress = (status as { type: "running"; progress?: number }).progress; return progress === undefined || progress < 100; }) ); return (
{/* Show processing indicator when attachments are being processed */} {hasProcessingAttachments && (
Processing...
)} !thread.isRunning}> thread.isRunning}>
); }; const MessageError: FC = () => { return ( ); }; const AssistantMessage: FC = () => { return (
); }; const AssistantActionBar: FC = () => { return ( message.isCopied}> !message.isCopied}> ); }; const UserMessage: FC = () => { return (
); }; const UserActionBar: FC = () => { return ( ); }; const EditComposer: FC = () => { return (
); }; const BranchPicker: FC = ({ className, ...rest }) => { return ( / ); };