import { ActionBarPrimitive, AssistantIf, BranchPickerPrimitive, ComposerPrimitive, ErrorPrimitive, MessagePrimitive, ThreadPrimitive, useAssistantState, useMessage, } from "@assistant-ui/react"; import { ArrowDownIcon, ArrowUpIcon, Brain, CheckCircle2, CheckIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, DownloadIcon, Loader2, PencilIcon, Plug2, Plus, RefreshCwIcon, Search, Sparkles, SquareIcon, } from "lucide-react"; import { useParams } from "next/navigation"; import Link from "next/link"; import { type FC, useState, useRef, useCallback, useEffect } from "react"; import { useAtomValue } from "jotai"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; 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 { ChainOfThought, ChainOfThoughtContent, ChainOfThoughtItem, ChainOfThoughtStep, ChainOfThoughtTrigger, } from "@/components/prompt-kit/chain-of-thought"; 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"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking"; /** * Props for the Thread component */ interface ThreadProps { messageThinkingSteps?: Map; } // Context to pass thinking steps to AssistantMessage import { createContext, useContext } from "react"; const ThinkingStepsContext = createContext>(new Map()); /** * Get icon based on step status and title */ function getStepIcon(status: "pending" | "in_progress" | "completed", title: string) { const titleLower = title.toLowerCase(); if (status === "in_progress") { return ; } if (status === "completed") { return ; } if (titleLower.includes("search") || titleLower.includes("knowledge")) { return ; } if (titleLower.includes("analy") || titleLower.includes("understand")) { return ; } return ; } /** * Chain of thought display component with smart expand/collapse behavior */ const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: boolean }> = ({ steps, isThreadRunning = true }) => { // Track which steps the user has manually toggled (overrides auto behavior) const [manualOverrides, setManualOverrides] = useState>({}); // Track previous step statuses to detect changes const prevStatusesRef = useRef>({}); // Derive effective status: if thread stopped and step is in_progress, treat as completed const getEffectiveStatus = (step: ThinkingStep): "pending" | "in_progress" | "completed" => { if (step.status === "in_progress" && !isThreadRunning) { return "completed"; // Thread was stopped, so mark as completed } return step.status; }; // Check if any step is effectively in progress const hasInProgressStep = steps.some(step => getEffectiveStatus(step) === "in_progress"); // Find the last completed step index (using effective status) const lastCompletedIndex = steps .map((s, i) => getEffectiveStatus(s) === "completed" ? i : -1) .filter(i => i !== -1) .pop(); // Clear manual overrides when a step's status changes useEffect(() => { const currentStatuses: Record = {}; steps.forEach(step => { currentStatuses[step.id] = step.status; // If status changed, clear any manual override for this step if (prevStatusesRef.current[step.id] && prevStatusesRef.current[step.id] !== step.status) { setManualOverrides(prev => { const next = { ...prev }; delete next[step.id]; return next; }); } }); prevStatusesRef.current = currentStatuses; }, [steps]); if (steps.length === 0) return null; const getStepOpenState = (step: ThinkingStep, index: number): boolean => { const effectiveStatus = getEffectiveStatus(step); // If user has manually toggled, respect that if (manualOverrides[step.id] !== undefined) { return manualOverrides[step.id]; } // Auto behavior: open if in progress if (effectiveStatus === "in_progress") { return true; } // Auto behavior: keep last completed step open if no in-progress step if (!hasInProgressStep && index === lastCompletedIndex) { return true; } // Default: collapsed return false; }; const handleToggle = (stepId: string, currentOpen: boolean) => { setManualOverrides(prev => ({ ...prev, [stepId]: !currentOpen, })); }; return (
{steps.map((step, index) => { const effectiveStatus = getEffectiveStatus(step); const icon = getStepIcon(effectiveStatus, step.title); const isOpen = getStepOpenState(step, index); return ( handleToggle(step.id, isOpen)} > {step.title} {step.items && step.items.length > 0 && ( {step.items.map((item, idx) => ( {item} ))} )} ); })}
); }; export const Thread: FC = ({ messageThinkingSteps = new Map() }) => { return ( 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", "Rise and shine", "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", "The night is young", "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); return (
{/* Greeting positioned above the composer - fixed position */}

{getTimeBasedGreeting(user?.email)}

{/* Composer - top edge fixed, expands downward only */}
); }; 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 ConnectorIndicator: FC = () => { const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const { connectors, isLoading: connectorsLoading } = useSearchSourceConnectors(false, searchSpaceId ? Number(searchSpaceId) : undefined); const { data: documentTypeCounts, isLoading: documentTypesLoading } = useAtomValue(documentTypeCountsAtom); const [isOpen, setIsOpen] = useState(false); const closeTimeoutRef = useRef(null); const isLoading = connectorsLoading || documentTypesLoading; // Get document types that have documents in the search space const activeDocumentTypes = documentTypeCounts ? Object.entries(documentTypeCounts).filter(([_, count]) => count > 0) : []; const hasConnectors = connectors.length > 0; const hasSources = hasConnectors || activeDocumentTypes.length > 0; const totalSourceCount = connectors.length + activeDocumentTypes.length; const handleMouseEnter = useCallback(() => { // Clear any pending close timeout if (closeTimeoutRef.current) { clearTimeout(closeTimeoutRef.current); closeTimeoutRef.current = null; } setIsOpen(true); }, []); const handleMouseLeave = useCallback(() => { // Delay closing by 150ms for better UX closeTimeoutRef.current = setTimeout(() => { setIsOpen(false); }, 150); }, []); if (!searchSpaceId) return null; return ( {hasSources ? (

Connected Sources

{totalSourceCount}
{/* Document types from the search space */} {activeDocumentTypes.map(([docType, count]) => (
{getConnectorIcon(docType, "size-3.5")} {getDocumentTypeLabel(docType)}
))} {/* Search source connectors */} {connectors.map((connector) => (
{getConnectorIcon(connector.connector_type, "size-3.5")} {connector.name}
))}
Manage connectors
) : (

No sources yet

Add documents or connect data sources to enhance search results.

Add Connector
)}
); }; 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; }) ); // Check if composer text is empty const isComposerEmpty = useAssistantState(({ composer }) => { const text = composer.text?.trim() || ""; return text.length === 0; }); const isSendDisabled = hasProcessingAttachments || isComposerEmpty; return (
{/* Show processing indicator when attachments are being processed */} {hasProcessingAttachments && (
Processing...
)} !thread.isRunning}> thread.isRunning}>
); }; const MessageError: FC = () => { return ( ); }; const AssistantMessageInner: FC = () => { const thinkingStepsMap = useContext(ThinkingStepsContext); // Get the current message ID to look up thinking steps const messageId = useMessage((m) => m.id); const thinkingSteps = thinkingStepsMap.get(messageId) || []; // Check if thread is still running (for stopping the spinner when cancelled) const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning); return ( <> {/* Show thinking steps BEFORE the text response */} {thinkingSteps.length > 0 && (
)}
); }; 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 ( / ); };