๐Ÿ“œ
files_copy2.js
โ† Back
๐Ÿ“ Javascript โšก Executable Ctrl+S: Save โ€ข Ctrl+R: Run โ€ข Ctrl+F: Find
// files.js - File Management and Display (function() { console.log("[files] Loading File Management module..."); // --- Utility Functions --- function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // --- Extract unique scope prefixes from file --- function extractScopePrefixes(content) { const lines = content.split('\n'); const prefixes = new Set(); const scopeRegex = /\/\*\s*([a-z0-9_-]+)-[a-z]+-[a-z0-9]+<\s*\*\/|<!--\s*([a-z0-9_-]+)-[a-z]+-[a-z0-9]+<\s*-->|\/\/\s*([a-z0-9_-]+)-[a-z]+-[a-z0-9]+</gi; for (const line of lines) { let m; while ((m = scopeRegex.exec(line))) { const name = m[1] || m[2] || m[3]; if (name) { prefixes.add(name.split('-')[0]); // just the prefix } } } return Array.from(prefixes).sort(); } // --- Analyze scope content and generate summary --- function analyzeScopeContent(content, language) { const summary = { functions: [], vars: [], classes: [], elements: [], description: '' }; const lines = content.split('\n'); for (let line of lines) { const trimmed = line.trim(); // JavaScript/TypeScript if (language === 'js' || language === 'ts') { // Function declarations: function name() or const name = () => if (trimmed.match(/^function\s+([a-zA-Z0-9_]+)/)) { const match = trimmed.match(/^function\s+([a-zA-Z0-9_]+)/); if (match) summary.functions.push(match[1]); } if (trimmed.match(/^(?:const|let|var)\s+([a-zA-Z0-9_]+)\s*=\s*(?:function|\(.*\)\s*=>)/)) { const match = trimmed.match(/^(?:const|let|var)\s+([a-zA-Z0-9_]+)/); if (match) summary.functions.push(match[1]); } // Variables: const/let/var if (trimmed.match(/^(?:const|let|var)\s+([a-zA-Z0-9_]+)\s*=/)) { const match = trimmed.match(/^(?:const|let|var)\s+([a-zA-Z0-9_]+)/); if (match && !summary.functions.includes(match[1])) { summary.vars.push(match[1]); } } // Classes if (trimmed.match(/^class\s+([a-zA-Z0-9_]+)/)) { const match = trimmed.match(/^class\s+([a-zA-Z0-9_]+)/); if (match) summary.classes.push(match[1]); } } // CSS if (language === 'css') { // Class selectors: .className if (trimmed.match(/^\.([a-zA-Z0-9_-]+)\s*\{/)) { const match = trimmed.match(/^\.([a-zA-Z0-9_-]+)/); if (match) summary.classes.push(match[1]); } // Element selectors: header, nav, etc. if (trimmed.match(/^([a-z]+)\s*\{/) && !trimmed.startsWith('.') && !trimmed.startsWith('#')) { const match = trimmed.match(/^([a-z]+)/); if (match) summary.elements.push(match[1]); } } // HTML if (language === 'html') { // Elements: <div, <button, etc. const elementMatches = trimmed.matchAll(/<([a-z]+)[\s>]/g); for (const match of elementMatches) { if (!summary.elements.includes(match[1])) { summary.elements.push(match[1]); } } // IDs: id="something" const idMatches = trimmed.matchAll(/id=["']([a-zA-Z0-9_-]+)["']/g); for (const match of idMatches) { summary.vars.push('#' + match[1]); } // Classes: class="something" const classMatches = trimmed.matchAll(/class=["']([a-zA-Z0-9_\s-]+)["']/g); for (const match of classMatches) { const classes = match[1].split(/\s+/); classes.forEach(cls => { if (cls && !summary.classes.includes(cls)) { summary.classes.push('.' + cls); } }); } } } // Generate description based on content if (summary.functions.length > 0) { summary.description = `Handles ${summary.functions.slice(0, 2).join(', ')}${summary.functions.length > 2 ? '...' : ''}`; } else if (summary.classes.length > 0) { summary.description = `Styles for ${summary.classes.slice(0, 2).join(', ')}${summary.classes.length > 2 ? '...' : ''}`; } else if (summary.elements.length > 0) { summary.description = `Contains ${summary.elements.slice(0, 2).join(', ')}${summary.elements.length > 2 ? '...' : ''}`; } return summary; } // --- Generate scope summary comment --- function generateScopeSummary(content, language) { const summary = analyzeScopeContent(content, language); let lines = []; if (summary.functions.length > 0) { lines.push('functions:'); summary.functions.forEach(fn => lines.push(' ' + fn)); } if (summary.vars.length > 0) { lines.push('vars:'); summary.vars.forEach(v => lines.push(' ' + v)); } if (summary.classes.length > 0) { lines.push('classes:'); summary.classes.forEach(cls => lines.push(' ' + cls)); } if (summary.elements.length > 0) { lines.push('elements:'); summary.elements.forEach(el => lines.push(' ' + el)); } if (summary.description) { lines.push(''); lines.push('description:'); lines.push(' ' + summary.description); } if (lines.length === 0) { lines.push('(empty scope)'); } // Format as comment based on language if (language === 'html') { return '<!--\n' + lines.join('\n') + '\n-->'; } else { return '/*\n' + lines.join('\n') + '\n*/'; } } // --- Filter content to only include specific scope prefix --- function filterByScope(content, scopePrefix, language = 'js') { if (!scopePrefix || scopePrefix === 'all') { return stripMetadataComments(content, language); } const lines = content.split('\n'); const filtered = []; let insideTargetScope = false; let insideAnyScope = false; let scopeDepth = 0; // Safe comment-wrapped markers only const OPEN_SCOPE = new RegExp( "^\\s*(?:/\\*\\s*([a-z0-9_-]+)-[a-z]+-\\d+<\\s*\\*/|<!--\\s*([a-z0-9_-]+)-[a-z]+-\\d+<\\s*-->|//\\s*([a-z0-9_-]+)-[a-z]+-\\d+<)\\s*$", "i" ); const CLOSE_SCOPE = new RegExp( "^\\s*(?:/\\*\\s*([a-z0-9_-]+)-[a-z]+-\\d+>\\s*\\*/|<!--\\s*([a-z0-9_-]+)-[a-z]+-\\d+>\\s*-->|//\\s*([a-z0-9_-]+)-[a-z]+-\\d+>)\\s*$", "i" ); const CONTAINER = /container[-_][a-z0-9_-]+[<>]/i; // Build Option-B style summary from real scope content function buildScopeSummary(scopeName, originalContent) { // Use your analyzer! const summaryComment = generateScopeSummary(originalContent, language); // Inject extra info return summaryComment.replace( /^\/\*/, `/* ${scopeName} (excluded โ€” active scope: ${scopePrefix.toUpperCase()})` ); } // ========================= // MAIN FILTER PASS // ========================= for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); // -------- OPENING SCOPE -------- const openMatch = trimmed.match(OPEN_SCOPE); if (openMatch) { const scopeName = openMatch[1] || openMatch[2] || openMatch[3]; const prefix = scopeName.split('-')[0]; filtered.push(line); // keep opening marker if (prefix === scopePrefix) { // Target scope: KEEP everything insideTargetScope = true; insideAnyScope = true; scopeDepth++; continue; } // NON-target scope: COLLAPSE it and insert Option-B summary let originalScopeContent = ""; let j = i + 1; // Collect real content until closing marker for (; j < lines.length; j++) { const closeCheck = lines[j].trim().match(CLOSE_SCOPE); if (closeCheck) break; originalScopeContent += lines[j] + "\n"; } // Build rich summary from TRUE content const summary = buildScopeSummary(scopeName, originalScopeContent); filtered.push(summary); // Close scope if (j < lines.length) filtered.push(lines[j]); i = j; // skip internal lines continue; } // -------- CLOSING SCOPE -------- const closeMatch = trimmed.match(CLOSE_SCOPE); if (closeMatch) { const scopeName = closeMatch[1] || closeMatch[2] || closeMatch[3]; const prefix = scopeName.split('-')[0]; filtered.push(line); if (prefix === scopePrefix) insideTargetScope = false; scopeDepth--; if (scopeDepth <= 0) { insideAnyScope = false; scopeDepth = 0; } continue; } // -------- Container markers stay untouched -------- if (CONTAINER.test(trimmed)) { filtered.push(line); continue; } // -------- Content handling -------- if (insideTargetScope) { // Keep real content filtered.push(line); } else if (!insideAnyScope) { // Outside of any scope = keep filtered.push(line); } // else: inside collapsed scope โ†’ skip } return filtered.join('\n').trim(); } function stripMetadataComments(content, language = 'js') { if (!content) return ''; console.log('[stripper] Starting strip - Language:', language, 'Content length:', content.length); const META_KEYS = [ '@updatedAt', '@updatedBy', '@container', '@position', '@editedBy', '@editedAt', '@relatedScopes' ]; const lines = content.split('\n'); const cleaned = []; let insideMetadata = false; let strippedCount = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); // Detect if line contains metadata keys const containsMeta = META_KEYS.some(k => line.includes(k)); // --- HANDLE CSS/JS METADATA BLOCKS --- // If we see a * line with metadata, we're inside a /* block if (trimmed.startsWith('*') && containsMeta && !insideMetadata) { console.log('[stripper] Found metadata in * line at', i, '- setting metadata mode'); insideMetadata = true; // Go back and remove the /* line if (cleaned.length > 0 && cleaned[cleaned.length - 1].trim() === '/*') { console.log('[stripper] Removing previous /* line'); cleaned.pop(); strippedCount++; } strippedCount++; continue; } // --- HANDLE HTML METADATA BLOCKS --- // If we see a line with just metadata (not starting with <!--), we're inside HTML comment if (containsMeta && !trimmed.startsWith('<!--') && !trimmed.startsWith('/*') && !trimmed.startsWith('//') && !insideMetadata) { console.log('[stripper] Found HTML metadata line at', i, '- setting metadata mode'); insideMetadata = true; // Go back and remove the <!-- line if (cleaned.length > 0 && cleaned[cleaned.length - 1].trim() === '<!--') { console.log('[stripper] Removing previous <!-- line'); cleaned.pop(); strippedCount++; } strippedCount++; continue; } // Inside metadata block if (insideMetadata) { console.log('[stripper] Inside metadata, stripping line', i); strippedCount++; // Check for end of block if (trimmed === '*/' || trimmed.endsWith('*/') || trimmed === '-->' || trimmed.endsWith('-->')) { console.log('[stripper] Metadata block end at line', i); insideMetadata = false; } continue; } // Metadata block start on same line: /* @something or <!-- @something if ((trimmed.startsWith('/*') || trimmed.startsWith('<!--')) && containsMeta) { console.log('[stripper] Found metadata block start at line', i); insideMetadata = true; strippedCount++; // If it also ends on same line, turn off metadata mode if (trimmed.endsWith('*/') || trimmed.endsWith('-->')) { insideMetadata = false; } continue; } // Single-line // metadata if (trimmed.startsWith('//') && containsMeta) { console.log('[stripper] Removing // metadata:', trimmed); strippedCount++; continue; } // Keep everything else cleaned.push(line); } console.log('[stripper] DONE - Stripped', strippedCount, 'lines. Original:', lines.length, 'Clean:', cleaned.length); return cleaned.join('\n').trim(); } // Detect language from file extension function detectLanguage(fileName) { const ext = fileName.split('.').pop().toLowerCase(); const langMap = { 'html': 'html', 'htm': 'html', 'blade.php': 'blade', 'vue': 'vue-html', 'js': 'js', 'jsx': 'js', 'ts': 'js', 'tsx': 'js', 'css': 'css', 'scss': 'css', 'php': 'js' // PHP uses /* */ comments like JS }; return langMap[ext] || 'js'; } // --- Local Storage Functions --- function getFilesFromLocalStorage() { try { return JSON.parse(localStorage.getItem('sftp_active_files') || '[]'); } catch { return []; } } function saveFilesToLocalStorage(files) { try { localStorage.setItem('sftp_active_files', JSON.stringify(files)); } catch (err) { console.error('[files] Failed to save files:', err); } } // --- File State Management --- function setFileState(fileName, state) { const files = getFilesFromLocalStorage(); const file = files.find(f => f.name === fileName); if (!file) return; if (state === 'active') { files.forEach(f => { if (f.name === fileName) { f.active = true; f.read = false; } else { f.active = false; } }); // Detect language and strip metadata const language = detectLanguage(file.name); const cleanContent = stripMetadataComments(file.content, language); // Set as working file with CLEAN content window.WorkingFile = { name: file.name, path: file.path, content: cleanContent, // โœ… Stripped of metadata originalContent: file.content // Keep original for reference }; console.log('[files] Working file set:', fileName, '- metadata stripped, language:', language); // Trigger event for chat window.dispatchEvent(new CustomEvent('workingFileChanged', { detail: window.WorkingFile })); } else if (state === 'read') { file.read = true; file.active = false; // Also strip metadata for read files and store them cleaned const language = detectLanguage(file.name); const cleanContent = stripMetadataComments(file.content, language); // Store cleaned version in a global array if (!window.ReadFiles) window.ReadFiles = []; // Check if already in read files const existingIndex = window.ReadFiles.findIndex(f => f.name === fileName); if (existingIndex >= 0) { // Update existing window.ReadFiles[existingIndex] = { name: file.name, path: file.path, content: cleanContent, originalContent: file.content }; } else { // Add new window.ReadFiles.push({ name: file.name, path: file.path, content: cleanContent, originalContent: file.content }); } console.log('[files] Read file added:', fileName, '- metadata stripped'); // Trigger event window.dispatchEvent(new CustomEvent('readFilesChanged', { detail: window.ReadFiles })); } else if (state === 'inactive') { file.read = false; file.active = false; // Clear working file if this was it if (window.WorkingFile && window.WorkingFile.name === fileName) { window.WorkingFile = null; window.dispatchEvent(new CustomEvent('workingFileChanged', { detail: null })); } // Remove from read files if (window.ReadFiles) { window.ReadFiles = window.ReadFiles.filter(f => f.name !== fileName); window.dispatchEvent(new CustomEvent('readFilesChanged', { detail: window.ReadFiles })); } } saveFilesToLocalStorage(files); } // --- Version Management --- function restoreVersion(fileName, versionIndex) { const files = getFilesFromLocalStorage(); const file = files.find(f => f.name === fileName); if (!file || !file.versions || !file.versions[versionIndex]) return; // Restore the selected version to current content file.content = file.versions[versionIndex].content; saveFilesToLocalStorage(files); // Update working file if this is the active file if (file.active && window.WorkingFile && window.WorkingFile.name === fileName) { const language = detectLanguage(fileName); const cleanContent = stripMetadataComments(file.content, language); window.WorkingFile.content = cleanContent; window.WorkingFile.originalContent = file.content; console.log('[files] Working file content updated after version restore - metadata stripped'); window.dispatchEvent(new CustomEvent('workingFileChanged', { detail: window.WorkingFile })); } // Trigger update event window.dispatchEvent(new Event('activeFilesUpdated')); } function deleteVersion(fileName, versionIndex) { const files = getFilesFromLocalStorage(); const file = files.find(f => f.name === fileName); if (!file || !file.versions || !file.versions[versionIndex]) return; // Remove the version file.versions.splice(versionIndex, 1); // Relabel remaining versions file.versions.forEach((version, idx) => { version.label = `v${idx + 1}`; }); saveFilesToLocalStorage(files); // Trigger update event window.dispatchEvent(new Event('activeFilesUpdated')); } // --- New File Dialog --- async function showNewFileDialog(container, onFileChange) { // Fetch templates from server via templates.php let templates = []; try { const response = await fetch('templates.php?action=list'); const data = await response.json(); if (data.success && Array.isArray(data.templates)) { templates = data.templates.map(name => ({ name: name, path: name })); console.log('[files] Found templates:', templates); } else { console.error('[files] Failed to load templates:', data.error || 'Unknown error'); } } catch (error) { console.error('[files] Failed to load templates:', error); } // Create dialog overlay const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 2147483647; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background: #1a1a1a; border: 2px solid #3a3a3a; border-radius: 12px; padding: 24px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; `; dialog.innerHTML = ` <h2 style="margin: 0 0 20px 0; color: #e6edf3; font-size: 20px; font-weight: 700;"> ๐Ÿ“„ Create New File </h2> <div style="margin-bottom: 16px;"> <label style="display: block; color: #9ca3af; font-size: 13px; font-weight: 600; margin-bottom: 8px;"> File Name </label> <input id="newFileName" type="text" placeholder="my-file.php" style=" width: 100%; padding: 10px 12px; background: #0a0a0a; border: 1px solid #3a3a3a; border-radius: 6px; color: #e0e0e0; font-size: 14px; font-family: monospace; outline: none; " /> </div> <div style="margin-bottom: 20px;"> <label style="display: block; color: #9ca3af; font-size: 13px; font-weight: 600; margin-bottom: 8px;"> Start from Template </label> <div id="templateList" style=" max-height: 300px; overflow-y: auto; border: 1px solid #3a3a3a; border-radius: 6px; background: #0a0a0a; "> <div class="template-option" data-template="" style=" padding: 12px; border-bottom: 1px solid #2a2a2a; cursor: pointer; transition: background 0.2s; "> <div style="font-weight: 600; color: #e6edf3;">๐Ÿ“ Empty File</div> <div style="font-size: 12px; color: #666; margin-top: 4px;">Start with a blank file</div> </div> ${templates.length === 0 ? ` <div style="padding: 12px; color: #666; font-size: 13px; text-align: center;"> No templates found in /templates folder </div> ` : ''} </div> </div> <div style="display: flex; gap: 12px;"> <button id="createFileBtn" style=" flex: 1; padding: 12px; background: #16a34a; border: 1px solid #15803d; border-radius: 6px; color: #fff; cursor: pointer; font-size: 14px; font-weight: 700; transition: all 0.2s; ">Create File</button> <button id="cancelFileBtn" style=" flex: 1; padding: 12px; background: #374151; border: 1px solid #4b5563; border-radius: 6px; color: #e0e0e0; cursor: pointer; font-size: 14px; font-weight: 700; transition: all 0.2s; ">Cancel</button> </div> `; overlay.appendChild(dialog); document.body.appendChild(overlay); const fileNameInput = dialog.querySelector('#newFileName'); const templateList = dialog.querySelector('#templateList'); const createBtn = dialog.querySelector('#createFileBtn'); const cancelBtn = dialog.querySelector('#cancelFileBtn'); let selectedTemplate = ''; // Add template options templates.forEach(template => { const option = document.createElement('div'); option.className = 'template-option'; option.dataset.template = template.path; option.style.cssText = ` padding: 12px; border-bottom: 1px solid #2a2a2a; cursor: pointer; transition: background 0.2s; `; option.innerHTML = ` <div style="font-weight: 600; color: #e6edf3;">๐Ÿ“„ ${escapeHtml(template.name)}</div> <div style="font-size: 11px; color: #666; margin-top: 4px; font-family: monospace;"> ${escapeHtml(template.path)} </div> `; templateList.appendChild(option); }); // Template selection templateList.addEventListener('click', (e) => { const option = e.target.closest('.template-option'); if (!option) return; templateList.querySelectorAll('.template-option').forEach(opt => { opt.style.background = '#0a0a0a'; opt.style.borderLeftWidth = '0'; }); option.style.background = '#1a1a1a'; option.style.borderLeft = '3px solid #16a34a'; selectedTemplate = option.dataset.template; }); // Hover effects for templates templateList.addEventListener('mouseover', (e) => { const option = e.target.closest('.template-option'); if (option && option.style.background !== 'rgb(26, 26, 26)') { option.style.background = '#151515'; } }); templateList.addEventListener('mouseout', (e) => { const option = e.target.closest('.template-option'); if (option && option.style.background !== 'rgb(26, 26, 26)') { option.style.background = '#0a0a0a'; } }); // Create file handler const createFile = async () => { const fileName = fileNameInput.value.trim(); if (!fileName) { alert('Please enter a file name'); return; } const files = getFilesFromLocalStorage(); if (files.find(f => f.name === fileName)) { alert('A file with this name already exists'); return; } let content = ''; // Load template content if selected if (selectedTemplate) { try { const response = await fetch('templates.php?action=read&file=' + encodeURIComponent(selectedTemplate)); const data = await response.json(); if (data.success) { content = data.content; console.log('[files] Loaded template content from:', selectedTemplate); } else { console.error('[files] Failed to load template:', data.error); alert('Failed to load template. Starting with empty file.'); } } catch (error) { console.error('[files] Failed to load template:', error); alert('Failed to load template. Starting with empty file.'); } } // Add new file files.push({ name: fileName, path: fileName, content: content, active: false, read: false, versions: [] }); saveFilesToLocalStorage(files); window.dispatchEvent(new Event('activeFilesUpdated')); // Close dialog document.body.removeChild(overlay); // Refresh file list renderFilesList(container, onFileChange); if (onFileChange) onFileChange(); }; createBtn.addEventListener('click', createFile); cancelBtn.addEventListener('click', () => { document.body.removeChild(overlay); }); // Enter to create fileNameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { createFile(); } }); // Close on overlay click overlay.addEventListener('click', (e) => { if (e.target === overlay) { document.body.removeChild(overlay); } }); // Focus input fileNameInput.focus(); } // --- Files List Rendering --- function renderFilesList(container, onFileChange) { const files = getFilesFromLocalStorage(); container.innerHTML = ''; if (files.length === 0) { const emptyMsg = document.createElement('div'); emptyMsg.style.cssText = ` color: #666; text-align: center; padding: 40px; font-size: 14px; `; emptyMsg.textContent = '๐Ÿ“ No files open - open files from Storage Editor'; container.appendChild(emptyMsg); return; } // Header with New File button const header = document.createElement('div'); header.style.cssText = ` margin-bottom: 20px; padding-bottom: 12px; border-bottom: 2px solid #2a2a2a; display: flex; justify-content: space-between; align-items: center; `; const headerLeft = document.createElement('div'); headerLeft.innerHTML = ` <h2 style="margin: 0; color: #e6edf3; font-size: 18px; font-weight: 700;"> ๐Ÿ“ All Files (${files.length}) </h2> <p style="margin: 8px 0 0 0; color: #64748b; font-size: 13px;"> Click file name to set as working file | Click versions to view history </p> `; const newFileBtn = document.createElement('button'); newFileBtn.style.cssText = ` padding: 10px 20px; background: #16a34a; border: 1px solid #15803d; border-radius: 6px; color: #fff; cursor: pointer; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.2s; `; newFileBtn.textContent = '+ New File'; newFileBtn.addEventListener('click', async () => { await showNewFileDialog(container, onFileChange); }); newFileBtn.addEventListener('mouseenter', () => { newFileBtn.style.background = '#15803d'; }); newFileBtn.addEventListener('mouseleave', () => { newFileBtn.style.background = '#16a34a'; }); header.appendChild(headerLeft); header.appendChild(newFileBtn); container.appendChild(header); // Files grid files.forEach(file => { const fileCard = document.createElement('div'); fileCard.style.cssText = ` background: #1a1a1a; border: 2px solid #2a2a2a; border-radius: 8px; padding: 16px; margin-bottom: 12px; transition: all 0.2s; `; // Style based on state if (file.active) { fileCard.style.borderColor = '#16a34a'; fileCard.style.background = 'rgba(22, 163, 74, 0.1)'; } else if (file.read) { fileCard.style.borderColor = '#3b82f6'; fileCard.style.background = 'rgba(59, 130, 246, 0.1)'; } const statusBadge = file.active ? '๐ŸŽฏ WORKING (clean)' : file.read ? '๐Ÿ”ต READ' : 'โšช INACTIVE'; const statusColor = file.active ? '#16a34a' : file.read ? '#3b82f6' : '#666'; const versionCount = file.versions ? file.versions.length : 0; const fileHeader = document.createElement('div'); fileHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px; `; fileHeader.innerHTML = ` <div style=" font-weight: 700; color: #e6edf3; font-size: 15px; font-family: monospace; cursor: pointer; ">${escapeHtml(file.name)}</div> <div style="display: flex; gap: 8px; align-items: center;"> <div style=" color: ${statusColor}; font-size: 11px; font-weight: 700; padding: 4px 8px; background: rgba(0,0,0,0.3); border-radius: 4px; cursor: pointer; " class="status-badge">${statusBadge}</div> </div> `; // Click file name to cycle states const fileName = fileHeader.querySelector('div[style*="cursor: pointer"]'); fileName.addEventListener('click', () => { if (!file.active && !file.read) { setFileState(file.name, 'read'); } else if (file.read) { setFileState(file.name, 'active'); } else if (file.active) { setFileState(file.name, 'inactive'); } renderFilesList(container, onFileChange); if (onFileChange) onFileChange(); }); // Click status badge to cycle states (alternative) const statusBadgeEl = fileHeader.querySelector('.status-badge'); statusBadgeEl.addEventListener('click', () => { if (!file.active && !file.read) { setFileState(file.name, 'read'); } else if (file.read) { setFileState(file.name, 'active'); } else if (file.active) { setFileState(file.name, 'inactive'); } renderFilesList(container, onFileChange); if (onFileChange) onFileChange(); }); fileCard.appendChild(fileHeader); // Path const pathDiv = document.createElement('div'); pathDiv.style.cssText = ` color: #64748b; font-size: 12px; margin-bottom: 8px; `; pathDiv.textContent = file.path || 'No path'; fileCard.appendChild(pathDiv); // Preview const previewDiv = document.createElement('div'); previewDiv.style.cssText = ` color: #888; font-size: 11px; font-family: monospace; background: #0a0a0a; padding: 8px; border-radius: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 8px; `; previewDiv.textContent = file.content ? file.content.substring(0, 80) + '...' : '(empty)'; fileCard.appendChild(previewDiv); // Add Clean Content Viewer for active files if (file.active) { const language = detectLanguage(file.name); const cleanContent = stripMetadataComments(file.content, language); // Extract scope prefixes const scopePrefixes = extractScopePrefixes(file.content); console.log('[files] Original content length:', file.content.length); console.log('[files] Clean content length:', cleanContent.length); console.log('[files] Language detected:', language); console.log('[files] Scope prefixes found:', scopePrefixes); const originalLines = file.content.split('\n').length; const cleanLines = cleanContent.split('\n').length; const removedLines = originalLines - cleanLines; const originalChars = file.content.length; const cleanChars = cleanContent.length; const savedChars = originalChars - cleanChars; const savedPercent = originalChars > 0 ? ((savedChars / originalChars) * 100).toFixed(1) : '0'; console.log('[files] Removed lines:', removedLines, 'Saved chars:', savedChars, 'Percent:', savedPercent + '%'); const cleanViewerHeader = document.createElement('div'); cleanViewerHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-top: 1px solid #2a2a2a; margin-top: 8px; cursor: pointer; user-select: none; `; cleanViewerHeader.innerHTML = ` <div style="color: #10b981; font-size: 12px; font-weight: 600;"> <span class="clean-toggle">โ–ถ</span> ๐Ÿ‘๏ธ View Clean Content </div> <div style="color: #64748b; font-size: 10px;"> ${removedLines} lines removed (${savedPercent}% smaller) </div> `; const cleanViewerBody = document.createElement('div'); cleanViewerBody.style.cssText = ` max-height: 0; overflow: hidden; transition: max-height 0.3s ease; margin-top: 8px; `; // Scope filter dropdown (only if scopes found) let scopeFilterHtml = ''; if (scopePrefixes.length > 0) { scopeFilterHtml = ` <div style="margin-bottom: 12px; padding: 8px 12px; background: #111; border-radius: 4px;"> <label style="color: #9ca3af; font-size: 11px; font-weight: 600; margin-right: 8px;"> ๐Ÿ“‹ Filter by Scope: </label> <select class="scope-filter" style=" padding: 4px 8px; background: #1a1a1a; border: 1px solid #3a3a3a; border-radius: 4px; color: #e0e0e0; font-size: 11px; font-family: monospace; "> <option value="all">All Content (Full File)</option> ${scopePrefixes.map(prefix => `<option value="${escapeHtml(prefix)}">${escapeHtml(prefix).toUpperCase()} scopes only</option>` ).join('')} </select> <span style="color: #64748b; font-size: 10px; margin-left: 8px;"> Select to show only specific scopes </span> </div> `; } // Stats const statsDiv = document.createElement('div'); statsDiv.style.cssText = ` padding: 8px 12px; background: #111; border-radius: 4px; margin-bottom: 8px; display: flex; gap: 16px; font-size: 11px; flex-wrap: wrap; `; statsDiv.innerHTML = scopeFilterHtml + ` <div style="color: #64748b;"> <strong style="color: #e6edf3;">Original:</strong> ${originalLines} lines, ${originalChars} chars </div> <div style="color: #64748b;"> <strong style="color: #10b981;">Clean:</strong> ${cleanLines} lines, ${cleanChars} chars </div> <div style="color: #ef4444; font-weight: 600;"> โ†“ ${removedLines} lines, ${savedChars} chars removed </div> `; // Side by side comparison const comparisonDiv = document.createElement('div'); comparisonDiv.style.cssText = ` display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px; `; comparisonDiv.innerHTML = ` <div style="display: flex; flex-direction: column; min-height: 200px;"> <div style=" padding: 6px 10px; background: #1a1a1a; border-radius: 4px 4px 0 0; font-weight: 700; font-size: 11px; color: #9ca3af; ">๐Ÿ“„ ORIGINAL (with metadata)</div> <textarea readonly style=" flex: 1; width: 100%; background: #0a0a0a; color: #9ca3af; border: 1px solid #2a2a2a; border-top: none; border-radius: 0 0 4px 4px; padding: 10px; font-family: 'Consolas', 'Monaco', monospace; font-size: 11px; line-height: 1.5; resize: vertical; outline: none; ">${escapeHtml(file.content)}</textarea> </div> <div style="display: flex; flex-direction: column; min-height: 200px;"> <div style=" padding: 6px 10px; background: #1a1a1a; border-radius: 4px 4px 0 0; font-weight: 700; font-size: 11px; color: #10b981; display: flex; justify-content: space-between; align-items: center; "> <span>โœจ CLEAN (sent to AI)</span> <button class="copy-clean-btn" style=" padding: 2px 8px; background: #16a34a; border: 1px solid #15803d; border-radius: 3px; color: #fff; cursor: pointer; font-size: 10px; font-weight: 700; transition: all 0.2s; ">๐Ÿ“‹ Copy</button> </div> <textarea readonly class="clean-content-area" style=" flex: 1; width: 100%; background: #0a0a0a; color: #e6edf3; border: 1px solid #2a2a2a; border-top: none; border-radius: 0 0 4px 4px; padding: 10px; font-family: 'Consolas', 'Monaco', monospace; font-size: 11px; line-height: 1.5; resize: vertical; outline: none; ">/* ======================================== * ๐Ÿงน CLEANED VERSION - All metadata stripped * Language: ${language} * Original: ${originalLines} lines, ${originalChars} chars * Clean: ${cleanLines} lines, ${cleanChars} chars * Removed: ${removedLines} lines, ${savedChars} chars * ======================================== */ ${escapeHtml(cleanContent)}</textarea> </div> `; cleanViewerBody.appendChild(statsDiv); cleanViewerBody.appendChild(comparisonDiv); // Scope filter handler const scopeFilter = statsDiv.querySelector('.scope-filter'); if (scopeFilter) { scopeFilter.addEventListener('change', () => { const selectedScope = scopeFilter.value; const cleanArea = comparisonDiv.querySelector('.clean-content-area'); let filteredContent; if (selectedScope === 'all') { filteredContent = cleanContent; } else { filteredContent = filterByScope(file.content, selectedScope); } const filteredLines = filteredContent.split('\n').length; const filteredChars = filteredContent.length; cleanArea.value = `/* ======================================== * ๐Ÿงน CLEANED VERSION - ${selectedScope === 'all' ? 'All Content' : selectedScope.toUpperCase() + ' Scopes Only'} * Language: ${language} * Filtered: ${filteredLines} lines, ${filteredChars} chars * ======================================== */ ${filteredContent}`; console.log('[files] Scope filter changed to:', selectedScope); }); } // Toggle clean viewer let cleanViewerExpanded = false; cleanViewerHeader.addEventListener('click', () => { cleanViewerExpanded = !cleanViewerExpanded; const toggle = cleanViewerHeader.querySelector('.clean-toggle'); if (cleanViewerExpanded) { cleanViewerBody.style.maxHeight = cleanViewerBody.scrollHeight + 'px'; toggle.textContent = 'โ–ผ'; } else { cleanViewerBody.style.maxHeight = '0'; toggle.textContent = 'โ–ถ'; } }); // Copy button const copyBtn = comparisonDiv.querySelector('.copy-clean-btn'); const cleanArea = comparisonDiv.querySelector('.clean-content-area'); copyBtn.addEventListener('click', async (e) => { e.stopPropagation(); try { await navigator.clipboard.writeText(cleanContent); const originalText = copyBtn.textContent; copyBtn.textContent = 'โœ… Copied!'; copyBtn.style.background = '#10b981'; setTimeout(() => { copyBtn.textContent = originalText; copyBtn.style.background = '#16a34a'; }, 2000); } catch (err) { cleanArea.select(); document.execCommand('copy'); copyBtn.textContent = 'โœ… Copied!'; } }); copyBtn.addEventListener('mouseenter', () => { copyBtn.style.background = '#15803d'; }); copyBtn.addEventListener('mouseleave', () => { if (copyBtn.textContent.includes('Copy')) { copyBtn.style.background = '#16a34a'; } }); fileCard.appendChild(cleanViewerHeader); fileCard.appendChild(cleanViewerBody); } // Versions section if (versionCount > 0) { const versionsHeader = document.createElement('div'); versionsHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-top: 1px solid #2a2a2a; margin-top: 8px; cursor: pointer; user-select: none; `; versionsHeader.innerHTML = ` <div style="color: #3b82f6; font-size: 12px; font-weight: 600;"> <span class="version-toggle">โ–ถ</span> ๐Ÿ’พ ${versionCount} Version${versionCount > 1 ? 's' : ''} </div> `; const versionsBody = document.createElement('div'); versionsBody.style.cssText = ` max-height: 0; overflow: hidden; transition: max-height 0.3s ease; margin-top: 8px; `; // Render versions (newest first) const reversedVersions = [...file.versions].reverse(); reversedVersions.forEach((version, idx) => { const realIndex = file.versions.length - 1 - idx; const date = new Date(version.timestamp); const isCurrent = file.content === version.content; const versionItem = document.createElement('div'); versionItem.style.cssText = ` display: flex; align-items: stretch; margin-bottom: 6px; gap: 4px; `; const versionBtn = document.createElement('button'); versionBtn.style.cssText = ` flex: 1; text-align: left; padding: 8px 12px; background: ${isCurrent ? '#1e3a5f' : '#0a0a0a'}; border: 1px solid ${isCurrent ? '#3b82f6' : '#2a2a2a'}; border-radius: 4px; color: #e0e0e0; cursor: ${isCurrent ? 'default' : 'pointer'}; font-size: 12px; transition: all 0.2s; `; versionBtn.innerHTML = ` <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;"> <span style="font-weight: 700; color: ${isCurrent ? '#3b82f6' : '#fff'};"> ${version.label} ${isCurrent ? '(Current)' : ''} </span> <span style="color: #666; font-size: 10px;">${date.toLocaleString()}</span> </div> <div style="color: #888; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"> ${version.content.substring(0, 60)}${version.content.length > 60 ? '...' : ''} </div> `; if (!isCurrent) { versionBtn.addEventListener('click', () => { if (confirm(`Restore ${version.label}? This will replace the current content.`)) { restoreVersion(file.name, realIndex); renderFilesList(container, onFileChange); if (onFileChange) onFileChange(); } }); versionBtn.addEventListener('mouseenter', () => { versionBtn.style.background = '#1a1a1a'; versionBtn.style.borderColor = '#3a3a3a'; }); versionBtn.addEventListener('mouseleave', () => { versionBtn.style.background = '#0a0a0a'; versionBtn.style.borderColor = '#2a2a2a'; }); } // Delete button const deleteBtn = document.createElement('button'); deleteBtn.style.cssText = ` width: 36px; padding: 8px; background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 4px; color: #ef4444; cursor: pointer; font-size: 16px; transition: all 0.2s; display: flex; align-items: center; justify-content: center; `; deleteBtn.innerHTML = '๐Ÿ—‘๏ธ'; deleteBtn.title = `Delete ${version.label}`; deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); if (confirm(`Delete ${version.label}? This cannot be undone.`)) { deleteVersion(file.name, realIndex); renderFilesList(container, onFileChange); if (onFileChange) onFileChange(); } }); deleteBtn.addEventListener('mouseenter', () => { deleteBtn.style.background = '#ef4444'; deleteBtn.style.borderColor = '#dc2626'; deleteBtn.style.color = '#fff'; }); deleteBtn.addEventListener('mouseleave', () => { deleteBtn.style.background = '#1a1a1a'; deleteBtn.style.borderColor = '#2a2a2a'; deleteBtn.style.color = '#ef4444'; }); versionItem.appendChild(versionBtn); versionItem.appendChild(deleteBtn); versionsBody.appendChild(versionItem); }); // Toggle versions let versionsExpanded = false; versionsHeader.addEventListener('click', () => { versionsExpanded = !versionsExpanded; const toggle = versionsHeader.querySelector('.version-toggle'); if (versionsExpanded) { versionsBody.style.maxHeight = versionsBody.scrollHeight + 'px'; toggle.textContent = 'โ–ผ'; } else { versionsBody.style.maxHeight = '0'; toggle.textContent = 'โ–ถ'; } }); fileCard.appendChild(versionsHeader); fileCard.appendChild(versionsBody); } container.appendChild(fileCard); }); } // Initialize global working file variable window.WorkingFile = null; // --- Expose API --- window.FilesManager = { getFiles: getFilesFromLocalStorage, saveFiles: saveFilesToLocalStorage, setFileState: setFileState, render: renderFilesList, restoreVersion: restoreVersion, deleteVersion: deleteVersion }; console.log('[files] File Management module loaded with working file support'); })();