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:
parent
719656e83b
commit
c15b14e09a
6 changed files with 294 additions and 0 deletions
38
web/backend/dist/index.html
vendored
Normal file
38
web/backend/dist/index.html
vendored
Normal 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
32
web/backend/dist/sessions.js
vendored
Normal 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
82
web/backend/dist/styles/vault.css
vendored
Normal 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
36
web/backend/dist/tags.js
vendored
Normal 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
42
web/backend/dist/tools-skills.js
vendored
Normal 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
64
web/backend/dist/vault.js
vendored
Normal 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;
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue