arkaos 2.78.0 → 2.79.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
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.79.0
|
|
Binary file
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Persona } from '~/types'
|
|
3
|
+
|
|
4
|
+
// PR62 v2.79.0 — 4-step AI-powered persona wizard.
|
|
5
|
+
// Replaces the original "fill a 30-field form" UX with:
|
|
6
|
+
// 1. Sources → operator pastes URLs / picks files, optionally types a name
|
|
7
|
+
// 2. Ingest → existing /api/knowledge/ingest-bulk fans the URLs out into
|
|
8
|
+
// background jobs and shows progress via WebSocket
|
|
9
|
+
// 3. Build → /api/personas/build (PR57) reads the indexed chunks and
|
|
10
|
+
// returns a draft Persona; operator reviews + edits
|
|
11
|
+
// 4. Save → POST /api/personas (manual create path), optional
|
|
12
|
+
// clone-to-agent at the end
|
|
13
|
+
//
|
|
14
|
+
// The wizard NEVER auto-saves — every transition is operator-confirmed.
|
|
15
|
+
|
|
16
|
+
const { apiBase } = useApi()
|
|
17
|
+
const toast = useToast()
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
(e: 'completed', persona: Persona): void
|
|
21
|
+
(e: 'cancelled'): void
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
type Step = 1 | 2 | 3 | 4
|
|
25
|
+
const step = ref<Step>(1)
|
|
26
|
+
|
|
27
|
+
// ─── Step 1 state ────────────────────────────────────────────────────────
|
|
28
|
+
const name = ref('')
|
|
29
|
+
const sourceLabel = ref('')
|
|
30
|
+
const sources = ref('')
|
|
31
|
+
const skipIngest = ref(false)
|
|
32
|
+
const sourceLineCount = computed(() =>
|
|
33
|
+
sources.value
|
|
34
|
+
.split('\n')
|
|
35
|
+
.map((s) => s.trim())
|
|
36
|
+
.filter((s) => s.length > 0)
|
|
37
|
+
.length,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// ─── Step 2 state ────────────────────────────────────────────────────────
|
|
41
|
+
const ingestJobs = ref<Array<{
|
|
42
|
+
source: string
|
|
43
|
+
job_id?: string
|
|
44
|
+
status: 'queued' | 'processing' | 'completed' | 'failed'
|
|
45
|
+
progress: number
|
|
46
|
+
error?: string
|
|
47
|
+
}>>([])
|
|
48
|
+
let ws: WebSocket | null = null
|
|
49
|
+
const allIngestComplete = computed(() =>
|
|
50
|
+
ingestJobs.value.length > 0
|
|
51
|
+
&& ingestJobs.value.every((j) => j.status === 'completed' || j.status === 'failed'),
|
|
52
|
+
)
|
|
53
|
+
const ingestCompletedCount = computed(() =>
|
|
54
|
+
ingestJobs.value.filter((j) => j.status === 'completed').length,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// ─── Step 3 state ────────────────────────────────────────────────────────
|
|
58
|
+
const draft = ref<Persona | null>(null)
|
|
59
|
+
const building = ref(false)
|
|
60
|
+
const buildError = ref<string | null>(null)
|
|
61
|
+
const chunksUsed = ref<number | null>(null)
|
|
62
|
+
|
|
63
|
+
// ─── Step 4 state ────────────────────────────────────────────────────────
|
|
64
|
+
const saving = ref(false)
|
|
65
|
+
const saveAndClone = ref(false)
|
|
66
|
+
const cloneDept = ref('strategy')
|
|
67
|
+
const cloneTier = ref<'1' | '2' | '3'>('2')
|
|
68
|
+
|
|
69
|
+
const departmentOptions = [
|
|
70
|
+
'dev', 'marketing', 'brand', 'finance', 'strategy', 'ecom', 'kb', 'ops',
|
|
71
|
+
'pm', 'saas', 'landing', 'content', 'community', 'sales', 'leadership', 'org',
|
|
72
|
+
].map((d) => ({ label: d, value: d }))
|
|
73
|
+
|
|
74
|
+
const tierOptions = [
|
|
75
|
+
{ label: 'Tier 1 — Squad Lead', value: '1' },
|
|
76
|
+
{ label: 'Tier 2 — Specialist', value: '2' },
|
|
77
|
+
{ label: 'Tier 3 — Support', value: '3' },
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
// ─── Step 1 → 2 transition ───────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async function startIngest() {
|
|
84
|
+
if (skipIngest.value) {
|
|
85
|
+
// Jump straight to step 3 — operator says content is already indexed.
|
|
86
|
+
step.value = 3
|
|
87
|
+
await runBuild()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
const cleaned = sources.value
|
|
91
|
+
.split('\n')
|
|
92
|
+
.map((s) => s.trim())
|
|
93
|
+
.filter((s) => s.length > 0)
|
|
94
|
+
if (cleaned.length === 0 || !name.value.trim()) return
|
|
95
|
+
step.value = 2
|
|
96
|
+
ingestJobs.value = cleaned.map((source) => ({
|
|
97
|
+
source,
|
|
98
|
+
status: 'queued',
|
|
99
|
+
progress: 0,
|
|
100
|
+
}))
|
|
101
|
+
try {
|
|
102
|
+
const res = await $fetch<{ jobs: Array<{ source: string, job_id?: string, error?: string }>, count: number }>(
|
|
103
|
+
`${apiBase}/api/knowledge/ingest-bulk`,
|
|
104
|
+
{ method: 'POST', body: { sources: cleaned } },
|
|
105
|
+
)
|
|
106
|
+
res.jobs.forEach((j) => {
|
|
107
|
+
const row = ingestJobs.value.find((r) => r.source === j.source)
|
|
108
|
+
if (!row) return
|
|
109
|
+
if (j.error) {
|
|
110
|
+
row.status = 'failed'
|
|
111
|
+
row.error = j.error
|
|
112
|
+
} else if (j.job_id) {
|
|
113
|
+
row.job_id = j.job_id
|
|
114
|
+
row.status = 'processing'
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
connectWebSocket()
|
|
118
|
+
} catch (err) {
|
|
119
|
+
toast.add({
|
|
120
|
+
title: 'Ingest failed',
|
|
121
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
122
|
+
color: 'error',
|
|
123
|
+
})
|
|
124
|
+
step.value = 1
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
function connectWebSocket() {
|
|
130
|
+
if (ws && ws.readyState === WebSocket.OPEN) return
|
|
131
|
+
const wsUrl = apiBase.replace('http://', 'ws://').replace('https://', 'wss://') + '/ws/tasks'
|
|
132
|
+
ws = new WebSocket(wsUrl)
|
|
133
|
+
ws.onmessage = (event) => {
|
|
134
|
+
try {
|
|
135
|
+
const data = JSON.parse(event.data)
|
|
136
|
+
const row = ingestJobs.value.find((j) => j.job_id === data.job_id)
|
|
137
|
+
if (!row) return
|
|
138
|
+
if (data.type === 'job_progress') {
|
|
139
|
+
row.progress = data.progress
|
|
140
|
+
row.status = 'processing'
|
|
141
|
+
} else if (data.type === 'job_complete') {
|
|
142
|
+
row.status = 'completed'
|
|
143
|
+
row.progress = 100
|
|
144
|
+
} else if (data.type === 'job_failed') {
|
|
145
|
+
row.status = 'failed'
|
|
146
|
+
row.error = data.error
|
|
147
|
+
}
|
|
148
|
+
} catch { /* ignore malformed messages */ }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
function disconnectWebSocket() {
|
|
154
|
+
if (ws) {
|
|
155
|
+
try { ws.close() } catch { /* already closed */ }
|
|
156
|
+
ws = null
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
onBeforeUnmount(() => {
|
|
162
|
+
disconnectWebSocket()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
// ─── Step 3: build the persona draft ────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async function runBuild() {
|
|
170
|
+
building.value = true
|
|
171
|
+
buildError.value = null
|
|
172
|
+
draft.value = null
|
|
173
|
+
try {
|
|
174
|
+
const res = await $fetch<{ persona: Persona, chunks_used: number, provider_name: string }>(
|
|
175
|
+
`${apiBase}/api/personas/build`,
|
|
176
|
+
{
|
|
177
|
+
method: 'POST',
|
|
178
|
+
body: {
|
|
179
|
+
name: name.value.trim(),
|
|
180
|
+
source_label: sourceLabel.value.trim() || name.value.trim(),
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
if ('error' in res && typeof (res as any).error === 'string') {
|
|
185
|
+
throw new Error((res as any).error)
|
|
186
|
+
}
|
|
187
|
+
draft.value = res.persona
|
|
188
|
+
chunksUsed.value = res.chunks_used
|
|
189
|
+
step.value = 4
|
|
190
|
+
} catch (err) {
|
|
191
|
+
buildError.value = err instanceof Error ? err.message : 'unknown error'
|
|
192
|
+
} finally {
|
|
193
|
+
building.value = false
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
// ─── Step 4: save + optional clone ──────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
async function savePersona() {
|
|
202
|
+
if (!draft.value) return
|
|
203
|
+
saving.value = true
|
|
204
|
+
try {
|
|
205
|
+
const created = await $fetch<Persona>(`${apiBase}/api/personas`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
body: draft.value,
|
|
208
|
+
})
|
|
209
|
+
if (saveAndClone.value && cloneDept.value && cloneTier.value) {
|
|
210
|
+
await $fetch(`${apiBase}/api/personas/${created.id}/clone`, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
body: { department: cloneDept.value, tier: Number(cloneTier.value) },
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
toast.add({
|
|
216
|
+
title: saveAndClone.value ? 'Persona saved + agent cloned' : 'Persona saved',
|
|
217
|
+
description: `${created.name} is now in your board.`,
|
|
218
|
+
color: 'success',
|
|
219
|
+
})
|
|
220
|
+
emit('completed', created)
|
|
221
|
+
} catch (err) {
|
|
222
|
+
toast.add({
|
|
223
|
+
title: 'Save failed',
|
|
224
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
225
|
+
color: 'error',
|
|
226
|
+
})
|
|
227
|
+
} finally {
|
|
228
|
+
saving.value = false
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
// ─── Auto-advance when ingest completes ─────────────────────────────────
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
watch(allIngestComplete, async (done) => {
|
|
237
|
+
if (done && step.value === 2 && ingestCompletedCount.value > 0) {
|
|
238
|
+
step.value = 3
|
|
239
|
+
await runBuild()
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
function cancel() {
|
|
245
|
+
disconnectWebSocket()
|
|
246
|
+
emit('cancelled')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
function backToStep1() {
|
|
251
|
+
disconnectWebSocket()
|
|
252
|
+
step.value = 1
|
|
253
|
+
ingestJobs.value = []
|
|
254
|
+
}
|
|
255
|
+
</script>
|
|
256
|
+
|
|
257
|
+
<template>
|
|
258
|
+
<UCard>
|
|
259
|
+
<template #header>
|
|
260
|
+
<div class="flex items-center justify-between">
|
|
261
|
+
<div>
|
|
262
|
+
<h3 class="text-lg font-semibold">AI Persona Builder</h3>
|
|
263
|
+
<p class="text-sm text-muted mt-1">
|
|
264
|
+
Step {{ step }} of 4 — {{ {
|
|
265
|
+
1: 'Sources',
|
|
266
|
+
2: 'Indexing',
|
|
267
|
+
3: 'Generating DNA',
|
|
268
|
+
4: 'Review & save',
|
|
269
|
+
}[step] }}
|
|
270
|
+
</p>
|
|
271
|
+
</div>
|
|
272
|
+
<UButton
|
|
273
|
+
label="Cancel"
|
|
274
|
+
variant="ghost"
|
|
275
|
+
color="neutral"
|
|
276
|
+
size="sm"
|
|
277
|
+
@click="cancel"
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
</template>
|
|
281
|
+
|
|
282
|
+
<!-- Progress indicator -->
|
|
283
|
+
<div class="flex items-center gap-2 mb-6">
|
|
284
|
+
<div
|
|
285
|
+
v-for="s in [1, 2, 3, 4] as Step[]"
|
|
286
|
+
:key="s"
|
|
287
|
+
class="flex-1 h-1 rounded-full"
|
|
288
|
+
:class="s <= step ? 'bg-primary' : 'bg-muted/30'"
|
|
289
|
+
/>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<!-- Step 1: Sources -->
|
|
293
|
+
<div v-if="step === 1" class="space-y-4">
|
|
294
|
+
<UFormField label="Person name" required>
|
|
295
|
+
<UInput
|
|
296
|
+
v-model="name"
|
|
297
|
+
placeholder="e.g. Alex Hormozi"
|
|
298
|
+
size="lg"
|
|
299
|
+
class="w-full"
|
|
300
|
+
/>
|
|
301
|
+
</UFormField>
|
|
302
|
+
|
|
303
|
+
<UFormField label="Source label (optional)" help="How this person should appear in the persona's source field. Defaults to the name above.">
|
|
304
|
+
<UInput
|
|
305
|
+
v-model="sourceLabel"
|
|
306
|
+
placeholder="e.g. Alex Hormozi — $100M Offers / $100M Leads"
|
|
307
|
+
class="w-full"
|
|
308
|
+
/>
|
|
309
|
+
</UFormField>
|
|
310
|
+
|
|
311
|
+
<UFormField
|
|
312
|
+
label="Sources (one URL per line)"
|
|
313
|
+
help="YouTube videos, articles, PDFs, blog posts about this person. The builder will search the indexed chunks and synthesise their behavioural DNA. Up to 50 sources per batch."
|
|
314
|
+
>
|
|
315
|
+
<UTextarea
|
|
316
|
+
v-model="sources"
|
|
317
|
+
:rows="6"
|
|
318
|
+
placeholder="https://www.youtube.com/watch?v=... https://example.com/article https://example.com/paper.pdf"
|
|
319
|
+
class="w-full font-mono text-sm"
|
|
320
|
+
:disabled="skipIngest"
|
|
321
|
+
/>
|
|
322
|
+
</UFormField>
|
|
323
|
+
|
|
324
|
+
<div class="flex items-center justify-between text-xs text-muted">
|
|
325
|
+
<span>{{ sourceLineCount }} source{{ sourceLineCount === 1 ? '' : 's' }} detected</span>
|
|
326
|
+
<UCheckbox
|
|
327
|
+
v-model="skipIngest"
|
|
328
|
+
label="Skip ingest — content for this person is already in the knowledge base"
|
|
329
|
+
/>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<div class="flex justify-end gap-2 pt-4">
|
|
333
|
+
<UButton
|
|
334
|
+
:label="skipIngest ? 'Generate from existing knowledge' : `Index ${sourceLineCount} source${sourceLineCount === 1 ? '' : 's'} & build`"
|
|
335
|
+
icon="i-lucide-arrow-right"
|
|
336
|
+
:disabled="!name.trim() || (!skipIngest && sourceLineCount === 0) || sourceLineCount > 50"
|
|
337
|
+
size="md"
|
|
338
|
+
@click="startIngest"
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
<p v-if="sourceLineCount > 50" class="text-xs text-red-400">
|
|
342
|
+
Over the 50-source cap. Trim the list before continuing.
|
|
343
|
+
</p>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<!-- Step 2: Ingest progress -->
|
|
347
|
+
<div v-else-if="step === 2" class="space-y-4">
|
|
348
|
+
<p class="text-sm text-muted">
|
|
349
|
+
Indexing {{ ingestJobs.length }} source{{ ingestJobs.length === 1 ? '' : 's' }} into the knowledge base.
|
|
350
|
+
This auto-advances when complete.
|
|
351
|
+
</p>
|
|
352
|
+
<div class="space-y-2">
|
|
353
|
+
<div
|
|
354
|
+
v-for="(job, idx) in ingestJobs"
|
|
355
|
+
:key="idx"
|
|
356
|
+
class="rounded-lg border border-default p-3"
|
|
357
|
+
>
|
|
358
|
+
<div class="flex items-center gap-3">
|
|
359
|
+
<UIcon
|
|
360
|
+
:name="{
|
|
361
|
+
queued: 'i-lucide-clock',
|
|
362
|
+
processing: 'i-lucide-loader-2 animate-spin',
|
|
363
|
+
completed: 'i-lucide-check-circle',
|
|
364
|
+
failed: 'i-lucide-x-circle',
|
|
365
|
+
}[job.status]"
|
|
366
|
+
:class="{
|
|
367
|
+
queued: 'text-muted',
|
|
368
|
+
processing: 'text-primary',
|
|
369
|
+
completed: 'text-green-500',
|
|
370
|
+
failed: 'text-red-500',
|
|
371
|
+
}[job.status]"
|
|
372
|
+
class="size-4 shrink-0"
|
|
373
|
+
/>
|
|
374
|
+
<div class="flex-1 min-w-0">
|
|
375
|
+
<p class="text-sm font-mono truncate">{{ job.source }}</p>
|
|
376
|
+
<UProgress
|
|
377
|
+
v-if="job.status === 'processing' || job.status === 'queued'"
|
|
378
|
+
:value="job.progress"
|
|
379
|
+
:max="100"
|
|
380
|
+
size="xs"
|
|
381
|
+
class="mt-1"
|
|
382
|
+
/>
|
|
383
|
+
<p v-if="job.error" class="text-xs text-red-400 mt-1">{{ job.error }}</p>
|
|
384
|
+
</div>
|
|
385
|
+
<span class="text-xs text-muted">{{ job.progress }}%</span>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<!-- Step 3: Building -->
|
|
392
|
+
<div v-else-if="step === 3" class="space-y-4">
|
|
393
|
+
<div v-if="building" class="flex flex-col items-center gap-4 py-12">
|
|
394
|
+
<UIcon name="i-lucide-loader-2" class="size-12 animate-spin text-primary" />
|
|
395
|
+
<p class="text-sm text-muted">
|
|
396
|
+
Reading indexed chunks about <strong>{{ name }}</strong>…
|
|
397
|
+
</p>
|
|
398
|
+
<p class="text-xs text-muted">
|
|
399
|
+
The builder searches the vector store, joins the top chunks, and asks the configured LLM to extract a behavioural-DNA persona.
|
|
400
|
+
</p>
|
|
401
|
+
</div>
|
|
402
|
+
<div v-else-if="buildError" class="rounded-lg border border-red-500/20 bg-red-500/5 p-4">
|
|
403
|
+
<div class="flex items-start gap-3">
|
|
404
|
+
<UIcon name="i-lucide-alert-circle" class="size-5 text-red-500 mt-0.5 shrink-0" />
|
|
405
|
+
<div class="flex-1">
|
|
406
|
+
<p class="text-sm font-medium text-red-400">Build failed</p>
|
|
407
|
+
<p class="text-xs text-muted mt-1">{{ buildError }}</p>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
<div class="flex gap-2 mt-3">
|
|
411
|
+
<UButton label="Retry" variant="outline" size="sm" @click="runBuild" />
|
|
412
|
+
<UButton label="Back to sources" variant="ghost" size="sm" @click="backToStep1" />
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
<!-- Step 4: Review & save -->
|
|
418
|
+
<div v-else-if="step === 4 && draft" class="space-y-4">
|
|
419
|
+
<div class="rounded-lg border border-green-500/20 bg-green-500/5 p-3">
|
|
420
|
+
<p class="text-sm text-green-400">
|
|
421
|
+
<UIcon name="i-lucide-sparkles" class="size-4 inline" />
|
|
422
|
+
Built from <strong>{{ chunksUsed }}</strong> knowledge chunk{{ chunksUsed === 1 ? '' : 's' }}. Edit any field below before saving.
|
|
423
|
+
</p>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<fieldset class="space-y-3">
|
|
427
|
+
<legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">Identity</legend>
|
|
428
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
429
|
+
<UFormField label="Name">
|
|
430
|
+
<UInput v-model="draft.name" class="w-full" />
|
|
431
|
+
</UFormField>
|
|
432
|
+
<UFormField label="Title">
|
|
433
|
+
<UInput v-model="draft.title" class="w-full" />
|
|
434
|
+
</UFormField>
|
|
435
|
+
</div>
|
|
436
|
+
<UFormField label="Tagline">
|
|
437
|
+
<UInput v-model="draft.tagline" class="w-full" />
|
|
438
|
+
</UFormField>
|
|
439
|
+
</fieldset>
|
|
440
|
+
|
|
441
|
+
<fieldset class="space-y-3">
|
|
442
|
+
<legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">Behavioural DNA</legend>
|
|
443
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
444
|
+
<UFormField label="MBTI">
|
|
445
|
+
<UInput v-model="draft.mbti" class="w-full" />
|
|
446
|
+
</UFormField>
|
|
447
|
+
<UFormField label="DISC primary">
|
|
448
|
+
<UInput v-model="draft.disc.primary" class="w-full" />
|
|
449
|
+
</UFormField>
|
|
450
|
+
<UFormField label="Enneagram type">
|
|
451
|
+
<UInput v-model.number="draft.enneagram.type" type="number" :min="1" :max="9" class="w-full" />
|
|
452
|
+
</UFormField>
|
|
453
|
+
<UFormField label="Enneagram wing">
|
|
454
|
+
<UInput v-model.number="draft.enneagram.wing" type="number" :min="1" :max="9" class="w-full" />
|
|
455
|
+
</UFormField>
|
|
456
|
+
</div>
|
|
457
|
+
</fieldset>
|
|
458
|
+
|
|
459
|
+
<fieldset class="space-y-3">
|
|
460
|
+
<legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">Knowledge</legend>
|
|
461
|
+
<UFormField label="Mental models" help="comma-separated">
|
|
462
|
+
<UInput
|
|
463
|
+
:model-value="draft.mental_models.join(', ')"
|
|
464
|
+
@update:model-value="(v: string) => draft && (draft.mental_models = v.split(',').map(s => s.trim()).filter(Boolean))"
|
|
465
|
+
class="w-full"
|
|
466
|
+
/>
|
|
467
|
+
</UFormField>
|
|
468
|
+
<UFormField label="Expertise domains" help="comma-separated">
|
|
469
|
+
<UInput
|
|
470
|
+
:model-value="draft.expertise_domains.join(', ')"
|
|
471
|
+
@update:model-value="(v: string) => draft && (draft.expertise_domains = v.split(',').map(s => s.trim()).filter(Boolean))"
|
|
472
|
+
class="w-full"
|
|
473
|
+
/>
|
|
474
|
+
</UFormField>
|
|
475
|
+
</fieldset>
|
|
476
|
+
|
|
477
|
+
<fieldset class="space-y-3">
|
|
478
|
+
<legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">Save options</legend>
|
|
479
|
+
<UCheckbox
|
|
480
|
+
v-model="saveAndClone"
|
|
481
|
+
label="Also clone to an agent immediately"
|
|
482
|
+
/>
|
|
483
|
+
<div v-if="saveAndClone" class="grid grid-cols-2 gap-3 pl-6">
|
|
484
|
+
<UFormField label="Department">
|
|
485
|
+
<USelect v-model="cloneDept" :items="departmentOptions" class="w-full" />
|
|
486
|
+
</UFormField>
|
|
487
|
+
<UFormField label="Tier">
|
|
488
|
+
<USelect v-model="cloneTier" :items="tierOptions" class="w-full" />
|
|
489
|
+
</UFormField>
|
|
490
|
+
</div>
|
|
491
|
+
</fieldset>
|
|
492
|
+
|
|
493
|
+
<div class="flex justify-end gap-2 pt-4">
|
|
494
|
+
<UButton label="Back" variant="ghost" @click="backToStep1" />
|
|
495
|
+
<UButton
|
|
496
|
+
:label="saveAndClone ? 'Save & clone' : 'Save persona'"
|
|
497
|
+
icon="i-lucide-check"
|
|
498
|
+
:loading="saving"
|
|
499
|
+
@click="savePersona"
|
|
500
|
+
/>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</UCard>
|
|
504
|
+
</template>
|
|
@@ -9,8 +9,30 @@ const { data, status, error, refresh } = fetchApi<{ personas: Persona[]; total:
|
|
|
9
9
|
|
|
10
10
|
const personas = computed(() => data.value?.personas ?? [])
|
|
11
11
|
|
|
12
|
-
// ---
|
|
13
|
-
|
|
12
|
+
// --- Creation mode ---
|
|
13
|
+
// PR62 v2.79.0 — three modes: list (default), wizard (AI builder), manual.
|
|
14
|
+
// The wizard is the new primary path; manual stays as fallback for
|
|
15
|
+
// operators who want to type every DNA field by hand.
|
|
16
|
+
type CreateMode = 'list' | 'wizard' | 'manual'
|
|
17
|
+
const createMode = ref<CreateMode>('list')
|
|
18
|
+
const showForm = computed(() => createMode.value === 'manual')
|
|
19
|
+
|
|
20
|
+
function startWizard() {
|
|
21
|
+
createMode.value = 'wizard'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function startManual() {
|
|
25
|
+
createMode.value = 'manual'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cancelCreation() {
|
|
29
|
+
createMode.value = 'list'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function onWizardComplete() {
|
|
33
|
+
createMode.value = 'list'
|
|
34
|
+
await refresh()
|
|
35
|
+
}
|
|
14
36
|
|
|
15
37
|
// --- Form state ---
|
|
16
38
|
function defaultForm() {
|
|
@@ -216,11 +238,29 @@ function discColor(disc: string): string {
|
|
|
216
238
|
|
|
217
239
|
<template #right>
|
|
218
240
|
<UButton
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
241
|
+
v-if="createMode === 'list'"
|
|
242
|
+
label="AI Builder"
|
|
243
|
+
icon="i-lucide-sparkles"
|
|
244
|
+
color="primary"
|
|
245
|
+
size="sm"
|
|
246
|
+
@click="startWizard"
|
|
247
|
+
/>
|
|
248
|
+
<UButton
|
|
249
|
+
v-if="createMode === 'list'"
|
|
250
|
+
label="Manual"
|
|
251
|
+
icon="i-lucide-plus"
|
|
252
|
+
variant="outline"
|
|
253
|
+
size="sm"
|
|
254
|
+
class="ml-2"
|
|
255
|
+
@click="startManual"
|
|
256
|
+
/>
|
|
257
|
+
<UButton
|
|
258
|
+
v-else
|
|
259
|
+
label="Back to list"
|
|
260
|
+
icon="i-lucide-arrow-left"
|
|
261
|
+
variant="ghost"
|
|
222
262
|
size="sm"
|
|
223
|
-
@click="
|
|
263
|
+
@click="cancelCreation"
|
|
224
264
|
/>
|
|
225
265
|
</template>
|
|
226
266
|
</UDashboardNavbar>
|
|
@@ -242,7 +282,15 @@ function discColor(disc: string): string {
|
|
|
242
282
|
|
|
243
283
|
<!-- Content -->
|
|
244
284
|
<template v-else>
|
|
245
|
-
<!--
|
|
285
|
+
<!-- PR62: AI Persona Wizard -->
|
|
286
|
+
<PersonaWizard
|
|
287
|
+
v-if="createMode === 'wizard'"
|
|
288
|
+
class="mb-8"
|
|
289
|
+
@completed="onWizardComplete"
|
|
290
|
+
@cancelled="cancelCreation"
|
|
291
|
+
/>
|
|
292
|
+
|
|
293
|
+
<!-- Manual create form (legacy / fallback) -->
|
|
246
294
|
<UCard v-if="showForm" class="mb-8">
|
|
247
295
|
<form @submit.prevent="createPersona" class="space-y-8 p-2">
|
|
248
296
|
<!-- Identity -->
|
package/package.json
CHANGED