feat(ui): M4 — agent activity, knowledge search, audit, correlation grouping
Add three new pages completing the control-room web UI: - Agent activity: polls /agent-activity every 5s, filterable by type/agent - Knowledge search: FTS over /knowledge/search with snippet + entity links - Audit trail: browseable audit log with actor/action/entity filters Enhanced live events page with correlation-id clustering (Groups toggle). Added fetchAgentActivity/searchKnowledge/fetchAudit to the API client. 11 nav items now cover all planned control-room views.
This commit is contained in:
91
web/src/pages/Knowledge.svelte
Normal file
91
web/src/pages/Knowledge.svelte
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import { searchKnowledge, type KnowledgeHit } from '$lib/api'
|
||||
import * as Card from '$lib/components/ui/card'
|
||||
import { Badge } from '$lib/components/ui/badge'
|
||||
import { Input } from '$lib/components/ui/input'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import { ScrollArea } from '$lib/components/ui/scroll-area'
|
||||
import SearchIcon from '@lucide/svelte/icons/search'
|
||||
|
||||
let query = $state('')
|
||||
let results = $state<KnowledgeHit[]>([])
|
||||
let loading = $state(false)
|
||||
let searched = $state(false)
|
||||
|
||||
async function search() {
|
||||
if (!query.trim()) return
|
||||
loading = true
|
||||
results = await searchKnowledge(query)
|
||||
loading = false
|
||||
searched = true
|
||||
}
|
||||
|
||||
function typeVariant(type: string): 'default' | 'secondary' | 'outline' {
|
||||
if (type === 'runbook') return 'secondary'
|
||||
if (type === 'investigation') return 'default'
|
||||
return 'outline'
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-4 p-6">
|
||||
<h1 class="text-lg font-semibold">Knowledge search</h1>
|
||||
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault()
|
||||
search()
|
||||
}}
|
||||
class="flex gap-2"
|
||||
>
|
||||
<div class="relative flex-1 max-w-lg">
|
||||
<SearchIcon class="absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search documents, runbooks, investigations…"
|
||||
bind:value={query}
|
||||
class="pl-8"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={loading || !query.trim()}>
|
||||
{loading ? 'Searching…' : 'Search'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{#if searched}
|
||||
<p class="text-sm text-muted-foreground">{results.length} result{results.length === 1 ? '' : 's'}{query ? ` for "${query}"` : ''}</p>
|
||||
{/if}
|
||||
|
||||
<ScrollArea class="flex-1">
|
||||
<div class="flex flex-col gap-3 pr-4">
|
||||
{#each results as hit (hit.id)}
|
||||
<Card.Root class="cursor-pointer transition-colors hover:bg-muted/50">
|
||||
<Card.Header>
|
||||
<div class="flex items-center gap-2">
|
||||
<Card.Title class="text-sm">{hit.title}</Card.Title>
|
||||
<Badge variant={typeVariant(hit.type)}>{hit.type}</Badge>
|
||||
</div>
|
||||
{#if hit.snippet}
|
||||
<Card.Description class="text-xs">{@html hit.snippet}</Card.Description>
|
||||
{/if}
|
||||
{#if hit.linked_entities?.length}
|
||||
<div class="mt-1 flex flex-wrap gap-1">
|
||||
{#each hit.linked_entities as slug}
|
||||
<button
|
||||
type="button"
|
||||
class="font-mono text-xs text-muted-foreground underline"
|
||||
onclick={() => (location.hash = '#/entity/' + encodeURIComponent(slug))}
|
||||
>
|
||||
{slug}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
{:else}
|
||||
{#if searched && !loading}
|
||||
<p class="py-12 text-center text-muted-foreground">No results found.</p>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
Reference in New Issue
Block a user