agent-nuvira 2.7.0 โ 2.7.2
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/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/orchestrator.js +17 -3
- package/dist/agents/orchestrator.js.map +1 -1
- package/dist/app.js +26 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +108 -2
- package/dist/cli/chat.js.map +1 -1
- package/dist/config/auth.js +18 -0
- package/dist/config/jwt.js +24 -0
- package/dist/config/keys.js +14 -0
- package/dist/gateway/chat-store.d.ts +32 -5
- package/dist/gateway/chat-store.d.ts.map +1 -1
- package/dist/gateway/chat-store.js +52 -6
- package/dist/gateway/chat-store.js.map +1 -1
- package/dist/gateway/registry.d.ts.map +1 -1
- package/dist/gateway/registry.js +54 -10
- package/dist/gateway/registry.js.map +1 -1
- package/dist/inference/model-validator.d.ts.map +1 -1
- package/dist/inference/model-validator.js +11 -1
- package/dist/inference/model-validator.js.map +1 -1
- package/dist/inference/tool-call-utils.d.ts +31 -0
- package/dist/inference/tool-call-utils.d.ts.map +1 -1
- package/dist/inference/tool-call-utils.js +41 -0
- package/dist/inference/tool-call-utils.js.map +1 -1
- package/dist/learning/reasoning-trace.d.ts +5 -3
- package/dist/learning/reasoning-trace.d.ts.map +1 -1
- package/dist/learning/reasoning-trace.js +2 -2
- package/dist/learning/reasoning-trace.js.map +1 -1
- package/dist/middleware/auth.js +30 -0
- package/dist/nlu/conversation-gate.js +1 -1
- package/dist/nlu/conversation-gate.js.map +1 -1
- package/dist/nlu/entities.d.ts.map +1 -1
- package/dist/nlu/entities.js +12 -0
- package/dist/nlu/entities.js.map +1 -1
- package/dist/nlu/intent.d.ts.map +1 -1
- package/dist/nlu/intent.js +83 -15
- package/dist/nlu/intent.js.map +1 -1
- package/dist/passport.js +34 -0
- package/dist/routes/auth.js +42 -0
- package/dist/routes/user.js +31 -0
- package/dist/server.js +17 -0
- package/dist/tools/pipeline-tool.d.ts.map +1 -1
- package/dist/tools/pipeline-tool.js +14 -0
- package/dist/tools/pipeline-tool.js.map +1 -1
- package/dist/tools/release-sync.d.ts +52 -16
- package/dist/tools/release-sync.d.ts.map +1 -1
- package/dist/tools/release-sync.js +143 -79
- package/dist/tools/release-sync.js.map +1 -1
- package/dist/web-dashboard/src/App.js +85 -0
- package/dist/web-dashboard/src/admin-auth.test.js +186 -0
- package/dist/web-dashboard/src/ansi.js +23 -0
- package/dist/web-dashboard/src/ansi.test.js +31 -0
- package/dist/web-dashboard/src/api-admin-auth.test.js +172 -0
- package/dist/web-dashboard/src/api-admin.test.js +65 -0
- package/dist/web-dashboard/src/api-hub.test.js +117 -0
- package/dist/web-dashboard/src/api.js +1421 -0
- package/dist/web-dashboard/src/api.test.js +51 -0
- package/dist/web-dashboard/src/artifacts.js +128 -0
- package/dist/web-dashboard/src/artifacts.test.js +139 -0
- package/dist/web-dashboard/src/components/AdminPanel.js +567 -0
- package/dist/web-dashboard/src/components/AdminPanel.test.js +288 -0
- package/dist/web-dashboard/src/components/AgentHub.js +1580 -0
- package/dist/web-dashboard/src/components/AgentHub.test.js +343 -0
- package/dist/web-dashboard/src/components/BedrockOnboarding.js +320 -0
- package/dist/web-dashboard/src/components/BenchmarkCharts.js +228 -0
- package/dist/web-dashboard/src/components/ChatPage.js +1586 -0
- package/dist/web-dashboard/src/components/ChatPage.test.js +899 -0
- package/dist/web-dashboard/src/components/ContactsPage.js +141 -0
- package/dist/web-dashboard/src/components/CostDashboard.js +105 -0
- package/dist/web-dashboard/src/components/DAGView.js +477 -0
- package/dist/web-dashboard/src/components/DAGView.test.js +147 -0
- package/dist/web-dashboard/src/components/EnvVarEditor.js +138 -0
- package/dist/web-dashboard/src/components/EvalsPage.js +73 -0
- package/dist/web-dashboard/src/components/EvalsPage.test.js +120 -0
- package/dist/web-dashboard/src/components/ExecutionHistory.js +201 -0
- package/dist/web-dashboard/src/components/GatewayPage.js +40 -0
- package/dist/web-dashboard/src/components/GatewayPage.test.js +74 -0
- package/dist/web-dashboard/src/components/HealthPanel.js +68 -0
- package/dist/web-dashboard/src/components/HistoryBrowser.js +34 -0
- package/dist/web-dashboard/src/components/Layout.js +50 -0
- package/dist/web-dashboard/src/components/Markdown.js +152 -0
- package/dist/web-dashboard/src/components/Markdown.test.js +126 -0
- package/dist/web-dashboard/src/components/MarkdownZeroDep.js +237 -0
- package/dist/web-dashboard/src/components/MemoryPanel.js +81 -0
- package/dist/web-dashboard/src/components/ModelTimeline.js +298 -0
- package/dist/web-dashboard/src/components/ModelsPanel.js +1484 -0
- package/dist/web-dashboard/src/components/ModelsPanel.test.js +460 -0
- package/dist/web-dashboard/src/components/Overview.js +123 -0
- package/dist/web-dashboard/src/components/PhaseTimeline.js +359 -0
- package/dist/web-dashboard/src/components/PhaseTimeline.test.js +234 -0
- package/dist/web-dashboard/src/components/PlatformConfigSection.js +384 -0
- package/dist/web-dashboard/src/components/PlatformConfigSection.test.js +106 -0
- package/dist/web-dashboard/src/components/PlatformsPage.js +250 -0
- package/dist/web-dashboard/src/components/QuotaPanel.js +159 -0
- package/dist/web-dashboard/src/components/QuotaPanel.test.js +85 -0
- package/dist/web-dashboard/src/components/RequestsPanel.js +229 -0
- package/dist/web-dashboard/src/components/RequestsPanel.test.js +103 -0
- package/dist/web-dashboard/src/components/RoutingInsightsPanel.js +938 -0
- package/dist/web-dashboard/src/components/RoutingInsightsPanel.test.js +339 -0
- package/dist/web-dashboard/src/components/RoutingWalkthrough.js +408 -0
- package/dist/web-dashboard/src/components/RoutingWalkthrough.test.js +209 -0
- package/dist/web-dashboard/src/components/TaskConsole.js +119 -0
- package/dist/web-dashboard/src/components/TaskConsole.test.js +123 -0
- package/dist/web-dashboard/src/components/TasksPage.js +256 -0
- package/dist/web-dashboard/src/components/TasksPage.test.js +103 -0
- package/dist/web-dashboard/src/components/TracePanel.js +306 -0
- package/dist/web-dashboard/src/components/WhatsAppPanel.js +195 -0
- package/dist/web-dashboard/src/components/WhatsAppPanel.test.js +122 -0
- package/dist/web-dashboard/src/jsonOrNull.js +37 -0
- package/dist/web-dashboard/src/main.js +15 -0
- package/dist/web-dashboard/src/mask.js +11 -0
- package/dist/web-dashboard/vite.config.js +23 -0
- package/dist/web-dashboard/vitest.config.js +15 -0
- package/package.json +5 -3
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the scrubbable per-action telemetry chart (ActionTimelineChart).
|
|
4
|
+
*
|
|
5
|
+
* Covers the empty state, the default scrub position (most recent day with
|
|
6
|
+
* events), click-a-day to jump the caret, the range-slider scrub, play/pause
|
|
7
|
+
* toggling, and the dedupeDayEvents helper that powers the per-day chips.
|
|
8
|
+
* Uses jsdom-safe paths only โ no getBoundingClientRect dependence (the drag
|
|
9
|
+
* path itself mirrors PhaseTimeline's window-listener pattern).
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
const vitest_1 = require("vitest");
|
|
46
|
+
const react_1 = require("@testing-library/react");
|
|
47
|
+
const ModelsPanel_1 = __importStar(require("./ModelsPanel"));
|
|
48
|
+
// โโโ Fixtures โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
49
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
50
|
+
const startOfToday = new Date().setUTCHours(0, 0, 0, 0);
|
|
51
|
+
/** Build 7 ascending day buckets (oldest โ newest); spec: bucket-index โ overrides. */
|
|
52
|
+
const makeTimeline = (spec = {}) => {
|
|
53
|
+
const buckets = [];
|
|
54
|
+
for (let i = 0; i < 7; i++) {
|
|
55
|
+
buckets.push({
|
|
56
|
+
day: startOfToday - (6 - i) * DAY_MS,
|
|
57
|
+
verified: 0,
|
|
58
|
+
killed: 0,
|
|
59
|
+
transient: 0,
|
|
60
|
+
partial: 0,
|
|
61
|
+
events: [],
|
|
62
|
+
...(spec[i] || {}),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return buckets;
|
|
66
|
+
};
|
|
67
|
+
// โโโ dedupeDayEvents โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
68
|
+
(0, vitest_1.describe)('dedupeDayEvents', () => {
|
|
69
|
+
(0, vitest_1.it)('dedupes per provider ร model ร outcome, latest wins, killed first', () => {
|
|
70
|
+
const events = [
|
|
71
|
+
{ provider: 'groq', model: 'm1', outcome: 'verified', at: 100 },
|
|
72
|
+
{ provider: 'groq', model: 'm1', outcome: 'verified', at: 200 },
|
|
73
|
+
{ provider: 'gemini', model: 'g1', outcome: 'unavailable', errorType: 'auth', at: 300 },
|
|
74
|
+
{ provider: 'nim', model: 'n1', outcome: 'error', errorType: 'server', at: 150 },
|
|
75
|
+
];
|
|
76
|
+
const deduped = (0, ModelsPanel_1.dedupeDayEvents)(events);
|
|
77
|
+
(0, vitest_1.expect)(deduped).toHaveLength(3);
|
|
78
|
+
// killed first (most actionable), then verified, then transient
|
|
79
|
+
(0, vitest_1.expect)(deduped[0].outcome).toBe('unavailable');
|
|
80
|
+
(0, vitest_1.expect)(deduped[1].outcome).toBe('verified');
|
|
81
|
+
(0, vitest_1.expect)(deduped[2].outcome).toBe('error');
|
|
82
|
+
// latest event wins for the repeated provider ร model ร outcome
|
|
83
|
+
(0, vitest_1.expect)(deduped[1].at).toBe(200);
|
|
84
|
+
});
|
|
85
|
+
(0, vitest_1.it)('prioritizes partial chips after killed, before verified (flaky mid-stream is actionable)', () => {
|
|
86
|
+
const events = [
|
|
87
|
+
{ provider: 'groq', model: 'm1', outcome: 'verified', at: 300 },
|
|
88
|
+
{ provider: 'groq', model: 'm1', outcome: 'partial', errorType: 'timeout', at: 100 },
|
|
89
|
+
{ provider: 'groq', model: 'm1', outcome: 'partial', errorType: 'timeout', at: 400 },
|
|
90
|
+
{ provider: 'nim', model: 'n1', outcome: 'error', errorType: 'server', at: 200 },
|
|
91
|
+
];
|
|
92
|
+
const deduped = (0, ModelsPanel_1.dedupeDayEvents)(events);
|
|
93
|
+
(0, vitest_1.expect)(deduped).toHaveLength(3);
|
|
94
|
+
// partial sorts before verified (flaky mid-stream is a worse reliability
|
|
95
|
+
// signal than a clean success), and after killed.
|
|
96
|
+
(0, vitest_1.expect)(deduped[0].outcome).toBe('partial');
|
|
97
|
+
(0, vitest_1.expect)(deduped[1].outcome).toBe('verified');
|
|
98
|
+
(0, vitest_1.expect)(deduped[2].outcome).toBe('error');
|
|
99
|
+
// repeated provider ร model ร outcome dedupes โ latest partial wins
|
|
100
|
+
(0, vitest_1.expect)(deduped[0].at).toBe(400);
|
|
101
|
+
});
|
|
102
|
+
(0, vitest_1.it)('dedupes partials per provider ร model ร outcome (a repeat does not double-count)', () => {
|
|
103
|
+
const events = [
|
|
104
|
+
{ provider: 'groq', model: 'm1', outcome: 'partial', errorType: 'timeout', at: 100 },
|
|
105
|
+
{ provider: 'groq', model: 'm1', outcome: 'partial', errorType: 'server', at: 200 },
|
|
106
|
+
];
|
|
107
|
+
const deduped = (0, ModelsPanel_1.dedupeDayEvents)(events);
|
|
108
|
+
(0, vitest_1.expect)(deduped).toHaveLength(1);
|
|
109
|
+
(0, vitest_1.expect)(deduped[0].outcome).toBe('partial');
|
|
110
|
+
(0, vitest_1.expect)(deduped[0].at).toBe(200);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
// โโโ Scrubbable chart behavior โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
114
|
+
(0, vitest_1.describe)('ActionTimelineChart', () => {
|
|
115
|
+
(0, vitest_1.it)('renders nothing when the timeline is empty', () => {
|
|
116
|
+
const { container } = (0, react_1.render)(<ModelsPanel_1.ActionTimelineChart timeline={[]}/>);
|
|
117
|
+
(0, vitest_1.expect)(container.innerHTML).toBe('');
|
|
118
|
+
});
|
|
119
|
+
(0, vitest_1.it)('defaults to the most recent day with events and shows its chips', () => {
|
|
120
|
+
// Events land on day 5 (older); today (day 6) is empty โ default = day 5.
|
|
121
|
+
const timeline = makeTimeline({
|
|
122
|
+
5: {
|
|
123
|
+
verified: 2,
|
|
124
|
+
events: [
|
|
125
|
+
{ provider: 'groq', model: 'llama-3.3-70b-versatile', outcome: 'verified', at: 200 },
|
|
126
|
+
{ provider: 'groq', model: 'llama-3.3-70b-versatile', outcome: 'verified', at: 100 },
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
(0, react_1.render)(<ModelsPanel_1.ActionTimelineChart timeline={timeline}/>);
|
|
131
|
+
// The day-detail panel describes the scrubbed day's learning.
|
|
132
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/what this action learned that day/i)).toBeTruthy();
|
|
133
|
+
// Verified chip for that day โ deduped to ONE chip despite 2 events.
|
|
134
|
+
(0, vitest_1.expect)(react_1.screen.getAllByText(/groq\/llama-3.3-70b-versatile/)).toHaveLength(1);
|
|
135
|
+
});
|
|
136
|
+
(0, vitest_1.it)('clicks a day bar to jump the caret and shows that dayโs killed chips', () => {
|
|
137
|
+
const timeline = makeTimeline({
|
|
138
|
+
0: {
|
|
139
|
+
killed: 1,
|
|
140
|
+
events: [
|
|
141
|
+
{ provider: 'gemini', model: 'g1', outcome: 'unavailable', errorType: 'auth', at: 10 },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
5: {
|
|
145
|
+
verified: 1,
|
|
146
|
+
events: [
|
|
147
|
+
{ provider: 'groq', model: 'm1', outcome: 'verified', at: 200 },
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
(0, react_1.render)(<ModelsPanel_1.ActionTimelineChart timeline={timeline}/>);
|
|
152
|
+
// Default scrub shows the most recent day with events (day 5 โ groq/m1).
|
|
153
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/groq\/m1/)).toBeTruthy();
|
|
154
|
+
// Click the day-0 bar (its title carries the killed count).
|
|
155
|
+
react_1.fireEvent.click(react_1.screen.getByTitle(/โ 1 killed/));
|
|
156
|
+
// The detail panel now shows that day's killed chip (predictive skip).
|
|
157
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/gemini\/g1/)).toBeTruthy();
|
|
158
|
+
(0, vitest_1.expect)(react_1.screen.getByTitle(/Killed by this action/)).toBeTruthy();
|
|
159
|
+
// The previously-shown day's chip is gone.
|
|
160
|
+
(0, vitest_1.expect)(react_1.screen.queryByText(/groq\/m1/)).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
(0, vitest_1.it)('scrubs via the range slider (day snap) and stops playback', () => {
|
|
163
|
+
const timeline = makeTimeline({
|
|
164
|
+
2: {
|
|
165
|
+
transient: 1,
|
|
166
|
+
events: [
|
|
167
|
+
{ provider: 'nim', model: 'n1', outcome: 'error', errorType: 'server', at: 50 },
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
(0, react_1.render)(<ModelsPanel_1.ActionTimelineChart timeline={timeline}/>);
|
|
172
|
+
const slider = react_1.screen.getByRole('slider', { name: 'Scrub action timeline' });
|
|
173
|
+
react_1.fireEvent.change(slider, { target: { value: '2' } });
|
|
174
|
+
// Day 2's transient chip is now visible.
|
|
175
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/nim\/n1/)).toBeTruthy();
|
|
176
|
+
});
|
|
177
|
+
(0, vitest_1.it)('shows a violet โธ chip for a partial mid-stream interruption and counts it in the day summary', () => {
|
|
178
|
+
const timeline = makeTimeline({
|
|
179
|
+
3: {
|
|
180
|
+
partial: 1,
|
|
181
|
+
events: [
|
|
182
|
+
{ provider: 'groq', model: 'm1', outcome: 'partial', errorType: 'timeout', at: 100 },
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
(0, react_1.render)(<ModelsPanel_1.ActionTimelineChart timeline={timeline}/>);
|
|
187
|
+
// The partial-only day is the default scrub position (it has events).
|
|
188
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/groq\/m1/)).toBeTruthy();
|
|
189
|
+
// The day summary surfaces the โธ partial count.
|
|
190
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/โธ 1/)).toBeTruthy();
|
|
191
|
+
// The chip carries the mid-stream interruption tooltip.
|
|
192
|
+
(0, vitest_1.expect)(react_1.screen.getByTitle(/Mid-stream interruption/)).toBeTruthy();
|
|
193
|
+
});
|
|
194
|
+
(0, vitest_1.it)('mixes partial chips with verified chips on the same day (flaky provider is visible, not buried)', () => {
|
|
195
|
+
const timeline = makeTimeline({
|
|
196
|
+
4: {
|
|
197
|
+
verified: 1,
|
|
198
|
+
partial: 1,
|
|
199
|
+
events: [
|
|
200
|
+
{ provider: 'groq', model: 'm1', outcome: 'verified', at: 300 },
|
|
201
|
+
{ provider: 'groq', model: 'm1', outcome: 'partial', errorType: 'timeout', at: 200 },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
(0, react_1.render)(<ModelsPanel_1.ActionTimelineChart timeline={timeline}/>);
|
|
206
|
+
// Both chips render โ the partial โธ chip sorts BEFORE the verified โ chip
|
|
207
|
+
// (flaky mid-stream is more actionable than a clean success).
|
|
208
|
+
const chips = react_1.screen.getAllByText(/groq\/m1/);
|
|
209
|
+
(0, vitest_1.expect)(chips).toHaveLength(2);
|
|
210
|
+
(0, vitest_1.expect)(react_1.screen.getByTitle(/Mid-stream interruption/)).toBeTruthy();
|
|
211
|
+
(0, vitest_1.expect)(react_1.screen.getByTitle(/Verified by this action/)).toBeTruthy();
|
|
212
|
+
});
|
|
213
|
+
(0, vitest_1.it)('toggles play/pause on the scrub button', () => {
|
|
214
|
+
(0, react_1.render)(<ModelsPanel_1.ActionTimelineChart timeline={makeTimeline()}/>);
|
|
215
|
+
const playBtn = react_1.screen.getByRole('button', { name: 'Play scrub' });
|
|
216
|
+
react_1.fireEvent.click(playBtn);
|
|
217
|
+
(0, vitest_1.expect)(react_1.screen.getByRole('button', { name: 'Pause scrub' })).toBeTruthy();
|
|
218
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: 'Pause scrub' }));
|
|
219
|
+
(0, vitest_1.expect)(react_1.screen.getByRole('button', { name: 'Play scrub' })).toBeTruthy();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
// โโโ FlakinessChip (P4 M4.4 registry presentation) โโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
223
|
+
// The registry row chip mirrors the CLI's `โธ flaky N%` โ a mid-stream
|
|
224
|
+
// flakiness EMA > 0 means the router deprioritizes this model (reliability
|
|
225
|
+
// scaled down, capped 40%), so the dashboard must surface it where routing
|
|
226
|
+
// reads availability.
|
|
227
|
+
(0, vitest_1.describe)('FlakinessChip', () => {
|
|
228
|
+
(0, vitest_1.it)('renders โธ flaky N% from the 0-1 EMA rate with the mid-stream tooltip', () => {
|
|
229
|
+
(0, react_1.render)(<ModelsPanel_1.FlakinessChip rate={0.25}/>);
|
|
230
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/โธ flaky 25%/)).toBeTruthy();
|
|
231
|
+
(0, vitest_1.expect)(react_1.screen.getByTitle(/flaky mid-stream 25%/)).toBeTruthy();
|
|
232
|
+
(0, vitest_1.expect)(react_1.screen.getByTitle(/deprioritizes flaky models/)).toBeTruthy();
|
|
233
|
+
});
|
|
234
|
+
(0, vitest_1.it)('rounds the percentage (0.4375 โ 44%)', () => {
|
|
235
|
+
(0, react_1.render)(<ModelsPanel_1.FlakinessChip rate={0.4375}/>);
|
|
236
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/โธ flaky 44%/)).toBeTruthy();
|
|
237
|
+
});
|
|
238
|
+
(0, vitest_1.it)('renders the full 100% at the EMA ceiling', () => {
|
|
239
|
+
(0, react_1.render)(<ModelsPanel_1.FlakinessChip rate={1}/>);
|
|
240
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/โธ flaky 100%/)).toBeTruthy();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
// โโโ ContextWindowChip (v1.60.x live context-window presentation) โโโโโโโโโโ
|
|
244
|
+
// The registry row chip mirrors the CLI's `โณ ctx` โ the LIVE provider-
|
|
245
|
+
// advertised context window the probe recorded (Ollama /api/tags + /api/show,
|
|
246
|
+
// OpenRouter /models, Gemini inputTokenLimit, NIM max_model_len). It is the
|
|
247
|
+
// real spec the router's context preflight prefers over static estimates.
|
|
248
|
+
(0, vitest_1.describe)('ContextWindowChip', () => {
|
|
249
|
+
(0, vitest_1.it)('renders a compact 128K for a 131,072-token window with the exact tokens in the tooltip', () => {
|
|
250
|
+
(0, react_1.render)(<ModelsPanel_1.ContextWindowChip tokens={131072}/>);
|
|
251
|
+
(0, vitest_1.expect)(react_1.screen.getByText('โณ 128K')).toBeTruthy();
|
|
252
|
+
(0, vitest_1.expect)(react_1.screen.getByTitle(/131,072 tokens/)).toBeTruthy();
|
|
253
|
+
(0, vitest_1.expect)(react_1.screen.getByTitle(/feeds the router's context preflight/)).toBeTruthy();
|
|
254
|
+
});
|
|
255
|
+
(0, vitest_1.it)('renders 1M for a 1,048,576-token window (Gemini 2.5 class)', () => {
|
|
256
|
+
(0, react_1.render)(<ModelsPanel_1.ContextWindowChip tokens={1048576}/>);
|
|
257
|
+
(0, vitest_1.expect)(react_1.screen.getByText('โณ 1M')).toBeTruthy();
|
|
258
|
+
});
|
|
259
|
+
(0, vitest_1.it)('renders 16K for a 16,384-token local model window', () => {
|
|
260
|
+
(0, react_1.render)(<ModelsPanel_1.ContextWindowChip tokens={16384}/>);
|
|
261
|
+
(0, vitest_1.expect)(react_1.screen.getByText('โณ 16K')).toBeTruthy();
|
|
262
|
+
});
|
|
263
|
+
(0, vitest_1.it)('renders the raw count below 1024 tokens', () => {
|
|
264
|
+
(0, react_1.render)(<ModelsPanel_1.ContextWindowChip tokens={512}/>);
|
|
265
|
+
(0, vitest_1.expect)(react_1.screen.getByText('โณ 512')).toBeTruthy();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
// โโโ FlakinessSparkline (P4 M4.4 healing trajectory) โโโโโโโโโโโโโโโโโโโโโโโ
|
|
269
|
+
// The mini sparkline plots the partialRate EMA trajectory: a trend toward 0
|
|
270
|
+
// = healing via clean successes; climbing = flakiness accumulating. Renders
|
|
271
|
+
// only when >= 2 samples exist so single-sample entries stay clean.
|
|
272
|
+
(0, vitest_1.describe)('keyHygieneWarning (ISSUE-004)', () => {
|
|
273
|
+
(0, vitest_1.it)('renders nothing when hygiene is absent or all counters are zero', () => {
|
|
274
|
+
(0, vitest_1.expect)((0, ModelsPanel_1.keyHygieneWarning)(undefined)).toBeNull();
|
|
275
|
+
(0, vitest_1.expect)((0, ModelsPanel_1.keyHygieneWarning)({ threshold: 3, consecutive: {} })).toBeNull();
|
|
276
|
+
});
|
|
277
|
+
(0, vitest_1.it)('shows each provider climbing toward the auto-clear threshold', () => {
|
|
278
|
+
const { container } = (0, react_1.render)((0, ModelsPanel_1.keyHygieneWarning)({
|
|
279
|
+
threshold: 3,
|
|
280
|
+
consecutive: { groq: 2, nim: 1 },
|
|
281
|
+
}));
|
|
282
|
+
const text = container.textContent || '';
|
|
283
|
+
(0, vitest_1.expect)(text).toContain('Key hygiene in progress');
|
|
284
|
+
(0, vitest_1.expect)(text).toContain('groq');
|
|
285
|
+
(0, vitest_1.expect)(text).toContain('2/3 consecutive auth failures');
|
|
286
|
+
(0, vitest_1.expect)(text).toContain('nim');
|
|
287
|
+
(0, vitest_1.expect)(text).toContain('1/3');
|
|
288
|
+
// Below threshold โ warns the key WILL be auto-cleared.
|
|
289
|
+
(0, vitest_1.expect)(text).toContain('key will be auto-cleared at the threshold');
|
|
290
|
+
});
|
|
291
|
+
(0, vitest_1.it)('flags a provider at/over the threshold as already cleared', () => {
|
|
292
|
+
const { container } = (0, react_1.render)((0, ModelsPanel_1.keyHygieneWarning)({
|
|
293
|
+
threshold: 3,
|
|
294
|
+
consecutive: { openrouter: 4 },
|
|
295
|
+
}));
|
|
296
|
+
const text = container.textContent || '';
|
|
297
|
+
(0, vitest_1.expect)(text).toContain('openrouter');
|
|
298
|
+
(0, vitest_1.expect)(text).toContain('4/3 consecutive auth failures');
|
|
299
|
+
(0, vitest_1.expect)(text).toContain('key auto-cleared');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
(0, vitest_1.describe)('FlakinessSparkline', () => {
|
|
303
|
+
(0, vitest_1.it)('renders nothing with fewer than 2 history points', () => {
|
|
304
|
+
const { container } = (0, react_1.render)(<ModelsPanel_1.FlakinessSparkline history={[{ t: 1, rate: 0.25 }]}/>);
|
|
305
|
+
(0, vitest_1.expect)(container.innerHTML).toBe('');
|
|
306
|
+
const { container: empty } = (0, react_1.render)(<ModelsPanel_1.FlakinessSparkline history={undefined}/>);
|
|
307
|
+
(0, vitest_1.expect)(empty.innerHTML).toBe('');
|
|
308
|
+
});
|
|
309
|
+
(0, vitest_1.it)('renders a polyline + end dot for a decaying (healing) trajectory', () => {
|
|
310
|
+
const { container } = (0, react_1.render)(<ModelsPanel_1.FlakinessSparkline history={[
|
|
311
|
+
{ t: 1000, rate: 0.4375 },
|
|
312
|
+
{ t: 2000, rate: 0.25 },
|
|
313
|
+
{ t: 3000, rate: 0.15 },
|
|
314
|
+
]}/>);
|
|
315
|
+
(0, vitest_1.expect)(container.querySelector('polyline')).toBeTruthy();
|
|
316
|
+
// Healing trend โ tooltip + green end dot.
|
|
317
|
+
(0, vitest_1.expect)(react_1.screen.getByTitle(/Flakiness healing/)).toBeTruthy();
|
|
318
|
+
(0, vitest_1.expect)(react_1.screen.getByTitle(/trending down/)).toBeTruthy();
|
|
319
|
+
(0, vitest_1.expect)(container.querySelector('circle')?.getAttribute('fill')).toBe('#3fb950');
|
|
320
|
+
});
|
|
321
|
+
(0, vitest_1.it)('flags a climbing (worse) trajectory with the accumulating tooltip', () => {
|
|
322
|
+
const { container } = (0, react_1.render)(<ModelsPanel_1.FlakinessSparkline history={[
|
|
323
|
+
{ t: 1000, rate: 0.15 },
|
|
324
|
+
{ t: 2000, rate: 0.25 },
|
|
325
|
+
{ t: 3000, rate: 0.4375 },
|
|
326
|
+
]}/>);
|
|
327
|
+
(0, vitest_1.expect)(react_1.screen.getByTitle(/Flakiness climbing/)).toBeTruthy();
|
|
328
|
+
(0, vitest_1.expect)(container.querySelector('circle')?.getAttribute('fill')).toBe('#bc8cff');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
// โโโ ActionTelemetryCard partial presentation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
332
|
+
// The card is the PRIMARY presentation surface for M3.4 partial chips โ
|
|
333
|
+
// asserting the violet border, the โธ header stat, and the Partial chips
|
|
334
|
+
// section keeps the feature's headline surface regression-proof.
|
|
335
|
+
(0, vitest_1.describe)('ActionTelemetryCard partial presentation', () => {
|
|
336
|
+
(0, vitest_1.it)('shows the โธ partial stat, violet border, and a Partial chips section with the streamed-chunk tooltip', () => {
|
|
337
|
+
const entry = {
|
|
338
|
+
action: 'chat',
|
|
339
|
+
verified: 2,
|
|
340
|
+
killed: 0,
|
|
341
|
+
transient: 1,
|
|
342
|
+
partial: 2,
|
|
343
|
+
verifiedModels: [{ provider: 'groq', model: 'm1', at: 100 }],
|
|
344
|
+
killedModels: [],
|
|
345
|
+
partialModels: [
|
|
346
|
+
{ provider: 'groq', model: 'm1', reason: 'timeout', at: 200, streamedChunks: 128 },
|
|
347
|
+
],
|
|
348
|
+
timeline: [],
|
|
349
|
+
};
|
|
350
|
+
const { container } = (0, react_1.render)(<ModelsPanel_1.ActionTelemetryCard entry={entry}/>);
|
|
351
|
+
// โธ 2 partial stat in the header.
|
|
352
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/โธ 2/)).toBeTruthy();
|
|
353
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/partial/)).toBeTruthy();
|
|
354
|
+
// Partial chips section heading.
|
|
355
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/mid-stream interruption/i)).toBeTruthy();
|
|
356
|
+
// The chip carries the streamed-chunk detail in its tooltip.
|
|
357
|
+
(0, vitest_1.expect)(react_1.screen.getByTitle(/~128 chunks in/)).toBeTruthy();
|
|
358
|
+
// Violet border wins (partial > killed > verified) โ jsdom computes rgb().
|
|
359
|
+
(0, vitest_1.expect)(container.querySelector('[style*="rgb(188, 140, 255)"]')).toBeTruthy();
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
// โโโ ModelsPanel fetch degradation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
363
|
+
// Regression tests for the reported error "Failed to execute 'json' on
|
|
364
|
+
// 'Response': Unexpected token '<'" โ caused by a STALE dashboard server
|
|
365
|
+
// (older version) returning the SPA index.html (HTTP 200, text/html) for an
|
|
366
|
+
// /api/* route its new frontend bundle calls. The panel must degrade, never
|
|
367
|
+
// crash.
|
|
368
|
+
const modelsHealth = {
|
|
369
|
+
totalModels: 1,
|
|
370
|
+
available: 1,
|
|
371
|
+
limited: 0,
|
|
372
|
+
unavailable: 0,
|
|
373
|
+
providers: [
|
|
374
|
+
{
|
|
375
|
+
provider: 'local', providerLabel: 'Ollama', icon: '๐ป',
|
|
376
|
+
apiConfigured: true, apiAccessible: true, overallStatus: 'available',
|
|
377
|
+
notes: '', freeTierInfo: '',
|
|
378
|
+
models: [{ id: 'llama3.2', name: 'llama3.2', status: 'available' }],
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
};
|
|
382
|
+
const jsonResponse = (data) => new Response(JSON.stringify(data), {
|
|
383
|
+
status: 200,
|
|
384
|
+
headers: { 'Content-Type': 'application/json' },
|
|
385
|
+
});
|
|
386
|
+
const htmlResponse = () => new Response('<!DOCTYPE html><html><body>SPA fallback</body></html>', {
|
|
387
|
+
status: 200,
|
|
388
|
+
headers: { 'Content-Type': 'text/html' },
|
|
389
|
+
});
|
|
390
|
+
(0, vitest_1.describe)('ModelsPanel fetch degradation', () => {
|
|
391
|
+
(0, vitest_1.afterEach)(() => {
|
|
392
|
+
(0, react_1.cleanup)();
|
|
393
|
+
vitest_1.vi.restoreAllMocks();
|
|
394
|
+
});
|
|
395
|
+
(0, vitest_1.it)('renders a friendly error instead of crashing when /api/models returns HTML (stale server)', async () => {
|
|
396
|
+
// The stale-server scenario: BOTH endpoints serve SPA index.html with 200.
|
|
397
|
+
vitest_1.vi.spyOn(globalThis, 'fetch').mockResolvedValue(htmlResponse());
|
|
398
|
+
(0, react_1.render)(<ModelsPanel_1.default />);
|
|
399
|
+
(0, vitest_1.expect)(await react_1.screen.findByText(/Model health endpoint returned an unexpected response/i)).toBeTruthy();
|
|
400
|
+
// The panel survives โ no uncaught "Unexpected token '<'" crash.
|
|
401
|
+
});
|
|
402
|
+
(0, vitest_1.it)('keeps the health grid when only /api/model-registry is HTML (registry/telemetry degrade, grid survives)', async () => {
|
|
403
|
+
vitest_1.vi.spyOn(globalThis, 'fetch').mockImplementation((input) => {
|
|
404
|
+
const url = String(input);
|
|
405
|
+
if (url.includes('/api/model-registry'))
|
|
406
|
+
return Promise.resolve(htmlResponse());
|
|
407
|
+
return Promise.resolve(jsonResponse(modelsHealth));
|
|
408
|
+
});
|
|
409
|
+
(0, react_1.render)(<ModelsPanel_1.default />);
|
|
410
|
+
// Health grid renders (fetch resolved and modelsData set).
|
|
411
|
+
// Function matcher: the h2 text node is "๐ง Model Provider Status".
|
|
412
|
+
(0, vitest_1.expect)(await react_1.screen.findByText((content) => content.includes('Model Provider Status'))).toBeTruthy();
|
|
413
|
+
// No registry/telemetry sections from the HTML-200 โ they degrade to hidden
|
|
414
|
+
// instead of throwing "Unexpected token '<'".
|
|
415
|
+
(0, vitest_1.expect)(react_1.screen.queryByText(/Model Availability Registry/i)).toBeNull();
|
|
416
|
+
(0, vitest_1.expect)(react_1.screen.queryByText(/Learned from real usage/i)).toBeNull();
|
|
417
|
+
});
|
|
418
|
+
(0, vitest_1.it)('recovers from a transient network failure (TypeError โ backoff retry โ grid renders)', async () => {
|
|
419
|
+
let modelsCalls = 0;
|
|
420
|
+
vitest_1.vi.spyOn(globalThis, 'fetch').mockImplementation((input) => {
|
|
421
|
+
const url = String(input);
|
|
422
|
+
if (url.includes('/api/model-registry'))
|
|
423
|
+
return Promise.resolve(jsonResponse({ enabled: false }));
|
|
424
|
+
modelsCalls += 1;
|
|
425
|
+
// First attempt: network-level rejection โ the reported "Failed to fetch".
|
|
426
|
+
if (modelsCalls === 1)
|
|
427
|
+
return Promise.reject(new TypeError('Failed to fetch'));
|
|
428
|
+
return Promise.resolve(jsonResponse(modelsHealth));
|
|
429
|
+
});
|
|
430
|
+
(0, react_1.render)(<ModelsPanel_1.default />);
|
|
431
|
+
// Wait for a DATA-dependent element (only rendered after the retried fetch
|
|
432
|
+
// lands) โ the section header renders before any data arrives.
|
|
433
|
+
(0, vitest_1.expect)(await react_1.screen.findByText(/llama3\.2/)).toBeTruthy();
|
|
434
|
+
// The transient failure was retried, not surfaced.
|
|
435
|
+
(0, vitest_1.expect)(modelsCalls).toBeGreaterThanOrEqual(2);
|
|
436
|
+
(0, vitest_1.expect)(react_1.screen.queryByText(/Failed to fetch/i)).toBeNull();
|
|
437
|
+
(0, vitest_1.expect)(react_1.screen.queryByText(/Dashboard server unreachable/i)).toBeNull();
|
|
438
|
+
});
|
|
439
|
+
(0, vitest_1.it)('shows a friendly retrying error when the dashboard is unreachable (all attempts fail)', async () => {
|
|
440
|
+
vitest_1.vi.spyOn(globalThis, 'fetch').mockRejectedValue(new TypeError('Failed to fetch'));
|
|
441
|
+
(0, react_1.render)(<ModelsPanel_1.default />);
|
|
442
|
+
// All retries exhaust after 300ms + 700ms backoff โ allow time for that.
|
|
443
|
+
(0, vitest_1.expect)(await react_1.screen.findByText(/Dashboard server unreachable/i, {}, { timeout: 5000 })).toBeTruthy();
|
|
444
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/Retrying automatically/i)).toBeTruthy();
|
|
445
|
+
});
|
|
446
|
+
(0, vitest_1.it)('keeps the grid when only /api/model-registry fails at the network level (optional section)', async () => {
|
|
447
|
+
vitest_1.vi.spyOn(globalThis, 'fetch').mockImplementation((input) => {
|
|
448
|
+
const url = String(input);
|
|
449
|
+
if (url.includes('/api/model-registry'))
|
|
450
|
+
return Promise.reject(new TypeError('Failed to fetch'));
|
|
451
|
+
return Promise.resolve(jsonResponse(modelsHealth));
|
|
452
|
+
});
|
|
453
|
+
(0, react_1.render)(<ModelsPanel_1.default />);
|
|
454
|
+
(0, vitest_1.expect)(await react_1.screen.findByText(/llama3\.2/)).toBeTruthy();
|
|
455
|
+
// The registry is optional โ a network failure there must not surface an
|
|
456
|
+
// error banner or hide the health grid.
|
|
457
|
+
(0, vitest_1.expect)(react_1.screen.queryByText(/Failed to fetch/i)).toBeNull();
|
|
458
|
+
(0, vitest_1.expect)(react_1.screen.queryByText(/Dashboard server unreachable/i)).toBeNull();
|
|
459
|
+
});
|
|
460
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = Overview;
|
|
4
|
+
const recharts_1 = require("recharts");
|
|
5
|
+
function formatCost(usd) {
|
|
6
|
+
if (usd === undefined || usd === null)
|
|
7
|
+
return '$0.00';
|
|
8
|
+
if (usd < 0.00001)
|
|
9
|
+
return '$0.00';
|
|
10
|
+
if (usd < 0.01)
|
|
11
|
+
return '$' + usd.toFixed(6);
|
|
12
|
+
return '$' + usd.toFixed(4);
|
|
13
|
+
}
|
|
14
|
+
function formatNumber(n) {
|
|
15
|
+
if (n === undefined || n === null)
|
|
16
|
+
return '0';
|
|
17
|
+
if (n >= 1_000_000)
|
|
18
|
+
return (n / 1_000_000).toFixed(1) + 'M';
|
|
19
|
+
if (n >= 1_000)
|
|
20
|
+
return (n / 1_000).toFixed(1) + 'K';
|
|
21
|
+
return n.toLocaleString();
|
|
22
|
+
}
|
|
23
|
+
const PROVIDER_COLORS = {
|
|
24
|
+
local: '#3fb950',
|
|
25
|
+
groq: '#58a6ff',
|
|
26
|
+
gemini: '#bc8cff',
|
|
27
|
+
nim: '#39d2c0',
|
|
28
|
+
openrouter: '#d29922',
|
|
29
|
+
};
|
|
30
|
+
function Overview({ data }) {
|
|
31
|
+
if (!data) {
|
|
32
|
+
return (<div className="loading-state">
|
|
33
|
+
<div className="loading-spinner"/>
|
|
34
|
+
<p>Connecting to dashboard...</p>
|
|
35
|
+
</div>);
|
|
36
|
+
}
|
|
37
|
+
const { cost, history, benchmarks, memory, health } = data;
|
|
38
|
+
const stats = [
|
|
39
|
+
{ icon: '๐ฐ', value: formatCost(cost.totalCost), label: 'Total Cost' },
|
|
40
|
+
{ icon: '๐', value: formatNumber(cost.totalRequests), label: 'API Requests' },
|
|
41
|
+
{ icon: '๐ง ', value: formatNumber(memory.total), label: 'Trajectories' },
|
|
42
|
+
{ icon: '๐', value: formatNumber(history.total), label: 'Chat Sessions' },
|
|
43
|
+
{ icon: '๐', value: formatNumber(benchmarks.totalRuns), label: 'Benchmark Runs' },
|
|
44
|
+
{ icon: '๐ฆ', value: formatNumber(health.vectors), label: 'Vector Entries' },
|
|
45
|
+
];
|
|
46
|
+
// Cost by provider chart data
|
|
47
|
+
const providerEntries = Object.entries(cost.byProvider || {})
|
|
48
|
+
.map(([name, value]) => ({ name, value }))
|
|
49
|
+
.sort((a, b) => b.value - a.value);
|
|
50
|
+
// Trajectories by project chart data
|
|
51
|
+
const projectEntries = Object.entries(memory.byFingerprint || {})
|
|
52
|
+
.map(([name, value]) => ({ name: name.length > 15 ? name.slice(0, 13) + '..' : name, value }))
|
|
53
|
+
.sort((a, b) => b.value - a.value)
|
|
54
|
+
.slice(0, 8);
|
|
55
|
+
return (<>
|
|
56
|
+
<h2 className="section-title">๐ System Overview</h2>
|
|
57
|
+
|
|
58
|
+
<div className="stats-grid">
|
|
59
|
+
{stats.map((stat) => (<div className="stat-card" key={stat.label}>
|
|
60
|
+
<div className="stat-icon">{stat.icon}</div>
|
|
61
|
+
<div className="stat-body">
|
|
62
|
+
<div className="stat-value">{stat.value}</div>
|
|
63
|
+
<div className="stat-label">{stat.label}</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>))}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Memory Health Summary Card */}
|
|
69
|
+
<div className="memory-health-card">
|
|
70
|
+
<h3 className="section-subtitle">๐ง Memory Health</h3>
|
|
71
|
+
<div className="memory-health-stats">
|
|
72
|
+
<div className="memory-health-stat">
|
|
73
|
+
<span className="memory-health-value">{formatNumber(memory.total)}</span>
|
|
74
|
+
<span className="memory-health-label">Trajectories</span>
|
|
75
|
+
</div>
|
|
76
|
+
<div className="memory-health-stat">
|
|
77
|
+
<span className="memory-health-value">{formatNumber(memory.facts?.total || 0)}</span>
|
|
78
|
+
<span className="memory-health-label">Facts Learned</span>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="memory-health-stat">
|
|
81
|
+
<span className="memory-health-value">{formatNumber(memory.recall?.last7d || 0)}</span>
|
|
82
|
+
<span className="memory-health-label">Recalls This Week</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="memory-health-stat">
|
|
85
|
+
<span className="memory-health-value">{health.patterns || 0}</span>
|
|
86
|
+
<span className="memory-health-label">Coding Patterns</span>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<p className="memory-health-status">
|
|
90
|
+
Backend: <code>{memory.backend || 'local'}</code>
|
|
91
|
+
{memory.recall?.last7d ? ` ยท ${memory.recall.last7d} recall(s) this week` : ' ยท no recalls yet'}
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<h3 className="section-subtitle">๐ฐ Cost by Provider</h3>
|
|
96
|
+
<div className="chart-container">
|
|
97
|
+
{providerEntries.length > 0 ? (<recharts_1.ResponsiveContainer width="100%" height={260}>
|
|
98
|
+
<recharts_1.BarChart data={providerEntries} margin={{ top: 20, right: 20, left: 10, bottom: 40 }}>
|
|
99
|
+
<recharts_1.CartesianGrid strokeDasharray="3 3" stroke="#21262d"/>
|
|
100
|
+
<recharts_1.XAxis dataKey="name" tick={{ fill: '#8b949e', fontSize: 11 }} axisLine={{ stroke: '#30363d' }}/>
|
|
101
|
+
<recharts_1.YAxis tick={{ fill: '#8b949e', fontSize: 11 }} axisLine={{ stroke: '#30363d' }} tickFormatter={(v) => '$' + v.toFixed(4)}/>
|
|
102
|
+
<recharts_1.Tooltip contentStyle={{ background: '#161b22', border: '1px solid #30363d', borderRadius: 8 }} labelStyle={{ color: '#e6edf3' }} formatter={(value) => [formatCost(value), 'Cost']}/>
|
|
103
|
+
<recharts_1.Bar dataKey="value" radius={[4, 4, 0, 0]}>
|
|
104
|
+
{providerEntries.map((entry) => (<recharts_1.Cell key={entry.name} fill={PROVIDER_COLORS[entry.name] || '#58a6ff'} fillOpacity={0.8}/>))}
|
|
105
|
+
</recharts_1.Bar>
|
|
106
|
+
</recharts_1.BarChart>
|
|
107
|
+
</recharts_1.ResponsiveContainer>) : (<div className="empty-state">No cost data yet.</div>)}
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<h3 className="section-subtitle">๐ Trajectories by Project Type</h3>
|
|
111
|
+
<div className="chart-container">
|
|
112
|
+
{projectEntries.length > 0 ? (<recharts_1.ResponsiveContainer width="100%" height={260}>
|
|
113
|
+
<recharts_1.BarChart data={projectEntries} margin={{ top: 20, right: 20, left: 10, bottom: 40 }}>
|
|
114
|
+
<recharts_1.CartesianGrid strokeDasharray="3 3" stroke="#21262d"/>
|
|
115
|
+
<recharts_1.XAxis dataKey="name" tick={{ fill: '#8b949e', fontSize: 11 }} axisLine={{ stroke: '#30363d' }}/>
|
|
116
|
+
<recharts_1.YAxis tick={{ fill: '#8b949e', fontSize: 11 }} axisLine={{ stroke: '#30363d' }}/>
|
|
117
|
+
<recharts_1.Tooltip contentStyle={{ background: '#161b22', border: '1px solid #30363d', borderRadius: 8 }} labelStyle={{ color: '#e6edf3' }}/>
|
|
118
|
+
<recharts_1.Bar dataKey="value" fill="#bc8cff" radius={[4, 4, 0, 0]} fillOpacity={0.8}/>
|
|
119
|
+
</recharts_1.BarChart>
|
|
120
|
+
</recharts_1.ResponsiveContainer>) : (<div className="empty-state">No trajectory data yet.</div>)}
|
|
121
|
+
</div>
|
|
122
|
+
</>);
|
|
123
|
+
}
|