import { ActionBarPrimitive, AssistantIf, BranchPickerPrimitive, ComposerPrimitive, ErrorPrimitive, MessagePrimitive, ThreadPrimitive, useAssistantState, useComposerRuntime, } from "@assistant-ui/react"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { AlertCircle, ArrowDownIcon, ArrowUpIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, DownloadIcon, FileText, Loader2, PencilIcon, RefreshCwIcon, SquareIcon, } from "lucide-react"; import { useParams } from "next/navigation"; import { type FC, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import { createPortal } from "react-dom"; import { mentionedDocumentIdsAtom, mentionedDocumentsAtom, messageDocumentsMapAtom, } from "@/atoms/chat/mentioned-documents.atom"; import { globalNewLLMConfigsAtom, llmPreferencesAtom, newLLMConfigsAtom, } from "@/atoms/new-llm-config/new-llm-config-query.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { ComposerAddAttachment, ComposerAttachments, UserMessageAttachments, } from "@/components/assistant-ui/attachment"; import { ConnectorIndicator } from "@/components/assistant-ui/connector-popup"; import { InlineMentionEditor, type InlineMentionEditorRef, } from "@/components/assistant-ui/inline-mention-editor"; 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 { DocumentMentionPicker, type DocumentMentionPickerRef, } from "@/components/new-chat/document-mention-picker"; import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking"; import { ThinkingStepsContext, ThinkingStepsDisplay, } from "@/components/assistant-ui/thinking-steps"; import { Button } from "@/components/ui/button"; import type { Document } from "@/contracts/types/document.types"; import { cn } from "@/lib/utils"; /** * Props for the Thread component */ interface ThreadProps { messageThinkingSteps?: Map; /** Optional header component to render at the top of the viewport (sticky) */ header?: React.ReactNode; } export const Thread: FC = ({ messageThinkingSteps = new Map(), header }) => { return ( {/* Optional sticky header for model selector etc. */} {header &&
{header}
} thread.isEmpty}> !thread.isEmpty}>
); }; const ThreadScrollToBottom: FC = () => { return ( ); }; const getTimeBasedGreeting = (userEmail?: string): string => { const hour = new Date().getHours(); // Extract first name from email if available const firstName = userEmail ? userEmail.split("@")[0].split(".")[0].charAt(0).toUpperCase() + userEmail.split("@")[0].split(".")[0].slice(1) : null; // Array of greeting variations for each time period const morningGreetings = ["Good morning", "Fresh start today", "Morning", "Hey there"]; const afternoonGreetings = ["Good afternoon", "Afternoon", "Hey there", "Hi there"]; const eveningGreetings = ["Good evening", "Evening", "Hey there", "Hi there"]; const nightGreetings = ["Good night", "Evening", "Hey there", "Winding down"]; const lateNightGreetings = ["Still up", "Night owl mode", "Up past bedtime", "Hi there"]; // Select a random greeting based on time let greeting: string; if (hour < 5) { // Late night: midnight to 5 AM greeting = lateNightGreetings[Math.floor(Math.random() * lateNightGreetings.length)]; } else if (hour < 12) { greeting = morningGreetings[Math.floor(Math.random() * morningGreetings.length)]; } else if (hour < 18) { greeting = afternoonGreetings[Math.floor(Math.random() * afternoonGreetings.length)]; } else if (hour < 22) { greeting = eveningGreetings[Math.floor(Math.random() * eveningGreetings.length)]; } else { // Night: 10 PM to midnight greeting = nightGreetings[Math.floor(Math.random() * nightGreetings.length)]; } // Add personalization with first name if available if (firstName) { return `${greeting}, ${firstName}!`; } return `${greeting}!`; }; const ThreadWelcome: FC = () => { const { data: user } = useAtomValue(currentUserAtom); // Memoize greeting so it doesn't change on re-renders (only on user change) const greeting = useMemo(() => getTimeBasedGreeting(user?.email), [user?.email]); return (
{/* Greeting positioned above the composer - fixed position */}

{greeting}

{/* Composer - top edge fixed, expands downward only */}
); }; const Composer: FC = () => { // ---- State for document mentions (using atoms to persist across remounts) ---- const [mentionedDocuments, setMentionedDocuments] = useAtom(mentionedDocumentsAtom); const [showDocumentPopover, setShowDocumentPopover] = useState(false); const [mentionQuery, setMentionQuery] = useState(""); const editorRef = useRef(null); const editorContainerRef = useRef(null); const documentPickerRef = useRef(null); const { search_space_id } = useParams(); const setMentionedDocumentIds = useSetAtom(mentionedDocumentIdsAtom); const composerRuntime = useComposerRuntime(); const hasAutoFocusedRef = useRef(false); // Check if thread is empty (new chat) const isThreadEmpty = useAssistantState(({ thread }) => thread.isEmpty); // Check if thread is currently running (streaming response) const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning); // Auto-focus editor when on new chat page useEffect(() => { if (isThreadEmpty && !hasAutoFocusedRef.current && editorRef.current) { // Small delay to ensure the editor is fully mounted const timeoutId = setTimeout(() => { editorRef.current?.focus(); hasAutoFocusedRef.current = true; }, 100); return () => clearTimeout(timeoutId); } }, [isThreadEmpty]); // Sync mentioned document IDs to atom for use in chat request useEffect(() => { setMentionedDocumentIds(mentionedDocuments.map((doc) => doc.id)); }, [mentionedDocuments, setMentionedDocumentIds]); // Handle text change from inline editor - sync with assistant-ui composer const handleEditorChange = useCallback( (text: string) => { composerRuntime.setText(text); }, [composerRuntime] ); // Handle @ mention trigger from inline editor const handleMentionTrigger = useCallback((query: string) => { setShowDocumentPopover(true); setMentionQuery(query); }, []); // Handle mention close const handleMentionClose = useCallback(() => { if (showDocumentPopover) { setShowDocumentPopover(false); setMentionQuery(""); } }, [showDocumentPopover]); // Handle keyboard navigation when popover is open const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (showDocumentPopover) { if (e.key === "ArrowDown") { e.preventDefault(); documentPickerRef.current?.moveDown(); return; } if (e.key === "ArrowUp") { e.preventDefault(); documentPickerRef.current?.moveUp(); return; } if (e.key === "Enter") { e.preventDefault(); documentPickerRef.current?.selectHighlighted(); return; } if (e.key === "Escape") { e.preventDefault(); setShowDocumentPopover(false); setMentionQuery(""); return; } } }, [showDocumentPopover] ); // Handle submit from inline editor (Enter key) const handleSubmit = useCallback(() => { // Prevent sending while a response is still streaming if (isThreadRunning) { return; } if (!showDocumentPopover) { composerRuntime.send(); // Clear the editor after sending editorRef.current?.clear(); setMentionedDocuments([]); setMentionedDocumentIds([]); } }, [ showDocumentPopover, isThreadRunning, composerRuntime, setMentionedDocuments, setMentionedDocumentIds, ]); // Handle document removal from inline editor const handleDocumentRemove = useCallback( (docId: number) => { setMentionedDocuments((prev) => { const updated = prev.filter((doc) => doc.id !== docId); // Immediately sync document IDs to avoid race conditions setMentionedDocumentIds(updated.map((doc) => doc.id)); return updated; }); }, [setMentionedDocuments, setMentionedDocumentIds] ); // Handle document selection from picker const handleDocumentsMention = useCallback( (documents: Document[]) => { // Insert chips into the inline editor for each new document const existingIds = new Set(mentionedDocuments.map((d) => d.id)); const newDocs = documents.filter((doc) => !existingIds.has(doc.id)); for (const doc of newDocs) { editorRef.current?.insertDocumentChip(doc); } // Update mentioned documents state setMentionedDocuments((prev) => { const existingIdSet = new Set(prev.map((d) => d.id)); const uniqueNewDocs = documents.filter((doc) => !existingIdSet.has(doc.id)); const updated = [...prev, ...uniqueNewDocs]; // Immediately sync document IDs to avoid race conditions setMentionedDocumentIds(updated.map((doc) => doc.id)); return updated; }); // Reset mention query but keep popover open for more selections setMentionQuery(""); }, [mentionedDocuments, setMentionedDocuments, setMentionedDocumentIds] ); return ( {/* -------- Inline Mention Editor -------- */}
{/* -------- Document mention popover (rendered via portal) -------- */} {showDocumentPopover && typeof document !== "undefined" && createPortal( <> {/* Backdrop */} ); }; const MessageError: FC = () => { return ( ); }; /** * Custom component to render thinking steps from Context */ const ThinkingStepsPart: FC = () => { const thinkingStepsMap = useContext(ThinkingStepsContext); // Get the current message ID to look up thinking steps const messageId = useAssistantState(({ message }) => message?.id); const thinkingSteps = thinkingStepsMap.get(messageId) || []; // Check if this specific message is currently streaming // A message is streaming if: thread is running AND this is the last assistant message const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning); const isLastMessage = useAssistantState(({ message }) => message?.isLast ?? false); const isMessageStreaming = isThreadRunning && isLastMessage; if (thinkingSteps.length === 0) return null; return (
); }; const AssistantMessageInner: FC = () => { return ( <> {/* Render thinking steps from message content - this ensures proper scroll tracking */}
); }; const AssistantMessage: FC = () => { return ( ); }; const AssistantActionBar: FC = () => { return ( message.isCopied}> !message.isCopied}> ); }; const UserMessage: FC = () => { const messageId = useAssistantState(({ message }) => message?.id); const messageDocumentsMap = useAtomValue(messageDocumentsMapAtom); const mentionedDocs = messageId ? messageDocumentsMap[messageId] : undefined; const hasAttachments = useAssistantState( ({ message }) => message?.attachments && message.attachments.length > 0 ); return (
{/* Display attachments and mentioned documents */} {(hasAttachments || (mentionedDocs && mentionedDocs.length > 0)) && (
{/* Attachments (images show as thumbnails, documents as chips) */} {/* Mentioned documents as chips */} {mentionedDocs?.map((doc) => ( {doc.title} ))}
)} {/* Message bubble with action bar positioned relative to it */}
); }; const UserActionBar: FC = () => { return ( ); }; const EditComposer: FC = () => { return (
); }; const BranchPicker: FC = ({ className, ...rest }) => { return ( / ); };