selftune 0.2.22 → 0.2.24
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/CHANGELOG.md +6 -0
- package/README.md +95 -15
- package/apps/local-dashboard/dist/assets/index-DgY2KGP-.css +1 -0
- package/apps/local-dashboard/dist/assets/index-Dmx7LPVX.js +15 -0
- package/apps/local-dashboard/dist/assets/vendor-react-C5oyHiV1.js +11 -0
- package/apps/local-dashboard/dist/assets/{vendor-table-BIiI3YhS.js → vendor-table-Bc_bbKd8.js} +1 -1
- package/apps/local-dashboard/dist/assets/vendor-ui-B3BPIYy7.js +1 -0
- package/apps/local-dashboard/dist/index.html +5 -5
- package/cli/selftune/adapters/codex/install.ts +310 -78
- package/cli/selftune/adapters/opencode/install.ts +3 -4
- package/cli/selftune/adapters/pi/hook.ts +273 -0
- package/cli/selftune/adapters/pi/install.ts +207 -0
- package/cli/selftune/alpha-upload/build-payloads.ts +3 -3
- package/cli/selftune/alpha-upload/stage-canonical.ts +17 -11
- package/cli/selftune/auto-update.ts +200 -8
- package/cli/selftune/canonical-export.ts +55 -25
- package/cli/selftune/command-surface.ts +397 -0
- package/cli/selftune/constants.ts +10 -1
- package/cli/selftune/contribute/contribute.ts +64 -13
- package/cli/selftune/contribution-config.ts +57 -3
- package/cli/selftune/contribution-preferences.ts +117 -0
- package/cli/selftune/contribution-signals.ts +8 -4
- package/cli/selftune/contribution-staging.ts +13 -2
- package/cli/selftune/contributions.ts +55 -121
- package/cli/selftune/creator-contributions.ts +29 -10
- package/cli/selftune/cron/setup.ts +7 -3
- package/cli/selftune/dashboard-contract.ts +87 -0
- package/cli/selftune/dashboard-server.ts +168 -17
- package/cli/selftune/dashboard.ts +350 -17
- package/cli/selftune/eval/baseline.ts +21 -5
- package/cli/selftune/eval/execution-eval.ts +170 -0
- package/cli/selftune/eval/family-overlap.ts +2 -2
- package/cli/selftune/eval/hooks-to-evals.ts +228 -82
- package/cli/selftune/eval/import-skillsbench.ts +2 -2
- package/cli/selftune/eval/invocation-classifier.ts +56 -0
- package/cli/selftune/eval/synthetic-evals.ts +5 -3
- package/cli/selftune/eval/unit-test-cli.ts +7 -4
- package/cli/selftune/evolution/apply-proposal.ts +295 -0
- package/cli/selftune/evolution/engines/judge-engine.ts +96 -0
- package/cli/selftune/evolution/engines/replay-engine.ts +180 -0
- package/cli/selftune/evolution/evidence.ts +2 -6
- package/cli/selftune/evolution/evolve-body.ts +152 -38
- package/cli/selftune/evolution/evolve.ts +244 -52
- package/cli/selftune/evolution/rollback.ts +0 -1
- package/cli/selftune/evolution/validate-body.ts +111 -49
- package/cli/selftune/evolution/validate-host-replay.ts +510 -60
- package/cli/selftune/evolution/validate-proposal.ts +11 -150
- package/cli/selftune/evolution/validate-routing.ts +51 -108
- package/cli/selftune/evolution/validation-contract.ts +91 -0
- package/cli/selftune/grading/auto-grade.ts +11 -7
- package/cli/selftune/grading/grade-session.ts +10 -16
- package/cli/selftune/hooks/skill-eval.ts +2 -1
- package/cli/selftune/hooks-shared/types.ts +1 -0
- package/cli/selftune/index.ts +58 -15
- package/cli/selftune/ingestors/claude-replay.ts +15 -10
- package/cli/selftune/ingestors/codex-wrapper.ts +3 -3
- package/cli/selftune/ingestors/opencode-ingest.ts +2 -2
- package/cli/selftune/ingestors/pi-ingest.ts +727 -0
- package/cli/selftune/init.ts +38 -4
- package/cli/selftune/localdb/direct-write.ts +120 -1
- package/cli/selftune/localdb/materialize.ts +6 -7
- package/cli/selftune/localdb/queries/cron.ts +34 -0
- package/cli/selftune/localdb/queries/dashboard.ts +834 -0
- package/cli/selftune/localdb/queries/evolution.ts +158 -0
- package/cli/selftune/localdb/queries/execution.ts +133 -0
- package/cli/selftune/localdb/queries/json.ts +18 -0
- package/cli/selftune/localdb/queries/monitoring.ts +263 -0
- package/cli/selftune/localdb/queries/raw.ts +95 -0
- package/cli/selftune/localdb/queries/staging.ts +270 -0
- package/cli/selftune/localdb/queries/trust.ts +392 -0
- package/cli/selftune/localdb/queries.ts +60 -2162
- package/cli/selftune/localdb/schema.ts +59 -0
- package/cli/selftune/monitoring/watch.ts +96 -29
- package/cli/selftune/normalization.ts +3 -0
- package/cli/selftune/observability.ts +12 -3
- package/cli/selftune/orchestrate/cli.ts +161 -0
- package/cli/selftune/orchestrate/execute.ts +295 -0
- package/cli/selftune/orchestrate/finalize.ts +157 -0
- package/cli/selftune/orchestrate/locks.ts +40 -0
- package/cli/selftune/orchestrate/plan.ts +131 -0
- package/cli/selftune/orchestrate/post-run.ts +59 -0
- package/cli/selftune/orchestrate/prepare.ts +334 -0
- package/cli/selftune/orchestrate/report.ts +182 -0
- package/cli/selftune/orchestrate/runtime.ts +120 -0
- package/cli/selftune/orchestrate/signals.ts +48 -0
- package/cli/selftune/orchestrate.ts +162 -1142
- package/cli/selftune/registry/client.ts +74 -0
- package/cli/selftune/registry/history.ts +54 -0
- package/cli/selftune/registry/index.ts +90 -0
- package/cli/selftune/registry/install.ts +141 -0
- package/cli/selftune/registry/list.ts +44 -0
- package/cli/selftune/registry/push.ts +171 -0
- package/cli/selftune/registry/rollback.ts +49 -0
- package/cli/selftune/registry/status.ts +62 -0
- package/cli/selftune/registry/sync.ts +125 -0
- package/cli/selftune/repair/skill-usage.ts +9 -3
- package/cli/selftune/routes/overview.ts +5 -2
- package/cli/selftune/routes/skill-report.ts +15 -2
- package/cli/selftune/schedule.ts +5 -5
- package/cli/selftune/status.ts +70 -2
- package/cli/selftune/sync.ts +127 -23
- package/cli/selftune/testing-readiness.ts +597 -0
- package/cli/selftune/types.ts +46 -5
- package/cli/selftune/uninstall.ts +2 -1
- package/cli/selftune/utils/canonical-log.ts +1 -9
- package/cli/selftune/utils/cli-error.ts +9 -0
- package/cli/selftune/utils/jsonl.ts +1 -30
- package/cli/selftune/utils/llm-call.ts +126 -6
- package/cli/selftune/utils/skill-discovery.ts +24 -0
- package/cli/selftune/workflows/proposals.ts +184 -0
- package/cli/selftune/workflows/skill-scaffold.ts +241 -0
- package/cli/selftune/workflows/workflows.ts +100 -26
- package/node_modules/@selftune/telemetry-contract/fixtures/complete-push.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +2 -2
- package/node_modules/@selftune/telemetry-contract/fixtures/golden.test.ts +0 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +2 -2
- package/node_modules/@selftune/telemetry-contract/package.json +1 -1
- package/node_modules/@selftune/telemetry-contract/src/index.ts +1 -0
- package/node_modules/@selftune/telemetry-contract/src/schemas.ts +63 -5
- package/node_modules/@selftune/telemetry-contract/src/types.ts +97 -7
- package/node_modules/@selftune/telemetry-contract/tests/compatibility.test.ts +0 -1
- package/package.json +25 -9
- package/packages/dashboard-core/AGENTS.md +18 -0
- package/packages/dashboard-core/README.md +30 -0
- package/packages/dashboard-core/index.ts +3 -0
- package/packages/dashboard-core/package.json +39 -0
- package/packages/dashboard-core/src/chrome/DashboardChrome.tsx +74 -0
- package/packages/dashboard-core/src/chrome/DashboardHeader.tsx +200 -0
- package/packages/dashboard-core/src/chrome/DashboardSidebar.tsx +219 -0
- package/packages/dashboard-core/src/chrome/RuntimeBadge.tsx +46 -0
- package/packages/dashboard-core/src/chrome/index.ts +14 -0
- package/packages/dashboard-core/src/chrome/types.ts +81 -0
- package/packages/dashboard-core/src/chrome/utils.ts +23 -0
- package/packages/dashboard-core/src/gates/FeatureGate.tsx +11 -0
- package/packages/dashboard-core/src/gates/LockedRoute.tsx +29 -0
- package/packages/dashboard-core/src/gates/UpgradeCard.tsx +89 -0
- package/packages/dashboard-core/src/gates/index.ts +3 -0
- package/packages/dashboard-core/src/host/DashboardHostProvider.tsx +62 -0
- package/packages/dashboard-core/src/host/adapter.ts +47 -0
- package/packages/dashboard-core/src/host/capabilities.ts +55 -0
- package/packages/dashboard-core/src/host/index.ts +3 -0
- package/packages/dashboard-core/src/models/analytics.ts +39 -0
- package/packages/dashboard-core/src/models/index.ts +4 -0
- package/packages/dashboard-core/src/models/overview.ts +98 -0
- package/packages/dashboard-core/src/models/runtime.ts +7 -0
- package/packages/dashboard-core/src/models/skills.ts +34 -0
- package/packages/dashboard-core/src/routes/index.ts +2 -0
- package/packages/dashboard-core/src/routes/manifest.test.ts +70 -0
- package/packages/dashboard-core/src/routes/manifest.ts +451 -0
- package/packages/dashboard-core/src/routes/types.ts +39 -0
- package/packages/dashboard-core/src/screens/analytics/AnalyticsScreen.tsx +278 -0
- package/packages/dashboard-core/src/screens/analytics/index.ts +1 -0
- package/packages/dashboard-core/src/screens/index.ts +37 -0
- package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.test.ts +101 -0
- package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.tsx +393 -0
- package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.test.tsx +113 -0
- package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.tsx +72 -0
- package/packages/dashboard-core/src/screens/overview/OverviewCoreSurface.tsx +71 -0
- package/packages/dashboard-core/src/screens/overview/OverviewOnboardingBanner.tsx +90 -0
- package/packages/dashboard-core/src/screens/overview/OverviewRunSummary.tsx +40 -0
- package/packages/dashboard-core/src/screens/overview/index.ts +16 -0
- package/packages/dashboard-core/src/screens/overview/types.ts +13 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportDailyBreakdownSection.tsx +99 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportDataQualityTabContent.tsx +35 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceRail.tsx +71 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceSection.tsx +63 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceTabContent.tsx +25 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportInvocationsSection.tsx +24 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportMissedQueriesSection.tsx +79 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportScaffold.tsx +150 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportSections.test.tsx +224 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.test.tsx +76 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.tsx +88 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportTrendSection.tsx +33 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportTrustBadge.tsx +67 -0
- package/packages/dashboard-core/src/screens/skill-report/index.ts +45 -0
- package/packages/dashboard-core/src/screens/skills/SkillsLibraryScreen.tsx +162 -0
- package/packages/dashboard-core/src/screens/skills/index.ts +6 -0
- package/packages/telemetry-contract/fixtures/complete-push.ts +1 -1
- package/packages/telemetry-contract/fixtures/evidence-only-push.ts +2 -2
- package/packages/telemetry-contract/fixtures/golden.test.ts +0 -1
- package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
- package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +2 -2
- package/packages/telemetry-contract/package.json +1 -1
- package/packages/telemetry-contract/src/index.ts +1 -0
- package/packages/telemetry-contract/src/schemas.ts +63 -5
- package/packages/telemetry-contract/src/types.ts +97 -7
- package/packages/telemetry-contract/tests/compatibility.test.ts +0 -1
- package/packages/ui/AGENTS.md +16 -0
- package/packages/ui/README.md +1 -1
- package/packages/ui/package.json +1 -1
- package/packages/ui/src/components/ActivityTimeline.tsx +152 -168
- package/packages/ui/src/components/AnalyticsCharts.tsx +344 -0
- package/packages/ui/src/components/EvidenceViewer.tsx +229 -464
- package/packages/ui/src/components/EvolutionTimeline.tsx +34 -87
- package/packages/ui/src/components/InfoTip.tsx +1 -2
- package/packages/ui/src/components/InvocationsPanel.tsx +413 -0
- package/packages/ui/src/components/JobHistoryTimeline.tsx +156 -0
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +18 -36
- package/packages/ui/src/components/OverviewPanels.tsx +693 -0
- package/packages/ui/src/components/PipelineStatusBar.tsx +65 -0
- package/packages/ui/src/components/SkillReportGuide.tsx +215 -0
- package/packages/ui/src/components/SkillReportPanels.tsx +919 -0
- package/packages/ui/src/components/SkillsLibrary.tsx +437 -0
- package/packages/ui/src/components/index.ts +56 -1
- package/packages/ui/src/components/section-cards.tsx +18 -35
- package/packages/ui/src/components/skill-health-grid.tsx +47 -37
- package/packages/ui/src/lib/constants.tsx +0 -1
- package/packages/ui/src/primitives/card.tsx +1 -1
- package/packages/ui/src/primitives/checkbox.tsx +1 -1
- package/packages/ui/src/primitives/dropdown-menu.tsx +2 -2
- package/packages/ui/src/primitives/select.tsx +2 -2
- package/packages/ui/src/primitives/tabs.tsx +7 -6
- package/packages/ui/src/types.ts +182 -4
- package/skill/SKILL.md +130 -318
- package/skill/agents/diagnosis-analyst.md +3 -3
- package/skill/agents/evolution-reviewer.md +3 -3
- package/skill/agents/integration-guide.md +3 -3
- package/skill/agents/pattern-analyst.md +2 -2
- package/skill/references/cli-quick-reference.md +89 -0
- package/skill/references/creator-playbook.md +131 -0
- package/skill/references/examples.md +48 -0
- package/skill/references/troubleshooting.md +47 -0
- package/skill/references/version-history.md +1 -1
- package/skill/selftune.contribute.json +11 -0
- package/skill/{Workflows → workflows}/Baseline.md +20 -1
- package/skill/{Workflows → workflows}/Contribute.md +23 -10
- package/skill/{Workflows → workflows}/Contributions.md +13 -5
- package/skill/workflows/CreateTestDeploy.md +170 -0
- package/skill/{Workflows → workflows}/CreatorContributions.md +18 -6
- package/skill/{Workflows → workflows}/Cron.md +1 -1
- package/skill/{Workflows → workflows}/Dashboard.md +20 -0
- package/skill/{Workflows → workflows}/Doctor.md +1 -1
- package/skill/{Workflows → workflows}/Evals.md +67 -2
- package/skill/{Workflows → workflows}/Evolve.md +119 -30
- package/skill/{Workflows → workflows}/EvolveBody.md +41 -1
- package/skill/{Workflows → workflows}/Grade.md +1 -1
- package/skill/{Workflows → workflows}/Ingest.md +60 -2
- package/skill/{Workflows → workflows}/Initialize.md +16 -9
- package/skill/{Workflows → workflows}/Orchestrate.md +13 -3
- package/skill/{Workflows → workflows}/PlatformHooks.md +19 -3
- package/skill/workflows/Registry.md +99 -0
- package/skill/{Workflows → workflows}/Schedule.md +3 -3
- package/skill/workflows/SignalsDashboard.md +87 -0
- package/skill/{Workflows → workflows}/Sync.md +3 -1
- package/skill/{Workflows → workflows}/UnitTest.md +19 -0
- package/skill/{Workflows → workflows}/Watch.md +42 -2
- package/skill/{Workflows → workflows}/Workflows.md +39 -2
- package/apps/local-dashboard/dist/assets/index-D8O-RG1I.js +0 -60
- package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +0 -1
- package/apps/local-dashboard/dist/assets/vendor-react-CKkiCskZ.js +0 -11
- package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +0 -12
- package/cli/selftune/utils/html.ts +0 -27
- package/packages/ui/src/components/RecentActivityFeed.tsx +0 -117
- /package/skill/{Workflows → workflows}/AlphaUpload.md +0 -0
- /package/skill/{Workflows → workflows}/AutoActivation.md +0 -0
- /package/skill/{Workflows → workflows}/Badge.md +0 -0
- /package/skill/{Workflows → workflows}/Composability.md +0 -0
- /package/skill/{Workflows → workflows}/EvolutionMemory.md +0 -0
- /package/skill/{Workflows → workflows}/ExportCanonical.md +0 -0
- /package/skill/{Workflows → workflows}/Hook.md +0 -0
- /package/skill/{Workflows → workflows}/ImportSkillsBench.md +0 -0
- /package/skill/{Workflows → workflows}/Quickstart.md +0 -0
- /package/skill/{Workflows → workflows}/Recover.md +0 -0
- /package/skill/{Workflows → workflows}/RepairSkillUsage.md +0 -0
- /package/skill/{Workflows → workflows}/Replay.md +0 -0
- /package/skill/{Workflows → workflows}/Rollback.md +0 -0
- /package/skill/{Workflows → workflows}/Telemetry.md +0 -0
- /package/skill/{Workflows → workflows}/Uninstall.md +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { RocketIcon } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
export interface OverviewOnboardingBannerProps {
|
|
7
|
+
skillCount: number;
|
|
8
|
+
storageKey?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function OverviewOnboardingBanner({
|
|
12
|
+
skillCount,
|
|
13
|
+
storageKey = "selftune-onboarding-dismissed",
|
|
14
|
+
}: OverviewOnboardingBannerProps) {
|
|
15
|
+
const [dismissed, setDismissed] = useState(() => {
|
|
16
|
+
try {
|
|
17
|
+
return localStorage.getItem(storageKey) === "true";
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (skillCount > 0 || dismissed) return null;
|
|
24
|
+
|
|
25
|
+
const dismiss = () => {
|
|
26
|
+
setDismissed(true);
|
|
27
|
+
try {
|
|
28
|
+
localStorage.setItem(storageKey, "true");
|
|
29
|
+
} catch {
|
|
30
|
+
// ignore local storage failures
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="col-span-12 rounded-xl border-2 border-dashed border-primary/30 bg-primary/5 p-8">
|
|
36
|
+
<div className="mx-auto flex max-w-md flex-col items-center gap-4 text-center">
|
|
37
|
+
<div className="flex size-12 items-center justify-center rounded-full bg-primary/10">
|
|
38
|
+
<RocketIcon className="size-6 text-primary" />
|
|
39
|
+
</div>
|
|
40
|
+
<h2 className="font-headline text-lg font-semibold">Welcome to selftune</h2>
|
|
41
|
+
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
42
|
+
No skills detected yet. Once you start using selftune in your project, skills will appear
|
|
43
|
+
here automatically.
|
|
44
|
+
</p>
|
|
45
|
+
<div className="grid w-full grid-cols-1 gap-3 text-left sm:grid-cols-3">
|
|
46
|
+
<div className="flex items-start gap-2.5 rounded-lg border bg-card p-3">
|
|
47
|
+
<div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-blue-500/10 text-xs font-bold text-blue-500">
|
|
48
|
+
1
|
|
49
|
+
</div>
|
|
50
|
+
<div>
|
|
51
|
+
<p className="text-xs font-medium">Run selftune</p>
|
|
52
|
+
<p className="text-[11px] text-muted-foreground">
|
|
53
|
+
Enable selftune in your project to start tracking skills
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="flex items-start gap-2.5 rounded-lg border bg-card p-3">
|
|
58
|
+
<div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-amber-500/10 text-xs font-bold text-amber-500">
|
|
59
|
+
2
|
|
60
|
+
</div>
|
|
61
|
+
<div>
|
|
62
|
+
<p className="text-xs font-medium">Skills appear</p>
|
|
63
|
+
<p className="text-[11px] text-muted-foreground">
|
|
64
|
+
Skills are detected and monitored automatically
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="flex items-start gap-2.5 rounded-lg border bg-card p-3">
|
|
69
|
+
<div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-emerald-500/10 text-xs font-bold text-emerald-500">
|
|
70
|
+
3
|
|
71
|
+
</div>
|
|
72
|
+
<div>
|
|
73
|
+
<p className="text-xs font-medium">Watch evolution</p>
|
|
74
|
+
<p className="text-[11px] text-muted-foreground">
|
|
75
|
+
Proposals flow in with validated improvements
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={dismiss}
|
|
83
|
+
className="text-xs text-muted-foreground hover:text-foreground"
|
|
84
|
+
>
|
|
85
|
+
Dismiss
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { timeAgo } from "@selftune/ui/lib";
|
|
4
|
+
|
|
5
|
+
export interface OverviewRunSummaryProps {
|
|
6
|
+
lastRun: string | null;
|
|
7
|
+
deployed: number;
|
|
8
|
+
evolved: number;
|
|
9
|
+
watched: number;
|
|
10
|
+
runCount: number;
|
|
11
|
+
historyAction?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function OverviewRunSummary({
|
|
15
|
+
lastRun,
|
|
16
|
+
deployed,
|
|
17
|
+
evolved,
|
|
18
|
+
watched,
|
|
19
|
+
runCount,
|
|
20
|
+
historyAction,
|
|
21
|
+
}: OverviewRunSummaryProps) {
|
|
22
|
+
if (runCount === 0) return null;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="col-span-12 flex items-center gap-6 rounded-xl border border-border/10 bg-card/50 px-5 py-3 text-xs text-muted-foreground">
|
|
26
|
+
<span className="font-headline text-[10px] uppercase tracking-[0.2em]">Last Cycle</span>
|
|
27
|
+
<span>{lastRun ? timeAgo(lastRun) : "Never"}</span>
|
|
28
|
+
<span className="text-muted-foreground/30">|</span>
|
|
29
|
+
<span>{deployed} deployed</span>
|
|
30
|
+
<span>{evolved} evolved</span>
|
|
31
|
+
<span>{watched} watched</span>
|
|
32
|
+
{historyAction ? (
|
|
33
|
+
<>
|
|
34
|
+
<span className="text-muted-foreground/30">|</span>
|
|
35
|
+
<span className="ml-auto">{historyAction}</span>
|
|
36
|
+
</>
|
|
37
|
+
) : null}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { OverviewCoreSurface, type OverviewCoreSurfaceProps } from "./OverviewCoreSurface";
|
|
2
|
+
export {
|
|
3
|
+
OverviewCompositionSurface,
|
|
4
|
+
type OverviewCompositionSurfaceProps,
|
|
5
|
+
} from "./OverviewCompositionSurface";
|
|
6
|
+
export {
|
|
7
|
+
OverviewComparisonSurface,
|
|
8
|
+
type OverviewComparisonSurfaceProps,
|
|
9
|
+
type OverviewComparisonWatchlistConfig,
|
|
10
|
+
} from "./OverviewComparisonSurface";
|
|
11
|
+
export {
|
|
12
|
+
OverviewOnboardingBanner,
|
|
13
|
+
type OverviewOnboardingBannerProps,
|
|
14
|
+
} from "./OverviewOnboardingBanner";
|
|
15
|
+
export { OverviewRunSummary, type OverviewRunSummaryProps } from "./OverviewRunSummary";
|
|
16
|
+
export type { OverviewComparisonRow } from "./types";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EvolutionEntry, TrustBucket } from "@selftune/ui/types";
|
|
2
|
+
|
|
3
|
+
export interface OverviewComparisonRow {
|
|
4
|
+
skillName: string;
|
|
5
|
+
subtext?: string | null;
|
|
6
|
+
triggerRate: number | null;
|
|
7
|
+
routingConfidence: number | null;
|
|
8
|
+
confidenceCoverage: number;
|
|
9
|
+
sessions: number;
|
|
10
|
+
lastEvolution: EvolutionEntry | null;
|
|
11
|
+
bucket: TrustBucket;
|
|
12
|
+
sortTimestamp?: string | null;
|
|
13
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
Table,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableCell,
|
|
13
|
+
TableHead,
|
|
14
|
+
TableHeader,
|
|
15
|
+
TableRow,
|
|
16
|
+
} from "@selftune/ui/primitives";
|
|
17
|
+
|
|
18
|
+
export interface SkillReportDailyBreakdownRow {
|
|
19
|
+
date: string;
|
|
20
|
+
evalCount: number;
|
|
21
|
+
passRate: number;
|
|
22
|
+
explicit: number;
|
|
23
|
+
implicit: number;
|
|
24
|
+
contextual: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SkillReportDailyBreakdownSectionProps {
|
|
28
|
+
rows: SkillReportDailyBreakdownRow[];
|
|
29
|
+
title?: ReactNode;
|
|
30
|
+
maxRows?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatShortDate(dateStr: string): string {
|
|
34
|
+
const date = new Date(dateStr);
|
|
35
|
+
if (Number.isNaN(date.getTime())) return dateStr;
|
|
36
|
+
return date.toLocaleDateString("en-US", {
|
|
37
|
+
month: "short",
|
|
38
|
+
day: "numeric",
|
|
39
|
+
year: "numeric",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatPercent(rate: number): string {
|
|
44
|
+
return `${(rate * 100).toFixed(1)}%`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function SkillReportDailyBreakdownSection({
|
|
48
|
+
rows,
|
|
49
|
+
title = "Daily Breakdown",
|
|
50
|
+
maxRows = 14,
|
|
51
|
+
}: SkillReportDailyBreakdownSectionProps) {
|
|
52
|
+
if (rows.length === 0) return null;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Card className="bg-muted border-none shadow-none ring-0">
|
|
56
|
+
<CardHeader>
|
|
57
|
+
<CardTitle className="font-headline text-lg tracking-tight">{title}</CardTitle>
|
|
58
|
+
</CardHeader>
|
|
59
|
+
<CardContent>
|
|
60
|
+
<Table>
|
|
61
|
+
<TableHeader>
|
|
62
|
+
<TableRow>
|
|
63
|
+
<TableHead className="text-xs uppercase text-muted-foreground">Date</TableHead>
|
|
64
|
+
<TableHead className="text-xs uppercase text-muted-foreground">Evals</TableHead>
|
|
65
|
+
<TableHead className="text-xs uppercase text-muted-foreground">Pass Rate</TableHead>
|
|
66
|
+
<TableHead className="text-xs uppercase text-muted-foreground">Explicit</TableHead>
|
|
67
|
+
<TableHead className="text-xs uppercase text-muted-foreground">Implicit</TableHead>
|
|
68
|
+
<TableHead className="text-xs uppercase text-muted-foreground">Contextual</TableHead>
|
|
69
|
+
</TableRow>
|
|
70
|
+
</TableHeader>
|
|
71
|
+
<TableBody>
|
|
72
|
+
{rows.slice(0, maxRows).map((row) => (
|
|
73
|
+
<TableRow key={row.date}>
|
|
74
|
+
<TableCell className="text-foreground">{formatShortDate(row.date)}</TableCell>
|
|
75
|
+
<TableCell className="font-mono text-muted-foreground">{row.evalCount}</TableCell>
|
|
76
|
+
<TableCell className="font-mono">
|
|
77
|
+
<span
|
|
78
|
+
className={
|
|
79
|
+
row.passRate >= 0.8
|
|
80
|
+
? "text-emerald-400"
|
|
81
|
+
: row.passRate >= 0.6
|
|
82
|
+
? "text-amber-400"
|
|
83
|
+
: "text-red-400"
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
{formatPercent(row.passRate)}
|
|
87
|
+
</span>
|
|
88
|
+
</TableCell>
|
|
89
|
+
<TableCell className="font-mono text-muted-foreground">{row.explicit}</TableCell>
|
|
90
|
+
<TableCell className="font-mono text-muted-foreground">{row.implicit}</TableCell>
|
|
91
|
+
<TableCell className="font-mono text-muted-foreground">{row.contextual}</TableCell>
|
|
92
|
+
</TableRow>
|
|
93
|
+
))}
|
|
94
|
+
</TableBody>
|
|
95
|
+
</Table>
|
|
96
|
+
</CardContent>
|
|
97
|
+
</Card>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { DataQualityPanel } from "@selftune/ui/components";
|
|
6
|
+
import { Card, CardContent } from "@selftune/ui/primitives";
|
|
7
|
+
import type { TrustFields } from "@selftune/ui/types";
|
|
8
|
+
|
|
9
|
+
export interface SkillReportDataQualityTabContentProps {
|
|
10
|
+
evidenceQuality?: TrustFields["evidence_quality"];
|
|
11
|
+
dataHygiene?: TrustFields["data_hygiene"];
|
|
12
|
+
emptyState?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function SkillReportDataQualityTabContent({
|
|
16
|
+
evidenceQuality,
|
|
17
|
+
dataHygiene,
|
|
18
|
+
emptyState,
|
|
19
|
+
}: SkillReportDataQualityTabContentProps) {
|
|
20
|
+
if (!evidenceQuality && !dataHygiene) {
|
|
21
|
+
return (
|
|
22
|
+
emptyState ?? (
|
|
23
|
+
<Card className="rounded-2xl border border-border/15 bg-card">
|
|
24
|
+
<CardContent className="py-12">
|
|
25
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
26
|
+
Detailed data-quality metrics are not available for this skill yet.
|
|
27
|
+
</p>
|
|
28
|
+
</CardContent>
|
|
29
|
+
</Card>
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return <DataQualityPanel evidenceQuality={evidenceQuality} dataHygiene={dataHygiene} />;
|
|
35
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import { EvolutionTimeline } from "@selftune/ui/components";
|
|
7
|
+
import type { EvolutionEntry } from "@selftune/ui/types";
|
|
8
|
+
|
|
9
|
+
export interface SkillReportEvidenceRailProps {
|
|
10
|
+
evolution: EvolutionEntry[];
|
|
11
|
+
activeProposal: string | null;
|
|
12
|
+
onSelect(proposalId: string): void;
|
|
13
|
+
collapsedProposalCount?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function SkillReportEvidenceRail({
|
|
17
|
+
evolution,
|
|
18
|
+
activeProposal,
|
|
19
|
+
onSelect,
|
|
20
|
+
collapsedProposalCount = 6,
|
|
21
|
+
}: SkillReportEvidenceRailProps) {
|
|
22
|
+
const proposalCount = useMemo(
|
|
23
|
+
() => new Set(evolution.map((entry) => entry.proposal_id)).size,
|
|
24
|
+
[evolution],
|
|
25
|
+
);
|
|
26
|
+
const shouldCollapse = proposalCount > collapsedProposalCount;
|
|
27
|
+
const [expanded, setExpanded] = useState(!shouldCollapse);
|
|
28
|
+
|
|
29
|
+
const visibleEntries = useMemo(() => {
|
|
30
|
+
if (expanded) return evolution;
|
|
31
|
+
|
|
32
|
+
const allowed = new Set<string>();
|
|
33
|
+
const collapsed: EvolutionEntry[] = [];
|
|
34
|
+
for (const entry of evolution) {
|
|
35
|
+
if (!allowed.has(entry.proposal_id)) {
|
|
36
|
+
if (allowed.size >= collapsedProposalCount) continue;
|
|
37
|
+
allowed.add(entry.proposal_id);
|
|
38
|
+
}
|
|
39
|
+
collapsed.push(entry);
|
|
40
|
+
}
|
|
41
|
+
return collapsed;
|
|
42
|
+
}, [collapsedProposalCount, evolution, expanded]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<aside className="w-full px-4 py-4 @5xl/main:w-[252px] @5xl/main:self-start @5xl/main:pr-0">
|
|
46
|
+
<div className="@5xl/main:sticky @5xl/main:top-16">
|
|
47
|
+
<div
|
|
48
|
+
className={`rounded-xl border border-border/10 bg-muted/20 px-3 py-3 text-xs ${expanded ? "themed-scroll max-h-[26rem] overflow-y-auto @5xl/main:max-h-[calc(100svh-6rem)]" : "overflow-visible"}`}
|
|
49
|
+
>
|
|
50
|
+
<EvolutionTimeline
|
|
51
|
+
entries={visibleEntries}
|
|
52
|
+
selectedProposalId={activeProposal}
|
|
53
|
+
onSelect={onSelect}
|
|
54
|
+
/>
|
|
55
|
+
{shouldCollapse ? (
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={() => setExpanded((value) => !value)}
|
|
59
|
+
className="mt-2 flex w-full items-center justify-center gap-1.5 py-2 text-[11px] text-muted-foreground transition-colors hover:text-foreground"
|
|
60
|
+
>
|
|
61
|
+
<ChevronDownIcon
|
|
62
|
+
className={`size-3 transition-transform duration-150 ${expanded ? "rotate-180" : ""}`}
|
|
63
|
+
/>
|
|
64
|
+
{expanded ? "Collapse timeline" : `Show full timeline (${proposalCount} proposals)`}
|
|
65
|
+
</button>
|
|
66
|
+
) : null}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</aside>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { EvidenceViewer } from "@selftune/ui/components";
|
|
6
|
+
import { Card, CardContent } from "@selftune/ui/primitives";
|
|
7
|
+
import type { EvidenceEntry, EvolutionEntry } from "@selftune/ui/types";
|
|
8
|
+
|
|
9
|
+
import { SkillReportEvidenceRail } from "./SkillReportEvidenceRail";
|
|
10
|
+
|
|
11
|
+
export interface SkillReportEvidenceSectionProps {
|
|
12
|
+
evolution: EvolutionEntry[];
|
|
13
|
+
activeProposal: string | null;
|
|
14
|
+
onSelect: (proposalId: string) => void;
|
|
15
|
+
evidence: EvidenceEntry[];
|
|
16
|
+
viewerProposalId: string;
|
|
17
|
+
showViewer: boolean;
|
|
18
|
+
emptyState?: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function SkillReportEvidenceSection({
|
|
22
|
+
evolution,
|
|
23
|
+
activeProposal,
|
|
24
|
+
onSelect,
|
|
25
|
+
evidence,
|
|
26
|
+
viewerProposalId,
|
|
27
|
+
showViewer,
|
|
28
|
+
emptyState,
|
|
29
|
+
}: SkillReportEvidenceSectionProps) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="overflow-hidden rounded-2xl border border-border/15 bg-card">
|
|
32
|
+
<div className="flex flex-col @5xl/main:grid @5xl/main:grid-cols-[252px_minmax(0,1fr)] @5xl/main:items-start">
|
|
33
|
+
{evolution.length > 0 ? (
|
|
34
|
+
<SkillReportEvidenceRail
|
|
35
|
+
evolution={evolution}
|
|
36
|
+
activeProposal={activeProposal}
|
|
37
|
+
onSelect={onSelect}
|
|
38
|
+
/>
|
|
39
|
+
) : null}
|
|
40
|
+
|
|
41
|
+
<div className="min-w-0 p-4 @xl/main:p-5">
|
|
42
|
+
{showViewer ? (
|
|
43
|
+
<EvidenceViewer
|
|
44
|
+
proposalId={viewerProposalId}
|
|
45
|
+
evolution={evolution}
|
|
46
|
+
evidence={evidence}
|
|
47
|
+
/>
|
|
48
|
+
) : (
|
|
49
|
+
(emptyState ?? (
|
|
50
|
+
<Card className="rounded-2xl">
|
|
51
|
+
<CardContent className="py-12">
|
|
52
|
+
<div className="flex items-center justify-center text-sm text-muted-foreground">
|
|
53
|
+
No recent evaluation evidence available
|
|
54
|
+
</div>
|
|
55
|
+
</CardContent>
|
|
56
|
+
</Card>
|
|
57
|
+
))
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PromptEvidencePanel } from "@selftune/ui/components";
|
|
4
|
+
import type { TrustFields } from "@selftune/ui/types";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
SkillReportEvidenceSection,
|
|
8
|
+
type SkillReportEvidenceSectionProps,
|
|
9
|
+
} from "./SkillReportEvidenceSection";
|
|
10
|
+
|
|
11
|
+
export interface SkillReportEvidenceTabContentProps extends SkillReportEvidenceSectionProps {
|
|
12
|
+
examples?: TrustFields["examples"];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function SkillReportEvidenceTabContent({
|
|
16
|
+
examples,
|
|
17
|
+
...evidenceSectionProps
|
|
18
|
+
}: SkillReportEvidenceTabContentProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div data-parity-root="skill-report-evidence" className="space-y-6">
|
|
21
|
+
{examples ? <PromptEvidencePanel examples={examples} /> : null}
|
|
22
|
+
<SkillReportEvidenceSection {...evidenceSectionProps} />
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { InvocationsPanel, type InvocationRow, type SessionMeta } from "@selftune/ui/components";
|
|
6
|
+
|
|
7
|
+
export interface SkillReportInvocationsSectionProps {
|
|
8
|
+
invocations: InvocationRow[];
|
|
9
|
+
sessionMetadata?: SessionMeta[];
|
|
10
|
+
callout?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function SkillReportInvocationsSection({
|
|
14
|
+
invocations,
|
|
15
|
+
sessionMetadata,
|
|
16
|
+
callout,
|
|
17
|
+
}: SkillReportInvocationsSectionProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="space-y-2">
|
|
20
|
+
{callout}
|
|
21
|
+
<InvocationsPanel invocations={invocations} sessionMetadata={sessionMetadata} />
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
export interface SkillReportMissedQueryRow {
|
|
6
|
+
id: string;
|
|
7
|
+
query: string;
|
|
8
|
+
confidence: number | null;
|
|
9
|
+
source: string | null;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SkillReportMissedQueriesSectionProps {
|
|
14
|
+
rows: SkillReportMissedQueryRow[];
|
|
15
|
+
emptyState?: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatRelativeTime(dateStr: string): string {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
const then = new Date(dateStr).getTime();
|
|
21
|
+
const diff = now - then;
|
|
22
|
+
|
|
23
|
+
const minutes = Math.floor(diff / 60_000);
|
|
24
|
+
if (minutes < 1) return "just now";
|
|
25
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
26
|
+
|
|
27
|
+
const hours = Math.floor(minutes / 60);
|
|
28
|
+
if (hours < 24) return `${hours}h ago`;
|
|
29
|
+
|
|
30
|
+
const days = Math.floor(hours / 24);
|
|
31
|
+
if (days < 30) return `${days}d ago`;
|
|
32
|
+
|
|
33
|
+
const date = new Date(dateStr);
|
|
34
|
+
if (Number.isNaN(date.getTime())) return dateStr;
|
|
35
|
+
return date.toLocaleDateString("en-US", {
|
|
36
|
+
month: "short",
|
|
37
|
+
day: "numeric",
|
|
38
|
+
year: "numeric",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function SkillReportMissedQueriesSection({
|
|
43
|
+
rows,
|
|
44
|
+
emptyState,
|
|
45
|
+
}: SkillReportMissedQueriesSectionProps) {
|
|
46
|
+
if (rows.length === 0) {
|
|
47
|
+
return (
|
|
48
|
+
emptyState ?? (
|
|
49
|
+
<div className="rounded-lg border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
|
|
50
|
+
No missed queries detected.
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div data-parity-root="skill-report-missed-queries" className="space-y-3">
|
|
58
|
+
{rows.map((row) => (
|
|
59
|
+
<div key={row.id} className="flex items-start gap-3 rounded-lg border border-border p-3">
|
|
60
|
+
<div className="mt-0.5 h-2 w-2 shrink-0 rounded-full bg-orange-500" />
|
|
61
|
+
<div className="min-w-0 flex-1">
|
|
62
|
+
<p className="truncate text-sm text-foreground">{row.query}</p>
|
|
63
|
+
<div className="mt-1 flex items-center gap-3 text-xs text-muted-foreground">
|
|
64
|
+
{row.confidence !== null && (
|
|
65
|
+
<span className="font-medium text-orange-600 dark:text-orange-400">
|
|
66
|
+
{(row.confidence * 100).toFixed(0)}% confidence
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
{row.source ? (
|
|
70
|
+
<span className="rounded bg-muted px-1.5 py-0.5">{row.source}</span>
|
|
71
|
+
) : null}
|
|
72
|
+
<span>{formatRelativeTime(row.createdAt)}</span>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|