// 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');
})();