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