feat(tools): persona selector across standalone tools + dashboard chat

Wire the legal-domain persona picker into corpus, deep-research, korrespond and
the dashboard chat. Each endpoint reads the chosen profile, resolves its packages
against client 57, and scopes retrieval via package_ids (falling back to family
when omitted). New dashboard tenants now subscribe to all DBN domain packages so
persona switching survives the subscription intersection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 23:03:31 +02:00
parent 662fbf7d6d
commit d156f8cf6b
13 changed files with 251 additions and 20 deletions
+45 -1
View File
@@ -77,6 +77,10 @@ require_once __DIR__ . '/includes/layout.php';
<button class="mode-pill" data-lang="uk" type="button">UK</button>
<button class="mode-pill" data-lang="pl" type="button">PL</button>
</div>
<label class="corpus-persona is-hidden" id="corpusPersonaControl" for="corpusPersonaSelect" title="Legal domain — scopes Hybrid search">
<span class="corpus-persona__label">Domain</span>
<select id="corpusPersonaSelect" class="drill-sort-select" aria-label="Legal domain persona"></select>
</label>
</div>
<div class="search-cats" role="group" aria-label="Category filter" id="searchCatPills">
<button class="mode-pill is-active" data-cat="" type="button">All</button>
@@ -743,6 +747,7 @@ require_once __DIR__ . '/includes/layout.php';
let searchMode = 'hybrid';
let searchLang = 'en';
let searchCat = '';
let searchPersona = '';
document.querySelectorAll('.search-modes .mode-pill').forEach(btn => {
btn.addEventListener('click', () => {
@@ -775,6 +780,40 @@ require_once __DIR__ . '/includes/layout.php';
const searchBtn = document.getElementById('corpusSearchBtn');
const searchResults = document.getElementById('corpusSearchResults');
// ── Persona (legal domain) selector — scopes Hybrid search ──────────────────
const personaControl = document.getElementById('corpusPersonaControl');
const personaSelect = document.getElementById('corpusPersonaSelect');
async function loadPersonas() {
if (!personaSelect) return;
try {
const r = await fetch('/api/personas.php', { credentials: 'same-origin', headers: { Accept: 'application/json' } });
const data = await r.json().catch(() => ({}));
if (!r.ok || data.ok !== true || !Array.isArray(data.personas) || !data.personas.length) return;
const fallback = data.default_persona || 'family';
personaSelect.innerHTML = '';
for (const p of data.personas) {
const opt = document.createElement('option');
opt.value = p.slug;
opt.textContent = p.name || p.slug;
personaSelect.appendChild(opt);
}
const hashPersona = new URLSearchParams(location.hash.slice(1)).get('persona');
const saved = hashPersona || sessionStorage.getItem('dbnPersona');
const initial = (saved && data.personas.some(p => p.slug === saved)) ? saved
: (data.personas.some(p => p.slug === fallback) ? fallback : data.personas[0].slug);
searchPersona = initial;
personaSelect.value = initial;
personaSelect.addEventListener('change', () => {
searchPersona = personaSelect.value;
sessionStorage.setItem('dbnPersona', searchPersona);
pushHash();
});
personaControl?.classList.remove('is-hidden');
} catch (_) { /* personas are optional UI sugar; ignore failures */ }
}
loadPersonas();
function runSearch() {
const q = searchInput.value.trim();
if (q.length < 3) {
@@ -792,7 +831,7 @@ require_once __DIR__ . '/includes/layout.php';
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: q, mode: searchMode, language: searchLang, limit: 8, category: searchCat || null }),
body: JSON.stringify({ query: q, mode: searchMode, language: searchLang, limit: 8, category: searchCat || null, profile: searchPersona || null }),
})
.then(r => r.json())
.then(data => {
@@ -872,6 +911,7 @@ require_once __DIR__ . '/includes/layout.php';
if (searchMode !== 'hybrid') p.set('mode', searchMode);
if (searchLang !== 'en') p.set('lang', searchLang);
if (searchCat) p.set('cat', searchCat);
if (searchPersona && searchPersona !== 'family') p.set('persona', searchPersona);
if (drillPanel && !drillPanel.hidden) {
if (drillState.category) p.set('drill', drillState.category);
if (drillState.sourceName) p.set('drillsrc', drillState.sourceName);
@@ -901,6 +941,10 @@ require_once __DIR__ . '/includes/layout.php';
searchCat = p.get('cat');
activatePill('#searchCatPills .mode-pill', 'cat', searchCat);
}
if (p.has('persona')) {
searchPersona = p.get('persona');
if (personaSelect) personaSelect.value = searchPersona;
}
if (p.has('drill')) openDrillByCategory(p.get('drill'));
if (p.has('drillsrc')) openDrillBySource(p.get('drillsrc'));
if (p.has('q') && searchInput) {