// active_file.js - Active File Display + Chat Snippet Rendering (ALL LAYOUT LIVES HERE)
(function() {
console.log("[active_file] Loading Active File Display module...");
window.SelectedScopes = [];
// --- 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'));
}
/*
function updateLiveMetadata(block) {
const username = window.CurrentUser?.username || "guest";
const now = Date.now();
if (!block.data.metadata) {
block.data.metadata = {};
}
block.data.metadata.updatedAt = now;
block.data.metadata.updatedBy = username;
// Refresh metadata badges if visible
if (typeof refreshMetadataUI === "function") {
refreshMetadataUI(block);
}
}
function debounce(fn, delay = 500) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
*/
// -----------------------------------------------------------------------------
// BLOCK RENDERING (EDITABLE ā ACTIVE FILE)
// -----------------------------------------------------------------------------
function renderScopeBlockEditable(block, blockId) {
if (!window.SelectedScopes) window.SelectedScopes = [];
// ---------- Debounce ----------
function debounce(fn, delay = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// ---------- Strip Metadata Safety (JS / HTML comment blocks) ----------
function stripAttributeComments(text, language) {
if (!text) return text;
const lines = text.split("\n");
const cleaned = [];
let inside = false;
const isHTML = (language === "html" || language === "htm");
for (let line of lines) {
const t = line.trim();
if (isHTML) {
if (!inside && t.startsWith("<!--") && t.includes("@")) {
inside = true;
continue;
}
if (inside) {
if (t.endsWith("-->")) inside = false;
continue;
}
} else {
if (!inside && t.startsWith("/*") && t.includes("@")) {
inside = true;
continue;
}
if (inside) {
if (t.endsWith("*/")) inside = false;
continue;
}
}
cleaned.push(line);
}
return cleaned.join("\n");
}
// ---------- Metadata Live Update ----------
function updateLiveMetadata() {
if (!block.data.metadata) block.data.metadata = {};
block.data.metadata.updatedBy = window.CurrentUser?.username || "guest";
block.data.metadata.updatedAt = Date.now();
renderMetadataPanel();
}
// ----------- RENDER METADATA "MENU" -----------
const metadataPanel = document.createElement("div");
metadataPanel.style.cssText = `
padding: 8px 16px;
background: #111827;
border-bottom: 1px solid #1f2937;
color: #d1d5db;
font-size: 12px;
display: none;
`;
function renderMetadataPanel() {
const md = block.data.metadata || {};
let html = "";
for (const key in md) {
html += `<div style="margin:2px 0;">${key}: <strong>${escapeHtml(String(md[key]))}</strong></div>`;
}
metadataPanel.innerHTML = html;
metadataPanel.style.display = html ? "block" : "none";
}
// ---------- LANG STYLE ----------
const style = window.StorageEditorScopes.getLanguageStyle(block.data.language);
const lineCount = block.content.split("\n").length;
const minHeight = Math.max(180, lineCount * 28);
// ---------- WRAPPER ----------
const wrapper = document.createElement("div");
wrapper.style.cssText = `
border: 2px solid ${style.color};
border-radius: 8px;
margin-bottom: 14px;
background: #0f0f0f;
overflow: hidden;
`;
// ---------- HEADER ----------
const header = document.createElement("div");
header.style.cssText = `
padding: 8px 12px;
background: #1a1a1a;
border-bottom: 2px solid ${style.color};
display: flex;
justify-content: space-between;
align-items: center;
font-family: monospace;
`;
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;">
${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;
color: ${style.color};
background: #0f0f0f;
">${style.label}</span>
<span style="
border: 1px solid #555;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
color: #fff;
background: #2a2a2a;
">${lineCount} lines</span>
<button class="add-scope-btn" style="
padding: 2px 6px;
font-size: 10px;
background:#2563eb;
border:1px solid #1e40af;
border-radius:4px;
color:white;
cursor:pointer;
">ā</button>
<span class="toggle-icon" style="cursor:pointer; font-size:14px; color:#fff;">ā¼</span>
</div>
`;
// ---------- TEXTAREA ----------
const content = document.createElement("textarea");
content.className = "block-content";
content.dataset.blockType = "scope";
content.dataset.startLine = block.startLine;
content.dataset.endLine = block.endLine;
content.style.cssText = `
width: 100%;
background: #0d0d0d;
color: #fff;
border: none;
padding: 10px 12px;
font-family: Consolas, monospace;
font-size: 14px;
resize: vertical;
min-height: ${minHeight}px;
line-height: 1.55;
`;
// Clean metadata comments out (just in case parser missed something)
content.value = stripAttributeComments(block.content, block.data.language);
content.addEventListener("input", debounce(updateLiveMetadata, 350));
// ---------- TOGGLE ----------
let expanded = true;
const toggleIcon = header.querySelector(".toggle-icon");
toggleIcon.addEventListener("click", (e) => {
e.stopPropagation();
expanded = !expanded;
toggleIcon.textContent = expanded ? "ā¼" : "ā¶";
content.style.maxHeight = expanded ? minHeight + "px" : "0px";
content.style.padding = expanded ? "10px 12px" : "0 12px";
metadataPanel.style.display = expanded ? "block" : "none";
});
// ---------- SELECT / DESELECT BUTTON ----------
const addBtn = header.querySelector(".add-scope-btn");
let isSelected = false;
addBtn.addEventListener("click", (e) => {
e.stopPropagation();
isSelected = !isSelected;
if (isSelected) {
window.SelectedScopes.push(JSON.parse(JSON.stringify(block)));
addBtn.textContent = "ā";
addBtn.style.background = "#10b981";
addBtn.style.borderColor = "#059669";
} else {
window.SelectedScopes = window.SelectedScopes.filter(
s => !(s.data?.name === block.data.name && s.startLine === block.startLine)
);
addBtn.textContent = "ā";
addBtn.style.background = "#2563eb";
addBtn.style.borderColor = "#1e40af";
}
});
// ---------- BUILD BLOCK ----------
wrapper.appendChild(header);
wrapper.appendChild(metadataPanel);
wrapper.appendChild(content);
// Show metadata UI if exists
renderMetadataPanel();
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].trim();
// Metadata format: @key:value@
const isMeta = /^@[A-Za-z0-9_\-]+:.*?@$/.test(line);
if (skipping && isMeta) {
// Skip top metadata lines
continue;
}
// The moment we hit a non-metadata line, stop skipping
skipping = false;
cleaned.push(lines[i]);
}
return cleaned.join("\n");
}
function saveBlocksAsVersion(activeFile, blocks, containerElement) {
const originalLines = activeFile.content.split('\n');
const textareas = containerElement.querySelectorAll('.block-content');
// ---------------------------------------------------------------------------
// 1. BUILD MAP: SCOPE RANGE ā { container, position, language }
// ---------------------------------------------------------------------------
const scopeMeta = new Map();
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,
language: childScope.data?.language || 'js'
});
});
} else if (topBlock.type === 'scope') {
const key = `${topBlock.startLine}-${topBlock.endLine}`;
scopeMeta.set(key, {
container: null,
position: null,
language: topBlock.data?.language || 'js'
});
}
});
// ---------------------------------------------------------------------------
// 2. COMMENT FORMAT HELPERS (LANGUAGE-AWARE)
// ---------------------------------------------------------------------------
function getCommentDelimiters(lang) {
// Any HTML-ish scope
if (lang === 'html' || lang === 'blade' || lang === 'vue-html') {
return { open: '<!--', close: '-->' };
}
// Default: JS / TS / CSS / PHP style block comments
return { open: '/*', close: '*/' };
}
function buildMetadataCommentBlock(lang, meta) {
const { open, close } = getCommentDelimiters(lang);
const lines = [];
if (lang === 'html' || lang === 'blade' || lang === 'vue-html') {
// HTML comment style
lines.push(`${open}`);
if (meta.updatedAt) lines.push(` @updatedAt:${meta.updatedAt}@`);
if (meta.updatedBy) lines.push(` @updatedBy:${meta.updatedBy}@`);
if (meta.container) lines.push(` @container:${meta.container}@`);
if (meta.position != null) lines.push(` @position:${meta.position}@`);
if (Array.isArray(meta.relatedScopes) && meta.relatedScopes.length) {
lines.push(` @relatedScopes:${meta.relatedScopes.join(', ')}@`);
}
lines.push(`${close}`);
} else {
// JS / PHP / CSS block comment style
lines.push(`${open}`);
if (meta.updatedAt) lines.push(` * @updatedAt:${meta.updatedAt}@`);
if (meta.updatedBy) lines.push(` * @updatedBy:${meta.updatedBy}@`);
if (meta.container) lines.push(` * @container:${meta.container}@`);
if (meta.position != null) lines.push(` * @position:${meta.position}@`);
if (Array.isArray(meta.relatedScopes) && meta.relatedScopes.length) {
lines.push(` * @relatedScopes:${meta.relatedScopes.join(', ')}@`);
}
lines.push(`${close}`);
}
return lines;
}
// ---------------------------------------------------------------------------
// 3. CAPTURE UPDATED TEXTAREAS (CONTENT ONLY)
// ---------------------------------------------------------------------------
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') {
// If you still have legacy bare @lines, you can run stripExistingMetadata,
// otherwise this is effectively just ta.value.
const cleanedContent = stripExistingMetadata
? stripExistingMetadata(ta.value)
: 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
});
}
});
// ---------------------------------------------------------------------------
// 4. RECONSTRUCT FILE (WITH LANGUAGE-CORRECT METADATA COMMENTS)
// ---------------------------------------------------------------------------
const newLines = [];
let lineIndex = 0;
while (lineIndex < originalLines.length) {
let handled = false;
for (const [key, update] of lineUpdates) {
if (lineIndex === update.startLine) {
// ====== DELETE EMPTY SCOPES COMPLETELY ======
if (update.type === 'scope') {
const trimmed = update.content.trim();
if (trimmed.length === 0) {
// Skip everything from startLine to endLine
lineIndex = update.endLine + 1;
handled = true;
break;
}
}
// ====== SCOPE: INSERT COMMENT-WRAPPED METADATA ======
if (update.type === 'scope') {
const blockInfo = scopeMeta.get(key) || {};
const username = window.CurrentUser?.username || "guest";
const timestamp = Date.now();
const metaObj = {
updatedAt: timestamp,
updatedBy: username
};
if (blockInfo.container) metaObj.container = blockInfo.container;
if (blockInfo.position != null) metaObj.position = blockInfo.position;
const lang = blockInfo.language || 'js';
const metaCommentLines = buildMetadataCommentBlock(lang, metaObj);
// opening marker line
newLines.push(originalLines[update.startLine]);
// metadata comment block (in the RIGHT language)
metaCommentLines.forEach(l => newLines.push(l));
// scope body
newLines.push(update.content);
// closing marker line
newLines.push(originalLines[update.endLine]);
lineIndex = update.endLine + 1;
handled = true;
}
// ====== UNMARKED BLOCK: JUST REPLACE ======
else if (update.type === 'unmarked') {
newLines.push(update.content);
lineIndex = update.endLine + 1;
handled = true;
}
break;
}
}
if (!handled) {
const line = originalLines[lineIndex];
// Preserve container markers exactly
if (
line.trim().startsWith('/*<CONTAINER') ||
line.trim().startsWith('<!--<CONTAINER')
) {
newLines.push(line);
lineIndex++;
continue;
}
if (
line.trim().startsWith('</CONTAINER>') ||
line.trim().startsWith('<!--</CONTAINER>')
) {
newLines.push(line);
lineIndex++;
continue;
}
// Skip any lines that belong to ranges we already updated
let skip = false;
for (const [, upd] of lineUpdates) {
if (lineIndex > upd.startLine && lineIndex <= upd.endLine) {
skip = true;
break;
}
}
if (!skip) newLines.push(line);
lineIndex++;
}
}
// ---------------------------------------------------------------------------
// 5. JOIN FINAL CONTENT & SAVE VERSION
// ---------------------------------------------------------------------------
const newContent = newLines.join('\n');
addVersionToFile(activeFile.name, newContent);
// UI: Success Feedback
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;
}
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;
`;
// ------------------------------------------------------
// ADD SORTING DROPDOWN
// ------------------------------------------------------
const sorter = document.createElement('select');
sorter.style.cssText = `
margin-bottom: 14px;
padding: 6px 10px;
border-radius: 6px;
background: #111;
color: white;
border: 1px solid #444;
font-size: 12px;
`;
sorter.innerHTML = `
<option value="normal">Normal View</option>
<option value="az">Scopes AāZ</option>
<option value="za">Scopes ZāA</option>
<option value="newest">Updated (Newest)</option>
<option value="oldest">Updated (Oldest)</option>
`;
contentArea.appendChild(sorter);
// ------------------------------------------------------
// BLOCK RENDER FUNCTION
// ------------------------------------------------------
function renderBlocks() {
// Clear everything below dropdown
const existing = contentArea.querySelectorAll('.rendered-block');
existing.forEach(e => e.remove());
// NORMAL VIEW ā your original behavior
if (sorter.value === 'normal') {
blocks.forEach((block, idx) => {
const blockId = `ai-chat-block-${idx}`;
let el = null;
if (block.type === 'container') {
el = renderContainerBlockEditable(block, blockId);
} else if (block.type === 'scope') {
el = renderScopeBlockEditable(block, blockId);
} else if (block.type === 'unmarked') {
const trimmed = block.content.trim();
if (trimmed.length > 0) {
el = renderUnmarkedBlockEditable(block, blockId);
}
}
if (el) {
el.classList.add('rendered-block');
contentArea.appendChild(el);
}
});
return;
}
// ------------------------------------------------------
// SCOPE-ONLY SORTED VIEW
// ------------------------------------------------------
let allScopes = [];
blocks.forEach(b => {
if (b.type === 'scope') allScopes.push(b);
if (b.type === 'container' && b.children) {
b.children.forEach(c => {
if (c.type === 'scope') allScopes.push(c);
});
}
});
// Apply sort mode
if (sorter.value === 'az') {
allScopes.sort((a, b) => (a.data.name || '').localeCompare(b.data.name || ''));
} else if (sorter.value === 'za') {
allScopes.sort((a, b) => (b.data.name || '').localeCompare(a.data.name || ''));
} else if (sorter.value === 'newest') {
allScopes.sort((a, b) => (b.data.metadata?.updatedAt || 0) - (a.data.metadata?.updatedAt || 0));
} else if (sorter.value === 'oldest') {
allScopes.sort((a, b) => (a.data.metadata?.updatedAt || 0) - (b.data.metadata?.updatedAt || 0));
}
// Render scopes ONLY
allScopes.forEach((block, idx) => {
const blockId = `sorted-scope-${idx}`;
const el = renderScopeBlockEditable(block, blockId);
el.classList.add('rendered-block');
contentArea.appendChild(el);
});
}
sorter.addEventListener('change', renderBlocks);
// Initial render
renderBlocks();
wrapper.appendChild(header);
wrapper.appendChild(contentArea);
container.appendChild(wrapper);
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');
})();