Changeset 0.19.1 (#83)
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user