šŸ“œ
active_file.js
← Back
šŸ“ Javascript ⚔ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// 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'); })();