arkaos 3.70.6 → 3.70.8
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/departments/index.vue +1 -1
- package/dashboard/app/pages/personas/[id].vue +283 -6
- package/dashboard/app/pages/personas/compare.vue +133 -14
- package/dashboard/app/pages/personas/index.vue +3 -2
- package/dashboard/app/pages/workflows.vue +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/dashboard/app/pages/personas.vue +0 -719
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.70.
|
|
1
|
+
3.70.8
|
|
@@ -89,7 +89,7 @@ function formatCost(cost: number | null): string {
|
|
|
89
89
|
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
|
|
90
90
|
td: 'border-b border-default',
|
|
91
91
|
}"
|
|
92
|
-
@select="(row: { original: DeptRow }) => navigateTo(`/departments/${row.original.department}`)"
|
|
92
|
+
@select="(_e: Event, row: { original: DeptRow }) => row?.original && navigateTo(`/departments/${row.original.department}`)"
|
|
93
93
|
>
|
|
94
94
|
<template #department-cell="{ row }">
|
|
95
95
|
<span class="font-semibold capitalize">{{ row.original.department }}</span>
|
|
@@ -308,10 +308,48 @@ function markedHtml(src: string): string {
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
// PR90a v3.31.0 — download persona as Markdown.
|
|
311
|
-
|
|
311
|
+
// v3.70.8 — instead of triggering a download, open a fullscreen
|
|
312
|
+
// dialog with raw markdown + live preview + Edit Bio + Download.
|
|
313
|
+
import { marked } from 'marked'
|
|
314
|
+
|
|
315
|
+
const mdOpen = ref(false)
|
|
316
|
+
const mdLoading = ref(false)
|
|
317
|
+
const mdRaw = ref('')
|
|
318
|
+
const mdMode = ref<'view' | 'edit'>('view')
|
|
319
|
+
const mdBioDraft = ref('')
|
|
320
|
+
const mdSaving = ref(false)
|
|
321
|
+
|
|
322
|
+
async function openMdViewer() {
|
|
323
|
+
if (!detail.value) return
|
|
324
|
+
mdOpen.value = true
|
|
325
|
+
mdMode.value = 'view'
|
|
326
|
+
mdBioDraft.value = detail.value.bio_md || ''
|
|
327
|
+
if (mdRaw.value) return
|
|
328
|
+
mdLoading.value = true
|
|
329
|
+
try {
|
|
330
|
+
const text = await $fetch<string>(
|
|
331
|
+
`${apiBase}/api/personas/${personaId}/markdown`,
|
|
332
|
+
{ responseType: 'text' },
|
|
333
|
+
)
|
|
334
|
+
mdRaw.value = typeof text === 'string' ? text : String(text ?? '')
|
|
335
|
+
} catch (err) {
|
|
336
|
+
toast.add({
|
|
337
|
+
title: 'Markdown fetch failed',
|
|
338
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
339
|
+
color: 'error',
|
|
340
|
+
})
|
|
341
|
+
mdOpen.value = false
|
|
342
|
+
} finally {
|
|
343
|
+
mdLoading.value = false
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function closeMdViewer() {
|
|
348
|
+
mdOpen.value = false
|
|
349
|
+
}
|
|
350
|
+
|
|
312
351
|
async function downloadMarkdown() {
|
|
313
352
|
if (!detail.value) return
|
|
314
|
-
downloadingMd.value = true
|
|
315
353
|
try {
|
|
316
354
|
const blob = await $fetch<Blob>(
|
|
317
355
|
`${apiBase}/api/personas/${personaId}/markdown`,
|
|
@@ -337,11 +375,60 @@ async function downloadMarkdown() {
|
|
|
337
375
|
description: err instanceof Error ? err.message : 'unknown error',
|
|
338
376
|
color: 'error',
|
|
339
377
|
})
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function copyMd() {
|
|
382
|
+
if (!mdRaw.value) return
|
|
383
|
+
try {
|
|
384
|
+
await navigator.clipboard.writeText(mdRaw.value)
|
|
385
|
+
toast.add({ title: 'Copied to clipboard', color: 'success', icon: 'i-lucide-check' })
|
|
386
|
+
} catch {
|
|
387
|
+
toast.add({ title: 'Copy failed', color: 'error' })
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const mdPreviewHtml = computed<string>(() => {
|
|
392
|
+
const source = mdMode.value === 'edit' ? mdBioDraft.value : mdRaw.value
|
|
393
|
+
if (!source) return ''
|
|
394
|
+
try {
|
|
395
|
+
return marked.parse(source, { breaks: true, gfm: true }) as string
|
|
396
|
+
} catch {
|
|
397
|
+
return ''
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
async function saveBio() {
|
|
402
|
+
if (!detail.value) return
|
|
403
|
+
mdSaving.value = true
|
|
404
|
+
try {
|
|
405
|
+
await $fetch(`${apiBase}/api/personas/${personaId}`, {
|
|
406
|
+
method: 'PUT',
|
|
407
|
+
body: { bio_md: mdBioDraft.value },
|
|
408
|
+
})
|
|
409
|
+
toast.add({ title: 'Bio updated', color: 'success', icon: 'i-lucide-check' })
|
|
410
|
+
mdRaw.value = '' // force refetch on next open so the export reflects the new bio
|
|
411
|
+
mdMode.value = 'view'
|
|
412
|
+
await refresh()
|
|
413
|
+
// Re-pull the exported MD now that the bio changed.
|
|
414
|
+
const text = await $fetch<string>(
|
|
415
|
+
`${apiBase}/api/personas/${personaId}/markdown`,
|
|
416
|
+
{ responseType: 'text' },
|
|
417
|
+
)
|
|
418
|
+
mdRaw.value = typeof text === 'string' ? text : String(text ?? '')
|
|
419
|
+
} catch (err) {
|
|
420
|
+
toast.add({
|
|
421
|
+
title: 'Save failed',
|
|
422
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
423
|
+
color: 'error',
|
|
424
|
+
})
|
|
340
425
|
} finally {
|
|
341
|
-
|
|
426
|
+
mdSaving.value = false
|
|
342
427
|
}
|
|
343
428
|
}
|
|
344
429
|
|
|
430
|
+
const downloadingMd = ref(false) // kept for the old button binding
|
|
431
|
+
|
|
345
432
|
// PR85a v3.11.0 — Clone to Agent dialog.
|
|
346
433
|
const cloneOpen = ref(false)
|
|
347
434
|
function onCloned(agentId: string) {
|
|
@@ -689,11 +776,10 @@ const vocabOptions = [
|
|
|
689
776
|
/>
|
|
690
777
|
<UButton
|
|
691
778
|
label="MD"
|
|
692
|
-
icon="i-lucide-
|
|
779
|
+
icon="i-lucide-file-text"
|
|
693
780
|
variant="ghost"
|
|
694
781
|
size="sm"
|
|
695
|
-
|
|
696
|
-
@click="downloadMarkdown"
|
|
782
|
+
@click="openMdViewer"
|
|
697
783
|
/>
|
|
698
784
|
<UButton label="Edit" icon="i-lucide-pencil" size="sm" @click="startEdit" />
|
|
699
785
|
<UButton
|
|
@@ -1265,5 +1351,196 @@ const vocabOptions = [
|
|
|
1265
1351
|
</div>
|
|
1266
1352
|
</DashboardState>
|
|
1267
1353
|
</template>
|
|
1354
|
+
|
|
1355
|
+
<!-- v3.70.8 — Markdown viewer/editor (fullscreen) -->
|
|
1356
|
+
<UModal
|
|
1357
|
+
v-model:open="mdOpen"
|
|
1358
|
+
:ui="{
|
|
1359
|
+
overlay: 'bg-default/80 backdrop-blur-sm',
|
|
1360
|
+
content: 'sm:max-w-[95vw] w-[95vw] h-[92vh] sm:rounded-xl ring-0 shadow-2xl',
|
|
1361
|
+
}"
|
|
1362
|
+
>
|
|
1363
|
+
<template #content>
|
|
1364
|
+
<div class="flex flex-col h-full bg-default rounded-xl overflow-hidden">
|
|
1365
|
+
<div class="flex items-center gap-3 px-4 py-3 border-b border-default/60 shrink-0">
|
|
1366
|
+
<UIcon name="i-lucide-file-text" class="size-4 text-muted shrink-0" />
|
|
1367
|
+
<span class="text-sm font-semibold">{{ detail?.name || personaId }}.md</span>
|
|
1368
|
+
<UBadge
|
|
1369
|
+
:label="mdMode === 'edit' ? 'editing bio' : 'preview'"
|
|
1370
|
+
:color="mdMode === 'edit' ? 'warning' : 'neutral'"
|
|
1371
|
+
variant="subtle"
|
|
1372
|
+
size="xs"
|
|
1373
|
+
class="ml-1"
|
|
1374
|
+
/>
|
|
1375
|
+
<span class="ml-auto flex items-center gap-1">
|
|
1376
|
+
<UButton
|
|
1377
|
+
v-if="mdMode === 'view'"
|
|
1378
|
+
size="sm"
|
|
1379
|
+
variant="ghost"
|
|
1380
|
+
icon="i-lucide-clipboard"
|
|
1381
|
+
@click="copyMd"
|
|
1382
|
+
>
|
|
1383
|
+
Copy
|
|
1384
|
+
</UButton>
|
|
1385
|
+
<UButton
|
|
1386
|
+
v-if="mdMode === 'view'"
|
|
1387
|
+
size="sm"
|
|
1388
|
+
variant="ghost"
|
|
1389
|
+
icon="i-lucide-download"
|
|
1390
|
+
@click="downloadMarkdown"
|
|
1391
|
+
>
|
|
1392
|
+
Download
|
|
1393
|
+
</UButton>
|
|
1394
|
+
<UButton
|
|
1395
|
+
v-if="mdMode === 'view'"
|
|
1396
|
+
size="sm"
|
|
1397
|
+
variant="soft"
|
|
1398
|
+
icon="i-lucide-pencil"
|
|
1399
|
+
@click="mdMode = 'edit'"
|
|
1400
|
+
>
|
|
1401
|
+
Edit bio
|
|
1402
|
+
</UButton>
|
|
1403
|
+
<UButton
|
|
1404
|
+
v-else
|
|
1405
|
+
size="sm"
|
|
1406
|
+
variant="ghost"
|
|
1407
|
+
@click="() => { mdMode = 'view'; mdBioDraft = detail?.bio_md || '' }"
|
|
1408
|
+
>
|
|
1409
|
+
Cancel
|
|
1410
|
+
</UButton>
|
|
1411
|
+
<UButton
|
|
1412
|
+
v-if="mdMode === 'edit'"
|
|
1413
|
+
size="sm"
|
|
1414
|
+
color="primary"
|
|
1415
|
+
icon="i-lucide-check"
|
|
1416
|
+
:loading="mdSaving"
|
|
1417
|
+
@click="saveBio"
|
|
1418
|
+
>
|
|
1419
|
+
Save bio
|
|
1420
|
+
</UButton>
|
|
1421
|
+
<UButton
|
|
1422
|
+
size="sm"
|
|
1423
|
+
variant="ghost"
|
|
1424
|
+
icon="i-lucide-x"
|
|
1425
|
+
aria-label="Close"
|
|
1426
|
+
@click="closeMdViewer"
|
|
1427
|
+
/>
|
|
1428
|
+
</span>
|
|
1429
|
+
</div>
|
|
1430
|
+
|
|
1431
|
+
<div
|
|
1432
|
+
v-if="mdLoading"
|
|
1433
|
+
class="flex-1 grid place-items-center text-sm text-muted"
|
|
1434
|
+
>
|
|
1435
|
+
<div class="flex items-center gap-2">
|
|
1436
|
+
<UIcon name="i-lucide-loader-2" class="size-4 animate-spin" />
|
|
1437
|
+
Loading markdown…
|
|
1438
|
+
</div>
|
|
1439
|
+
</div>
|
|
1440
|
+
|
|
1441
|
+
<div v-else class="flex-1 grid grid-cols-2 min-h-0">
|
|
1442
|
+
<!-- Source side: read-only in view mode, textarea in edit mode -->
|
|
1443
|
+
<div class="border-r border-default/60 flex flex-col min-h-0">
|
|
1444
|
+
<div class="px-3 py-1.5 border-b border-default/60 text-[11px] uppercase tracking-wide text-muted shrink-0">
|
|
1445
|
+
{{ mdMode === 'edit' ? 'Bio (editable, supports Markdown)' : 'Source' }}
|
|
1446
|
+
</div>
|
|
1447
|
+
<textarea
|
|
1448
|
+
v-if="mdMode === 'edit'"
|
|
1449
|
+
v-model="mdBioDraft"
|
|
1450
|
+
class="md-editor flex-1 w-full p-4 bg-transparent text-default font-mono text-sm leading-relaxed resize-none focus:outline-none"
|
|
1451
|
+
spellcheck="false"
|
|
1452
|
+
placeholder="Write the persona bio here…"
|
|
1453
|
+
/>
|
|
1454
|
+
<pre
|
|
1455
|
+
v-else
|
|
1456
|
+
class="flex-1 min-h-0 overflow-auto p-4 font-mono text-sm leading-relaxed text-default whitespace-pre-wrap"
|
|
1457
|
+
>{{ mdRaw }}</pre>
|
|
1458
|
+
</div>
|
|
1459
|
+
|
|
1460
|
+
<!-- Preview side -->
|
|
1461
|
+
<div class="flex flex-col min-h-0">
|
|
1462
|
+
<div class="px-3 py-1.5 border-b border-default/60 text-[11px] uppercase tracking-wide text-muted shrink-0">
|
|
1463
|
+
Preview
|
|
1464
|
+
</div>
|
|
1465
|
+
<div
|
|
1466
|
+
class="md-preview flex-1 min-h-0 overflow-auto p-6 prose prose-sm prose-invert max-w-none"
|
|
1467
|
+
v-html="mdPreviewHtml"
|
|
1468
|
+
/>
|
|
1469
|
+
</div>
|
|
1470
|
+
</div>
|
|
1471
|
+
</div>
|
|
1472
|
+
</template>
|
|
1473
|
+
</UModal>
|
|
1268
1474
|
</UDashboardPanel>
|
|
1269
1475
|
</template>
|
|
1476
|
+
|
|
1477
|
+
<style scoped>
|
|
1478
|
+
.md-editor:focus,
|
|
1479
|
+
.md-editor:focus-visible {
|
|
1480
|
+
outline: none !important;
|
|
1481
|
+
box-shadow: none !important;
|
|
1482
|
+
}
|
|
1483
|
+
.md-preview :deep(h1),
|
|
1484
|
+
.md-preview :deep(h2),
|
|
1485
|
+
.md-preview :deep(h3) {
|
|
1486
|
+
font-weight: 600;
|
|
1487
|
+
margin-top: 1.25em;
|
|
1488
|
+
margin-bottom: 0.5em;
|
|
1489
|
+
color: rgb(var(--ui-text));
|
|
1490
|
+
}
|
|
1491
|
+
.md-preview :deep(h1) { font-size: 1.5rem; }
|
|
1492
|
+
.md-preview :deep(h2) { font-size: 1.25rem; }
|
|
1493
|
+
.md-preview :deep(h3) { font-size: 1.05rem; }
|
|
1494
|
+
.md-preview :deep(p) { margin: 0.5em 0; line-height: 1.6; }
|
|
1495
|
+
.md-preview :deep(ul),
|
|
1496
|
+
.md-preview :deep(ol) { padding-left: 1.25rem; margin: 0.5em 0; }
|
|
1497
|
+
.md-preview :deep(li) { margin: 0.2em 0; }
|
|
1498
|
+
.md-preview :deep(code) {
|
|
1499
|
+
font-family: ui-monospace, SFMono-Regular, monospace;
|
|
1500
|
+
background-color: rgb(var(--ui-bg-elevated) / 0.6);
|
|
1501
|
+
padding: 0.1em 0.35em;
|
|
1502
|
+
border-radius: 4px;
|
|
1503
|
+
font-size: 0.875em;
|
|
1504
|
+
}
|
|
1505
|
+
.md-preview :deep(pre) {
|
|
1506
|
+
background-color: rgb(var(--ui-bg-elevated) / 0.4);
|
|
1507
|
+
padding: 0.75rem 1rem;
|
|
1508
|
+
border-radius: 8px;
|
|
1509
|
+
overflow-x: auto;
|
|
1510
|
+
margin: 0.75em 0;
|
|
1511
|
+
}
|
|
1512
|
+
.md-preview :deep(pre code) {
|
|
1513
|
+
background: transparent;
|
|
1514
|
+
padding: 0;
|
|
1515
|
+
}
|
|
1516
|
+
.md-preview :deep(blockquote) {
|
|
1517
|
+
border-left: 3px solid rgb(var(--ui-primary) / 0.5);
|
|
1518
|
+
padding-left: 0.75rem;
|
|
1519
|
+
margin: 0.75em 0;
|
|
1520
|
+
color: rgb(var(--ui-text-muted));
|
|
1521
|
+
}
|
|
1522
|
+
.md-preview :deep(a) {
|
|
1523
|
+
color: rgb(var(--ui-primary));
|
|
1524
|
+
text-decoration: underline;
|
|
1525
|
+
text-underline-offset: 2px;
|
|
1526
|
+
}
|
|
1527
|
+
.md-preview :deep(table) {
|
|
1528
|
+
border-collapse: collapse;
|
|
1529
|
+
margin: 0.75em 0;
|
|
1530
|
+
font-size: 0.875em;
|
|
1531
|
+
}
|
|
1532
|
+
.md-preview :deep(th),
|
|
1533
|
+
.md-preview :deep(td) {
|
|
1534
|
+
border: 1px solid rgb(var(--ui-border));
|
|
1535
|
+
padding: 0.4em 0.7em;
|
|
1536
|
+
}
|
|
1537
|
+
.md-preview :deep(th) {
|
|
1538
|
+
background-color: rgb(var(--ui-bg-elevated) / 0.4);
|
|
1539
|
+
font-weight: 600;
|
|
1540
|
+
}
|
|
1541
|
+
.md-preview :deep(hr) {
|
|
1542
|
+
border: none;
|
|
1543
|
+
border-top: 1px solid rgb(var(--ui-border));
|
|
1544
|
+
margin: 1.25em 0;
|
|
1545
|
+
}
|
|
1546
|
+
</style>
|
|
@@ -1,16 +1,51 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// PR96c v3.57.0 — Compare two personas side-by-side.
|
|
3
|
+
// v3.70.8 — picker UI when query params are missing (was blank before).
|
|
3
4
|
//
|
|
4
|
-
// Driven by `?a=p1&b=p2`.
|
|
5
|
-
//
|
|
5
|
+
// Driven by `?a=p1&b=p2`. The page renders pickers when either id is
|
|
6
|
+
// missing so the operator can land on /personas/compare directly.
|
|
6
7
|
|
|
7
8
|
const route = useRoute()
|
|
9
|
+
const router = useRouter()
|
|
8
10
|
const { fetchApi } = useApi()
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
interface PersonaSummary {
|
|
13
|
+
id: string
|
|
14
|
+
name: string
|
|
15
|
+
title?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { data: listData, status: listStatus } = fetchApi<{ personas: PersonaSummary[] }>(
|
|
19
|
+
'/api/personas',
|
|
20
|
+
)
|
|
21
|
+
const personaList = computed(() => listData.value?.personas ?? [])
|
|
22
|
+
const loadingList = computed(() => listStatus.value === 'pending')
|
|
23
|
+
|
|
24
|
+
const personaOptions = computed(() =>
|
|
25
|
+
personaList.value.map((p) => ({
|
|
26
|
+
label: p.name + (p.title ? ` — ${p.title}` : ''),
|
|
27
|
+
value: p.id,
|
|
28
|
+
})),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const leftId = computed(() => String(route.query.a ?? '').trim())
|
|
32
|
+
const rightId = computed(() => String(route.query.b ?? '').trim())
|
|
33
|
+
|
|
34
|
+
function setLeft(id: string) {
|
|
35
|
+
router.replace({ query: { ...route.query, a: id || undefined } })
|
|
36
|
+
}
|
|
37
|
+
function setRight(id: string) {
|
|
38
|
+
router.replace({ query: { ...route.query, b: id || undefined } })
|
|
39
|
+
}
|
|
40
|
+
function swapSides() {
|
|
41
|
+
router.replace({
|
|
42
|
+
query: { ...route.query, a: rightId.value || undefined, b: leftId.value || undefined },
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ids = computed<string[]>(() =>
|
|
47
|
+
[leftId.value, rightId.value].filter(Boolean).slice(0, 2),
|
|
48
|
+
)
|
|
14
49
|
|
|
15
50
|
interface PersonaDetail {
|
|
16
51
|
id: string
|
|
@@ -46,11 +81,11 @@ const { data: b, status: bStatus } = fetchApi<PersonaDetail>(
|
|
|
46
81
|
|
|
47
82
|
const loading = computed(() => aStatus.value === 'pending' || bStatus.value === 'pending')
|
|
48
83
|
const errorMsg = computed(() => {
|
|
49
|
-
if (ids.value.length < 2) return 'Pass two persona ids via ?a=p1&b=p2'
|
|
50
84
|
if (a.value?.error) return `Left: ${a.value.error}`
|
|
51
85
|
if (b.value?.error) return `Right: ${b.value.error}`
|
|
52
86
|
return null
|
|
53
87
|
})
|
|
88
|
+
const bothSelected = computed(() => leftId.value && rightId.value)
|
|
54
89
|
|
|
55
90
|
function diffClass(left: unknown, right: unknown): string {
|
|
56
91
|
return left !== right ? 'bg-yellow-500/10 border-yellow-500/30' : ''
|
|
@@ -78,13 +113,95 @@ const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeable
|
|
|
78
113
|
</template>
|
|
79
114
|
|
|
80
115
|
<template #body>
|
|
81
|
-
<div
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
116
|
+
<div class="p-4 space-y-4">
|
|
117
|
+
<!-- Empty state: no personas at all -->
|
|
118
|
+
<div
|
|
119
|
+
v-if="!loadingList && personaList.length === 0"
|
|
120
|
+
class="rounded-xl border border-default p-12 text-center"
|
|
121
|
+
>
|
|
122
|
+
<UIcon name="i-lucide-users" class="size-10 mx-auto mb-3 text-muted opacity-50" />
|
|
123
|
+
<p class="text-default font-medium">No personas to compare yet.</p>
|
|
124
|
+
<p class="text-sm text-muted mt-1">
|
|
125
|
+
Create at least two personas before using the compare view.
|
|
126
|
+
</p>
|
|
127
|
+
<UButton to="/personas" class="mt-4" icon="i-lucide-arrow-left">
|
|
128
|
+
Back to personas
|
|
129
|
+
</UButton>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<!-- Empty state: only 1 persona -->
|
|
133
|
+
<div
|
|
134
|
+
v-else-if="!loadingList && personaList.length < 2"
|
|
135
|
+
class="rounded-xl border border-default p-12 text-center"
|
|
136
|
+
>
|
|
137
|
+
<UIcon name="i-lucide-users" class="size-10 mx-auto mb-3 text-muted opacity-50" />
|
|
138
|
+
<p class="text-default font-medium">You need at least 2 personas.</p>
|
|
139
|
+
<p class="text-sm text-muted mt-1">
|
|
140
|
+
Currently {{ personaList.length }} persona in the system.
|
|
141
|
+
</p>
|
|
142
|
+
<UButton to="/personas/new" class="mt-4" icon="i-lucide-plus">
|
|
143
|
+
Create another persona
|
|
144
|
+
</UButton>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<!-- Pickers (always visible when we have 2+ personas) -->
|
|
148
|
+
<div v-else class="space-y-4">
|
|
149
|
+
<div class="grid grid-cols-[1fr_auto_1fr] gap-2 items-end">
|
|
150
|
+
<div>
|
|
151
|
+
<label class="text-xs text-muted uppercase tracking-wide mb-1 block">Left</label>
|
|
152
|
+
<USelectMenu
|
|
153
|
+
:model-value="leftId"
|
|
154
|
+
:items="personaOptions"
|
|
155
|
+
placeholder="Pick a persona…"
|
|
156
|
+
value-key="value"
|
|
157
|
+
class="w-full"
|
|
158
|
+
@update:model-value="(v: any) => setLeft(typeof v === 'string' ? v : (v?.value ?? ''))"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
<UButton
|
|
162
|
+
icon="i-lucide-arrow-left-right"
|
|
163
|
+
variant="ghost"
|
|
164
|
+
size="sm"
|
|
165
|
+
class="mb-0.5"
|
|
166
|
+
:disabled="!bothSelected"
|
|
167
|
+
title="Swap sides"
|
|
168
|
+
@click="swapSides"
|
|
169
|
+
/>
|
|
170
|
+
<div>
|
|
171
|
+
<label class="text-xs text-muted uppercase tracking-wide mb-1 block">Right</label>
|
|
172
|
+
<USelectMenu
|
|
173
|
+
:model-value="rightId"
|
|
174
|
+
:items="personaOptions"
|
|
175
|
+
placeholder="Pick a persona…"
|
|
176
|
+
value-key="value"
|
|
177
|
+
class="w-full"
|
|
178
|
+
@update:model-value="(v: any) => setRight(typeof v === 'string' ? v : (v?.value ?? ''))"
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<!-- Hint when not both selected -->
|
|
184
|
+
<div
|
|
185
|
+
v-if="!bothSelected"
|
|
186
|
+
class="rounded-xl border border-default border-dashed p-10 text-center text-sm text-muted"
|
|
187
|
+
>
|
|
188
|
+
<UIcon name="i-lucide-columns-2" class="size-7 mx-auto mb-2 opacity-50" />
|
|
189
|
+
Pick two personas above to see the side-by-side diff.
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- Error from detail fetch -->
|
|
193
|
+
<div v-else-if="errorMsg" class="p-4 text-sm text-error border border-error/30 rounded-lg">
|
|
194
|
+
{{ errorMsg }}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<!-- Loading detail -->
|
|
198
|
+
<div v-else-if="loading" class="p-6 text-center text-sm text-muted">
|
|
199
|
+
<UIcon name="i-lucide-loader-2" class="size-4 animate-spin inline mr-2" />
|
|
200
|
+
Loading personas…
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<!-- Comparison content -->
|
|
204
|
+
<div v-else-if="a && b" class="space-y-4 max-w-6xl">
|
|
88
205
|
<section class="grid grid-cols-2 gap-3">
|
|
89
206
|
<NuxtLink :to="`/personas/${a.id}`" class="rounded-lg border border-default p-4 hover:border-primary/40">
|
|
90
207
|
<p class="text-xs text-muted uppercase tracking-wide">Left</p>
|
|
@@ -207,6 +324,8 @@ const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeable
|
|
|
207
324
|
<p class="text-xs text-muted pt-4 italic">
|
|
208
325
|
Yellow tint = different. Red removed, green added.
|
|
209
326
|
</p>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
210
329
|
</div>
|
|
211
330
|
</template>
|
|
212
331
|
</UDashboardPanel>
|
|
@@ -133,7 +133,8 @@ const columns: TableColumn<Persona>[] = [
|
|
|
133
133
|
{ id: 'actions', header: '' },
|
|
134
134
|
]
|
|
135
135
|
|
|
136
|
-
function goToPersona(id: string) {
|
|
136
|
+
function goToPersona(id: string | undefined | null) {
|
|
137
|
+
if (!id) return
|
|
137
138
|
navigateTo(`/personas/${id}`)
|
|
138
139
|
}
|
|
139
140
|
|
|
@@ -617,7 +618,7 @@ async function undoTrashIds(ids: string[]) {
|
|
|
617
618
|
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
|
|
618
619
|
td: 'border-b border-default',
|
|
619
620
|
}"
|
|
620
|
-
@select="(row: { original: Persona }) => goToPersona(row
|
|
621
|
+
@select="(_e: Event, row: { original: Persona }) => goToPersona(row?.original?.id)"
|
|
621
622
|
>
|
|
622
623
|
<template #select-header>
|
|
623
624
|
<UCheckbox
|
|
@@ -241,7 +241,7 @@ const columns: TableColumn<Workflow>[] = [
|
|
|
241
241
|
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
|
|
242
242
|
td: 'border-b border-default',
|
|
243
243
|
}"
|
|
244
|
-
@select="(row: { original: Workflow }) => { selected = row.original; sidePanelTab = 'flow'; runs = []; editingYaml = false; loadRuns(row.original.id) }"
|
|
244
|
+
@select="(_e: Event, row: { original: Workflow }) => { if (!row?.original) return; selected = row.original; sidePanelTab = 'flow'; runs = []; editingYaml = false; loadRuns(row.original.id) }"
|
|
245
245
|
>
|
|
246
246
|
<template #name-cell="{ row }">
|
|
247
247
|
<div class="min-w-0">
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -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>
|