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
|
@@ -1,599 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// PR74 v2.92.0 — Persona detail + edit drawer.
|
|
3
|
-
//
|
|
4
|
-
// Click a persona card on the list → this drawer opens with every
|
|
5
|
-
// field visible. Toggle to Edit mode to mutate any field, then save
|
|
6
|
-
// via PUT /api/personas/{id} (writes to both JSON store + Obsidian).
|
|
7
|
-
|
|
8
|
-
import type { Persona } from '~/types'
|
|
9
|
-
|
|
10
|
-
interface DetailResponse extends Persona {
|
|
11
|
-
_source_store?: 'obsidian' | 'json'
|
|
12
|
-
_obsidian_path?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const props = defineProps<{
|
|
16
|
-
modelValue: boolean
|
|
17
|
-
personaId: string | null
|
|
18
|
-
}>()
|
|
19
|
-
|
|
20
|
-
const emit = defineEmits<{
|
|
21
|
-
(e: 'update:modelValue', value: boolean): void
|
|
22
|
-
(e: 'saved', persona: Persona): void
|
|
23
|
-
(e: 'deleted', personaId: string): void
|
|
24
|
-
}>()
|
|
25
|
-
|
|
26
|
-
const { apiBase } = useApi()
|
|
27
|
-
const toast = useToast()
|
|
28
|
-
const confirmDialog = useConfirmDialog()
|
|
29
|
-
|
|
30
|
-
const detail = ref<DetailResponse | null>(null)
|
|
31
|
-
const editing = ref(false)
|
|
32
|
-
const draft = ref<Persona | null>(null)
|
|
33
|
-
const saving = ref(false)
|
|
34
|
-
const deleting = ref(false)
|
|
35
|
-
const loading = ref(false)
|
|
36
|
-
const loadError = ref<string | null>(null)
|
|
37
|
-
|
|
38
|
-
watch(
|
|
39
|
-
() => [props.modelValue, props.personaId] as const,
|
|
40
|
-
async ([open, id]) => {
|
|
41
|
-
if (!open || !id) {
|
|
42
|
-
detail.value = null
|
|
43
|
-
editing.value = false
|
|
44
|
-
draft.value = null
|
|
45
|
-
loadError.value = null
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
await loadDetail(id)
|
|
49
|
-
},
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
async function loadDetail(id: string) {
|
|
53
|
-
loading.value = true
|
|
54
|
-
loadError.value = null
|
|
55
|
-
try {
|
|
56
|
-
const data = await $fetch<DetailResponse | { error: string }>(
|
|
57
|
-
`${apiBase}/api/personas/${id}`,
|
|
58
|
-
)
|
|
59
|
-
if ('error' in data && data.error) {
|
|
60
|
-
loadError.value = data.error
|
|
61
|
-
detail.value = null
|
|
62
|
-
} else {
|
|
63
|
-
detail.value = data as DetailResponse
|
|
64
|
-
}
|
|
65
|
-
} catch (err) {
|
|
66
|
-
loadError.value = err instanceof Error ? err.message : 'unknown error'
|
|
67
|
-
} finally {
|
|
68
|
-
loading.value = false
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function startEdit() {
|
|
73
|
-
if (!detail.value) return
|
|
74
|
-
draft.value = JSON.parse(JSON.stringify(detail.value)) as Persona
|
|
75
|
-
editing.value = true
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function cancelEdit() {
|
|
79
|
-
draft.value = null
|
|
80
|
-
editing.value = false
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function saveEdit() {
|
|
84
|
-
if (!draft.value || !props.personaId) return
|
|
85
|
-
saving.value = true
|
|
86
|
-
try {
|
|
87
|
-
const res = await $fetch<{
|
|
88
|
-
id: string
|
|
89
|
-
updated: boolean
|
|
90
|
-
json_written: boolean
|
|
91
|
-
obsidian_path: string | null
|
|
92
|
-
error?: string
|
|
93
|
-
}>(`${apiBase}/api/personas/${props.personaId}`, {
|
|
94
|
-
method: 'PUT',
|
|
95
|
-
body: draft.value,
|
|
96
|
-
})
|
|
97
|
-
if (res.error) throw new Error(res.error)
|
|
98
|
-
toast.add({
|
|
99
|
-
title: 'Persona saved',
|
|
100
|
-
description: res.obsidian_path
|
|
101
|
-
? `Wrote ${res.obsidian_path.split('/').slice(-2).join('/')}`
|
|
102
|
-
: 'Saved to JSON store',
|
|
103
|
-
color: 'success',
|
|
104
|
-
})
|
|
105
|
-
emit('saved', draft.value)
|
|
106
|
-
detail.value = { ...detail.value, ...draft.value } as DetailResponse
|
|
107
|
-
editing.value = false
|
|
108
|
-
} catch (err) {
|
|
109
|
-
toast.add({
|
|
110
|
-
title: 'Save failed',
|
|
111
|
-
description: err instanceof Error ? err.message : 'unknown error',
|
|
112
|
-
color: 'error',
|
|
113
|
-
})
|
|
114
|
-
} finally {
|
|
115
|
-
saving.value = false
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function deletePersona() {
|
|
120
|
-
if (!props.personaId) return
|
|
121
|
-
const ok = await confirmDialog({
|
|
122
|
-
title: `Delete persona "${detail.value?.name ?? 'Unknown'}"?`,
|
|
123
|
-
description:
|
|
124
|
-
'Removes it from the JSON store. The Obsidian file (if any) is '
|
|
125
|
-
+ 'left in place — delete manually from Obsidian if you want it gone.',
|
|
126
|
-
confirmLabel: 'Delete persona',
|
|
127
|
-
variant: 'danger',
|
|
128
|
-
})
|
|
129
|
-
if (!ok) return
|
|
130
|
-
deleting.value = true
|
|
131
|
-
try {
|
|
132
|
-
await $fetch(`${apiBase}/api/personas/${props.personaId}`, {
|
|
133
|
-
method: 'DELETE',
|
|
134
|
-
})
|
|
135
|
-
toast.add({
|
|
136
|
-
title: 'Persona deleted',
|
|
137
|
-
description: detail.value?.name ?? '',
|
|
138
|
-
color: 'success',
|
|
139
|
-
})
|
|
140
|
-
emit('deleted', props.personaId)
|
|
141
|
-
emit('update:modelValue', false)
|
|
142
|
-
} catch (err) {
|
|
143
|
-
toast.add({
|
|
144
|
-
title: 'Delete failed',
|
|
145
|
-
description: err instanceof Error ? err.message : 'unknown error',
|
|
146
|
-
color: 'error',
|
|
147
|
-
})
|
|
148
|
-
} finally {
|
|
149
|
-
deleting.value = false
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function closeDrawer() {
|
|
154
|
-
if (editing.value && !saving.value) {
|
|
155
|
-
const ok = await confirmDialog({
|
|
156
|
-
title: 'Discard unsaved edits?',
|
|
157
|
-
description: 'Any changes you made will be lost.',
|
|
158
|
-
confirmLabel: 'Discard',
|
|
159
|
-
cancelLabel: 'Keep editing',
|
|
160
|
-
variant: 'danger',
|
|
161
|
-
})
|
|
162
|
-
if (!ok) return
|
|
163
|
-
}
|
|
164
|
-
cancelEdit()
|
|
165
|
-
emit('update:modelValue', false)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function listToCsv(list: string[] | undefined): string {
|
|
169
|
-
return (list ?? []).join(', ')
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function csvToList(value: string): string[] {
|
|
173
|
-
return value.split(',').map((s) => s.trim()).filter(Boolean)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const mbtiOptions = [
|
|
177
|
-
'INTJ', 'INTP', 'ENTJ', 'ENTP',
|
|
178
|
-
'INFJ', 'INFP', 'ENFJ', 'ENFP',
|
|
179
|
-
'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ',
|
|
180
|
-
'ISTP', 'ISFP', 'ESTP', 'ESFP',
|
|
181
|
-
].map((t) => ({ label: t, value: t }))
|
|
182
|
-
|
|
183
|
-
const discOptions = [
|
|
184
|
-
{ label: 'D — Dominance', value: 'D' },
|
|
185
|
-
{ label: 'I — Influence', value: 'I' },
|
|
186
|
-
{ label: 'S — Steadiness', value: 'S' },
|
|
187
|
-
{ label: 'C — Conscientiousness', value: 'C' },
|
|
188
|
-
]
|
|
189
|
-
|
|
190
|
-
const vocabOptions = [
|
|
191
|
-
{ label: 'Lay (no jargon)', value: 'lay' },
|
|
192
|
-
{ label: 'Specialist (industry terms)', value: 'specialist' },
|
|
193
|
-
{ label: 'Expert (research-level)', value: 'expert' },
|
|
194
|
-
]
|
|
195
|
-
|
|
196
|
-
// PR77 v2.95.0 — hero gradient by MBTI grouping + initials avatar
|
|
197
|
-
// + reverse-usage stat (how many agents link this persona).
|
|
198
|
-
|
|
199
|
-
const { fetchApi } = useApi()
|
|
200
|
-
const { data: usageData } = fetchApi<{
|
|
201
|
-
by_persona: Record<string, { agent_count: number, agent_ids: string[] }>
|
|
202
|
-
}>('/api/personas/usage')
|
|
203
|
-
|
|
204
|
-
const linkedAgentCount = computed(() => {
|
|
205
|
-
if (!detail.value?.id) return 0
|
|
206
|
-
return usageData.value?.by_persona?.[detail.value.id]?.agent_count ?? 0
|
|
207
|
-
})
|
|
208
|
-
const linkedAgentIds = computed(() => {
|
|
209
|
-
if (!detail.value?.id) return []
|
|
210
|
-
return usageData.value?.by_persona?.[detail.value.id]?.agent_ids ?? []
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
function heroInitials(name: string | undefined): string {
|
|
214
|
-
if (!name) return '·'
|
|
215
|
-
const parts = name.trim().split(/\s+/)
|
|
216
|
-
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
|
|
217
|
-
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function mbtiGradientClass(mbti: string | undefined): string {
|
|
221
|
-
if (!mbti) return 'bg-gradient-to-br from-muted/20 to-muted/5'
|
|
222
|
-
const code = mbti.toUpperCase()
|
|
223
|
-
if (['INTJ', 'INTP', 'ENTJ', 'ENTP'].includes(code))
|
|
224
|
-
return 'bg-gradient-to-br from-blue-500/30 to-indigo-600/10'
|
|
225
|
-
if (['INFJ', 'INFP', 'ENFJ', 'ENFP'].includes(code))
|
|
226
|
-
return 'bg-gradient-to-br from-emerald-500/30 to-teal-600/10'
|
|
227
|
-
if (['ISTJ', 'ISFJ', 'ESTJ', 'ESFJ'].includes(code))
|
|
228
|
-
return 'bg-gradient-to-br from-amber-500/30 to-orange-600/10'
|
|
229
|
-
if (['ISTP', 'ISFP', 'ESTP', 'ESFP'].includes(code))
|
|
230
|
-
return 'bg-gradient-to-br from-rose-500/30 to-pink-600/10'
|
|
231
|
-
return 'bg-gradient-to-br from-primary/20 to-primary/5'
|
|
232
|
-
}
|
|
233
|
-
</script>
|
|
234
|
-
|
|
235
|
-
<template>
|
|
236
|
-
<USlideover
|
|
237
|
-
:open="modelValue"
|
|
238
|
-
:ui="{ content: 'max-w-2xl w-full' }"
|
|
239
|
-
@update:open="(v) => v ? null : closeDrawer()"
|
|
240
|
-
>
|
|
241
|
-
<template #content>
|
|
242
|
-
<UCard
|
|
243
|
-
:ui="{
|
|
244
|
-
root: 'h-full flex flex-col rounded-none',
|
|
245
|
-
body: 'flex-1 overflow-y-auto',
|
|
246
|
-
}"
|
|
247
|
-
>
|
|
248
|
-
<template #header>
|
|
249
|
-
<div
|
|
250
|
-
class="-m-4 mb-0 p-5 rounded-t-lg"
|
|
251
|
-
:class="mbtiGradientClass(detail?.mbti)"
|
|
252
|
-
>
|
|
253
|
-
<div class="flex items-start justify-between gap-3">
|
|
254
|
-
<div class="flex items-start gap-4 min-w-0 flex-1">
|
|
255
|
-
<div class="shrink-0 size-14 rounded-xl bg-default/80 border border-default flex items-center justify-center shadow-md backdrop-blur-sm">
|
|
256
|
-
<span class="text-lg font-bold tracking-tight text-highlighted">
|
|
257
|
-
{{ heroInitials(detail?.name) }}
|
|
258
|
-
</span>
|
|
259
|
-
</div>
|
|
260
|
-
<div class="min-w-0 flex-1">
|
|
261
|
-
<h2 class="text-2xl font-bold truncate text-highlighted">
|
|
262
|
-
{{ detail?.name ?? 'Persona' }}
|
|
263
|
-
</h2>
|
|
264
|
-
<p v-if="detail?.title" class="text-sm text-muted truncate mt-0.5">
|
|
265
|
-
{{ detail.title }}
|
|
266
|
-
</p>
|
|
267
|
-
<div class="flex items-center gap-2 mt-2 flex-wrap">
|
|
268
|
-
<UBadge
|
|
269
|
-
v-if="detail?._source_store === 'obsidian'"
|
|
270
|
-
label="From Obsidian"
|
|
271
|
-
icon="i-lucide-file-text"
|
|
272
|
-
color="primary"
|
|
273
|
-
variant="subtle"
|
|
274
|
-
size="xs"
|
|
275
|
-
/>
|
|
276
|
-
<UBadge
|
|
277
|
-
v-else-if="detail?._source_store === 'json'"
|
|
278
|
-
label="JSON store"
|
|
279
|
-
variant="outline"
|
|
280
|
-
size="xs"
|
|
281
|
-
/>
|
|
282
|
-
<UBadge
|
|
283
|
-
v-if="detail?.mbti"
|
|
284
|
-
:label="detail.mbti"
|
|
285
|
-
variant="soft"
|
|
286
|
-
size="xs"
|
|
287
|
-
/>
|
|
288
|
-
<UBadge
|
|
289
|
-
v-if="linkedAgentCount > 0"
|
|
290
|
-
:label="`${linkedAgentCount} agent${linkedAgentCount === 1 ? '' : 's'}`"
|
|
291
|
-
color="primary"
|
|
292
|
-
variant="subtle"
|
|
293
|
-
size="xs"
|
|
294
|
-
/>
|
|
295
|
-
</div>
|
|
296
|
-
<p
|
|
297
|
-
v-if="detail?._obsidian_path"
|
|
298
|
-
class="text-[10px] text-muted/70 font-mono truncate mt-2"
|
|
299
|
-
:title="detail._obsidian_path"
|
|
300
|
-
>
|
|
301
|
-
{{ detail._obsidian_path.split('/').slice(-2).join('/') }}
|
|
302
|
-
</p>
|
|
303
|
-
</div>
|
|
304
|
-
</div>
|
|
305
|
-
<div class="flex items-center gap-1 shrink-0">
|
|
306
|
-
<UButton
|
|
307
|
-
v-if="!editing"
|
|
308
|
-
icon="i-lucide-pencil"
|
|
309
|
-
variant="ghost"
|
|
310
|
-
size="sm"
|
|
311
|
-
aria-label="Edit persona"
|
|
312
|
-
@click="startEdit"
|
|
313
|
-
/>
|
|
314
|
-
<UButton
|
|
315
|
-
v-if="!editing"
|
|
316
|
-
icon="i-lucide-trash-2"
|
|
317
|
-
color="error"
|
|
318
|
-
variant="ghost"
|
|
319
|
-
size="sm"
|
|
320
|
-
:loading="deleting"
|
|
321
|
-
aria-label="Delete persona"
|
|
322
|
-
@click="deletePersona"
|
|
323
|
-
/>
|
|
324
|
-
<UButton
|
|
325
|
-
icon="i-lucide-x"
|
|
326
|
-
variant="ghost"
|
|
327
|
-
size="sm"
|
|
328
|
-
aria-label="Close"
|
|
329
|
-
@click="closeDrawer"
|
|
330
|
-
/>
|
|
331
|
-
</div>
|
|
332
|
-
</div>
|
|
333
|
-
</div>
|
|
334
|
-
</template>
|
|
335
|
-
|
|
336
|
-
<div v-if="loading" class="flex items-center justify-center py-12">
|
|
337
|
-
<UIcon name="i-lucide-loader-2" class="size-8 animate-spin text-muted" />
|
|
338
|
-
</div>
|
|
339
|
-
|
|
340
|
-
<div v-else-if="loadError" class="flex flex-col items-center justify-center gap-3 py-12">
|
|
341
|
-
<UIcon name="i-lucide-alert-triangle" class="size-10 text-red-500" />
|
|
342
|
-
<p class="text-sm text-muted">{{ loadError }}</p>
|
|
343
|
-
</div>
|
|
344
|
-
|
|
345
|
-
<div v-else-if="detail && !editing" class="space-y-6">
|
|
346
|
-
<p v-if="detail.tagline" class="text-base italic text-muted">
|
|
347
|
-
"{{ detail.tagline }}"
|
|
348
|
-
</p>
|
|
349
|
-
|
|
350
|
-
<section v-if="detail.title || detail.source">
|
|
351
|
-
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted mb-2">Identity</h3>
|
|
352
|
-
<dl class="grid grid-cols-3 gap-2 text-sm">
|
|
353
|
-
<dt class="text-muted">Title</dt>
|
|
354
|
-
<dd class="col-span-2">{{ detail.title || '—' }}</dd>
|
|
355
|
-
<dt class="text-muted">Source</dt>
|
|
356
|
-
<dd class="col-span-2 font-mono text-xs">{{ detail.source || '—' }}</dd>
|
|
357
|
-
</dl>
|
|
358
|
-
</section>
|
|
359
|
-
|
|
360
|
-
<section>
|
|
361
|
-
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted mb-2">Behavioural DNA</h3>
|
|
362
|
-
<dl class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
|
|
363
|
-
<dt class="text-muted">MBTI</dt>
|
|
364
|
-
<dd>{{ detail.mbti || '—' }}</dd>
|
|
365
|
-
<dt class="text-muted">DISC</dt>
|
|
366
|
-
<dd>
|
|
367
|
-
{{ detail.disc?.primary || '—' }}{{ detail.disc?.secondary ? `/${detail.disc.secondary}` : '' }}
|
|
368
|
-
</dd>
|
|
369
|
-
<dt class="text-muted">Enneagram</dt>
|
|
370
|
-
<dd>
|
|
371
|
-
{{ detail.enneagram?.type ?? '—' }}w{{ detail.enneagram?.wing ?? '?' }}
|
|
372
|
-
</dd>
|
|
373
|
-
</dl>
|
|
374
|
-
<div class="mt-3 space-y-1.5">
|
|
375
|
-
<div
|
|
376
|
-
v-for="trait in ([
|
|
377
|
-
['Openness', detail.big_five?.openness ?? 0],
|
|
378
|
-
['Conscientiousness', detail.big_five?.conscientiousness ?? 0],
|
|
379
|
-
['Extraversion', detail.big_five?.extraversion ?? 0],
|
|
380
|
-
['Agreeableness', detail.big_five?.agreeableness ?? 0],
|
|
381
|
-
['Neuroticism', detail.big_five?.neuroticism ?? 0],
|
|
382
|
-
] as Array<[string, number]>)"
|
|
383
|
-
:key="trait[0]"
|
|
384
|
-
class="flex items-center gap-3"
|
|
385
|
-
>
|
|
386
|
-
<span class="text-xs text-muted w-36 shrink-0">{{ trait[0] }}</span>
|
|
387
|
-
<div class="flex-1 h-2 rounded-full bg-muted/15 overflow-hidden">
|
|
388
|
-
<div class="h-2 rounded-full bg-primary" :style="{ width: `${trait[1]}%` }" />
|
|
389
|
-
</div>
|
|
390
|
-
<span class="text-xs font-mono w-10 text-right">{{ trait[1] }}</span>
|
|
391
|
-
</div>
|
|
392
|
-
</div>
|
|
393
|
-
</section>
|
|
394
|
-
|
|
395
|
-
<section v-if="detail.mental_models?.length">
|
|
396
|
-
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted mb-2">
|
|
397
|
-
Mental models ({{ detail.mental_models.length }})
|
|
398
|
-
</h3>
|
|
399
|
-
<div class="flex flex-wrap gap-1.5">
|
|
400
|
-
<UBadge
|
|
401
|
-
v-for="m in detail.mental_models"
|
|
402
|
-
:key="m"
|
|
403
|
-
:label="m"
|
|
404
|
-
variant="outline"
|
|
405
|
-
size="xs"
|
|
406
|
-
/>
|
|
407
|
-
</div>
|
|
408
|
-
</section>
|
|
409
|
-
|
|
410
|
-
<section v-if="detail.expertise_domains?.length">
|
|
411
|
-
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted mb-2">
|
|
412
|
-
Expertise ({{ detail.expertise_domains.length }})
|
|
413
|
-
</h3>
|
|
414
|
-
<div class="flex flex-wrap gap-1.5">
|
|
415
|
-
<UBadge
|
|
416
|
-
v-for="e in detail.expertise_domains"
|
|
417
|
-
:key="e"
|
|
418
|
-
:label="e"
|
|
419
|
-
variant="soft"
|
|
420
|
-
size="xs"
|
|
421
|
-
/>
|
|
422
|
-
</div>
|
|
423
|
-
</section>
|
|
424
|
-
|
|
425
|
-
<section v-if="detail.frameworks?.length">
|
|
426
|
-
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted mb-2">
|
|
427
|
-
Frameworks ({{ detail.frameworks.length }})
|
|
428
|
-
</h3>
|
|
429
|
-
<div class="flex flex-wrap gap-1.5">
|
|
430
|
-
<UBadge
|
|
431
|
-
v-for="f in detail.frameworks"
|
|
432
|
-
:key="f"
|
|
433
|
-
:label="f"
|
|
434
|
-
variant="outline"
|
|
435
|
-
size="xs"
|
|
436
|
-
/>
|
|
437
|
-
</div>
|
|
438
|
-
</section>
|
|
439
|
-
|
|
440
|
-
<section v-if="detail.key_quotes?.length">
|
|
441
|
-
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted mb-2">
|
|
442
|
-
Key quotes ({{ detail.key_quotes.length }})
|
|
443
|
-
</h3>
|
|
444
|
-
<ul class="space-y-2">
|
|
445
|
-
<li
|
|
446
|
-
v-for="q in detail.key_quotes"
|
|
447
|
-
:key="q"
|
|
448
|
-
class="text-sm italic text-muted border-l-2 border-primary/30 pl-3"
|
|
449
|
-
>
|
|
450
|
-
"{{ q }}"
|
|
451
|
-
</li>
|
|
452
|
-
</ul>
|
|
453
|
-
</section>
|
|
454
|
-
|
|
455
|
-
<section v-if="detail.communication">
|
|
456
|
-
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted mb-2">Communication</h3>
|
|
457
|
-
<dl class="grid grid-cols-3 gap-2 text-sm">
|
|
458
|
-
<dt class="text-muted">Tone</dt>
|
|
459
|
-
<dd class="col-span-2">{{ detail.communication.tone || '—' }}</dd>
|
|
460
|
-
<dt class="text-muted">Vocabulary</dt>
|
|
461
|
-
<dd class="col-span-2">{{ detail.communication.vocabulary_level || '—' }}</dd>
|
|
462
|
-
</dl>
|
|
463
|
-
</section>
|
|
464
|
-
|
|
465
|
-
<!-- PR77 v2.95.0 — linked agents (reverse from /api/personas/usage) -->
|
|
466
|
-
<section v-if="linkedAgentCount > 0">
|
|
467
|
-
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted mb-2">
|
|
468
|
-
Linked to {{ linkedAgentCount }} agent{{ linkedAgentCount === 1 ? '' : 's' }}
|
|
469
|
-
</h3>
|
|
470
|
-
<div class="flex flex-wrap gap-1.5">
|
|
471
|
-
<NuxtLink
|
|
472
|
-
v-for="aid in linkedAgentIds"
|
|
473
|
-
:key="aid"
|
|
474
|
-
:to="`/agents/${aid}`"
|
|
475
|
-
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 text-xs font-mono hover:border-primary/40 hover:text-primary transition-colors"
|
|
476
|
-
>
|
|
477
|
-
<UIcon name="i-lucide-arrow-right" class="size-3" />
|
|
478
|
-
{{ aid }}
|
|
479
|
-
</NuxtLink>
|
|
480
|
-
</div>
|
|
481
|
-
</section>
|
|
482
|
-
</div>
|
|
483
|
-
|
|
484
|
-
<div v-else-if="draft" class="space-y-5">
|
|
485
|
-
<section class="space-y-3">
|
|
486
|
-
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Identity</h3>
|
|
487
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
488
|
-
<UFormField label="Name" required>
|
|
489
|
-
<UInput v-model="draft.name" class="w-full" />
|
|
490
|
-
</UFormField>
|
|
491
|
-
<UFormField label="Title">
|
|
492
|
-
<UInput v-model="draft.title" class="w-full" />
|
|
493
|
-
</UFormField>
|
|
494
|
-
<UFormField label="Source">
|
|
495
|
-
<UInput v-model="draft.source" class="w-full" />
|
|
496
|
-
</UFormField>
|
|
497
|
-
<UFormField label="Tagline">
|
|
498
|
-
<UInput v-model="draft.tagline" class="w-full" />
|
|
499
|
-
</UFormField>
|
|
500
|
-
</div>
|
|
501
|
-
</section>
|
|
502
|
-
|
|
503
|
-
<section class="space-y-3">
|
|
504
|
-
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Behavioural DNA</h3>
|
|
505
|
-
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
506
|
-
<UFormField label="MBTI">
|
|
507
|
-
<USelect v-model="draft.mbti" :items="mbtiOptions" class="w-full" />
|
|
508
|
-
</UFormField>
|
|
509
|
-
<UFormField label="DISC primary">
|
|
510
|
-
<USelect v-model="draft.disc.primary" :items="discOptions" class="w-full" />
|
|
511
|
-
</UFormField>
|
|
512
|
-
<UFormField label="Enneagram">
|
|
513
|
-
<UInput v-model.number="draft.enneagram.type" type="number" :min="1" :max="9" class="w-full" />
|
|
514
|
-
</UFormField>
|
|
515
|
-
<UFormField label="Wing">
|
|
516
|
-
<UInput v-model.number="draft.enneagram.wing" type="number" :min="1" :max="9" class="w-full" />
|
|
517
|
-
</UFormField>
|
|
518
|
-
</div>
|
|
519
|
-
<div class="space-y-2">
|
|
520
|
-
<div
|
|
521
|
-
v-for="key in (['openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism'] as const)"
|
|
522
|
-
:key="key"
|
|
523
|
-
class="flex items-center gap-3"
|
|
524
|
-
>
|
|
525
|
-
<label class="text-xs text-muted w-36 shrink-0 capitalize">{{ key }}</label>
|
|
526
|
-
<UInput
|
|
527
|
-
v-model.number="draft.big_five[key]"
|
|
528
|
-
type="number"
|
|
529
|
-
:min="0"
|
|
530
|
-
:max="100"
|
|
531
|
-
class="w-20"
|
|
532
|
-
/>
|
|
533
|
-
<input
|
|
534
|
-
v-model.number="draft.big_five[key]"
|
|
535
|
-
type="range"
|
|
536
|
-
:min="0"
|
|
537
|
-
:max="100"
|
|
538
|
-
class="flex-1"
|
|
539
|
-
/>
|
|
540
|
-
</div>
|
|
541
|
-
</div>
|
|
542
|
-
</section>
|
|
543
|
-
|
|
544
|
-
<section class="space-y-3">
|
|
545
|
-
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Knowledge</h3>
|
|
546
|
-
<UFormField label="Mental models" help="comma-separated">
|
|
547
|
-
<UInput
|
|
548
|
-
:model-value="listToCsv(draft.mental_models)"
|
|
549
|
-
@update:model-value="(v: string) => draft && (draft.mental_models = csvToList(v))"
|
|
550
|
-
class="w-full"
|
|
551
|
-
/>
|
|
552
|
-
</UFormField>
|
|
553
|
-
<UFormField label="Expertise domains" help="comma-separated">
|
|
554
|
-
<UInput
|
|
555
|
-
:model-value="listToCsv(draft.expertise_domains)"
|
|
556
|
-
@update:model-value="(v: string) => draft && (draft.expertise_domains = csvToList(v))"
|
|
557
|
-
class="w-full"
|
|
558
|
-
/>
|
|
559
|
-
</UFormField>
|
|
560
|
-
<UFormField label="Frameworks" help="comma-separated">
|
|
561
|
-
<UInput
|
|
562
|
-
:model-value="listToCsv(draft.frameworks)"
|
|
563
|
-
@update:model-value="(v: string) => draft && (draft.frameworks = csvToList(v))"
|
|
564
|
-
class="w-full"
|
|
565
|
-
/>
|
|
566
|
-
</UFormField>
|
|
567
|
-
</section>
|
|
568
|
-
|
|
569
|
-
<section class="space-y-3">
|
|
570
|
-
<h3 class="text-xs font-semibold uppercase tracking-wider text-muted">Communication</h3>
|
|
571
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
572
|
-
<UFormField label="Tone">
|
|
573
|
-
<UInput v-model="draft.communication.tone" class="w-full" />
|
|
574
|
-
</UFormField>
|
|
575
|
-
<UFormField label="Vocabulary level">
|
|
576
|
-
<USelect v-model="draft.communication.vocabulary_level" :items="vocabOptions" class="w-full" />
|
|
577
|
-
</UFormField>
|
|
578
|
-
</div>
|
|
579
|
-
</section>
|
|
580
|
-
</div>
|
|
581
|
-
|
|
582
|
-
<template #footer>
|
|
583
|
-
<div v-if="editing" class="flex justify-end gap-2">
|
|
584
|
-
<UButton label="Cancel" variant="ghost" :disabled="saving" @click="cancelEdit" />
|
|
585
|
-
<UButton
|
|
586
|
-
label="Save"
|
|
587
|
-
icon="i-lucide-check"
|
|
588
|
-
:loading="saving"
|
|
589
|
-
@click="saveEdit"
|
|
590
|
-
/>
|
|
591
|
-
</div>
|
|
592
|
-
<p v-else class="text-xs text-muted text-right">
|
|
593
|
-
Click ✏️ to edit. Saves to JSON store + Obsidian vault when configured.
|
|
594
|
-
</p>
|
|
595
|
-
</template>
|
|
596
|
-
</UCard>
|
|
597
|
-
</template>
|
|
598
|
-
</USlideover>
|
|
599
|
-
</template>
|