/** * Tiny localStorage shim for the CCAI demo. * * Single namespace `ccai-vibe-demo` so we never collide with anything else * the host page is doing. Schema-versioned so we can migrate if/when the * shape changes. */ const NS = 'ccai-vibe-demo'; const SCHEMA_VERSION = 1; /** Demo trio — ids/names mirror backend/app/services/extra_personas.py */ export const DEFAULT_DEMO_PERSONAS = [ { participant_id: 'extra_elena_financial_strategist', name: 'Elena — Financial Strategist', }, { participant_id: 'extra_marcus_technology_strategist', name: 'Marcus — Technology Strategist', }, { participant_id: 'extra_amira_security_advisor', name: 'Dr. Amira — Security & Privacy Advisor', }, ]; /** Default panel for first load / empty saved selection (demo trio). */ export const DEFAULT_PARTICIPANT_IDS = DEFAULT_DEMO_PERSONAS.map( (p) => p.participant_id, ); export const DEFAULT_PARTICIPANTS_ENABLED = Object.fromEntries( DEFAULT_PARTICIPANT_IDS.map((id) => [id, true]), ); /** Use saved selection when non-empty; otherwise the demo default trio. */ export function resolveInitialParticipants(persisted) { const saved = persisted?.participants_selected; if (Array.isArray(saved) && saved.length > 0) { return { selectedIds: saved, enabledMap: persisted.participants_enabled || {}, }; } return { selectedIds: [...DEFAULT_PARTICIPANT_IDS], enabledMap: { ...DEFAULT_PARTICIPANTS_ENABLED }, }; } const DEFAULTS = { schema_version: SCHEMA_VERSION, expert_personas: [], participants_selected: [...DEFAULT_PARTICIPANT_IDS], participants_enabled: { ...DEFAULT_PARTICIPANTS_ENABLED }, model_assignments: {}, orchestrator_model_id: null, summarizer_model_id: null, max_participants: 5, theme: null, // Sparse map of user overrides to ConversationLimits fields. Empty // map = "use server defaults". The server clamps anything we send, // so storing whatever the user typed (including stale values from // an earlier session) is safe. conversation_limits: {}, // When true, the participants dropdown shows "Select N Automatically" // as the active mode and the per-persona checkboxes are hidden. // The auto-select happens just before /chat/start. auto_select_mode: false, // In-the-loop human participant. null when no human is configured. // Shape when set: // { participant_id, name, profile_text, // credential_summary: { // name, expertise, personality, // credibility_for_question, bias_to_watch // } } // Persisted across page reloads so the user doesn't have to re-author // their summary every session. Cleared from storage when the user // removes the human via the sidebar. human_participant: null, // Conversation-format plugin choices (see backend // /api/chat/conversation-formats). null means "use server default" // — the backend returns `default_structure_id` / `default_decision_id` // alongside the catalog so the UI can highlight them. conversation_structure_id: null, decision_method_id: null, }; function readAll() { try { const raw = window.localStorage.getItem(NS); if (!raw) return { ...DEFAULTS }; const parsed = JSON.parse(raw); if (parsed.schema_version !== SCHEMA_VERSION) { // Future-proofing: migrate or wipe. For v1 we just merge with defaults. return { ...DEFAULTS, ...parsed, schema_version: SCHEMA_VERSION }; } return { ...DEFAULTS, ...parsed }; } catch (err) { console.warn('localStorage read failed:', err); return { ...DEFAULTS }; } } function writeAll(state) { try { window.localStorage.setItem(NS, JSON.stringify(state)); } catch (err) { console.warn('localStorage write failed:', err); } } export function loadState() { return readAll(); } export function patchState(patch) { const current = readAll(); const next = { ...current, ...patch }; writeAll(next); return next; } export function setExpertPersonas(list) { return patchState({ expert_personas: list }); } export function setParticipantsSelected(ids) { return patchState({ participants_selected: ids }); } export function setParticipantsEnabled(map) { return patchState({ participants_enabled: map }); } export function setModelAssignments(map) { return patchState({ model_assignments: map }); } export function setOrchestratorModelId(modelId) { return patchState({ orchestrator_model_id: modelId }); } export function setSummarizerModelId(modelId) { return patchState({ summarizer_model_id: modelId }); } export function setMaxParticipants(n) { return patchState({ max_participants: n }); } export function setTheme(theme) { return patchState({ theme }); } export function setConversationLimits(limitsMap) { return patchState({ conversation_limits: limitsMap || {} }); } export function setAutoSelectMode(on) { return patchState({ auto_select_mode: !!on }); } export function setHumanParticipant(humanOrNull) { return patchState({ human_participant: humanOrNull || null }); } export function setConversationStructureId(id) { return patchState({ conversation_structure_id: id || null }); } export function setDecisionMethodId(id) { return patchState({ decision_method_id: id || null }); }