feat(ui): add Obsidian-inspired vault sidebar with vanilla JS components

- Add vault.js for sidebar tree view
- Add tags.js for tag cloud
- Add sessions.js for session history
- Add tools-skills.js for tool skills panel
- Add vault.css with Obsidian-inspired styling
- Update index.html with sidebar structure and script tags
This commit is contained in:
Dark aura 2026-05-07 05:51:30 +01:00
parent 719656e83b
commit c15b14e09a
6 changed files with 294 additions and 0 deletions

38
web/backend/dist/index.html vendored Normal file
View file

@ -0,0 +1,38 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PicoClaw</title>
<script type="module" crossorigin src="/assets/index-C2w0BPus.js"></script>
<link rel="modulepreload" crossorigin href="/assets/shim-ClcZ9bMo.js">
<link rel="stylesheet" crossorigin href="/assets/index-DHuJSidx.css">
<link rel="stylesheet" href="/styles/vault.css">
</head>
<body>
<div id="root"></div>
<div id="vault-sidebar" class="sidebar hidden">
<div class="sidebar-header">
<h3>Vault</h3>
<button id="close-sidebar"></button>
</div>
<div id="vault-tree" class="tree-view"></div>
<div id="tag-cloud" class="tag-cloud"></div>
<div id="session-history" class="session-history"></div>
<div id="tool-skills-panel" class="tool-skills-panel"></div>
</div>
<button id="toggle-sidebar">☰ Vault</button>
<script src="/vault.js"></script>
<script src="/tags.js"></script>
<script src="/sessions.js"></script>
<script src="/tools-skills.js"></script>
</body>
</html>

32
web/backend/dist/sessions.js vendored Normal file
View file

@ -0,0 +1,32 @@
// web/backend/dist/sessions.js
// Session History Controller (~4KB)
function loadSessionHistory() {
const history = document.getElementById('session-history');
if (!history) return;
fetch('/api/sessions/search?limit=20')
.then(r => r.json())
.then(sessions => {
history.innerHTML = '<h4>Recent Sessions</h4>';
sessions.forEach(session => {
const div = document.createElement('div');
div.className = 'session-item';
div.innerHTML = `
<div class="session-title">${session.title || 'Untitled'}</div>
<div class="session-tags">${(session.tags || []).map(t => `#${t}`).join(' ')}</div>
<div class="session-date">${new Date(session.timestamp).toLocaleDateString()}</div>
`;
div.onclick = () => loadSessionContext(session.id);
history.appendChild(div);
});
})
.catch(e => console.error('Failed to load sessions:', e));
}
function loadSessionContext(sessionId) {
// TODO: load session context into chat
console.log('Load session:', sessionId);
}
// Expose
window.loadSessionHistory = loadSessionHistory;

82
web/backend/dist/styles/vault.css vendored Normal file
View file

@ -0,0 +1,82 @@
/* web/backend/dist/styles/vault.css */
.sidebar {
position: fixed;
left: 0;
top: 0;
width: 300px;
height: 100%;
background: var(--background-primary, #1e1e1e);
color: var(--text-normal, #dcddde);
padding: 10px;
overflow-y: auto;
z-index: 1000;
}
.sidebar.hidden {
display: none;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.sidebar-header h3 {
margin: 0;
}
.tree-item {
padding: 5px 10px;
cursor: pointer;
border-radius: 4px;
}
.tree-item:hover {
background: var(--background-secondary, #2d2d2d);
}
.tag-badge {
display: inline-block;
background: var(--background-secondary, #2d2d2d);
border-radius: 12px;
padding: 2px 8px;
margin: 2px;
cursor: pointer;
font-size: 0.9em;
}
.tag-badge:hover {
background: var(--interactive-accent, #5865f2);
color: white;
}
.skill-item {
padding: 8px;
border-bottom: 1px solid var(--background-secondary, #2d2d2d);
}
.skill-name {
font-weight: bold;
}
.skill-usage {
font-size: 0.9em;
color: var(--text-muted, #72767d);
}
.session-item {
padding: 8px;
border-bottom: 1px solid var(--background-secondary, #2d2d2d);
cursor: pointer;
}
.session-title {
font-weight: bold;
}
.session-tags {
font-size: 0.9em;
color: var(--text-muted, #72767d);
}

36
web/backend/dist/tags.js vendored Normal file
View file

@ -0,0 +1,36 @@
// web/backend/dist/tags.js
// Tag Cloud Controller (~3KB)
function loadTagCloud() {
const cloud = document.getElementById('tag-cloud');
if (!cloud) return;
fetch('/api/vault/tags')
.then(r => r.json())
.then(tags => {
cloud.innerHTML = '<h4>Tags</h4>';
tags.forEach(tag => {
const badge = document.createElement('span');
badge.className = 'tag-badge';
badge.textContent = `#${tag.name} (${tag.count})`;
badge.onclick = () => filterByTag(tag.name);
cloud.appendChild(badge);
});
})
.catch(e => console.error('Failed to load tags:', e));
}
function filterByTag(tagName) {
fetch(`/api/sessions/search?tag=${encodeURIComponent(tagName)}`)
.then(r => r.json())
.then(data => displaySearchResults(data))
.catch(e => console.error('Failed to filter by tag:', e));
}
function displaySearchResults(data) {
// TODO: render search results in UI
console.log('Search results:', data);
}
// Expose
window.loadTagCloud = loadTagCloud;
window.filterByTag = filterByTag;

42
web/backend/dist/tools-skills.js vendored Normal file
View file

@ -0,0 +1,42 @@
// web/backend/dist/tools-skills.js
// Tool Skills Panel Controller (~3KB)
function loadToolSkills() {
const panel = document.getElementById('tool-skills-panel');
if (!panel) return;
fetch('/api/tools/skills')
.then(r => r.json())
.then(skills => {
panel.innerHTML = '<h4>Tool Skills</h4>';
skills.forEach(skill => {
const div = document.createElement('div');
div.className = 'skill-item';
div.innerHTML = `
<div class="skill-name">${skill.name || 'Unknown'}</div>
<div class="skill-usage">Used ${skill.usage_count || 0} times</div>
<div class="skill-tags">${(skill.tags || []).map(t => `#${t}`).join(' ')}</div>
`;
panel.appendChild(div);
});
})
.catch(e => console.error('Failed to load tool skills:', e));
}
async function searchCommunityRegistry(query) {
try {
const response = await fetch(`/api/tools/registry?q=${encodeURIComponent(query || '')}`);
const results = await response.json();
displayRegistryResults(results);
} catch (e) {
console.error('Failed to search registry:', e);
}
}
function displayRegistryResults(results) {
console.log('Registry results:', results);
// TODO: render results in UI
}
// Expose
window.loadToolSkills = loadToolSkills;
window.searchCommunityRegistry = searchCommunityRegistry;

64
web/backend/dist/vault.js vendored Normal file
View file

@ -0,0 +1,64 @@
// web/backend/dist/vault.js
// Vault Sidebar Controller (Vanilla JS, ~5KB)
document.addEventListener('DOMContentLoaded', () => {
const sidebar = document.getElementById('vault-sidebar');
const toggleBtn = document.getElementById('toggle-sidebar');
const closeBtn = document.getElementById('close-sidebar');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
sidebar.classList.toggle('hidden');
if (!sidebar.classList.contains('hidden')) {
loadVaultTree();
}
});
}
if (closeBtn) {
closeBtn.addEventListener('click', () => {
sidebar.classList.add('hidden');
});
}
async function loadVaultTree() {
try {
const response = await fetch('/api/vault/list');
const data = await response.json();
renderTree(data.notes || []);
} catch (e) {
console.error('Failed to load vault tree:', e);
}
}
function renderTree(items) {
const tree = document.getElementById('vault-tree');
if (!tree) return;
tree.innerHTML = '';
items.forEach(item => {
const div = document.createElement('div');
div.className = 'tree-item';
div.textContent = item.name || item.path || 'Untitled';
div.onclick = () => loadNote(item.path || item.name);
tree.appendChild(div);
});
}
async function loadNote(path) {
try {
const response = await fetch(`/api/vault/note?path=${encodeURIComponent(path)}`);
const data = await response.json();
showNoteModal(data);
} catch (e) {
console.error('Failed to load note:', e);
}
}
function showNoteModal(noteData) {
// Simple modal implementation
alert(`Note: ${noteData.title || 'Note'}\n\n${noteData.content || ''}`);
}
// Expose functions for other scripts
window.loadVaultTree = loadVaultTree;
window.loadNote = loadNote;
});