diff --git a/surfsense_web/app/dashboard/[search_space_id]/researcher/[chat_id]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/researcher/[chat_id]/page.tsx index b103025..d371e9e 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/researcher/[chat_id]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/researcher/[chat_id]/page.tsx @@ -240,7 +240,7 @@ const SourcesDialogContent = ({ const ChatPage = () => { const [token, setToken] = React.useState(null); const [activeTab, setActiveTab] = useState(""); - const [dialogOpen, setDialogOpen] = useState(false); + const [dialogOpenId, setDialogOpenId] = useState(null); const [sourcesPage, setSourcesPage] = useState(1); const [expandedSources, setExpandedSources] = useState(false); const [canScrollLeft, setCanScrollLeft] = useState(false); @@ -260,6 +260,13 @@ const ChatPage = () => { const { search_space_id, chat_id } = useParams(); + // Function to scroll terminal to bottom + const scrollTerminalToBottom = () => { + if (terminalMessagesRef.current) { + terminalMessagesRef.current.scrollTop = terminalMessagesRef.current.scrollHeight; + } + }; + // Get token from localStorage on client side only React.useEffect(() => { setToken(localStorage.getItem('surfsense_bearer_token')); @@ -469,54 +476,60 @@ const ChatPage = () => { updateChat(); }, [messages, status, chat_id, researchMode, selectedConnectors, search_space_id]); - // Log messages whenever they update and extract annotations from the latest assistant message if available - useEffect(() => { - console.log('Messages updated:', messages); - - // Extract annotations from the latest assistant message if available + // Memoize connector sources to prevent excessive re-renders + const processedConnectorSources = React.useMemo(() => { + if (messages.length === 0) return connectorSources; + + // Only process when we have a complete message (not streaming) + if (status !== 'ready') return connectorSources; + + // Find the latest assistant message const assistantMessages = messages.filter(msg => msg.role === 'assistant'); - if (assistantMessages.length > 0) { - const latestAssistantMessage = assistantMessages[assistantMessages.length - 1]; - if (latestAssistantMessage?.annotations) { - const annotations = latestAssistantMessage.annotations as any[]; - - // Debug log to track streaming annotations - if (process.env.NODE_ENV === 'development') { - console.log('Streaming annotations:', annotations); - - // Log counts of each annotation type - const terminalInfoCount = annotations.filter(a => a.type === 'TERMINAL_INFO').length; - const sourcesCount = annotations.filter(a => a.type === 'SOURCES').length; - const answerCount = annotations.filter(a => a.type === 'ANSWER').length; - - console.log(`Annotation counts - Terminal: ${terminalInfoCount}, Sources: ${sourcesCount}, Answer: ${answerCount}`); - } - - // Process SOURCES annotation - get the last one to ensure we have the latest - const sourcesAnnotations = annotations.filter( - (annotation) => annotation.type === 'SOURCES' - ); - - if (sourcesAnnotations.length > 0) { - // Get the last SOURCES annotation to ensure we have the most recent one - const latestSourcesAnnotation = sourcesAnnotations[sourcesAnnotations.length - 1]; - if (latestSourcesAnnotation.content) { - setConnectorSources(latestSourcesAnnotation.content); - } - } - - // Check for terminal info annotations and scroll terminal to bottom if they exist - const terminalInfoAnnotations = annotations.filter( - (annotation) => annotation.type === 'TERMINAL_INFO' - ); - - if (terminalInfoAnnotations.length > 0) { - // Schedule scrolling after the DOM has been updated - setTimeout(scrollTerminalToBottom, 100); - } - } + if (assistantMessages.length === 0) return connectorSources; + + const latestAssistantMessage = assistantMessages[assistantMessages.length - 1]; + if (!latestAssistantMessage?.annotations) return connectorSources; + + // Find the latest SOURCES annotation + const annotations = latestAssistantMessage.annotations as any[]; + const sourcesAnnotations = annotations.filter(a => a.type === 'SOURCES'); + + if (sourcesAnnotations.length === 0) return connectorSources; + + const latestSourcesAnnotation = sourcesAnnotations[sourcesAnnotations.length - 1]; + if (!latestSourcesAnnotation.content) return connectorSources; + + // Use this content if it differs from current + return latestSourcesAnnotation.content; + }, [messages, status, connectorSources]); + + // Update connector sources when processed value changes + useEffect(() => { + if (processedConnectorSources !== connectorSources) { + setConnectorSources(processedConnectorSources); } - }, [messages]); + }, [processedConnectorSources, connectorSources]); + + // Check and scroll terminal when terminal info is available + useEffect(() => { + if (messages.length === 0 || status !== 'ready') return; + + // Find the latest assistant message + const assistantMessages = messages.filter(msg => msg.role === 'assistant'); + if (assistantMessages.length === 0) return; + + const latestAssistantMessage = assistantMessages[assistantMessages.length - 1]; + if (!latestAssistantMessage?.annotations) return; + + // Check for terminal info annotations + const annotations = latestAssistantMessage.annotations as any[]; + const terminalInfoAnnotations = annotations.filter(a => a.type === 'TERMINAL_INFO'); + + if (terminalInfoAnnotations.length > 0) { + // Schedule scrolling after the DOM has been updated + setTimeout(scrollTerminalToBottom, 100); + } + }, [messages, status]); // Custom handleSubmit function to include selected connectors and answer type const handleSubmit = (e: React.FormEvent) => { @@ -543,24 +556,22 @@ const ChatPage = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; - // Function to scroll terminal to bottom - const scrollTerminalToBottom = () => { - if (terminalMessagesRef.current) { - terminalMessagesRef.current.scrollTop = terminalMessagesRef.current.scrollHeight; - } - }; - // Scroll to bottom when messages change useEffect(() => { scrollToBottom(); }, [messages]); - // Set activeTab when connectorSources change - useEffect(() => { - if (connectorSources.length > 0) { - setActiveTab(connectorSources[0].type); - } + // Set activeTab when connectorSources change using a memoized value + const activeTabValue = React.useMemo(() => { + return connectorSources.length > 0 ? connectorSources[0].type : ""; }, [connectorSources]); + + // Update activeTab when the memoized value changes + useEffect(() => { + if (activeTabValue && activeTabValue !== activeTab) { + setActiveTab(activeTabValue); + } + }, [activeTabValue, activeTab]); // Scroll terminal to bottom when expanded useEffect(() => { @@ -617,7 +628,7 @@ const ChatPage = () => { }; // Function to get a citation source by ID - const getCitationSource = (citationId: number, messageIndex?: number): Source | null => { + const getCitationSource = React.useCallback((citationId: number, messageIndex?: number): Source | null => { if (!messages || messages.length === 0) return null; // If no specific message index is provided, use the latest assistant message @@ -699,7 +710,7 @@ const ChatPage = () => { return foundSource || null; } - }; + }, [messages]); return ( <> @@ -900,7 +911,7 @@ const ChatPage = () => { ))} {connector.sources.length > INITIAL_SOURCES_DISPLAY && ( - setDialogOpen(open)}> + setDialogOpenId(open ? connector.id : null)}> - - - - + + + )} ); -}; +}); + +Citation.displayName = 'Citation'; /** * Function to render text with citations diff --git a/surfsense_web/components/markdown-viewer.tsx b/surfsense_web/components/markdown-viewer.tsx index 3cd7543..1df6501 100644 --- a/surfsense_web/components/markdown-viewer.tsx +++ b/surfsense_web/components/markdown-viewer.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; import rehypeSanitize from "rehype-sanitize"; @@ -14,82 +14,87 @@ interface MarkdownViewerProps { } export function MarkdownViewer({ content, className, getCitationSource }: MarkdownViewerProps) { + // Memoize the markdown components to prevent unnecessary re-renders + const components = useMemo(() => { + return { + // Define custom components for markdown elements + p: ({node, children, ...props}: any) => { + // If there's no getCitationSource function, just render normally + if (!getCitationSource) { + return

{children}

; + } + + // Process citations within paragraph content + return

{processCitationsInReactChildren(children, getCitationSource)}

; + }, + a: ({node, children, ...props}: any) => { + // Process citations within link content if needed + const processedChildren = getCitationSource + ? processCitationsInReactChildren(children, getCitationSource) + : children; + return {processedChildren}; + }, + li: ({node, children, ...props}: any) => { + // Process citations within list item content + const processedChildren = getCitationSource + ? processCitationsInReactChildren(children, getCitationSource) + : children; + return
  • {processedChildren}
  • ; + }, + ul: ({node, ...props}: any) =>
      , + ol: ({node, ...props}: any) =>
        , + h1: ({node, children, ...props}: any) => { + const processedChildren = getCitationSource + ? processCitationsInReactChildren(children, getCitationSource) + : children; + return

        {processedChildren}

        ; + }, + h2: ({node, children, ...props}: any) => { + const processedChildren = getCitationSource + ? processCitationsInReactChildren(children, getCitationSource) + : children; + return

        {processedChildren}

        ; + }, + h3: ({node, children, ...props}: any) => { + const processedChildren = getCitationSource + ? processCitationsInReactChildren(children, getCitationSource) + : children; + return

        {processedChildren}

        ; + }, + h4: ({node, children, ...props}: any) => { + const processedChildren = getCitationSource + ? processCitationsInReactChildren(children, getCitationSource) + : children; + return

        {processedChildren}

        ; + }, + blockquote: ({node, ...props}: any) =>
        , + hr: ({node, ...props}: any) =>
        , + img: ({node, ...props}: any) => , + table: ({node, ...props}: any) =>
        , + th: ({node, ...props}: any) =>
        , + td: ({node, ...props}: any) => , + code: ({node, className, children, ...props}: any) => { + const match = /language-(\w+)/.exec(className || ''); + const isInline = !match; + return isInline + ? {children} + : ( +
        +
        +                {children}
        +              
        +
        + ); + } + }; + }, [getCitationSource]); + return (
        { - // If there's no getCitationSource function, just render normally - if (!getCitationSource) { - return

        {children}

        ; - } - - // Process citations within paragraph content - return

        {processCitationsInReactChildren(children, getCitationSource)}

        ; - }, - a: ({node, children, ...props}) => { - // Process citations within link content if needed - const processedChildren = getCitationSource - ? processCitationsInReactChildren(children, getCitationSource) - : children; - return {processedChildren}; - }, - li: ({node, children, ...props}) => { - // Process citations within list item content - const processedChildren = getCitationSource - ? processCitationsInReactChildren(children, getCitationSource) - : children; - return
      1. {processedChildren}
      2. ; - }, - ul: ({node, ...props}) =>
          , - ol: ({node, ...props}) =>
            , - h1: ({node, children, ...props}) => { - const processedChildren = getCitationSource - ? processCitationsInReactChildren(children, getCitationSource) - : children; - return

            {processedChildren}

            ; - }, - h2: ({node, children, ...props}) => { - const processedChildren = getCitationSource - ? processCitationsInReactChildren(children, getCitationSource) - : children; - return

            {processedChildren}

            ; - }, - h3: ({node, children, ...props}) => { - const processedChildren = getCitationSource - ? processCitationsInReactChildren(children, getCitationSource) - : children; - return

            {processedChildren}

            ; - }, - h4: ({node, children, ...props}) => { - const processedChildren = getCitationSource - ? processCitationsInReactChildren(children, getCitationSource) - : children; - return

            {processedChildren}

            ; - }, - blockquote: ({node, ...props}) =>
            , - hr: ({node, ...props}) =>
            , - img: ({node, ...props}) => , - table: ({node, ...props}) =>
            , - th: ({node, ...props}) =>
            , - td: ({node, ...props}) => , - code: ({node, className, children, ...props}: any) => { - const match = /language-(\w+)/.exec(className || ''); - const isInline = !match; - return isInline - ? {children} - : ( -
            -
            -                    {children}
            -                  
            -
            - ); - } - }} + components={components} > {content} @@ -98,7 +103,7 @@ export function MarkdownViewer({ content, className, getCitationSource }: Markdo } // Helper function to process citations within React children -function processCitationsInReactChildren(children: React.ReactNode, getCitationSource: (id: number) => Source | null): React.ReactNode { +const processCitationsInReactChildren = (children: React.ReactNode, getCitationSource: (id: number) => Source | null): React.ReactNode => { // If children is not an array or string, just return it if (!children || (typeof children !== 'string' && !Array.isArray(children))) { return children; @@ -120,10 +125,10 @@ function processCitationsInReactChildren(children: React.ReactNode, getCitationS } return children; -} +}; // Process citation references in text content -function processCitationsInText(text: string, getCitationSource: (id: number) => Source | null): React.ReactNode[] { +const processCitationsInText = (text: string, getCitationSource: (id: number) => Source | null): React.ReactNode[] => { // Use improved regex to catch citation numbers more reliably // This will match patterns like [1], [42], etc. including when they appear at the end of a line or sentence const citationRegex = /\[(\d+)\]/g; @@ -131,14 +136,8 @@ function processCitationsInText(text: string, getCitationSource: (id: number) => let lastIndex = 0; let match; let position = 0; - - // Debug log for troubleshooting - console.log("Processing citations in text:", text); while ((match = citationRegex.exec(text)) !== null) { - // Log each match for debugging - console.log("Citation match found:", match[0], "at index", match.index); - // Add text before the citation if (match.index > lastIndex) { parts.push(text.substring(lastIndex, match.index)); @@ -148,9 +147,6 @@ function processCitationsInText(text: string, getCitationSource: (id: number) => const citationId = parseInt(match[1], 10); const source = getCitationSource(citationId); - // Log the citation details - console.log("Citation ID:", citationId, "Source:", source ? "found" : "not found"); - parts.push( } return parts; -} \ No newline at end of file +}; \ No newline at end of file