- Taskbar: the theme toggle is icon-only now, matching the Settings button beside it. The theme name moves into the title/aria-label so an icon-only control still has an accessible name and the current theme stays discoverable on hover. - Pages: Fleet, Knowledge, Learning, Ops and Signals used p-4 (or p-4 md:p-6) while Tasks used p-2, so windows didn't line up. All now p-2. App Store and Settings are deliberately untouched — they have no root padding, using a full-bleed header whose border spans the window; insetting them would break that. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
175 lines
6.0 KiB
Svelte
175 lines
6.0 KiB
Svelte
<script lang="ts">
|
|
import { onMount, tick } from 'svelte'
|
|
import uPlot from 'uplot'
|
|
import 'uplot/dist/uPlot.min.css'
|
|
import {
|
|
fetchLearningTimeline,
|
|
fetchLearningTrend,
|
|
fetchPatterns,
|
|
fetchSkills,
|
|
type CapabilityTimelineItem,
|
|
type TrendBucket,
|
|
type Pattern,
|
|
type Skill
|
|
} from '$lib/api'
|
|
import * as Card from '$lib/components/ui/card'
|
|
import { Badge } from '$lib/components/ui/badge'
|
|
import TrendingUpIcon from '@lucide/svelte/icons/trending-up'
|
|
import SparklesIcon from '@lucide/svelte/icons/sparkles'
|
|
|
|
let timeline = $state<CapabilityTimelineItem[]>([])
|
|
let trend = $state<TrendBucket[]>([])
|
|
let patterns = $state<Pattern[]>([])
|
|
let skills = $state<Skill[]>([])
|
|
let loading = $state(true)
|
|
let chartEl = $state<HTMLDivElement | null>(null)
|
|
|
|
async function load() {
|
|
loading = true
|
|
const [t, tr, p, s] = await Promise.all([
|
|
fetchLearningTimeline(),
|
|
fetchLearningTrend(),
|
|
fetchPatterns(),
|
|
fetchSkills()
|
|
])
|
|
timeline = t
|
|
trend = tr
|
|
patterns = p
|
|
skills = s
|
|
loading = false
|
|
await tick()
|
|
renderChart()
|
|
}
|
|
|
|
onMount(load)
|
|
|
|
function renderChart() {
|
|
if (!chartEl || trend.length === 0) return
|
|
chartEl.innerHTML = ''
|
|
const xs = trend.map((b) => new Date(b.day).getTime() / 1000)
|
|
const succ = trend.map((b) => b.successes)
|
|
const fail = trend.map((b) => b.failures)
|
|
new uPlot(
|
|
{
|
|
width: chartEl.clientWidth || 600,
|
|
height: 180,
|
|
series: [
|
|
{},
|
|
{ label: 'succeeded', stroke: '#3fb950', width: 2 },
|
|
{ label: 'failed', stroke: '#f85149', width: 2 }
|
|
],
|
|
axes: [{ stroke: '#8b949e' }, { stroke: '#8b949e' }],
|
|
scales: { x: { time: true } },
|
|
legend: { show: true }
|
|
},
|
|
[xs, succ, fail],
|
|
chartEl
|
|
)
|
|
}
|
|
|
|
function fmtDate(iso: string | null): string {
|
|
if (!iso) return '—'
|
|
return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
}
|
|
|
|
function timelineVariant(item: CapabilityTimelineItem): 'default' | 'secondary' | 'destructive' {
|
|
if (item.total === 0 || item.successes === 0) return 'destructive'
|
|
if (item.successes === item.total) return 'default'
|
|
return 'secondary'
|
|
}
|
|
</script>
|
|
|
|
<div class="flex h-full flex-col gap-4 overflow-y-auto p-2">
|
|
<h1 class="text-lg font-semibold">Learning</h1>
|
|
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<Card.Title class="text-sm">Execution outcomes — last 30 days</Card.Title>
|
|
<Card.Description class="text-xs">Every gated action, by day it ran, succeeded vs failed.</Card.Description>
|
|
</Card.Header>
|
|
<Card.Content>
|
|
{#if trend.length === 0}
|
|
{#if !loading}<p class="py-8 text-center text-sm text-muted-foreground">No executions in the last 30 days yet.</p>{/if}
|
|
{:else}
|
|
<div bind:this={chartEl} class="w-full"></div>
|
|
{/if}
|
|
</Card.Content>
|
|
</Card.Root>
|
|
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<Card.Title class="flex items-center gap-1.5 text-sm"><TrendingUpIcon class="size-4" /> Capability timeline</Card.Title>
|
|
<Card.Description class="text-xs">What Nomos has learned to do, ordered by when it first succeeded.</Card.Description>
|
|
</Card.Header>
|
|
<Card.Content>
|
|
<div class="flex flex-col gap-2">
|
|
{#each timeline as item (item.verb)}
|
|
<div class="flex items-center justify-between gap-3 rounded-lg border px-3 py-2">
|
|
<div>
|
|
<span class="font-mono text-sm">{item.verb}</span>
|
|
<span class="ml-2 text-xs text-muted-foreground">
|
|
{item.first_success ? `first succeeded ${fmtDate(item.first_success)}` : 'no successes yet'}
|
|
</span>
|
|
</div>
|
|
<Badge variant={timelineVariant(item)}>{item.successes}/{item.total}</Badge>
|
|
</div>
|
|
{:else}
|
|
{#if !loading}<p class="py-8 text-center text-sm text-muted-foreground">No executions yet.</p>{/if}
|
|
{/each}
|
|
</div>
|
|
</Card.Content>
|
|
</Card.Root>
|
|
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<Card.Title class="text-sm">Patterns</Card.Title>
|
|
<Card.Description class="text-xs">Statistically validated behaviors, extracted from outcome feedback.</Card.Description>
|
|
</Card.Header>
|
|
<Card.Content>
|
|
{#if patterns.length === 0}
|
|
<p class="py-6 text-center text-sm text-muted-foreground">
|
|
No patterns learned yet — patterns emerge once outcome feedback is recorded for repeated actions.
|
|
</p>
|
|
{:else}
|
|
<div class="flex flex-col gap-2">
|
|
{#each patterns as p (p.id)}
|
|
<div class="flex items-center justify-between gap-3 rounded-lg border px-3 py-2">
|
|
<div>
|
|
<span class="text-sm">{p.pattern}</span>
|
|
<span class="ml-2 text-xs text-muted-foreground">{p.applies_type} · {p.action}</span>
|
|
</div>
|
|
<Badge variant="outline">{(p.confidence * 100).toFixed(0)}% conf.</Badge>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</Card.Content>
|
|
</Card.Root>
|
|
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<Card.Title class="flex items-center gap-1.5 text-sm"><SparklesIcon class="size-4" /> Promoted skills</Card.Title>
|
|
<Card.Description class="text-xs">Procedures promoted from validated patterns.</Card.Description>
|
|
</Card.Header>
|
|
<Card.Content>
|
|
{#if skills.length === 0}
|
|
<p class="py-6 text-center text-sm text-muted-foreground">No skills promoted yet.</p>
|
|
{:else}
|
|
<div class="flex flex-col gap-2">
|
|
{#each skills as s (s.id)}
|
|
<div class="flex items-center justify-between gap-3 rounded-lg border px-3 py-2">
|
|
<div>
|
|
<span class="text-sm">{s.name}</span>
|
|
<span class="ml-2 text-xs text-muted-foreground">{s.status}</span>
|
|
</div>
|
|
{#if s.success_rate != null}
|
|
<Badge variant="outline">{(s.success_rate * 100).toFixed(0)}% success</Badge>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</Card.Content>
|
|
</Card.Root>
|
|
</div>
|