// 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
};
}
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);
// Update model selector if it exists
const modelSelector = document.querySelector('#modelSelector');
if (modelSelector) {
modelSelector.value = 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] = tabs;
const [chatContent, activeFileContent, filesContent] = 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';
}
}
// --- 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>
</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;
">
<!-- Model Selector -->
<div style="
margin-bottom: 16px;
padding: 12px;
background: #1a1a1a;
border: 2px solid #2a2a2a;
border-radius: 8px;
">
<label style="
display: block;
color: #888;
font-size: 12px;
font-weight: 600;
margin-bottom: 8px;
">🤖 AI Model:</label>
<select id="modelSelector" style="
width: 100%;
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="grok-code-fast-1">🤖 Grok Code Fast 1</option>
<option value="deepseek-chat">🧠 DeepSeek Chat</option>
<option value="deepseek-reasoner">🧠 DeepSeek Reasoner</option>
<option value="gpt-4o">🌟 GPT-4o</option>
<option value="gpt-4o-mini">🌟 GPT-4o Mini</option>
<option value="grok-3">🤖 Grok 3</option>
</select>
</div>
<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>
</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 chatContent = el.querySelector('#chatContent');
const activeFileContent = el.querySelector('#activeFileContent');
const filesContent = el.querySelector('#filesContent');
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');
const modelSelector = el.querySelector('#modelSelector');
let currentScope = '';
let conversationHistory = [];
let isProcessing = false;
// Initialize model selector with Settings module if available
if (modelSelector && window.Settings) {
const models = window.Settings.getAllModels();
modelSelector.innerHTML = '';
Object.keys(models).forEach(key => {
const model = models[key];
const option = document.createElement('option');
option.value = key;
option.textContent = `${model.icon} ${model.name}`;
modelSelector.appendChild(option);
});
modelSelector.value = currentModel;
}
// Model selector change handler
if (modelSelector) {
modelSelector.addEventListener('change', () => {
currentModel = modelSelector.value;
console.log('[chat] Model changed to:', currentModel);
});
}
// 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];
const contents = [chatContent, activeFileContent, filesContent];
chatTab.addEventListener('click', () => switchTab('chat', tabs, contents));
activeFileTab.addEventListener('click', () => switchTab('activeFile', tabs, contents));
filesTab.addEventListener('click', () => switchTab('files', tabs, contents));
// 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 {
// Build system message with file context
let systemMessage = 'You are a helpful AI assistant for web development.';
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');
})();