// chat.js - AI Chat Interface with Scope Targeting
(function() {
console.log("[chat] Loading AI Chat interface...");
// --- Utility Functions ---
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// --- 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) {
if (messagesPlaceholder && messagesPlaceholder.parentNode) {
messagesPlaceholder.remove();
}
const userMsg = document.createElement('div');
userMsg.style.cssText = `
margin-bottom: 16px;
padding: 12px 16px;
background: #1e3a5f;
border-radius: 8px;
max-width: 80%;
margin-left: auto;
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);
messagesContainer.appendChild(userMsg);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
function addAIMessage(messagesContainer, message, currentScope) {
const aiMsg = document.createElement('div');
aiMsg.style.cssText = `
margin-bottom: 16px;
padding: 12px 16px;
background: #2a2a2a;
border-radius: 8px;
max-width: 80%;
margin-right: auto;
color: #e0e0e0;
font-size: 14px;
line-height: 1.5;
`;
aiMsg.textContent = `🤖 ${message} ${currentScope ? `(working on scope: ${currentScope})` : ''}`;
messagesContainer.appendChild(aiMsg);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// --- 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;
">
<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 input = el.querySelector('#aiChatInput');
const sendBtn = el.querySelector('#aiChatSend');
const scopeSelector = el.querySelector('#scopeSelector');
const scopeIndicator = el.querySelector('#scopeIndicator');
let currentScope = '';
// Initial scope selector population
currentScope = updateScopeSelector(scopeSelector, currentScope);
updateScopeIndicator(scopeIndicator, currentScope);
// 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);
};
// 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 = () => {
const message = input.value.trim();
if (!message) return;
console.log('[chat] User message:', message);
console.log('[chat] Target scope:', currentScope || 'none');
input.value = '';
addUserMessage(messagesContainer, message, currentScope, messagesPlaceholder);
// TODO: Add AI API call here with scope context
setTimeout(() => {
addAIMessage(messagesContainer, 'AI response will go here...', currentScope);
}, 500);
};
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
};
// --- 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');
})();