arkaos 2.95.0 → 2.97.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.
- package/VERSION +1 -1
- package/dashboard/app/pages/agents/[id].vue +24 -24
- package/dashboard/app/pages/personas/[id].vue +691 -0
- package/dashboard/app/pages/personas/index.vue +296 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/dashboard/app/components/PersonaDetailDrawer.vue +0 -599
- package/dashboard/app/pages/personas.vue +0 -719
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR78 v2.96.0 — Persona detail page.
|
|
3
|
+
//
|
|
4
|
+
// Mirror of agents/[id].vue: gradient hero + initials avatar + stats
|
|
5
|
+
// row + UTabs (DNA / Communication / Knowledge / Linked Agents).
|
|
6
|
+
// Edit drawer reused from PR74 (PersonaDetailDrawer's edit form)
|
|
7
|
+
// but slimmed: the page itself is the read view; the drawer only
|
|
8
|
+
// flips into edit mode.
|
|
9
|
+
//
|
|
10
|
+
// Replaces the previous drawer-everywhere UX with the page-per-record
|
|
11
|
+
// pattern that already shipped for /agents.
|
|
12
|
+
|
|
13
|
+
import type { Persona } from '~/types'
|
|
14
|
+
|
|
15
|
+
interface DetailResponse extends Persona {
|
|
16
|
+
_source_store?: 'obsidian' | 'json'
|
|
17
|
+
_obsidian_path?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const route = useRoute()
|
|
21
|
+
const personaId = route.params.id as string
|
|
22
|
+
|
|
23
|
+
const { fetchApi, apiBase } = useApi()
|
|
24
|
+
const toast = useToast()
|
|
25
|
+
const confirmDialog = useConfirmDialog()
|
|
26
|
+
|
|
27
|
+
const { data: detail, status, error, refresh } = fetchApi<DetailResponse>(`/api/personas/${personaId}`)
|
|
28
|
+
|
|
29
|
+
const { data: usageData } = fetchApi<{
|
|
30
|
+
by_persona: Record<string, { agent_count: number, agent_ids: string[] }>
|
|
31
|
+
}>('/api/personas/usage')
|
|
32
|
+
|
|
33
|
+
const linkedAgentIds = computed<string[]>(() =>
|
|
34
|
+
usageData.value?.by_persona?.[personaId]?.agent_ids ?? [],
|
|
35
|
+
)
|
|
36
|
+
const linkedAgentCount = computed(() => linkedAgentIds.value.length)
|
|
37
|
+
|
|
38
|
+
// ─── Hero helpers (matching personas/index.vue + agents/[id].vue) ───────
|
|
39
|
+
|
|
40
|
+
function heroInitials(name: string | undefined): string {
|
|
41
|
+
if (!name) return '·'
|
|
42
|
+
const parts = name.trim().split(/\s+/)
|
|
43
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
|
|
44
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mbtiGradientClass(mbti: string | undefined): string {
|
|
48
|
+
if (!mbti) return 'bg-gradient-to-br from-muted/20 to-muted/5'
|
|
49
|
+
const code = mbti.toUpperCase()
|
|
50
|
+
if (['INTJ', 'INTP', 'ENTJ', 'ENTP'].includes(code))
|
|
51
|
+
return 'bg-gradient-to-br from-blue-500/30 to-indigo-600/10'
|
|
52
|
+
if (['INFJ', 'INFP', 'ENFJ', 'ENFP'].includes(code))
|
|
53
|
+
return 'bg-gradient-to-br from-emerald-500/30 to-teal-600/10'
|
|
54
|
+
if (['ISTJ', 'ISFJ', 'ESTJ', 'ESFJ'].includes(code))
|
|
55
|
+
return 'bg-gradient-to-br from-amber-500/30 to-orange-600/10'
|
|
56
|
+
if (['ISTP', 'ISFP', 'ESTP', 'ESFP'].includes(code))
|
|
57
|
+
return 'bg-gradient-to-br from-rose-500/30 to-pink-600/10'
|
|
58
|
+
return 'bg-gradient-to-br from-primary/20 to-primary/5'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const mbtiDescriptions: Record<string, string> = {
|
|
62
|
+
INTJ: 'Ni-Te-Fi-Se — The Architect',
|
|
63
|
+
INTP: 'Ti-Ne-Si-Fe — The Logician',
|
|
64
|
+
ENTJ: 'Te-Ni-Se-Fi — The Commander',
|
|
65
|
+
ENTP: 'Ne-Ti-Fe-Si — The Debater',
|
|
66
|
+
INFJ: 'Ni-Fe-Ti-Se — The Advocate',
|
|
67
|
+
INFP: 'Fi-Ne-Si-Te — The Mediator',
|
|
68
|
+
ENFJ: 'Fe-Ni-Se-Ti — The Protagonist',
|
|
69
|
+
ENFP: 'Ne-Fi-Te-Si — The Campaigner',
|
|
70
|
+
ISTJ: 'Si-Te-Fi-Ne — The Inspector',
|
|
71
|
+
ISFJ: 'Si-Fe-Ti-Ne — The Defender',
|
|
72
|
+
ESTJ: 'Te-Si-Ne-Fi — The Executive',
|
|
73
|
+
ESFJ: 'Fe-Si-Ne-Ti — The Consul',
|
|
74
|
+
ISTP: 'Ti-Se-Ni-Fe — The Virtuoso',
|
|
75
|
+
ISFP: 'Fi-Se-Ni-Te — The Adventurer',
|
|
76
|
+
ESTP: 'Se-Ti-Fe-Ni — The Entrepreneur',
|
|
77
|
+
ESFP: 'Se-Fi-Te-Ni — The Entertainer',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const discLetters = ['D', 'I', 'S', 'C'] as const
|
|
81
|
+
const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism'] as const
|
|
82
|
+
const bigFiveLabels: Record<string, string> = {
|
|
83
|
+
openness: 'Openness',
|
|
84
|
+
conscientiousness: 'Conscientiousness',
|
|
85
|
+
extraversion: 'Extraversion',
|
|
86
|
+
agreeableness: 'Agreeableness',
|
|
87
|
+
neuroticism: 'Neuroticism',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function discBarValue(letter: string): number {
|
|
91
|
+
if (!detail.value?.disc) return 20
|
|
92
|
+
if (detail.value.disc.primary === letter) return 90
|
|
93
|
+
if (detail.value.disc.secondary === letter) return 70
|
|
94
|
+
return 20
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function discBarColor(letter: string): string {
|
|
98
|
+
const colors: Record<string, string> = {
|
|
99
|
+
D: 'bg-red-500', I: 'bg-yellow-500', S: 'bg-green-500', C: 'bg-blue-500',
|
|
100
|
+
}
|
|
101
|
+
return colors[letter] ?? 'bg-primary'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function bigFiveBarColor(value: number): string {
|
|
105
|
+
if (value >= 75) return 'bg-primary'
|
|
106
|
+
if (value >= 50) return 'bg-blue-400'
|
|
107
|
+
if (value >= 30) return 'bg-yellow-500'
|
|
108
|
+
return 'bg-neutral-500'
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Tabs ───────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
const tabs = [
|
|
114
|
+
{ label: 'DNA', value: 'dna', icon: 'i-lucide-dna' },
|
|
115
|
+
{ label: 'Communication', value: 'communication', icon: 'i-lucide-message-square' },
|
|
116
|
+
{ label: 'Knowledge', value: 'knowledge', icon: 'i-lucide-brain' },
|
|
117
|
+
{ label: 'Linked Agents', value: 'agents', icon: 'i-lucide-users' },
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
// ─── Edit drawer state ─────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
const editOpen = ref(false)
|
|
123
|
+
const draft = ref<Persona | null>(null)
|
|
124
|
+
const saving = ref(false)
|
|
125
|
+
const dirty = ref(false)
|
|
126
|
+
|
|
127
|
+
function startEdit() {
|
|
128
|
+
if (!detail.value) return
|
|
129
|
+
draft.value = JSON.parse(JSON.stringify(detail.value)) as Persona
|
|
130
|
+
dirty.value = false
|
|
131
|
+
editOpen.value = true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function markDirty() { dirty.value = true }
|
|
135
|
+
|
|
136
|
+
async function tryCloseEdit() {
|
|
137
|
+
if (dirty.value && !saving.value) {
|
|
138
|
+
const ok = await confirmDialog({
|
|
139
|
+
title: 'Discard unsaved edits?',
|
|
140
|
+
description: 'Any changes you made will be lost.',
|
|
141
|
+
confirmLabel: 'Discard',
|
|
142
|
+
cancelLabel: 'Keep editing',
|
|
143
|
+
variant: 'danger',
|
|
144
|
+
})
|
|
145
|
+
if (!ok) return
|
|
146
|
+
}
|
|
147
|
+
editOpen.value = false
|
|
148
|
+
draft.value = null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function saveEdit() {
|
|
152
|
+
if (!draft.value) return
|
|
153
|
+
saving.value = true
|
|
154
|
+
try {
|
|
155
|
+
const res = await $fetch<{
|
|
156
|
+
id: string
|
|
157
|
+
updated: boolean
|
|
158
|
+
json_written: boolean
|
|
159
|
+
obsidian_path: string | null
|
|
160
|
+
error?: string
|
|
161
|
+
}>(`${apiBase}/api/personas/${personaId}`, {
|
|
162
|
+
method: 'PUT',
|
|
163
|
+
body: draft.value,
|
|
164
|
+
})
|
|
165
|
+
if (res.error) throw new Error(res.error)
|
|
166
|
+
toast.add({
|
|
167
|
+
title: 'Persona saved',
|
|
168
|
+
description: res.obsidian_path
|
|
169
|
+
? `Wrote ${res.obsidian_path.split('/').slice(-2).join('/')}`
|
|
170
|
+
: 'Saved to JSON store',
|
|
171
|
+
color: 'success',
|
|
172
|
+
})
|
|
173
|
+
await refresh()
|
|
174
|
+
editOpen.value = false
|
|
175
|
+
draft.value = null
|
|
176
|
+
dirty.value = false
|
|
177
|
+
} catch (err) {
|
|
178
|
+
toast.add({
|
|
179
|
+
title: 'Save failed',
|
|
180
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
181
|
+
color: 'error',
|
|
182
|
+
})
|
|
183
|
+
} finally {
|
|
184
|
+
saving.value = false
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function deletePersona() {
|
|
189
|
+
if (!detail.value) return
|
|
190
|
+
const ok = await confirmDialog({
|
|
191
|
+
title: `Delete persona "${detail.value.name}"?`,
|
|
192
|
+
description:
|
|
193
|
+
'Removes it from the JSON store. The Obsidian file (if any) is '
|
|
194
|
+
+ 'left in place — delete manually from Obsidian if you want it gone.',
|
|
195
|
+
confirmLabel: 'Delete persona',
|
|
196
|
+
variant: 'danger',
|
|
197
|
+
})
|
|
198
|
+
if (!ok) return
|
|
199
|
+
try {
|
|
200
|
+
await $fetch(`${apiBase}/api/personas/${personaId}`, { method: 'DELETE' })
|
|
201
|
+
toast.add({
|
|
202
|
+
title: 'Persona deleted',
|
|
203
|
+
description: detail.value.name,
|
|
204
|
+
color: 'success',
|
|
205
|
+
})
|
|
206
|
+
await navigateTo('/personas')
|
|
207
|
+
} catch (err) {
|
|
208
|
+
toast.add({
|
|
209
|
+
title: 'Delete failed',
|
|
210
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
211
|
+
color: 'error',
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function listToCsv(list: string[] | undefined): string {
|
|
217
|
+
return (list ?? []).join(', ')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function csvToList(value: string): string[] {
|
|
221
|
+
return value.split(',').map((s) => s.trim()).filter(Boolean)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const mbtiOptions = [
|
|
225
|
+
'INTJ', 'INTP', 'ENTJ', 'ENTP',
|
|
226
|
+
'INFJ', 'INFP', 'ENFJ', 'ENFP',
|
|
227
|
+
'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ',
|
|
228
|
+
'ISTP', 'ISFP', 'ESTP', 'ESFP',
|
|
229
|
+
].map((t) => ({ label: t, value: t }))
|
|
230
|
+
|
|
231
|
+
const discOptions = [
|
|
232
|
+
{ label: 'D — Dominance', value: 'D' },
|
|
233
|
+
{ label: 'I — Influence', value: 'I' },
|
|
234
|
+
{ label: 'S — Steadiness', value: 'S' },
|
|
235
|
+
{ label: 'C — Conscientiousness', value: 'C' },
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
const vocabOptions = [
|
|
239
|
+
{ label: 'Lay (no jargon)', value: 'lay' },
|
|
240
|
+
{ label: 'Specialist (industry terms)', value: 'specialist' },
|
|
241
|
+
{ label: 'Expert (research-level)', value: 'expert' },
|
|
242
|
+
]
|
|
243
|
+
</script>
|
|
244
|
+
|
|
245
|
+
<template>
|
|
246
|
+
<UDashboardPanel id="persona-detail">
|
|
247
|
+
<template #header>
|
|
248
|
+
<UDashboardNavbar :title="detail?.name ?? 'Persona'">
|
|
249
|
+
<template #leading>
|
|
250
|
+
<UDashboardSidebarCollapse />
|
|
251
|
+
</template>
|
|
252
|
+
<template #trailing>
|
|
253
|
+
<UButton
|
|
254
|
+
label="Back"
|
|
255
|
+
variant="ghost"
|
|
256
|
+
icon="i-lucide-arrow-left"
|
|
257
|
+
to="/personas"
|
|
258
|
+
aria-label="Back to personas list"
|
|
259
|
+
/>
|
|
260
|
+
</template>
|
|
261
|
+
</UDashboardNavbar>
|
|
262
|
+
</template>
|
|
263
|
+
|
|
264
|
+
<template #body>
|
|
265
|
+
<DashboardState
|
|
266
|
+
:status="status"
|
|
267
|
+
:error="error"
|
|
268
|
+
:empty="!detail"
|
|
269
|
+
empty-title="Persona not found"
|
|
270
|
+
empty-icon="i-lucide-user-x"
|
|
271
|
+
loading-label="Loading persona"
|
|
272
|
+
:on-retry="() => refresh()"
|
|
273
|
+
>
|
|
274
|
+
<div v-if="detail" class="space-y-6 pb-12">
|
|
275
|
+
<!-- HERO -->
|
|
276
|
+
<section
|
|
277
|
+
class="relative overflow-hidden rounded-2xl border border-default p-6 md:p-8"
|
|
278
|
+
:class="mbtiGradientClass(detail.mbti)"
|
|
279
|
+
>
|
|
280
|
+
<div class="flex items-start gap-5">
|
|
281
|
+
<div class="shrink-0 size-20 rounded-2xl bg-default/80 border border-default flex items-center justify-center shadow-lg backdrop-blur-sm">
|
|
282
|
+
<span class="text-2xl font-bold tracking-tight text-highlighted">
|
|
283
|
+
{{ heroInitials(detail.name) }}
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
<div class="flex-1 min-w-0 space-y-2">
|
|
287
|
+
<div class="flex items-start justify-between gap-3 flex-wrap">
|
|
288
|
+
<div class="min-w-0">
|
|
289
|
+
<h1 class="text-3xl md:text-4xl font-bold tracking-tight text-highlighted">
|
|
290
|
+
{{ detail.name }}
|
|
291
|
+
</h1>
|
|
292
|
+
<p v-if="detail.title" class="text-base md:text-lg text-muted mt-0.5">
|
|
293
|
+
{{ detail.title }}
|
|
294
|
+
</p>
|
|
295
|
+
</div>
|
|
296
|
+
<div class="flex items-center gap-2">
|
|
297
|
+
<UButton label="Edit" icon="i-lucide-pencil" size="sm" @click="startEdit" />
|
|
298
|
+
<UButton
|
|
299
|
+
icon="i-lucide-trash-2"
|
|
300
|
+
color="error"
|
|
301
|
+
variant="ghost"
|
|
302
|
+
size="sm"
|
|
303
|
+
aria-label="Delete persona"
|
|
304
|
+
@click="deletePersona"
|
|
305
|
+
/>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
<p v-if="detail.tagline" class="text-sm italic text-muted">
|
|
309
|
+
"{{ detail.tagline }}"
|
|
310
|
+
</p>
|
|
311
|
+
<div class="flex flex-wrap items-center gap-2 pt-1">
|
|
312
|
+
<UBadge
|
|
313
|
+
v-if="detail._source_store === 'obsidian'"
|
|
314
|
+
label="From Obsidian"
|
|
315
|
+
icon="i-lucide-file-text"
|
|
316
|
+
color="primary"
|
|
317
|
+
variant="subtle"
|
|
318
|
+
size="sm"
|
|
319
|
+
/>
|
|
320
|
+
<UBadge
|
|
321
|
+
v-else-if="detail._source_store === 'json'"
|
|
322
|
+
label="JSON store"
|
|
323
|
+
variant="outline"
|
|
324
|
+
size="sm"
|
|
325
|
+
/>
|
|
326
|
+
<UBadge v-if="detail.mbti" :label="detail.mbti" variant="soft" size="sm" />
|
|
327
|
+
<UBadge
|
|
328
|
+
v-if="detail.disc?.primary"
|
|
329
|
+
:label="`DISC: ${detail.disc.primary}${detail.disc.secondary ? '/' + detail.disc.secondary : ''}`"
|
|
330
|
+
variant="subtle"
|
|
331
|
+
size="sm"
|
|
332
|
+
/>
|
|
333
|
+
</div>
|
|
334
|
+
<p
|
|
335
|
+
v-if="detail._obsidian_path"
|
|
336
|
+
class="text-[10px] text-muted/70 font-mono truncate mt-2"
|
|
337
|
+
:title="detail._obsidian_path"
|
|
338
|
+
>
|
|
339
|
+
{{ detail._obsidian_path.split('/').slice(-2).join('/') }}
|
|
340
|
+
</p>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</section>
|
|
344
|
+
|
|
345
|
+
<!-- STATS -->
|
|
346
|
+
<section class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
347
|
+
<div class="rounded-xl border border-default p-4 bg-elevated/20">
|
|
348
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-1">Linked agents</p>
|
|
349
|
+
<p class="text-2xl font-bold">{{ linkedAgentCount }}</p>
|
|
350
|
+
</div>
|
|
351
|
+
<div class="rounded-xl border border-default p-4 bg-elevated/20">
|
|
352
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-1">Mental models</p>
|
|
353
|
+
<p class="text-2xl font-bold">{{ detail.mental_models?.length ?? 0 }}</p>
|
|
354
|
+
</div>
|
|
355
|
+
<div class="rounded-xl border border-default p-4 bg-elevated/20">
|
|
356
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-1">Expertise domains</p>
|
|
357
|
+
<p class="text-2xl font-bold">{{ detail.expertise_domains?.length ?? 0 }}</p>
|
|
358
|
+
</div>
|
|
359
|
+
<div class="rounded-xl border border-default p-4 bg-elevated/20">
|
|
360
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-1">Frameworks</p>
|
|
361
|
+
<p class="text-2xl font-bold">{{ detail.frameworks?.length ?? 0 }}</p>
|
|
362
|
+
</div>
|
|
363
|
+
</section>
|
|
364
|
+
|
|
365
|
+
<!-- TABS -->
|
|
366
|
+
<UTabs :items="tabs" default-value="dna" class="w-full">
|
|
367
|
+
<template #content="{ item }">
|
|
368
|
+
<!-- DNA -->
|
|
369
|
+
<div v-if="item.value === 'dna'" class="space-y-6 mt-6">
|
|
370
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
371
|
+
<UCard>
|
|
372
|
+
<div class="space-y-2">
|
|
373
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide">MBTI</p>
|
|
374
|
+
<p class="text-4xl font-bold font-mono tracking-widest">
|
|
375
|
+
{{ detail.mbti || '----' }}
|
|
376
|
+
</p>
|
|
377
|
+
<p v-if="detail.mbti && mbtiDescriptions[detail.mbti]" class="text-sm text-muted">
|
|
378
|
+
{{ mbtiDescriptions[detail.mbti] }}
|
|
379
|
+
</p>
|
|
380
|
+
</div>
|
|
381
|
+
</UCard>
|
|
382
|
+
|
|
383
|
+
<UCard>
|
|
384
|
+
<div class="space-y-2">
|
|
385
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide">Enneagram</p>
|
|
386
|
+
<p class="text-3xl font-bold">
|
|
387
|
+
Type {{ detail.enneagram?.type ?? '-' }}
|
|
388
|
+
<span v-if="detail.enneagram?.wing" class="text-xl font-normal text-muted">
|
|
389
|
+
w{{ detail.enneagram.wing }}
|
|
390
|
+
</span>
|
|
391
|
+
</p>
|
|
392
|
+
</div>
|
|
393
|
+
</UCard>
|
|
394
|
+
|
|
395
|
+
<UCard>
|
|
396
|
+
<div class="space-y-3">
|
|
397
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide">DISC</p>
|
|
398
|
+
<p class="text-3xl font-bold font-mono">
|
|
399
|
+
{{ detail.disc?.primary ?? '' }}{{ detail.disc?.secondary ?? '' }}
|
|
400
|
+
</p>
|
|
401
|
+
<div class="space-y-2 pt-1">
|
|
402
|
+
<div v-for="letter in discLetters" :key="letter" class="flex items-center gap-2">
|
|
403
|
+
<span class="w-4 text-xs font-mono font-bold text-muted">{{ letter }}</span>
|
|
404
|
+
<div class="flex-1 h-2 rounded-full bg-muted/20">
|
|
405
|
+
<div
|
|
406
|
+
class="h-2 rounded-full"
|
|
407
|
+
:class="discBarColor(letter)"
|
|
408
|
+
:style="{ width: `${discBarValue(letter)}%` }"
|
|
409
|
+
/>
|
|
410
|
+
</div>
|
|
411
|
+
<span class="w-6 text-right text-xs font-mono text-muted">
|
|
412
|
+
{{ discBarValue(letter) }}
|
|
413
|
+
</span>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
</UCard>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<UCard>
|
|
421
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-4">
|
|
422
|
+
Big Five (OCEAN)
|
|
423
|
+
</p>
|
|
424
|
+
<div v-if="detail.big_five" class="space-y-3">
|
|
425
|
+
<div v-for="key in bigFiveKeys" :key="key" class="flex items-center gap-3">
|
|
426
|
+
<span class="w-36 text-sm text-muted">{{ bigFiveLabels[key] }}</span>
|
|
427
|
+
<div class="flex-1 h-2 rounded-full bg-muted/20">
|
|
428
|
+
<div
|
|
429
|
+
class="h-2 rounded-full"
|
|
430
|
+
:class="bigFiveBarColor((detail.big_five as any)[key] ?? 0)"
|
|
431
|
+
:style="{ width: `${(detail.big_five as any)[key] ?? 0}%` }"
|
|
432
|
+
/>
|
|
433
|
+
</div>
|
|
434
|
+
<span class="w-8 text-right text-sm font-mono">
|
|
435
|
+
{{ (detail.big_five as any)[key] ?? 0 }}
|
|
436
|
+
</span>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
</UCard>
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
<!-- COMMUNICATION -->
|
|
443
|
+
<div v-else-if="item.value === 'communication'" class="space-y-4 mt-6">
|
|
444
|
+
<UCard v-if="detail.communication">
|
|
445
|
+
<dl class="grid grid-cols-3 gap-2 text-sm">
|
|
446
|
+
<dt class="text-muted">Tone</dt>
|
|
447
|
+
<dd class="col-span-2">{{ detail.communication.tone || '—' }}</dd>
|
|
448
|
+
<dt class="text-muted">Vocabulary</dt>
|
|
449
|
+
<dd class="col-span-2">{{ detail.communication.vocabulary_level || '—' }}</dd>
|
|
450
|
+
</dl>
|
|
451
|
+
</UCard>
|
|
452
|
+
<div v-else class="py-8 text-center text-sm text-muted">
|
|
453
|
+
No communication data available.
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
<!-- KNOWLEDGE -->
|
|
458
|
+
<div v-else-if="item.value === 'knowledge'" class="space-y-4 mt-6">
|
|
459
|
+
<UCard v-if="detail.mental_models?.length">
|
|
460
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-3">
|
|
461
|
+
Mental models ({{ detail.mental_models.length }})
|
|
462
|
+
</p>
|
|
463
|
+
<div class="flex flex-wrap gap-1.5">
|
|
464
|
+
<UBadge v-for="m in detail.mental_models" :key="m" :label="m" variant="outline" size="sm" />
|
|
465
|
+
</div>
|
|
466
|
+
</UCard>
|
|
467
|
+
|
|
468
|
+
<UCard v-if="detail.expertise_domains?.length">
|
|
469
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-3">
|
|
470
|
+
Expertise ({{ detail.expertise_domains.length }})
|
|
471
|
+
</p>
|
|
472
|
+
<div class="flex flex-wrap gap-1.5">
|
|
473
|
+
<UBadge v-for="e in detail.expertise_domains" :key="e" :label="e" variant="soft" size="sm" />
|
|
474
|
+
</div>
|
|
475
|
+
</UCard>
|
|
476
|
+
|
|
477
|
+
<UCard v-if="detail.frameworks?.length">
|
|
478
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-3">
|
|
479
|
+
Frameworks ({{ detail.frameworks.length }})
|
|
480
|
+
</p>
|
|
481
|
+
<ul class="space-y-2">
|
|
482
|
+
<li v-for="f in detail.frameworks" :key="f" class="flex items-center gap-2 text-sm">
|
|
483
|
+
<UIcon name="i-lucide-check" class="size-3.5 text-primary shrink-0" />
|
|
484
|
+
{{ f }}
|
|
485
|
+
</li>
|
|
486
|
+
</ul>
|
|
487
|
+
</UCard>
|
|
488
|
+
|
|
489
|
+
<UCard v-if="detail.key_quotes?.length">
|
|
490
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-3">
|
|
491
|
+
Key quotes ({{ detail.key_quotes.length }})
|
|
492
|
+
</p>
|
|
493
|
+
<ul class="space-y-2">
|
|
494
|
+
<li
|
|
495
|
+
v-for="q in detail.key_quotes"
|
|
496
|
+
:key="q"
|
|
497
|
+
class="text-sm italic text-muted border-l-2 border-primary/30 pl-3"
|
|
498
|
+
>
|
|
499
|
+
"{{ q }}"
|
|
500
|
+
</li>
|
|
501
|
+
</ul>
|
|
502
|
+
</UCard>
|
|
503
|
+
|
|
504
|
+
<div
|
|
505
|
+
v-if="!detail.mental_models?.length && !detail.expertise_domains?.length && !detail.frameworks?.length && !detail.key_quotes?.length"
|
|
506
|
+
class="py-8 text-center text-sm text-muted"
|
|
507
|
+
>
|
|
508
|
+
No knowledge data available.
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<!-- LINKED AGENTS -->
|
|
513
|
+
<div v-else-if="item.value === 'agents'" class="space-y-4 mt-6">
|
|
514
|
+
<UCard v-if="linkedAgentCount > 0">
|
|
515
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-3">
|
|
516
|
+
Linked to {{ linkedAgentCount }} agent{{ linkedAgentCount === 1 ? '' : 's' }}
|
|
517
|
+
</p>
|
|
518
|
+
<div class="space-y-2">
|
|
519
|
+
<NuxtLink
|
|
520
|
+
v-for="aid in linkedAgentIds"
|
|
521
|
+
:key="aid"
|
|
522
|
+
:to="`/agents/${aid}`"
|
|
523
|
+
class="flex items-center justify-between p-3 rounded-lg border border-default hover:border-primary/40 transition-colors"
|
|
524
|
+
>
|
|
525
|
+
<span class="font-mono text-sm">{{ aid }}</span>
|
|
526
|
+
<UIcon name="i-lucide-arrow-right" class="size-4 text-muted" />
|
|
527
|
+
</NuxtLink>
|
|
528
|
+
</div>
|
|
529
|
+
</UCard>
|
|
530
|
+
<div v-else class="py-8 text-center text-sm text-muted">
|
|
531
|
+
Not linked to any agent yet. Open an agent's edit drawer
|
|
532
|
+
to attach this persona.
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
</template>
|
|
536
|
+
</UTabs>
|
|
537
|
+
|
|
538
|
+
<!-- EDIT DRAWER -->
|
|
539
|
+
<USlideover
|
|
540
|
+
:open="editOpen"
|
|
541
|
+
:ui="{ content: 'max-w-2xl w-full' }"
|
|
542
|
+
@update:open="(v) => v ? null : tryCloseEdit()"
|
|
543
|
+
>
|
|
544
|
+
<template #content>
|
|
545
|
+
<UCard
|
|
546
|
+
v-if="draft"
|
|
547
|
+
:ui="{
|
|
548
|
+
root: 'h-full flex flex-col rounded-none',
|
|
549
|
+
body: 'flex-1 overflow-y-auto',
|
|
550
|
+
}"
|
|
551
|
+
>
|
|
552
|
+
<template #header>
|
|
553
|
+
<div class="flex items-center justify-between">
|
|
554
|
+
<h2 class="text-xl font-bold">Edit {{ draft.name || 'persona' }}</h2>
|
|
555
|
+
<UButton
|
|
556
|
+
icon="i-lucide-x"
|
|
557
|
+
variant="ghost"
|
|
558
|
+
size="sm"
|
|
559
|
+
aria-label="Close"
|
|
560
|
+
@click="tryCloseEdit"
|
|
561
|
+
/>
|
|
562
|
+
</div>
|
|
563
|
+
</template>
|
|
564
|
+
|
|
565
|
+
<div class="space-y-5">
|
|
566
|
+
<section class="space-y-3">
|
|
567
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Identity</h3>
|
|
568
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
569
|
+
<UFormField label="Name" required>
|
|
570
|
+
<UInput v-model="draft.name" class="w-full" @update:model-value="markDirty" />
|
|
571
|
+
</UFormField>
|
|
572
|
+
<UFormField label="Title">
|
|
573
|
+
<UInput v-model="draft.title" class="w-full" @update:model-value="markDirty" />
|
|
574
|
+
</UFormField>
|
|
575
|
+
<UFormField label="Source">
|
|
576
|
+
<UInput v-model="draft.source" class="w-full" @update:model-value="markDirty" />
|
|
577
|
+
</UFormField>
|
|
578
|
+
<UFormField label="Tagline">
|
|
579
|
+
<UInput v-model="draft.tagline" class="w-full" @update:model-value="markDirty" />
|
|
580
|
+
</UFormField>
|
|
581
|
+
</div>
|
|
582
|
+
</section>
|
|
583
|
+
|
|
584
|
+
<section class="space-y-3">
|
|
585
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Behavioural DNA</h3>
|
|
586
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
587
|
+
<UFormField label="MBTI">
|
|
588
|
+
<USelect v-model="draft.mbti" :items="mbtiOptions" class="w-full" @update:model-value="markDirty" />
|
|
589
|
+
</UFormField>
|
|
590
|
+
<UFormField label="DISC primary">
|
|
591
|
+
<USelect v-model="draft.disc.primary" :items="discOptions" class="w-full" @update:model-value="markDirty" />
|
|
592
|
+
</UFormField>
|
|
593
|
+
<UFormField label="Enneagram">
|
|
594
|
+
<UInput v-model.number="draft.enneagram.type" type="number" :min="1" :max="9" class="w-full" @update:model-value="markDirty" />
|
|
595
|
+
</UFormField>
|
|
596
|
+
<UFormField label="Wing">
|
|
597
|
+
<UInput v-model.number="draft.enneagram.wing" type="number" :min="1" :max="9" class="w-full" @update:model-value="markDirty" />
|
|
598
|
+
</UFormField>
|
|
599
|
+
</div>
|
|
600
|
+
<div class="space-y-2">
|
|
601
|
+
<div
|
|
602
|
+
v-for="key in bigFiveKeys"
|
|
603
|
+
:key="key"
|
|
604
|
+
class="flex items-center gap-3"
|
|
605
|
+
>
|
|
606
|
+
<label class="text-xs text-muted w-36 shrink-0 capitalize">{{ key }}</label>
|
|
607
|
+
<UInput
|
|
608
|
+
v-model.number="(draft.big_five as any)[key]"
|
|
609
|
+
type="number"
|
|
610
|
+
:min="0"
|
|
611
|
+
:max="100"
|
|
612
|
+
class="w-20"
|
|
613
|
+
@update:model-value="markDirty"
|
|
614
|
+
/>
|
|
615
|
+
<input
|
|
616
|
+
v-model.number="(draft.big_five as any)[key]"
|
|
617
|
+
type="range"
|
|
618
|
+
:min="0"
|
|
619
|
+
:max="100"
|
|
620
|
+
class="flex-1"
|
|
621
|
+
@input="markDirty"
|
|
622
|
+
/>
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
</section>
|
|
626
|
+
|
|
627
|
+
<section class="space-y-3">
|
|
628
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Knowledge</h3>
|
|
629
|
+
<UFormField label="Mental models" help="comma-separated">
|
|
630
|
+
<UInput
|
|
631
|
+
:model-value="listToCsv(draft.mental_models)"
|
|
632
|
+
@update:model-value="(v: string) => { if (draft) { draft.mental_models = csvToList(v); markDirty() } }"
|
|
633
|
+
class="w-full"
|
|
634
|
+
/>
|
|
635
|
+
</UFormField>
|
|
636
|
+
<UFormField label="Expertise domains" help="comma-separated">
|
|
637
|
+
<UInput
|
|
638
|
+
:model-value="listToCsv(draft.expertise_domains)"
|
|
639
|
+
@update:model-value="(v: string) => { if (draft) { draft.expertise_domains = csvToList(v); markDirty() } }"
|
|
640
|
+
class="w-full"
|
|
641
|
+
/>
|
|
642
|
+
</UFormField>
|
|
643
|
+
<UFormField label="Frameworks" help="comma-separated">
|
|
644
|
+
<UInput
|
|
645
|
+
:model-value="listToCsv(draft.frameworks)"
|
|
646
|
+
@update:model-value="(v: string) => { if (draft) { draft.frameworks = csvToList(v); markDirty() } }"
|
|
647
|
+
class="w-full"
|
|
648
|
+
/>
|
|
649
|
+
</UFormField>
|
|
650
|
+
</section>
|
|
651
|
+
|
|
652
|
+
<section class="space-y-3">
|
|
653
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Communication</h3>
|
|
654
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
655
|
+
<UFormField label="Tone">
|
|
656
|
+
<UInput v-model="draft.communication.tone" class="w-full" @update:model-value="markDirty" />
|
|
657
|
+
</UFormField>
|
|
658
|
+
<UFormField label="Vocabulary level">
|
|
659
|
+
<USelect v-model="draft.communication.vocabulary_level" :items="vocabOptions" class="w-full" @update:model-value="markDirty" />
|
|
660
|
+
</UFormField>
|
|
661
|
+
</div>
|
|
662
|
+
</section>
|
|
663
|
+
</div>
|
|
664
|
+
|
|
665
|
+
<template #footer>
|
|
666
|
+
<div class="flex items-center justify-between gap-2">
|
|
667
|
+
<span v-if="dirty" class="text-xs text-yellow-400">
|
|
668
|
+
<UIcon name="i-lucide-circle-dot" class="size-3 inline" />
|
|
669
|
+
Unsaved changes
|
|
670
|
+
</span>
|
|
671
|
+
<span v-else class="text-xs text-muted">No changes</span>
|
|
672
|
+
<div class="flex gap-2">
|
|
673
|
+
<UButton label="Cancel" variant="ghost" :disabled="saving" @click="tryCloseEdit" />
|
|
674
|
+
<UButton
|
|
675
|
+
label="Save"
|
|
676
|
+
icon="i-lucide-check"
|
|
677
|
+
:loading="saving"
|
|
678
|
+
:disabled="!dirty"
|
|
679
|
+
@click="saveEdit"
|
|
680
|
+
/>
|
|
681
|
+
</div>
|
|
682
|
+
</div>
|
|
683
|
+
</template>
|
|
684
|
+
</UCard>
|
|
685
|
+
</template>
|
|
686
|
+
</USlideover>
|
|
687
|
+
</div>
|
|
688
|
+
</DashboardState>
|
|
689
|
+
</template>
|
|
690
|
+
</UDashboardPanel>
|
|
691
|
+
</template>
|