`).join('');
}
function formatDate(timestamp) {
const date = new Date(timestamp * 1000);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
return date.toLocaleDateString();
}
async function deleteDocument(docId) {
try {
const response = await fetch(`/api/documents/${docId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${state.token}` } });
if (response.ok) {
state.documents = state.documents.filter(d => d.doc_id !== docId);
// Clear selection if deleted doc was selected
if (state.selectedDocument === docId) {
state.selectedDocument = null;
hideSummary();
}
// Remove from summaries cache
delete state.summaries[docId];
renderDocuments();
loadBuckets();
showToast('Document deleted', 'success');
}
} catch (error) { showToast('Failed to delete', 'error'); }
}
// ==================== Document Summary ====================
function selectDocument(docId) {
state.selectedDocument = docId;
renderDocuments();
displaySummary(docId);
}
async function displaySummary(docId) {
const doc = state.documents.find(d => d.doc_id === docId);
if (!doc) return;
// Check if summary is cached
if (state.summaries[docId]) {
showSummaryPanel(doc.filename, state.summaries[docId].summary);
} else {
// Show loading state
showSummaryPanel(doc.filename, 'Generating summary...');
// Fetch summary from server
await fetchSummary(docId);
}
}
async function fetchSummary(docId) {
try {
const response = await fetch(`/api/documents/${docId}/summary`, {
headers: { 'Authorization': `Bearer ${state.token}` }
});
const data = await response.json();
if (response.ok && data.summary) {
// Cache the summary
state.summaries[docId] = {
summary: data.summary,
filename: data.filename
};
// Update display if still selected
if (state.selectedDocument === docId) {
showSummaryPanel(data.filename, data.summary);
}
} else {
// Show error state
if (state.selectedDocument === docId) {
showSummaryPanel(data.filename || 'Document', 'Unable to generate summary.');
}
}
} catch (error) {
console.error('Failed to fetch summary:', error);
if (state.selectedDocument === docId) {
const doc = state.documents.find(d => d.doc_id === docId);
showSummaryPanel(doc?.filename || 'Document', 'Failed to load summary.');
}
}
}
function showSummaryPanel(filename, summaryText) {
elements.summaryPanel.classList.remove('hidden');
elements.summaryTitle.textContent = filename;
elements.summaryText.textContent = summaryText;
}
function hideSummary() {
elements.summaryPanel.classList.add('hidden');
state.selectedDocument = null;
renderDocuments();
}
function initSummaryPanel() {
elements.summaryClose.addEventListener('click', hideSummary);
}
// ==================== Document Viewer ====================
async function viewDocument(docId, filename) {
try {
// Fetch the document with proper authorization
const response = await fetch(`/api/documents/${docId}/view`, {
headers: { 'Authorization': `Bearer ${state.token}` }
});
if (!response.ok) {
showToast('Failed to load document', 'error');
return;
}
// Get the blob and create a URL
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
// Open in a new tab
window.open(blobUrl, '_blank');
} catch (error) {
console.error('Failed to view document:', error);
showToast('Failed to open document', 'error');
}
}
elements.closeDocViewer.addEventListener('click', () => elements.docViewerModal.classList.remove('active'));
// ==================== Upload ====================
let currentPollInterval = null; // Track the current polling interval for cancellation
function initUpload() {
elements.uploadZone.addEventListener('click', () => elements.fileInput.click());
elements.fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) uploadFiles(Array.from(e.target.files));
});
elements.uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); elements.uploadZone.classList.add('dragover'); });
elements.uploadZone.addEventListener('dragleave', () => elements.uploadZone.classList.remove('dragover'));
elements.uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
elements.uploadZone.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) uploadFiles(Array.from(e.dataTransfer.files));
});
// Cancel upload button
elements.cancelUploadBtn.addEventListener('click', cancelUpload);
}
function cancelUpload() {
state.uploadCancelled = true;
// Abort any ongoing fetch request
if (state.currentUploadAbortController) {
state.currentUploadAbortController.abort();
state.currentUploadAbortController = null;
}
// Clear any polling interval
if (currentPollInterval) {
clearInterval(currentPollInterval);
currentPollInterval = null;
}
// Reset UI
elements.uploadProgress.classList.add('hidden');
elements.uploadZone.style.pointerEvents = '';
elements.fileInput.value = '';
elements.progressFill.style.width = '0%';
showToast('Upload cancelled', 'info');
}
async function uploadFiles(files) {
// Reset cancellation state
state.uploadCancelled = false;
elements.uploadProgress.classList.remove('hidden');
elements.uploadZone.style.pointerEvents = 'none';
const bucketId = elements.uploadBucketSelect.value;
let completed = 0;
// Process files sequentially to avoid overwhelming the client,
// but the server handles them in background.
for (const file of files) {
// Check if cancelled before processing each file
if (state.uploadCancelled) {
break;
}
elements.uploadStatus.textContent = `Uploading ${file.name}...`;
elements.progressFill.style.width = '10%'; // Initial progress
const formData = new FormData();
formData.append('file', file);
formData.append('bucket_id', bucketId);
// Create abort controller for this request
state.currentUploadAbortController = new AbortController();
try {
// Initial upload request
const response = await fetch('/api/documents/upload', {
method: 'POST',
headers: { 'Authorization': `Bearer ${state.token}` },
body: formData,
signal: state.currentUploadAbortController.signal
});
if (response.status === 202) {
// Async processing started
const data = await response.json();
await pollUploadStatus(data.doc_id, file.name);
if (!state.uploadCancelled) {
completed++;
}
} else if (response.ok) {
// Instant completion (legacy or small file)
const data = await response.json();
handleUploadSuccess(data);
completed++;
} else {
const data = await response.json();
showToast(`Failed: ${file.name} - ${data.error}`, 'error');
}
} catch (e) {
if (e.name === 'AbortError') {
// Upload was cancelled by user
break;
}
console.error(e);
showToast(`Failed to upload ${file.name}`, 'error');
}
}
// Clean up abort controller
state.currentUploadAbortController = null;
// Only update UI if not cancelled (cancelUpload already handles UI reset)
if (!state.uploadCancelled) {
elements.uploadProgress.classList.add('hidden');
elements.uploadZone.style.pointerEvents = '';
elements.fileInput.value = '';
elements.progressFill.style.width = '0%';
// Load documents first, then show summary
await loadDocuments();
loadBuckets();
}
}
async function pollUploadStatus(docId, filename) {
return new Promise((resolve, reject) => {
currentPollInterval = setInterval(async () => {
// Check if cancelled
if (state.uploadCancelled) {
clearInterval(currentPollInterval);
currentPollInterval = null;
resolve();
return;
}
try {
const response = await fetch(`/api/documents/${docId}/status`, {
headers: { 'Authorization': `Bearer ${state.token}` }
});
if (response.ok) {
const statusData = await response.json();
// Update UI
elements.uploadStatus.textContent = `Processing ${filename}: ${statusData.message || '...'}`;
// Map 0-100 progress to UI width (keeping 10% buffer)
if (statusData.progress) {
elements.progressFill.style.width = `${Math.max(10, statusData.progress)}%`;
}
if (statusData.status === 'completed') {
clearInterval(currentPollInterval);
currentPollInterval = null;
if (statusData.result) {
handleUploadSuccess(statusData.result);
}
resolve();
} else if (statusData.status === 'failed') {
clearInterval(currentPollInterval);
currentPollInterval = null;
showToast(`Processing failed: ${filename} - ${statusData.error}`, 'error');
resolve(); // Resolve anyway to continue with next file
}
} else {
// Status check failed - might be network glitch, ignore once
}
} catch (e) {
console.error("Polling error", e);
// Continue polling despite error
}
}, 2000); // Check every 2 seconds
});
}
function handleUploadSuccess(data) {
showToast(`Ready: ${data.filename}`, 'success');
// Cache the summary
if (data.summary) {
state.summaries[data.doc_id] = {
summary: data.summary,
filename: data.filename
};
}
// Auto-display this document
state.selectedDocument = data.doc_id;
// We will re-render documents shortly after this returns
if (data.summary) {
// Defer slightly to ensure DOM is ready if needed
setTimeout(() => {
showSummaryPanel(data.filename, data.summary);
}, 500);
}
}
// ==================== Chat ====================
function initChat() {
elements.chatInput.addEventListener('input', () => {
elements.chatInput.style.height = 'auto';
elements.chatInput.style.height = Math.min(elements.chatInput.scrollHeight, 150) + 'px';
elements.sendBtn.disabled = !elements.chatInput.value.trim();
});
elements.chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
});
elements.sendBtn.addEventListener('click', sendMessage);
// Stop generation button
elements.stopBtn.addEventListener('click', stopGeneration);
}
function stopGeneration() {
if (state.streamAbortController) {
state.streamAbortController.abort();
state.streamAbortController = null;
}
// Hide stop button, show send button
elements.stopBtn.classList.add('hidden');
elements.sendBtn.classList.remove('hidden');
elements.typingIndicator.classList.add('hidden');
state.isLoading = false;
// Add a note that generation was stopped
if (state.messages.length > 0) {
const lastMsg = state.messages[state.messages.length - 1];
if (lastMsg.role === 'assistant' && lastMsg.content) {
lastMsg.content += '\n\n*[Generation stopped]*';
renderMessages();
saveCurrentChat();
}
}
showToast('Generation stopped', 'info');
}
async function sendMessage() {
const message = elements.chatInput.value.trim();
if (!message || state.isLoading) return;
elements.chatInput.value = '';
elements.chatInput.style.height = 'auto';
elements.sendBtn.disabled = true;
elements.welcomeScreen.classList.add('hidden');
// Create a chat ID if this is the first message
if (state.messages.length === 0 && !state.currentChatId) {
state.currentChatId = Date.now().toString();
}
const targetChatId = state.currentChatId;
addMessage('user', message);
elements.typingIndicator.classList.remove('hidden');
state.isLoading = true;
scrollToBottom();
// Show stop button, hide send button
elements.sendBtn.classList.add('hidden');
elements.stopBtn.classList.remove('hidden');
// Create abort controller for this request
state.streamAbortController = new AbortController();
try {
// Use streaming endpoint for instant response
const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: {
'Authorization': `Bearer ${state.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
bucket_id: state.chatBucket || null,
chat_id: state.currentChatId
}),
signal: state.streamAbortController.signal
});
if (!response.ok) {
throw new Error('Stream request failed');
}
elements.typingIndicator.classList.add('hidden');
// Create a placeholder message for streaming
let streamingContent = '';
let sources = [];
// Add empty assistant message and get reference to its content element
state.messages.push({ role: 'assistant', content: '', sources: [] });
renderMessages();
scrollToBottom();
// Get direct reference to the streaming message element for fast updates
const messageElements = elements.chatMessages.querySelectorAll('.message.assistant .message-content');
const streamingElement = messageElements[messageElements.length - 1];
const reader = response.body.getReader();
const decoder = new TextDecoder();
// Throttle DOM updates for smooth rendering (update every 50ms max)
let lastUpdateTime = 0;
let pendingUpdate = false;
const UPDATE_INTERVAL = 50; // ms
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.type === 'sources') {
sources = data.sources || [];
} else if (data.type === 'chunk' || data.type === 'content') {
// Support both 'chunk' (legacy) and 'content' (specialized queries)
streamingContent += data.content;
// Update state for saving later
state.messages[state.messages.length - 1].content = streamingContent;
state.messages[state.messages.length - 1].sources = sources;
// Throttled DOM update for smooth rendering
const now = Date.now();
if (now - lastUpdateTime >= UPDATE_INTERVAL) {
if (streamingElement) {
streamingElement.innerHTML = formatContent(streamingContent);
}
lastUpdateTime = now;
pendingUpdate = false;
} else {
pendingUpdate = true;
}
// No auto-scroll during streaming - stay at current position
} else if (data.type === 'done') {
// Final update with any pending content
if (pendingUpdate && streamingElement) {
streamingElement.innerHTML = formatContent(streamingContent);
}
// Streaming complete - do final render for proper formatting
renderMessages();
saveCurrentChat();
// No auto-scroll - user stays at current position
} else if (data.type === 'error') {
state.messages[state.messages.length - 1].content = data.content || 'Error generating response';
renderMessages();
}
} catch (e) {
// Skip malformed JSON
}
}
}
}
} catch (err) {
elements.typingIndicator.classList.add('hidden');
// Only show error if not aborted by user
if (err.name !== 'AbortError') {
addMessageToChat(targetChatId, 'assistant', 'Connection error. Please try again.');
}
}
// Cleanup: hide stop button, show send button
elements.stopBtn.classList.add('hidden');
elements.sendBtn.classList.remove('hidden');
state.streamAbortController = null;
state.isLoading = false;
// No auto-scroll - user stays at current position
}
function addMessage(role, content, sources = []) {
// Create a new chat ID if this is the first message
if (state.messages.length === 0 && !state.currentChatId) {
state.currentChatId = Date.now().toString();
}
state.messages.push({ role, content, sources });
renderMessages();
// Auto-save after assistant responds (complete exchange)
if (role === 'assistant') {
saveCurrentChat();
}
}
// Add message to a specific chat (handles case where user switched chats during loading)
function addMessageToChat(chatId, role, content, sources = []) {
// If this is the current chat, add directly
if (chatId === state.currentChatId) {
state.messages.push({ role, content, sources });
renderMessages();
saveCurrentChat();
} else {
// Add to the chat in history
const chatIndex = state.chatHistory.findIndex(c => c.id === chatId);
if (chatIndex >= 0) {
state.chatHistory[chatIndex].messages.push({ role, content, sources });
saveChatHistory();
syncChatToServer(state.chatHistory[chatIndex]);
renderChatHistory();
showToast('Response added to previous chat', 'info');
}
}
}
function renderMessages() {
// Preserve summary panel state before re-rendering
const summaryVisible = !elements.summaryPanel.classList.contains('hidden');
const summaryTitle = elements.summaryTitle.textContent;
const summaryText = elements.summaryText.textContent;
if (state.messages.length === 0) {
// Clear chat messages and show welcome screen
elements.chatMessages.innerHTML = '';
elements.welcomeScreen.classList.remove('hidden');
elements.chatMessages.appendChild(elements.welcomeScreen);
// Re-show summary if it was visible
if (summaryVisible) {
elements.summaryPanel.classList.remove('hidden');
}
return;
}
elements.welcomeScreen.classList.add('hidden');
const html = state.messages.map((msg, i) => {
const avatar = msg.role === 'user' ? (state.user?.username?.charAt(0).toUpperCase() || 'U') : '🧠';
const isUserMessage = msg.role === 'user';
return `
${avatar}
${formatContent(msg.content, isUserMessage)}
`;
}).join('');
// Build full content with summary panel and welcome screen
const summaryPanelHTML = `
📄${summaryTitle}
${summaryText}
`;
elements.chatMessages.innerHTML = summaryPanelHTML + html + elements.welcomeScreen.outerHTML;
document.getElementById('welcomeScreen')?.classList.add('hidden');
// Re-bind summary panel elements and event listener
elements.summaryPanel = document.getElementById('summaryPanel');
elements.summaryTitle = document.getElementById('summaryTitle');
elements.summaryText = document.getElementById('summaryText');
elements.summaryClose = document.getElementById('summaryClose');
elements.summaryClose.addEventListener('click', hideSummary);
}
function formatContent(content, isUserMessage = false) {
// Enhanced markdown parsing for beautiful formatting
let html = content;
// For user messages, escape HTML and preserve line breaks
if (isUserMessage) {
// Escape HTML to prevent XSS
html = html
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
// Convert line breaks to
html = html.replace(/\n/g, ' ');
return html;
}
// Escape HTML special characters first (except for already parsed markdown)
// Skip this if content looks like it's already HTML
if (!html.includes('
');
// Bold headers at start of line (NotebookLM style)
html = html.replace(/^(\*\*[^*]+\*\*):?\s*$/gm, '
$1
');
// Bold text: **text**
html = html.replace(/\*\*([^*]+)\*\*/g, '$1');
// Italic text: *text*
html = html.replace(/(?$1');
// Inline code: `code`
html = html.replace(/`([^`]+)`/g, '$1');
// Horizontal rule: --- or ***
html = html.replace(/^[-*]{3,}$/gm, '');
// Numbered lists: 1. Item, 2. Item, etc.
html = html.replace(/^(\d+)\.\s+(.+)$/gm, '
$1. $2
');
// Bullet points: • Item or - Item or * Item at start of line
html = html.replace(/^[\•\-\*]\s+(.+)$/gm, '
$1
');
// Sub-bullets with indentation (2+ spaces before bullet)
html = html.replace(/^[\s]{2,}[\•\-\*]\s+(.+)$/gm, '
$1
');
// Wrap consecutive numbered list items
html = html.replace(/(
[\s\S]*?<\/li>\n?)+/g, '$&');
// Wrap consecutive bullet items
html = html.replace(/(
[\s\S]*?<\/li>\n?)+/g, '
$&
');
// Wrap consecutive sub-bullet items
html = html.replace(/(
[\s\S]*?<\/li>\n?)+/g, '
$&
');
// Blockquotes: > text
html = html.replace(/^>\s+(.+)$/gm, '
$1
');
// Merge consecutive blockquotes
html = html.replace(/<\/blockquote>\n
/g, ' ');
// Double newlines become paragraph breaks
html = html.replace(/\n\n+/g, '
');
// Single newlines become line breaks (but not inside lists)
html = html.replace(/\n/g, ' ');
// Clean up br tags in lists, headers, tables
html = html.replace(/
/g, '
');
html = html.replace(/ /g, '');
html = html.replace(/
/g, '
');
html = html.replace(/<\/ol> /g, '');
html = html.replace(/
/g, '
');
html = html.replace(/
/g, '
');
html = html.replace(/
/g, '
');
html = html.replace(/ ]*> /g, '');
html = html.replace(/
/g, '
');
// Wrap in paragraph
html = '
' + html + '
';
// Clean up empty paragraphs
html = html.replace(/
<\/p>/g, '');
html = html.replace(/
(\s| )*<\/p>/g, '');
html = html.replace(/
<(h\d|ul|ol|table|div|pre|hr|blockquote)/g, '<$1');
html = html.replace(/<\/(h\d|ul|ol|table|div|pre|blockquote)><\/p>/g, '$1>');
html = html.replace(/
m.role === 'user');
if (firstUserMsg) {
// Truncate to first 40 chars
let topic = firstUserMsg.content.substring(0, 40);
if (firstUserMsg.content.length > 40) topic += '...';
return topic;
}
return 'New Conversation';
}
function saveChatHistory() {
localStorage.setItem('Iribl AI_chat_history', JSON.stringify(state.chatHistory));
}
// Sync chat to server
async function syncChatToServer(chatData) {
if (!state.token) return;
try {
await fetch('/api/chats', {
method: 'POST',
headers: {
'Authorization': `Bearer ${state.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(chatData)
});
} catch (error) {
console.error('Failed to sync chat to server:', error);
}
}
// Load chat history from server
async function loadChatHistoryFromServer() {
if (!state.token) return;
try {
const response = await fetch('/api/chats', {
headers: { 'Authorization': `Bearer ${state.token}` }
});
if (response.ok) {
const data = await response.json();
if (data.chats && data.chats.length > 0) {
// Merge server chats with local (server takes priority)
state.chatHistory = data.chats;
saveChatHistory(); // Update local storage
renderChatHistory();
}
}
} catch (error) {
console.error('Failed to load chats from server:', error);
}
}
// Delete chat from server
async function deleteChatFromServer(chatId) {
if (!state.token) return;
try {
await fetch(`/api/chats/${chatId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${state.token}` }
});
} catch (error) {
console.error('Failed to delete chat from server:', error);
}
}
function saveCurrentChat() {
// Only save if there are messages
if (state.messages.length === 0) return null;
const chatId = state.currentChatId || Date.now().toString();
const topic = generateChatTopic(state.messages);
// Check if this chat already exists
const existingIndex = state.chatHistory.findIndex(c => c.id === chatId);
const chatData = {
id: chatId,
topic: topic,
messages: [...state.messages],
timestamp: Date.now(),
bucket: state.chatBucket
};
if (existingIndex >= 0) {
// Update existing chat
state.chatHistory[existingIndex] = chatData;
} else {
// Add new chat at the beginning
state.chatHistory.unshift(chatData);
}
saveChatHistory();
renderChatHistory();
// Sync to server
syncChatToServer(chatData);
return chatId;
}
function startNewChat() {
// Warn if AI is still generating
if (state.isLoading) {
showToast('AI is still responding - response will go to current chat', 'info');
}
// Save current chat first if it has messages
if (state.messages.length > 0) {
saveCurrentChat();
}
// Clear current chat
state.messages = [];
state.currentChatId = null;
// Reset UI
renderMessages();
elements.welcomeScreen.classList.remove('hidden');
hideSummary();
renderChatHistory();
showToast('Started new chat', 'info');
}
function loadChatFromHistory(chatId) {
// Warn if AI is still generating
if (state.isLoading) {
showToast('AI is still responding - response will go to current chat', 'info');
}
// Save current chat first if it has messages
if (state.messages.length > 0 && state.currentChatId !== chatId) {
saveCurrentChat();
}
const chat = state.chatHistory.find(c => c.id === chatId);
if (!chat) return;
// Load the chat
state.messages = [...chat.messages];
state.currentChatId = chat.id;
state.chatBucket = chat.bucket || '';
// Update bucket dropdown
if (elements.chatBucketSelect) {
elements.chatBucketSelect.value = state.chatBucket;
const bucketName = state.chatBucket ?
state.buckets.find(b => b.bucket_id === state.chatBucket)?.name || 'Selected Bucket' :
'All Documents';
elements.chatBucketTrigger.querySelector('.select-value').textContent = bucketName;
}
// Render messages
renderMessages();
// Show/hide welcome screen based on whether chat has messages
if (state.messages.length === 0) {
elements.welcomeScreen.classList.remove('hidden');
} else {
elements.welcomeScreen.classList.add('hidden');
}
renderChatHistory();
scrollToBottom();
}
function deleteChatFromHistory(chatId) {
event.stopPropagation();
state.chatHistory = state.chatHistory.filter(c => c.id !== chatId);
// If deleting current chat, clear it
if (state.currentChatId === chatId) {
state.messages = [];
state.currentChatId = null;
renderMessages();
elements.welcomeScreen.classList.remove('hidden');
}
saveChatHistory();
renderChatHistory();
// Delete from server
deleteChatFromServer(chatId);
showToast('Chat deleted', 'success');
}
function renderChatHistory() {
// Filter chats by selected bucket
let filteredChats = state.chatHistory;
if (state.selectedBucket) {
filteredChats = state.chatHistory.filter(chat =>
chat.bucket === state.selectedBucket ||
// Also include chats with no bucket for backwards compatibility
(!chat.bucket && !state.selectedBucket)
);
}
const count = filteredChats.length;
const totalCount = state.chatHistory.length;
// Show filtered count vs total if filtering is active
elements.chatHistoryCount.textContent = state.selectedBucket && count !== totalCount ?
`(${count}/${totalCount})` : `(${totalCount})`;
if (count === 0) {
elements.chatHistoryList.innerHTML = state.selectedBucket ?
`
`;
}).join('');
}
function clearCurrentChat() {
// Warn if AI is still generating
if (state.isLoading) {
showToast('AI is still responding - response will go to current chat', 'info');
}
// If there's a current chat, clear its messages but keep it in history
if (state.currentChatId) {
const chatIndex = state.chatHistory.findIndex(c => c.id === state.currentChatId);
if (chatIndex >= 0) {
// Clear the messages in history
state.chatHistory[chatIndex].messages = [];
saveChatHistory();
// Sync cleared chat to server
syncChatToServer(state.chatHistory[chatIndex]);
}
}
// Clear current chat messages
state.messages = [];
// Reset UI
renderMessages();
elements.welcomeScreen.classList.remove('hidden');
hideSummary();
renderChatHistory();
showToast('Chat cleared', 'info');
}
function initChatHistory() {
// New Chat button handler
elements.newChatBtn.addEventListener('click', startNewChat);
// Clear Chat button handler (sidebar)
elements.clearChatBtn.addEventListener('click', (e) => {
e.stopPropagation();
clearCurrentChat();
});
// Clear Chat button handler (top)
elements.clearChatBtnTop.addEventListener('click', clearCurrentChat);
// Render existing history
renderChatHistory();
// Auto-save current chat when sending messages (hook into sendMessage)
// This is handled by updating currentChatId after first message
}
// ==================== Init ====================
function init() {
initSidebars();
initMobileNavigation();
initCollapsible();
initCustomDropdowns();
initUpload();
initChat();
initSummaryPanel();
initChatHistory();
verifyToken();
}
document.addEventListener('DOMContentLoaded', init);