diff --git a/surfsense_web/components/assistant-ui/assistant-message.tsx b/surfsense_web/components/assistant-ui/assistant-message.tsx index df7ce3484..b2ca9d729 100644 --- a/surfsense_web/components/assistant-ui/assistant-message.tsx +++ b/surfsense_web/components/assistant-ui/assistant-message.tsx @@ -5,10 +5,14 @@ import { MessagePrimitive, useAssistantState, } from "@assistant-ui/react"; -import { useAtomValue } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { CheckIcon, CopyIcon, DownloadIcon, RefreshCwIcon } from "lucide-react"; import type { FC } from "react"; -import { useContext, useState } 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"; @@ -88,44 +92,80 @@ function parseMessageId(assistantUiMessageId: string | undefined): number | null } export const AssistantMessage: FC = () => { - const [isCommentPanelOpen, setIsCommentPanelOpen] = useState(false); + 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 commentCount = commentsData?.total_count ?? 0; + 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 ( - {/* Comment trigger and floating panel */} - {dbMessageId && searchSpaceId && ( -
- setIsCommentPanelOpen(!isCommentPanelOpen)} - /> - {isCommentPanelOpen && ( -
- setIsCommentPanelOpen(false)} - maxHeight={400} + {searchSpaceId && commentsEnabled && !isMessageStreaming && ( +
+
+ {!hasComments && ( + -
- )} + )} + + {showCommentPanel && dbMessageId && ( +
+ +
+ )} +
)} diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 51f9c0d33..5e0f33e85 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -26,6 +26,7 @@ import { import { useParams } from "next/navigation"; import { type FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; +import { showCommentsGutterAtom } from "@/atoms/chat/current-thread.atom"; import { mentionedDocumentIdsAtom, mentionedDocumentsAtom, @@ -60,57 +61,63 @@ import { Button } from "@/components/ui/button"; import type { Document } from "@/contracts/types/document.types"; import { cn } from "@/lib/utils"; -/** - * Props for the Thread component - */ interface ThreadProps { messageThinkingSteps?: Map; - /** Optional header component to render at the top of the viewport (sticky) */ header?: React.ReactNode; } export const Thread: FC = ({ messageThinkingSteps = new Map(), header }) => { return ( - - - {/* Optional sticky header for model selector etc. */} - {header &&
{header}
} - - thread.isEmpty}> - - - - - - - - !thread.isEmpty}> -
- -
-
-
-
-
+
); }; +const ThreadContent: FC<{ header?: React.ReactNode }> = ({ header }) => { + const showGutter = useAtomValue(showCommentsGutterAtom); + + return ( + + + {header &&
{header}
} + + thread.isEmpty}> + + + + + + + + !thread.isEmpty}> +
+ +
+
+
+
+
+ ); +}; + const ThreadScrollToBottom: FC = () => { return ( diff --git a/surfsense_web/lib/chat/thread-persistence.ts b/surfsense_web/lib/chat/thread-persistence.ts index 23dd35800..738d1062f 100644 --- a/surfsense_web/lib/chat/thread-persistence.ts +++ b/surfsense_web/lib/chat/thread-persistence.ts @@ -23,6 +23,7 @@ export interface ThreadRecord { search_space_id: number; created_at: string; updated_at: string; + has_comments?: boolean; } export interface MessageRecord {