Changeset 0.19.1 (#83)

This commit is contained in:
2026-03-01 00:28:58 +00:00
parent 748f49bedd
commit 37a9b1a68e
12 changed files with 580 additions and 22 deletions

View File

@@ -25,6 +25,14 @@ async function loadProjects() {
const saved = JSON.parse(localStorage.getItem('cs-collapsed-projects') || '{}');
App.collapsedProjects = saved;
} catch (_) {}
// Restore active project (v0.19.1)
const savedActive = localStorage.getItem('cs-active-project');
if (savedActive && App.projects.some(p => p.id === savedActive)) {
App.activeProjectId = savedActive;
} else {
App.activeProjectId = null;
localStorage.removeItem('cs-active-project');
}
} catch (e) {
console.warn('Failed to load projects:', e.message);
App.projects = [];
@@ -87,6 +95,11 @@ async function deleteProject(projectId) {
App.projects = App.projects.filter(p => p.id !== projectId);
// Unassign chats client-side
App.chats.forEach(c => { if (c.projectId === projectId) c.projectId = null; });
// Clear active if this was the active project (v0.19.1)
if (App.activeProjectId === projectId) {
App.activeProjectId = null;
localStorage.removeItem('cs-active-project');
}
UI.renderChatList();
UI.toast('Project deleted', 'success');
} catch (e) {
@@ -118,6 +131,19 @@ async function setProjectColor(projectId) {
input.click();
}
// ── Active Project (v0.19.1) ────────────────
function toggleActiveProject(projectId) {
if (App.activeProjectId === projectId) {
App.activeProjectId = null;
localStorage.removeItem('cs-active-project');
} else {
App.activeProjectId = projectId;
localStorage.setItem('cs-active-project', projectId);
}
UI.renderChatList();
}
// ── Move Chat to Project ────────────────────
async function moveChatToProject(chatId, projectId) {
@@ -265,9 +291,19 @@ function showProjectMenu(e, projectId) {
e.stopPropagation();
dismissChatContextMenu();
const isActive = App.activeProjectId === projectId;
const activeLabel = isActive ? 'Unpin project' : 'Pin as active';
const menu = document.createElement('div');
menu.className = 'project-ctx-menu';
menu.innerHTML = `
<button class="ctx-item" onclick="toggleActiveProject('${projectId}');dismissChatContextMenu()">
<span class="ctx-icon">${isActive ? '📌' : '📌'}</span> ${activeLabel}
</button>
<button class="ctx-item" onclick="openProjectPanel('${projectId}');dismissChatContextMenu()">
<span class="ctx-icon">⚙</span> Project settings
</button>
<div class="ctx-divider"></div>
<button class="ctx-item" onclick="renameProject('${projectId}');dismissChatContextMenu()">
<span class="ctx-icon">✏</span> Rename
</button>
@@ -286,3 +322,218 @@ function showProjectMenu(e, projectId) {
_ctxMenu = menu;
setTimeout(() => document.addEventListener('click', dismissChatContextMenu, { once: true }), 0);
}
// ═══════════════════════════════════════════
// PROJECT DETAIL PANEL (v0.19.1)
// ═══════════════════════════════════════════
let _projectPanelId = null; // currently-displayed project ID
function _registerProjectPanel() {
const body = document.getElementById('sidePanelBody');
if (!body || typeof PanelRegistry === 'undefined') return;
// Create the panel element dynamically
const el = document.createElement('div');
el.className = 'side-panel-page';
el.id = 'sidePanelProject';
el.style.display = 'none';
el.innerHTML = `
<div class="project-panel" id="projectPanelInner">
<div class="project-panel-section">
<label class="project-panel-label">System Prompt</label>
<textarea id="projectSystemPrompt" class="project-panel-textarea"
placeholder="Instructions for all chats in this project..." rows="6"></textarea>
<button class="btn-small btn-primary" id="projectPromptSave" style="margin-top:6px">Save</button>
<span id="projectPromptStatus" class="project-panel-status"></span>
</div>
<div class="project-panel-section">
<div class="project-panel-section-header">
<label class="project-panel-label">Knowledge Bases</label>
<button class="btn-small" id="projectAddKB" title="Add KB">+ Add</button>
</div>
<div id="projectKBList" class="project-panel-list"></div>
</div>
<div class="project-panel-section">
<label class="project-panel-label">Notes</label>
<div id="projectNoteList" class="project-panel-list"></div>
</div>
</div>`;
body.appendChild(el);
// Wire save button
el.querySelector('#projectPromptSave').addEventListener('click', _saveProjectPrompt);
// Wire KB add button
el.querySelector('#projectAddKB').addEventListener('click', _showKBPicker);
PanelRegistry.register('project', {
element: el,
label: 'Project',
onOpen() { if (_projectPanelId) _loadProjectPanel(_projectPanelId); },
});
}
async function openProjectPanel(projectId) {
_projectPanelId = projectId;
const proj = App.projects.find(p => p.id === projectId);
if (typeof PanelRegistry !== 'undefined') {
PanelRegistry.open('project');
// Update label to show project name
const label = document.getElementById('sidePanelLabel');
if (label && proj) label.textContent = proj.name;
}
await _loadProjectPanel(projectId);
}
async function _loadProjectPanel(projectId) {
const proj = App.projects.find(p => p.id === projectId);
if (!proj) return;
// Load full project data from server (includes settings)
try {
const full = await API.getProject(projectId);
const sp = (full.settings && full.settings.system_prompt) || '';
document.getElementById('projectSystemPrompt').value = sp;
document.getElementById('projectPromptStatus').textContent = '';
} catch (_) {
document.getElementById('projectSystemPrompt').value = '';
}
// Load KBs
await _renderProjectKBs(projectId);
// Load notes
await _renderProjectNotes(projectId);
}
async function _saveProjectPrompt() {
if (!_projectPanelId) return;
const sp = document.getElementById('projectSystemPrompt').value;
const status = document.getElementById('projectPromptStatus');
try {
await API.updateProject(_projectPanelId, {
settings: { system_prompt: sp }
});
status.textContent = '✓ Saved';
status.className = 'project-panel-status success';
setTimeout(() => { status.textContent = ''; }, 2000);
} catch (e) {
status.textContent = '✗ Failed';
status.className = 'project-panel-status error';
}
}
async function _renderProjectKBs(projectId) {
const list = document.getElementById('projectKBList');
if (!list) return;
try {
const resp = await API.listProjectKBs(projectId);
const kbs = resp.data || resp || [];
if (kbs.length === 0) {
list.innerHTML = '<div class="project-panel-empty">No KBs bound to this project</div>';
return;
}
list.innerHTML = kbs.map(kb => `
<div class="project-panel-item">
<span class="project-panel-item-name">${esc(kb.name || kb.kb_id)}</span>
<button class="project-panel-item-remove" onclick="_removeProjectKB('${projectId}','${kb.kb_id}')" title="Remove">✕</button>
</div>`).join('');
} catch (_) {
list.innerHTML = '<div class="project-panel-empty">Failed to load KBs</div>';
}
}
async function _removeProjectKB(projectId, kbId) {
try {
await API.removeKBFromProject(projectId, kbId);
await _renderProjectKBs(projectId);
UI.toast('KB removed from project', 'success');
} catch (e) {
UI.toast('Failed to remove KB', 'error');
}
}
async function _showKBPicker() {
if (!_projectPanelId) return;
try {
// Get all KBs user can see
const allKBs = await API.listKnowledgeBases();
const kbList = allKBs.data || allKBs || [];
if (kbList.length === 0) {
UI.toast('No knowledge bases available', 'info');
return;
}
// Get already-bound KBs
const boundResp = await API.listProjectKBs(_projectPanelId);
const boundIds = new Set((boundResp.data || boundResp || []).map(k => k.kb_id));
// Filter to unbound only
const available = kbList.filter(kb => !boundIds.has(kb.id));
if (available.length === 0) {
UI.toast('All KBs already bound to this project', 'info');
return;
}
// Simple picker: use a dropdown menu
const btn = document.getElementById('projectAddKB');
const rect = btn.getBoundingClientRect();
dismissChatContextMenu();
const menu = document.createElement('div');
menu.className = 'project-ctx-menu';
menu.style.left = Math.min(rect.left, window.innerWidth - 200) + 'px';
menu.style.top = (rect.bottom + 4) + 'px';
menu.innerHTML = available.map(kb => `
<button class="ctx-item" onclick="_addProjectKB('${_projectPanelId}','${kb.id}');dismissChatContextMenu()">
${esc(kb.name)}
</button>`).join('');
document.body.appendChild(menu);
_ctxMenu = menu;
setTimeout(() => document.addEventListener('click', dismissChatContextMenu, { once: true }), 0);
} catch (e) {
UI.toast('Failed to load KBs', 'error');
}
}
async function _addProjectKB(projectId, kbId) {
try {
await API.addKBToProject(projectId, kbId, true);
await _renderProjectKBs(projectId);
UI.toast('KB added to project', 'success');
} catch (e) {
UI.toast('Failed to add KB', 'error');
}
}
async function _renderProjectNotes(projectId) {
const list = document.getElementById('projectNoteList');
if (!list) return;
try {
const resp = await API.listProjectNotes(projectId);
const notes = resp.data || resp || [];
if (notes.length === 0) {
list.innerHTML = '<div class="project-panel-empty">No notes in this project</div>';
return;
}
list.innerHTML = notes.map(n => `
<div class="project-panel-item">
<span class="project-panel-item-name">${esc(n.title || n.note_id)}</span>
<button class="project-panel-item-remove" onclick="_removeProjectNote('${projectId}','${n.note_id}')" title="Remove">✕</button>
</div>`).join('');
} catch (_) {
list.innerHTML = '<div class="project-panel-empty">Failed to load notes</div>';
}
}
async function _removeProjectNote(projectId, noteId) {
try {
await API.removeNoteFromProject(projectId, noteId);
await _renderProjectNotes(projectId);
UI.toast('Note removed from project', 'success');
} catch (e) {
UI.toast('Failed to remove note', 'error');
}
}