arkaos 3.70.5 → 3.70.7

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