/** * Enhanced Chat Interface - Similar to chat.z.ai * Features: Better input, chat history, session resumption, smooth animations */ // ============================================ // Enhanced Chat Input Experience // ============================================ // Auto-focus chat input when switching to chat view function focusChatInput() { setTimeout(() => { const input = document.getElementById('chat-input'); if (input) { input.focus(); // Move cursor to end input.setSelectionRange(input.value.length, input.value.length); } }, 100); } // Smooth textarea resize with animation function enhanceChatInput() { const input = document.getElementById('chat-input'); if (!input) return; // Auto-resize with smooth transition input.style.transition = 'height 0.2s ease'; input.addEventListener('input', function() { this.style.height = 'auto'; const newHeight = Math.min(this.scrollHeight, 200); this.style.height = newHeight + 'px'; }); // Focus animation input.addEventListener('focus', function() { this.parentElement.classList.add('input-focused'); }); input.addEventListener('blur', function() { this.parentElement.classList.remove('input-focused'); }); } // ============================================ // Chat History & Session Management // ============================================ // Auto-load chat history when page loads // Make this a named function so it can be called to refresh the sidebar // @param {Array} sessionsToRender - Optional: specific sessions to render (for project filtering) async function loadChatHistory(sessionsToRender = null) { try { const historyList = document.getElementById('chat-history-list'); if (!historyList) return; // Skip update if we're showing "Loading session..." to avoid conflicts // with attachToSession's loading state if (historyList.textContent.includes('Loading session')) { console.log('[loadChatHistory] Skipping update - showing loading state'); return; } let allSessions; if (sessionsToRender) { // Use provided sessions (for project filtering) allSessions = sessionsToRender; console.log('[loadChatHistory] Rendering provided sessions:', allSessions.length); } else { // CRITICAL FIX: If no sessions provided, check if there's an active project // If there is, we should NOT fetch from API - instead wait for project to provide sessions if (window.projectManager && window.projectManager.activeProjectId) { const activeProject = window.projectManager.projects.get( window.projectManager.activeProjectId.replace('project-', '') ); if (activeProject) { // Use the active project's sessions allSessions = activeProject.sessions || []; console.log('[loadChatHistory] Using active project sessions:', allSessions.length, 'project:', activeProject.name); } else { // No project found, fetch from API as fallback const res = await fetch('/claude/api/claude/sessions'); const data = await res.json(); allSessions = [ ...(data.active || []).map(s => ({...s, status: 'active'})), ...(data.historical || []).map(s => ({...s, status: 'historical'})) ]; console.log('[loadChatHistory] No active project found, loaded from API:', allSessions.length); } } else { // No project manager or no active project, fetch all sessions const res = await fetch('/claude/api/claude/sessions'); const data = await res.json(); allSessions = [ ...(data.active || []).map(s => ({...s, status: 'active'})), ...(data.historical || []).map(s => ({...s, status: 'historical'})) ]; console.log('[loadChatHistory] No active project, loaded all sessions from API:', allSessions.length); } } // Sort by creation date (newest first) allSessions.sort((a, b) => new Date(b.createdAt || b.created_at) - new Date(a.createdAt || a.created_at)); // CRITICAL DEBUG: Log session details for debugging console.log('[loadChatHistory] Total sessions to render:', allSessions.length); allSessions.forEach((s, i) => { console.log(`[loadChatHistory] Session ${i}:`, { id: s.id.substring(0, 8), workingDir: s.workingDir, project: s.metadata?.project, status: s.status }); }); if (allSessions.length === 0) { historyList.innerHTML = '
No sessions in this project
'; return; } historyList.innerHTML = allSessions.map(session => { const title = session.metadata?.project || session.project || session.id.substring(0, 12) + '...'; const date = new Date(session.createdAt || session.created_at).toLocaleDateString(); const isActive = session.id === (window.attachedSessionId || null); return `
${session.status === 'historical' ? '📁' : 'đŸ’Ŧ'}
${title}
${date} ${session.status === 'historical' ? 'Historical' : 'Active'}
${session.status === 'historical' ? 'Resume' : ''}
`; }).join(''); // CRITICAL FIX: Also update session tabs with the same sessions if (window.sessionTabs && typeof window.sessionTabs.setSessions === 'function') { window.sessionTabs.setSessions(allSessions); console.log('[loadChatHistory] Updated session tabs with', allSessions.length, 'sessions'); } } catch (error) { console.error('[loadChatHistory] Error loading chat history:', error); } } // Auto-load chat history when page loads if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', loadChatHistory); } else { // DOM already loaded, load immediately loadChatHistory(); } // Resume historical session async function resumeSession(sessionId) { console.log('Resuming historical session:', sessionId); // Show loading message if (typeof appendSystemMessage === 'function') { appendSystemMessage('📂 Loading historical session...'); } try { // Load the historical session const res = await fetch('/claude/api/claude/sessions/' + sessionId); // Check if response is OK if (!res.ok) { const errorText = await res.text(); console.error('Session fetch error:', res.status, errorText); // Handle 404 - session not found if (res.status === 404) { if (typeof appendSystemMessage === 'function') { appendSystemMessage('❌ Session not found. It may have been deleted or the ID is incorrect.'); } return; } throw new Error(`HTTP ${res.status}: ${errorText}`); } // Parse JSON with error handling let data; try { data = await res.json(); } catch (jsonError) { const responseText = await res.text(); console.error('JSON parse error:', jsonError); console.error('Response text:', responseText); throw new Error('Invalid JSON response from server'); } // CRITICAL FIX: API returns session directly, not wrapped in {session: ...} const session = data.session || data; if (session && session.id) { if (typeof attachToSession === 'function') { attachToSession(sessionId); } // Update UI const sessionIdEl = document.getElementById('current-session-id'); if (sessionIdEl) sessionIdEl.textContent = sessionId; // Load session messages if (typeof clearChatDisplay === 'function') { clearChatDisplay(); } // Add historical messages if (session.outputBuffer && session.outputBuffer.length > 0) { session.outputBuffer.forEach(entry => { if (typeof appendMessage === 'function') { appendMessage('assistant', entry.content, false); } }); } // Show resume message const sessionDate = new Date(session.createdAt || session.created_at); if (typeof appendSystemMessage === 'function') { appendSystemMessage('✅ Resumed historical session from ' + sessionDate.toLocaleString()); appendSystemMessage('â„šī¸ This is a read-only historical session. Start a new chat to continue working.'); } // Update active state in sidebar if (typeof loadChatHistory === 'function') { loadChatHistory(); } // Subscribe to session (for any future updates) if (typeof subscribeToSession === 'function') { subscribeToSession(sessionId); } } else { throw new Error('No session data in response'); } } catch (error) { console.error('Error resuming session:', error); if (typeof appendSystemMessage === 'function') { appendSystemMessage('❌ Failed to resume session: ' + error.message); // Remove the loading message const messagesContainer = document.getElementById('chat-messages'); if (messagesContainer) { const loadingMessages = messagesContainer.querySelectorAll('.chat-system'); loadingMessages.forEach(msg => { if (msg.textContent.includes('Loading historical session')) { msg.remove(); } }); } } } } // ============================================ // Enhanced Message Rendering // ============================================ // Enhanced append with animations function appendMessageWithAnimation(role, content, animate = true) { const messagesContainer = document.getElementById('chat-messages'); if (!messagesContainer) return; const messageDiv = document.createElement('div'); messageDiv.className = `chat-message chat-message-${role} ${animate ? 'message-appear' : ''}`; const avatar = role === 'user' ? '👤' : '🤖'; const label = role === 'user' ? 'You' : 'Claude'; // Strip dyad tags for display const displayContent = stripDyadTags(content); messageDiv.innerHTML = `
${avatar}
${label} ${new Date().toLocaleTimeString()}
${formatMessageText(displayContent)}
`; messagesContainer.appendChild(messageDiv); // Scroll to bottom messagesContainer.scrollTop = messagesContainer.scrollHeight; // Update token usage if (typeof updateTokenUsage === 'function') { updateTokenUsage(content.length); } } // Strip dyad tags from message for display function stripDyadTags(content) { let stripped = content; // Remove dyad-write tags and replace with placeholder stripped = stripped.replace(/([\s\S]*?)<\/dyad-write>/g, (match, content) => { return `
📄 Code generated
${escapeHtml(content.trim())}
`; }); // Remove other dyad tags stripped = stripped.replace(/]+>/g, (match) => { const tagType = match.match(/dyad-(\w+)/)?.[1] || 'operation'; const icons = { 'rename': 'âœī¸', 'delete': 'đŸ—‘ī¸', 'add-dependency': 'đŸ“Ļ', 'command': '⚡' }; return `${icons[tagType] || 'âš™ī¸'} ${tagType}`; }); return stripped; } // Format message text with markdown-like rendering function formatMessageText(text) { // Basic markdown-like formatting let formatted = escapeHtml(text); // Code blocks formatted = formatted.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { return `
${escapeHtml(code.trim())}
`; }); // Inline code formatted = formatted.replace(/`([^`]+)`/g, '$1'); // Bold formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '$1'); // Links formatted = formatted.replace(/https?:\/\/[^\s]+/g, '$&'); // Line breaks formatted = formatted.replace(/\n/g, '
'); return formatted; } // Escape HTML function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ============================================ // Quick Actions & Suggestions // ============================================ // Show quick action suggestions function showQuickActions() { const messagesContainer = document.getElementById('chat-messages'); if (!messagesContainer) return; const quickActions = document.createElement('div'); quickActions.className = 'quick-actions'; quickActions.innerHTML = `
💡 Quick Actions
`; messagesContainer.appendChild(quickActions); } // Execute quick action function executeQuickAction(action) { const actions = { 'create-react': 'Create a React app with components and routing', 'create-nextjs': 'Create a Next.js app with server-side rendering', 'create-vue': 'Create a Vue 3 app with composition API', 'create-html': 'Create a responsive HTML5 page with modern styling', 'explain-code': 'Explain the codebase structure and main files', 'fix-bug': 'Help me fix a bug in my code' }; const prompt = actions[action]; if (prompt) { const input = document.getElementById('chat-input'); if (input) { input.value = prompt; input.focus(); // Auto-send after short delay setTimeout(() => { if (typeof sendChatMessage === 'function') { sendChatMessage(); } }, 300); } } } // ============================================ // Enhanced Chat View Loading // ============================================ // Hook into loadChatView to add enhancements document.addEventListener('DOMContentLoaded', () => { // Wait for chat-functions.js to load setTimeout(() => { // Override loadChatView to add enhancements if (typeof window.loadChatView === 'function') { const originalLoadChatView = window.loadChatView; window.loadChatView = async function() { // Call original function first await originalLoadChatView.call(this); // Add our enhancements setTimeout(() => { enhanceChatInput(); focusChatInput(); // Show quick actions on first load const messagesContainer = document.getElementById('chat-messages'); if (messagesContainer && messagesContainer.querySelector('.chat-welcome')) { showQuickActions(); } }, 100); }; } }, 1000); }); // Auto-start enhancements when chat view is active const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.target.id === 'chat-view' && mutation.target.classList.contains('active')) { enhanceChatInput(); focusChatInput(); } }); }); // Start observing after DOM loads if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { const chatView = document.getElementById('chat-view'); if (chatView) observer.observe(chatView, { attributes: true }); }, 1500); }); } else { setTimeout(() => { const chatView = document.getElementById('chat-view'); if (chatView) observer.observe(chatView, { attributes: true }); }, 1500); } // ============================================ // Archive & Merge Sessions // ============================================ // Track selected sessions for merge window.selectedSessionsForMerge = new Set(); /** * Archive a session */ async function archiveSession(sessionId) { console.log('[Archive] Archiving session:', sessionId); const confirmed = confirm('Archive this session? It will be hidden from the main list but can be unarchived later.'); if (!confirmed) return; try { const res = await fetch(`/claude/api/claude/sessions/${sessionId}/archive`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' } }); if (!res.ok) { // Try to get more error details let errorMessage = 'Failed to archive session'; try { const errorData = await res.json(); errorMessage = errorData.message || errorData.error || errorMessage; } catch (e) { errorMessage = `HTTP ${res.status}: ${res.statusText}`; } throw new Error(errorMessage); } // Refresh the session list if (typeof loadChatHistory === 'function') { await loadChatHistory(); } // Show success message if (typeof appendSystemMessage === 'function') { appendSystemMessage('✅ Session archived successfully'); } } catch (error) { console.error('[Archive] Error:', error); if (typeof appendSystemMessage === 'function') { appendSystemMessage('❌ Failed to archive session: ' + error.message); } } } /** * Toggle session selection for merge */ function toggleSessionSelection(sessionId) { if (window.selectedSessionsForMerge.has(sessionId)) { window.selectedSessionsForMerge.delete(sessionId); } else { window.selectedSessionsForMerge.add(sessionId); } // Update UI const item = document.querySelector(`[data-session-id="${sessionId}"]`); if (item) { item.classList.toggle('selected-for-merge', window.selectedSessionsForMerge.has(sessionId)); } // Show/hide merge button updateMergeButtonVisibility(); } /** * Update merge button visibility based on selection */ function updateMergeButtonVisibility() { const mergeBtn = document.getElementById('merge-sessions-btn'); if (!mergeBtn) return; if (window.selectedSessionsForMerge.size >= 2) { mergeBtn.style.display = 'flex'; mergeBtn.textContent = `🔀 Emerge ${window.selectedSessionsForMerge.size} Sessions`; } else { mergeBtn.style.display = 'none'; } } /** * Merge selected sessions */ async function mergeSessions() { const sessionIds = Array.from(window.selectedSessionsForMerge); if (sessionIds.length < 2) { alert('Please select at least 2 sessions to merge'); return; } console.log('[Merge] Merging sessions:', sessionIds); const confirmed = confirm(`Merge ${sessionIds.length} sessions into one? This will create a new session with all messages from the selected sessions.`); if (!confirmed) return; try { if (typeof appendSystemMessage === 'function') { appendSystemMessage('🔀 Merging sessions...'); } const res = await fetch('/claude/api/claude/sessions/merge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionIds }) }); if (!res.ok) throw new Error('Failed to merge sessions'); const data = await res.json(); if (data.success && data.session) { // Clear selection window.selectedSessionsForMerge.clear(); updateMergeButtonVisibility(); // Remove all selected classes document.querySelectorAll('.selected-for-merge').forEach(el => { el.classList.remove('selected-for-merge'); }); // Refresh the session list if (typeof loadChatHistory === 'function') { await loadChatHistory(); } // Attach to the new merged session if (typeof attachToSession === 'function') { await attachToSession(data.session.id); } if (typeof appendSystemMessage === 'function') { appendSystemMessage('✅ Sessions merged successfully!'); } } } catch (error) { console.error('[Merge] Error:', error); if (typeof appendSystemMessage === 'function') { appendSystemMessage('❌ Failed to merge sessions: ' + error.message); } } } /** * Show archived sessions view */ async function showArchivedSessions() { console.log('[Archive] Loading archived sessions...'); try { const res = await fetch('/claude/api/claude/sessions?archived=true'); const data = await res.json(); const archivedSessions = data.archived || []; const historyList = document.getElementById('chat-history-list'); if (!historyList) return; if (archivedSessions.length === 0) { historyList.innerHTML = `
No archived sessions
`; return; } // Update header to show back button const historyHeader = document.querySelector('.chat-history-header h3'); if (historyHeader) { historyHeader.innerHTML = ` Archived Sessions `; } historyList.innerHTML = archivedSessions.map(session => { const title = session.metadata?.project || session.workingDir?.split('/').pop() || session.id.substring(0, 12) + '...'; const archivedDate = new Date(session.archivedAt).toLocaleDateString(); return `
đŸ“Ļ
${title}
Archived: ${archivedDate}
`; }).join(''); } catch (error) { console.error('[Archive] Error loading archived sessions:', error); } } /** * Unarchive a session */ async function unarchiveSession(sessionId) { console.log('[Archive] Unarchiving session:', sessionId); try { const res = await fetch(`/claude/api/claude/sessions/${sessionId}/unarchive`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' } }); if (!res.ok) throw new Error('Failed to unarchive session'); // Refresh the archived list await showArchivedSessions(); // Show success message if (typeof appendSystemMessage === 'function') { appendSystemMessage('✅ Session unarchived successfully'); } } catch (error) { console.error('[Archive] Error:', error); if (typeof appendSystemMessage === 'function') { appendSystemMessage('❌ Failed to unarchive session: ' + error.message); } } } // Export functions if (typeof window !== 'undefined') { window.resumeSession = resumeSession; window.executeQuickAction = executeQuickAction; window.showQuickActions = showQuickActions; window.enhanceChatInput = enhanceChatInput; window.focusChatInput = focusChatInput; window.appendMessageWithAnimation = appendMessageWithAnimation; window.archiveSession = archiveSession; window.toggleSessionSelection = toggleSessionSelection; window.mergeSessions = mergeSessions; window.showArchivedSessions = showArchivedSessions; window.unarchiveSession = unarchiveSession; }