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