import { ActionBarPrimitive, AssistantIf, ErrorPrimitive, MessagePrimitive, useAssistantState, useMessage, } from "@assistant-ui/react"; import { useAtomValue } from "jotai"; import { CheckIcon, CopyIcon, DownloadIcon, MessageSquare, RefreshCwIcon } from "lucide-react"; import type { FC } from "react"; import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { commentsEnabledAtom, targetCommentIdAtom } from "@/atoms/chat/current-thread.atom"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { MarkdownText } from "@/components/assistant-ui/markdown-text"; import { ThinkingStepsContext, ThinkingStepsDisplay, } from "@/components/assistant-ui/thinking-steps"; import { ToolFallback } from "@/components/assistant-ui/tool-fallback"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { CommentPanelContainer } from "@/components/chat-comments/comment-panel-container/comment-panel-container"; import { CommentSheet } from "@/components/chat-comments/comment-sheet/comment-sheet"; import { useComments } from "@/hooks/use-comments"; import { useMediaQuery } from "@/hooks/use-media-query"; import { cn } from "@/lib/utils"; export 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 */}
); }; function parseMessageId(assistantUiMessageId: string | undefined): number | null { if (!assistantUiMessageId) return null; const match = assistantUiMessageId.match(/^msg-(\d+)$/); return match ? Number.parseInt(match[1], 10) : null; } export const AssistantMessage: FC = () => { const [isSheetOpen, setIsSheetOpen] = useState(false); const [isInlineOpen, setIsInlineOpen] = useState(false); const messageRef = useRef(null); const commentPanelRef = useRef(null); const commentTriggerRef = useRef(null); const messageId = useAssistantState(({ message }) => message?.id); const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const dbMessageId = parseMessageId(messageId); const commentsEnabled = useAtomValue(commentsEnabledAtom); // Desktop: >= 1024px (inline expandable), Medium: 768px-1023px (right sheet), Mobile: <768px (bottom sheet) const isMediumScreen = useMediaQuery("(min-width: 768px) and (max-width: 1023px)"); const isDesktop = useMediaQuery("(min-width: 1024px)"); const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning); const isLastMessage = useAssistantState(({ message }) => message?.isLast ?? false); const isMessageStreaming = isThreadRunning && isLastMessage; const { data: commentsData, isSuccess: commentsLoaded } = useComments({ messageId: dbMessageId ?? 0, enabled: !!dbMessageId, }); const targetCommentId = useAtomValue(targetCommentIdAtom); const hasTargetComment = useMemo(() => { if (!targetCommentId || !commentsData?.comments) return false; return commentsData.comments.some( (c) => c.id === targetCommentId || c.replies?.some((r) => r.id === targetCommentId) ); }, [targetCommentId, commentsData]); const commentCount = commentsData?.total_count ?? 0; const hasComments = commentCount > 0; const showCommentTrigger = searchSpaceId && commentsEnabled && !isMessageStreaming && dbMessageId; // Close floating panel when clicking outside (but not on portaled popover/dropdown content) useEffect(() => { if (!isInlineOpen) return; const handleClickOutside = (e: MouseEvent) => { const target = e.target as Element; if ( commentPanelRef.current?.contains(target) || commentTriggerRef.current?.contains(target) || target.closest?.("[data-radix-popper-content-wrapper]") ) return; setIsInlineOpen(false); }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isInlineOpen]); // Auto-open floating panel on desktop when this message has the target comment useEffect(() => { if (hasTargetComment && isDesktop && commentsLoaded) { setIsInlineOpen(true); const timeoutId = setTimeout(() => { messageRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }); }, 100); return () => clearTimeout(timeoutId); } }, [hasTargetComment, isDesktop, commentsLoaded]); // Auto-open sheet on mobile/tablet when this message has the target comment useEffect(() => { if (hasTargetComment && !isDesktop && commentsLoaded) { setIsSheetOpen(true); } }, [hasTargetComment, isDesktop, commentsLoaded]); const sheetSide = isMediumScreen ? "right" : "bottom"; return ( {/* Comment trigger — right-aligned, just below user query on all screen sizes */} {showCommentTrigger && (
)} {/* Desktop floating comment panel — overlays on top of chat content */} {showCommentTrigger && isDesktop && isInlineOpen && dbMessageId && (
)} {/* Comment sheet — bottom for mobile, right for medium screens */} {showCommentTrigger && !isDesktop && ( )}
); }; const AssistantActionBar: FC = () => { const { isLast } = useMessage(); return ( message.isCopied}> !message.isCopied}> {/* Only allow regenerating the last assistant message */} {isLast && ( )} ); };