selftune 0.2.2 → 0.2.6
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 +11 -0
- package/apps/local-dashboard/dist/assets/index-C75H1Q3n.css +1 -0
- package/apps/local-dashboard/dist/assets/index-axE4kz3Q.js +15 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-r2k_Ku_V.js +346 -0
- package/apps/local-dashboard/dist/index.html +3 -3
- package/cli/selftune/analytics.ts +354 -0
- package/cli/selftune/badge/badge.ts +2 -2
- package/cli/selftune/dashboard-server.ts +3 -3
- package/cli/selftune/evolution/evolve-body.ts +1 -1
- package/cli/selftune/evolution/evolve.ts +1 -1
- package/cli/selftune/index.ts +15 -1
- package/cli/selftune/init.ts +5 -1
- package/cli/selftune/observability.ts +63 -2
- package/cli/selftune/orchestrate.ts +1 -1
- package/cli/selftune/quickstart.ts +1 -1
- package/cli/selftune/status.ts +2 -2
- package/cli/selftune/types.ts +1 -0
- package/cli/selftune/utils/llm-call.ts +2 -1
- package/package.json +6 -4
- package/packages/ui/README.md +113 -0
- package/packages/ui/index.ts +10 -0
- package/packages/ui/package.json +62 -0
- package/packages/ui/src/components/ActivityTimeline.tsx +171 -0
- package/packages/ui/src/components/EvidenceViewer.tsx +718 -0
- package/packages/ui/src/components/EvolutionTimeline.tsx +252 -0
- package/packages/ui/src/components/InfoTip.tsx +19 -0
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +164 -0
- package/packages/ui/src/components/index.ts +7 -0
- package/packages/ui/src/components/section-cards.tsx +155 -0
- package/packages/ui/src/components/skill-health-grid.tsx +686 -0
- package/packages/ui/src/lib/constants.tsx +43 -0
- package/packages/ui/src/lib/format.ts +37 -0
- package/packages/ui/src/lib/index.ts +3 -0
- package/packages/ui/src/lib/utils.ts +6 -0
- package/packages/ui/src/primitives/badge.tsx +52 -0
- package/packages/ui/src/primitives/button.tsx +58 -0
- package/packages/ui/src/primitives/card.tsx +103 -0
- package/packages/ui/src/primitives/checkbox.tsx +27 -0
- package/packages/ui/src/primitives/collapsible.tsx +7 -0
- package/packages/ui/src/primitives/dropdown-menu.tsx +266 -0
- package/packages/ui/src/primitives/index.ts +55 -0
- package/packages/ui/src/primitives/label.tsx +20 -0
- package/packages/ui/src/primitives/select.tsx +197 -0
- package/packages/ui/src/primitives/table.tsx +114 -0
- package/packages/ui/src/primitives/tabs.tsx +82 -0
- package/packages/ui/src/primitives/tooltip.tsx +64 -0
- package/packages/ui/src/types.ts +87 -0
- package/packages/ui/tsconfig.json +17 -0
- package/skill/SKILL.md +3 -0
- package/skill/Workflows/Telemetry.md +59 -0
- package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +0 -15
- package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +0 -1
- package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +0 -346
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import { Badge } from "../primitives/badge"
|
|
3
|
+
import { cn } from "../lib/utils"
|
|
4
|
+
import type { EvalSnapshot, EvolutionEntry } from "../types"
|
|
5
|
+
import { timeAgo } from "../lib/format"
|
|
6
|
+
import {
|
|
7
|
+
CircleDotIcon,
|
|
8
|
+
RocketIcon,
|
|
9
|
+
ShieldCheckIcon,
|
|
10
|
+
XCircleIcon,
|
|
11
|
+
UndoIcon,
|
|
12
|
+
TrendingUpIcon,
|
|
13
|
+
TrendingDownIcon,
|
|
14
|
+
ChevronDownIcon,
|
|
15
|
+
ChevronRightIcon,
|
|
16
|
+
} from "lucide-react"
|
|
17
|
+
|
|
18
|
+
const ACTION_ICON: Record<string, React.ReactNode> = {
|
|
19
|
+
created: <CircleDotIcon className="size-3.5" />,
|
|
20
|
+
validated: <ShieldCheckIcon className="size-3.5" />,
|
|
21
|
+
deployed: <RocketIcon className="size-3.5" />,
|
|
22
|
+
rejected: <XCircleIcon className="size-3.5" />,
|
|
23
|
+
rolled_back: <UndoIcon className="size-3.5" />,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ACTION_COLOR: Record<string, string> = {
|
|
27
|
+
created: "bg-blue-500",
|
|
28
|
+
validated: "bg-amber-500",
|
|
29
|
+
deployed: "bg-emerald-500",
|
|
30
|
+
rejected: "bg-red-500",
|
|
31
|
+
rolled_back: "bg-red-400",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ACTION_RING: Record<string, string> = {
|
|
35
|
+
created: "ring-blue-500/30",
|
|
36
|
+
validated: "ring-amber-500/30",
|
|
37
|
+
deployed: "ring-emerald-500/30",
|
|
38
|
+
rejected: "ring-red-500/30",
|
|
39
|
+
rolled_back: "ring-red-400/30",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ACTION_LINE: Record<string, string> = {
|
|
43
|
+
created: "bg-blue-500/30",
|
|
44
|
+
validated: "bg-amber-500/30",
|
|
45
|
+
deployed: "bg-emerald-500/30",
|
|
46
|
+
rejected: "bg-red-500/30",
|
|
47
|
+
rolled_back: "bg-red-400/30",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface Props {
|
|
51
|
+
entries: EvolutionEntry[]
|
|
52
|
+
selectedProposalId: string | null
|
|
53
|
+
onSelect: (proposalId: string) => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Group evolution entries by proposal_id, ordered newest-first. */
|
|
57
|
+
function groupByProposal(entries: EvolutionEntry[]) {
|
|
58
|
+
const map = new Map<string, EvolutionEntry[]>()
|
|
59
|
+
for (const e of entries) {
|
|
60
|
+
const group = map.get(e.proposal_id) ?? []
|
|
61
|
+
group.push(e)
|
|
62
|
+
map.set(e.proposal_id, group)
|
|
63
|
+
}
|
|
64
|
+
for (const group of map.values()) {
|
|
65
|
+
group.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
|
|
66
|
+
}
|
|
67
|
+
return Array.from(map.entries()).sort((a, b) => {
|
|
68
|
+
const aLast = a[1][a[1].length - 1]
|
|
69
|
+
const bLast = b[1][b[1].length - 1]
|
|
70
|
+
return new Date(bLast.timestamp).getTime() - new Date(aLast.timestamp).getTime()
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function terminalAction(entries: EvolutionEntry[]): string {
|
|
75
|
+
return entries[entries.length - 1].action
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Find the best eval_snapshot across all steps in a proposal group */
|
|
79
|
+
function findEvalSnapshot(steps: EvolutionEntry[]): EvalSnapshot | null {
|
|
80
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
81
|
+
if (steps[i].eval_snapshot) return steps[i].eval_snapshot!
|
|
82
|
+
}
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function PassRateDelta({ snapshot }: { snapshot: EvalSnapshot }) {
|
|
87
|
+
const net = snapshot.net_change
|
|
88
|
+
if (net === undefined || net === null) return null
|
|
89
|
+
const pct = Math.round(net * 100)
|
|
90
|
+
const isPositive = pct > 0
|
|
91
|
+
return (
|
|
92
|
+
<span className={cn(
|
|
93
|
+
"inline-flex items-center gap-0.5 text-[10px] font-mono font-medium",
|
|
94
|
+
isPositive ? "text-emerald-600 dark:text-emerald-400" : "text-red-500",
|
|
95
|
+
)}>
|
|
96
|
+
{isPositive ? <TrendingUpIcon className="size-2.5" /> : <TrendingDownIcon className="size-2.5" />}
|
|
97
|
+
{isPositive ? "+" : ""}{pct}%
|
|
98
|
+
</span>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const LIFECYCLE_STEPS = [
|
|
103
|
+
{ action: "created", label: "Created", desc: "Proposal generated from session data" },
|
|
104
|
+
{ action: "validated", label: "Validated", desc: "Eval tests run, awaiting deployment" },
|
|
105
|
+
{ action: "deployed", label: "Deployed", desc: "Accepted and applied to skill file" },
|
|
106
|
+
{ action: "rejected", label: "Rejected", desc: "Failed validation criteria" },
|
|
107
|
+
{ action: "rolled_back", label: "Rolled Back", desc: "Reverted after deployment" },
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
function LifecycleLegend() {
|
|
111
|
+
const [open, setOpen] = useState(false)
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="px-2 pb-2">
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onClick={() => setOpen(!open)}
|
|
118
|
+
className="flex items-center gap-1 text-[10px] text-muted-foreground/70 hover:text-muted-foreground transition-colors w-full"
|
|
119
|
+
>
|
|
120
|
+
{open ? <ChevronDownIcon className="size-3" /> : <ChevronRightIcon className="size-3" />}
|
|
121
|
+
Lifecycle stages
|
|
122
|
+
</button>
|
|
123
|
+
{open && (
|
|
124
|
+
<div className="mt-1.5 space-y-1.5 rounded-md border bg-muted/30 p-2">
|
|
125
|
+
{LIFECYCLE_STEPS.map((step) => (
|
|
126
|
+
<div key={step.action} className="flex items-start gap-2">
|
|
127
|
+
<div className={cn(
|
|
128
|
+
"size-2 rounded-full mt-1 shrink-0",
|
|
129
|
+
ACTION_COLOR[step.action],
|
|
130
|
+
)} />
|
|
131
|
+
<div className="min-w-0">
|
|
132
|
+
<span className="text-[10px] font-medium">{step.label}</span>
|
|
133
|
+
<p className="text-[10px] text-muted-foreground/70 leading-tight">{step.desc}</p>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function EvolutionTimeline({ entries, selectedProposalId, onSelect }: Props) {
|
|
144
|
+
const groups = groupByProposal(entries)
|
|
145
|
+
|
|
146
|
+
if (groups.length === 0) {
|
|
147
|
+
return (
|
|
148
|
+
<div className="flex items-center justify-center rounded-lg border border-dashed py-6 px-3">
|
|
149
|
+
<p className="text-xs text-muted-foreground">No evolution history yet</p>
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div className="flex flex-col gap-0">
|
|
156
|
+
<h2 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-2 pb-2 sticky top-0 z-10 bg-background">
|
|
157
|
+
Evolution
|
|
158
|
+
</h2>
|
|
159
|
+
<LifecycleLegend />
|
|
160
|
+
<nav className="flex flex-col">
|
|
161
|
+
{groups.map(([proposalId, steps], groupIdx) => {
|
|
162
|
+
const terminal = terminalAction(steps)
|
|
163
|
+
const isSelected = selectedProposalId === proposalId
|
|
164
|
+
const lastStep = steps[steps.length - 1]
|
|
165
|
+
const dotColor = ACTION_COLOR[terminal] ?? "bg-muted-foreground"
|
|
166
|
+
const ringColor = ACTION_RING[terminal] ?? "ring-muted-foreground/30"
|
|
167
|
+
const lineColor = ACTION_LINE[terminal] ?? "bg-border"
|
|
168
|
+
const isLast = groupIdx === groups.length - 1
|
|
169
|
+
const snapshot = findEvalSnapshot(steps)
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div key={proposalId} className="relative flex gap-3">
|
|
173
|
+
{/* Vertical connector line */}
|
|
174
|
+
<div className="flex flex-col items-center">
|
|
175
|
+
<div className={cn(
|
|
176
|
+
"flex items-center justify-center size-7 rounded-full ring-2 text-white shrink-0 z-10",
|
|
177
|
+
dotColor,
|
|
178
|
+
ringColor,
|
|
179
|
+
)}>
|
|
180
|
+
{ACTION_ICON[terminal] ?? <CircleDotIcon className="size-3.5" />}
|
|
181
|
+
</div>
|
|
182
|
+
{!isLast && (
|
|
183
|
+
<div className={cn("w-0.5 flex-1 min-h-[16px]", lineColor)} />
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* Content */}
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onClick={() => onSelect(proposalId)}
|
|
191
|
+
className={cn(
|
|
192
|
+
"flex-1 min-w-0 rounded-md px-2.5 py-2 text-left transition-all mb-1",
|
|
193
|
+
"hover:bg-accent/50",
|
|
194
|
+
isSelected
|
|
195
|
+
? "bg-primary/5 ring-1 ring-primary/20"
|
|
196
|
+
: "",
|
|
197
|
+
)}
|
|
198
|
+
>
|
|
199
|
+
<div className="flex items-center gap-1.5">
|
|
200
|
+
<Badge
|
|
201
|
+
variant={terminal === "deployed" ? "default" : terminal === "rejected" || terminal === "rolled_back" ? "destructive" : "secondary"}
|
|
202
|
+
className="text-[10px] capitalize"
|
|
203
|
+
>
|
|
204
|
+
{terminal.replace("_", " ")}
|
|
205
|
+
</Badge>
|
|
206
|
+
<span className="text-[10px] text-muted-foreground">
|
|
207
|
+
{timeAgo(lastStep.timestamp)}
|
|
208
|
+
</span>
|
|
209
|
+
</div>
|
|
210
|
+
{/* Pass rate delta from eval snapshot */}
|
|
211
|
+
{snapshot && (
|
|
212
|
+
<div className="flex items-center gap-1.5 mt-1">
|
|
213
|
+
<PassRateDelta snapshot={snapshot} />
|
|
214
|
+
{snapshot.before_pass_rate !== undefined && snapshot.after_pass_rate !== undefined && (
|
|
215
|
+
<span className="text-[10px] text-muted-foreground/60 font-mono">
|
|
216
|
+
{Math.round(snapshot.before_pass_rate * 100)}→{Math.round(snapshot.after_pass_rate * 100)}%
|
|
217
|
+
</span>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
<div className="flex items-center gap-1.5 mt-1">
|
|
222
|
+
<span className="text-[10px] font-mono text-muted-foreground/70">
|
|
223
|
+
#{proposalId.slice(0, 8)}
|
|
224
|
+
</span>
|
|
225
|
+
{/* Step dots */}
|
|
226
|
+
{steps.length > 1 && (
|
|
227
|
+
<div className="flex gap-0.5 ml-auto">
|
|
228
|
+
{steps.map((s, i) => (
|
|
229
|
+
<div
|
|
230
|
+
key={`${s.action}-${i}`}
|
|
231
|
+
className={cn(
|
|
232
|
+
"size-1.5 rounded-full",
|
|
233
|
+
ACTION_COLOR[s.action] ?? "bg-muted-foreground",
|
|
234
|
+
)}
|
|
235
|
+
/>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
{lastStep.details && (
|
|
241
|
+
<p className="text-[11px] text-muted-foreground/80 line-clamp-2 mt-1 leading-snug">
|
|
242
|
+
{lastStep.details}
|
|
243
|
+
</p>
|
|
244
|
+
)}
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
)
|
|
248
|
+
})}
|
|
249
|
+
</nav>
|
|
250
|
+
</div>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../primitives/tooltip"
|
|
2
|
+
import { InfoIcon } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
/** Small info icon that shows a tooltip on hover. Used to explain metrics and concepts. */
|
|
5
|
+
export function InfoTip({ text }: { text: string }) {
|
|
6
|
+
return (
|
|
7
|
+
<Tooltip>
|
|
8
|
+
<TooltipTrigger
|
|
9
|
+
className="inline-flex items-center text-muted-foreground/50 hover:text-muted-foreground transition-colors cursor-help"
|
|
10
|
+
onClick={(e) => e.preventDefault()}
|
|
11
|
+
>
|
|
12
|
+
<InfoIcon className="size-3" />
|
|
13
|
+
</TooltipTrigger>
|
|
14
|
+
<TooltipContent side="top" className="max-w-[220px]">
|
|
15
|
+
{text}
|
|
16
|
+
</TooltipContent>
|
|
17
|
+
</Tooltip>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import { Badge } from "../primitives/badge"
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from "../primitives/card"
|
|
10
|
+
import {
|
|
11
|
+
Collapsible,
|
|
12
|
+
CollapsibleContent,
|
|
13
|
+
CollapsibleTrigger,
|
|
14
|
+
} from "../primitives/collapsible"
|
|
15
|
+
import type { OrchestrateRunReport, OrchestrateRunSkillAction } from "../types"
|
|
16
|
+
import { timeAgo } from "../lib/format"
|
|
17
|
+
import {
|
|
18
|
+
BotIcon,
|
|
19
|
+
CheckCircleIcon,
|
|
20
|
+
ChevronRightIcon,
|
|
21
|
+
EyeIcon,
|
|
22
|
+
SkipForwardIcon,
|
|
23
|
+
ZapIcon,
|
|
24
|
+
} from "lucide-react"
|
|
25
|
+
|
|
26
|
+
const ACTION_ICON: Record<string, React.ReactNode> = {
|
|
27
|
+
evolve: <ZapIcon className="size-3 text-amber-500" />,
|
|
28
|
+
watch: <EyeIcon className="size-3 text-blue-500" />,
|
|
29
|
+
skip: <SkipForwardIcon className="size-3 text-muted-foreground" />,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function SkillActionRow({ action }: { action: OrchestrateRunSkillAction }) {
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex items-start gap-2 py-1">
|
|
35
|
+
<div className="mt-0.5 shrink-0">{ACTION_ICON[action.action] ?? null}</div>
|
|
36
|
+
<div className="flex-1 min-w-0">
|
|
37
|
+
<div className="flex items-center gap-1.5">
|
|
38
|
+
<span className="text-xs font-medium truncate">{action.skill}</span>
|
|
39
|
+
<Badge
|
|
40
|
+
variant={
|
|
41
|
+
action.rolledBack ? "destructive"
|
|
42
|
+
: action.action === "evolve" && action.deployed ? "default"
|
|
43
|
+
: action.action === "evolve" ? "secondary"
|
|
44
|
+
: action.action === "watch" ? "outline"
|
|
45
|
+
: "secondary"
|
|
46
|
+
}
|
|
47
|
+
className="text-[10px] h-4 px-1.5 shrink-0"
|
|
48
|
+
>
|
|
49
|
+
{action.rolledBack ? "rolled back"
|
|
50
|
+
: action.action === "evolve" && action.deployed ? "deployed"
|
|
51
|
+
: action.action === "evolve" ? "evolved"
|
|
52
|
+
: action.action}
|
|
53
|
+
</Badge>
|
|
54
|
+
{action.alert && (
|
|
55
|
+
<Badge variant="destructive" className="text-[10px] h-4 px-1.5 shrink-0">
|
|
56
|
+
alert
|
|
57
|
+
</Badge>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
<p className="text-[11px] text-muted-foreground line-clamp-1">{action.reason}</p>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function RunCard({ run }: { run: OrchestrateRunReport }) {
|
|
67
|
+
const [open, setOpen] = useState(false)
|
|
68
|
+
const nonSkipActions = run.skill_actions.filter((a) => a.action !== "skip")
|
|
69
|
+
const skipActions = run.skill_actions.filter((a) => a.action === "skip")
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Collapsible open={open} onOpenChange={setOpen}>
|
|
73
|
+
<CollapsibleTrigger className="w-full text-left">
|
|
74
|
+
<div className="flex items-start gap-3 py-2 hover:bg-muted/50 rounded-md px-2 -mx-2 transition-colors">
|
|
75
|
+
<div className={`mt-1.5 size-2 shrink-0 rounded-full ${
|
|
76
|
+
run.deployed > 0 ? "bg-emerald-500"
|
|
77
|
+
: run.evolved > 0 ? "bg-amber-400"
|
|
78
|
+
: "bg-muted-foreground/40"
|
|
79
|
+
}`} />
|
|
80
|
+
<div className="flex-1 min-w-0">
|
|
81
|
+
<div className="flex items-center gap-2">
|
|
82
|
+
<span className="text-xs font-mono text-muted-foreground">{timeAgo(run.timestamp)}</span>
|
|
83
|
+
{run.dry_run && (
|
|
84
|
+
<Badge variant="outline" className="text-[10px] h-4 px-1.5">dry-run</Badge>
|
|
85
|
+
)}
|
|
86
|
+
{run.approval_mode === "review" && (
|
|
87
|
+
<Badge variant="outline" className="text-[10px] h-4 px-1.5">review</Badge>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
|
|
91
|
+
{run.deployed > 0 && <span className="text-emerald-600 font-medium">{run.deployed} deployed</span>}
|
|
92
|
+
{run.evolved > 0 && <span>{run.evolved} evolved</span>}
|
|
93
|
+
{run.watched > 0 && <span>{run.watched} watched</span>}
|
|
94
|
+
{run.skipped > 0 && <span>{run.skipped} skipped</span>}
|
|
95
|
+
<span>{(run.elapsed_ms / 1000).toFixed(1)}s</span>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<ChevronRightIcon className={`size-4 text-muted-foreground shrink-0 mt-1 transition-transform duration-200 ${open ? "rotate-90" : ""}`} />
|
|
99
|
+
</div>
|
|
100
|
+
</CollapsibleTrigger>
|
|
101
|
+
<CollapsibleContent>
|
|
102
|
+
<div className="ml-5 pl-3 border-l border-border space-y-0.5 pb-2">
|
|
103
|
+
{nonSkipActions.map((action, i) => (
|
|
104
|
+
<SkillActionRow key={`${action.skill}-${i}`} action={action} />
|
|
105
|
+
))}
|
|
106
|
+
{skipActions.length > 0 && (
|
|
107
|
+
<details className="group">
|
|
108
|
+
<summary className="text-[11px] text-muted-foreground cursor-pointer hover:text-foreground py-1">
|
|
109
|
+
{skipActions.length} skipped
|
|
110
|
+
</summary>
|
|
111
|
+
<div className="space-y-0.5">
|
|
112
|
+
{skipActions.map((action, i) => (
|
|
113
|
+
<SkillActionRow key={`${action.skill}-skip-${i}`} action={action} />
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
</details>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</CollapsibleContent>
|
|
120
|
+
</Collapsible>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function OrchestrateRunsPanel({ runs }: { runs: OrchestrateRunReport[] }) {
|
|
125
|
+
if (runs.length === 0) {
|
|
126
|
+
return (
|
|
127
|
+
<Card>
|
|
128
|
+
<CardHeader>
|
|
129
|
+
<CardTitle className="flex items-center gap-2 text-sm">
|
|
130
|
+
<BotIcon className="size-4" />
|
|
131
|
+
Orchestrate Runs
|
|
132
|
+
</CardTitle>
|
|
133
|
+
</CardHeader>
|
|
134
|
+
<CardContent>
|
|
135
|
+
<p className="text-sm text-muted-foreground text-center py-4">
|
|
136
|
+
No orchestrate runs yet. Run <code className="text-xs bg-muted px-1 py-0.5 rounded">selftune orchestrate</code> to start.
|
|
137
|
+
</p>
|
|
138
|
+
</CardContent>
|
|
139
|
+
</Card>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const totalDeployed = runs.reduce((sum, r) => sum + r.deployed, 0)
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Card>
|
|
147
|
+
<CardHeader>
|
|
148
|
+
<CardTitle className="flex items-center gap-2 text-sm">
|
|
149
|
+
<BotIcon className="size-4" />
|
|
150
|
+
Orchestrate Runs
|
|
151
|
+
</CardTitle>
|
|
152
|
+
<CardDescription>
|
|
153
|
+
{runs.length} recent run{runs.length !== 1 ? "s" : ""}
|
|
154
|
+
{totalDeployed > 0 && <> · {totalDeployed} total deployments</>}
|
|
155
|
+
</CardDescription>
|
|
156
|
+
</CardHeader>
|
|
157
|
+
<CardContent className="space-y-0">
|
|
158
|
+
{runs.slice(0, 10).map((run) => (
|
|
159
|
+
<RunCard key={run.run_id} run={run} />
|
|
160
|
+
))}
|
|
161
|
+
</CardContent>
|
|
162
|
+
</Card>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { ActivityPanel } from "./ActivityTimeline";
|
|
2
|
+
export { EvidenceViewer } from "./EvidenceViewer";
|
|
3
|
+
export { EvolutionTimeline } from "./EvolutionTimeline";
|
|
4
|
+
export { InfoTip } from "./InfoTip";
|
|
5
|
+
export { OrchestrateRunsPanel } from "./OrchestrateRunsPanel";
|
|
6
|
+
export { SectionCards } from "./section-cards";
|
|
7
|
+
export { SkillHealthGrid } from "./skill-health-grid";
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Badge } from "../primitives/badge"
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardAction,
|
|
5
|
+
CardDescription,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
} from "../primitives/card"
|
|
9
|
+
import { InfoTip } from "./InfoTip"
|
|
10
|
+
import {
|
|
11
|
+
TrendingUpIcon,
|
|
12
|
+
TrendingDownIcon,
|
|
13
|
+
AlertTriangleIcon,
|
|
14
|
+
ActivityIcon,
|
|
15
|
+
EyeIcon,
|
|
16
|
+
FlaskConicalIcon,
|
|
17
|
+
LayersIcon,
|
|
18
|
+
SearchXIcon,
|
|
19
|
+
} from "lucide-react"
|
|
20
|
+
|
|
21
|
+
interface SectionCardsProps {
|
|
22
|
+
skillsCount: number
|
|
23
|
+
avgPassRate: number | null
|
|
24
|
+
unmatchedCount: number
|
|
25
|
+
sessionsCount: number
|
|
26
|
+
pendingCount: number
|
|
27
|
+
evidenceCount: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function SectionCards({
|
|
31
|
+
skillsCount,
|
|
32
|
+
avgPassRate,
|
|
33
|
+
unmatchedCount,
|
|
34
|
+
sessionsCount,
|
|
35
|
+
pendingCount,
|
|
36
|
+
evidenceCount,
|
|
37
|
+
}: SectionCardsProps) {
|
|
38
|
+
const passRateStr = avgPassRate !== null ? `${Math.round(avgPassRate * 100)}%` : "--"
|
|
39
|
+
const passRateGood = avgPassRate !== null && avgPassRate >= 0.7
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-3">
|
|
43
|
+
<Card className="@container/card">
|
|
44
|
+
<CardHeader>
|
|
45
|
+
<CardDescription className="flex items-center gap-1.5">
|
|
46
|
+
<LayersIcon className="size-3.5" />
|
|
47
|
+
Skills Monitored
|
|
48
|
+
<InfoTip text="Total number of skills detected and being tracked by selftune" />
|
|
49
|
+
</CardDescription>
|
|
50
|
+
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
|
51
|
+
{skillsCount}
|
|
52
|
+
</CardTitle>
|
|
53
|
+
<CardAction>
|
|
54
|
+
<Badge variant="outline">
|
|
55
|
+
<ActivityIcon className="size-3" />
|
|
56
|
+
live
|
|
57
|
+
</Badge>
|
|
58
|
+
</CardAction>
|
|
59
|
+
</CardHeader>
|
|
60
|
+
</Card>
|
|
61
|
+
|
|
62
|
+
<Card className="@container/card">
|
|
63
|
+
<CardHeader>
|
|
64
|
+
<CardDescription className="flex items-center gap-1.5">
|
|
65
|
+
<FlaskConicalIcon className="size-3.5" />
|
|
66
|
+
Avg Pass Rate
|
|
67
|
+
<InfoTip text="Average percentage of eval test cases that passed across all graded skills (5+ checks)" />
|
|
68
|
+
</CardDescription>
|
|
69
|
+
<CardTitle className={`text-2xl font-semibold tabular-nums @[250px]/card:text-3xl ${!passRateGood && avgPassRate !== null ? "text-red-600" : ""}`}>
|
|
70
|
+
{passRateStr}
|
|
71
|
+
</CardTitle>
|
|
72
|
+
{avgPassRate !== null && (
|
|
73
|
+
<CardAction>
|
|
74
|
+
<Badge variant={passRateGood ? "outline" : "destructive"}>
|
|
75
|
+
{passRateGood ? (
|
|
76
|
+
<TrendingUpIcon className="size-3" />
|
|
77
|
+
) : (
|
|
78
|
+
<TrendingDownIcon className="size-3" />
|
|
79
|
+
)}
|
|
80
|
+
{passRateStr}
|
|
81
|
+
</Badge>
|
|
82
|
+
</CardAction>
|
|
83
|
+
)}
|
|
84
|
+
</CardHeader>
|
|
85
|
+
</Card>
|
|
86
|
+
|
|
87
|
+
<Card className="@container/card">
|
|
88
|
+
<CardHeader>
|
|
89
|
+
<CardDescription className="flex items-center gap-1.5">
|
|
90
|
+
<SearchXIcon className="size-3.5" />
|
|
91
|
+
Unmatched Queries
|
|
92
|
+
<InfoTip text="User prompts that didn't match any skill's trigger criteria — potential gaps in coverage" />
|
|
93
|
+
</CardDescription>
|
|
94
|
+
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
|
95
|
+
{unmatchedCount}
|
|
96
|
+
</CardTitle>
|
|
97
|
+
{unmatchedCount > 0 && (
|
|
98
|
+
<CardAction>
|
|
99
|
+
<Badge variant="destructive">
|
|
100
|
+
<AlertTriangleIcon className="size-3" />
|
|
101
|
+
needs attention
|
|
102
|
+
</Badge>
|
|
103
|
+
</CardAction>
|
|
104
|
+
)}
|
|
105
|
+
</CardHeader>
|
|
106
|
+
</Card>
|
|
107
|
+
|
|
108
|
+
<Card className="@container/card">
|
|
109
|
+
<CardHeader>
|
|
110
|
+
<CardDescription className="flex items-center gap-1.5">
|
|
111
|
+
<ActivityIcon className="size-3.5" />
|
|
112
|
+
Sessions
|
|
113
|
+
<InfoTip text="Total agent sessions that have been recorded and analyzed" />
|
|
114
|
+
</CardDescription>
|
|
115
|
+
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
|
116
|
+
{sessionsCount}
|
|
117
|
+
</CardTitle>
|
|
118
|
+
</CardHeader>
|
|
119
|
+
</Card>
|
|
120
|
+
|
|
121
|
+
<Card className="@container/card">
|
|
122
|
+
<CardHeader>
|
|
123
|
+
<CardDescription className="flex items-center gap-1.5">
|
|
124
|
+
<AlertTriangleIcon className="size-3.5" />
|
|
125
|
+
Pending Proposals
|
|
126
|
+
<InfoTip text="Evolution proposals that have been generated but not yet validated or deployed" />
|
|
127
|
+
</CardDescription>
|
|
128
|
+
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
|
129
|
+
{pendingCount}
|
|
130
|
+
</CardTitle>
|
|
131
|
+
{pendingCount > 0 && (
|
|
132
|
+
<CardAction>
|
|
133
|
+
<Badge variant="secondary">
|
|
134
|
+
awaiting review
|
|
135
|
+
</Badge>
|
|
136
|
+
</CardAction>
|
|
137
|
+
)}
|
|
138
|
+
</CardHeader>
|
|
139
|
+
</Card>
|
|
140
|
+
|
|
141
|
+
<Card className="@container/card">
|
|
142
|
+
<CardHeader>
|
|
143
|
+
<CardDescription className="flex items-center gap-1.5">
|
|
144
|
+
<EyeIcon className="size-3.5" />
|
|
145
|
+
Total Evidence
|
|
146
|
+
<InfoTip text="Number of evidence entries documenting skill changes with before/after validation results" />
|
|
147
|
+
</CardDescription>
|
|
148
|
+
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
|
149
|
+
{evidenceCount}
|
|
150
|
+
</CardTitle>
|
|
151
|
+
</CardHeader>
|
|
152
|
+
</Card>
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|