arkaos 3.78.0 → 4.0.1
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/README.md +42 -30
- package/VERSION +1 -1
- package/arka/SKILL.md +2 -2
- package/config/agent-allowlists/laravel.yaml +1 -0
- package/config/agent-allowlists/node.yaml +1 -0
- package/config/agent-allowlists/nuxt.yaml +1 -0
- package/config/agent-allowlists/python.yaml +1 -0
- package/core/agents/__pycache__/registry_gen.cpython-313.pyc +0 -0
- package/core/agents/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/agents/registry_gen.py +6 -1
- package/core/agents/schema.py +4 -0
- package/core/cognition/__pycache__/reorganizer.cpython-313.pyc +0 -0
- package/core/cognition/reorganizer.py +37 -7
- package/core/governance/__pycache__/design_system_lint.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/design_system_lint_cli.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/agent_match.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/chunker.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/ingest.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/sources.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/agent_match.py +114 -0
- package/core/knowledge/chunker.py +45 -0
- package/core/knowledge/ingest.py +156 -78
- package/core/knowledge/sources.py +138 -0
- package/core/knowledge/vector_store.py +52 -0
- package/core/squads/__pycache__/loader.cpython-313.pyc +0 -0
- package/core/squads/loader.py +25 -0
- package/core/sync/__pycache__/agent_provisioner.cpython-313.pyc +0 -0
- package/core/sync/agent_provisioner.py +19 -8
- package/dashboard/app/components/KnowledgeSourcesList.vue +40 -13
- package/dashboard/app/pages/cognition.vue +9 -4
- package/dashboard/app/pages/knowledge/[id].vue +669 -0
- package/dashboard/app/pages/knowledge/index.vue +1281 -0
- package/dashboard/app/types/index.d.ts +1 -1
- package/departments/brand/agents/ux-designer.yaml +15 -1
- package/departments/brand/agents/ux-researcher.yaml +73 -0
- package/departments/brand/agents/ux-strategist.yaml +72 -0
- package/departments/dev/agents/ai-engineering/ai-engineering-lead.yaml +76 -0
- package/departments/dev/agents/architect.yaml +9 -3
- package/departments/dev/agents/backend-core/laravel-eng.yaml +76 -0
- package/departments/dev/agents/backend-core/node-ts-eng.yaml +76 -0
- package/departments/dev/agents/backend-core/python-eng.yaml +76 -0
- package/departments/dev/agents/backend-dev.yaml +10 -4
- package/departments/dev/agents/data-platform/etl-eng.yaml +74 -0
- package/departments/dev/agents/dba.yaml +7 -3
- package/departments/dev/references/backend-knowledge-and-tools.md +70 -0
- package/departments/ecom/agents/retention-manager.yaml +13 -1
- package/departments/leadership/agents/culture-coach.yaml +20 -0
- package/departments/leadership/agents/hr-specialist.yaml +18 -0
- package/departments/leadership/agents/leadership-director.yaml +10 -0
- package/departments/org/agents/chief-of-staff.yaml +76 -0
- package/departments/org/agents/coo.yaml +11 -0
- package/departments/org/agents/okr-steward.yaml +71 -0
- package/departments/org/agents/org-designer.yaml +23 -0
- package/departments/org/skills/okr-cadence/SKILL.md +34 -0
- package/departments/org/skills/principles-audit/SKILL.md +36 -0
- package/departments/pm/agents/pm-director.yaml +21 -8
- package/departments/pm/agents/product-owner.yaml +24 -2
- package/departments/pm/agents/scrum-master.yaml +21 -0
- package/departments/pm/agents/strategic-pm.yaml +72 -0
- package/departments/pm/skills/discovery-plan/SKILL.md +7 -1
- package/departments/quality/agents/cqo.yaml +8 -0
- package/departments/saas/agents/cs-manager.yaml +19 -2
- package/departments/saas/agents/growth-engineer.yaml +14 -1
- package/departments/saas/agents/metrics-analyst.yaml +17 -1
- package/departments/saas/agents/revops-lead.yaml +73 -0
- package/departments/saas/skills/leaky-bucket/SKILL.md +28 -0
- package/departments/saas/skills/voc-loop/SKILL.md +29 -0
- package/departments/sales/agents/sales-director.yaml +9 -0
- package/departments/sales/agents/sdr.yaml +72 -0
- package/departments/strategy/agents/decision-quality.yaml +72 -0
- package/departments/strategy/agents/strategy-director.yaml +13 -0
- package/departments/strategy/skills/premortem/SKILL.md +33 -0
- package/knowledge/agents-registry-v2.json +1218 -78
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/bench/__init__.py +5 -0
- package/scripts/bench/__pycache__/__init__.cpython-313.pyc +0 -0
- package/scripts/bench/__pycache__/harness.cpython-313.pyc +0 -0
- package/scripts/bench/__pycache__/run.cpython-313.pyc +0 -0
- package/scripts/bench/harness.py +138 -0
- package/scripts/bench/run.py +136 -0
- package/scripts/dashboard-api.py +376 -13
- package/scripts/tools/__pycache__/docs_stats.cpython-313.pyc +0 -0
- package/scripts/tools/docs_stats.py +154 -0
- package/dashboard/app/pages/knowledge.vue +0 -918
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR2 — Per-source knowledge detail page.
|
|
3
|
+
//
|
|
4
|
+
// Polymorphic view for a single indexed source (id like `src-xxxx`).
|
|
5
|
+
// Loads GET /api/knowledge/sources/{id}. Renders media (YouTube embed,
|
|
6
|
+
// native video/audio, or download), the full transcript, the chunks this
|
|
7
|
+
// source contributed to the vector store, and a placeholder for PR3 agent
|
|
8
|
+
// attribution. Unknown ids return 404 -> "Source not found" empty state.
|
|
9
|
+
|
|
10
|
+
const route = useRoute()
|
|
11
|
+
const sourceId = route.params.id as string
|
|
12
|
+
|
|
13
|
+
const { fetchApi, apiBase } = useApi()
|
|
14
|
+
const toast = useToast()
|
|
15
|
+
|
|
16
|
+
interface SourceChunk {
|
|
17
|
+
text: string
|
|
18
|
+
heading: string
|
|
19
|
+
metadata: Record<string, unknown>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SourceDetail {
|
|
23
|
+
id: string
|
|
24
|
+
source: string
|
|
25
|
+
type: '' | 'youtube' | 'web' | 'pdf' | 'audio' | 'video' | 'markdown'
|
|
26
|
+
title: string
|
|
27
|
+
duration: number
|
|
28
|
+
language: string
|
|
29
|
+
thumbnail_path: string
|
|
30
|
+
media_path: string
|
|
31
|
+
transcript: string
|
|
32
|
+
chunk_count: number
|
|
33
|
+
status: string
|
|
34
|
+
error: string
|
|
35
|
+
created_at: string
|
|
36
|
+
updated_at: string
|
|
37
|
+
chunks: SourceChunk[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { data: source, status, error } = await fetchApi<SourceDetail>(
|
|
41
|
+
`/api/knowledge/sources/${sourceId}`
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// --- Type badge mapping (mirrors knowledge.vue) ---
|
|
45
|
+
const typeColorMap: Record<string, 'error' | 'primary' | 'warning' | 'success' | 'neutral'> = {
|
|
46
|
+
youtube: 'error',
|
|
47
|
+
web: 'primary',
|
|
48
|
+
pdf: 'warning',
|
|
49
|
+
audio: 'success',
|
|
50
|
+
markdown: 'neutral',
|
|
51
|
+
video: 'error'
|
|
52
|
+
}
|
|
53
|
+
const typeIconMap: Record<string, string> = {
|
|
54
|
+
youtube: 'i-lucide-youtube',
|
|
55
|
+
web: 'i-lucide-globe',
|
|
56
|
+
pdf: 'i-lucide-file-text',
|
|
57
|
+
audio: 'i-lucide-headphones',
|
|
58
|
+
markdown: 'i-lucide-file-code',
|
|
59
|
+
video: 'i-lucide-video'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const statusColorMap: Record<string, 'success' | 'error' | 'neutral'> = {
|
|
63
|
+
ready: 'success',
|
|
64
|
+
failed: 'error',
|
|
65
|
+
pending: 'neutral'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Derived labels ---
|
|
69
|
+
function sourceLabel(src: string): string {
|
|
70
|
+
if (src.startsWith('http')) {
|
|
71
|
+
try {
|
|
72
|
+
const u = new URL(src)
|
|
73
|
+
return u.hostname + u.pathname
|
|
74
|
+
} catch {
|
|
75
|
+
return src
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return src
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const headerTitle = computed(() => {
|
|
82
|
+
const t = source.value?.title?.trim()
|
|
83
|
+
if (t) return t
|
|
84
|
+
return source.value ? sourceLabel(source.value.source) : 'Source'
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const typeLabel = computed(() => {
|
|
88
|
+
const t = source.value?.type
|
|
89
|
+
if (!t) return 'Unknown'
|
|
90
|
+
return t.charAt(0).toUpperCase() + t.slice(1)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// --- Media decision ---
|
|
94
|
+
// YouTube: prefer the original embed player (zero-storage, always available)
|
|
95
|
+
// when we can parse a video id from the source URL. Otherwise fall back to
|
|
96
|
+
// the native player when the backend has a stored media file.
|
|
97
|
+
const youtubeEmbedUrl = computed<string | null>(() => {
|
|
98
|
+
if (source.value?.type !== 'youtube') return null
|
|
99
|
+
const src = source.value.source
|
|
100
|
+
if (!src.startsWith('http')) return null
|
|
101
|
+
try {
|
|
102
|
+
const u = new URL(src)
|
|
103
|
+
let id = ''
|
|
104
|
+
if (u.hostname.includes('youtu.be')) {
|
|
105
|
+
id = u.pathname.replace(/^\//, '')
|
|
106
|
+
} else if (u.searchParams.get('v')) {
|
|
107
|
+
id = u.searchParams.get('v') ?? ''
|
|
108
|
+
} else if (u.pathname.startsWith('/embed/')) {
|
|
109
|
+
id = u.pathname.replace('/embed/', '')
|
|
110
|
+
}
|
|
111
|
+
if (!id) return null
|
|
112
|
+
return `https://www.youtube.com/embed/${id}`
|
|
113
|
+
} catch {
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const hasStoredMedia = computed(() => Boolean(source.value?.media_path))
|
|
119
|
+
const mediaSrc = computed(() => `${apiBase}/api/knowledge/sources/${sourceId}/media`)
|
|
120
|
+
const downloadUrl = computed(() => `${apiBase}/api/knowledge/sources/${sourceId}/download`)
|
|
121
|
+
|
|
122
|
+
const useNativeVideo = computed(() =>
|
|
123
|
+
!youtubeEmbedUrl.value
|
|
124
|
+
&& hasStoredMedia.value
|
|
125
|
+
&& (source.value?.type === 'video' || source.value?.type === 'youtube')
|
|
126
|
+
)
|
|
127
|
+
const useNativeAudio = computed(() =>
|
|
128
|
+
!youtubeEmbedUrl.value && hasStoredMedia.value && source.value?.type === 'audio'
|
|
129
|
+
)
|
|
130
|
+
const isExternalSource = computed(() => Boolean(source.value?.source?.startsWith('http')))
|
|
131
|
+
|
|
132
|
+
// --- Transcript helpers ---
|
|
133
|
+
const wordCount = computed(() => {
|
|
134
|
+
const t = source.value?.transcript?.trim()
|
|
135
|
+
if (!t) return 0
|
|
136
|
+
return t.split(/\s+/).length
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
function formatDuration(seconds: number): string {
|
|
140
|
+
if (!seconds || seconds <= 0) return ''
|
|
141
|
+
const s = Math.floor(seconds % 60)
|
|
142
|
+
const m = Math.floor((seconds / 60) % 60)
|
|
143
|
+
const h = Math.floor(seconds / 3600)
|
|
144
|
+
const pad = (n: number) => n.toString().padStart(2, '0')
|
|
145
|
+
if (h > 0) return `${h}:${pad(m)}:${pad(s)}`
|
|
146
|
+
return `${m}:${pad(s)}`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function copyTranscript() {
|
|
150
|
+
const text = source.value?.transcript ?? ''
|
|
151
|
+
if (!text) return
|
|
152
|
+
try {
|
|
153
|
+
await navigator.clipboard.writeText(text)
|
|
154
|
+
toast.add({
|
|
155
|
+
title: 'Transcript copied',
|
|
156
|
+
description: `${wordCount.value} words`,
|
|
157
|
+
color: 'success',
|
|
158
|
+
icon: 'i-lucide-check'
|
|
159
|
+
})
|
|
160
|
+
} catch {
|
|
161
|
+
toast.add({
|
|
162
|
+
title: 'Copy failed',
|
|
163
|
+
description: 'Clipboard is not available in this context.',
|
|
164
|
+
color: 'error'
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- Chunk expand/collapse ---
|
|
170
|
+
const expanded = ref<Set<number>>(new Set())
|
|
171
|
+
function toggleChunk(idx: number) {
|
|
172
|
+
const next = new Set(expanded.value)
|
|
173
|
+
if (next.has(idx)) {
|
|
174
|
+
next.delete(idx)
|
|
175
|
+
} else {
|
|
176
|
+
next.add(idx)
|
|
177
|
+
}
|
|
178
|
+
expanded.value = next
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- Agents: semantic matches + propose-only learning suggestion ---
|
|
182
|
+
interface AgentMatch {
|
|
183
|
+
id: string
|
|
184
|
+
name: string
|
|
185
|
+
department: string
|
|
186
|
+
role: string
|
|
187
|
+
score: number
|
|
188
|
+
matched_terms: string[]
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
interface AgentMatchesResponse {
|
|
192
|
+
matches: AgentMatch[]
|
|
193
|
+
source_id?: string
|
|
194
|
+
count?: number
|
|
195
|
+
reason?: string
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const agentMatches = ref<AgentMatch[]>([])
|
|
199
|
+
const agentMatchesLoading = ref(false)
|
|
200
|
+
const agentMatchesReason = ref<string | null>(null)
|
|
201
|
+
const proposalPending = ref(false)
|
|
202
|
+
|
|
203
|
+
async function fetchAgentMatches() {
|
|
204
|
+
if (!source.value) return
|
|
205
|
+
agentMatchesLoading.value = true
|
|
206
|
+
agentMatchesReason.value = null
|
|
207
|
+
try {
|
|
208
|
+
const res = await $fetch<AgentMatchesResponse>(
|
|
209
|
+
`${apiBase}/api/knowledge/sources/${sourceId}/agent-matches`,
|
|
210
|
+
{ params: { top_n: 5 } }
|
|
211
|
+
)
|
|
212
|
+
agentMatches.value = res.matches ?? []
|
|
213
|
+
agentMatchesReason.value = res.reason ?? null
|
|
214
|
+
} catch {
|
|
215
|
+
agentMatchesReason.value = 'request failed'
|
|
216
|
+
agentMatches.value = []
|
|
217
|
+
} finally {
|
|
218
|
+
agentMatchesLoading.value = false
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function scorePercent(score: number): number {
|
|
223
|
+
return Math.round(Math.max(0, Math.min(1, score)) * 100)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function generateProposal() {
|
|
227
|
+
if (proposalPending.value) return
|
|
228
|
+
proposalPending.value = true
|
|
229
|
+
try {
|
|
230
|
+
const res = await $fetch<{ proposal_path: string, agents: number }>(
|
|
231
|
+
`${apiBase}/api/knowledge/sources/${sourceId}/agent-proposal`,
|
|
232
|
+
{
|
|
233
|
+
method: 'POST',
|
|
234
|
+
body: { agent_ids: agentMatches.value.map(m => m.id) }
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
toast.add({
|
|
238
|
+
title: 'Proposal saved',
|
|
239
|
+
description: res.proposal_path,
|
|
240
|
+
color: 'success',
|
|
241
|
+
icon: 'i-lucide-check'
|
|
242
|
+
})
|
|
243
|
+
} catch {
|
|
244
|
+
toast.add({
|
|
245
|
+
title: 'Could not generate proposal',
|
|
246
|
+
description: 'The proposal request failed. Please try again.',
|
|
247
|
+
color: 'error'
|
|
248
|
+
})
|
|
249
|
+
} finally {
|
|
250
|
+
proposalPending.value = false
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Non-blocking: fetch matches once the source resolves on the client.
|
|
255
|
+
onMounted(() => {
|
|
256
|
+
if (source.value) fetchAgentMatches()
|
|
257
|
+
})
|
|
258
|
+
</script>
|
|
259
|
+
|
|
260
|
+
<template>
|
|
261
|
+
<UDashboardPanel id="knowledge-source">
|
|
262
|
+
<template #header>
|
|
263
|
+
<UDashboardNavbar :title="headerTitle">
|
|
264
|
+
<template #leading>
|
|
265
|
+
<UDashboardSidebarCollapse />
|
|
266
|
+
</template>
|
|
267
|
+
<template #trailing>
|
|
268
|
+
<UButton
|
|
269
|
+
label="Back"
|
|
270
|
+
variant="ghost"
|
|
271
|
+
icon="i-lucide-arrow-left"
|
|
272
|
+
to="/knowledge"
|
|
273
|
+
aria-label="Back to knowledge base"
|
|
274
|
+
/>
|
|
275
|
+
</template>
|
|
276
|
+
</UDashboardNavbar>
|
|
277
|
+
</template>
|
|
278
|
+
|
|
279
|
+
<template #body>
|
|
280
|
+
<!-- Loading -->
|
|
281
|
+
<div v-if="status === 'pending'" class="flex items-center justify-center py-24">
|
|
282
|
+
<UIcon name="i-lucide-loader-2" class="size-8 animate-spin text-muted" />
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<!-- Not found (404 from API, or no source data resolved) -->
|
|
286
|
+
<div
|
|
287
|
+
v-else-if="(error && (error.statusCode === 404 || error.data?.error === 'not found')) || !source"
|
|
288
|
+
class="flex flex-col items-center justify-center gap-4 py-24"
|
|
289
|
+
>
|
|
290
|
+
<UIcon name="i-lucide-file-x" class="size-12 text-muted" />
|
|
291
|
+
<p class="text-sm text-muted">
|
|
292
|
+
Source not found.
|
|
293
|
+
</p>
|
|
294
|
+
<UButton
|
|
295
|
+
label="Back to Knowledge"
|
|
296
|
+
variant="outline"
|
|
297
|
+
icon="i-lucide-arrow-left"
|
|
298
|
+
to="/knowledge"
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<!-- Error (non-404 failure) -->
|
|
303
|
+
<div
|
|
304
|
+
v-else-if="error"
|
|
305
|
+
class="flex flex-col items-center justify-center gap-4 py-24"
|
|
306
|
+
role="alert"
|
|
307
|
+
>
|
|
308
|
+
<UIcon name="i-lucide-alert-triangle" class="size-12 text-red-500" />
|
|
309
|
+
<p class="text-sm text-muted">
|
|
310
|
+
Failed to load this source.
|
|
311
|
+
</p>
|
|
312
|
+
<UButton
|
|
313
|
+
label="Back to Knowledge"
|
|
314
|
+
variant="outline"
|
|
315
|
+
icon="i-lucide-arrow-left"
|
|
316
|
+
to="/knowledge"
|
|
317
|
+
/>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<!-- Content -->
|
|
321
|
+
<div v-else class="space-y-6 pb-12">
|
|
322
|
+
<!-- ===== HEADER ===== -->
|
|
323
|
+
<section class="rounded-2xl border border-default bg-elevated/10 p-6">
|
|
324
|
+
<div class="flex items-start gap-4">
|
|
325
|
+
<div class="shrink-0 size-12 rounded-xl bg-default/80 border border-default flex items-center justify-center">
|
|
326
|
+
<UIcon
|
|
327
|
+
:name="typeIconMap[source.type] ?? 'i-lucide-file'"
|
|
328
|
+
class="size-6 text-muted"
|
|
329
|
+
/>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="flex-1 min-w-0 space-y-2">
|
|
332
|
+
<h1 class="text-2xl font-bold tracking-tight text-highlighted break-words">
|
|
333
|
+
{{ headerTitle }}
|
|
334
|
+
</h1>
|
|
335
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
336
|
+
<UBadge
|
|
337
|
+
:label="typeLabel"
|
|
338
|
+
:icon="typeIconMap[source.type]"
|
|
339
|
+
:color="typeColorMap[source.type] ?? 'neutral'"
|
|
340
|
+
variant="subtle"
|
|
341
|
+
size="sm"
|
|
342
|
+
/>
|
|
343
|
+
<UBadge
|
|
344
|
+
:label="source.status || 'unknown'"
|
|
345
|
+
:color="statusColorMap[source.status] ?? 'neutral'"
|
|
346
|
+
variant="subtle"
|
|
347
|
+
size="sm"
|
|
348
|
+
class="capitalize"
|
|
349
|
+
/>
|
|
350
|
+
<UBadge
|
|
351
|
+
v-if="source.language"
|
|
352
|
+
:label="source.language"
|
|
353
|
+
variant="outline"
|
|
354
|
+
size="sm"
|
|
355
|
+
/>
|
|
356
|
+
<UBadge
|
|
357
|
+
v-if="source.duration > 0"
|
|
358
|
+
:label="formatDuration(source.duration)"
|
|
359
|
+
icon="i-lucide-clock"
|
|
360
|
+
variant="outline"
|
|
361
|
+
size="sm"
|
|
362
|
+
/>
|
|
363
|
+
<UBadge
|
|
364
|
+
:label="`${source.chunk_count} chunk${source.chunk_count === 1 ? '' : 's'}`"
|
|
365
|
+
variant="subtle"
|
|
366
|
+
size="sm"
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
369
|
+
<p class="text-xs text-muted font-mono break-all">
|
|
370
|
+
{{ source.source }}
|
|
371
|
+
</p>
|
|
372
|
+
<p class="text-xs text-muted/60 font-mono select-all">
|
|
373
|
+
{{ source.id }}
|
|
374
|
+
</p>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
<div
|
|
378
|
+
v-if="source.status === 'failed' && source.error"
|
|
379
|
+
class="mt-4 rounded-lg border border-red-500/20 bg-red-500/5 p-3"
|
|
380
|
+
role="alert"
|
|
381
|
+
>
|
|
382
|
+
<div class="flex items-start gap-2">
|
|
383
|
+
<UIcon name="i-lucide-alert-circle" class="size-4 text-red-500 mt-0.5 shrink-0" />
|
|
384
|
+
<p class="text-sm text-red-400">
|
|
385
|
+
{{ source.error }}
|
|
386
|
+
</p>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
</section>
|
|
390
|
+
|
|
391
|
+
<!-- ===== MEDIA ===== -->
|
|
392
|
+
<section class="rounded-xl border border-default bg-elevated/10 p-5">
|
|
393
|
+
<div class="flex items-center justify-between gap-3 mb-4">
|
|
394
|
+
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted">
|
|
395
|
+
Media
|
|
396
|
+
</h2>
|
|
397
|
+
<UButton
|
|
398
|
+
v-if="hasStoredMedia"
|
|
399
|
+
label="Download"
|
|
400
|
+
icon="i-lucide-download"
|
|
401
|
+
variant="outline"
|
|
402
|
+
size="sm"
|
|
403
|
+
:to="downloadUrl"
|
|
404
|
+
target="_blank"
|
|
405
|
+
external
|
|
406
|
+
aria-label="Download original media file"
|
|
407
|
+
/>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<!-- YouTube embed (zero-storage, preferred) -->
|
|
411
|
+
<div
|
|
412
|
+
v-if="youtubeEmbedUrl"
|
|
413
|
+
class="relative w-full overflow-hidden rounded-lg bg-black"
|
|
414
|
+
style="aspect-ratio: 16 / 9"
|
|
415
|
+
>
|
|
416
|
+
<iframe
|
|
417
|
+
:src="youtubeEmbedUrl"
|
|
418
|
+
:title="headerTitle"
|
|
419
|
+
class="absolute inset-0 size-full"
|
|
420
|
+
frameborder="0"
|
|
421
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
422
|
+
allowfullscreen
|
|
423
|
+
/>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<!-- Native video player -->
|
|
427
|
+
<video
|
|
428
|
+
v-else-if="useNativeVideo"
|
|
429
|
+
:src="mediaSrc"
|
|
430
|
+
controls
|
|
431
|
+
class="w-full rounded-lg bg-black"
|
|
432
|
+
preload="metadata"
|
|
433
|
+
>
|
|
434
|
+
Your browser does not support the video element.
|
|
435
|
+
</video>
|
|
436
|
+
|
|
437
|
+
<!-- Native audio player -->
|
|
438
|
+
<audio
|
|
439
|
+
v-else-if="useNativeAudio"
|
|
440
|
+
:src="mediaSrc"
|
|
441
|
+
controls
|
|
442
|
+
class="w-full"
|
|
443
|
+
preload="metadata"
|
|
444
|
+
>
|
|
445
|
+
Your browser does not support the audio element.
|
|
446
|
+
</audio>
|
|
447
|
+
|
|
448
|
+
<!-- No media -->
|
|
449
|
+
<div v-else class="flex flex-col items-start gap-2 py-2">
|
|
450
|
+
<p class="text-sm text-muted">
|
|
451
|
+
No media for this source.
|
|
452
|
+
</p>
|
|
453
|
+
<UButton
|
|
454
|
+
v-if="isExternalSource"
|
|
455
|
+
:label="sourceLabel(source.source)"
|
|
456
|
+
icon="i-lucide-external-link"
|
|
457
|
+
variant="link"
|
|
458
|
+
size="sm"
|
|
459
|
+
:to="source.source"
|
|
460
|
+
target="_blank"
|
|
461
|
+
external
|
|
462
|
+
:padded="false"
|
|
463
|
+
/>
|
|
464
|
+
</div>
|
|
465
|
+
</section>
|
|
466
|
+
|
|
467
|
+
<!-- ===== TRANSCRIPT ===== -->
|
|
468
|
+
<section class="rounded-xl border border-default bg-elevated/10 p-5">
|
|
469
|
+
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap">
|
|
470
|
+
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted">
|
|
471
|
+
Transcript
|
|
472
|
+
</h2>
|
|
473
|
+
<div class="flex items-center gap-3">
|
|
474
|
+
<span v-if="wordCount > 0" class="text-xs font-mono text-muted">
|
|
475
|
+
{{ wordCount }} words
|
|
476
|
+
<template v-if="source.duration > 0">· {{ formatDuration(source.duration) }}</template>
|
|
477
|
+
</span>
|
|
478
|
+
<UButton
|
|
479
|
+
v-if="source.transcript"
|
|
480
|
+
label="Copy"
|
|
481
|
+
icon="i-lucide-copy"
|
|
482
|
+
variant="ghost"
|
|
483
|
+
size="xs"
|
|
484
|
+
aria-label="Copy transcript to clipboard"
|
|
485
|
+
@click="copyTranscript"
|
|
486
|
+
/>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
<p v-if="!source.transcript" class="text-sm text-muted py-2">
|
|
490
|
+
No transcript available.
|
|
491
|
+
</p>
|
|
492
|
+
<div
|
|
493
|
+
v-else
|
|
494
|
+
class="max-h-96 overflow-y-auto rounded-lg border border-default bg-default/40 p-4"
|
|
495
|
+
>
|
|
496
|
+
<p class="text-sm leading-relaxed whitespace-pre-wrap font-mono text-highlighted/90">
|
|
497
|
+
{{ source.transcript }}
|
|
498
|
+
</p>
|
|
499
|
+
</div>
|
|
500
|
+
</section>
|
|
501
|
+
|
|
502
|
+
<!-- ===== KNOWLEDGE (CHUNKS) ===== -->
|
|
503
|
+
<section class="rounded-xl border border-default bg-elevated/10 p-5">
|
|
504
|
+
<div class="flex items-center justify-between gap-3 mb-4">
|
|
505
|
+
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted">
|
|
506
|
+
Knowledge attributed
|
|
507
|
+
</h2>
|
|
508
|
+
<UBadge
|
|
509
|
+
:label="`${source.chunk_count} chunk${source.chunk_count === 1 ? '' : 's'}`"
|
|
510
|
+
variant="subtle"
|
|
511
|
+
size="sm"
|
|
512
|
+
/>
|
|
513
|
+
</div>
|
|
514
|
+
<p v-if="!source.chunks?.length" class="text-sm text-muted py-2">
|
|
515
|
+
No chunks indexed from this source.
|
|
516
|
+
</p>
|
|
517
|
+
<ul v-else class="space-y-2">
|
|
518
|
+
<li
|
|
519
|
+
v-for="(chunk, idx) in source.chunks"
|
|
520
|
+
:key="idx"
|
|
521
|
+
class="rounded-lg border border-default bg-default/30 p-3"
|
|
522
|
+
>
|
|
523
|
+
<button
|
|
524
|
+
type="button"
|
|
525
|
+
class="flex items-start gap-2 w-full text-left"
|
|
526
|
+
:aria-expanded="expanded.has(idx)"
|
|
527
|
+
:aria-label="expanded.has(idx) ? 'Collapse chunk' : 'Expand chunk'"
|
|
528
|
+
@click="toggleChunk(idx)"
|
|
529
|
+
>
|
|
530
|
+
<UIcon
|
|
531
|
+
:name="expanded.has(idx) ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
|
|
532
|
+
class="size-4 text-muted mt-0.5 shrink-0"
|
|
533
|
+
/>
|
|
534
|
+
<div class="flex-1 min-w-0">
|
|
535
|
+
<p
|
|
536
|
+
v-if="chunk.heading"
|
|
537
|
+
class="text-sm font-semibold text-highlighted mb-1"
|
|
538
|
+
>
|
|
539
|
+
{{ chunk.heading }}
|
|
540
|
+
</p>
|
|
541
|
+
<p
|
|
542
|
+
class="text-sm text-muted whitespace-pre-wrap"
|
|
543
|
+
:class="expanded.has(idx) ? '' : 'line-clamp-2'"
|
|
544
|
+
>
|
|
545
|
+
{{ chunk.text }}
|
|
546
|
+
</p>
|
|
547
|
+
</div>
|
|
548
|
+
</button>
|
|
549
|
+
</li>
|
|
550
|
+
</ul>
|
|
551
|
+
</section>
|
|
552
|
+
|
|
553
|
+
<!-- ===== AGENTS ===== -->
|
|
554
|
+
<section class="rounded-xl border border-default bg-elevated/10 p-5">
|
|
555
|
+
<div class="flex items-start justify-between gap-3 mb-4 flex-wrap">
|
|
556
|
+
<div class="flex-1 min-w-0 space-y-1">
|
|
557
|
+
<div class="flex items-center gap-2">
|
|
558
|
+
<UIcon name="i-lucide-users" class="size-4 text-muted" />
|
|
559
|
+
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted">
|
|
560
|
+
Agents
|
|
561
|
+
</h2>
|
|
562
|
+
</div>
|
|
563
|
+
<p class="text-sm text-muted">
|
|
564
|
+
Agents whose expertise matches this source — suggested to learn from it.
|
|
565
|
+
</p>
|
|
566
|
+
</div>
|
|
567
|
+
<div class="flex flex-col items-end gap-1">
|
|
568
|
+
<UButton
|
|
569
|
+
label="Generate proposal"
|
|
570
|
+
icon="i-lucide-file-output"
|
|
571
|
+
variant="outline"
|
|
572
|
+
size="sm"
|
|
573
|
+
:loading="proposalPending"
|
|
574
|
+
:disabled="agentMatchesLoading || !agentMatches.length"
|
|
575
|
+
aria-label="Generate a review-only learning proposal for the matched agents"
|
|
576
|
+
@click="generateProposal"
|
|
577
|
+
/>
|
|
578
|
+
<p class="text-xs text-muted/70 max-w-xs text-right">
|
|
579
|
+
Generates a review-only proposal — it never edits agent files automatically.
|
|
580
|
+
</p>
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
|
|
584
|
+
<!-- Loading -->
|
|
585
|
+
<div
|
|
586
|
+
v-if="agentMatchesLoading"
|
|
587
|
+
class="flex items-center gap-2 py-4 text-sm text-muted"
|
|
588
|
+
>
|
|
589
|
+
<UIcon name="i-lucide-loader-2" class="size-4 animate-spin" />
|
|
590
|
+
Finding relevant agents…
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<!-- Embedder offline -->
|
|
594
|
+
<p
|
|
595
|
+
v-else-if="agentMatchesReason === 'embedder unavailable'"
|
|
596
|
+
class="text-sm text-muted py-2"
|
|
597
|
+
>
|
|
598
|
+
Semantic matching is offline (vector embeddings unavailable). Install fastembed to enable agent suggestions.
|
|
599
|
+
</p>
|
|
600
|
+
|
|
601
|
+
<!-- No matches / no source text -->
|
|
602
|
+
<p
|
|
603
|
+
v-else-if="!agentMatches.length"
|
|
604
|
+
class="text-sm text-muted py-2"
|
|
605
|
+
>
|
|
606
|
+
No agent suggestions for this source yet.
|
|
607
|
+
</p>
|
|
608
|
+
|
|
609
|
+
<!-- Matches -->
|
|
610
|
+
<ul v-else class="space-y-2">
|
|
611
|
+
<li
|
|
612
|
+
v-for="agent in agentMatches"
|
|
613
|
+
:key="agent.id"
|
|
614
|
+
class="rounded-lg border border-default bg-default/30 p-4"
|
|
615
|
+
>
|
|
616
|
+
<div class="flex items-start justify-between gap-3 flex-wrap">
|
|
617
|
+
<div class="min-w-0 space-y-1">
|
|
618
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
619
|
+
<NuxtLink
|
|
620
|
+
:to="`/agents/${agent.id}`"
|
|
621
|
+
class="text-sm font-semibold text-highlighted hover:text-primary transition-colors"
|
|
622
|
+
>
|
|
623
|
+
{{ agent.name }}
|
|
624
|
+
</NuxtLink>
|
|
625
|
+
<UBadge
|
|
626
|
+
v-if="agent.department"
|
|
627
|
+
:label="agent.department"
|
|
628
|
+
variant="subtle"
|
|
629
|
+
color="neutral"
|
|
630
|
+
size="xs"
|
|
631
|
+
/>
|
|
632
|
+
</div>
|
|
633
|
+
<p v-if="agent.role" class="text-xs text-muted">
|
|
634
|
+
{{ agent.role }}
|
|
635
|
+
</p>
|
|
636
|
+
</div>
|
|
637
|
+
<div class="flex flex-col items-end gap-1 shrink-0 min-w-32">
|
|
638
|
+
<span class="text-xs font-mono text-muted">
|
|
639
|
+
{{ scorePercent(agent.score) }}% match
|
|
640
|
+
</span>
|
|
641
|
+
<UProgress
|
|
642
|
+
:value="scorePercent(agent.score)"
|
|
643
|
+
:max="100"
|
|
644
|
+
size="xs"
|
|
645
|
+
class="w-32"
|
|
646
|
+
:aria-label="`${agent.name} relevance ${scorePercent(agent.score)} percent`"
|
|
647
|
+
/>
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
<div
|
|
651
|
+
v-if="agent.matched_terms?.length"
|
|
652
|
+
class="mt-3 flex flex-wrap gap-1.5"
|
|
653
|
+
>
|
|
654
|
+
<UBadge
|
|
655
|
+
v-for="term in agent.matched_terms"
|
|
656
|
+
:key="term"
|
|
657
|
+
:label="term"
|
|
658
|
+
variant="soft"
|
|
659
|
+
color="primary"
|
|
660
|
+
size="xs"
|
|
661
|
+
/>
|
|
662
|
+
</div>
|
|
663
|
+
</li>
|
|
664
|
+
</ul>
|
|
665
|
+
</section>
|
|
666
|
+
</div>
|
|
667
|
+
</template>
|
|
668
|
+
</UDashboardPanel>
|
|
669
|
+
</template>
|