📜
chat_copy4.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// chat.js - AI Chat Interface with Scope Targeting and File Context (function() { console.log("[chat] Loading AI Chat interface..."); // --- Utility Functions --- function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function generateSessionId() { return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } // Get API configuration from Settings module function getApiConfig() { if (window.Settings) { const settings = window.Settings.get(); return { endpoint: window.Settings.getApiEndpoint(), defaultModel: settings.defaultModel, maxTokens: settings.maxTokens, temperature: settings.temperature }; } // Fallback if Settings not loaded return { endpoint: 'api.php', defaultModel: 'grok-code-fast-1', maxTokens: 2000, temperature: 0.7 }; } // Get API configuration from Settings module function getApiConfig() { if (window.Settings) { const settings = window.Settings.get(); return { endpoint: window.Settings.getApiEndpoint(), defaultModel: settings.defaultModel, maxTokens: settings.maxTokens, temperature: settings.temperature, chatPrompt: settings.chatPrompt, snippetsPrompt: settings.snippetsPrompt, fullCodePrompt: settings.fullCodePrompt, responseMode: settings.responseMode, currentMode: settings.currentMode }; } // Fallback if Settings not loaded return { endpoint: 'api.php', defaultModel: 'grok-code-fast-1', maxTokens: 2000, temperature: 0.7, chatPrompt: 'You are a helpful AI assistant for web development.', snippetsPrompt: 'You are an expert at writing small, focused code snippets. Provide concise, working code examples.', fullCodePrompt: 'You are an expert full-stack developer. Provide complete, production-ready code solutions.', responseMode: 'normal', currentMode: 'chat' }; } let currentModel = getApiConfig().defaultModel; let sessionId = generateSessionId(); // Listen for settings updates window.addEventListener('settingsUpdated', (e) => { const newSettings = e.detail; currentModel = newSettings.defaultModel; console.log('[chat] Settings updated, new default model:', currentModel); }); // --- API Call Function --- async function callAI(question, systemMessage, conversationHistory) { const config = getApiConfig(); const modelConfig = window.Settings ? window.Settings.getModelConfig(currentModel) : { provider: 'unknown', name: currentModel }; try { const response = await fetch(config.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Session-Id': sessionId, 'X-Username': 'user' // You can make this dynamic based on your auth system }, body: JSON.stringify({ question: question, model: currentModel, system: systemMessage, sessionId: sessionId, maxTokens: config.maxTokens, temperature: config.temperature }) }); if (!response.ok) { throw new Error('API request failed: ' + response.status); } const data = await response.json(); if (data.error) { throw new Error(data.error); } return { answer: data.answer || '(no response)', usage: data.usage, provider: data.provider, model: data.model }; } catch (error) { console.error('[chat] API Error:', error); throw error; } } // --- File Context Functions --- function getFileContext() { if (!window.FilesManager) { console.warn('[chat] FilesManager not available'); return { active: null, read: [] }; } const files = window.FilesManager.getFiles(); const activeFile = files.find(f => f.active); const readFiles = files.filter(f => f.read); return { active: activeFile, read: readFiles }; } function buildFileContextMessage() { const fileContext = getFileContext(); let contextMessage = ''; if (fileContext.active) { contextMessage += '\n\n## ACTIVE FILE (Primary Context)\n'; contextMessage += 'File: ' + fileContext.active.name + '\n'; contextMessage += 'Path: ' + fileContext.active.path + '\n'; contextMessage += '```\n' + fileContext.active.content + '\n```'; } if (fileContext.read.length > 0) { contextMessage += '\n\n## READ FILES (Additional Context)\n'; fileContext.read.forEach(function(file) { contextMessage += '\nFile: ' + file.name + '\n'; contextMessage += 'Path: ' + file.path + '\n'; contextMessage += '```\n' + file.content + '\n```\n'; }); } return contextMessage; } // --- Scope Management Functions --- function getAvailableScopes(activeFile) { if (!activeFile || !window.StorageEditorScopes) { return []; } const parsed = window.StorageEditorScopes.parseScopes(activeFile.content); const scopeNames = new Set(); // Extract base scope name (first part before underscore or hyphen) parsed.scopes.forEach(scope => { const baseName = scope.name.split(/[_-]/)[0]; scopeNames.add(baseName); }); return Array.from(scopeNames).sort(); } function updateScopeSelector(scopeSelector, currentValue) { const files = window.FilesManager ? window.FilesManager.getFiles() : []; const activeFile = files.find(f => f.active); const scopes = getAvailableScopes(activeFile); // Clear existing options except first two while (scopeSelector.options.length > 2) { scopeSelector.remove(2); } // Add scope options scopes.forEach(scope => { const option = document.createElement('option'); option.value = scope; option.textContent = `📦 ${scope}`; scopeSelector.appendChild(option); }); // Restore selection if it still exists if (currentValue && (currentValue === '' || currentValue === '__new__' || scopes.includes(currentValue))) { scopeSelector.value = currentValue; } else { scopeSelector.value = ''; } return scopeSelector.value; } function updateScopeIndicator(scopeIndicator, currentScope) { if (currentScope === '') { scopeIndicator.textContent = 'NO SCOPE'; scopeIndicator.style.background = '#374151'; scopeIndicator.style.color = '#9ca3af'; } else if (currentScope === '__new__') { scopeIndicator.textContent = 'NEW SCOPE'; scopeIndicator.style.background = '#7c3aed'; scopeIndicator.style.color = '#fff'; } else { scopeIndicator.textContent = currentScope.toUpperCase(); scopeIndicator.style.background = '#16a34a'; scopeIndicator.style.color = '#fff'; } } // --- Message Display Functions --- function addUserMessage(messagesContainer, message, currentScope, messagesPlaceholder, conversationHistory) { if (messagesPlaceholder && messagesPlaceholder.parentNode) { messagesPlaceholder.remove(); } const wrapper = document.createElement('div'); wrapper.style.cssText = ` margin-bottom: 16px; display: flex; justify-content: flex-end; align-items: flex-start; gap: 8px; `; wrapper.dataset.role = 'user'; wrapper.dataset.includedInContext = 'true'; const userMsg = document.createElement('div'); userMsg.className = 'message-content'; userMsg.style.cssText = ` padding: 12px 16px; background: #1e3a5f; border-radius: 8px; max-width: 80%; color: #e0e0e0; font-size: 14px; line-height: 1.5; `; let scopeBadge = ''; if (currentScope) { const badgeColor = currentScope === '__new__' ? '#7c3aed' : '#16a34a'; const badgeText = currentScope === '__new__' ? 'NEW SCOPE' : currentScope; scopeBadge = ` <div style=" display: inline-block; background: ${badgeColor}; color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; margin-bottom: 6px; font-family: monospace; ">🎯 ${badgeText}</div><br> `; } userMsg.innerHTML = scopeBadge + escapeHtml(message); // Controls const controls = document.createElement('div'); controls.style.cssText = ` display: flex; flex-direction: column; gap: 4px; padding-top: 4px; `; const eyeBtn = document.createElement('button'); eyeBtn.className = 'msg-eye-btn'; eyeBtn.innerHTML = '👁️'; eyeBtn.title = 'Hide from context'; eyeBtn.style.cssText = ` width: 32px; height: 32px; background: #374151; border: 1px solid #4b5563; border-radius: 4px; cursor: pointer; font-size: 16px; transition: all 0.2s; padding: 0; display: flex; align-items: center; justify-content: center; `; const deleteBtn = document.createElement('button'); deleteBtn.innerHTML = '❌'; deleteBtn.title = 'Delete message'; deleteBtn.style.cssText = ` width: 32px; height: 32px; background: #374151; border: 1px solid #4b5563; border-radius: 4px; cursor: pointer; font-size: 16px; transition: all 0.2s; padding: 0; display: flex; align-items: center; justify-content: center; `; // Eye button toggle eyeBtn.addEventListener('click', () => { const isHidden = wrapper.dataset.includedInContext === 'false'; if (isHidden) { wrapper.dataset.includedInContext = 'true'; userMsg.style.opacity = '1'; eyeBtn.innerHTML = '👁️'; eyeBtn.title = 'Hide from context'; } else { wrapper.dataset.includedInContext = 'false'; userMsg.style.opacity = '0.4'; eyeBtn.innerHTML = '🙈'; eyeBtn.title = 'Show in context'; } updateConversationHistory(messagesContainer, conversationHistory); }); // Delete button deleteBtn.addEventListener('click', () => { if (confirm('Delete this message?')) { wrapper.remove(); updateConversationHistory(messagesContainer, conversationHistory); } }); // Hover effects eyeBtn.addEventListener('mouseenter', () => { eyeBtn.style.background = '#4b5563'; }); eyeBtn.addEventListener('mouseleave', () => { eyeBtn.style.background = '#374151'; }); deleteBtn.addEventListener('mouseenter', () => { deleteBtn.style.background = '#ef4444'; deleteBtn.style.borderColor = '#dc2626'; }); deleteBtn.addEventListener('mouseleave', () => { deleteBtn.style.background = '#374151'; deleteBtn.style.borderColor = '#4b5563'; }); controls.appendChild(eyeBtn); controls.appendChild(deleteBtn); wrapper.appendChild(userMsg); wrapper.appendChild(controls); messagesContainer.appendChild(wrapper); messagesContainer.scrollTop = messagesContainer.scrollHeight; } function addAIMessage(messagesContainer, message, currentScope, conversationHistory, isStreaming) { const wrapper = document.createElement('div'); wrapper.style.cssText = ` margin-bottom: 16px; display: flex; justify-content: flex-start; align-items: flex-start; gap: 8px; `; wrapper.dataset.role = 'assistant'; wrapper.dataset.includedInContext = 'true'; // Controls const controls = document.createElement('div'); controls.style.cssText = ` display: flex; flex-direction: column; gap: 4px; padding-top: 4px; `; const eyeBtn = document.createElement('button'); eyeBtn.className = 'msg-eye-btn'; eyeBtn.innerHTML = '👁️'; eyeBtn.title = 'Hide from context'; eyeBtn.style.cssText = ` width: 32px; height: 32px; background: #374151; border: 1px solid #4b5563; border-radius: 4px; cursor: pointer; font-size: 16px; transition: all 0.2s; padding: 0; display: flex; align-items: center; justify-content: center; `; const deleteBtn = document.createElement('button'); deleteBtn.innerHTML = '❌'; deleteBtn.title = 'Delete message'; deleteBtn.style.cssText = ` width: 32px; height: 32px; background: #374151; border: 1px solid #4b5563; border-radius: 4px; cursor: pointer; font-size: 16px; transition: all 0.2s; padding: 0; display: flex; align-items: center; justify-content: center; `; const aiMsg = document.createElement('div'); aiMsg.className = 'message-content'; aiMsg.style.cssText = ` padding: 12px 16px; background: #2a2a2a; border-radius: 8px; max-width: 80%; color: #e0e0e0; font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; `; if (isStreaming) { aiMsg.innerHTML = '<span style="color: #888;">🤖 Thinking...</span>'; } else { const scopeInfo = currentScope ? ` <span style="color: #888; font-size: 12px;">(scope: ${currentScope})</span>` : ''; aiMsg.innerHTML = message + scopeInfo; } // Eye button toggle eyeBtn.addEventListener('click', () => { const isHidden = wrapper.dataset.includedInContext === 'false'; if (isHidden) { wrapper.dataset.includedInContext = 'true'; aiMsg.style.opacity = '1'; eyeBtn.innerHTML = '👁️'; eyeBtn.title = 'Hide from context'; } else { wrapper.dataset.includedInContext = 'false'; aiMsg.style.opacity = '0.4'; eyeBtn.innerHTML = '🙈'; eyeBtn.title = 'Show in context'; } updateConversationHistory(messagesContainer, conversationHistory); }); // Delete button deleteBtn.addEventListener('click', () => { if (confirm('Delete this message?')) { wrapper.remove(); updateConversationHistory(messagesContainer, conversationHistory); } }); // Hover effects eyeBtn.addEventListener('mouseenter', () => { eyeBtn.style.background = '#4b5563'; }); eyeBtn.addEventListener('mouseleave', () => { eyeBtn.style.background = '#374151'; }); deleteBtn.addEventListener('mouseenter', () => { deleteBtn.style.background = '#ef4444'; deleteBtn.style.borderColor = '#dc2626'; }); deleteBtn.addEventListener('mouseleave', () => { deleteBtn.style.background = '#374151'; deleteBtn.style.borderColor = '#4b5563'; }); controls.appendChild(eyeBtn); controls.appendChild(deleteBtn); wrapper.appendChild(controls); wrapper.appendChild(aiMsg); messagesContainer.appendChild(wrapper); messagesContainer.scrollTop = messagesContainer.scrollHeight; return aiMsg; } // --- Update Conversation History from DOM --- function updateConversationHistory(messagesContainer, conversationHistory) { conversationHistory.length = 0; const wrappers = messagesContainer.querySelectorAll('[data-role]'); wrappers.forEach(wrapper => { if (wrapper.dataset.includedInContext === 'true') { const role = wrapper.dataset.role; const content = wrapper.querySelector('.message-content').textContent.trim(); // Clean up the content (remove emoji prefix and scope info) let cleanContent = content; if (role === 'assistant') { cleanContent = content.replace(/^🤖\s+/, '').replace(/\s+\(working on scope:.*\)$/, ''); } conversationHistory.push({ role: role, content: cleanContent }); } }); console.log('[chat] Updated conversation history:', conversationHistory.length, 'messages'); } // --- File Context Badge Display --- function renderFileContextBadge() { const fileContext = getFileContext(); if (!fileContext.active && fileContext.read.length === 0) { return '<div style="color: #666; font-size: 12px; margin-bottom: 12px;">📎 No files in context</div>'; } let badgeHTML = '<div style="margin-bottom: 12px; padding: 8px 12px; background: rgba(139, 92, 246, 0.1); border: 1px solid #8b5cf6; border-radius: 6px;">'; badgeHTML += '<div style="color: #c4b5fd; font-weight: 700; margin-bottom: 6px; font-size: 12px;">📎 FILE CONTEXT</div>'; if (fileContext.active) { badgeHTML += '<div style="font-size: 12px; margin-bottom: 4px;">'; badgeHTML += '<span style="color: #16a34a; font-weight: 700;">🟢</span> '; badgeHTML += '<span style="color: #e6edf3; font-family: monospace;">' + escapeHtml(fileContext.active.name) + '</span>'; badgeHTML += '</div>'; } if (fileContext.read.length > 0) { fileContext.read.forEach(function(file) { badgeHTML += '<div style="font-size: 11px; margin-left: 8px; color: #9ca3af;">'; badgeHTML += '<span style="color: #3b82f6;">🔵</span> '; badgeHTML += '<span style="font-family: monospace;">' + escapeHtml(file.name) + '</span>'; badgeHTML += '</div>'; }); } badgeHTML += '</div>'; return badgeHTML; } // --- Tab Switching --- function switchTab(tabName, tabs, contents) { const [chatTab, activeFileTab, filesTab, settingsTab] = tabs; const [chatContent, activeFileContent, filesContent, settingsContent] = contents; tabs.forEach(tab => { tab.style.background = '#1a1a1a'; tab.style.borderBottomColor = 'transparent'; tab.style.color = '#666'; }); contents.forEach(content => { content.style.display = 'none'; }); if (tabName === 'chat') { chatTab.style.background = '#2a2a2a'; chatTab.style.borderBottomColor = '#16a34a'; chatTab.style.color = '#fff'; chatContent.style.display = 'flex'; } else if (tabName === 'activeFile') { activeFileTab.style.background = '#2a2a2a'; activeFileTab.style.borderBottomColor = '#3b82f6'; activeFileTab.style.color = '#fff'; activeFileContent.style.display = 'block'; } else if (tabName === 'files') { filesTab.style.background = '#2a2a2a'; filesTab.style.borderBottomColor = '#8b5cf6'; filesTab.style.color = '#fff'; filesContent.style.display = 'block'; } else if (tabName === 'settings') { settingsTab.style.background = '#2a2a2a'; settingsTab.style.borderBottomColor = '#f59e0b'; settingsTab.style.color = '#fff'; settingsContent.style.display = 'block'; } } // --- AI Chat Slide Configuration --- const aiChatSlide = { title: 'AI Chat', html: ` <div style="display: flex; flex-direction: column; height: 100%;"> <!-- Tab Navigation --> <div style=" display: flex; background: #1a1a1a; border-bottom: 2px solid #2a2a2a; "> <button id="chatTab" class="ai-tab active" style=" flex: 1; padding: 14px 20px; background: #2a2a2a; border: none; border-bottom: 3px solid #16a34a; color: #fff; cursor: pointer; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.2s; ">💬 Chat</button> <button id="activeFileTab" class="ai-tab" style=" flex: 1; padding: 14px 20px; background: #1a1a1a; border: none; border-bottom: 3px solid transparent; color: #666; cursor: pointer; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.2s; ">📄 Active File</button> <button id="filesTab" class="ai-tab" style=" flex: 1; padding: 14px 20px; background: #1a1a1a; border: none; border-bottom: 3px solid transparent; color: #666; cursor: pointer; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.2s; ">📁 Files</button> <button id="settingsTab" class="ai-tab" style=" flex: 1; padding: 14px 20px; background: #1a1a1a; border: none; border-bottom: 3px solid transparent; color: #666; cursor: pointer; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.2s; ">⚙️ Settings</button> </div> <!-- Tab Content Container --> <div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;"> <!-- Chat Tab Content --> <div id="chatContent" class="tab-content" style=" flex: 1; display: flex; flex-direction: column; "> <div id="aiChatMessages" style=" flex: 1; overflow-y: auto; padding: 20px; background: #0a0a0a; "> <div id="fileContextBadge"></div> <div id="aiChatMessagesPlaceholder" style="color: #666; text-align: center; padding: 40px;"> 💬 No messages yet. Start a conversation! </div> </div> <div style=" padding: 16px; background: #1a1a1a; border-top: 1px solid #2a2a2a; "> <!-- Scope Selector --> <div style=" display: flex; align-items: center; gap: 12px; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #2a2a2a; "> <label style=" color: #888; font-size: 13px; font-weight: 600; white-space: nowrap; ">🎯 Target Scope:</label> <select id="scopeSelector" style=" flex: 1; padding: 8px 12px; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 4px; color: #e0e0e0; font-size: 13px; font-family: monospace; cursor: pointer; outline: none; "> <option value="">❌ None (General Chat)</option> <option value="__new__">✨ Create New Scope</option> </select> <div id="scopeIndicator" style=" padding: 6px 12px; background: #374151; border-radius: 4px; color: #9ca3af; font-size: 11px; font-weight: 600; white-space: nowrap; ">NO SCOPE</div> </div> <!-- Message Input --> <div style="display: flex; gap: 8px;"> <textarea id="aiChatInput" placeholder="Type your message..." style=" flex: 1; padding: 12px; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 4px; color: #e0e0e0; font-size: 14px; font-family: 'Segoe UI', sans-serif; resize: vertical; min-height: 60px; max-height: 200px; " ></textarea> <button id="aiChatSend" style=" padding: 12px 24px; background: #16a34a; border: 1px solid #15803d; border-radius: 4px; color: #fff; cursor: pointer; font-size: 14px; font-weight: 600; align-self: flex-end; transition: all 0.2s; " >Send</button> </div> </div> </div> <!-- Active File Tab Content --> <div id="activeFileContent" class="tab-content" style=" flex: 1; overflow-y: auto; padding: 20px; background: #0a0a0a; display: none; "> <div id="aiChatActiveFileContainer"></div> </div> <!-- Files Tab Content --> <div id="filesContent" class="tab-content" style=" flex: 1; overflow-y: auto; padding: 20px; background: #0a0a0a; display: none; "> <div id="aiChatFilesContainer"></div> </div> <!-- Settings Tab Content --> <div id="settingsContent" class="tab-content" style=" flex: 1; overflow-y: auto; padding: 20px; background: #0a0a0a; display: none; "> <div style="padding: 40px; text-align: center; color: #666;"> Click the Settings tab to open settings... </div> </div> </div> </div> `, onRender(el) { console.log('[chat] Rendering AI chat interface with tabs'); const chatTab = el.querySelector('#chatTab'); const activeFileTab = el.querySelector('#activeFileTab'); const filesTab = el.querySelector('#filesTab'); const settingsTab = el.querySelector('#settingsTab'); const chatContent = el.querySelector('#chatContent'); const activeFileContent = el.querySelector('#activeFileContent'); const filesContent = el.querySelector('#filesContent'); const settingsContent = el.querySelector('#settingsContent'); const messagesContainer = el.querySelector('#aiChatMessages'); const activeFileContainer = el.querySelector('#aiChatActiveFileContainer'); const filesContainer = el.querySelector('#aiChatFilesContainer'); const messagesPlaceholder = el.querySelector('#aiChatMessagesPlaceholder'); const fileContextBadge = el.querySelector('#fileContextBadge'); const input = el.querySelector('#aiChatInput'); const sendBtn = el.querySelector('#aiChatSend'); const scopeSelector = el.querySelector('#scopeSelector'); const scopeIndicator = el.querySelector('#scopeIndicator'); let currentScope = ''; let conversationHistory = []; let isProcessing = false; // Function to update file context badge const updateFileContextBadge = function() { if (fileContextBadge) { fileContextBadge.innerHTML = renderFileContextBadge(); } }; // Initial scope selector population and file context display currentScope = updateScopeSelector(scopeSelector, currentScope); updateScopeIndicator(scopeIndicator, currentScope); updateFileContextBadge(); // Scope selector change handler scopeSelector.addEventListener('change', () => { const value = scopeSelector.value; if (value === '__new__') { const newScopeName = prompt('Enter new scope name (base name only):'); if (newScopeName && newScopeName.trim()) { const baseName = newScopeName.trim().toLowerCase().replace(/[^a-z0-9-]/g, ''); if (baseName) { currentScope = baseName; updateScopeIndicator(scopeIndicator, currentScope); // Add it to the selector const option = document.createElement('option'); option.value = baseName; option.textContent = `📦 ${baseName}`; scopeSelector.appendChild(option); scopeSelector.value = baseName; } else { scopeSelector.value = ''; currentScope = ''; updateScopeIndicator(scopeIndicator, currentScope); } } else { scopeSelector.value = ''; currentScope = ''; updateScopeIndicator(scopeIndicator, currentScope); } } else { currentScope = value; updateScopeIndicator(scopeIndicator, currentScope); } }); // Tab switching const tabs = [chatTab, activeFileTab, filesTab, settingsTab]; const contents = [chatContent, activeFileContent, filesContent, settingsContent]; chatTab.addEventListener('click', () => switchTab('chat', tabs, contents)); activeFileTab.addEventListener('click', () => switchTab('activeFile', tabs, contents)); filesTab.addEventListener('click', () => switchTab('files', tabs, contents)); settingsTab.addEventListener('click', () => { switchTab('settings', tabs, contents); // Render Settings UI inside the settings tab if (window.Settings && window.Settings.slide) { settingsContent.innerHTML = window.Settings.slide.html; // Ensure the onRender runs with settingsContent as root try { window.Settings.slide.onRender(settingsContent); } catch (e) { console.error('[chat] Error rendering settings UI:', e); settingsContent.innerHTML = '<div style="color:#f44;padding:20px">❌ Failed to load Settings UI.</div>'; } } else { settingsContent.innerHTML = '<div style="color:#f44;padding:20px">❌ Settings module not found.</div>'; } }); // Tab hover effects tabs.forEach(tab => { tab.addEventListener('mouseenter', () => { if (tab.style.background === 'rgb(26, 26, 26)') { tab.style.background = '#242424'; } }); tab.addEventListener('mouseleave', () => { if (tab.style.background === 'rgb(36, 36, 36)') { tab.style.background = '#1a1a1a'; } }); }); // Function to update active file display const updateActiveFile = () => { if (activeFileContainer && window.ActiveFileDisplay) { window.ActiveFileDisplay.render(activeFileContainer); } // Update scope selector when file changes currentScope = updateScopeSelector(scopeSelector, currentScope); updateScopeIndicator(scopeIndicator, currentScope); // Update file context badge updateFileContextBadge(); }; // Function to render files list const updateFilesList = () => { if (filesContainer && window.FilesManager) { window.FilesManager.render(filesContainer, updateActiveFile); } }; // Initial renders updateActiveFile(); updateFilesList(); // Listen for file changes window.addEventListener('activeFilesUpdated', () => { updateActiveFile(); updateFilesList(); }); // Send message handler const sendMessage = async () => { const message = input.value.trim(); if (!message || isProcessing) return; isProcessing = true; input.value = ''; input.disabled = true; sendBtn.disabled = true; sendBtn.textContent = '⏳ Sending...'; console.log('[chat] User message:', message); console.log('[chat] Target scope:', currentScope || 'none'); console.log('[chat] Model:', currentModel); // Add user message addUserMessage(messagesContainer, message, currentScope, messagesPlaceholder, conversationHistory); // Create AI message placeholder const aiMsgElement = addAIMessage(messagesContainer, '', currentScope, conversationHistory, true); try { // Get config from Settings module const config = getApiConfig(); // Build system message with file context let systemMessage = ''; // Select prompt based on current mode from settings if (config.currentMode === 'chat') { systemMessage = config.chatPrompt; } else if (config.currentMode === 'snippets') { systemMessage = config.snippetsPrompt; } else if (config.currentMode === 'fullcode') { systemMessage = config.fullCodePrompt; } // Add response mode instruction if (config.responseMode === 'raw') { systemMessage += '\n\nIMPORTANT: Return ONLY the requested code with NO explanations, comments, or markdown formatting. Just pure code.'; } const fileContextMessage = buildFileContextMessage(); if (fileContextMessage) { systemMessage += fileContextMessage; } if (currentScope) { systemMessage += '\n\nYou are working on scope: ' + currentScope; } // Call API const result = await callAI(message, systemMessage, conversationHistory); // Display response const scopeInfo = currentScope ? ` <span style="color: #888; font-size: 12px;">(scope: ${currentScope})</span>` : ''; aiMsgElement.innerHTML = escapeHtml(result.answer) + scopeInfo; // Show usage info if available if (result.usage) { const usageDiv = document.createElement('div'); usageDiv.style.cssText = 'margin-top: 8px; padding: 8px; background: rgba(0,0,0,0.3); border-radius: 4px; font-size: 11px; color: #888;'; usageDiv.textContent = `📊 Tokens: ${result.usage.prompt_tokens || 0} in, ${result.usage.completion_tokens || 0} out | Model: ${result.model}`; aiMsgElement.appendChild(usageDiv); } console.log('[chat] AI response received:', result); } catch (error) { aiMsgElement.innerHTML = '<span style="color: #ef4444;">❌ Error: ' + escapeHtml(error.message) + '</span>'; console.error('[chat] Error:', error); } finally { isProcessing = false; input.disabled = false; sendBtn.disabled = false; sendBtn.textContent = 'Send'; input.focus(); } }; if (sendBtn) { sendBtn.addEventListener('click', sendMessage); sendBtn.addEventListener('mouseenter', () => { sendBtn.style.background = '#15803d'; }); sendBtn.addEventListener('mouseleave', () => { sendBtn.style.background = '#16a34a'; }); } if (input) { input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); input.focus(); } } }; // --- Expose AI Chat API --- window.AIChat = { open: () => { if (window.AppOverlay) { window.AppOverlay.open([aiChatSlide]); } else { console.error('[chat] AppOverlay not available'); } }, slide: aiChatSlide, getFileContext: getFileContext, buildFileContextMessage: buildFileContextMessage }; // --- Register as AppItems component --- if (window.AppItems) { window.AppItems.push({ title: 'AI Chat', html: aiChatSlide.html, onRender: aiChatSlide.onRender }); console.log('[chat] Registered as AppItems component'); } console.log('[chat] AI Chat interface loaded'); })();