feat(ChatWindow): Implement message buffering for improved UI updates and token handling

This commit is contained in:
Willie Zutz 2025-08-05 00:07:15 -06:00
parent 71120c997a
commit 643db447eb
4 changed files with 125 additions and 89 deletions

View file

@ -420,6 +420,9 @@ const ChatWindow = ({ id }: { id?: string }) => {
let sources: Document[] | undefined = undefined; let sources: Document[] | undefined = undefined;
let recievedMessage = ''; let recievedMessage = '';
let messageBuffer = '';
let tokenCount = 0;
const bufferThreshold = 10;
let added = false; let added = false;
let messageChatHistory = chatHistory; let messageChatHistory = chatHistory;
@ -542,11 +545,18 @@ const ChatWindow = ({ id }: { id?: string }) => {
} }
if (data.type === 'response') { if (data.type === 'response') {
// Add to buffer instead of immediately updating UI
messageBuffer += data.data;
recievedMessage += data.data;
tokenCount++;
// Only update UI every bufferThreshold tokens
if (tokenCount >= bufferThreshold) {
if (!added) { if (!added) {
setMessages((prevMessages) => [ setMessages((prevMessages) => [
...prevMessages, ...prevMessages,
{ {
content: data.data, content: messageBuffer,
messageId: data.messageId, // Use the AI message ID from the backend messageId: data.messageId, // Use the AI message ID from the backend
chatId: chatId!, chatId: chatId!,
role: 'assistant', role: 'assistant',
@ -559,33 +569,31 @@ const ChatWindow = ({ id }: { id?: string }) => {
setMessages((prev) => setMessages((prev) =>
prev.map((message) => { prev.map((message) => {
if (message.messageId === data.messageId) { if (message.messageId === data.messageId) {
return { ...message, content: message.content + data.data }; return { ...message, content: recievedMessage };
} }
return message; return message;
}), }),
); );
} }
recievedMessage += data.data; // Reset buffer and counter
messageBuffer = '';
tokenCount = 0;
setScrollTrigger((prev) => prev + 1); setScrollTrigger((prev) => prev + 1);
} }
}
if (data.type === 'messageEnd') { if (data.type === 'messageEnd') {
// Clear analysis progress // Clear analysis progress
setAnalysisProgress(null); setAnalysisProgress(null);
setChatHistory((prevHistory) => [ // Ensure final message content is displayed (flush any remaining buffer)
...prevHistory,
['human', message],
['assistant', recievedMessage],
]);
// Always update the message, adding modelStats if available
setMessages((prev) => setMessages((prev) =>
prev.map((message) => { prev.map((message) => {
if (message.messageId === data.messageId) { if (message.messageId === data.messageId) {
return { return {
...message, ...message,
content: recievedMessage, // Use the complete received message
// Include model stats if available, otherwise null // Include model stats if available, otherwise null
modelStats: data.modelStats || null, modelStats: data.modelStats || null,
// Make sure the searchQuery is preserved (if available in the message data) // Make sure the searchQuery is preserved (if available in the message data)
@ -597,6 +605,12 @@ const ChatWindow = ({ id }: { id?: string }) => {
}), }),
); );
setChatHistory((prevHistory) => [
...prevHistory,
['human', message],
['assistant', recievedMessage],
]);
setLoading(false); setLoading(false);
setScrollTrigger((prev) => prev + 1); setScrollTrigger((prev) => prev + 1);

View file

@ -1,5 +1,6 @@
import { Document } from '@langchain/core/documents'; import { Document } from '@langchain/core/documents';
import { useState } from 'react'; import { useState, useRef } from 'react';
import { createPortal } from 'react-dom';
import MessageSource from './MessageSource'; import MessageSource from './MessageSource';
interface CitationLinkProps { interface CitationLinkProps {
@ -10,6 +11,9 @@ interface CitationLinkProps {
const CitationLink = ({ number, source, url }: CitationLinkProps) => { const CitationLink = ({ number, source, url }: CitationLinkProps) => {
const [showTooltip, setShowTooltip] = useState(false); const [showTooltip, setShowTooltip] = useState(false);
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
const spanRef = useRef<HTMLSpanElement>(null);
const linkContent = ( const linkContent = (
<a <a
href={url} href={url}
@ -21,19 +25,45 @@ const CitationLink = ({ number, source, url }: CitationLinkProps) => {
</a> </a>
); );
const handleMouseEnter = () => {
if (spanRef.current) {
const rect = spanRef.current.getBoundingClientRect();
setTooltipPosition({
x: rect.left + rect.width / 2,
y: rect.top,
});
setShowTooltip(true);
}
};
const handleMouseLeave = () => {
setShowTooltip(false);
};
// If we have source data, wrap with tooltip // If we have source data, wrap with tooltip
if (source) { if (source) {
return ( return (
<div className="relative inline-block"> <>
<div <span
onMouseEnter={() => setShowTooltip(true)} ref={spanRef}
onMouseLeave={() => setShowTooltip(false)} className="relative inline-block"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
> >
{linkContent} {linkContent}
</div> </span>
{showTooltip && ( {showTooltip &&
<div className="absolute z-50 bottom-full mb-2 left-1/2 transform -translate-x-1/2 animate-in fade-in-0 duration-150"> typeof window !== 'undefined' &&
createPortal(
<div
className="fixed z-50 animate-in fade-in-0 duration-150"
style={{
left: tooltipPosition.x,
top: tooltipPosition.y - 8,
transform: 'translate(-50%, -100%)',
}}
>
<div className="bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 shadow-lg w-96"> <div className="bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 shadow-lg w-96">
<MessageSource <MessageSource
source={source} source={source}
@ -42,9 +72,10 @@ const CitationLink = ({ number, source, url }: CitationLinkProps) => {
</div> </div>
{/* Tooltip arrow */} {/* Tooltip arrow */}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-light-200 dark:border-t-dark-200"></div> <div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-light-200 dark:border-t-dark-200"></div>
</div> </div>,
document.body,
)} )}
</div> </>
); );
} }

View file

@ -100,7 +100,6 @@ const MessageTabs = ({
// Process message content // Process message content
useEffect(() => { useEffect(() => {
const citationRegex = /\[([^\]]+)\]/g;
const regex = /\[(\d+)\]/g; const regex = /\[(\d+)\]/g;
let processedMessage = message.content; let processedMessage = message.content;
@ -119,9 +118,7 @@ const MessageTabs = ({
message.sources.length > 0 message.sources.length > 0
) { ) {
setParsedMessage( setParsedMessage(
processedMessage.replace( processedMessage.replace(regex, (_, capturedContent: string) => {
citationRegex,
(_, capturedContent: string) => {
const numbers = capturedContent const numbers = capturedContent
.split(',') .split(',')
.map((numStr) => numStr.trim()); .map((numStr) => numStr.trim());
@ -146,8 +143,7 @@ const MessageTabs = ({
.join(''); .join('');
return linksHtml; return linksHtml;
}, }),
),
); );
setSpeechMessage(message.content.replace(regex, '')); setSpeechMessage(message.content.replace(regex, ''));
return; return;

View file

@ -58,11 +58,8 @@ function normalizeUsageMetadata(usageData: any): {
} }
/** /**
* Simplified Agent using createReactAgent * SimplifiedAgent class that provides a streamlined interface for creating and managing an AI agent
* * with customizable focus modes and tools.
* This agent replaces the complex LangGraph supervisor pattern with a single
* tool-calling agent that handles analysis and synthesis internally while
* using specialized tools for search, file processing, and URL summarization.
*/ */
export class SimplifiedAgent { export class SimplifiedAgent {
private llm: BaseChatModel; private llm: BaseChatModel;
@ -95,7 +92,6 @@ export class SimplifiedAgent {
// Select appropriate tools based on focus mode and available files // Select appropriate tools based on focus mode and available files
const tools = this.getToolsForFocusMode(focusMode, fileIds); const tools = this.getToolsForFocusMode(focusMode, fileIds);
// Create the enhanced system prompt that includes analysis and synthesis instructions
const enhancedSystemPrompt = this.createEnhancedSystemPrompt( const enhancedSystemPrompt = this.createEnhancedSystemPrompt(
focusMode, focusMode,
fileIds, fileIds,
@ -159,9 +155,6 @@ export class SimplifiedAgent {
} }
} }
/**
* Create enhanced system prompt that includes analysis and synthesis capabilities
*/
private createEnhancedSystemPrompt( private createEnhancedSystemPrompt(
focusMode: string, focusMode: string,
fileIds: string[] = [], fileIds: string[] = [],
@ -348,15 +341,16 @@ Your task is to provide answers that are:
- Passing true is **required** to include images or links within the page content. - Passing true is **required** to include images or links within the page content.
- You will receive a summary of the content from each URL if the content of the page is long. If the content of the page is short, you will receive the full content. - You will receive a summary of the content from each URL if the content of the page is long. If the content of the page is short, you will receive the full content.
- You may request up to 5 URLs per turn. - You may request up to 5 URLs per turn.
- If you recieve a request to summarize a specific URL you **must** use this tool to retrieve it.
5. **Analyze**: Examine the retrieved information for relevance, accuracy, and completeness. 5. **Analyze**: Examine the retrieved information for relevance, accuracy, and completeness.
- If you have sufficient information, you can move on to the synthesis stage. - If you have sufficient information, you can move on to the respond stage.
- If you need to gather more information, consider revisiting the search or supplement stages.${ - If you need to gather more information, consider revisiting the search or supplement stages.${
fileIds.length > 0 fileIds.length > 0
? ` ? `
- Consider both web search results and file content when analyzing information completeness.` - Consider both web search results and file content when analyzing information completeness.`
: '' : ''
} }
6. **Synthesize**: Combine all information into a coherent, well-cited response 6. **Respond**: Combine all information into a coherent, well-cited response
- Ensure that all sources are properly cited and referenced - Ensure that all sources are properly cited and referenced
- Resolve any remaining contradictions or gaps in the information, if necessary, execute more targeted searches or retrieve specific sources${ - Resolve any remaining contradictions or gaps in the information, if necessary, execute more targeted searches or retrieve specific sources${
fileIds.length > 0 fileIds.length > 0
@ -457,13 +451,14 @@ Your task is to provide answers that are:
- You will receive relevant excerpts from documents that match your search criteria. - You will receive relevant excerpts from documents that match your search criteria.
- Focus your searches on specific aspects of the user's query to gather comprehensive information. - Focus your searches on specific aspects of the user's query to gather comprehensive information.
3. **Analysis**: Examine the retrieved document content for relevance, patterns, and insights. 3. **Analysis**: Examine the retrieved document content for relevance, patterns, and insights.
- If you have sufficient information from the documents, you can move on to the synthesis stage. - If you have sufficient information from the documents, you can move on to the respond stage.
- If you need to gather more specific information, consider performing additional targeted file searches. - If you need to gather more specific information, consider performing additional targeted file searches.
- Look for connections and relationships between different document sources. - Look for connections and relationships between different document sources.
4. **Synthesize**: Combine all document insights into a coherent, well-cited response 4. **Respond**: Combine all document insights into a coherent, well-cited response
- Ensure that all sources are properly cited and referenced - Ensure that all sources are properly cited and referenced
- Resolve any contradictions or gaps in the document information - Resolve any contradictions or gaps in the document information
- Provide comprehensive analysis based on the available document content - Provide comprehensive analysis based on the available document content
- Only respond with your final answer once you've gathered all relevant information and are done with tool use
## Current Context ## Current Context
- Today's Date: ${formatDateForLLM(new Date())} - Today's Date: ${formatDateForLLM(new Date())}