Files
clawd/kanban/files.html
Echo f371f579a1 Ecosistem multi-agent complet
- SOUL-base.md partajat pentru toți agenții
- 5 agenți specializați: work, health, growth, sprijin, scout
- Fiecare agent cu SOUL.md, TOOLS.md, USER.md, AGENTS.md proprii
- Symlinks pentru resurse partajate (notes/, kanban/, projects/)
- Tags de domeniu (@work, @health, etc.) în YouTube notes
- Script update_notes_index.py îmbunătățit cu domenii
- HEARTBEAT.md cu verificări periodice
- Grup sprijin pagină și fișe activități
- Cleanup: șters agents/echo/ orfan
2026-01-30 13:46:57 +00:00

705 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Echo · Files</title>
<link rel="stylesheet" href="common.css">
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="swipe-nav.js"></script>
<style>
.main {
display: flex;
flex-direction: column;
height: calc(100vh - 52px);
}
/* Toolbar */
.toolbar {
padding: var(--space-3) var(--space-5);
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-4);
flex-wrap: wrap;
}
.breadcrumb {
display: flex;
align-items: center;
gap: var(--space-1);
font-size: var(--text-sm);
color: var(--text-muted);
flex-wrap: wrap;
}
.breadcrumb-item {
color: var(--text-muted);
text-decoration: none;
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
cursor: pointer;
}
.breadcrumb-item:hover {
color: var(--text-primary);
background: var(--bg-surface-hover);
}
.breadcrumb-item.current {
color: var(--text-primary);
}
.breadcrumb-sep {
color: var(--text-muted);
}
.breadcrumb-sep svg {
width: 14px;
height: 14px;
}
.toolbar-actions {
display: flex;
gap: var(--space-2);
}
/* View modes */
.view-toggle {
display: flex;
gap: 2px;
background: var(--bg-base);
border-radius: var(--radius-md);
padding: 2px;
}
.view-btn {
padding: var(--space-2);
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
}
.view-btn:hover {
color: var(--text-primary);
}
.view-btn.active {
background: var(--accent);
color: white;
}
.view-btn svg {
width: 16px;
height: 16px;
}
/* Content area */
.content-area {
flex: 1;
display: flex;
overflow: hidden;
}
/* Browse panel */
.browse-panel {
flex: 1;
overflow: auto;
padding: var(--space-5);
}
.browse-panel.hidden {
display: none;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: var(--space-3);
}
.file-item {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: var(--space-4);
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: var(--space-2);
}
.file-item:hover {
border-color: var(--accent);
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
}
.file-item.active {
border-color: var(--accent);
background: var(--accent-subtle);
}
.file-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
}
.file-icon svg {
width: 32px;
height: 32px;
}
.file-icon.folder svg {
color: var(--accent);
}
.file-name {
font-size: var(--text-sm);
color: var(--text-primary);
word-break: break-word;
line-height: 1.3;
}
.file-size {
font-size: var(--text-xs);
color: var(--text-muted);
}
/* Editor panel */
.editor-panel {
flex: 1;
display: none;
flex-direction: column;
background: var(--bg-base);
}
.editor-panel.active {
display: flex;
}
.editor-header {
padding: var(--space-3) var(--space-5);
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.editor-title {
display: flex;
align-items: center;
gap: var(--space-2);
color: var(--text-primary);
font-size: var(--text-sm);
font-weight: 500;
}
.editor-title svg {
width: 16px;
height: 16px;
color: var(--text-muted);
}
.editor-actions {
display: flex;
gap: var(--space-2);
}
.editor-body {
flex: 1;
overflow: hidden;
}
#codeEditor {
width: 100%;
height: 100%;
background: transparent;
color: var(--text-secondary);
border: none;
padding: var(--space-5);
font-family: var(--font-mono);
font-size: 14px;
line-height: 1.6;
resize: none;
outline: none;
}
#markdownPreview {
display: none;
width: 100%;
height: 100%;
padding: var(--space-5);
overflow-y: auto;
color: var(--text-secondary);
line-height: 1.7;
}
#markdownPreview h1, #markdownPreview h2, #markdownPreview h3 {
color: var(--text-primary);
margin-top: 1.5em;
margin-bottom: 0.5em;
}
#markdownPreview h1 { font-size: 1.8em; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
#markdownPreview h2 { font-size: 1.4em; }
#markdownPreview h3 { font-size: 1.2em; }
#markdownPreview p { margin-bottom: 1em; }
#markdownPreview ul, #markdownPreview ol { margin-bottom: 1em; padding-left: 2em; }
#markdownPreview li { margin-bottom: 0.3em; }
#markdownPreview strong { color: var(--text-primary); }
#markdownPreview hr { border: none; border-top: 1px solid var(--border); margin: 2em 0; }
#markdownPreview code { background: var(--bg-surface); padding: 2px 6px; border-radius: 4px; font-family: var(--font-mono); }
#markdownPreview pre { background: var(--bg-surface); padding: 1em; border-radius: 8px; overflow-x: auto; }
#markdownPreview blockquote { border-left: 3px solid var(--accent); padding-left: 1em; margin-left: 0; color: var(--text-muted); }
.preview-active #codeEditor { display: none; }
.preview-active #markdownPreview { display: block; }
.btn-preview.active { background: var(--accent); color: white; }
.editor-footer {
padding: var(--space-2) var(--space-5);
background: var(--bg-surface);
border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
font-size: var(--text-xs);
color: var(--text-muted);
}
.status-saved { color: var(--success); }
.status-modified { color: var(--warning); }
.status-error { color: var(--error); }
/* Empty state */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted);
gap: var(--space-3);
}
.empty-state svg {
width: 48px;
height: 48px;
opacity: 0.5;
}
@media (max-width: 768px) {
.toolbar {
padding: var(--space-3);
}
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
}
</style>
</head>
<body>
<header class="header">
<a href="index.html" class="logo">
<i data-lucide="circle-dot"></i>
Echo
</a>
<nav class="nav">
<a href="index.html" class="nav-item">
<i data-lucide="layout-list"></i>
<span>Tasks</span>
</a>
<a href="notes.html" class="nav-item">
<i data-lucide="file-text"></i>
<span>Notes</span>
</a>
<a href="files.html" class="nav-item active">
<i data-lucide="folder"></i>
<span>Files</span>
</a>
<a href="grup-sprijin.html" class="nav-item">
<i data-lucide="heart-handshake"></i>
<span>Grup</span>
</a>
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
<i data-lucide="sun" id="themeIcon"></i>
</button>
</nav>
</header>
<main class="main">
<div class="toolbar">
<div class="breadcrumb" id="breadcrumb">
<span class="breadcrumb-item current" onclick="loadPath('')">~/clawd</span>
</div>
<div class="toolbar-actions">
<div class="view-toggle">
<button class="view-btn active" id="browseBtn" onclick="showBrowse()" title="Browse">
<i data-lucide="layout-grid"></i>
</button>
<button class="view-btn" id="editorBtn" onclick="showEditor()" title="Editor">
<i data-lucide="code"></i>
</button>
</div>
</div>
</div>
<div class="content-area">
<div class="browse-panel" id="browsePanel">
<div class="file-grid" id="fileGrid">
<div class="empty-state">
<i data-lucide="loader"></i>
<p>Se încarcă...</p>
</div>
</div>
</div>
<div class="editor-panel" id="editorPanel">
<div class="editor-header">
<div class="editor-title">
<i data-lucide="file"></i>
<span id="editorFileName">Niciun fișier</span>
</div>
<div class="editor-actions">
<button class="btn btn-ghost" onclick="showBrowse()" title="Înapoi">
<i data-lucide="arrow-left"></i>
</button>
<button class="btn btn-ghost btn-preview" onclick="togglePreview()" id="previewBtn" style="display:none;" title="Preview Markdown">
<i data-lucide="eye"></i>
</button>
<button class="btn btn-ghost" onclick="reloadFile()" id="reloadBtn" disabled title="Reload">
<i data-lucide="refresh-cw"></i>
</button>
<button class="btn btn-primary" onclick="saveFile()" id="saveBtn" disabled>
<i data-lucide="save"></i>
Save
</button>
</div>
</div>
<div class="editor-body" id="editorBody">
<textarea id="codeEditor" placeholder="Selectează un fișier..."></textarea>
<div id="markdownPreview"></div>
</div>
<div class="editor-footer">
<span id="statusText">Ready</span>
<span id="fileInfo"></span>
</div>
</div>
</div>
</main>
<script>
// Theme
function initTheme() {
const saved = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', saved);
updateThemeIcon(saved);
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
updateThemeIcon(next);
}
function updateThemeIcon(theme) {
const icon = document.getElementById('themeIcon');
if (icon) {
icon.setAttribute('data-lucide', theme === 'dark' ? 'sun' : 'moon');
lucide.createIcons();
}
}
initTheme();
lucide.createIcons();
const API_BASE = window.location.pathname.includes('/echo/') ? '/echo' : '';
let currentPath = '';
let currentFile = null;
let originalContent = '';
let isModified = false;
function showBrowse() {
if (isModified && !confirm('Ai modificări nesalvate. Continui?')) return;
document.getElementById('browsePanel').classList.remove('hidden');
document.getElementById('editorPanel').classList.remove('active');
document.getElementById('browseBtn').classList.add('active');
document.getElementById('editorBtn').classList.remove('active');
}
function showEditor() {
document.getElementById('browsePanel').classList.add('hidden');
document.getElementById('editorPanel').classList.add('active');
document.getElementById('browseBtn').classList.remove('active');
document.getElementById('editorBtn').classList.add('active');
}
async function loadPath(path = '') {
currentPath = path;
updateBreadcrumb();
try {
const response = await fetch(`${API_BASE}/api/files?path=${encodeURIComponent(path)}&action=list`);
const data = await response.json();
if (data.error) {
showError(data.error);
return;
}
if (data.type === 'dir') {
renderFileGrid(data.items);
updateURL(path);
} else if (data.type === 'file') {
openFile(path, data);
}
} catch (e) {
showError('Eroare: ' + e.message);
}
}
function updateBreadcrumb() {
const breadcrumb = document.getElementById('breadcrumb');
const parts = currentPath.split('/').filter(p => p);
let html = `<span class="breadcrumb-item ${parts.length === 0 ? 'current' : ''}" onclick="loadPath('')">~/clawd</span>`;
let buildPath = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
buildPath += (buildPath ? '/' : '') + part;
const p = buildPath;
const isCurrent = i === parts.length - 1;
html += `
<span class="breadcrumb-sep"><i data-lucide="chevron-right"></i></span>
<span class="breadcrumb-item ${isCurrent ? 'current' : ''}" onclick="loadPath('${p}')">${part}</span>
`;
}
breadcrumb.innerHTML = html;
lucide.createIcons();
}
function renderFileGrid(items) {
const grid = document.getElementById('fileGrid');
if (items.length === 0) {
grid.innerHTML = `
<div class="empty-state">
<i data-lucide="folder-open"></i>
<p>Folder gol</p>
</div>
`;
lucide.createIcons();
return;
}
items.sort((a, b) => {
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
return a.name.localeCompare(b.name);
});
grid.innerHTML = items.map(item => `
<div class="file-item ${currentFile === item.path ? 'active' : ''}" onclick="handleClick('${item.path}', '${item.type}')">
<div class="file-icon ${item.type === 'dir' ? 'folder' : ''}">
<i data-lucide="${item.type === 'dir' ? 'folder' : getFileIcon(item.name)}"></i>
</div>
<div class="file-name">${item.name}</div>
${item.size ? `<div class="file-size">${formatSize(item.size)}</div>` : ''}
</div>
`).join('');
lucide.createIcons();
}
function getFileIcon(name) {
const ext = name.split('.').pop().toLowerCase();
const icons = {
'md': 'file-text',
'txt': 'file-text',
'json': 'file-json',
'js': 'file-code',
'py': 'file-code',
'html': 'file-code',
'css': 'file-code',
'sh': 'terminal',
'yml': 'file-cog',
'yaml': 'file-cog',
'log': 'file-text',
'xsd': 'file-code',
'pdf': 'file-text'
};
return icons[ext] || 'file';
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function handleClick(path, type) {
if (type === 'dir') {
loadPath(path);
} else {
if (isModified && !confirm('Ai modificări nesalvate. Continui?')) return;
loadPath(path);
}
}
function openFile(path, data) {
currentFile = path;
originalContent = data.content;
updateURL(path);
document.getElementById('editorFileName').textContent = data.name;
document.getElementById('codeEditor').value = data.content;
document.getElementById('saveBtn').disabled = false;
document.getElementById('reloadBtn').disabled = false;
document.getElementById('fileInfo').textContent = formatSize(data.size);
// Show preview button for markdown files
const isMarkdown = path.endsWith('.md');
document.getElementById('previewBtn').style.display = isMarkdown ? 'flex' : 'none';
// Reset preview state
document.getElementById('editorBody').classList.remove('preview-active');
document.getElementById('previewBtn').classList.remove('active');
if (data.truncated) {
setStatus('Fișier trunchiat', 'error');
} else {
setStatus('Loaded', 'saved');
}
isModified = false;
showEditor();
}
function togglePreview() {
const editorBody = document.getElementById('editorBody');
const previewBtn = document.getElementById('previewBtn');
const preview = document.getElementById('markdownPreview');
const content = document.getElementById('codeEditor').value;
if (editorBody.classList.contains('preview-active')) {
// Switch to edit mode
editorBody.classList.remove('preview-active');
previewBtn.classList.remove('active');
setStatus('Edit mode', 'saved');
} else {
// Switch to preview mode
preview.innerHTML = marked.parse(content);
editorBody.classList.add('preview-active');
previewBtn.classList.add('active');
setStatus('Preview mode', 'saved');
}
}
async function saveFile() {
if (!currentFile) return;
const content = document.getElementById('codeEditor').value;
try {
document.getElementById('saveBtn').disabled = true;
setStatus('Se salvează...', 'modified');
const response = await fetch(`${API_BASE}/api/files`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: currentFile, content })
});
const data = await response.json();
if (data.error) {
setStatus('Eroare: ' + data.error, 'error');
} else {
originalContent = content;
isModified = false;
setStatus('Salvat ✓', 'saved');
}
} catch (e) {
setStatus('Eroare: ' + e.message, 'error');
}
document.getElementById('saveBtn').disabled = false;
}
function reloadFile() {
if (currentFile && confirm('Renunți la modificări?')) {
loadPath(currentFile);
}
}
function setStatus(text, type) {
const status = document.getElementById('statusText');
status.textContent = text;
status.className = 'status-' + type;
}
function showError(msg) {
document.getElementById('fileGrid').innerHTML = `
<div class="empty-state">
<i data-lucide="alert-circle"></i>
<p style="color: var(--error)">${msg}</p>
</div>
`;
lucide.createIcons();
}
function updateURL(path) {
if (path) {
history.replaceState(null, '', `#${path}`);
} else {
history.replaceState(null, '', window.location.pathname);
}
}
function getPathFromURL() {
const hash = window.location.hash;
return hash ? decodeURIComponent(hash.slice(1)) : '';
}
window.addEventListener('hashchange', () => loadPath(getPathFromURL()));
document.getElementById('codeEditor').addEventListener('input', function() {
isModified = this.value !== originalContent;
setStatus(isModified ? 'Modified' : 'Ready', isModified ? 'modified' : 'saved');
});
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
saveFile();
}
});
// Init
loadPath(getPathFromURL());
</script>
</body>
</html>