This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/persona-kb.js
2026-03-04 16:06:12 +00:00

178 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// persona-kb.js — PersonaKnowledge 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);
}
}