// 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 = `
Bind KBs to this persona — users will search these KBs automatically when using this persona.
Loading knowledge bases…
`;
// ── 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 = '
No knowledge bases with content available.
';
return;
}
list.innerHTML = _allKBs.map(kb => {
const sel = _selected[kb.id] || { selected: false, auto_search: false };
return `
`;
}).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);
}
}