extonlawrence commited on
Commit
693b70b
·
1 Parent(s): c8ca331

Refactor persona detail page with lock valid and archived support

Browse files
src/routes/settings/(nav)/personas/[...persona]/+page.svelte CHANGED
@@ -3,73 +3,45 @@
3
  import { goto } from "$app/navigation";
4
  import { page } from "$app/state";
5
  import { useSettingsStore } from "$lib/stores/settings";
 
 
6
  import CarbonTrashCan from "~icons/carbon/trash-can";
7
- import CarbonEdit from "~icons/carbon/edit";
 
8
 
9
  const settings = useSettingsStore();
10
 
11
  // Selected persona comes from the URL param to mirror model routing
12
- let selectedPersonaId = $derived(page.params.persona ?? $settings.personas[0]?.id ?? null);
 
 
 
 
 
 
 
 
 
13
  let selectedPersona = $derived(
14
- $settings.personas.find((p) => p.id === selectedPersonaId) ?? $settings.personas[0]
15
- );
16
-
17
- // Local editable copy of selected persona
18
- let editableName = $state("");
19
- let editableAge = $state("");
20
- let editableGender = $state("");
21
- let editableJobSector = $state("");
22
- let editableStance = $state("");
23
- let editableCommunicationStyle = $state("");
24
- let editableGoalInDebate = $state("");
25
- let editableIncomeBracket = $state("");
26
- let editablePoliticalLeanings = $state("");
27
- let editableGeographicContext = $state("");
28
 
29
- // Update editable fields when selection changes
30
- $effect(() => {
31
- if (selectedPersona) {
32
- editableName = selectedPersona.name;
33
- editableAge = selectedPersona.age;
34
- editableGender = selectedPersona.gender;
35
- editableJobSector = selectedPersona.jobSector || "";
36
- editableStance = selectedPersona.stance || "";
37
- editableCommunicationStyle = selectedPersona.communicationStyle || "";
38
- editableGoalInDebate = selectedPersona.goalInDebate || "";
39
- editableIncomeBracket = selectedPersona.incomeBracket || "";
40
- editablePoliticalLeanings = selectedPersona.politicalLeanings || "";
41
- editableGeographicContext = selectedPersona.geographicContext || "";
42
- }
43
- });
44
-
45
- function savePersona() {
46
- if (!selectedPersona) return;
47
 
48
- const updatedPersonas = $settings.personas.map((p) =>
49
- p.id === selectedPersona.id
50
- ? {
51
- ...p,
52
- name: editableName,
53
- age: editableAge,
54
- gender: editableGender,
55
- jobSector: editableJobSector,
56
- stance: editableStance,
57
- communicationStyle: editableCommunicationStyle,
58
- goalInDebate: editableGoalInDebate,
59
- incomeBracket: editableIncomeBracket,
60
- politicalLeanings: editablePoliticalLeanings,
61
- geographicContext: editableGeographicContext,
62
- updatedAt: new Date(),
63
- }
64
- : p
65
- );
66
 
67
- $settings.personas = updatedPersonas;
68
- }
69
 
70
  function togglePersona() {
71
  if (!selectedPersona) return;
72
- if (hasChanges) savePersona();
73
 
74
  const isActive = $settings.activePersonas.includes(selectedPersona.id);
75
  if (isActive) {
@@ -89,8 +61,14 @@
89
  function deletePersona() {
90
  if (!selectedPersona) return;
91
 
92
- // Can't delete if it's the only persona
93
- if ($settings.personas.length === 1) {
 
 
 
 
 
 
94
  alert("Cannot delete the last persona.");
95
  return;
96
  }
@@ -101,335 +79,233 @@
101
  return;
102
  }
103
 
104
- if (confirm(`Are you sure you want to delete "${selectedPersona.name}"?`)) {
105
- const nextId = $settings.personas.find((p) => p.id !== selectedPersona!.id)?.id || null;
106
- $settings.personas = $settings.personas.filter((p) => p.id !== selectedPersona.id);
107
- goto(`${base}/settings/personas/${nextId ?? ""}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
  }
110
 
111
- let hasChanges = $derived(
112
- selectedPersona &&
113
- (editableName !== selectedPersona.name ||
114
- editableAge !== selectedPersona.age ||
115
- editableGender !== selectedPersona.gender ||
116
- editableJobSector !== (selectedPersona.jobSector || "") ||
117
- editableStance !== (selectedPersona.stance || "") ||
118
- editableCommunicationStyle !== (selectedPersona.communicationStyle || "") ||
119
- editableGoalInDebate !== (selectedPersona.goalInDebate || "") ||
120
- editableIncomeBracket !== (selectedPersona.incomeBracket || "") ||
121
- editablePoliticalLeanings !== (selectedPersona.politicalLeanings || "") ||
122
- editableGeographicContext !== (selectedPersona.geographicContext || ""))
123
- );
124
 
125
- // Function to show datalist on focus
126
- function showDatalist(event: FocusEvent) {
127
- const input = event.target as HTMLInputElement;
128
- // Dispatch a synthetic input event to trigger the datalist dropdown
129
- input.dispatchEvent(new Event('input', { bubbles: true }));
130
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  </script>
132
 
133
  <!-- Persona Detail View -->
134
  {#if selectedPersona}
135
- <div class="flex h-full w-full flex-col overflow-hidden">
136
- <div class="flex flex-col gap-6">
137
- <!-- Group 1: Core Identity -->
138
- <div>
139
- <h3 class="mb-3 text-sm font-semibold text-gray-800 dark:text-gray-200">Core Identity</h3>
140
- <div class="grid grid-cols-1 gap-4 md:grid-cols-3 md:gap-6">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  <div class="flex flex-col gap-2">
142
  <label for="persona-name" class="text-sm font-medium text-gray-700 dark:text-gray-300">
143
  Name <span class="text-red-500">*</span>
144
  </label>
145
- <div class="relative">
146
  <input
147
  id="persona-name"
148
  type="text"
149
- bind:value={editableName}
150
  required
151
- class="peer w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 pr-9 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
 
 
152
  maxlength="100"
153
  />
154
- <CarbonEdit class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 opacity-50 transition-opacity peer-focus:opacity-0 dark:text-gray-500" />
155
- </div>
156
  </div>
157
 
158
- <div class="flex flex-col gap-2">
159
- <label for="age" class="text-sm font-medium text-gray-700 dark:text-gray-300">
160
- Age <span class="text-red-500">*</span>
161
- </label>
162
- <div class="relative">
163
- <input
164
- id="age"
165
- type="text"
166
- list="age-options"
167
- bind:value={editableAge}
168
- onfocus={showDatalist}
169
- required
170
- class="peer w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 pr-9 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
171
- maxlength="50"
172
- />
173
- <datalist id="age-options">
174
- <option value="18-25">18-25</option>
175
- <option value="26-35">26-35</option>
176
- <option value="36-45">36-45</option>
177
- <option value="46-55">46-55</option>
178
- <option value="56-65">56-65</option>
179
- <option value="66+">66+</option>
180
- </datalist>
181
- <CarbonEdit class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 opacity-50 transition-opacity peer-focus:opacity-0 dark:text-gray-500" />
182
- </div>
183
  </div>
184
 
185
- <div class="flex flex-col gap-2">
186
- <label for="gender" class="text-sm font-medium text-gray-700 dark:text-gray-300">
187
- Gender <span class="text-red-500">*</span>
188
- </label>
189
- <div class="relative">
190
- <input
191
- id="gender"
192
- type="text"
193
- list="gender-options"
194
- bind:value={editableGender}
195
- onfocus={showDatalist}
196
- required
197
- class="peer w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 pr-9 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
198
- maxlength="50"
199
- />
200
- <datalist id="gender-options">
201
- <option value="Male">Male</option>
202
- <option value="Female">Female</option>
203
- <option value="Prefer not to say">Prefer not to say</option>
204
- </datalist>
205
- <CarbonEdit class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 opacity-50 transition-opacity peer-focus:opacity-0 dark:text-gray-500" />
206
- </div>
207
- </div>
208
  </div>
209
  </div>
210
 
211
- <!-- Group 2: Professional & Stance -->
212
- <div>
213
- <h3 class="mb-3 text-sm font-semibold text-gray-800 dark:text-gray-200">Professional & Stance</h3>
214
- <div class="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6">
215
- <div class="flex flex-col gap-2">
216
- <label for="job-sector" class="text-sm font-medium text-gray-700 dark:text-gray-300">
217
- Job Sector
218
- </label>
219
- <div class="relative">
220
- <input
221
- id="job-sector"
222
- type="text"
223
- list="job-sector-options"
224
- bind:value={editableJobSector}
225
- onfocus={showDatalist}
226
- class="peer w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 pr-9 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
227
- maxlength="200"
228
- />
229
- <datalist id="job-sector-options">
230
- <option value="Healthcare provider">Healthcare provider</option>
231
- <option value="Small business owner">Small business owner</option>
232
- <option value="Tech worker">Tech worker</option>
233
- <option value="Teacher">Teacher</option>
234
- <option value="Unemployed/Retired">Unemployed/Retired</option>
235
- <option value="Government worker">Government worker</option>
236
- <option value="Student">Student</option>
237
- </datalist>
238
- <CarbonEdit class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 opacity-50 transition-opacity peer-focus:opacity-0 dark:text-gray-500" />
239
- </div>
240
- </div>
241
 
242
- <div class="flex flex-col gap-2">
243
- <label for="stance" class="text-sm font-medium text-gray-700 dark:text-gray-300">
244
- Stance
245
- </label>
246
- <div class="relative">
247
- <input
248
- id="stance"
249
- type="text"
250
- list="stance-options"
251
- bind:value={editableStance}
252
- onfocus={showDatalist}
253
- class="peer w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 pr-9 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
254
- maxlength="200"
255
- />
256
- <datalist id="stance-options">
257
- <option value="In Favor of Medicare for All">In Favor of Medicare for All</option>
258
- <option value="Hardline Insurance Advocate">Hardline Insurance Advocate</option>
259
- <option value="Improvement of Current System">Improvement of Current System</option>
260
- <option value="Public Option Supporter">Public Option Supporter</option>
261
- <option value="Status Quo">Status Quo</option>
262
- </datalist>
263
- <CarbonEdit class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 opacity-50 transition-opacity peer-focus:opacity-0 dark:text-gray-500" />
264
- </div>
265
- </div>
266
- </div>
267
  </div>
268
 
269
- <!-- Group 3: Communication & Goals -->
270
- <div>
271
- <h3 class="mb-3 text-sm font-semibold text-gray-800 dark:text-gray-200">Communication & Goals</h3>
272
- <div class="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6">
273
- <div class="flex flex-col gap-2">
274
- <label for="communication-style" class="text-sm font-medium text-gray-700 dark:text-gray-300">
275
- Communication Style
276
- </label>
277
- <div class="relative">
278
- <input
279
- id="communication-style"
280
- type="text"
281
- list="communication-style-options"
282
- bind:value={editableCommunicationStyle}
283
- onfocus={showDatalist}
284
- class="peer w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 pr-9 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
285
- maxlength="200"
286
- />
287
- <datalist id="communication-style-options">
288
- <option value="Direct">Direct</option>
289
- <option value="Technical/Jargon use">Technical/Jargon use</option>
290
- <option value="Informal">Informal</option>
291
- <option value="Philosophical">Philosophical</option>
292
- <option value="Pragmatic">Pragmatic</option>
293
- <option value="Conversational">Conversational</option>
294
- </datalist>
295
- <CarbonEdit class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 opacity-50 transition-opacity peer-focus:opacity-0 dark:text-gray-500" />
296
- </div>
297
- </div>
298
 
299
- <div class="flex flex-col gap-2">
300
- <label for="goal-in-debate" class="text-sm font-medium text-gray-700 dark:text-gray-300">
301
- Goal in the Debate
302
- </label>
303
- <div class="relative">
304
- <input
305
- id="goal-in-debate"
306
- type="text"
307
- list="goal-in-debate-options"
308
- bind:value={editableGoalInDebate}
309
- onfocus={showDatalist}
310
- class="peer w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 pr-9 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
311
- maxlength="300"
312
- />
313
- <datalist id="goal-in-debate-options">
314
- <option value="Keep discussion grounded">Keep discussion grounded</option>
315
- <option value="Explain complexity">Explain complexity</option>
316
- <option value="Advocate for change">Advocate for change</option>
317
- <option value="Defend current system">Defend current system</option>
318
- <option value="Find compromise">Find compromise</option>
319
- </datalist>
320
- <CarbonEdit class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 opacity-50 transition-opacity peer-focus:opacity-0 dark:text-gray-500" />
321
- </div>
322
- </div>
323
- </div>
324
  </div>
325
 
326
- <!-- Group 4: Demographics -->
327
- <div>
328
- <h3 class="mb-3 text-sm font-semibold text-gray-800 dark:text-gray-200">Demographics</h3>
329
- <div class="grid grid-cols-1 gap-4 md:grid-cols-3 md:gap-6">
330
- <div class="flex flex-col gap-2">
331
- <label for="income-bracket" class="text-sm font-medium text-gray-700 dark:text-gray-300">
332
- Income Bracket
333
- </label>
334
- <div class="relative">
335
- <input
336
- id="income-bracket"
337
- type="text"
338
- list="income-bracket-options"
339
- bind:value={editableIncomeBracket}
340
- onfocus={showDatalist}
341
- class="peer w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 pr-9 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
342
- maxlength="100"
343
- />
344
- <datalist id="income-bracket-options">
345
- <option value="Low">Low</option>
346
- <option value="Middle">Middle</option>
347
- <option value="High">High</option>
348
- <option value="Comfortable">Comfortable</option>
349
- <option value="Struggling">Struggling</option>
350
- </datalist>
351
- <CarbonEdit class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 opacity-50 transition-opacity peer-focus:opacity-0 dark:text-gray-500" />
352
- </div>
353
- </div>
354
 
355
- <div class="flex flex-col gap-2">
356
- <label for="political-leanings" class="text-sm font-medium text-gray-700 dark:text-gray-300">
357
- Political Leanings
358
- </label>
359
- <div class="relative">
360
- <input
361
- id="political-leanings"
362
- type="text"
363
- list="political-leanings-options"
364
- bind:value={editablePoliticalLeanings}
365
- onfocus={showDatalist}
366
- class="peer w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 pr-9 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
367
- maxlength="100"
368
- />
369
- <datalist id="political-leanings-options">
370
- <option value="Liberal">Liberal</option>
371
- <option value="Conservative">Conservative</option>
372
- <option value="Moderate">Moderate</option>
373
- <option value="Libertarian">Libertarian</option>
374
- <option value="Non-affiliated">Non-affiliated</option>
375
- <option value="Progressive">Progressive</option>
376
- </datalist>
377
- <CarbonEdit class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 opacity-50 transition-opacity peer-focus:opacity-0 dark:text-gray-500" />
378
- </div>
379
- </div>
380
 
381
- <div class="flex flex-col gap-2">
382
- <label for="geographic-context" class="text-sm font-medium text-gray-700 dark:text-gray-300">
383
- Geographic Context
384
- </label>
385
- <div class="relative">
386
- <input
387
- id="geographic-context"
388
- type="text"
389
- list="geographic-context-options"
390
- bind:value={editableGeographicContext}
391
- onfocus={showDatalist}
392
- class="peer w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 pr-9 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
393
- maxlength="100"
394
- />
395
- <datalist id="geographic-context-options">
396
- <option value="Rural">Rural</option>
397
- <option value="Urban">Urban</option>
398
- <option value="Suburban">Suburban</option>
399
- </datalist>
400
- <CarbonEdit class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 opacity-50 transition-opacity peer-focus:opacity-0 dark:text-gray-500" />
401
- </div>
402
- </div>
403
  </div>
404
- </div>
405
- </div>
406
-
407
- <!-- Sticky buttons -->
408
- <div
409
- class="sticky bottom-0 flex flex-wrap gap-2 py-4"
410
- >
411
- <button
412
- class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
413
- onclick={togglePersona}
414
- >
415
- {$settings.activePersonas.includes(selectedPersona.id) ? "Deactivate" : "Activate"}
416
- </button>
417
- <button
418
- class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-50 disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
419
- onclick={savePersona}
420
- disabled={!hasChanges}
421
- >
422
- Save
423
- </button>
424
- <button
425
- class="ml-auto flex items-center gap-2 rounded-lg border border-red-300 bg-white px-4 py-2 text-sm font-semibold text-red-600 hover:bg-red-50 dark:border-red-700 dark:bg-gray-800 dark:hover:bg-red-900/20"
426
- onclick={deletePersona}
427
- >
428
- <CarbonTrashCan />
429
- Delete
430
- </button>
431
  </div>
432
  </div>
433
  {/if}
434
-
435
-
 
3
  import { goto } from "$app/navigation";
4
  import { page } from "$app/state";
5
  import { useSettingsStore } from "$lib/stores/settings";
6
+ import SelectField from "$lib/components/SelectField.svelte";
7
+ import ComboboxField from "$lib/components/ComboboxField.svelte";
8
  import CarbonTrashCan from "~icons/carbon/trash-can";
9
+ import type { Persona } from "$lib/types/Persona";
10
+ import { debounce } from "$lib/utils/debounce";
11
 
12
  const settings = useSettingsStore();
13
 
14
  // Selected persona comes from the URL param to mirror model routing
15
+ const availablePersonas = $derived($settings.personas.filter((persona) => !persona.archived));
16
+ let selectedPersonaId = $derived.by(() => {
17
+ const paramId = page.params.persona;
18
+ // If URL param exists and persona exists (even if archived), use it
19
+ if (paramId && $settings.personas.some((persona) => persona.id === paramId)) {
20
+ return paramId;
21
+ }
22
+ // Otherwise use first available persona
23
+ return availablePersonas[0]?.id ?? null;
24
+ });
25
  let selectedPersona = $derived(
26
+ $settings.personas.find((persona) => persona.id === selectedPersonaId) ?? null
27
+ );
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ // Core update function - shared by both debounced and immediate updates
30
+ function updatePersona(field: keyof Persona, value: string) {
31
+ if (!selectedPersona || selectedPersona.locked) return;
32
+ $settings.personas = $settings.personas.map((p) =>
33
+ p.id === selectedPersona.id ? { ...p, [field]: value, updatedAt: new Date() } : p
34
+ );
35
+ }
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ // Debounced update for text inputs (triggered on blur)
38
+ const updatePersonaField = debounce(updatePersona, 300);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ // Immediate update for selects/dropdowns
41
+ const updatePersonaImmediate = updatePersona;
42
 
43
  function togglePersona() {
44
  if (!selectedPersona) return;
 
45
 
46
  const isActive = $settings.activePersonas.includes(selectedPersona.id);
47
  if (isActive) {
 
61
  function deletePersona() {
62
  if (!selectedPersona) return;
63
 
64
+ // Can't delete if it's locked
65
+ if (selectedPersona.locked) {
66
+ alert("Cannot delete a locked persona.");
67
+ return;
68
+ }
69
+
70
+ const nonArchivedCount = $settings.personas.filter((persona) => !persona.archived).length;
71
+ if (nonArchivedCount <= 1) {
72
  alert("Cannot delete the last persona.");
73
  return;
74
  }
 
79
  return;
80
  }
81
 
82
+ if (
83
+ confirm(
84
+ `Are you sure you want to remove "${selectedPersona.name}"? Past responses will continue to show their persona details, but this persona will be hidden from future use.`
85
+ )
86
+ ) {
87
+ const remainingPersona = $settings.personas.find(
88
+ (persona) => !persona.archived && persona.id !== selectedPersona.id
89
+ );
90
+
91
+ $settings.personas = $settings.personas.map((persona) =>
92
+ persona.id === selectedPersona.id
93
+ ? { ...persona, archived: true, updatedAt: new Date() }
94
+ : persona
95
+ );
96
+
97
+ const filteredActive = $settings.activePersonas.filter((id) => id !== selectedPersona.id);
98
+ if (filteredActive.length !== $settings.activePersonas.length) {
99
+ void settings.instantSet({ activePersonas: filteredActive });
100
+ }
101
+
102
+ const targetId = remainingPersona?.id ?? $settings.personas.find((p) => !p.archived)?.id ?? "";
103
+ goto(`${base}/settings/personas/${targetId}`);
104
  }
105
  }
106
 
107
+ // Options for SelectFields
108
+ const ageOptions = ["18-25", "26-35", "36-45", "46-55", "56-65", "66+"];
109
+ const genderOptions = ["Male", "Female", "Prefer not to say"];
110
+ const incomeBracketOptions = ["Low", "Middle", "High", "Comfortable", "Struggling"];
111
+ const politicalLeaningsOptions = ["Liberal", "Conservative", "Moderate", "Libertarian", "Non-affiliated", "Progressive"];
112
+ const geographicContextOptions = ["Rural", "Urban", "Suburban"];
 
 
 
 
 
 
 
113
 
114
+ // Suggestions for ComboboxFields
115
+ const jobSectorSuggestions = [
116
+ "Healthcare provider",
117
+ "Small business owner",
118
+ "Tech worker",
119
+ "Teacher",
120
+ "Unemployed/Retired",
121
+ "Government worker",
122
+ "Student"
123
+ ];
124
+ const stanceSuggestions = [
125
+ "In Favor of Medicare for All",
126
+ "Hardline Insurance Advocate",
127
+ "Improvement of Current System",
128
+ "Public Option Supporter",
129
+ "Status Quo"
130
+ ];
131
+ const communicationStyleSuggestions = [
132
+ "Direct",
133
+ "Technical/Jargon use",
134
+ "Informal",
135
+ "Philosophical",
136
+ "Pragmatic",
137
+ "Conversational"
138
+ ];
139
+ const goalInDebateSuggestions = [
140
+ "Keep discussion grounded",
141
+ "Explain complexity",
142
+ "Advocate for change",
143
+ "Defend current system",
144
+ "Find compromise"
145
+ ];
146
  </script>
147
 
148
  <!-- Persona Detail View -->
149
  {#if selectedPersona}
150
+ <div class="flex w-full flex-col gap-6">
151
+ <!-- Header Section -->
152
+ <div class="flex flex-col gap-1">
153
+ <div class="flex items-center gap-2">
154
+ <h2 class="text-base font-semibold md:text-lg">
155
+ {selectedPersona.name}
156
+ </h2>
157
+ {#if selectedPersona.locked}
158
+ <span class="flex h-[20px] items-center rounded-full border border-amber-500 px-2 text-[11px] font-medium uppercase tracking-tight text-amber-700 dark:border-amber-400 dark:text-amber-300">
159
+ Locked
160
+ </span>
161
+ {/if}
162
+ </div>
163
+ <div class="flex flex-wrap items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
164
+ {#if selectedPersona.age}
165
+ <span>{selectedPersona.age}</span>
166
+ {/if}
167
+ {#if selectedPersona.age && selectedPersona.gender}
168
+ <span aria-hidden="true">•</span>
169
+ {/if}
170
+ {#if selectedPersona.gender}
171
+ <span>{selectedPersona.gender}</span>
172
+ {/if}
173
+ {#if (selectedPersona.age || selectedPersona.gender) && selectedPersona.jobSector}
174
+ <span aria-hidden="true">•</span>
175
+ {/if}
176
+ {#if selectedPersona.jobSector}
177
+ <span>{selectedPersona.jobSector}</span>
178
+ {/if}
179
+ </div>
180
+ </div>
181
+
182
+ <!-- Actions -->
183
+ <div class="flex flex-wrap items-center gap-2">
184
+ <button
185
+ class="flex w-fit items-center rounded-full bg-black px-3 py-1.5 text-sm !text-white shadow-sm hover:bg-black/90 dark:bg-white/80 dark:!text-gray-900 dark:hover:bg-white/90"
186
+ onclick={togglePersona}
187
+ >
188
+ {$settings.activePersonas.includes(selectedPersona.id) ? "Deactivate" : "Activate"}
189
+ </button>
190
+
191
+ {#if !selectedPersona.locked}
192
+ <button
193
+ class="inline-flex items-center rounded-full border border-red-200 px-2.5 py-1 text-sm text-red-600 hover:bg-red-50 dark:border-red-700 dark:text-red-400 dark:hover:bg-red-900/20"
194
+ onclick={deletePersona}
195
+ >
196
+ <CarbonTrashCan class="mr-1.5 shrink-0 text-xs" />
197
+ Delete
198
+ </button>
199
+ {/if}
200
+ </div>
201
+
202
+ <!-- Form Fields -->
203
+ <div class="w-full space-y-6">
204
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-[minmax(0,1fr)_auto_auto]">
205
  <div class="flex flex-col gap-2">
206
  <label for="persona-name" class="text-sm font-medium text-gray-700 dark:text-gray-300">
207
  Name <span class="text-red-500">*</span>
208
  </label>
 
209
  <input
210
  id="persona-name"
211
  type="text"
212
+ value={selectedPersona.name}
213
  required
214
+ disabled={selectedPersona.locked}
215
+ onblur={(e) => updatePersonaField('name', e.currentTarget.value)}
216
+ class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm transition-colors focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 disabled:cursor-not-allowed disabled:opacity-60 dark:border-gray-600 dark:bg-gray-800"
217
  maxlength="100"
218
  />
 
 
219
  </div>
220
 
221
+ <div class="w-full md:w-32">
222
+ <SelectField
223
+ label="Age"
224
+ value={selectedPersona.age}
225
+ options={ageOptions}
226
+ disabled={selectedPersona.locked}
227
+ required={true}
228
+ onChange={(value) => updatePersonaImmediate('age', value)}
229
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  </div>
231
 
232
+ <div class="w-full md:w-40">
233
+ <SelectField
234
+ label="Gender"
235
+ value={selectedPersona.gender}
236
+ options={genderOptions}
237
+ disabled={selectedPersona.locked}
238
+ required={true}
239
+ onChange={(value) => updatePersonaImmediate('gender', value)}
240
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  </div>
242
  </div>
243
 
244
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
245
+ <ComboboxField
246
+ label="Job Sector"
247
+ value={selectedPersona.jobSector || ""}
248
+ suggestions={jobSectorSuggestions}
249
+ disabled={selectedPersona.locked}
250
+ maxlength={200}
251
+ onChange={(value) => updatePersonaField('jobSector', value)}
252
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
+ <ComboboxField
255
+ label="Stance"
256
+ value={selectedPersona.stance || ""}
257
+ suggestions={stanceSuggestions}
258
+ disabled={selectedPersona.locked}
259
+ maxlength={200}
260
+ onChange={(value) => updatePersonaField('stance', value)}
261
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  </div>
263
 
264
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
265
+ <ComboboxField
266
+ label="Communication Style"
267
+ value={selectedPersona.communicationStyle || ""}
268
+ suggestions={communicationStyleSuggestions}
269
+ disabled={selectedPersona.locked}
270
+ maxlength={200}
271
+ onChange={(value) => updatePersonaField('communicationStyle', value)}
272
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
 
274
+ <ComboboxField
275
+ label="Goal in the Debate"
276
+ value={selectedPersona.goalInDebate || ""}
277
+ suggestions={goalInDebateSuggestions}
278
+ disabled={selectedPersona.locked}
279
+ maxlength={300}
280
+ onChange={(value) => updatePersonaField('goalInDebate', value)}
281
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  </div>
283
 
284
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
285
+ <SelectField
286
+ label="Income Bracket"
287
+ value={selectedPersona.incomeBracket || ""}
288
+ options={incomeBracketOptions}
289
+ disabled={selectedPersona.locked}
290
+ onChange={(value) => updatePersonaImmediate('incomeBracket', value)}
291
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
+ <SelectField
294
+ label="Political Leanings"
295
+ value={selectedPersona.politicalLeanings || ""}
296
+ options={politicalLeaningsOptions}
297
+ disabled={selectedPersona.locked}
298
+ onChange={(value) => updatePersonaImmediate('politicalLeanings', value)}
299
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
+ <SelectField
302
+ label="Geographic Context"
303
+ value={selectedPersona.geographicContext || ""}
304
+ options={geographicContextOptions}
305
+ disabled={selectedPersona.locked}
306
+ onChange={(value) => updatePersonaImmediate('geographicContext', value)}
307
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  </div>
310
  </div>
311
  {/if}