import { ActionBarPrimitive, AssistantIf, ErrorPrimitive, MessagePrimitive, useAssistantState, } from "@assistant-ui/react"; import { useAtom, useAtomValue, useSetAtom } 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 { addingCommentToMessageIdAtom, clearTargetCommentIdAtom, commentsCollapsedAtom, commentsEnabledAtom, targetCommentIdAtom, } from "@/atoms/chat/current-thread.atom"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { BranchPicker } from "@/components/assistant-ui/branch-picker"; 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 { CommentTrigger } from "@/components/chat-comments/comment-trigger/comment-trigger"; 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 [messageHeight, setMessageHeight] = useState(undefined); const [isSheetOpen, setIsSheetOpen] = useState(false); const messageRef = useRef(null); const messageId = useAssistantState(({ message }) => message?.id); const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const dbMessageId = parseMessageId(messageId); const commentsEnabled = useAtomValue(commentsEnabledAtom); const commentsCollapsed = useAtomValue(commentsCollapsedAtom); const [addingCommentToMessageId, setAddingCommentToMessageId] = useAtom( addingCommentToMessageIdAtom ); // Screen size detection for responsive comment UI // Mobile: < 768px (bottom sheet), Medium: 768px - 1024px (right sheet), Desktop: >= 1024px (inline panel) 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, }); // Target comment navigation - read target from global atom const targetCommentId = useAtomValue(targetCommentIdAtom); const clearTargetCommentId = useSetAtom(clearTargetCommentIdAtom); // Check if target comment belongs to this message (including replies) 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 isAddingComment = dbMessageId !== null && addingCommentToMessageId === dbMessageId; const showCommentPanel = hasComments || isAddingComment; const handleToggleAddComment = () => { if (!dbMessageId) return; setAddingCommentToMessageId(isAddingComment ? null : dbMessageId); }; const handleCommentTriggerClick = () => { setIsSheetOpen(true); }; useEffect(() => { if (!messageRef.current) return; const el = messageRef.current; const update = () => setMessageHeight(el.offsetHeight); update(); const observer = new ResizeObserver(update); observer.observe(el); return () => observer.disconnect(); }, []); // Auto-open sheet on mobile/tablet when this message has the target comment useEffect(() => { if (hasTargetComment && !isDesktop && commentsLoaded) { setIsSheetOpen(true); } }, [hasTargetComment, isDesktop, commentsLoaded]); // Scroll message into view when it contains target comment (desktop) useEffect(() => { if (hasTargetComment && isDesktop && commentsLoaded && messageRef.current) { // Small delay to ensure DOM is ready after comments render const timeoutId = setTimeout(() => { messageRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }); }, 100); return () => clearTimeout(timeoutId); } }, [hasTargetComment, isDesktop, commentsLoaded]); const showCommentTrigger = searchSpaceId && commentsEnabled && !isMessageStreaming && dbMessageId; // Determine sheet side based on screen size const sheetSide = isMediumScreen ? "right" : "bottom"; return ( {/* Desktop comment panel - only on lg screens and above, hidden when collapsed */} {searchSpaceId && commentsEnabled && !isMessageStreaming && !commentsCollapsed && (
{!hasComments && ( )} {showCommentPanel && dbMessageId && (
)}
)} {/* Mobile & Medium screen comment trigger - shown below lg breakpoint */} {showCommentTrigger && !isDesktop && (
)} {/* Comment sheet - bottom for mobile, right for medium screens */} {showCommentTrigger && !isDesktop && ( )}
); }; const AssistantActionBar: FC = () => { return ( message.isCopied}> !message.isCopied}> ); };