Files
clawd/dashboard/notes.html
Echo 10fb3d6fb5 refactor: mutat kb/ -> memory/kb/ pentru memory search
- Mutat toate fișierele din kb/ în memory/kb/
- Actualizat toate referințele în fișiere (.md, .py, .html)
- Actualizat 10 joburi cron cu noi căi
- Memory search indexează acum 58 fișiere din memory/
- TOOLS.md actualizat cu documentație completă
2026-02-01 21:18:45 +00:00

1268 lines
44 KiB
HTML

<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<title>Echo · KB</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;
display: flex;
align-items: center;
gap: var(--space-2);
}
.note-file-link {
opacity: 0.4;
transition: opacity var(--transition-fast);
flex-shrink: 0;
}
.note-file-link:hover {
opacity: 1;
}
.note-file-link svg {
width: 14px;
height: 14px;
color: var(--text-muted);
}
.note-tags {
display: inline;
line-height: 1.4;
}
.note-tag {
font-size: var(--text-sm);
margin-right: 6px;
}
.note-tag.domain {
color: #f97316;
}
.note-tag.type {
color: #8b5cf6;
}
.note-tag.tag {
color: #94a3b8;
}
.note-tags-toggle {
color: #94a3b8;
cursor: pointer;
font-size: var(--text-sm);
}
.note-tags-toggle:hover {
color: var(--text-primary);
}
.note-tags-full {
color: #94a3b8;
}
/* 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: flex-start;
gap: var(--space-3);
position: sticky;
top: 0;
background: var(--bg-base);
z-index: 10;
flex-wrap: wrap;
}
.note-viewer-header h2 {
font-size: var(--text-lg);
color: var(--text-primary);
font-weight: 600;
flex: 1;
min-width: 200px;
}
.viewer-path {
font-size: var(--text-xs);
color: var(--text-muted);
font-family: var(--font-mono);
text-decoration: none;
opacity: 0.7;
transition: opacity var(--transition-fast);
flex: 1;
min-width: 200px;
}
.viewer-path:hover {
opacity: 1;
color: var(--accent);
}
.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); }
/* Filter bar with pills */
.filter-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-4);
padding: var(--space-3);
background: var(--bg-surface);
border-radius: var(--radius-lg);
border: 1px solid rgba(255, 255, 255, 0.1);
}
[data-theme="light"] .filter-bar {
border-color: rgba(0, 0, 0, 0.1);
}
.filter-separator {
color: var(--text-muted);
opacity: 0.3;
margin: 0 var(--space-1);
}
.filter-group {
display: flex;
align-items: center;
gap: var(--space-1);
flex-wrap: wrap;
}
.filter-pill {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
background: var(--bg-secondary);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: var(--radius-full);
font-size: var(--text-sm);
color: var(--text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
white-space: nowrap;
}
[data-theme="light"] .filter-pill {
border-color: rgba(0, 0, 0, 0.1);
}
.filter-pill:hover {
background: var(--bg-surface-hover);
border-color: var(--accent);
}
.filter-pill.active {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.filter-pill.dimmed {
opacity: 0.4;
}
.filter-pill.dimmed:hover {
opacity: 0.7;
}
/* Category pills - teal */
.filter-pill.category {
background: rgba(20, 184, 166, 0.15);
border-color: rgba(20, 184, 166, 0.4);
color: #14b8a6;
}
.filter-pill.category:hover {
background: rgba(20, 184, 166, 0.25);
}
.filter-pill.category.active {
background: #14b8a6;
color: white;
}
/* Project pills - blue */
.filter-pill.project {
background: rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.4);
color: #3b82f6;
}
.filter-pill.project:hover {
background: rgba(59, 130, 246, 0.25);
}
.filter-pill.project.active {
background: #3b82f6;
color: white;
}
/* Domain pills - orange */
.filter-pill.domain {
background: rgba(249, 115, 22, 0.15);
border-color: rgba(249, 115, 22, 0.4);
color: #f97316;
}
.filter-pill.domain:hover {
background: rgba(249, 115, 22, 0.25);
}
.filter-pill.domain.active {
background: #f97316;
color: white;
}
/* Type pills - purple */
.filter-pill.type {
background: rgba(139, 92, 246, 0.15);
border-color: rgba(139, 92, 246, 0.4);
color: #8b5cf6;
}
.filter-pill.type:hover {
background: rgba(139, 92, 246, 0.25);
}
.filter-pill.type.active {
background: #8b5cf6;
color: white;
}
/* Tag pills - gray */
.filter-pill.tag {
background: rgba(100, 116, 139, 0.15);
border-color: rgba(100, 116, 139, 0.4);
color: #94a3b8;
}
.filter-pill.tag:hover {
background: rgba(100, 116, 139, 0.25);
}
.filter-pill.tag.active {
background: #64748b;
color: white;
}
.filter-pill-count {
font-size: var(--text-xs);
opacity: 0.7;
}
.filter-actions {
display: flex;
align-items: center;
gap: var(--space-2);
margin-left: auto;
}
.filter-btn {
padding: var(--space-2);
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
}
.filter-btn:hover {
background: var(--bg-surface-hover);
color: var(--text-primary);
}
.filter-btn.clear:hover {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.filter-btn svg {
width: 16px;
height: 16px;
}
.more-tags-toggle {
color: var(--text-muted);
font-size: var(--text-sm);
cursor: pointer;
padding: var(--space-1) var(--space-2);
}
.more-tags-toggle:hover {
color: var(--text-primary);
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.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;
}
/* Category pills (📁) - teal */
.tag-pill.category {
background: rgba(20, 184, 166, 0.2);
border-color: rgba(20, 184, 166, 0.5);
color: #14b8a6;
}
.tag-pill.category:hover {
background: rgba(20, 184, 166, 0.3);
}
.tag-pill.category.active {
background: #14b8a6;
border-color: #14b8a6;
color: white;
}
/* Domain pills (@) - purple */
.tag-pill.domain {
background: rgba(139, 92, 246, 0.2);
border-color: rgba(139, 92, 246, 0.5);
color: #8b5cf6;
}
.tag-pill.domain:hover {
background: rgba(139, 92, 246, 0.3);
}
.tag-pill.domain.active {
background: #8b5cf6;
border-color: #8b5cf6;
color: white;
}
/* Light mode adjustments */
[data-theme="light"] .tag-pill.category {
color: #0d9488;
}
[data-theme="light"] .tag-pill.domain {
color: #7c3aed;
}
.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>KB</span>
</a>
<a href="files.html" class="nav-item">
<i data-lucide="folder"></i>
<span>Files</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">Knowledge Base</h1>
<div class="search-bar">
<input type="text" class="input" id="searchInput" placeholder="Caută în notițe..." oninput="filterNotes()">
</div>
</div>
<div class="filter-bar" id="filterBar">
<div class="filter-group" id="categoryPills"></div>
<span class="filter-separator" id="sep1">|</span>
<div class="filter-group" id="projectPills"></div>
<span class="filter-separator" id="sep2">|</span>
<div class="filter-group" id="typePills"></div>
<span class="filter-separator" id="sep3">|</span>
<div class="filter-group" id="tagPills"></div>
<span class="more-tags-toggle" id="moreTagsToggle" onclick="toggleMoreTags()"></span>
<div class="filter-actions">
<button class="filter-btn clear" onclick="clearFilters()" title="Resetează" id="clearBtn" style="display: none;">
<i data-lucide="x"></i>
</button>
<button class="filter-btn" onclick="refreshIndex()" title="Reîncarcă">
<i data-lucide="refresh-cw" id="refreshIcon"></i>
</button>
</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>
<a id="viewerPath" href="#" class="viewer-path" target="_blank"></a>
<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 = [];
let lastFilteredNotes = null;
const notesBasePath = "notes-data/";
const indexPath = notesBasePath + "index.json";
// Load notes index from JSON
async function loadNotesIndex() {
try {
const response = await fetch(indexPath + '?t=' + Date.now());
const data = await response.json();
notesIndex = Array.isArray(data) ? data : (data.notes || []);
console.log(`Loaded ${notesIndex.length} notes from index.json`);
} catch (e) {
console.error('Failed to load notes index:', e);
notesIndex = [];
}
}
// Current filter state
let selectedFilters = {
category: null,
project: null,
domain: null,
type: null,
tags: new Set()
};
let showAllTags = false;
// Get counts for each filter value
function getFilterCounts(filteredNotes = null) {
const notes = filteredNotes || notesIndex;
const counts = {
categories: {},
projects: {},
domains: {},
types: {},
tags: {}
};
notes.forEach(note => {
if (note.category) counts.categories[note.category] = (counts.categories[note.category] || 0) + 1;
if (note.project) counts.projects[note.project] = (counts.projects[note.project] || 0) + 1;
if (note.domains) note.domains.forEach(d => counts.domains[d] = (counts.domains[d] || 0) + 1);
if (note.types) note.types.forEach(t => counts.types[t] = (counts.types[t] || 0) + 1);
if (note.tags) note.tags.forEach(t => counts.tags[t] = (counts.tags[t] || 0) + 1);
});
return counts;
}
// Render filter pills
function renderFilterPills(filteredNotes = null) {
const counts = getFilterCounts();
const visibleCounts = filteredNotes ? getFilterCounts(filteredNotes) : counts;
// Categories
const catHtml = Object.entries(counts.categories)
.sort((a, b) => b[1] - a[1])
.map(([cat, count]) => {
const isActive = selectedFilters.category === cat;
const isVisible = visibleCounts.categories[cat] > 0;
const dimmed = filteredNotes && !isVisible && !isActive ? 'dimmed' : '';
return `<span class="filter-pill category ${isActive ? 'active' : ''} ${dimmed}" onclick="toggleFilter('category', '${cat}')">
📁${cat} <span class="filter-pill-count">${count}</span>
</span>`;
}).join('');
document.getElementById('categoryPills').innerHTML = catHtml;
// Projects (only show if category=projects or no category selected)
const showProjects = !selectedFilters.category || selectedFilters.category === 'projects';
const projHtml = showProjects && Object.keys(counts.projects).length > 0 ? Object.entries(counts.projects)
.sort((a, b) => b[1] - a[1])
.map(([proj, count]) => {
const isActive = selectedFilters.project === proj;
const isVisible = visibleCounts.projects[proj] > 0;
const dimmed = filteredNotes && !isVisible && !isActive ? 'dimmed' : '';
return `<span class="filter-pill project ${isActive ? 'active' : ''} ${dimmed}" onclick="toggleFilter('project', '${proj}')">
📂${proj} <span class="filter-pill-count">${count}</span>
</span>`;
}).join('') : '';
document.getElementById('projectPills').innerHTML = projHtml;
document.getElementById('sep1').style.display = projHtml ? '' : 'none';
// Domains (@work, @health, etc.) - orange
const domainHtml = Object.entries(counts.domains)
.sort((a, b) => b[1] - a[1])
.map(([domain, count]) => {
const isActive = selectedFilters.domain === domain;
const isVisible = visibleCounts.domains[domain] > 0;
const dimmed = filteredNotes && !isVisible && !isActive ? 'dimmed' : '';
return `<span class="filter-pill domain ${isActive ? 'active' : ''} ${dimmed}" onclick="toggleFilter('domain', '${domain}')">
@${domain} <span class="filter-pill-count">${count}</span>
</span>`;
}).join('');
// Types (@meditatie, @exercitiu, etc.)
const typeHtml = Object.entries(counts.types)
.sort((a, b) => b[1] - a[1])
.map(([type, count]) => {
const isActive = selectedFilters.type === type;
const isVisible = visibleCounts.types[type] > 0;
const dimmed = filteredNotes && !isVisible && !isActive ? 'dimmed' : '';
return `<span class="filter-pill type ${isActive ? 'active' : ''} ${dimmed}" onclick="toggleFilter('type', '${type}')">
@${type} <span class="filter-pill-count">${count}</span>
</span>`;
}).join('');
// Combine domains and types in typePills
document.getElementById('typePills').innerHTML = domainHtml + typeHtml;
document.getElementById('sep2').style.display = (domainHtml || typeHtml) ? '' : 'none';
// Tags - collapsed by default, show only count
const tagEntries = Object.entries(counts.tags).sort((a, b) => b[1] - a[1]);
const activeTagsCount = selectedFilters.tags.size;
let tagHtml = '';
if (showAllTags) {
// Show all tags
tagHtml = tagEntries.map(([tag, count]) => {
const isActive = selectedFilters.tags.has(tag);
const isVisible = visibleCounts.tags[tag] > 0;
const dimmed = filteredNotes && !isVisible && !isActive ? 'dimmed' : '';
return `<span class="filter-pill tag ${isActive ? 'active' : ''} ${dimmed}" onclick="toggleFilter('tag', '${tag}')">
#${tag} <span class="filter-pill-count">${count}</span>
</span>`;
}).join('');
} else if (activeTagsCount > 0) {
// Show only active tags
tagHtml = [...selectedFilters.tags].map(tag => {
const count = counts.tags[tag] || 0;
return `<span class="filter-pill tag active" onclick="toggleFilter('tag', '${tag}')">
#${tag} <span class="filter-pill-count">${count}</span>
</span>`;
}).join('');
}
document.getElementById('tagPills').innerHTML = tagHtml;
document.getElementById('sep3').style.display = (tagHtml || tagEntries.length > 0) ? '' : 'none';
// More tags toggle
const moreToggle = document.getElementById('moreTagsToggle');
if (tagEntries.length > 0) {
const hiddenCount = activeTagsCount > 0 ? tagEntries.length - activeTagsCount : tagEntries.length;
if (showAllTags) {
moreToggle.textContent = `-${hiddenCount} tags`;
} else {
moreToggle.textContent = `+${hiddenCount} tags`;
}
moreToggle.style.display = '';
} else {
moreToggle.style.display = 'none';
}
// Clear button
const hasFilters = selectedFilters.category || selectedFilters.project ||
selectedFilters.domain || selectedFilters.type || selectedFilters.tags.size > 0;
document.getElementById('clearBtn').style.display = hasFilters ? '' : 'none';
}
// Toggle filter
function toggleFilter(filterType, value) {
if (filterType === 'tag') {
if (selectedFilters.tags.has(value)) {
selectedFilters.tags.delete(value);
} else {
selectedFilters.tags.add(value);
}
} else {
selectedFilters[filterType] = selectedFilters[filterType] === value ? null : value;
// Reset child filters when parent changes
if (filterType === 'category' && value !== 'projects') {
selectedFilters.project = null;
}
}
filterNotes();
}
// Toggle more tags
function toggleMoreTags() {
showAllTags = !showAllTags;
renderFilterPills(lastFilteredNotes);
}
// Toggle tags in note card
function toggleNoteTags(noteId) {
const full = document.getElementById('tags_' + noteId);
const toggle = full.parentElement;
const count = toggle.querySelector('.note-tags-count');
if (full.style.display === 'none') {
full.style.display = 'inline';
count.style.display = 'none';
} else {
full.style.display = 'none';
count.style.display = 'inline';
}
}
// Clear all filters
function clearFilters() {
selectedFilters = { category: null, project: null, domain: null, type: null, tags: new Set() };
showAllTags = false;
document.getElementById('searchInput').value = '';
filterNotes();
}
// Refresh index from server
async function refreshIndex() {
const icon = document.getElementById('refreshIcon');
if (icon) icon.style.animation = 'spin 1s linear infinite';
try {
const response = await fetch('api/refresh-index', { method: 'POST' });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.success) {
await loadNotesIndex();
filterNotes();
lucide.createIcons();
console.log('Index refreshed:', data.message);
} else {
console.error('Refresh failed:', data.error);
alert('Eroare: ' + (data.error || 'Unknown error'));
}
} catch (e) {
console.error('Refresh error:', e);
alert('Eroare la refresh: ' + e.message);
} finally {
if (icon) icon.style.animation = '';
}
}
// Filter notes
function filterNotes() {
const query = document.getElementById('searchInput').value.toLowerCase().trim();
let filtered = notesIndex;
// Filter by category
if (selectedFilters.category) {
filtered = filtered.filter(n => n.category === selectedFilters.category);
}
// Filter by project
if (selectedFilters.project) {
filtered = filtered.filter(n => n.project === selectedFilters.project);
}
// Filter by domain (@work, @health, etc.)
if (selectedFilters.domain) {
filtered = filtered.filter(n => n.domains && n.domains.includes(selectedFilters.domain));
}
// Filter by type (@meditatie, @exercitiu, etc.)
if (selectedFilters.type) {
filtered = filtered.filter(n => n.types && n.types.includes(selectedFilters.type));
}
// Filter by tags (AND)
if (selectedFilters.tags.size > 0) {
filtered = filtered.filter(n => {
const noteTags = n.tags || [];
return [...selectedFilters.tags].every(t => noteTags.includes(t));
});
}
// Filter by search
if (query) {
filtered = filtered.filter(n => {
const titleMatch = n.title.toLowerCase().includes(query);
const tagsMatch = (n.tags || []).some(t => t.toLowerCase().includes(query));
const contentMatch = (notesCache[n.file] || '').toLowerCase().includes(query);
return titleMatch || tagsMatch || contentMatch;
});
}
lastFilteredNotes = filtered;
renderFilterPills(filtered);
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) {
// Domains (portocaliu), Types (mov), Tags colapsate cu expand
const tags = note.tags || [];
const noteId = note.file.replace(/[^a-zA-Z0-9]/g, '_');
const tagsHtml = tags.length > 0
? `<span class="note-tags-toggle" onclick="event.stopPropagation(); toggleNoteTags('${noteId}')">
<span class="note-tags-count">${tags.length} tags</span>
<span class="note-tags-full" id="tags_${noteId}" style="display:none">${tags.join(' · ')}</span>
</span>`
: '';
const allTags = [
...(note.domains || []).map(d => `<span class="note-tag domain">${d}</span>`),
...(note.types || []).map(t => `<span class="note-tag type">${t}</span>`),
tagsHtml
].filter(Boolean).join('');
// Convert notes-data/ to kb/ for files.html links
const filesPath = note.file.replace(/^notes-data\//, 'memory/kb/');
return `
<div class="note-card" onclick="openNote('${note.file}')">
<div class="note-title">
${note.title}
<a href="files.html#${filesPath}" class="note-file-link" onclick="event.stopPropagation()" title="${filesPath}">
<i data-lucide="external-link"></i>
</a>
</div>
<div class="note-tags">${allTags}</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;
const pathEl = document.getElementById('viewerPath');
// Convert notes-data/ to kb/ for display and links
const filesPath = note.file.replace(/^notes-data\//, 'memory/kb/');
pathEl.textContent = filesPath;
pathEl.href = 'files.html#' + filesPath;
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(file + '?t=' + Date.now());
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(note.file + '?t=' + Date.now());
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();
renderFilterPills();
renderNotesAccordion(notesIndex);
lucide.createIcons();
preloadNotes();
checkHash();
}
init();
</script>
</body>
</html>