import React, { useEffect, useState, useRef, useCallback } from "react"; import NavBar from "../components/NavBar"; import ChatWindow from "../components/ChatWindow"; import { apiService } from "../services/api"; const POLL_INTERVAL = 600; // 0.6 seconds const INITIAL_ERROR_STATE = { visible: false, message: '' }; const DEBOUNCE_DELAY = 300; // 300ms debounce for user input function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; } export default function App() { const containerRef = useRef(null); const inputRef = useRef(null); const pollingRef = useRef(null); const scrollTimeoutRef = useRef(null); const [conversation, setConversation] = useState([]); const [lastMessage, setLastMessage] = useState(null); const [userInput, setUserInput] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(INITIAL_ERROR_STATE); const [done, setDone] = useState(true); const debouncedUserInput = useDebounce(userInput, DEBOUNCE_DELAY); const errorTimerRef = useRef(null); const handleError = useCallback((error, context) => { console.error(`${context}:`, error); const isConversationFetchError = error.status === 404; const errorMessage = isConversationFetchError ? "Error fetching conversation. Retrying..." // Updated message : `Error ${context.toLowerCase()}. Please try again.`; setError(prevError => { // If the same 404 error is already being displayed, don't reset state (prevents flickering) if (prevError.visible && prevError.message === errorMessage) { return prevError; } return { visible: true, message: errorMessage }; }); // Clear any existing timeout if (errorTimerRef.current) { clearTimeout(errorTimerRef.current); } // Only auto-dismiss non-404 errors after 3 seconds if (!isConversationFetchError) { errorTimerRef.current = setTimeout(() => setError(INITIAL_ERROR_STATE), 3000); } }, []); const clearErrorOnSuccess = useCallback(() => { if (errorTimerRef.current) { clearTimeout(errorTimerRef.current); } setError(INITIAL_ERROR_STATE); }, []); const fetchConversationHistory = useCallback(async () => { try { const data = await apiService.getConversationHistory(); const newConversation = data.messages || []; setConversation(prevConversation => JSON.stringify(prevConversation) !== JSON.stringify(newConversation) ? newConversation : prevConversation ); if (newConversation.length > 0) { const lastMsg = newConversation[newConversation.length - 1]; const isAgentMessage = lastMsg.actor === "agent"; setLoading(!isAgentMessage); setDone(lastMsg.response.next === "done"); setLastMessage(prevLastMessage => !prevLastMessage || lastMsg.response.response !== prevLastMessage.response.response ? lastMsg : prevLastMessage ); } else { setLoading(false); setDone(true); setLastMessage(null); } // Successfully fetched data, clear any persistent errors clearErrorOnSuccess(); } catch (err) { handleError(err, "fetching conversation"); } }, [handleError, clearErrorOnSuccess]); // Setup polling with cleanup useEffect(() => { pollingRef.current = setInterval(fetchConversationHistory, POLL_INTERVAL); return () => clearInterval(pollingRef.current); }, [fetchConversationHistory]); const scrollToBottom = useCallback(() => { if (containerRef.current) { if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } scrollTimeoutRef.current = setTimeout(() => { const element = containerRef.current; element.scrollTop = element.scrollHeight; scrollTimeoutRef.current = null; }, 100); } }, []); const handleContentChange = useCallback(() => { scrollToBottom(); }, [scrollToBottom]); useEffect(() => { if (lastMessage) { scrollToBottom(); } }, [lastMessage, scrollToBottom]); useEffect(() => { if (inputRef.current && !loading && !done) { inputRef.current.focus(); } return () => { if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } }; }, [loading, done]); const handleSendMessage = async () => { const trimmedInput = userInput.trim(); if (!trimmedInput) return; try { setLoading(true); setError(INITIAL_ERROR_STATE); await apiService.sendMessage(trimmedInput); setUserInput(""); } catch (err) { handleError(err, "sending message"); setLoading(false); } }; const handleConfirm = async () => { try { setLoading(true); setError(INITIAL_ERROR_STATE); await apiService.confirm(); } catch (err) { handleError(err, "confirming action"); setLoading(false); } }; const handleStartNewChat = async () => { try { setError(INITIAL_ERROR_STATE); setLoading(true); await apiService.startWorkflow(); setConversation([]); setLastMessage(null); } catch (err) { handleError(err, "starting new chat"); } finally { setLoading(false); } }; return (