arkaos 2.94.0 → 2.96.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,674 +0,0 @@
1
- <script setup lang="ts">
2
- import type { Persona } from '~/types'
3
-
4
- const { fetchApi, apiBase } = useApi()
5
- const toast = useToast()
6
-
7
- // --- Fetch personas (no await — non-blocking) ---
8
- const { data, status, error, refresh } = fetchApi<{ personas: Persona[]; total: number }>('/api/personas')
9
-
10
- const personas = computed(() => data.value?.personas ?? [])
11
-
12
- // PR74 v2.92.0 — detail/edit drawer state
13
- const detailOpen = ref(false)
14
- const detailPersonaId = ref<string | null>(null)
15
-
16
- function openDetail(persona: Persona) {
17
- detailPersonaId.value = persona.id
18
- detailOpen.value = true
19
- }
20
-
21
- async function onDetailSaved() {
22
- await refresh()
23
- }
24
-
25
- async function onDetailDeleted(_id: string) {
26
- await refresh()
27
- }
28
-
29
- // --- Creation mode ---
30
- // PR62 v2.79.0 — three modes: list (default), wizard (AI builder), manual.
31
- // The wizard is the new primary path; manual stays as fallback for
32
- // operators who want to type every DNA field by hand.
33
- type CreateMode = 'list' | 'wizard' | 'manual'
34
- const createMode = ref<CreateMode>('list')
35
- const showForm = computed(() => createMode.value === 'manual')
36
-
37
- function startWizard() {
38
- createMode.value = 'wizard'
39
- }
40
-
41
- function startManual() {
42
- createMode.value = 'manual'
43
- }
44
-
45
- function cancelCreation() {
46
- createMode.value = 'list'
47
- }
48
-
49
- async function onWizardComplete() {
50
- createMode.value = 'list'
51
- await refresh()
52
- }
53
-
54
- // --- Form state ---
55
- function defaultForm() {
56
- return {
57
- name: '',
58
- title: '',
59
- source: '',
60
- tagline: '',
61
- mbti: '',
62
- disc_primary: '',
63
- disc_secondary: '',
64
- enneagram_type: '',
65
- enneagram_wing: '',
66
- big_five_o: 50,
67
- big_five_c: 50,
68
- big_five_e: 50,
69
- big_five_a: 50,
70
- big_five_n: 50,
71
- mental_models: '',
72
- expertise_domains: '',
73
- frameworks: '',
74
- communication_tone: '',
75
- }
76
- }
77
-
78
- const form = ref(defaultForm())
79
- const creating = ref(false)
80
-
81
- // --- Options ---
82
- const mbtiTypes = [
83
- 'INTJ', 'INTP', 'ENTJ', 'ENTP',
84
- 'INFJ', 'INFP', 'ENFJ', 'ENFP',
85
- 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ',
86
- 'ISTP', 'ISFP', 'ESTP', 'ESFP',
87
- ].map(t => ({ label: t, value: t }))
88
-
89
- const discTypes = [
90
- { label: 'D — Dominance', value: 'D' },
91
- { label: 'I — Influence', value: 'I' },
92
- { label: 'S — Steadiness', value: 'S' },
93
- { label: 'C — Conscientiousness', value: 'C' },
94
- ]
95
-
96
- const enneagramTypes = Array.from({ length: 9 }, (_, i) => ({
97
- label: `Type ${i + 1}`,
98
- value: String(i + 1),
99
- }))
100
-
101
- const departmentOptions = [
102
- 'dev', 'marketing', 'brand', 'finance', 'strategy',
103
- 'ecom', 'kb', 'ops', 'pm', 'saas',
104
- 'landing', 'content', 'community', 'sales', 'leadership', 'org',
105
- ].map(d => ({ label: d, value: d }))
106
-
107
- const tierOptions = [
108
- { label: 'Tier 1 — Squad Leads', value: '1' },
109
- { label: 'Tier 2 — Specialists', value: '2' },
110
- { label: 'Tier 3 — Support', value: '3' },
111
- ]
112
-
113
- // --- Create persona ---
114
- async function createPersona() {
115
- if (!form.value.name.trim()) return
116
-
117
- creating.value = true
118
- try {
119
- await $fetch(`${apiBase}/api/personas`, {
120
- method: 'POST',
121
- body: {
122
- name: form.value.name,
123
- title: form.value.title,
124
- source: form.value.source,
125
- tagline: form.value.tagline,
126
- mbti: form.value.mbti,
127
- disc: {
128
- primary: form.value.disc_primary,
129
- secondary: form.value.disc_secondary,
130
- },
131
- enneagram: {
132
- type: form.value.enneagram_type ? Number(form.value.enneagram_type) : null,
133
- wing: form.value.enneagram_wing ? Number(form.value.enneagram_wing) : null,
134
- },
135
- big_five: {
136
- openness: form.value.big_five_o,
137
- conscientiousness: form.value.big_five_c,
138
- extraversion: form.value.big_five_e,
139
- agreeableness: form.value.big_five_a,
140
- neuroticism: form.value.big_five_n,
141
- },
142
- mental_models: form.value.mental_models
143
- ? form.value.mental_models.split(',').map(s => s.trim()).filter(Boolean)
144
- : [],
145
- expertise_domains: form.value.expertise_domains
146
- ? form.value.expertise_domains.split(',').map(s => s.trim()).filter(Boolean)
147
- : [],
148
- frameworks: form.value.frameworks
149
- ? form.value.frameworks.split(',').map(s => s.trim()).filter(Boolean)
150
- : [],
151
- communication: {
152
- tone: form.value.communication_tone,
153
- },
154
- },
155
- })
156
-
157
- toast.add({ title: 'Persona created', description: `${form.value.name} has been added.`, color: 'success' })
158
- form.value = defaultForm()
159
- showForm.value = false
160
- await refresh()
161
- } catch {
162
- toast.add({ title: 'Error', description: 'Failed to create persona.', color: 'error' })
163
- } finally {
164
- creating.value = false
165
- }
166
- }
167
-
168
- // --- Delete persona ---
169
- const deleting = ref<string | null>(null)
170
-
171
- async function deletePersona(persona: Persona) {
172
- deleting.value = persona.id
173
- try {
174
- await $fetch(`${apiBase}/api/personas/${persona.id}`, { method: 'DELETE' })
175
- toast.add({ title: 'Persona deleted', description: `${persona.name} has been removed.`, color: 'success' })
176
- await refresh()
177
- } catch {
178
- toast.add({ title: 'Error', description: 'Failed to delete persona.', color: 'error' })
179
- } finally {
180
- deleting.value = null
181
- }
182
- }
183
-
184
- // --- Clone to agent (inline expansion, no modal) ---
185
- const cloneExpandedId = ref<string | null>(null)
186
- const cloneDepartment = ref('')
187
- const cloneTier = ref('')
188
- const cloning = ref(false)
189
-
190
- function toggleClone(persona: Persona) {
191
- if (cloneExpandedId.value === persona.id) {
192
- cloneExpandedId.value = null
193
- } else {
194
- cloneExpandedId.value = persona.id
195
- cloneDepartment.value = ''
196
- cloneTier.value = ''
197
- }
198
- }
199
-
200
- async function cloneToAgent(persona: Persona) {
201
- if (!cloneDepartment.value || !cloneTier.value) return
202
-
203
- cloning.value = true
204
- try {
205
- await $fetch(`${apiBase}/api/personas/${persona.id}/clone`, {
206
- method: 'POST',
207
- body: {
208
- department: cloneDepartment.value,
209
- tier: Number(cloneTier.value),
210
- },
211
- })
212
- toast.add({
213
- title: 'Agent created',
214
- description: `${persona.name} cloned to ${cloneDepartment.value} department.`,
215
- color: 'success',
216
- })
217
- cloneExpandedId.value = null
218
- await refresh()
219
- } catch {
220
- toast.add({ title: 'Error', description: 'Failed to clone persona to agent.', color: 'error' })
221
- } finally {
222
- cloning.value = false
223
- }
224
- }
225
-
226
- // --- DNA badge colors ---
227
- function mbtiColor(mbti: string): string {
228
- if (!mbti) return 'neutral'
229
- const analysts = ['INTJ', 'INTP', 'ENTJ', 'ENTP']
230
- const diplomats = ['INFJ', 'INFP', 'ENFJ', 'ENFP']
231
- const sentinels = ['ISTJ', 'ISFJ', 'ESTJ', 'ESFJ']
232
- if (analysts.includes(mbti)) return 'primary'
233
- if (diplomats.includes(mbti)) return 'success'
234
- if (sentinels.includes(mbti)) return 'warning'
235
- return 'error'
236
- }
237
-
238
- function discColor(disc: string): string {
239
- const colors: Record<string, string> = { D: 'error', I: 'warning', S: 'success', C: 'primary' }
240
- return colors[disc] ?? 'neutral'
241
- }
242
- </script>
243
-
244
- <template>
245
- <UDashboardPanel id="personas">
246
- <template #header>
247
- <UDashboardNavbar title="Personas">
248
- <template #leading>
249
- <UDashboardSidebarCollapse />
250
- </template>
251
-
252
- <template #trailing>
253
- <UBadge v-if="data?.total" :label="data.total" variant="subtle" />
254
- </template>
255
-
256
- <template #right>
257
- <UButton
258
- v-if="createMode === 'list'"
259
- label="AI Builder"
260
- icon="i-lucide-sparkles"
261
- color="primary"
262
- size="sm"
263
- @click="startWizard"
264
- />
265
- <UButton
266
- v-if="createMode === 'list'"
267
- label="Manual"
268
- icon="i-lucide-plus"
269
- variant="outline"
270
- size="sm"
271
- class="ml-2"
272
- @click="startManual"
273
- />
274
- <UButton
275
- v-else
276
- label="Back to list"
277
- icon="i-lucide-arrow-left"
278
- variant="ghost"
279
- size="sm"
280
- @click="cancelCreation"
281
- />
282
- </template>
283
- </UDashboardNavbar>
284
- </template>
285
-
286
- <template #body>
287
- <div class="overflow-y-auto h-[calc(100vh-4rem)]">
288
- <!-- Loading -->
289
- <div v-if="status === 'pending'" class="flex items-center justify-center py-12" aria-label="Loading personas">
290
- <UIcon name="i-lucide-loader-2" class="size-8 animate-spin text-muted" />
291
- </div>
292
-
293
- <!-- Error -->
294
- <div v-else-if="error" class="flex flex-col items-center justify-center gap-4 py-12" role="alert">
295
- <UIcon name="i-lucide-alert-triangle" class="size-12 text-red-500" />
296
- <p class="text-sm text-muted">Failed to load personas.</p>
297
- <UButton label="Retry" variant="outline" color="primary" icon="i-lucide-refresh-cw" @click="refresh()" />
298
- </div>
299
-
300
- <!-- Content -->
301
- <template v-else>
302
- <!-- PR62: AI Persona Wizard -->
303
- <PersonaWizard
304
- v-if="createMode === 'wizard'"
305
- class="mb-8"
306
- @completed="onWizardComplete"
307
- @cancelled="cancelCreation"
308
- />
309
-
310
- <!-- Manual create form (legacy / fallback) -->
311
- <UCard v-if="showForm" class="mb-8">
312
- <form @submit.prevent="createPersona" class="space-y-8 p-2">
313
- <!-- Identity -->
314
- <fieldset>
315
- <legend class="text-xs font-bold uppercase tracking-widest text-muted mb-4">Identity</legend>
316
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
317
- <UFormField label="Name" required>
318
- <UInput
319
- v-model="form.name"
320
- placeholder="e.g. Alex Hormozi"
321
- aria-label="Persona name"
322
- class="w-full"
323
- required
324
- />
325
- </UFormField>
326
- <UFormField label="Title">
327
- <UInput
328
- v-model="form.title"
329
- placeholder="e.g. Business Strategy"
330
- aria-label="Persona title"
331
- class="w-full"
332
- />
333
- </UFormField>
334
- <UFormField label="Source">
335
- <UInput
336
- v-model="form.source"
337
- placeholder="e.g. Alex Hormozi"
338
- aria-label="Persona source"
339
- class="w-full"
340
- />
341
- </UFormField>
342
- <UFormField label="Tagline">
343
- <UInput
344
- v-model="form.tagline"
345
- placeholder="e.g. The Natural Commander"
346
- aria-label="Persona tagline"
347
- class="w-full"
348
- />
349
- </UFormField>
350
- </div>
351
- </fieldset>
352
-
353
- <!-- Behavioral DNA -->
354
- <fieldset>
355
- <legend class="text-xs font-bold uppercase tracking-widest text-muted mb-4">Behavioral DNA</legend>
356
- <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
357
- <UFormField label="MBTI">
358
- <USelect
359
- v-model="form.mbti"
360
- :items="mbtiTypes"
361
- placeholder="Select MBTI"
362
- aria-label="MBTI type"
363
- class="w-full"
364
- />
365
- </UFormField>
366
- <UFormField label="DISC Primary">
367
- <USelect
368
- v-model="form.disc_primary"
369
- :items="discTypes"
370
- placeholder="Primary"
371
- aria-label="DISC primary type"
372
- class="w-full"
373
- />
374
- </UFormField>
375
- <UFormField label="DISC Secondary">
376
- <USelect
377
- v-model="form.disc_secondary"
378
- :items="discTypes"
379
- placeholder="Secondary"
380
- aria-label="DISC secondary type"
381
- class="w-full"
382
- />
383
- </UFormField>
384
- <UFormField label="Enneagram Type">
385
- <USelect
386
- v-model="form.enneagram_type"
387
- :items="enneagramTypes"
388
- placeholder="Type"
389
- aria-label="Enneagram type"
390
- class="w-full"
391
- />
392
- </UFormField>
393
- </div>
394
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
395
- <UFormField label="Enneagram Wing (1-9)">
396
- <UInput
397
- v-model="form.enneagram_wing"
398
- type="number"
399
- :min="1"
400
- :max="9"
401
- placeholder="e.g. 4"
402
- aria-label="Enneagram wing"
403
- class="w-full"
404
- />
405
- </UFormField>
406
- </div>
407
- </fieldset>
408
-
409
- <!-- Big Five -->
410
- <fieldset>
411
- <legend class="text-xs font-bold uppercase tracking-widest text-muted mb-4">Big Five / OCEAN (0-100)</legend>
412
- <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
413
- <UFormField label="Openness">
414
- <UInput
415
- v-model.number="form.big_five_o"
416
- type="number"
417
- :min="0"
418
- :max="100"
419
- aria-label="Openness score"
420
- class="w-full"
421
- />
422
- </UFormField>
423
- <UFormField label="Conscientiousness">
424
- <UInput
425
- v-model.number="form.big_five_c"
426
- type="number"
427
- :min="0"
428
- :max="100"
429
- aria-label="Conscientiousness score"
430
- class="w-full"
431
- />
432
- </UFormField>
433
- <UFormField label="Extraversion">
434
- <UInput
435
- v-model.number="form.big_five_e"
436
- type="number"
437
- :min="0"
438
- :max="100"
439
- aria-label="Extraversion score"
440
- class="w-full"
441
- />
442
- </UFormField>
443
- <UFormField label="Agreeableness">
444
- <UInput
445
- v-model.number="form.big_five_a"
446
- type="number"
447
- :min="0"
448
- :max="100"
449
- aria-label="Agreeableness score"
450
- class="w-full"
451
- />
452
- </UFormField>
453
- <UFormField label="Neuroticism">
454
- <UInput
455
- v-model.number="form.big_five_n"
456
- type="number"
457
- :min="0"
458
- :max="100"
459
- aria-label="Neuroticism score"
460
- class="w-full"
461
- />
462
- </UFormField>
463
- </div>
464
- </fieldset>
465
-
466
- <!-- Knowledge & Communication -->
467
- <fieldset>
468
- <legend class="text-xs font-bold uppercase tracking-widest text-muted mb-4">Knowledge & Communication</legend>
469
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
470
- <UFormField label="Mental Models" hint="Comma-separated">
471
- <UInput
472
- v-model="form.mental_models"
473
- placeholder="e.g. Grand Slam Offer, Value Equation"
474
- aria-label="Mental models, comma-separated"
475
- class="w-full"
476
- />
477
- </UFormField>
478
- <UFormField label="Expertise Domains" hint="Comma-separated">
479
- <UInput
480
- v-model="form.expertise_domains"
481
- placeholder="e.g. business strategy, offer creation"
482
- aria-label="Expertise domains, comma-separated"
483
- class="w-full"
484
- />
485
- </UFormField>
486
- <UFormField label="Frameworks" hint="Comma-separated">
487
- <UInput
488
- v-model="form.frameworks"
489
- placeholder="e.g. $100M Offers, Value Equation"
490
- aria-label="Frameworks, comma-separated"
491
- class="w-full"
492
- />
493
- </UFormField>
494
- <UFormField label="Communication Tone">
495
- <UInput
496
- v-model="form.communication_tone"
497
- placeholder="e.g. direct, high-energy"
498
- aria-label="Communication tone"
499
- class="w-full"
500
- />
501
- </UFormField>
502
- </div>
503
- </fieldset>
504
-
505
- <!-- Submit -->
506
- <UButton
507
- type="submit"
508
- label="Create Persona"
509
- icon="i-lucide-sparkles"
510
- size="lg"
511
- block
512
- :loading="creating"
513
- :disabled="!form.name.trim()"
514
- />
515
- </form>
516
- </UCard>
517
-
518
- <!-- Empty state -->
519
- <div v-if="!personas.length && !showForm" class="flex flex-col items-center justify-center gap-6 py-20">
520
- <div class="rounded-full bg-muted/10 p-6">
521
- <UIcon name="i-lucide-users" class="size-12 text-muted" />
522
- </div>
523
- <div class="text-center space-y-2">
524
- <h3 class="text-base font-semibold">No personas yet</h3>
525
- <p class="text-sm text-muted max-w-sm">
526
- Personas define the behavioral DNA for your AI agents. Create one to get started.
527
- </p>
528
- </div>
529
- <UButton
530
- label="Create your first persona"
531
- icon="i-lucide-plus"
532
- size="lg"
533
- @click="showForm = true"
534
- />
535
- </div>
536
-
537
- <!-- PR74 v2.92.0 — detail/edit drawer -->
538
- <PersonaDetailDrawer
539
- v-model="detailOpen"
540
- :persona-id="detailPersonaId"
541
- @saved="onDetailSaved"
542
- @deleted="onDetailDeleted"
543
- />
544
-
545
- <!-- Personas Grid -->
546
- <div v-if="personas.length" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
547
- <UCard
548
- v-for="persona in personas"
549
- :key="persona.id"
550
- class="group flex flex-col cursor-pointer hover:border-primary/40 transition-colors"
551
- role="button"
552
- tabindex="0"
553
- @click="openDetail(persona)"
554
- @keydown.enter="openDetail(persona)"
555
- >
556
- <div class="flex flex-col gap-3 flex-1">
557
- <!-- Header -->
558
- <div>
559
- <h3 class="text-base font-bold truncate">{{ persona.name }}</h3>
560
- <p v-if="persona.title" class="text-sm text-muted truncate mt-0.5">{{ persona.title }}</p>
561
- <p v-if="persona.source" class="text-xs text-muted/60 mt-1">
562
- Source: {{ persona.source }}
563
- </p>
564
- </div>
565
-
566
- <!-- Tagline -->
567
- <p v-if="persona.tagline" class="text-sm text-muted italic leading-relaxed">
568
- "{{ persona.tagline }}"
569
- </p>
570
-
571
- <!-- DNA Badges -->
572
- <div class="flex flex-wrap gap-1.5">
573
- <UBadge
574
- v-if="persona.mbti"
575
- :label="persona.mbti"
576
- :color="mbtiColor(persona.mbti) as any"
577
- variant="subtle"
578
- size="xs"
579
- />
580
- <UBadge
581
- v-if="persona.disc?.primary"
582
- :label="`DISC: ${persona.disc.primary}${persona.disc.secondary ? '/' + persona.disc.secondary : ''}`"
583
- :color="discColor(persona.disc.primary) as any"
584
- variant="subtle"
585
- size="xs"
586
- />
587
- <UBadge
588
- v-if="persona.enneagram?.type"
589
- :label="`E${persona.enneagram.type}${persona.enneagram.wing ? 'w' + persona.enneagram.wing : ''}`"
590
- variant="outline"
591
- size="xs"
592
- />
593
- </div>
594
-
595
- <!-- Expertise domains -->
596
- <div v-if="persona.expertise_domains?.length" class="flex flex-wrap gap-1">
597
- <UBadge
598
- v-for="domain in persona.expertise_domains.slice(0, 3)"
599
- :key="domain"
600
- :label="domain"
601
- variant="outline"
602
- size="xs"
603
- color="neutral"
604
- />
605
- <UBadge
606
- v-if="persona.expertise_domains.length > 3"
607
- :label="`+${persona.expertise_domains.length - 3}`"
608
- variant="outline"
609
- size="xs"
610
- color="neutral"
611
- />
612
- </div>
613
-
614
- <!-- Actions -->
615
- <div class="pt-3 mt-auto border-t border-default space-y-3" @click.stop>
616
- <div class="flex gap-2">
617
- <UButton
618
- label="Clone to Agent"
619
- icon="i-lucide-copy"
620
- size="sm"
621
- variant="solid"
622
- class="flex-1"
623
- @click.stop="toggleClone(persona)"
624
- />
625
- <UButton
626
- icon="i-lucide-trash-2"
627
- size="sm"
628
- variant="ghost"
629
- color="error"
630
- :loading="deleting === persona.id"
631
- aria-label="Delete persona"
632
- @click.stop="deletePersona(persona)"
633
- />
634
- </div>
635
-
636
- <!-- Inline clone expansion -->
637
- <div v-if="cloneExpandedId === persona.id" class="space-y-3 pt-2">
638
- <UFormField label="Department" required>
639
- <USelect
640
- v-model="cloneDepartment"
641
- :items="departmentOptions"
642
- placeholder="Select department"
643
- aria-label="Target department"
644
- class="w-full"
645
- />
646
- </UFormField>
647
- <UFormField label="Tier" required>
648
- <USelect
649
- v-model="cloneTier"
650
- :items="tierOptions"
651
- placeholder="Select tier"
652
- aria-label="Agent tier"
653
- class="w-full"
654
- />
655
- </UFormField>
656
- <UButton
657
- label="Confirm Clone"
658
- icon="i-lucide-check"
659
- size="sm"
660
- block
661
- :loading="cloning"
662
- :disabled="!cloneDepartment || !cloneTier"
663
- @click="cloneToAgent(persona)"
664
- />
665
- </div>
666
- </div>
667
- </div>
668
- </UCard>
669
- </div>
670
- </template>
671
- </div>
672
- </template>
673
- </UDashboardPanel>
674
- </template>