// active_file.js - Active File Display + Chat Snippet Rendering (ALL LAYOUT LIVES HERE)
(function() {
console.log("[active_file] Loading Active File Display module...");
// --- Utility Functions ---
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// --- Version Management ---
function addVersionToFile(fileName, content) {
if (!window.FilesManager) {
console.error('[active_file] FilesManager not available');
return;
}
const files = window.FilesManager.getFiles();
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;
window.FilesManager.saveFiles(files);
// Trigger update event
window.dispatchEvent(new Event('activeFilesUpdated'));
}
// -----------------------------------------------------------------------------
// BLOCK RENDERING (EDITABLE â ACTIVE FILE)
// -----------------------------------------------------------------------------
function renderScopeBlockEditable(block, blockId) {
const style = window.StorageEditorScopes.getLanguageStyle(block.data.language);
const lineCount = block.content.split("\n").length;
// More minimum height
const minHeight = Math.max(180, lineCount * 28);
const wrapper = document.createElement("div");
wrapper.style.cssText = `
border: 2px solid ${style.color};
border-radius: 8px;
margin-bottom: 14px;
background: #0f0f0f; /* DARK BG */
overflow: hidden;
`;
// HEADER (DARK THEME)
const header = document.createElement("div");
header.style.cssText = `
padding: 8px 12px;
background: #1a1a1a; /* DARK HEADER */
border-bottom: 2px solid ${style.color};
display: flex;
justify-content: space-between;
align-items: center;
font-family: monospace;
cursor: pointer;
user-select: none;
`;
header.innerHTML = `
<div style="display:flex; align-items:center; gap:10px;">
<span style="font-size:18px; color:${style.color};">${style.icon}</span>
<span style="
font-size:15px;
font-weight:800;
color:#ffffff; /* white name */
">${escapeHtml(block.data.name)}</span>
</div>
<div style="display:flex; align-items:center; gap:8px;">
<span style="
border: 1px solid ${style.color};
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 800;
color: ${style.color};
background: #0f0f0f;
">${style.label}</span>
<span style="
border: 1px solid #555;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 700;
color: #fff;
background: #2a2a2a;
">${lineCount} lines</span>
<span class="toggle-icon"
style="
font-size: 14px;
color: #ffffff;
margin-left: 4px;
"
>âŧ</span>
</div>
`;
// CONTENT AREA (DARK TEXTAREA)
const content = document.createElement("textarea");
content.className = "block-content";
content.dataset.blockId = blockId;
content.dataset.blockType = "scope";
content.dataset.startLine = block.startLine;
content.dataset.endLine = block.endLine;
content.style.cssText = `
width: 100%;
background: #0d0d0d; /* DARK */
color: #ffffff; /* WHITE TEXT */
border: none;
padding: 10px 12px;
font-family: Consolas, monospace;
font-size: 14px;
line-height: 1.55;
resize: vertical;
outline: none;
box-sizing: border-box;
min-height: ${minHeight}px;
transition: max-height 0.25s ease, padding 0.25s ease;
`;
content.value = block.content;
// Collapse state
let isExpanded = true;
header.addEventListener("click", () => {
isExpanded = !isExpanded;
const icon = header.querySelector(".toggle-icon");
if (isExpanded) {
content.style.maxHeight = minHeight + "px";
content.style.padding = "10px 12px";
icon.textContent = "âŧ";
} else {
content.style.maxHeight = "0px";
content.style.padding = "0 12px";
icon.textContent = "âļ";
}
});
wrapper.appendChild(header);
wrapper.appendChild(content);
return wrapper;
}
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 = `
margin-bottom: 12px;
border-radius: 8px;
overflow: hidden;
background: rgba(55,65,81,0.05);
opacity: 0.7;
border: 2px dashed #374151;
`;
// HEADER BAR
const header = document.createElement('div');
header.className = 'scope-toggle';
header.style.cssText = `
width: 100%;
background: #374151;
color: #fff;
padding: 6px 10px;
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
font-size: 13px;
font-weight: 600;
font-family: monospace;
`;
const containerLabel = block.container
? `<span style="color:#8b5cf6; font-size:11px; font-weight:600;">(${escapeHtml(block.container)})</span>`
: '';
header.innerHTML = `
<span class="toggle-icon" style="font-size:12px;">âŧ</span>
<span style="font-size:18px;">đ</span>
<span style="letter-spacing:0.5px;">UNMARKED</span>
${containerLabel}
<span style="margin-left:auto; font-size:11px; color:#d1d5db;">${lineCount}L</span>
`;
const content = document.createElement('textarea');
content.className = 'block-content';
content.dataset.blockId = blockId;
content.dataset.blockType = 'unmarked';
content.dataset.startLine = block.startLine;
content.dataset.endLine = block.endLine;
content.style.cssText = `
width: 100%;
height: ${textareaHeight}px;
background: #1a1a1a;
color: #9ca3af;
border: none;
border-top: 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;
box-sizing: border-box;
`;
content.value = block.content;
let isExpanded = true;
header.addEventListener('click', () => {
isExpanded = !isExpanded;
const toggle = header.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(header);
wrapper.appendChild(content);
return wrapper;
}
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: 10px;
margin-bottom: 14px;
overflow: hidden;
`;
wrapper.dataset.blockType = 'container';
wrapper.dataset.startLine = block.startLine;
wrapper.dataset.endLine = block.endLine;
// HEADER
const header = document.createElement('div');
header.style.cssText = `
background: #7c3aed;
padding: 6px 10px;
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: 8px; font-size: 15px;">
<span class="container-toggle">âŧ</span>
<span style="font-size: 18px;">đĻ</span>
<span style="font-family: monospace; text-transform: uppercase; letter-spacing: .5px;">
${escapeHtml(block.data.name)}
</span>
<span style="
background: rgba(255,255,255,0.25);
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
">${block.children.length} blocks</span>
</div>
<div style="font-size: 10px; color: rgba(255,255,255,0.85); font-weight: 600;">
${lineRange}
</div>
`;
const body = document.createElement('div');
body.style.cssText = `
padding: 0;
transition: max-height .25s 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));
}
}
});
let isExpanded = true;
header.addEventListener('click', () => {
const toggle = header.querySelector('.container-toggle');
if (isExpanded) {
body.style.maxHeight = '0';
toggle.textContent = 'âļ';
} else {
body.style.maxHeight = 'none';
toggle.textContent = 'âŧ';
}
isExpanded = !isExpanded;
});
wrapper.appendChild(header);
wrapper.appendChild(body);
return wrapper;
}
// -----------------------------------------------------------------------------
// SAVE BLOCKS AS VERSION (EDITABLE ACTIVE FILE)
// -----------------------------------------------------------------------------
function injectMetadataIntoScopeBlock(originalLines, update, blockLookup) {
const openLine = originalLines[update.startLine];
const metadataLines = [];
// Timestamp
metadataLines.push(`@updatedAt:${Date.now()}@`);
// User (static for now)
metadataLines.push(`@updatedBy:ai@`);
// Container + Position (if known)
if (blockLookup && blockLookup.container) {
metadataLines.push(`@container:${blockLookup.container}@`);
}
if (blockLookup && typeof blockLookup.position === "number") {
metadataLines.push(`@position:${blockLookup.position}@`);
}
// Placeholder for related scopes
metadataLines.push(`@relatedScopes:@`);
return [
openLine,
...metadataLines,
update.content,
originalLines[update.endLine]
];
}
function buildMetadataLines(blockInfo) {
const metaLines = [];
// Required basics
metaLines.push(`@updatedAt:${Date.now()}@`);
metaLines.push(`@updatedBy:ai@`);
// Container + position, if we know them
if (blockInfo && blockInfo.container) {
metaLines.push(`@container:${blockInfo.container}@`);
}
if (blockInfo && typeof blockInfo.position === "number") {
metaLines.push(`@position:${blockInfo.position}@`);
}
// Placeholder for future AI wiring
metaLines.push(`@relatedScopes:@`);
return metaLines;
}
function stripExistingMetadata(rawContent) {
if (!rawContent) return "";
const lines = rawContent.split("\n");
const cleaned = [];
let skipping = true;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Metadata lines look like: @key:value@
const trimmed = line.trim();
const isMeta =
trimmed.startsWith("@") &&
trimmed.endsWith("@") &&
trimmed.includes(":");
if (skipping && isMeta) {
// skip this line
continue;
} else {
skipping = false;
cleaned.push(line);
}
}
return cleaned.join("\n");
}
function saveBlocksAsVersion(activeFile, blocks, containerElement) {
const originalLines = activeFile.content.split('\n');
const textareas = containerElement.querySelectorAll('.block-content');
// Build scope metadata map from blocks:
// key: "startLine-endLine" â { container, position }
const scopeMeta = new Map();
// First pass: collect info from block structure
blocks.forEach(topBlock => {
if (topBlock.type === 'container') {
const containerName = topBlock.data?.name || null;
const scopeChildren = topBlock.children.filter(c => c.type === 'scope');
scopeChildren.forEach((childScope, idx) => {
const key = `${childScope.startLine}-${childScope.endLine}`;
scopeMeta.set(key, {
container: containerName,
position: idx + 1
});
});
} else if (topBlock.type === 'scope') {
// Top-level scope (no container)
const key = `${topBlock.startLine}-${topBlock.endLine}`;
scopeMeta.set(key, {
container: null,
position: null
});
}
});
// Create a map of line ranges to new content (as before)
const lineUpdates = new Map();
textareas.forEach(ta => {
const blockType = ta.dataset.blockType;
const startLine = parseInt(ta.dataset.startLine, 10);
const endLine = parseInt(ta.dataset.endLine, 10);
if (blockType === 'scope') {
// We'll strip any old metadata from the textarea content
const cleanedContent = stripExistingMetadata(ta.value);
lineUpdates.set(`${startLine}-${endLine}`, {
type: 'scope',
content: cleanedContent,
startLine,
endLine
});
} else if (blockType === 'unmarked') {
lineUpdates.set(`${startLine}-${endLine}`, {
type: 'unmarked',
content: ta.value,
startLine,
endLine
});
}
});
// Reconstruct the file (preserve your existing logic)
const newLines = [];
let lineIndex = 0;
while (lineIndex < originalLines.length) {
let handled = false;
for (const [key, update] of lineUpdates) {
if (lineIndex === update.startLine) {
if (update.type === 'scope') {
// Get metadata for this scope (container + position)
const blockInfo = scopeMeta.get(key) || null;
// Opening marker
newLines.push(originalLines[update.startLine]);
// Metadata lines (below marker)
const metaLines = buildMetadataLines(blockInfo);
metaLines.forEach(line => newLines.push(line));
// Updated content
newLines.push(update.content);
// Closing marker
newLines.push(originalLines[update.endLine]);
lineIndex = update.endLine + 1;
} else if (update.type === 'unmarked') {
// Replace entire unmarked block
newLines.push(update.content);
lineIndex = update.endLine + 1;
}
handled = true;
break;
}
}
if (!handled) {
const line = originalLines[lineIndex];
// Preserve container markers exactly as-is
if (
line.trim().startsWith('/*<CONTAINER') ||
line.trim().startsWith('<!--<CONTAINER')
) {
newLines.push(line);
lineIndex++;
} else if (
line.trim().startsWith('</CONTAINER>') ||
line.trim().startsWith('<!--</CONTAINER>')
) {
newLines.push(line);
lineIndex++;
} else {
// Skip lines that are inside blocks we've already processed
let insideProcessedBlock = false;
for (const [, update] of lineUpdates) {
if (lineIndex > update.startLine && lineIndex <= update.endLine) {
insideProcessedBlock = true;
break;
}
}
if (!insideProcessedBlock) {
newLines.push(line);
}
lineIndex++;
}
}
}
const newContent = newLines.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);
}
}
// -----------------------------------------------------------------------------
// MAIN ACTIVE FILE RENDER FUNCTION (EDITABLE)
// -----------------------------------------------------------------------------
function renderActiveFileScopes(container) {
if (!window.FilesManager) {
console.error('[active_file] FilesManager not available');
return;
}
const files = window.FilesManager.getFiles();
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 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;
}
const wrapper = document.createElement('div');
wrapper.style.cssText = `
background: #1a1a1a;
border: 2px solid #2a2a2a;
border-radius: 8px;
margin-bottom: 20px;
overflow: hidden;
`;
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>
`;
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') {
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';
});
}
}
// -----------------------------------------------------------------------------
// CHAT SNIPPET RENDERING (READ-ONLY) â ALL LAYOUT HERE
// -----------------------------------------------------------------------------
// A read-only scope block (for Chat) â HTML string
function renderScopeBlockChat(block, blockId, isInChat) {
const style = window.StorageEditorScopes.getLanguageStyle(block.data.language);
const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`;
// Build metadata badges
let metadataBadges = '';
if (block.data.header) {
const h = block.data.header;
const actionIcon = h.action === 'new' ? 'â¨' : 'âī¸';
metadataBadges += `
<div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;">
<span style="color: #6b7280; font-size: 11px; font-weight: 600;">Container:</span>
<span style="color: #111827; font-size: 11px; font-family: monospace;">${escapeHtml(h.container)}</span>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;">
<span style="color: #6b7280; font-size: 11px; font-weight: 600;">Position:</span>
<span style="color: #111827; font-size: 11px; font-family: monospace;">#${h.position}</span>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;">
<span style="color: #6b7280; font-size: 11px; font-weight: 600;">Action:</span>
<span style="color: #111827; font-size: 11px; font-family: monospace;">${actionIcon} ${h.action.toUpperCase()}</span>
</div>
`;
} else if (block.data.container) {
metadataBadges += `
<div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;">
<span style="color: #6b7280; font-size: 11px; font-weight: 600;">Container:</span>
<span style="color: #111827; font-size: 11px; font-family: monospace;">${escapeHtml(block.data.container)}</span>
</div>
`;
}
// Language
metadataBadges += `
<div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;">
<span style="color: #6b7280; font-size: 11px; font-weight: 600;">Language:</span>
<span style="color: #111827; font-size: 11px; font-family: monospace;">${style.label}</span>
</div>
`;
// Attributes
if (block.data.attributes) {
const attrs = block.data.attributes;
if (attrs.editedBy) {
metadataBadges += `
<div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;">
<span style="color: #6b7280; font-size: 11px; font-weight: 600;">Edited By:</span>
<span style="color: #111827; font-size: 11px; font-family: monospace;">${escapeHtml(attrs.editedBy)}</span>
</div>
`;
}
if (attrs.editedAt) {
const timestamp = attrs.editedAt;
let displayTime = timestamp;
try {
const date = new Date(parseInt(timestamp));
if (!isNaN(date.getTime())) {
displayTime = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
}
} catch (e) {}
metadataBadges += `
<div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;">
<span style="color: #6b7280; font-size: 11px; font-weight: 600;">Edited At:</span>
<span style="color: #111827; font-size: 11px;">${escapeHtml(displayTime)}</span>
</div>
`;
}
Object.keys(attrs).forEach(key => {
if (key !== 'editedBy' && key !== 'editedAt') {
metadataBadges += `
<div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;">
<span style="color: #6b7280; font-size: 11px; font-weight: 600;">${escapeHtml(key)}:</span>
<span style="color: #111827; font-size: 11px; font-family: monospace;">${escapeHtml(attrs[key])}</span>
</div>
`;
}
});
}
if (!isInChat) {
metadataBadges += `
<div style="padding: 8px 12px; display: flex; justify-content: space-between;">
<span style="color: #6b7280; font-size: 11px; font-weight: 600;">Lines:</span>
<span style="color: #111827; font-size: 11px; font-family: monospace;">${lineRange}</span>
</div>
`;
}
// Chat action footer
let actionFooter = '';
if (isInChat && block.data.header) {
const h = block.data.header;
const btnText = h.action === 'new' ? '⨠INSERT INTO FILE' : 'âī¸ REPLACE IN FILE';
const btnId = `action-btn-${blockId}`;
actionFooter = `
<div style="padding: 12px; background: #ffffff; border-top: 2px solid ${style.color};">
<button id="${btnId}" class="scope-action-btn" style="
background: #ffffff;
color: #111827;
border: 2px solid #111827;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-weight: 700;
font-size: 12px;
width: 100%;
text-transform: uppercase;
letter-spacing: 0.5px;
transition: all 0.2s;
"
data-action="${h.action}"
data-container="${escapeHtml(h.container)}"
data-position="${h.position}"
data-scope-name="${escapeHtml(block.data.name)}"
data-language="${escapeHtml(h.language)}"
data-block-id="${blockId}"
onmouseover="this.style.background='#111827'; this.style.color='#ffffff'"
onmouseout="this.style.background='#ffffff'; this.style.color='#111827'"
>${btnText}</button>
</div>
`;
}
const metadataMenuId = `metadata-menu-${blockId}`;
return `
<div class="block-scope" data-block-id="${blockId}" style="
position: relative;
display: flex;
flex-direction: column;
margin-bottom: 16px;
border-radius: 6px;
overflow: hidden;
border: 2px solid ${style.color};
background: #ffffff;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
">
<!-- Header -->
<div style="
background: #ffffff;
padding: 10px 16px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 2px solid ${style.color};
">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 18px;">${style.icon}</span>
<div style="
font-weight: 700;
color: #111827;
font-family: monospace;
font-size: 13px;
letter-spacing: 0.3px;
">${escapeHtml(block.data.name)}</div>
<div style="
background: ${style.bg};
border: 1px solid ${style.color};
padding: 2px 8px;
border-radius: 3px;
font-size: 10px;
font-weight: 700;
color: #111827;
text-transform: uppercase;
">${style.label}</div>
</div>
<button
onclick="(function(e) {
e.stopPropagation();
const menu = document.getElementById('${metadataMenuId}');
const isVisible = menu.style.display === 'block';
menu.style.display = isVisible ? 'none' : 'block';
})(event)"
style="
background: #ffffff;
border: 1px solid #111827;
color: #111827;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
font-weight: 600;
transition: all 0.2s;
"
onmouseover="this.style.background='#f3f4f6'"
onmouseout="this.style.background='#ffffff'"
>âšī¸ Info</button>
</div>
<!-- Pull-up Metadata Menu -->
<div id="${metadataMenuId}" style="
display: none;
position: fixed;
bottom: ${isInChat ? '100px' : '20px'};
left: 50%;
transform: translateX(-50%);
width: 90%;
max-width: 500px;
background: #ffffff;
border: 2px solid ${style.color};
border-radius: 8px;
box-shadow: 0 20px 50px rgba(0,0,0,0.3);
z-index: 2147483647;
max-height: 400px;
overflow-y: auto;
">
${metadataBadges}
</div>
<!-- Content Area -->
<textarea class="block-content" data-block-id="${blockId}" style="
width: 100%;
height: 300px;
background: #ffffff;
color: #111827;
border: none;
padding: 16px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
line-height: 1.6;
resize: none;
outline: none;
overflow-y: auto;
box-sizing: border-box;
pointer-events: none;
user-select: text;
">${escapeHtml(block.content)}</textarea>
${actionFooter}
</div>
`;
}
// Universal snippet renderer used by Chat â ALL LAYOUT NOW LIVES HERE
function renderChatAnswer(answerText, parentElement) {
if (!answerText) return "<pre></pre>";
const parseScopes = window.StorageEditorScopes && window.StorageEditorScopes.parseScopes;
// Only parse if markers likely exist and parser is present
if (parseScopes && (answerText.includes("<!--") || answerText.includes("//") || answerText.includes("/*"))) {
try {
const parsed = parseScopes(answerText);
if (parsed.scopes && parsed.scopes.length > 0) {
let html = "";
const lines = answerText.split('\n');
parsed.scopes.forEach((scope, idx) => {
const scopeContent = lines.slice(scope.startLine + 1, scope.endLine).join('\n');
html += renderScopeBlockChat(
{
data: {
language: scope.language,
name: scope.name,
header: scope.header,
container: scope.container,
attributes: scope.attributes
},
startLine: scope.startLine,
endLine: scope.endLine,
content: scopeContent
},
"scope-render-" + Date.now() + "-" + idx,
true // isInChat
);
});
if (parentElement) {
parentElement.innerHTML = html;
// Setup action buttons (insert / replace into file)
setTimeout(() => {
parentElement.querySelectorAll('.scope-action-btn').forEach(btn => {
btn.addEventListener('click', function() {
const action = this.dataset.action;
const container = this.dataset.container;
const position = parseInt(this.dataset.position);
const scopeName = this.dataset.scopeName;
const language = this.dataset.language;
const blockId = this.dataset.blockId;
const textarea = parentElement.querySelector(`.block-content[data-block-id="${blockId}"]`);
const content = textarea ? textarea.value : '';
try {
const attributes = {
editedBy: 'ai',
editedAt: Date.now().toString()
};
let result;
if (action === 'new') {
result = window.StorageEditorScopes.insertAt(container, position, {
name: scopeName,
language: language,
content: content,
attributes: attributes
});
} else if (action === 'edit') {
result = window.StorageEditorScopes.replace(container, position, scopeName, content, attributes);
}
this.style.background = '#10b981';
this.textContent = 'â
APPLIED';
this.disabled = true;
this.style.cursor = 'not-allowed';
console.log('[ScopeAction]', result);
setTimeout(() => {
this.style.transition = 'opacity 0.3s';
this.style.opacity = '0';
setTimeout(() => this.remove(), 300);
}, 2000);
} catch (error) {
console.error('[ScopeAction] Error:', error);
this.style.background = '#ef4444';
this.textContent = 'â ERROR';
alert('Error: ' + error.message);
}
});
});
}, 100);
// Clean visual markers out of textareas
setTimeout(() => {
parentElement.querySelectorAll("textarea.block-content").forEach(area => {
area.value = area.value
// HTML markers <!-- name< --> OR <!-- name> -->
.replace(/<!--\s*[a-z0-9_-]+<\s*-->/gi, "")
.replace(/<!--\s*[a-z0-9_-]+>\s*-->/gi, "")
// CSS markers /* name< */ /* name> */
.replace(/\/\*\s*[a-z0-9_-]+<\s*\*\//gi, "")
.replace(/\/\*\s*[a-z0-9_-]+>\s*\*\//gi, "")
// JS markers // name< // name>
.replace(/\/\/\s*[a-z0-9_-]+</gi, "")
.replace(/\/\/\s*[a-z0-9_-]+>/gi, "")
.trim();
});
}, 120);
}
return html;
}
} catch (err) {
console.error("Snippet parsing failed:", err);
return `<pre>${escapeHtml(answerText)}</pre>`;
}
}
// Not a scope-formatted snippet â return plain
if (parentElement) {
parentElement.innerHTML = `<pre>${escapeHtml(answerText)}</pre>`;
}
return `<pre>${escapeHtml(answerText)}</pre>`;
}
// -----------------------------------------------------------------------------
// EXPOSE API
// -----------------------------------------------------------------------------
window.ActiveFileDisplay = {
// Editable active file view
render: renderActiveFileScopes,
// Chat snippet renderer (replaces StorageEditorScopes.renderAnswer)
renderAnswer: renderChatAnswer
};
console.log('[active_file] Active File Display module loaded');
})();