import {
ActionBarPrimitive,
AssistantIf,
ErrorPrimitive,
MessagePrimitive,
useAssistantState,
} from "@assistant-ui/react";
import { useAtom, useAtomValue } from "jotai";
import { CheckIcon, CopyIcon, DownloadIcon, RefreshCwIcon } from "lucide-react";
import type { FC } from "react";
import { useContext, useEffect, useRef, useState } from "react";
import {
addingCommentToMessageIdAtom,
commentsEnabledAtom,
} 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 { CommentTrigger } from "@/components/chat-comments/comment-trigger/comment-trigger";
import { useComments } from "@/hooks/use-comments";
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 messageRef = useRef(null);
const messageId = useAssistantState(({ message }) => message?.id);
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
const dbMessageId = parseMessageId(messageId);
const commentsEnabled = useAtomValue(commentsEnabledAtom);
const [addingCommentToMessageId, setAddingCommentToMessageId] = useAtom(
addingCommentToMessageIdAtom
);
const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning);
const isLastMessage = useAssistantState(({ message }) => message?.isLast ?? false);
const isMessageStreaming = isThreadRunning && isLastMessage;
const { data: commentsData } = useComments({
messageId: dbMessageId ?? 0,
enabled: !!dbMessageId,
});
const hasComments = (commentsData?.total_count ?? 0) > 0;
const isAddingComment = dbMessageId !== null && addingCommentToMessageId === dbMessageId;
const showCommentPanel = hasComments || isAddingComment;
const handleToggleAddComment = () => {
if (!dbMessageId) return;
setAddingCommentToMessageId(isAddingComment ? null : dbMessageId);
};
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();
}, []);
return (
{searchSpaceId && commentsEnabled && !isMessageStreaming && (
{!hasComments && (
)}
{showCommentPanel && dbMessageId && (
)}
)}
);
};
const AssistantActionBar: FC = () => {
return (
message.isCopied}>
!message.isCopied}>
);
};