/**
* 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}
${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 `
${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;
}