arkaos 2.78.0 → 2.80.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/core/sync/__pycache__/update_orchestrator.cpython-313.pyc +0 -0
- package/dashboard/app/components/DashboardState.vue +112 -0
- package/dashboard/app/components/PersonaWizard.vue +504 -0
- package/dashboard/app/pages/agents/index.vue +10 -21
- package/dashboard/app/pages/budget.vue +8 -11
- package/dashboard/app/pages/health.vue +10 -21
- package/dashboard/app/pages/index.vue +7 -15
- package/dashboard/app/pages/personas.vue +55 -7
- package/dashboard/app/pages/tasks.vue +7 -15
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.80.0
|
|
Binary file
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR64 v2.80.0 — shared loading / error / empty wrapper.
|
|
3
|
+
//
|
|
4
|
+
// Every page in dashboard/app/pages/ used to duplicate the same triple:
|
|
5
|
+
//
|
|
6
|
+
// <div v-if="status === 'pending'"><spinner/></div>
|
|
7
|
+
// <div v-else-if="error"><alert + Retry/></div>
|
|
8
|
+
// <template v-else>... content ...</template>
|
|
9
|
+
//
|
|
10
|
+
// Five copies of that pattern with subtle drift (different icon sizes,
|
|
11
|
+
// different empty-state shapes, inconsistent ARIA roles, some with
|
|
12
|
+
// retry buttons missing). PR64 extracts it into one component so the
|
|
13
|
+
// rest of the dashboard work (PR63 Settings, PR65 Budget, PR66 Index
|
|
14
|
+
// rebuild, etc.) inherits a consistent shell.
|
|
15
|
+
//
|
|
16
|
+
// Slots:
|
|
17
|
+
// default — the success/content block (only rendered on 'success')
|
|
18
|
+
// empty — optional override for the empty state (defaults to
|
|
19
|
+
// generic "no data" with the empty-* props below)
|
|
20
|
+
// loading — optional override for the spinner (rarely needed)
|
|
21
|
+
// error — optional override for the error state (rarely needed)
|
|
22
|
+
//
|
|
23
|
+
// The component never owns the data — pages still call useFetch /
|
|
24
|
+
// fetchApi and pass `status` + `error` + an `empty` boolean in.
|
|
25
|
+
|
|
26
|
+
import type { AsyncDataRequestStatus } from 'nuxt/app'
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
/** useFetch/useAsyncData status. 'pending' shows spinner. */
|
|
30
|
+
status: AsyncDataRequestStatus
|
|
31
|
+
/** Error from useFetch — present means render the error block. */
|
|
32
|
+
error?: Error | null
|
|
33
|
+
/** True when the request succeeded but returned no rows.
|
|
34
|
+
* Pages compute this from their data shape (e.g. `!list.length`). */
|
|
35
|
+
empty?: boolean
|
|
36
|
+
/** Heading for the default empty state. */
|
|
37
|
+
emptyTitle?: string
|
|
38
|
+
/** Helper text for the default empty state. */
|
|
39
|
+
emptyDescription?: string
|
|
40
|
+
/** Icon for the default empty state. Defaults to inbox. */
|
|
41
|
+
emptyIcon?: string
|
|
42
|
+
/** Optional retry handler — when provided, the error block shows a button. */
|
|
43
|
+
onRetry?: () => void | Promise<void>
|
|
44
|
+
/** Optional ARIA label for the loading region. */
|
|
45
|
+
loadingLabel?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
49
|
+
error: null,
|
|
50
|
+
empty: false,
|
|
51
|
+
emptyTitle: 'No data',
|
|
52
|
+
emptyDescription: '',
|
|
53
|
+
emptyIcon: 'i-lucide-inbox',
|
|
54
|
+
loadingLabel: 'Loading',
|
|
55
|
+
})
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<!-- Loading -->
|
|
60
|
+
<div
|
|
61
|
+
v-if="status === 'pending'"
|
|
62
|
+
class="flex items-center justify-center py-12"
|
|
63
|
+
:aria-label="loadingLabel"
|
|
64
|
+
role="status"
|
|
65
|
+
>
|
|
66
|
+
<slot name="loading">
|
|
67
|
+
<UIcon name="i-lucide-loader-2" class="size-8 animate-spin text-muted" />
|
|
68
|
+
</slot>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Error -->
|
|
72
|
+
<div
|
|
73
|
+
v-else-if="error"
|
|
74
|
+
class="flex flex-col items-center justify-center gap-4 py-12"
|
|
75
|
+
role="alert"
|
|
76
|
+
>
|
|
77
|
+
<slot name="error" :error="error">
|
|
78
|
+
<UIcon name="i-lucide-alert-triangle" class="size-12 text-red-500" />
|
|
79
|
+
<p class="text-sm text-muted">
|
|
80
|
+
{{ error.message || 'Failed to load data.' }}
|
|
81
|
+
</p>
|
|
82
|
+
<UButton
|
|
83
|
+
v-if="onRetry"
|
|
84
|
+
label="Retry"
|
|
85
|
+
variant="outline"
|
|
86
|
+
color="primary"
|
|
87
|
+
icon="i-lucide-refresh-cw"
|
|
88
|
+
@click="onRetry"
|
|
89
|
+
/>
|
|
90
|
+
</slot>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<!-- Empty -->
|
|
94
|
+
<div
|
|
95
|
+
v-else-if="empty"
|
|
96
|
+
class="flex flex-col items-center justify-center gap-4 py-16"
|
|
97
|
+
>
|
|
98
|
+
<slot name="empty">
|
|
99
|
+
<UIcon :name="emptyIcon" class="size-16 text-muted" />
|
|
100
|
+
<h3 class="text-lg font-semibold text-highlighted">{{ emptyTitle }}</h3>
|
|
101
|
+
<p
|
|
102
|
+
v-if="emptyDescription"
|
|
103
|
+
class="text-sm text-muted text-center max-w-md"
|
|
104
|
+
>
|
|
105
|
+
{{ emptyDescription }}
|
|
106
|
+
</p>
|
|
107
|
+
</slot>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Content -->
|
|
111
|
+
<slot v-else />
|
|
112
|
+
</template>
|
|
@@ -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>
|
|
@@ -127,26 +127,15 @@ function goToAgent(id: string) {
|
|
|
127
127
|
</template>
|
|
128
128
|
|
|
129
129
|
<template #body>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<UButton label="Retry" variant="outline" color="primary" icon="i-lucide-refresh-cw" @click="refresh()" />
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
<!-- Empty -->
|
|
143
|
-
<div v-else-if="!agents.length" class="flex flex-col items-center justify-center gap-4 py-12">
|
|
144
|
-
<UIcon name="i-lucide-users" class="size-12 text-muted" />
|
|
145
|
-
<p class="text-sm text-muted">No agents found.</p>
|
|
146
|
-
</div>
|
|
147
|
-
|
|
148
|
-
<!-- Content -->
|
|
149
|
-
<template v-else>
|
|
130
|
+
<DashboardState
|
|
131
|
+
:status="status"
|
|
132
|
+
:error="error"
|
|
133
|
+
:empty="!agents.length"
|
|
134
|
+
empty-title="No agents found"
|
|
135
|
+
empty-icon="i-lucide-users"
|
|
136
|
+
loading-label="Loading agents"
|
|
137
|
+
:on-retry="() => refresh()"
|
|
138
|
+
>
|
|
150
139
|
<div class="flex flex-wrap items-center gap-3 mb-4">
|
|
151
140
|
<UInput
|
|
152
141
|
v-model="search"
|
|
@@ -219,7 +208,7 @@ function goToAgent(id: string) {
|
|
|
219
208
|
@update:page="(val) => page = val"
|
|
220
209
|
/>
|
|
221
210
|
</div>
|
|
222
|
-
</
|
|
211
|
+
</DashboardState>
|
|
223
212
|
</template>
|
|
224
213
|
</UDashboardPanel>
|
|
225
214
|
</template>
|
|
@@ -37,17 +37,13 @@ const tierLabels: Record<number, string> = {
|
|
|
37
37
|
</template>
|
|
38
38
|
|
|
39
39
|
<template #body>
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<UButton label="Retry" variant="outline" icon="i-lucide-refresh-cw" @click="refresh()" />
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<div v-else class="space-y-6">
|
|
40
|
+
<DashboardState
|
|
41
|
+
:status="status"
|
|
42
|
+
:error="error"
|
|
43
|
+
loading-label="Loading budget"
|
|
44
|
+
:on-retry="() => refresh()"
|
|
45
|
+
>
|
|
46
|
+
<div class="space-y-6">
|
|
51
47
|
<!-- Monthly Summary -->
|
|
52
48
|
<UCard>
|
|
53
49
|
<div class="space-y-3">
|
|
@@ -127,6 +123,7 @@ const tierLabels: Record<number, string> = {
|
|
|
127
123
|
</div>
|
|
128
124
|
</div>
|
|
129
125
|
</div>
|
|
126
|
+
</DashboardState>
|
|
130
127
|
</template>
|
|
131
128
|
</UDashboardPanel>
|
|
132
129
|
</template>
|
|
@@ -30,26 +30,15 @@ const allPassed = computed(() => passed.value === total.value && total.value > 0
|
|
|
30
30
|
</template>
|
|
31
31
|
|
|
32
32
|
<template #body>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<UButton label="Retry" variant="outline" color="primary" icon="i-lucide-refresh-cw" @click="refresh()" />
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
<!-- Empty -->
|
|
46
|
-
<div v-else-if="!checks.length" class="flex flex-col items-center justify-center gap-4 py-12">
|
|
47
|
-
<UIcon name="i-lucide-heart-pulse" class="size-12 text-muted" />
|
|
48
|
-
<p class="text-sm text-muted">No health checks available.</p>
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
<!-- Content -->
|
|
52
|
-
<template v-else>
|
|
33
|
+
<DashboardState
|
|
34
|
+
:status="status"
|
|
35
|
+
:error="error"
|
|
36
|
+
:empty="!checks.length"
|
|
37
|
+
empty-title="No health checks available"
|
|
38
|
+
empty-icon="i-lucide-heart-pulse"
|
|
39
|
+
loading-label="Loading health checks"
|
|
40
|
+
:on-retry="() => refresh()"
|
|
41
|
+
>
|
|
53
42
|
<!-- Overall Status -->
|
|
54
43
|
<div
|
|
55
44
|
class="mb-6 rounded-lg border p-6 text-center"
|
|
@@ -92,7 +81,7 @@ const allPassed = computed(() => passed.value === total.value && total.value > 0
|
|
|
92
81
|
/>
|
|
93
82
|
</div>
|
|
94
83
|
</div>
|
|
95
|
-
</
|
|
84
|
+
</DashboardState>
|
|
96
85
|
</template>
|
|
97
86
|
</UDashboardPanel>
|
|
98
87
|
</template>
|
|
@@ -39,20 +39,12 @@ function formatCurrency(value: number): string {
|
|
|
39
39
|
</template>
|
|
40
40
|
|
|
41
41
|
<template #body>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<div v-else-if="error" class="flex flex-col items-center justify-center gap-4 py-12" role="alert">
|
|
49
|
-
<UIcon name="i-lucide-alert-triangle" class="size-12 text-red-500" />
|
|
50
|
-
<p class="text-sm text-muted">Failed to load overview data.</p>
|
|
51
|
-
<UButton label="Retry" variant="outline" color="primary" icon="i-lucide-refresh-cw" @click="refresh()" />
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
<!-- Content -->
|
|
55
|
-
<template v-else>
|
|
42
|
+
<DashboardState
|
|
43
|
+
:status="status"
|
|
44
|
+
:error="error"
|
|
45
|
+
loading-label="Loading overview"
|
|
46
|
+
:on-retry="() => refresh()"
|
|
47
|
+
>
|
|
56
48
|
<!-- Stats Grid -->
|
|
57
49
|
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
|
|
58
50
|
<div
|
|
@@ -120,7 +112,7 @@ function formatCurrency(value: number): string {
|
|
|
120
112
|
</div>
|
|
121
113
|
</div>
|
|
122
114
|
</div>
|
|
123
|
-
</
|
|
115
|
+
</DashboardState>
|
|
124
116
|
</template>
|
|
125
117
|
</UDashboardPanel>
|
|
126
118
|
</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 -->
|
|
@@ -94,20 +94,12 @@ const columns: TableColumn<Task>[] = [
|
|
|
94
94
|
</template>
|
|
95
95
|
|
|
96
96
|
<template #body>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<div v-else-if="error" class="flex flex-col items-center justify-center gap-4 py-12" role="alert">
|
|
104
|
-
<UIcon name="i-lucide-alert-triangle" class="size-12 text-red-500" />
|
|
105
|
-
<p class="text-sm text-muted">Failed to load tasks.</p>
|
|
106
|
-
<UButton label="Retry" variant="outline" color="primary" icon="i-lucide-refresh-cw" @click="refresh()" />
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
<!-- Content -->
|
|
110
|
-
<template v-else>
|
|
97
|
+
<DashboardState
|
|
98
|
+
:status="status"
|
|
99
|
+
:error="error"
|
|
100
|
+
loading-label="Loading tasks"
|
|
101
|
+
:on-retry="() => refresh()"
|
|
102
|
+
>
|
|
111
103
|
<!-- Summary Cards -->
|
|
112
104
|
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
|
113
105
|
<div class="rounded-lg border border-default p-4 text-center">
|
|
@@ -197,7 +189,7 @@ const columns: TableColumn<Task>[] = [
|
|
|
197
189
|
</template>
|
|
198
190
|
</UTable>
|
|
199
191
|
</div>
|
|
200
|
-
</
|
|
192
|
+
</DashboardState>
|
|
201
193
|
</template>
|
|
202
194
|
</UDashboardPanel>
|
|
203
195
|
</template>
|
package/package.json
CHANGED