// ai_chat_scopes.js - Integration between AI Chat and Block Editor
(function() {
console.log("[ai_chat_scopes] Loading AI Chat with Scopes integration...");
// --- File Management Functions ---
function getFilesFromLocalStorage() {
try {
return JSON.parse(localStorage.getItem('sftp_active_files') || '[]');
} catch {
return [];
}
}
function saveFilesToLocalStorage(files) {
try {
localStorage.setItem('sftp_active_files', JSON.stringify(files));
} catch (err) {
console.error('[ai_chat_scopes] Failed to save files:', err);
}
}
function setFileState(fileName, state) {
const files = getFilesFromLocalStorage();
const file = files.find(f => f.name === fileName);
if (!file) return;
if (state === 'active') {
files.forEach(f => {
if (f.name === fileName) {
f.active = true;
f.read = false;
} else {
f.active = false;
}
});
} else if (state === 'read') {
file.read = true;
file.active = false;
} else if (state === 'inactive') {
file.read = false;
file.active = false;
}
saveFilesToLocalStorage(files);
}
function renderFilesList(container, onFileChange) {
const files = getFilesFromLocalStorage();
container.innerHTML = '';
if (files.length === 0) {
const emptyMsg = document.createElement('div');
emptyMsg.style.cssText = `
color: #666;
text-align: center;
padding: 40px;
font-size: 14px;
`;
emptyMsg.textContent = '📁 No files open - open files from Storage Editor';
container.appendChild(emptyMsg);
return;
}
// Header with New File button
const header = document.createElement('div');
header.style.cssText = `
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid #2a2a2a;
display: flex;
justify-content: space-between;
align-items: center;
`;
const headerLeft = document.createElement('div');
headerLeft.innerHTML = `
<h2 style="margin: 0; color: #e6edf3; font-size: 18px; font-weight: 700;">
📁 All Files (${files.length})
</h2>
<p style="margin: 8px 0 0 0; color: #64748b; font-size: 13px;">
Click file name to set state | Click versions to view history
</p>
`;
const newFileBtn = document.createElement('button');
newFileBtn.style.cssText = `
padding: 10px 20px;
background: #16a34a;
border: 1px solid #15803d;
border-radius: 6px;
color: #fff;
cursor: pointer;
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
transition: all 0.2s;
`;
newFileBtn.textContent = '+ New File';
newFileBtn.addEventListener('click', async () => {
await showNewFileDialog(container, onFileChange);
});
newFileBtn.addEventListener('mouseenter', () => {
newFileBtn.style.background = '#15803d';
});
newFileBtn.addEventListener('mouseleave', () => {
newFileBtn.style.background = '#16a34a';
});
header.appendChild(headerLeft);
header.appendChild(newFileBtn);
container.appendChild(header);
// Files grid
files.forEach(file => {
const fileCard = document.createElement('div');
fileCard.style.cssText = `
background: #1a1a1a;
border: 2px solid #2a2a2a;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
transition: all 0.2s;
`;
// Style based on state
if (file.active) {
fileCard.style.borderColor = '#16a34a';
fileCard.style.background = 'rgba(22, 163, 74, 0.1)';
} else if (file.read) {
fileCard.style.borderColor = '#3b82f6';
fileCard.style.background = 'rgba(59, 130, 246, 0.1)';
}
const statusBadge = file.active ? '🟢 ACTIVE' : file.read ? '🔵 READ' : '⚪ INACTIVE';
const statusColor = file.active ? '#16a34a' : file.read ? '#3b82f6' : '#666';
const versionCount = file.versions ? file.versions.length : 0;
const fileHeader = document.createElement('div');
fileHeader.style.cssText = `
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 8px;
cursor: pointer;
`;
fileHeader.innerHTML = `
<div style="
font-weight: 700;
color: #e6edf3;
font-size: 15px;
font-family: monospace;
">${escapeHtml(file.name)}</div>
<div style="
color: ${statusColor};
font-size: 11px;
font-weight: 700;
padding: 4px 8px;
background: rgba(0,0,0,0.3);
border-radius: 4px;
">${statusBadge}</div>
`;
// Click file name to cycle states
fileHeader.addEventListener('click', () => {
if (!file.active && !file.read) {
setFileState(file.name, 'read');
} else if (file.read) {
setFileState(file.name, 'active');
} else if (file.active) {
setFileState(file.name, 'inactive');
}
renderFilesList(container, onFileChange);
if (onFileChange) onFileChange();
});
fileCard.appendChild(fileHeader);
// Path
const pathDiv = document.createElement('div');
pathDiv.style.cssText = `
color: #64748b;
font-size: 12px;
margin-bottom: 8px;
`;
pathDiv.textContent = file.path || 'No path';
fileCard.appendChild(pathDiv);
// Preview
const previewDiv = document.createElement('div');
previewDiv.style.cssText = `
color: #888;
font-size: 11px;
font-family: monospace;
background: #0a0a0a;
padding: 8px;
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 8px;
`;
previewDiv.textContent = file.content ? file.content.substring(0, 80) + '...' : '(empty)';
fileCard.appendChild(previewDiv);
// Versions section
if (versionCount > 0) {
const versionsHeader = document.createElement('div');
versionsHeader.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-top: 1px solid #2a2a2a;
margin-top: 8px;
cursor: pointer;
user-select: none;
`;
versionsHeader.innerHTML = `
<div style="color: #3b82f6; font-size: 12px; font-weight: 600;">
<span class="version-toggle">▶</span> 💾 ${versionCount} Version${versionCount > 1 ? 's' : ''}
</div>
`;
const versionsBody = document.createElement('div');
versionsBody.style.cssText = `
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
margin-top: 8px;
`;
// Render versions (newest first)
const reversedVersions = [...file.versions].reverse();
reversedVersions.forEach((version, idx) => {
const realIndex = file.versions.length - 1 - idx;
const date = new Date(version.timestamp);
const isCurrent = file.content === version.content;
const versionItem = document.createElement('div');
versionItem.style.cssText = `
display: flex;
align-items: stretch;
margin-bottom: 6px;
gap: 4px;
`;
const versionBtn = document.createElement('button');
versionBtn.style.cssText = `
flex: 1;
text-align: left;
padding: 8px 12px;
background: ${isCurrent ? '#1e3a5f' : '#0a0a0a'};
border: 1px solid ${isCurrent ? '#3b82f6' : '#2a2a2a'};
border-radius: 4px;
color: #e0e0e0;
cursor: ${isCurrent ? 'default' : 'pointer'};
font-size: 12px;
transition: all 0.2s;
`;
versionBtn.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<span style="font-weight: 700; color: ${isCurrent ? '#3b82f6' : '#fff'};">
${version.label} ${isCurrent ? '(Current)' : ''}
</span>
<span style="color: #666; font-size: 10px;">${date.toLocaleString()}</span>
</div>
<div style="color: #888; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
${version.content.substring(0, 60)}${version.content.length > 60 ? '...' : ''}
</div>
`;
if (!isCurrent) {
versionBtn.addEventListener('click', () => {
if (confirm(`Restore ${version.label}? This will replace the current content.`)) {
restoreVersion(file.name, realIndex);
renderFilesList(container, onFileChange);
if (onFileChange) onFileChange();
}
});
versionBtn.addEventListener('mouseenter', () => {
versionBtn.style.background = '#1a1a1a';
versionBtn.style.borderColor = '#3a3a3a';
});
versionBtn.addEventListener('mouseleave', () => {
versionBtn.style.background = '#0a0a0a';
versionBtn.style.borderColor = '#2a2a2a';
});
}
// Delete button (only for non-current versions)
const deleteBtn = document.createElement('button');
deleteBtn.style.cssText = `
width: 36px;
padding: 8px;
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 4px;
color: #ef4444;
cursor: pointer;
font-size: 16px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
`;
deleteBtn.innerHTML = '🗑️';
deleteBtn.title = `Delete ${version.label}`;
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (confirm(`Delete ${version.label}? This cannot be undone.`)) {
deleteVersion(file.name, realIndex);
renderFilesList(container, onFileChange);
if (onFileChange) onFileChange();
}
});
deleteBtn.addEventListener('mouseenter', () => {
deleteBtn.style.background = '#ef4444';
deleteBtn.style.borderColor = '#dc2626';
deleteBtn.style.color = '#fff';
});
deleteBtn.addEventListener('mouseleave', () => {
deleteBtn.style.background = '#1a1a1a';
deleteBtn.style.borderColor = '#2a2a2a';
deleteBtn.style.color = '#ef4444';
});
versionItem.appendChild(versionBtn);
versionItem.appendChild(deleteBtn);
versionsBody.appendChild(versionItem);
});
// Toggle versions
let versionsExpanded = false;
versionsHeader.addEventListener('click', () => {
versionsExpanded = !versionsExpanded;
const toggle = versionsHeader.querySelector('.version-toggle');
if (versionsExpanded) {
versionsBody.style.maxHeight = versionsBody.scrollHeight + 'px';
toggle.textContent = '▼';
} else {
versionsBody.style.maxHeight = '0';
toggle.textContent = '▶';
}
});
fileCard.appendChild(versionsHeader);
fileCard.appendChild(versionsBody);
}
container.appendChild(fileCard);
});
}
function restoreVersion(fileName, versionIndex) {
const files = getFilesFromLocalStorage();
const file = files.find(f => f.name === fileName);
if (!file || !file.versions || !file.versions[versionIndex]) return;
// Restore the selected version to current content
file.content = file.versions[versionIndex].content;
saveFilesToLocalStorage(files);
// Trigger update event
window.dispatchEvent(new Event('activeFilesUpdated'));
}
function deleteVersion(fileName, versionIndex) {
const files = getFilesFromLocalStorage();
const file = files.find(f => f.name === fileName);
if (!file || !file.versions || !file.versions[versionIndex]) return;
// Remove the version
file.versions.splice(versionIndex, 1);
// Relabel remaining versions
file.versions.forEach((version, idx) => {
version.label = `v${idx + 1}`;
});
saveFilesToLocalStorage(files);
// Trigger update event
window.dispatchEvent(new Event('activeFilesUpdated'));
}
// Show new file dialog with template selection
async function showNewFileDialog(container, onFileChange) {
// Fetch templates from server via templates.php
let templates = [];
try {
const response = await fetch('templates.php?action=list');
const data = await response.json();
if (data.success && Array.isArray(data.templates)) {
templates = data.templates.map(name => ({
name: name,
path: name
}));
console.log('[ai_chat] Found templates:', templates);
} else {
console.error('[ai_chat] Failed to load templates:', data.error || 'Unknown error');
}
} catch (error) {
console.error('[ai_chat] Failed to load templates:', error);
}
// Create dialog overlay
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 2147483647; /* the maximum safe integer for z-index */
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #1a1a1a;
border: 2px solid #3a3a3a;
border-radius: 12px;
padding: 24px;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
`;
dialog.innerHTML = `
<h2 style="margin: 0 0 20px 0; color: #e6edf3; font-size: 20px; font-weight: 700;">
📄 Create New File
</h2>
<div style="margin-bottom: 16px;">
<label style="display: block; color: #9ca3af; font-size: 13px; font-weight: 600; margin-bottom: 8px;">
File Name
</label>
<input
id="newFileName"
type="text"
placeholder="my-file.php"
style="
width: 100%;
padding: 10px 12px;
background: #0a0a0a;
border: 1px solid #3a3a3a;
border-radius: 6px;
color: #e0e0e0;
font-size: 14px;
font-family: monospace;
outline: none;
"
/>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; color: #9ca3af; font-size: 13px; font-weight: 600; margin-bottom: 8px;">
Start from Template
</label>
<div id="templateList" style="
max-height: 300px;
overflow-y: auto;
border: 1px solid #3a3a3a;
border-radius: 6px;
background: #0a0a0a;
">
<div class="template-option" data-template="" style="
padding: 12px;
border-bottom: 1px solid #2a2a2a;
cursor: pointer;
transition: background 0.2s;
">
<div style="font-weight: 600; color: #e6edf3;">📝 Empty File</div>
<div style="font-size: 12px; color: #666; margin-top: 4px;">Start with a blank file</div>
</div>
${templates.length === 0 ? `
<div style="padding: 12px; color: #666; font-size: 13px; text-align: center;">
No templates found in /templates folder
</div>
` : ''}
</div>
</div>
<div style="display: flex; gap: 12px;">
<button id="createFileBtn" style="
flex: 1;
padding: 12px;
background: #16a34a;
border: 1px solid #15803d;
border-radius: 6px;
color: #fff;
cursor: pointer;
font-size: 14px;
font-weight: 700;
transition: all 0.2s;
">Create File</button>
<button id="cancelFileBtn" style="
flex: 1;
padding: 12px;
background: #374151;
border: 1px solid #4b5563;
border-radius: 6px;
color: #e0e0e0;
cursor: pointer;
font-size: 14px;
font-weight: 700;
transition: all 0.2s;
">Cancel</button>
</div>
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
const fileNameInput = dialog.querySelector('#newFileName');
const templateList = dialog.querySelector('#templateList');
const createBtn = dialog.querySelector('#createFileBtn');
const cancelBtn = dialog.querySelector('#cancelFileBtn');
let selectedTemplate = '';
// Add template options
templates.forEach(template => {
const option = document.createElement('div');
option.className = 'template-option';
option.dataset.template = template.path;
option.style.cssText = `
padding: 12px;
border-bottom: 1px solid #2a2a2a;
cursor: pointer;
transition: background 0.2s;
`;
option.innerHTML = `
<div style="font-weight: 600; color: #e6edf3;">📄 ${escapeHtml(template.name)}</div>
<div style="font-size: 11px; color: #666; margin-top: 4px; font-family: monospace;">
${escapeHtml(template.path)}
</div>
`;
templateList.appendChild(option);
});
// Template selection
templateList.addEventListener('click', (e) => {
const option = e.target.closest('.template-option');
if (!option) return;
templateList.querySelectorAll('.template-option').forEach(opt => {
opt.style.background = '#0a0a0a';
opt.style.borderLeftWidth = '0';
});
option.style.background = '#1a1a1a';
option.style.borderLeft = '3px solid #16a34a';
selectedTemplate = option.dataset.template;
});
// Hover effects for templates
templateList.addEventListener('mouseover', (e) => {
const option = e.target.closest('.template-option');
if (option && option.style.background !== 'rgb(26, 26, 26)') {
option.style.background = '#151515';
}
});
templateList.addEventListener('mouseout', (e) => {
const option = e.target.closest('.template-option');
if (option && option.style.background !== 'rgb(26, 26, 26)') {
option.style.background = '#0a0a0a';
}
});
// Create file handler
const createFile = async () => {
const fileName = fileNameInput.value.trim();
if (!fileName) {
alert('Please enter a file name');
return;
}
const files = getFilesFromLocalStorage();
if (files.find(f => f.name === fileName)) {
alert('A file with this name already exists');
return;
}
let content = '';
// Load template content if selected
if (selectedTemplate) {
try {
const response = await fetch('templates.php?action=read&file=' + encodeURIComponent(selectedTemplate));
const data = await response.json();
if (data.success) {
content = data.content;
console.log('[ai_chat] Loaded template content from:', selectedTemplate);
} else {
console.error('[ai_chat] Failed to load template:', data.error);
alert('Failed to load template. Starting with empty file.');
}
} catch (error) {
console.error('[ai_chat] Failed to load template:', error);
alert('Failed to load template. Starting with empty file.');
}
}
// Add new file
files.push({
name: fileName,
path: fileName,
content: content,
active: false,
read: false,
versions: []
});
saveFilesToLocalStorage(files);
window.dispatchEvent(new Event('activeFilesUpdated'));
// Close dialog
document.body.removeChild(overlay);
// Refresh file list
renderFilesList(container, onFileChange);
if (onFileChange) onFileChange();
};
createBtn.addEventListener('click', createFile);
cancelBtn.addEventListener('click', () => {
document.body.removeChild(overlay);
});
// Enter to create
fileNameInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
createFile();
}
});
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
document.body.removeChild(overlay);
}
});
// Focus input
fileNameInput.focus();
}
function renderFileBar(container, onFileChange) {
const files = getFilesFromLocalStorage();
container.innerHTML = '';
container.style.cssText = `
display: flex;
gap: 8px;
padding: 12px;
background: #1a1a1a;
border-bottom: 1px solid #2a2a2a;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
`;
if (files.length === 0) {
const emptyMsg = document.createElement('div');
emptyMsg.style.cssText = `
color: #666;
font-size: 13px;
padding: 4px 12px;
`;
emptyMsg.textContent = 'No files open - open files from Storage Editor';
container.appendChild(emptyMsg);
return;
}
files.forEach(file => {
const fileBtn = document.createElement('button');
fileBtn.textContent = file.name;
fileBtn.style.cssText = `
padding: 8px 16px;
border: 1px solid #3a3a3a;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
white-space: nowrap;
flex-shrink: 0;
transition: all 0.2s;
`;
if (file.active) {
fileBtn.style.background = '#16a34a';
fileBtn.style.color = '#fff';
fileBtn.style.borderColor = '#15803d';
fileBtn.title = 'Active - Click to deactivate';
} else if (file.read) {
fileBtn.style.background = '#3b82f6';
fileBtn.style.color = '#fff';
fileBtn.style.borderColor = '#2563eb';
fileBtn.title = 'Read - Click to set as active';
} else {
fileBtn.style.background = '#2a2a2a';
fileBtn.style.color = '#888';
fileBtn.style.borderColor = '#3a3a3a';
fileBtn.title = 'Inactive - Click to mark as read';
}
fileBtn.addEventListener('click', () => {
if (!file.active && !file.read) {
setFileState(file.name, 'read');
} else if (file.read) {
setFileState(file.name, 'active');
} else if (file.active) {
setFileState(file.name, 'inactive');
}
renderFileBar(container, onFileChange);
if (onFileChange) onFileChange();
});
container.appendChild(fileBtn);
});
}
// --- Active File Display with Scopes ---
function renderActiveFileScopes(container) {
const files = getFilesFromLocalStorage();
const activeFile = files.find(f => f.active);
container.innerHTML = '';
if (!activeFile) {
const emptyMsg = document.createElement('div');
emptyMsg.style.cssText = `
color: #666;
text-align: center;
padding: 40px;
font-size: 14px;
`;
emptyMsg.textContent = '📄 No active file selected';
container.appendChild(emptyMsg);
return;
}
// Check if StorageEditorScopes is available
if (!window.StorageEditorScopes || typeof StorageEditorScopes.buildBlockStructure !== 'function') {
const errorMsg = document.createElement('div');
errorMsg.style.cssText = `
color: #ef4444;
text-align: center;
padding: 40px;
font-size: 14px;
`;
errorMsg.innerHTML = '⚠️ Scopes module not loaded<br><small style="color: #888;">Load storage-scopes.js to see block structure</small>';
container.appendChild(errorMsg);
return;
}
// Build block structure
const blocks = StorageEditorScopes.buildBlockStructure(activeFile.content);
if (!blocks || blocks.length === 0) {
const noScopesMsg = document.createElement('div');
noScopesMsg.style.cssText = `
color: #666;
text-align: center;
padding: 40px;
font-size: 14px;
`;
noScopesMsg.innerHTML = `
<div style="font-size: 32px; margin-bottom: 12px;">📄</div>
<div><strong>${activeFile.name}</strong></div>
<div style="margin-top: 8px; font-size: 12px;">No scopes found in this file</div>
`;
container.appendChild(noScopesMsg);
return;
}
// Wrapper for the entire file context section
const wrapper = document.createElement('div');
wrapper.style.cssText = `
background: #1a1a1a;
border: 2px solid #2a2a2a;
border-radius: 8px;
margin-bottom: 20px;
overflow: hidden;
`;
// File header (clickable to toggle)
const header = document.createElement('div');
header.style.cssText = `
background: #1a1a1a;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #2a2a2a;
`;
header.innerHTML = `
<div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="color: #16a34a; font-weight: 700; font-size: 16px;">
📄 ${activeFile.name}
</span>
<span style="color: #64748b; font-size: 12px;">
${blocks.length} blocks
</span>
</div>
<div style="color: #64748b; font-size: 11px; margin-top: 4px;">
Active file context (editable)
</div>
</div>
<button id="makeVersionBtn" style="
padding: 8px 16px;
background: #3b82f6;
border: 1px solid #2563eb;
border-radius: 4px;
color: #fff;
cursor: pointer;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
transition: all 0.2s;
">💾 Make Version</button>
`;
// Content area
const contentArea = document.createElement('div');
contentArea.style.cssText = `
overflow: hidden;
background: #0a0a0a;
padding: 16px;
`;
// Render blocks
blocks.forEach((block, idx) => {
const blockId = `ai-chat-block-${idx}`;
if (block.type === 'container') {
contentArea.appendChild(renderContainerBlockEditable(block, blockId));
} else if (block.type === 'scope') {
contentArea.appendChild(renderScopeBlockEditable(block, blockId));
} else if (block.type === 'unmarked') {
// Skip empty unmarked blocks
const trimmed = block.content.trim();
if (trimmed.length > 0) {
contentArea.appendChild(renderUnmarkedBlockEditable(block, blockId));
}
}
});
wrapper.appendChild(header);
wrapper.appendChild(contentArea);
container.appendChild(wrapper);
// Make Version button handler
const makeVersionBtn = header.querySelector('#makeVersionBtn');
if (makeVersionBtn) {
makeVersionBtn.addEventListener('click', () => {
saveBlocksAsVersion(activeFile, blocks, container);
});
makeVersionBtn.addEventListener('mouseenter', () => {
makeVersionBtn.style.background = '#2563eb';
});
makeVersionBtn.addEventListener('mouseleave', () => {
makeVersionBtn.style.background = '#3b82f6';
});
}
}
// Save all blocks as a version
function saveBlocksAsVersion(activeFile, blocks, containerElement) {
// Collect all textarea values
const textareas = containerElement.querySelectorAll('.block-content');
const updates = new Map();
textareas.forEach(ta => {
const blockId = ta.dataset.blockId;
updates.set(blockId, ta.value);
});
// Reconstruct file content
const lines = [];
function processBlock(block, blockId) {
const updatedContent = updates.get(blockId);
if (block.type === 'container') {
lines.push(activeFile.content.split('\n')[block.startLine]); // Opening marker
block.children.forEach((child, idx) => {
const childId = `${blockId}-child-${idx}`;
processBlock(child, childId);
});
lines.push(activeFile.content.split('\n')[block.endLine]); // Closing marker
} else if (block.type === 'scope') {
lines.push(activeFile.content.split('\n')[block.startLine]); // Opening marker
lines.push(updatedContent);
lines.push(activeFile.content.split('\n')[block.endLine]); // Closing marker
} else if (block.type === 'unmarked') {
lines.push(updatedContent);
}
}
blocks.forEach((block, idx) => {
processBlock(block, `ai-chat-block-${idx}`);
});
const newContent = lines.join('\n');
// Add version to file
addVersionToFile(activeFile.name, newContent);
// Show success message
const btn = containerElement.querySelector('#makeVersionBtn');
if (btn) {
const originalText = btn.textContent;
btn.textContent = '✅ VERSION SAVED';
btn.style.background = '#10b981';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '#3b82f6';
}, 2000);
}
}
// --- Version Management ---
function addVersionToFile(fileName, content) {
const files = getFilesFromLocalStorage();
const file = files.find(f => f.name === fileName);
if (!file) return;
// Initialize versions array if it doesn't exist
if (!file.versions) {
file.versions = [];
}
// Add new version with timestamp
file.versions.push({
content: content,
timestamp: Date.now(),
label: `v${file.versions.length + 1}`
});
// Update current content
file.content = content;
saveFilesToLocalStorage(files);
// Trigger update event
window.dispatchEvent(new Event('activeFilesUpdated'));
}
// Render editable scope block
function renderScopeBlockEditable(block, blockId) {
const style = window.StorageEditorScopes.getLanguageStyle(block.data.language);
const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`;
const lineCount = block.content.split('\n').length;
const textareaHeight = Math.max(80, lineCount * 22 + 32);
const wrapper = document.createElement('div');
wrapper.style.cssText = `
display: flex;
margin-bottom: 12px;
border-radius: 8px;
overflow: hidden;
background: ${style.bg};
`;
const sidebar = document.createElement('div');
sidebar.className = 'scope-toggle';
sidebar.style.cssText = `
background: ${style.color};
width: 40px;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
gap: 8px;
flex-shrink: 0;
cursor: pointer;
user-select: none;
`;
sidebar.innerHTML = `
<span class="toggle-icon" style="font-size: 12px;">▼</span>
<span style="font-size: 20px;">${style.icon}</span>
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-weight: 700;
color: #000;
font-family: monospace;
font-size: 13px;
letter-spacing: 1px;
">${escapeHtml(block.data.name)}</div>
<div style="
background: rgba(0,0,0,0.2);
padding: 4px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
color: #000;
">${style.label}</div>
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 9px;
color: rgba(0,0,0,0.6);
font-weight: 600;
margin-top: auto;
">${lineCount}L</div>
`;
const content = document.createElement('textarea');
content.className = 'block-content';
content.dataset.blockId = blockId;
content.style.cssText = `
flex: 1;
height: ${textareaHeight}px;
background: #1e1e1e;
color: #e6edf3;
border: none;
border-left: 2px solid ${style.color};
padding: 16px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
line-height: 1.6;
resize: none;
outline: none;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
`;
content.value = block.content;
// Toggle functionality
let isExpanded = true;
sidebar.addEventListener('click', () => {
isExpanded = !isExpanded;
const toggle = sidebar.querySelector('.toggle-icon');
if (isExpanded) {
content.style.maxHeight = 'none';
content.style.padding = '16px';
toggle.textContent = '▼';
} else {
content.style.maxHeight = '0';
content.style.padding = '0 16px';
toggle.textContent = '▶';
}
});
wrapper.appendChild(sidebar);
wrapper.appendChild(content);
return wrapper;
}
// Render editable unmarked block
function renderUnmarkedBlockEditable(block, blockId) {
const lineCount = block.endLine - block.startLine + 1;
const textareaHeight = Math.max(60, lineCount * 20 + 24);
const wrapper = document.createElement('div');
wrapper.style.cssText = `
display: flex;
margin-bottom: 12px;
border-radius: 8px;
overflow: hidden;
background: rgba(55, 65, 81, 0.05);
opacity: 0.7;
`;
const sidebar = document.createElement('div');
sidebar.className = 'scope-toggle';
sidebar.style.cssText = `
background: #374151;
width: 40px;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
gap: 8px;
flex-shrink: 0;
cursor: pointer;
user-select: none;
`;
sidebar.innerHTML = `
<span class="toggle-icon" style="font-size: 12px;">▼</span>
<span style="font-size: 18px;">📝</span>
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-weight: 600;
color: #9ca3af;
font-size: 11px;
letter-spacing: 0.5px;
">UNMARKED</div>
${block.container ? `
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 9px;
color: #8b5cf6;
font-weight: 600;
">${escapeHtml(block.container)}</div>
` : ''}
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 9px;
color: #6b7280;
margin-top: auto;
">${lineCount}L</div>
`;
const content = document.createElement('textarea');
content.className = 'block-content';
content.dataset.blockId = blockId;
content.style.cssText = `
flex: 1;
height: ${textareaHeight}px;
background: #1a1a1a;
color: #9ca3af;
border: none;
border-left: 2px dashed #374151;
padding: 12px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
line-height: 1.5;
resize: none;
outline: none;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
`;
content.value = block.content;
// Toggle functionality
let isExpanded = true;
sidebar.addEventListener('click', () => {
isExpanded = !isExpanded;
const toggle = sidebar.querySelector('.toggle-icon');
if (isExpanded) {
content.style.maxHeight = 'none';
content.style.padding = '12px';
toggle.textContent = '▼';
} else {
content.style.maxHeight = '0';
content.style.padding = '0 12px';
toggle.textContent = '▶';
}
});
wrapper.appendChild(sidebar);
wrapper.appendChild(content);
return wrapper;
}
// Render editable container block
function renderContainerBlockEditable(block, blockId) {
const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`;
const wrapper = document.createElement('div');
wrapper.style.cssText = `
background: rgba(139, 92, 246, 0.05);
border: 3px solid #8b5cf6;
border-radius: 12px;
margin-bottom: 20px;
overflow: hidden;
`;
const header = document.createElement('div');
header.style.cssText = `
background: #7c3aed;
padding: 14px 20px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
`;
header.innerHTML = `
<div style="font-weight: 700; color: #fff; display: flex; align-items: center; gap: 12px; font-size: 16px;">
<span class="container-toggle">▼</span>
<span style="font-size: 20px;">📦</span>
<span style="font-family: monospace; text-transform: uppercase; letter-spacing: 0.5px;">
${escapeHtml(block.data.name)}
</span>
<span style="
background: rgba(255,255,255,0.2);
padding: 3px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
">${block.children.length} blocks</span>
</div>
<div style="font-size: 11px; color: rgba(255,255,255,0.7); font-weight: 600;">
${lineRange}
</div>
`;
const body = document.createElement('div');
body.style.cssText = `
padding: 16px;
transition: max-height 0.3s ease;
overflow: hidden;
`;
block.children.forEach((child, idx) => {
const childId = `${blockId}-child-${idx}`;
if (child.type === 'scope') {
body.appendChild(renderScopeBlockEditable(child, childId));
} else if (child.type === 'unmarked') {
const trimmed = child.content.trim();
if (trimmed.length > 0) {
body.appendChild(renderUnmarkedBlockEditable(child, childId));
}
}
});
// Toggle functionality
let isExpanded = true;
header.addEventListener('click', () => {
isExpanded = !isExpanded;
const toggle = header.querySelector('.container-toggle');
if (isExpanded) {
body.style.maxHeight = 'none';
toggle.textContent = '▼';
} else {
body.style.maxHeight = '0';
body.style.padding = '0 16px';
toggle.textContent = '▶';
}
});
wrapper.appendChild(header);
wrapper.appendChild(body);
return wrapper;
}
// Render read-only scope block
function renderScopeBlockReadOnly(block, blockId) {
const style = window.StorageEditorScopes.getLanguageStyle(block.data.language);
const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`;
const lineCount = block.content.split('\n').length;
const wrapper = document.createElement('div');
wrapper.style.cssText = `
display: flex;
margin-bottom: 12px;
border-radius: 8px;
overflow: hidden;
background: ${style.bg};
`;
const sidebar = document.createElement('div');
sidebar.className = 'scope-toggle';
sidebar.style.cssText = `
background: ${style.color};
width: 40px;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
gap: 8px;
flex-shrink: 0;
cursor: pointer;
user-select: none;
`;
sidebar.innerHTML = `
<span class="toggle-icon" style="font-size: 12px;">▼</span>
<span style="font-size: 20px;">${style.icon}</span>
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-weight: 700;
color: #000;
font-family: monospace;
font-size: 13px;
letter-spacing: 1px;
">${escapeHtml(block.data.name)}</div>
<div style="
background: rgba(0,0,0,0.2);
padding: 4px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
color: #000;
">${style.label}</div>
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 9px;
color: rgba(0,0,0,0.6);
font-weight: 600;
margin-top: auto;
">${lineCount}L</div>
`;
const content = document.createElement('pre');
content.style.cssText = `
flex: 1;
margin: 0;
background: #1e1e1e;
color: #e6edf3;
border: none;
border-left: 2px solid ${style.color};
padding: 16px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
transition: max-height 0.3s ease, padding 0.3s ease;
`;
content.textContent = block.content;
// Toggle functionality
let isExpanded = true;
sidebar.addEventListener('click', () => {
isExpanded = !isExpanded;
const toggle = sidebar.querySelector('.toggle-icon');
if (isExpanded) {
content.style.maxHeight = 'none';
content.style.padding = '16px';
toggle.textContent = '▼';
} else {
content.style.maxHeight = '0';
content.style.padding = '0 16px';
toggle.textContent = '▶';
}
});
wrapper.appendChild(sidebar);
wrapper.appendChild(content);
return wrapper;
}
// Render read-only unmarked block
function renderUnmarkedBlockReadOnly(block, blockId) {
const lineCount = block.endLine - block.startLine + 1;
const wrapper = document.createElement('div');
wrapper.style.cssText = `
display: flex;
margin-bottom: 12px;
border-radius: 8px;
overflow: hidden;
background: rgba(55, 65, 81, 0.05);
opacity: 0.7;
`;
const sidebar = document.createElement('div');
sidebar.className = 'scope-toggle';
sidebar.style.cssText = `
background: #374151;
width: 40px;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
gap: 8px;
flex-shrink: 0;
cursor: pointer;
user-select: none;
`;
sidebar.innerHTML = `
<span class="toggle-icon" style="font-size: 12px;">▼</span>
<span style="font-size: 18px;">📝</span>
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-weight: 600;
color: #9ca3af;
font-size: 11px;
letter-spacing: 0.5px;
">UNMARKED</div>
${block.container ? `
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 9px;
color: #8b5cf6;
font-weight: 600;
">${escapeHtml(block.container)}</div>
` : ''}
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 9px;
color: #6b7280;
margin-top: auto;
">${lineCount}L</div>
`;
const content = document.createElement('pre');
content.style.cssText = `
flex: 1;
margin: 0;
background: #1a1a1a;
color: #9ca3af;
border: none;
border-left: 2px dashed #374151;
padding: 12px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
transition: max-height 0.3s ease, padding 0.3s ease;
`;
content.textContent = block.content;
// Toggle functionality
let isExpanded = true;
sidebar.addEventListener('click', () => {
isExpanded = !isExpanded;
const toggle = sidebar.querySelector('.toggle-icon');
if (isExpanded) {
content.style.maxHeight = 'none';
content.style.padding = '12px';
toggle.textContent = '▼';
} else {
content.style.maxHeight = '0';
content.style.padding = '0 12px';
toggle.textContent = '▶';
}
});
wrapper.appendChild(sidebar);
wrapper.appendChild(content);
return wrapper;
}
// Render read-only container block
function renderContainerBlockReadOnly(block, blockId) {
const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`;
const wrapper = document.createElement('div');
wrapper.style.cssText = `
background: rgba(139, 92, 246, 0.05);
border: 3px solid #8b5cf6;
border-radius: 12px;
margin-bottom: 20px;
overflow: hidden;
`;
const header = document.createElement('div');
header.style.cssText = `
background: #7c3aed;
padding: 14px 20px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
`;
header.innerHTML = `
<div style="font-weight: 700; color: #fff; display: flex; align-items: center; gap: 12px; font-size: 16px;">
<span class="container-toggle">▼</span>
<span style="font-size: 20px;">📦</span>
<span style="font-family: monospace; text-transform: uppercase; letter-spacing: 0.5px;">
${escapeHtml(block.data.name)}
</span>
<span style="
background: rgba(255,255,255,0.2);
padding: 3px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
">${block.children.length} blocks</span>
</div>
<div style="font-size: 11px; color: rgba(255,255,255,0.7); font-weight: 600;">
${lineRange}
</div>
`;
const body = document.createElement('div');
body.style.cssText = `
padding: 16px;
transition: max-height 0.3s ease;
overflow: hidden;
`;
block.children.forEach((child, idx) => {
const childId = `${blockId}-child-${idx}`;
if (child.type === 'scope') {
body.appendChild(renderScopeBlockReadOnly(child, childId));
} else if (child.type === 'unmarked') {
const trimmed = child.content.trim();
if (trimmed.length > 0) {
body.appendChild(renderUnmarkedBlockReadOnly(child, childId));
}
}
});
// Toggle functionality
let isExpanded = true;
header.addEventListener('click', () => {
isExpanded = !isExpanded;
const toggle = header.querySelector('.container-toggle');
if (isExpanded) {
body.style.maxHeight = 'none';
toggle.textContent = '▼';
} else {
body.style.maxHeight = '0';
body.style.padding = '0 16px';
toggle.textContent = '▶';
}
});
wrapper.appendChild(header);
wrapper.appendChild(body);
return wrapper;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// --- 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('[ai_chat_scopes] 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 = '';
// Function to get unique scope names from active file
function getAvailableScopes() {
const files = getFilesFromLocalStorage();
const activeFile = files.find(f => f.active);
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 => {
// Get the base name - everything before first _ or -
const baseName = scope.name.split(/[_-]/)[0];
scopeNames.add(baseName);
});
return Array.from(scopeNames).sort();
}
// Function to populate scope selector
function updateScopeSelector() {
const scopes = getAvailableScopes();
const currentValue = scopeSelector.value;
// 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 = '';
currentScope = '';
updateScopeIndicator();
}
}
// Function to update scope indicator
function updateScopeIndicator() {
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';
}
}
// 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();
// 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();
}
} else {
scopeSelector.value = '';
currentScope = '';
updateScopeIndicator();
}
} else {
currentScope = value;
updateScopeIndicator();
}
});
// Initial scope selector population
updateScopeSelector();
// Tab switching function
function switchTab(tabName) {
const tabs = [chatTab, activeFileTab, filesTab];
const contents = [chatContent, activeFileContent, filesContent];
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';
}
}
// Tab click handlers
chatTab.addEventListener('click', () => switchTab('chat'));
activeFileTab.addEventListener('click', () => switchTab('activeFile'));
filesTab.addEventListener('click', () => switchTab('files'));
// Tab hover effects
[chatTab, activeFileTab, filesTab].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) {
renderActiveFileScopes(activeFileContainer);
}
// Update scope selector when file changes
updateScopeSelector();
};
// Function to render files list
const updateFilesList = () => {
if (filesContainer) {
renderFilesList(filesContainer, updateActiveFile);
}
};
// Initial renders
updateActiveFile();
updateFilesList();
// Listen for file changes from Storage Editor
window.addEventListener('activeFilesUpdated', () => {
updateActiveFile();
updateFilesList();
});
// Send message handler
const sendMessage = () => {
const message = input.value.trim();
if (!message) return;
console.log('[ai_chat_scopes] User message:', message);
console.log('[ai_chat_scopes] Target scope:', currentScope || 'none');
input.value = '';
if (messagesPlaceholder && messagesPlaceholder.parentNode) {
messagesPlaceholder.remove();
}
// Add user message with scope indicator
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;
// TODO: Add AI API call here with scope context
setTimeout(() => {
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 = `🤖 AI response will go here... ${currentScope ? `(working on scope: ${currentScope})` : ''}`;
messagesContainer.appendChild(aiMsg);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}, 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.AIChatScopes = {
open: () => {
if (window.AppOverlay) {
window.AppOverlay.open([aiChatSlide]);
} else {
console.error('[ai_chat_scopes] 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('[ai_chat_scopes] Registered as AppItems component');
}
console.log('[ai_chat_scopes] AI Chat with Scopes integration loaded');
})();