File size: 5,279 Bytes
0bb4dfa
 
 
 
 
 
 
 
 
 
 
4944128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0bb4dfa
 
 
4944128
 
0bb4dfa
 
 
 
 
1ec996f
 
 
 
 
af5d1df
 
 
 
11bf9b7
 
d4017c8
 
11bf9b7
 
 
 
 
 
 
e661e91
 
 
 
 
 
0bb4dfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1ec996f
 
 
 
af5d1df
 
 
 
11bf9b7
 
 
 
e661e91
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/**
 * 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 });
}