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 && (
)}
);
};