178 lines
6.3 KiB
JavaScript
178 lines
6.3 KiB
JavaScript
// persona-kb.js — Persona–Knowledge Base binding UI (v0.17.0)
|
||
//
|
||
// Provides renderPersonaKBPicker(container, opts) which renders a multi-select
|
||
// KB picker with auto_search toggles for use in persona create/edit forms.
|
||
// Pattern: (container, options) → control object with getValues/setValues/clear.
|
||
|
||
'use strict';
|
||
|
||
// ── Persona-KB Picker ───────────────────────────
|
||
|
||
/**
|
||
* Renders a KB picker into a container element.
|
||
*
|
||
* Options:
|
||
* scope - 'admin' | 'team' | 'personal' — controls which KBs are shown
|
||
* teamId - required for team scope
|
||
* prefix - DOM id prefix (default: 'pkb')
|
||
*
|
||
* Returns: { getValues(), setValues(kbs), clear(), refresh() }
|
||
*/
|
||
function renderPersonaKBPicker(containerEl, options = {}) {
|
||
const pfx = options.prefix || 'pkb';
|
||
let _allKBs = []; // available KBs from server
|
||
let _selected = {}; // kb_id → { selected: bool, auto_search: bool }
|
||
|
||
containerEl.innerHTML = `
|
||
<div class="persona-kb-picker" id="${pfx}_picker">
|
||
<label>Knowledge Bases</label>
|
||
<div class="form-hint">Bind KBs to this persona — users will search these KBs automatically when using this persona.</div>
|
||
<div class="persona-kb-list" id="${pfx}_list">
|
||
<div class="empty-hint">Loading knowledge bases…</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// ── Load available KBs ──
|
||
async function loadKBs() {
|
||
try {
|
||
let resp;
|
||
if (options.scope === 'team' && options.teamId) {
|
||
resp = await API._get(`/api/v1/teams/${options.teamId}/knowledge-bases`);
|
||
} else if (options.scope === 'admin') {
|
||
resp = await API._get('/api/v1/knowledge-bases');
|
||
} else {
|
||
// User scope — show discoverable KBs
|
||
resp = await API._get('/api/v1/knowledge-bases-discoverable');
|
||
}
|
||
_allKBs = (resp.data || []).filter(kb => kb.chunk_count > 0);
|
||
} catch (e) {
|
||
_allKBs = [];
|
||
}
|
||
render();
|
||
}
|
||
|
||
// ── Render KB list ──
|
||
function render() {
|
||
const list = document.getElementById(`${pfx}_list`);
|
||
if (!list) return;
|
||
|
||
if (_allKBs.length === 0) {
|
||
list.innerHTML = '<div class="empty-hint">No knowledge bases with content available.</div>';
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = _allKBs.map(kb => {
|
||
const sel = _selected[kb.id] || { selected: false, auto_search: false };
|
||
return `
|
||
<label class="persona-kb-item ${sel.selected ? 'selected' : ''}">
|
||
<input type="checkbox" data-kb-id="${esc(kb.id)}" ${sel.selected ? 'checked' : ''}>
|
||
<span class="kb-name">${esc(kb.name)}</span>
|
||
<span class="kb-meta">${kb.document_count} docs · ${kb.chunk_count} chunks</span>
|
||
<label class="auto-search-toggle ${sel.selected ? '' : 'hidden'}" title="Auto-inject top results into context (no tool call needed)">
|
||
<input type="checkbox" data-kb-auto="${esc(kb.id)}" ${sel.auto_search ? 'checked' : ''}>
|
||
<span>Auto-inject</span>
|
||
</label>
|
||
</label>`;
|
||
}).join('');
|
||
|
||
// Wire checkboxes
|
||
list.querySelectorAll('input[data-kb-id]').forEach(cb => {
|
||
cb.addEventListener('change', function() {
|
||
const kbId = this.dataset.kbId;
|
||
if (!_selected[kbId]) _selected[kbId] = { selected: false, auto_search: false };
|
||
_selected[kbId].selected = this.checked;
|
||
render();
|
||
});
|
||
});
|
||
|
||
list.querySelectorAll('input[data-kb-auto]').forEach(cb => {
|
||
cb.addEventListener('change', function() {
|
||
const kbId = this.dataset.kbAuto;
|
||
if (_selected[kbId]) {
|
||
_selected[kbId].auto_search = this.checked;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Control object ──
|
||
const control = {
|
||
getValues() {
|
||
const kbIds = [];
|
||
const autoSearch = {};
|
||
for (const [kbId, state] of Object.entries(_selected)) {
|
||
if (state.selected) {
|
||
kbIds.push(kbId);
|
||
if (state.auto_search) {
|
||
autoSearch[kbId] = true;
|
||
}
|
||
}
|
||
}
|
||
return { kb_ids: kbIds, auto_search: autoSearch };
|
||
},
|
||
|
||
setValues(personaKBs) {
|
||
_selected = {};
|
||
if (Array.isArray(personaKBs)) {
|
||
for (const pkb of personaKBs) {
|
||
_selected[pkb.kb_id] = {
|
||
selected: true,
|
||
auto_search: !!pkb.auto_search,
|
||
};
|
||
}
|
||
}
|
||
render();
|
||
},
|
||
|
||
clear() {
|
||
_selected = {};
|
||
render();
|
||
},
|
||
|
||
async refresh() {
|
||
await loadKBs();
|
||
}
|
||
};
|
||
|
||
// Auto-load on create
|
||
loadKBs();
|
||
|
||
return control;
|
||
}
|
||
|
||
|
||
// ── Persona-KB Save/Load Helpers ────────────────
|
||
|
||
/**
|
||
* Load persona KB bindings and populate the picker.
|
||
* @param {string} personaId
|
||
* @param {object} kbPicker - control object from renderPersonaKBPicker
|
||
* @param {string} apiPrefix - e.g. '/api/v1/admin' or '/api/v1/teams/:teamId'
|
||
*/
|
||
async function loadPersonaKBs(personaId, kbPicker, apiPrefix) {
|
||
if (!personaId || !kbPicker) return;
|
||
try {
|
||
const resp = await API._get(`${apiPrefix}/personas/${personaId}/knowledge-bases`);
|
||
kbPicker.setValues(resp.data || []);
|
||
} catch (e) {
|
||
console.warn('Failed to load persona KBs:', e.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Save persona KB bindings from the picker.
|
||
* @param {string} personaId
|
||
* @param {object} kbPicker - control object from renderPersonaKBPicker
|
||
* @param {string} apiPrefix - e.g. '/api/v1/admin' or '/api/v1/teams/:teamId'
|
||
*/
|
||
async function savePersonaKBs(personaId, kbPicker, apiPrefix) {
|
||
if (!personaId || !kbPicker) return;
|
||
const values = kbPicker.getValues();
|
||
try {
|
||
await API._put(`${apiPrefix}/personas/${personaId}/knowledge-bases`, values);
|
||
} catch (e) {
|
||
console.warn('Failed to save persona KBs:', e.message);
|
||
}
|
||
}
|