Guilherme34's picture
Upload folder using huggingface_hub
aa15bce verified
'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<ChatBubble[]>([]);
const [error, setError] = useState<string | null>(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 (
<main className="chat-bg min-h-screen p-4 sm:p-6">
<div className="chat-wrap flex flex-col">
<ChatHeader onOpenSettings={openSettings} onClearHistory={triggerClearHistory} />
<div className="card flex-1 overflow-hidden">
<ChatMessages
messages={messages}
isWaitingForResponse={isWaitingForResponse}
scrollContainerRef={scrollContainerRef}
onScroll={handleScroll}
/>
<div className="border-t border-gray-200 p-3">
{error && <ErrorBanner message={error} onDismiss={clearError} />}
<ChatInput
value={input}
canSubmit={canSubmit}
placeholder={inputPlaceholder}
onChange={handleInputChange}
onSubmit={handleSubmit}
/>
</div>
</div>
<SettingsModal open={open} onClose={closeSettings} settings={settings} onSave={setSettings} />
</div>
</main>
);
}