Changeset 0.19.2 (#84)
This commit is contained in:
@@ -157,6 +157,9 @@ async function moveChatToProject(chatId, projectId) {
|
||||
try {
|
||||
await API.removeChannelFromProject(oldProjectId, chatId);
|
||||
chat.projectId = null;
|
||||
// Remove from order tracking (v0.19.2)
|
||||
const order = getProjectChannelOrder(oldProjectId);
|
||||
setProjectChannelOrder(oldProjectId, order.filter(id => id !== chatId));
|
||||
UI.renderChatList();
|
||||
} catch (e) {
|
||||
UI.toast('Failed to remove from project', 'error');
|
||||
@@ -165,6 +168,15 @@ async function moveChatToProject(chatId, projectId) {
|
||||
try {
|
||||
await API.addChannelToProject(projectId, chatId, 0);
|
||||
chat.projectId = projectId;
|
||||
// Add to order tracking (v0.19.2)
|
||||
const order = getProjectChannelOrder(projectId);
|
||||
if (!order.includes(chatId)) order.push(chatId);
|
||||
setProjectChannelOrder(projectId, order);
|
||||
// Remove from old project order
|
||||
if (oldProjectId) {
|
||||
const oldOrder = getProjectChannelOrder(oldProjectId);
|
||||
setProjectChannelOrder(oldProjectId, oldOrder.filter(id => id !== chatId));
|
||||
}
|
||||
UI.renderChatList();
|
||||
} catch (e) {
|
||||
UI.toast('Failed to move to project', 'error');
|
||||
@@ -190,6 +202,21 @@ function showChatContextMenu(e, chatId) {
|
||||
// Move to project submenu
|
||||
let items = '';
|
||||
if (chat.projectId) {
|
||||
// Reorder within project (v0.19.2)
|
||||
const order = getProjectChannelOrder(chat.projectId);
|
||||
const idx = order.indexOf(chatId);
|
||||
if (idx > 0) {
|
||||
items += `<button class="ctx-item" onclick="_moveChatInProject('${chat.projectId}','${chatId}',-1);dismissChatContextMenu()">
|
||||
<span class="ctx-icon">↑</span> Move up
|
||||
</button>`;
|
||||
}
|
||||
if (idx >= 0 && idx < order.length - 1) {
|
||||
items += `<button class="ctx-item" onclick="_moveChatInProject('${chat.projectId}','${chatId}',1);dismissChatContextMenu()">
|
||||
<span class="ctx-icon">↓</span> Move down
|
||||
</button>`;
|
||||
}
|
||||
if (idx >= 0) items += '<div class="ctx-divider"></div>';
|
||||
|
||||
items += `<button class="ctx-item" onclick="moveChatToProject('${chatId}', null);dismissChatContextMenu()">
|
||||
<span class="ctx-icon">↩</span> Remove from project
|
||||
</button>`;
|
||||
@@ -347,6 +374,13 @@ function _registerProjectPanel() {
|
||||
<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">
|
||||
<label class="project-panel-label">Default Persona</label>
|
||||
<select id="projectPersonaSelect" class="project-panel-select">
|
||||
<option value="">None (use model selector)</option>
|
||||
</select>
|
||||
<div class="project-panel-hint">New chats in this project inherit the persona's model, parameters, and system prompt.</div>
|
||||
</div>
|
||||
<div class="project-panel-section">
|
||||
<div class="project-panel-section-header">
|
||||
<label class="project-panel-label">Knowledge Bases</label>
|
||||
@@ -358,6 +392,12 @@ function _registerProjectPanel() {
|
||||
<label class="project-panel-label">Notes</label>
|
||||
<div id="projectNoteList" class="project-panel-list"></div>
|
||||
</div>
|
||||
<div class="project-panel-section project-panel-footer">
|
||||
<label class="project-panel-checkbox">
|
||||
<input type="checkbox" id="projectArchiveToggle">
|
||||
<span>Archive this project</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>`;
|
||||
body.appendChild(el);
|
||||
|
||||
@@ -367,6 +407,12 @@ function _registerProjectPanel() {
|
||||
// Wire KB add button
|
||||
el.querySelector('#projectAddKB').addEventListener('click', _showKBPicker);
|
||||
|
||||
// Wire persona select (v0.19.2)
|
||||
el.querySelector('#projectPersonaSelect').addEventListener('change', _saveProjectPersona);
|
||||
|
||||
// Wire archive toggle (v0.19.2)
|
||||
el.querySelector('#projectArchiveToggle').addEventListener('change', _toggleProjectArchive);
|
||||
|
||||
PanelRegistry.register('project', {
|
||||
element: el,
|
||||
label: 'Project',
|
||||
@@ -391,15 +437,21 @@ async function _loadProjectPanel(projectId) {
|
||||
if (!proj) return;
|
||||
|
||||
// Load full project data from server (includes settings)
|
||||
let fullSettings = {};
|
||||
try {
|
||||
const full = await API.getProject(projectId);
|
||||
const sp = (full.settings && full.settings.system_prompt) || '';
|
||||
document.getElementById('projectSystemPrompt').value = sp;
|
||||
fullSettings = full.settings || {};
|
||||
document.getElementById('projectSystemPrompt').value = fullSettings.system_prompt || '';
|
||||
document.getElementById('projectPromptStatus').textContent = '';
|
||||
// Archive toggle
|
||||
document.getElementById('projectArchiveToggle').checked = !!full.is_archived;
|
||||
} catch (_) {
|
||||
document.getElementById('projectSystemPrompt').value = '';
|
||||
}
|
||||
|
||||
// Populate persona picker (v0.19.2)
|
||||
await _renderPersonaPicker(fullSettings.persona_id || '');
|
||||
|
||||
// Load KBs
|
||||
await _renderProjectKBs(projectId);
|
||||
|
||||
@@ -537,3 +589,143 @@ async function _removeProjectNote(projectId, noteId) {
|
||||
UI.toast('Failed to remove note', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// PERSONA PICKER (v0.19.2)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
async function _renderPersonaPicker(currentPersonaId) {
|
||||
const select = document.getElementById('projectPersonaSelect');
|
||||
if (!select) return;
|
||||
select.innerHTML = '<option value="">None (use model selector)</option>';
|
||||
try {
|
||||
const resp = await API.listPresets();
|
||||
const personas = resp.data || resp || [];
|
||||
personas.forEach(p => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.name + (p.base_model_id ? ` (${p.base_model_id})` : '');
|
||||
if (p.id === currentPersonaId) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
} catch (_) { /* best effort */ }
|
||||
}
|
||||
|
||||
async function _saveProjectPersona() {
|
||||
if (!_projectPanelId) return;
|
||||
const personaId = document.getElementById('projectPersonaSelect').value;
|
||||
try {
|
||||
await API.updateProject(_projectPanelId, {
|
||||
settings: { persona_id: personaId || null }
|
||||
});
|
||||
UI.toast(personaId ? 'Persona set' : 'Persona cleared', 'success');
|
||||
} catch (e) {
|
||||
UI.toast('Failed to update persona', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// ARCHIVE TOGGLE (v0.19.2)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
async function _toggleProjectArchive() {
|
||||
if (!_projectPanelId) return;
|
||||
const checked = document.getElementById('projectArchiveToggle').checked;
|
||||
try {
|
||||
await API.updateProject(_projectPanelId, { is_archived: checked });
|
||||
const proj = App.projects.find(p => p.id === _projectPanelId);
|
||||
if (proj) proj.isArchived = checked;
|
||||
UI.renderChatList();
|
||||
UI.toast(checked ? 'Project archived' : 'Project unarchived', 'success');
|
||||
} catch (e) {
|
||||
UI.toast('Failed to update archive status', 'error');
|
||||
document.getElementById('projectArchiveToggle').checked = !checked;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// CHANNEL REORDER WITHIN PROJECT (v0.19.2)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
// Stores project channel order: projectId → [channelId, ...]
|
||||
const _projectChannelOrder = {};
|
||||
|
||||
function getProjectChannelOrder(projectId) {
|
||||
return _projectChannelOrder[projectId] || [];
|
||||
}
|
||||
|
||||
function setProjectChannelOrder(projectId, channelIds) {
|
||||
_projectChannelOrder[projectId] = channelIds;
|
||||
}
|
||||
|
||||
// Called after loadProjects + loadChats to initialize order from server positions
|
||||
async function loadProjectChannelPositions() {
|
||||
for (const proj of App.projects) {
|
||||
try {
|
||||
const resp = await API.listProjectChannels(proj.id);
|
||||
const channels = resp.data || resp || [];
|
||||
// Sorted by position from server
|
||||
setProjectChannelOrder(proj.id, channels.map(c => c.channel_id));
|
||||
} catch (_) { /* best effort — fall back to no order */ }
|
||||
}
|
||||
}
|
||||
|
||||
function _getReorderedProjectChats(projectId, chats) {
|
||||
const order = getProjectChannelOrder(projectId);
|
||||
const projChats = chats.filter(c => c.projectId === projectId);
|
||||
if (order.length === 0) return projChats;
|
||||
|
||||
// Sort by position order; unordered chats go to end
|
||||
const posMap = new Map(order.map((id, i) => [id, i]));
|
||||
return projChats.sort((a, b) => {
|
||||
const pa = posMap.has(a.id) ? posMap.get(a.id) : 9999;
|
||||
const pb = posMap.has(b.id) ? posMap.get(b.id) : 9999;
|
||||
return pa - pb;
|
||||
});
|
||||
}
|
||||
|
||||
async function _reorderOnDrop(e, projectId) {
|
||||
// Get all chat items in this project group from the DOM
|
||||
const group = e.currentTarget;
|
||||
const chatItems = group.querySelectorAll('.chat-item');
|
||||
const newOrder = [];
|
||||
chatItems.forEach(el => {
|
||||
const onclick = el.getAttribute('onclick') || '';
|
||||
const m = onclick.match(/selectChat\('([^']+)'\)/);
|
||||
if (m) newOrder.push(m[1]);
|
||||
});
|
||||
|
||||
if (newOrder.length === 0) return;
|
||||
|
||||
// Optimistic update
|
||||
setProjectChannelOrder(projectId, newOrder);
|
||||
|
||||
try {
|
||||
await API.reorderProjectChannels(projectId, newOrder);
|
||||
} catch (e) {
|
||||
UI.toast('Failed to save order', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function _moveChatInProject(projectId, chatId, direction) {
|
||||
const order = getProjectChannelOrder(projectId);
|
||||
const idx = order.indexOf(chatId);
|
||||
if (idx < 0) return;
|
||||
const newIdx = idx + direction;
|
||||
if (newIdx < 0 || newIdx >= order.length) return;
|
||||
|
||||
// Swap
|
||||
[order[idx], order[newIdx]] = [order[newIdx], order[idx]];
|
||||
setProjectChannelOrder(projectId, order);
|
||||
UI.renderChatList();
|
||||
|
||||
try {
|
||||
await API.reorderProjectChannels(projectId, order);
|
||||
} catch (e) {
|
||||
// Revert on failure
|
||||
[order[idx], order[newIdx]] = [order[newIdx], order[idx]];
|
||||
setProjectChannelOrder(projectId, order);
|
||||
UI.renderChatList();
|
||||
UI.toast('Failed to reorder', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user