'use client'; import { useCallback, useEffect, useState } from 'react'; import SettingsModal, { useSettings } from '@/components/SettingsModal'; import { ChatHeader } from '@/components/chat/ChatHeader'; import { ChatInput } from '@/components/chat/ChatInput'; import { ChatMessages } from '@/components/chat/ChatMessages'; import { ErrorBanner } from '@/components/chat/ErrorBanner'; import { useAutoScroll } from '@/components/chat/useAutoScroll'; import type { ChatBubble } from '@/components/chat/types'; const POLL_INTERVAL_MS = 1500; const formatEscapeCharacters = (text: string): string => { return text .replace(/\\n/g, '\n') .replace(/\\t/g, '\t') .replace(/\\r/g, '\r') .replace(/\\\\/g, '\\'); }; const isRenderableMessage = (entry: any) => typeof entry?.role === 'string' && typeof entry?.content === 'string' && entry.content.trim().length > 0; const toBubbles = (payload: any): ChatBubble[] => { if (!Array.isArray(payload?.messages)) return []; return payload.messages .filter(isRenderableMessage) .map((message: any, index: number) => ({ id: `history-${index}`, role: message.role, text: formatEscapeCharacters(message.content), })); }; export default function Page() { const { settings, setSettings } = useSettings(); const [open, setOpen] = useState(false); const [input, setInput] = useState(''); const [messages, setMessages] = useState([]); const [error, setError] = useState(null); const [isWaitingForResponse, setIsWaitingForResponse] = useState(false); const { scrollContainerRef, handleScroll } = useAutoScroll({ items: messages, isWaiting: isWaitingForResponse, }); const openSettings = useCallback(() => setOpen(true), [setOpen]); const closeSettings = useCallback(() => setOpen(false), [setOpen]); const loadHistory = useCallback(async () => { try { const res = await fetch('/api/chat/history', { cache: 'no-store' }); if (!res.ok) return; const data = await res.json(); setMessages(toBubbles(data)); } catch (err: any) { if (err?.name === 'AbortError') return; console.error('Failed to load chat history', err); } }, []); useEffect(() => { void loadHistory(); }, [loadHistory]); // Detect and store browser timezone on first load useEffect(() => { const detectAndStoreTimezone = async () => { // Only run if timezone not already stored if (settings.timezone) return; try { const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // Send to server const response = await fetch('/api/timezone', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ timezone: browserTimezone }), }); if (response.ok) { // Update local settings setSettings({ ...settings, timezone: browserTimezone }); } } catch (error) { // Fail silently - timezone detection is not critical console.debug('Timezone detection failed:', error); } }; void detectAndStoreTimezone(); }, [settings, setSettings]); useEffect(() => { const intervalId = window.setInterval(() => { void loadHistory(); }, POLL_INTERVAL_MS); return () => window.clearInterval(intervalId); }, [loadHistory]); const canSubmit = input.trim().length > 0; const inputPlaceholder = 'Type a message…'; const sendMessage = useCallback( async (text: string) => { const trimmed = text.trim(); if (!trimmed) return; setError(null); setIsWaitingForResponse(true); // Optimistically add the user message immediately const userMessage: ChatBubble = { id: `user-${Date.now()}`, role: 'user', text: formatEscapeCharacters(trimmed), }; setMessages(prev => { const newMessages = [...prev, userMessage]; return newMessages; }); try { const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: trimmed }], }), }); if (!(res.ok || res.status === 202)) { const detail = await res.text(); throw new Error(detail || `Request failed (${res.status})`); } } catch (err: any) { console.error('Failed to send message', err); setError(err?.message || 'Failed to send message'); // Remove the optimistic message on error setMessages(prev => prev.filter(msg => msg.id !== userMessage.id)); setIsWaitingForResponse(false); throw err instanceof Error ? err : new Error('Failed to send message'); } finally { // Poll until we get the assistant's response let pollAttempts = 0; const maxPollAttempts = 30; // Max 30 attempts (30 seconds) const pollForAssistantResponse = async () => { pollAttempts++; try { const res = await fetch('/api/chat/history', { cache: 'no-store' }); if (res.ok) { const data = await res.json(); const currentMessages = toBubbles(data); // Check if the last message is from assistant and contains our user message const lastMessage = currentMessages[currentMessages.length - 1]; const hasUserMessage = currentMessages.some(msg => msg.text === trimmed && msg.role === 'user'); const hasAssistantResponse = lastMessage?.role === 'assistant' && hasUserMessage; if (hasAssistantResponse) { // We got the assistant response, update messages and stop loading setMessages(currentMessages); setIsWaitingForResponse(false); return; } } } catch (err) { console.error('Error polling for response:', err); } // Continue polling if we haven't exceeded max attempts if (pollAttempts < maxPollAttempts) { setTimeout(pollForAssistantResponse, 1000); // Poll every second } else { // Timeout - stop loading and update messages anyway setIsWaitingForResponse(false); await loadHistory(); } }; // Start polling after a brief delay setTimeout(pollForAssistantResponse, 1000); } }, [loadHistory], ); const handleClearHistory = useCallback(async () => { try { const res = await fetch('/api/chat/history', { method: 'DELETE' }); if (!res.ok) { console.error('Failed to clear chat history', res.statusText); return; } setMessages([]); } catch (err) { console.error('Failed to clear chat history', err); } }, [setMessages]); const triggerClearHistory = useCallback(() => { void handleClearHistory(); }, [handleClearHistory]); const handleSubmit = useCallback(async () => { if (!canSubmit) return; const value = input; setInput(''); try { await sendMessage(value); } catch { setInput(value); } }, [canSubmit, input, sendMessage, setInput]); const handleInputChange = useCallback((value: string) => { setInput(value); }, [setInput]); const clearError = useCallback(() => setError(null), [setError]); return (
{error && }
); }