From 2714f89c814d89eac6c24dc4614f6f0b656feb64 Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Tue, 9 Sep 2025 21:40:35 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(frontend):=20Add=20zustand=20libra?= =?UTF-8?q?ry=20for=20managing=20loading=20state=20in=20the=20application?= =?UTF-8?q?=20=F0=9F=94=A7=20(frontend):=20Refactor=20navigation.tsx=20to?= =?UTF-8?q?=20improve=20code=20readability=20and=20maintainability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/navigation.tsx | 432 +++++++++++++++++----------- frontend/package-lock.json | 31 +- frontend/package.json | 3 +- frontend/src/app/chat/page.tsx | 135 ++++----- frontend/src/stores/loadingStore.ts | 11 + 5 files changed, 370 insertions(+), 242 deletions(-) create mode 100644 frontend/src/stores/loadingStore.ts diff --git a/frontend/components/navigation.tsx b/frontend/components/navigation.tsx index 7419a25a..cf0d66ef 100644 --- a/frontend/components/navigation.tsx +++ b/frontend/components/navigation.tsx @@ -1,143 +1,176 @@ -"use client" +"use client"; -import Link from "next/link" -import { usePathname } from "next/navigation" -import { Library, MessageSquare, Settings2, Plus, FileText } from "lucide-react" -import { cn } from "@/lib/utils" -import { useState, useEffect, useRef, useCallback } from "react" -import { useChat } from "@/contexts/chat-context" +import { useChat } from "@/contexts/chat-context"; +import { cn } from "@/lib/utils"; +import { + FileText, + Library, + MessageSquare, + Plus, + Settings2, +} from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useCallback, useEffect, useRef, useState } from "react"; -import { EndpointType } from "@/contexts/chat-context" +import { EndpointType } from "@/contexts/chat-context"; +import { useLoadingStore } from "@/stores/loadingStore"; interface RawConversation { - response_id: string - title: string - endpoint: string + response_id: string; + title: string; + endpoint: string; messages: Array<{ - role: string - content: string - timestamp?: string - response_id?: string - }> - created_at?: string - last_activity?: string - previous_response_id?: string - total_messages: number - [key: string]: unknown + role: string; + content: string; + timestamp?: string; + response_id?: string; + }>; + created_at?: string; + last_activity?: string; + previous_response_id?: string; + total_messages: number; + [key: string]: unknown; } interface ChatConversation { - response_id: string - title: string - endpoint: EndpointType + response_id: string; + title: string; + endpoint: EndpointType; messages: Array<{ - role: string - content: string - timestamp?: string - response_id?: string - }> - created_at?: string - last_activity?: string - previous_response_id?: string - total_messages: number - [key: string]: unknown + role: string; + content: string; + timestamp?: string; + response_id?: string; + }>; + created_at?: string; + last_activity?: string; + previous_response_id?: string; + total_messages: number; + [key: string]: unknown; } - - export function Navigation() { - const pathname = usePathname() - const { endpoint, refreshTrigger, loadConversation, currentConversationId, setCurrentConversationId, startNewConversation, conversationDocs, addConversationDoc, refreshConversations, placeholderConversation, setPlaceholderConversation } = useChat() - const [conversations, setConversations] = useState([]) - const [loadingConversations, setLoadingConversations] = useState(false) - const [previousConversationCount, setPreviousConversationCount] = useState(0) - const fileInputRef = useRef(null) - + const pathname = usePathname(); + const { + endpoint, + refreshTrigger, + loadConversation, + currentConversationId, + setCurrentConversationId, + startNewConversation, + conversationDocs, + addConversationDoc, + refreshConversations, + placeholderConversation, + setPlaceholderConversation, + } = useChat(); + + const { loading } = useLoadingStore(); + + const [conversations, setConversations] = useState([]); + const [loadingConversations, setLoadingConversations] = useState(false); + const [loadingNewConversation, setLoadingNewConversation] = useState(false); + const [previousConversationCount, setPreviousConversationCount] = useState(0); + const fileInputRef = useRef(null); + const handleNewConversation = () => { - // Ensure current conversation appears in sidebar before starting a new one - refreshConversations() - // Use context helper to fully reset conversation state - startNewConversation() - // Notify chat view even if state was already 'new' - if (typeof window !== 'undefined') { - window.dispatchEvent(new CustomEvent('newConversation')) + setLoadingNewConversation(true); + refreshConversations(); + startNewConversation(); + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent("newConversation")); } - } + // Clear loading state after a short delay to show the new conversation is created + setTimeout(() => { + setLoadingNewConversation(false); + }, 300); + }; const handleFileUpload = async (file: File) => { - console.log("Navigation file upload:", file.name) - + console.log("Navigation file upload:", file.name); + // Trigger loading start event for chat page - window.dispatchEvent(new CustomEvent('fileUploadStart', { - detail: { filename: file.name } - })) - - try { - const formData = new FormData() - formData.append('file', file) - formData.append('endpoint', endpoint) - - const response = await fetch('/api/upload_context', { - method: 'POST', - body: formData, + window.dispatchEvent( + new CustomEvent("fileUploadStart", { + detail: { filename: file.name }, }) - + ); + + try { + const formData = new FormData(); + formData.append("file", file); + formData.append("endpoint", endpoint); + + const response = await fetch("/api/upload_context", { + method: "POST", + body: formData, + }); + if (!response.ok) { - const errorText = await response.text() - console.error("Upload failed:", errorText) - + const errorText = await response.text(); + console.error("Upload failed:", errorText); + // Trigger error event for chat page to handle - window.dispatchEvent(new CustomEvent('fileUploadError', { - detail: { filename: file.name, error: 'Failed to process document' } - })) - + window.dispatchEvent( + new CustomEvent("fileUploadError", { + detail: { + filename: file.name, + error: "Failed to process document", + }, + }) + ); + // Trigger loading end event - window.dispatchEvent(new CustomEvent('fileUploadComplete')) - return + window.dispatchEvent(new CustomEvent("fileUploadComplete")); + return; } - - const result = await response.json() - console.log("Upload result:", result) - + + const result = await response.json(); + console.log("Upload result:", result); + // Add the file to conversation docs if (result.filename) { - addConversationDoc(result.filename) + addConversationDoc(result.filename); } - + // Trigger file upload event for chat page to handle - window.dispatchEvent(new CustomEvent('fileUploaded', { - detail: { file, result } - })) - + window.dispatchEvent( + new CustomEvent("fileUploaded", { + detail: { file, result }, + }) + ); + // Trigger loading end event - window.dispatchEvent(new CustomEvent('fileUploadComplete')) - + window.dispatchEvent(new CustomEvent("fileUploadComplete")); } catch (error) { - console.error('Upload failed:', error) + console.error("Upload failed:", error); // Trigger loading end event even on error - window.dispatchEvent(new CustomEvent('fileUploadComplete')) - + window.dispatchEvent(new CustomEvent("fileUploadComplete")); + // Trigger error event for chat page to handle - window.dispatchEvent(new CustomEvent('fileUploadError', { - detail: { filename: file.name, error: 'Failed to process document' } - })) + window.dispatchEvent( + new CustomEvent("fileUploadError", { + detail: { filename: file.name, error: "Failed to process document" }, + }) + ); } - } + }; const handleFilePickerClick = () => { - fileInputRef.current?.click() - } + fileInputRef.current?.click(); + }; const handleFilePickerChange = (e: React.ChangeEvent) => { - const files = e.target.files + const files = e.target.files; if (files && files.length > 0) { - handleFileUpload(files[0]) + handleFileUpload(files[0]); } // Reset the input so the same file can be selected again if (fileInputRef.current) { - fileInputRef.current.value = '' + fileInputRef.current.value = ""; } - } + }; const routes = [ { @@ -158,99 +191,123 @@ export function Navigation() { href: "/settings", active: pathname === "/settings", }, - ] + ]; - const isOnChatPage = pathname === "/" || pathname === "/chat" + const isOnChatPage = pathname === "/" || pathname === "/chat"; const createDefaultPlaceholder = useCallback(() => { return { - response_id: 'new-conversation-' + Date.now(), - title: 'New conversation', + response_id: "new-conversation-" + Date.now(), + title: "New conversation", endpoint: endpoint, - messages: [{ - role: 'assistant', - content: 'How can I assist?', - timestamp: new Date().toISOString() - }], + messages: [ + { + role: "assistant", + content: "How can I assist?", + timestamp: new Date().toISOString(), + }, + ], created_at: new Date().toISOString(), last_activity: new Date().toISOString(), - total_messages: 1 - } as ChatConversation - }, [endpoint]) + total_messages: 1, + } as ChatConversation; + }, [endpoint]); const fetchConversations = useCallback(async () => { - setLoadingConversations(true) + setLoadingConversations(true); try { // Fetch from the selected endpoint only - const apiEndpoint = endpoint === 'chat' ? '/api/chat/history' : '/api/langflow/history' - - const response = await fetch(apiEndpoint) + const apiEndpoint = + endpoint === "chat" ? "/api/chat/history" : "/api/langflow/history"; + + const response = await fetch(apiEndpoint); if (response.ok) { - const history = await response.json() - const rawConversations = history.conversations || [] - + const history = await response.json(); + const rawConversations = history.conversations || []; + // Cast conversations to proper type and ensure endpoint is correct - const conversations: ChatConversation[] = rawConversations.map((conv: RawConversation) => ({ - ...conv, - endpoint: conv.endpoint as EndpointType - })) - + const conversations: ChatConversation[] = rawConversations.map( + (conv: RawConversation) => ({ + ...conv, + endpoint: conv.endpoint as EndpointType, + }) + ); + // Sort conversations by last activity (most recent first) conversations.sort((a: ChatConversation, b: ChatConversation) => { - const aTime = new Date(a.last_activity || a.created_at || 0).getTime() - const bTime = new Date(b.last_activity || b.created_at || 0).getTime() - return bTime - aTime - }) - - setConversations(conversations) - + const aTime = new Date( + a.last_activity || a.created_at || 0 + ).getTime(); + const bTime = new Date( + b.last_activity || b.created_at || 0 + ).getTime(); + return bTime - aTime; + }); + + setConversations(conversations); + // If no conversations exist and no placeholder is shown, create a default placeholder if (conversations.length === 0 && !placeholderConversation) { - setPlaceholderConversation(createDefaultPlaceholder()) + setPlaceholderConversation(createDefaultPlaceholder()); } } else { - setConversations([]) - + setConversations([]); + // Also create placeholder when request fails and no conversations exist if (!placeholderConversation) { - setPlaceholderConversation(createDefaultPlaceholder()) + setPlaceholderConversation(createDefaultPlaceholder()); } } - + // Conversation documents are now managed in chat context - } catch (error) { - console.error(`Failed to fetch ${endpoint} conversations:`, error) - setConversations([]) + console.error(`Failed to fetch ${endpoint} conversations:`, error); + setConversations([]); } finally { - setLoadingConversations(false) + setLoadingConversations(false); } - }, [endpoint, placeholderConversation, setPlaceholderConversation, createDefaultPlaceholder]) + }, [ + endpoint, + placeholderConversation, + setPlaceholderConversation, + createDefaultPlaceholder, + ]); // Fetch chat conversations when on chat page, endpoint changes, or refresh is triggered useEffect(() => { if (isOnChatPage) { - fetchConversations() + fetchConversations(); } - }, [isOnChatPage, endpoint, refreshTrigger, fetchConversations]) + }, [isOnChatPage, endpoint, refreshTrigger, fetchConversations]); // Clear placeholder when conversation count increases (new conversation was created) useEffect(() => { - const currentCount = conversations.length - + const currentCount = conversations.length; + // If we had a placeholder and the conversation count increased, clear the placeholder and highlight the new conversation - if (placeholderConversation && currentCount > previousConversationCount && conversations.length > 0) { - setPlaceholderConversation(null) + if ( + placeholderConversation && + currentCount > previousConversationCount && + conversations.length > 0 + ) { + setPlaceholderConversation(null); // Highlight the most recent conversation (first in sorted array) without loading its messages - const newestConversation = conversations[0] + const newestConversation = conversations[0]; if (newestConversation) { - setCurrentConversationId(newestConversation.response_id) + setCurrentConversationId(newestConversation.response_id); } } - + // Update the previous count - setPreviousConversationCount(currentCount) - }, [conversations.length, placeholderConversation, setPlaceholderConversation, previousConversationCount, conversations, setCurrentConversationId]) + setPreviousConversationCount(currentCount); + }, [ + conversations.length, + placeholderConversation, + setPlaceholderConversation, + previousConversationCount, + conversations, + setCurrentConversationId, + ]); return (
@@ -262,13 +319,20 @@ export function Navigation() { href={route.href} className={cn( "text-sm group flex p-3 w-full justify-start font-medium cursor-pointer hover:bg-accent hover:text-accent-foreground rounded-lg transition-all", - route.active - ? "bg-accent text-accent-foreground shadow-sm" - : "text-foreground hover:text-accent-foreground", + route.active + ? "bg-accent text-accent-foreground shadow-sm" + : "text-foreground hover:text-accent-foreground" )} >
- + {route.label}
@@ -286,22 +350,27 @@ export function Navigation() { {/* Conversations Section */}
-

Conversations

-
- +
{/* Conversations List - grows naturally, doesn't fill all space */}
- {loadingConversations ? ( -
Loading...
+ {loadingNewConversation ? ( +
+ Loading... +
) : ( <> {/* Show placeholder conversation if it exists */} @@ -310,8 +379,8 @@ export function Navigation() { className="p-2 rounded-lg bg-accent/50 border border-dashed border-accent cursor-pointer group" onClick={() => { // Don't load placeholder as a real conversation, just focus the input - if (typeof window !== 'undefined') { - window.dispatchEvent(new CustomEvent('focusInput')) + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent("focusInput")); } }} > @@ -323,19 +392,29 @@ export function Navigation() {
)} - + {/* Show regular conversations */} {conversations.length === 0 && !placeholderConversation ? ( -
No conversations yet
+
+ No conversations yet +
) : ( conversations.map((conversation) => (
{ - loadConversation(conversation) + if (loading) return; + loadConversation(conversation); + refreshConversations(); }} >
@@ -346,7 +425,9 @@ export function Navigation() {
{conversation.last_activity && (
- {new Date(conversation.last_activity).toLocaleDateString()} + {new Date( + conversation.last_activity + ).toLocaleDateString()}
)}
@@ -359,10 +440,13 @@ export function Navigation() { {/* Conversation Knowledge Section - appears right after last conversation */}
-

Conversation knowledge

- @@ -376,7 +460,9 @@ export function Navigation() { />
{conversationDocs.length === 0 ? ( -
No documents yet
+
+ No documents yet +
) : ( conversationDocs.map((doc, index) => (
)}
- ) + ); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index da0fb436..63c356a2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -37,7 +37,8 @@ "react-icons": "^5.5.0", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zustand": "^5.0.8" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -8267,6 +8268,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/frontend/package.json b/frontend/package.json index ac02091b..6de3493f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,7 +38,8 @@ "react-icons": "^5.5.0", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zustand": "^5.0.8" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/frontend/src/app/chat/page.tsx b/frontend/src/app/chat/page.tsx index 8b3b48df..1373db20 100644 --- a/frontend/src/app/chat/page.tsx +++ b/frontend/src/app/chat/page.tsx @@ -1,5 +1,12 @@ "use client"; +import { ProtectedRoute } from "@/components/protected-route"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { useAuth } from "@/contexts/auth-context"; +import { type EndpointType, useChat } from "@/contexts/chat-context"; +import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; +import { useTask } from "@/contexts/task-context"; import { AtSign, Bot, @@ -15,14 +22,8 @@ import { Zap, } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -import { ProtectedRoute } from "@/components/protected-route"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; -import { useAuth } from "@/contexts/auth-context"; -import { type EndpointType, useChat } from "@/contexts/chat-context"; -import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; -import { useTask } from "@/contexts/task-context"; import { useGetNudgesQuery } from "../api/queries/useGetNudgesQuery"; +import { useLoadingStore } from "@/stores/loadingStore"; import Nudges from "./nudges"; interface Message { @@ -106,7 +107,7 @@ function ChatPage() { }, ]); const [input, setInput] = useState(""); - const [loading, setLoading] = useState(false); + const { loading, setLoading } = useLoadingStore(); const [asyncMode, setAsyncMode] = useState(true); const [streamingMessage, setStreamingMessage] = useState<{ content: string; @@ -192,7 +193,7 @@ function ChatPage() { "Upload failed with status:", response.status, "Response:", - errorText, + errorText ); throw new Error("Failed to process document"); } @@ -446,7 +447,7 @@ function ChatPage() { console.log( "Loading conversation with", conversationData.messages.length, - "messages", + "messages" ); // Convert backend message format to frontend Message interface const convertedMessages: Message[] = conversationData.messages.map( @@ -574,7 +575,7 @@ function ChatPage() { ) === "string" ? toolCall.function?.arguments || toolCall.arguments : JSON.stringify( - toolCall.function?.arguments || toolCall.arguments, + toolCall.function?.arguments || toolCall.arguments ), result: toolCall.result, status: "completed", @@ -593,7 +594,7 @@ function ChatPage() { } return message; - }, + } ); setMessages(convertedMessages); @@ -682,7 +683,7 @@ function ChatPage() { console.log( "Chat page received file upload error event:", filename, - error, + error ); // Replace the last message with error message @@ -696,37 +697,37 @@ function ChatPage() { window.addEventListener( "fileUploadStart", - handleFileUploadStart as EventListener, + handleFileUploadStart as EventListener ); window.addEventListener( "fileUploaded", - handleFileUploaded as EventListener, + handleFileUploaded as EventListener ); window.addEventListener( "fileUploadComplete", - handleFileUploadComplete as EventListener, + handleFileUploadComplete as EventListener ); window.addEventListener( "fileUploadError", - handleFileUploadError as EventListener, + handleFileUploadError as EventListener ); return () => { window.removeEventListener( "fileUploadStart", - handleFileUploadStart as EventListener, + handleFileUploadStart as EventListener ); window.removeEventListener( "fileUploaded", - handleFileUploaded as EventListener, + handleFileUploaded as EventListener ); window.removeEventListener( "fileUploadComplete", - handleFileUploadComplete as EventListener, + handleFileUploadComplete as EventListener ); window.removeEventListener( "fileUploadError", - handleFileUploadError as EventListener, + handleFileUploadError as EventListener ); }; }, [endpoint, setPreviousResponseIds]); @@ -753,7 +754,7 @@ function ChatPage() { }, [isFilterDropdownOpen]); const { data: nudges = [], cancel: cancelNudges } = useGetNudgesQuery( - previousResponseIds[endpoint], + previousResponseIds[endpoint] ); const handleSSEStream = async (userMessage: Message) => { @@ -858,7 +859,7 @@ function ChatPage() { console.log( "Received chunk:", chunk.type || chunk.object, - chunk, + chunk ); // Extract response ID if present @@ -874,14 +875,14 @@ function ChatPage() { if (chunk.delta.function_call) { console.log( "Function call in delta:", - chunk.delta.function_call, + chunk.delta.function_call ); // Check if this is a new function call if (chunk.delta.function_call.name) { console.log( "New function call:", - chunk.delta.function_call.name, + chunk.delta.function_call.name ); const functionCall: FunctionCall = { name: chunk.delta.function_call.name, @@ -897,7 +898,7 @@ function ChatPage() { else if (chunk.delta.function_call.arguments) { console.log( "Function call arguments delta:", - chunk.delta.function_call.arguments, + chunk.delta.function_call.arguments ); const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1]; @@ -909,14 +910,14 @@ function ChatPage() { chunk.delta.function_call.arguments; console.log( "Accumulated arguments:", - lastFunctionCall.argumentsString, + lastFunctionCall.argumentsString ); // Try to parse arguments if they look complete if (lastFunctionCall.argumentsString.includes("}")) { try { const parsed = JSON.parse( - lastFunctionCall.argumentsString, + lastFunctionCall.argumentsString ); lastFunctionCall.arguments = parsed; lastFunctionCall.status = "completed"; @@ -924,7 +925,7 @@ function ChatPage() { } catch (e) { console.log( "Arguments not yet complete or invalid JSON:", - e, + e ); } } @@ -957,7 +958,7 @@ function ChatPage() { else if (toolCall.function.arguments) { console.log( "Tool call arguments delta:", - toolCall.function.arguments, + toolCall.function.arguments ); const lastFunctionCall = currentFunctionCalls[ @@ -971,7 +972,7 @@ function ChatPage() { toolCall.function.arguments; console.log( "Accumulated tool arguments:", - lastFunctionCall.argumentsString, + lastFunctionCall.argumentsString ); // Try to parse arguments if they look complete @@ -980,7 +981,7 @@ function ChatPage() { ) { try { const parsed = JSON.parse( - lastFunctionCall.argumentsString, + lastFunctionCall.argumentsString ); lastFunctionCall.arguments = parsed; lastFunctionCall.status = "completed"; @@ -988,7 +989,7 @@ function ChatPage() { } catch (e) { console.log( "Tool arguments not yet complete or invalid JSON:", - e, + e ); } } @@ -1020,7 +1021,7 @@ function ChatPage() { console.log( "Error parsing function call on finish:", fc, - e, + e ); } } @@ -1036,12 +1037,12 @@ function ChatPage() { console.log( "🟢 CREATING function call (added):", chunk.item.id, - chunk.item.tool_name || chunk.item.name, + chunk.item.tool_name || chunk.item.name ); // Try to find an existing pending call to update (created by earlier deltas) let existing = currentFunctionCalls.find( - (fc) => fc.id === chunk.item.id, + (fc) => fc.id === chunk.item.id ); if (!existing) { existing = [...currentFunctionCalls] @@ -1050,7 +1051,7 @@ function ChatPage() { (fc) => fc.status === "pending" && !fc.id && - fc.name === (chunk.item.tool_name || chunk.item.name), + fc.name === (chunk.item.tool_name || chunk.item.name) ); } @@ -1063,7 +1064,7 @@ function ChatPage() { chunk.item.inputs || existing.arguments; console.log( "🟢 UPDATED existing pending function call with id:", - existing.id, + existing.id ); } else { const functionCall: FunctionCall = { @@ -1081,7 +1082,7 @@ function ChatPage() { currentFunctionCalls.map((fc) => ({ id: fc.id, name: fc.name, - })), + })) ); } } @@ -1092,7 +1093,7 @@ function ChatPage() { ) { console.log( "Function args delta (Realtime API):", - chunk.delta, + chunk.delta ); const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1]; @@ -1103,7 +1104,7 @@ function ChatPage() { lastFunctionCall.argumentsString += chunk.delta || ""; console.log( "Accumulated arguments (Realtime API):", - lastFunctionCall.argumentsString, + lastFunctionCall.argumentsString ); } } @@ -1114,26 +1115,26 @@ function ChatPage() { ) { console.log( "Function args done (Realtime API):", - chunk.arguments, + chunk.arguments ); const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1]; if (lastFunctionCall) { try { lastFunctionCall.arguments = JSON.parse( - chunk.arguments || "{}", + chunk.arguments || "{}" ); lastFunctionCall.status = "completed"; console.log( "Parsed function arguments (Realtime API):", - lastFunctionCall.arguments, + lastFunctionCall.arguments ); } catch (e) { lastFunctionCall.arguments = { raw: chunk.arguments }; lastFunctionCall.status = "error"; console.log( "Error parsing function arguments (Realtime API):", - e, + e ); } } @@ -1147,14 +1148,14 @@ function ChatPage() { console.log( "🔵 UPDATING function call (done):", chunk.item.id, - chunk.item.tool_name || chunk.item.name, + chunk.item.tool_name || chunk.item.name ); console.log( "🔵 Looking for existing function calls:", currentFunctionCalls.map((fc) => ({ id: fc.id, name: fc.name, - })), + })) ); // Find existing function call by ID or name @@ -1162,14 +1163,14 @@ function ChatPage() { (fc) => fc.id === chunk.item.id || fc.name === chunk.item.tool_name || - fc.name === chunk.item.name, + fc.name === chunk.item.name ); if (functionCall) { console.log( "🔵 FOUND existing function call, updating:", functionCall.id, - functionCall.name, + functionCall.name ); // Update existing function call with completion data functionCall.status = @@ -1192,7 +1193,7 @@ function ChatPage() { "🔴 WARNING: Could not find existing function call to update:", chunk.item.id, chunk.item.tool_name, - chunk.item.name, + chunk.item.name ); } } @@ -1213,7 +1214,7 @@ function ChatPage() { fc.name === chunk.item.name || fc.name === chunk.item.type || fc.name.includes(chunk.item.type.replace("_call", "")) || - chunk.item.type.includes(fc.name), + chunk.item.type.includes(fc.name) ); if (functionCall) { @@ -1257,12 +1258,12 @@ function ChatPage() { "🟡 CREATING tool call (added):", chunk.item.id, chunk.item.tool_name || chunk.item.name, - chunk.item.type, + chunk.item.type ); // Dedupe by id or pending with same name let existing = currentFunctionCalls.find( - (fc) => fc.id === chunk.item.id, + (fc) => fc.id === chunk.item.id ); if (!existing) { existing = [...currentFunctionCalls] @@ -1274,7 +1275,7 @@ function ChatPage() { fc.name === (chunk.item.tool_name || chunk.item.name || - chunk.item.type), + chunk.item.type) ); } @@ -1290,7 +1291,7 @@ function ChatPage() { chunk.item.inputs || existing.arguments; console.log( "🟡 UPDATED existing pending tool call with id:", - existing.id, + existing.id ); } else { const functionCall = { @@ -1311,7 +1312,7 @@ function ChatPage() { id: fc.id, name: fc.name, type: fc.type, - })), + })) ); } } @@ -1589,7 +1590,7 @@ function ChatPage() { const handleForkConversation = ( messageIndex: number, - event?: React.MouseEvent, + event?: React.MouseEvent ) => { // Prevent any default behavior and stop event propagation if (event) { @@ -1654,7 +1655,7 @@ function ChatPage() { const renderFunctionCalls = ( functionCalls: FunctionCall[], - messageIndex?: number, + messageIndex?: number ) => { if (!functionCalls || functionCalls.length === 0) return null; @@ -2022,7 +2023,7 @@ function ChatPage() {
{renderFunctionCalls( message.functionCalls || [], - index, + index )}

{message.content} @@ -2053,7 +2054,7 @@ function ChatPage() {

{renderFunctionCalls( streamingMessage.functionCalls, - messages.length, + messages.length )}

{streamingMessage.content} @@ -2196,7 +2197,7 @@ function ChatPage() { const filteredFilters = availableFilters.filter((filter) => filter.name .toLowerCase() - .includes(filterSearchTerm.toLowerCase()), + .includes(filterSearchTerm.toLowerCase()) ); if (e.key === "Escape") { @@ -2214,7 +2215,7 @@ function ChatPage() { if (e.key === "ArrowDown") { e.preventDefault(); setSelectedFilterIndex((prev) => - prev < filteredFilters.length - 1 ? prev + 1 : 0, + prev < filteredFilters.length - 1 ? prev + 1 : 0 ); return; } @@ -2222,7 +2223,7 @@ function ChatPage() { if (e.key === "ArrowUp") { e.preventDefault(); setSelectedFilterIndex((prev) => - prev > 0 ? prev - 1 : filteredFilters.length - 1, + prev > 0 ? prev - 1 : filteredFilters.length - 1 ); return; } @@ -2240,7 +2241,7 @@ function ChatPage() { ) { e.preventDefault(); handleFilterSelect( - filteredFilters[selectedFilterIndex], + filteredFilters[selectedFilterIndex] ); return; } @@ -2259,7 +2260,7 @@ function ChatPage() { ) { e.preventDefault(); handleFilterSelect( - filteredFilters[selectedFilterIndex], + filteredFilters[selectedFilterIndex] ); return; } @@ -2339,7 +2340,7 @@ function ChatPage() { .filter((filter) => filter.name .toLowerCase() - .includes(filterSearchTerm.toLowerCase()), + .includes(filterSearchTerm.toLowerCase()) ) .map((filter, index) => (