feat(transport): swap Anthropic → opencode; add persona switcher
Part 1 — Transport swap: - Replace anthropic.AsyncAnthropic streaming with httpx SSE client calling opencode's OpenAI-compat /v1/chat/completions on sin:4096 - Auth: basic auth opencode:$OPENCODE_PASSWORD - Env: OPENCODE_URL (default http://sin:4096), OPENCODE_PASSWORD - Sidecar binding (sin:4098) consulted per message to resolve active persona; voice read from binding → cart → env default - Helper _session_id_for_user: deterministic sha256 slug per email so sidecar binding survives WebSocket reconnects - anthropic dep retained in pyproject.toml (not removed — P4 may use it) Part 2 — Persona switcher: - PERSONAS dict: bt7274, friday, samantha (slug → voice/backend/prompt) - POST /api/persona — bind persona via sidecar, maps slug → full config - GET /api/persona/current — return current binding - GET /api/personas — list available personas - chat.html: persona <select> in topnav with server-rendered active state - chat.js: onChange → fetch /api/persona, update __personaName + status badge + system message in conversation feed TODO: add CSS polish for .topnav__persona-wrap (inherits base styles for now)
This commit is contained in:
@@ -411,6 +411,53 @@ if ($recal) {
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- persona switcher ----------
|
||||
|
||||
const $personaSelect = document.getElementById('persona-select');
|
||||
const $personaStatus = document.getElementById('persona-status');
|
||||
|
||||
if ($personaSelect) {
|
||||
// Set initial selection from server-rendered bound slug
|
||||
if (window.__boundSlug) {
|
||||
$personaSelect.value = window.__boundSlug;
|
||||
}
|
||||
|
||||
$personaSelect.addEventListener('change', async () => {
|
||||
const slug = $personaSelect.value;
|
||||
if (!slug) return; // "— default —" selected
|
||||
|
||||
$personaSelect.disabled = true;
|
||||
if ($personaStatus) $personaStatus.textContent = 'binding…';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/persona', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ slug }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.detail || `HTTP ${resp.status}`);
|
||||
}
|
||||
const data = await resp.json();
|
||||
// Update display name in UI
|
||||
if ($personaStatus) $personaStatus.textContent = data.display || slug;
|
||||
// Update the speaker label for subsequent BT messages
|
||||
if (data.display) window.__personaName = data.display;
|
||||
// Subtle system message in conversation so the switch is visible
|
||||
const body = makeMsg('system');
|
||||
body.textContent = `persona bound → ${data.display || slug}`;
|
||||
} catch (err) {
|
||||
console.warn('persona bind failed:', err.message);
|
||||
if ($personaStatus) $personaStatus.textContent = 'bind failed';
|
||||
// revert select to previous value
|
||||
$personaSelect.value = window.__boundSlug || '';
|
||||
} finally {
|
||||
$personaSelect.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- init ----------
|
||||
|
||||
connect();
|
||||
|
||||
Reference in New Issue
Block a user