Files
clawd/kanban/notes.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

755 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 · Notes</title>
<link rel="stylesheet" href="common.css">
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script src="swipe-nav.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
.main {
max-width: 1200px;
margin: 0 auto;
padding: var(--space-5);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-6);
flex-wrap: wrap;
gap: var(--space-4);
}
.page-title {
font-size: var(--text-xl);
font-weight: 600;
color: var(--text-primary);
}
.search-bar {
width: 300px;
}
/* Date sections (accordion) */
.date-section {
margin-bottom: var(--space-3);
}
.date-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
background: var(--bg-surface);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: var(--radius-md);
cursor: pointer;
user-select: none;
transition: all var(--transition-fast);
}
[data-theme="light"] .date-header {
border-color: rgba(0, 0, 0, 0.15);
}
.date-header:hover {
filter: brightness(1.1);
}
/* Section colors */
[data-section="today"] .date-header {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.25), rgba(37, 99, 235, 0.15));
border-left: 3px solid #3b82f6;
}
[data-section="yesterday"] .date-header {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.25), rgba(124, 58, 237, 0.15));
border-left: 3px solid #8b5cf6;
}
[data-section="thisWeek"] .date-header {
background: linear-gradient(135deg, rgba(20, 184, 166, 0.25), rgba(13, 148, 136, 0.15));
border-left: 3px solid #14b8a6;
}
[data-section="older"] .date-header {
background: linear-gradient(135deg, rgba(249, 115, 22, 0.25), rgba(234, 88, 12, 0.15));
border-left: 3px solid #f97316;
}
.date-header-left {
display: flex;
align-items: center;
gap: var(--space-3);
}
.date-header-left svg {
width: 18px;
height: 18px;
color: var(--text-muted);
transition: transform var(--transition-fast);
}
.date-section.collapsed .date-header-left svg {
transform: rotate(-90deg);
}
.date-label {
font-size: var(--text-sm);
font-weight: 600;
color: var(--text-primary);
}
.date-sublabel {
font-size: var(--text-xs);
color: var(--text-muted);
margin-left: var(--space-2);
}
.date-section.collapsed .date-content {
display: none;
}
.date-content {
padding: var(--space-4) 0;
}
/* Notes grid inside sections */
.notes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--space-4);
}
.note-card {
background: var(--bg-surface);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: var(--radius-lg);
padding: var(--space-4);
cursor: pointer;
transition: all var(--transition-base);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
[data-theme="light"] .note-card {
border-color: rgba(0, 0, 0, 0.15);
}
.note-card:hover {
border-color: var(--accent);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.note-title {
font-size: var(--text-sm);
font-weight: 600;
color: var(--text-primary);
line-height: 1.4;
}
.note-tags {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
}
/* Empty section */
.empty-section {
padding: var(--space-4);
text-align: center;
color: var(--text-muted);
font-size: var(--text-sm);
}
/* Note viewer overlay */
.note-viewer {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 200;
overflow: auto;
}
.note-viewer.active {
display: block;
}
.note-viewer-content {
max-width: 800px;
margin: var(--space-5) auto;
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
min-height: calc(100vh - var(--space-10));
}
.note-viewer-header {
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
background: var(--bg-base);
z-index: 10;
}
.note-viewer-header h2 {
font-size: var(--text-lg);
color: var(--text-primary);
font-weight: 600;
}
.note-viewer-body {
padding: var(--space-6);
}
/* Markdown */
.markdown-body {
color: var(--text-secondary);
line-height: 1.7;
}
.markdown-body h1 {
font-size: 1.8rem;
color: var(--text-primary);
margin-bottom: var(--space-4);
padding-bottom: var(--space-3);
border-bottom: 1px solid var(--border);
}
.markdown-body h2 {
font-size: 1.3rem;
color: var(--accent);
margin-top: var(--space-6);
margin-bottom: var(--space-3);
}
.markdown-body h3 {
font-size: 1.1rem;
color: var(--text-primary);
margin-top: var(--space-5);
margin-bottom: var(--space-2);
}
.markdown-body p { margin-bottom: var(--space-4); }
.markdown-body ul, .markdown-body ol {
margin-bottom: var(--space-4);
padding-left: var(--space-6);
}
.markdown-body li { margin-bottom: var(--space-2); }
.markdown-body a {
color: var(--accent);
text-decoration: none;
}
.markdown-body a:hover { text-decoration: underline; }
.markdown-body code {
background: var(--bg-surface);
padding: 2px 6px;
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: 0.9em;
}
.markdown-body pre {
background: var(--bg-surface);
padding: var(--space-4);
border-radius: var(--radius-md);
overflow-x: auto;
margin-bottom: var(--space-4);
}
.markdown-body pre code {
background: none;
padding: 0;
}
.markdown-body blockquote {
border-left: 3px solid var(--accent);
padding-left: var(--space-4);
color: var(--text-muted);
margin-bottom: var(--space-4);
}
.markdown-body strong { color: var(--text-primary); }
/* Tag filter pills */
.tag-filter {
margin-bottom: var(--space-5);
}
.tag-filter-label {
font-size: var(--text-xs);
color: var(--text-muted);
margin-bottom: var(--space-2);
display: block;
}
.tag-pills {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.tag-pill {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-2) var(--space-3);
background: var(--bg-surface);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: var(--radius-full);
font-size: var(--text-sm);
color: var(--text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
user-select: none;
}
[data-theme="light"] .tag-pill {
border-color: rgba(0, 0, 0, 0.15);
}
.tag-pill:hover {
background: var(--bg-surface-hover);
border-color: var(--accent);
}
.tag-pill.active {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.tag-pill-count {
font-size: var(--text-xs);
opacity: 0.7;
}
.clear-filters {
font-size: var(--text-xs);
color: var(--text-muted);
background: none;
border: none;
cursor: pointer;
padding: var(--space-2);
margin-left: var(--space-2);
}
.clear-filters:hover {
color: var(--accent);
}
/* No results */
.no-results {
text-align: center;
padding: var(--space-10);
color: var(--text-muted);
}
.no-results svg {
width: 48px;
height: 48px;
margin-bottom: var(--space-4);
opacity: 0.5;
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: stretch;
}
.search-bar { width: 100%; }
.notes-grid {
grid-template-columns: 1fr;
}
.note-viewer-content {
margin: 0;
border-radius: 0;
min-height: 100vh;
}
}
</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 active">
<i data-lucide="file-text"></i>
<span>Notes</span>
</a>
<a href="files.html" class="nav-item">
<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="page-header">
<h1 class="page-title">Notes</h1>
<div class="search-bar">
<input type="text" class="input" id="searchInput" placeholder="Caută în notițe..." oninput="filterNotes()">
</div>
</div>
<div class="tag-filter">
<span class="tag-filter-label">Filtrează după tags:</span>
<div class="tag-pills" id="tagPills"></div>
</div>
<div id="notesContainer">
<div class="no-results">
<i data-lucide="loader"></i>
<p>Se încarcă...</p>
</div>
</div>
</main>
<!-- Note viewer overlay -->
<div class="note-viewer" id="noteViewer" onclick="if(event.target === this) closeNote()">
<div class="note-viewer-content">
<div class="note-viewer-header">
<h2 id="viewerTitle">Titlu</h2>
<button class="btn btn-ghost" onclick="closeNote()">
<i data-lucide="x"></i>
</button>
</div>
<div class="note-viewer-body">
<div id="viewerContent" class="markdown-body"></div>
</div>
</div>
</div>
<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 notesCache = {};
let notesIndex = [];
const notesBasePath = "youtube-notes/";
const indexPath = notesBasePath + "index.json";
// Load notes index from JSON
async function loadNotesIndex() {
try {
const response = await fetch(indexPath + '?t=' + Date.now());
notesIndex = await response.json();
console.log(`Loaded ${notesIndex.length} notes from index.json`);
} catch (e) {
console.error('Failed to load notes index:', e);
notesIndex = [];
}
}
let selectedTags = new Set();
// Extract all tags with counts
function getAllTags() {
const tagCounts = {};
notesIndex.forEach(note => {
note.tags.forEach(tag => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
});
});
// Sort by count descending
return Object.entries(tagCounts)
.sort((a, b) => b[1] - a[1])
.map(([tag, count]) => ({ tag, count }));
}
// Render tag pills
function renderTagPills() {
const container = document.getElementById('tagPills');
const tags = getAllTags();
let html = tags.map(({ tag, count }) => `
<span class="tag-pill ${selectedTags.has(tag) ? 'active' : ''}"
onclick="toggleTag('${tag}')">
${tag} <span class="tag-pill-count">(${count})</span>
</span>
`).join('');
if (selectedTags.size > 0) {
html += `<button class="clear-filters" onclick="clearTagFilters()">✕ Clear</button>`;
}
container.innerHTML = html;
}
// Toggle tag selection
function toggleTag(tag) {
if (selectedTags.has(tag)) {
selectedTags.delete(tag);
} else {
selectedTags.add(tag);
}
renderTagPills();
filterNotes();
}
// Clear all tag filters
function clearTagFilters() {
selectedTags.clear();
renderTagPills();
filterNotes();
}
// Filter notes by search and tags
function filterNotes() {
const query = document.getElementById('searchInput').value.toLowerCase().trim();
let filtered = notesIndex;
// Filter by selected tags (AND logic)
if (selectedTags.size > 0) {
filtered = filtered.filter(note =>
[...selectedTags].every(tag => note.tags.includes(tag))
);
}
// Filter by search query
if (query) {
filtered = filtered.filter(note => {
const titleMatch = note.title.toLowerCase().includes(query);
const tagsMatch = note.tags.some(t => t.toLowerCase().includes(query));
const contentMatch = (notesCache[note.file] || '').toLowerCase().includes(query);
return titleMatch || tagsMatch || contentMatch;
});
}
renderNotesAccordion(filtered);
}
// Group notes by date category
function groupNotesByDate(notes) {
const today = new Date();
today.setHours(0, 0, 0, 0);
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const weekAgo = new Date(today);
weekAgo.setDate(weekAgo.getDate() - 7);
const groups = {
today: { label: 'Azi', sublabel: formatDate(today), notes: [], expanded: true },
yesterday: { label: 'Ieri', sublabel: formatDate(yesterday), notes: [], expanded: false },
thisWeek: { label: 'Săptămâna aceasta', sublabel: '', notes: [], expanded: false },
older: { label: 'Mai vechi', sublabel: '', notes: [], expanded: false }
};
notes.forEach(note => {
const noteDate = new Date(note.date);
noteDate.setHours(0, 0, 0, 0);
if (noteDate.getTime() === today.getTime()) {
groups.today.notes.push(note);
} else if (noteDate.getTime() === yesterday.getTime()) {
groups.yesterday.notes.push(note);
} else if (noteDate >= weekAgo) {
groups.thisWeek.notes.push(note);
} else {
groups.older.notes.push(note);
}
});
return groups;
}
function formatDate(date) {
return date.toLocaleDateString('ro-RO', { day: 'numeric', month: 'short', year: 'numeric' });
}
function renderNotesAccordion(notes = notesIndex) {
const container = document.getElementById('notesContainer');
if (notes.length === 0) {
container.innerHTML = `
<div class="no-results">
<i data-lucide="search-x"></i>
<p>Nicio notiță găsită</p>
</div>
`;
lucide.createIcons();
return;
}
const groups = groupNotesByDate(notes);
let html = '';
Object.entries(groups).forEach(([key, group]) => {
if (group.notes.length === 0) return;
const collapsed = group.expanded ? '' : 'collapsed';
html += `
<div class="date-section ${collapsed}" data-section="${key}">
<div class="date-header" onclick="toggleSection('${key}')">
<div class="date-header-left">
<i data-lucide="chevron-down"></i>
<span class="date-label">${group.label}</span>
<span class="date-sublabel">${group.sublabel}</span>
</div>
<span class="badge">${group.notes.length}</span>
</div>
<div class="date-content">
<div class="notes-grid">
${group.notes.map(note => renderNoteCard(note)).join('')}
</div>
</div>
</div>
`;
});
container.innerHTML = html;
lucide.createIcons();
}
function renderNoteCard(note) {
return `
<div class="note-card" onclick="openNote('${note.file}')">
<div class="note-title">${note.title}</div>
<div class="note-tags">
${note.tags.map(t => `<span class="tag">${t}</span>`).join('')}
</div>
</div>
`;
}
function toggleSection(sectionKey) {
const section = document.querySelector(`[data-section="${sectionKey}"]`);
section.classList.toggle('collapsed');
}
async function openNote(file) {
const note = notesIndex.find(n => n.file === file);
if (!note) return;
document.getElementById('viewerTitle').textContent = note.title;
document.getElementById('viewerContent').innerHTML = '<p style="color: var(--text-muted)">Se încarcă...</p>';
document.getElementById('noteViewer').classList.add('active');
document.body.style.overflow = 'hidden';
// Update URL
const noteId = file.replace(/^\d{4}-\d{2}-\d{2}_/, '').replace(/\.md$/, '');
history.replaceState(null, '', '#' + noteId);
try {
let content = notesCache[file];
if (!content) {
const response = await fetch(notesBasePath + file);
content = await response.text();
notesCache[file] = content;
}
document.getElementById('viewerContent').innerHTML = marked.parse(content);
} catch (error) {
document.getElementById('viewerContent').innerHTML = `<p style="color: var(--error)">Eroare: ${error.message}</p>`;
}
}
function closeNote() {
document.getElementById('noteViewer').classList.remove('active');
document.body.style.overflow = '';
history.replaceState(null, '', window.location.pathname);
}
async function preloadNotes() {
for (const note of notesIndex) {
try {
const response = await fetch(notesBasePath + note.file);
notesCache[note.file] = await response.text();
} catch (e) {
notesCache[note.file] = '';
}
}
}
// searchNotes replaced by filterNotes above
// Handle hash for deep links
function checkHash() {
if (window.location.hash) {
const id = window.location.hash.slice(1);
const note = notesIndex.find(n => {
const noteId = n.file.replace(/^\d{4}-\d{2}-\d{2}_/, '').replace(/\.md$/, '');
return noteId === id;
});
if (note) {
openNote(note.file);
}
}
}
// ESC to close
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeNote();
});
// Init - load index first, then render
async function init() {
await loadNotesIndex();
renderTagPills();
renderNotesAccordion();
preloadNotes();
checkHash();
}
init();
</script>
</body>
</html>