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.
Files changed (114) hide show
  1. package/dist/agents/orchestrator.d.ts.map +1 -1
  2. package/dist/agents/orchestrator.js +17 -3
  3. package/dist/agents/orchestrator.js.map +1 -1
  4. package/dist/app.js +26 -0
  5. package/dist/cli/chat.d.ts.map +1 -1
  6. package/dist/cli/chat.js +108 -2
  7. package/dist/cli/chat.js.map +1 -1
  8. package/dist/config/auth.js +18 -0
  9. package/dist/config/jwt.js +24 -0
  10. package/dist/config/keys.js +14 -0
  11. package/dist/gateway/chat-store.d.ts +32 -5
  12. package/dist/gateway/chat-store.d.ts.map +1 -1
  13. package/dist/gateway/chat-store.js +52 -6
  14. package/dist/gateway/chat-store.js.map +1 -1
  15. package/dist/gateway/registry.d.ts.map +1 -1
  16. package/dist/gateway/registry.js +54 -10
  17. package/dist/gateway/registry.js.map +1 -1
  18. package/dist/inference/model-validator.d.ts.map +1 -1
  19. package/dist/inference/model-validator.js +11 -1
  20. package/dist/inference/model-validator.js.map +1 -1
  21. package/dist/inference/tool-call-utils.d.ts +31 -0
  22. package/dist/inference/tool-call-utils.d.ts.map +1 -1
  23. package/dist/inference/tool-call-utils.js +41 -0
  24. package/dist/inference/tool-call-utils.js.map +1 -1
  25. package/dist/learning/reasoning-trace.d.ts +5 -3
  26. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  27. package/dist/learning/reasoning-trace.js +2 -2
  28. package/dist/learning/reasoning-trace.js.map +1 -1
  29. package/dist/middleware/auth.js +30 -0
  30. package/dist/nlu/conversation-gate.js +1 -1
  31. package/dist/nlu/conversation-gate.js.map +1 -1
  32. package/dist/nlu/entities.d.ts.map +1 -1
  33. package/dist/nlu/entities.js +12 -0
  34. package/dist/nlu/entities.js.map +1 -1
  35. package/dist/nlu/intent.d.ts.map +1 -1
  36. package/dist/nlu/intent.js +83 -15
  37. package/dist/nlu/intent.js.map +1 -1
  38. package/dist/passport.js +34 -0
  39. package/dist/routes/auth.js +42 -0
  40. package/dist/routes/user.js +31 -0
  41. package/dist/server.js +17 -0
  42. package/dist/tools/pipeline-tool.d.ts.map +1 -1
  43. package/dist/tools/pipeline-tool.js +14 -0
  44. package/dist/tools/pipeline-tool.js.map +1 -1
  45. package/dist/tools/release-sync.d.ts +52 -16
  46. package/dist/tools/release-sync.d.ts.map +1 -1
  47. package/dist/tools/release-sync.js +143 -79
  48. package/dist/tools/release-sync.js.map +1 -1
  49. package/dist/web-dashboard/src/App.js +85 -0
  50. package/dist/web-dashboard/src/admin-auth.test.js +186 -0
  51. package/dist/web-dashboard/src/ansi.js +23 -0
  52. package/dist/web-dashboard/src/ansi.test.js +31 -0
  53. package/dist/web-dashboard/src/api-admin-auth.test.js +172 -0
  54. package/dist/web-dashboard/src/api-admin.test.js +65 -0
  55. package/dist/web-dashboard/src/api-hub.test.js +117 -0
  56. package/dist/web-dashboard/src/api.js +1421 -0
  57. package/dist/web-dashboard/src/api.test.js +51 -0
  58. package/dist/web-dashboard/src/artifacts.js +128 -0
  59. package/dist/web-dashboard/src/artifacts.test.js +139 -0
  60. package/dist/web-dashboard/src/components/AdminPanel.js +567 -0
  61. package/dist/web-dashboard/src/components/AdminPanel.test.js +288 -0
  62. package/dist/web-dashboard/src/components/AgentHub.js +1580 -0
  63. package/dist/web-dashboard/src/components/AgentHub.test.js +343 -0
  64. package/dist/web-dashboard/src/components/BedrockOnboarding.js +320 -0
  65. package/dist/web-dashboard/src/components/BenchmarkCharts.js +228 -0
  66. package/dist/web-dashboard/src/components/ChatPage.js +1586 -0
  67. package/dist/web-dashboard/src/components/ChatPage.test.js +899 -0
  68. package/dist/web-dashboard/src/components/ContactsPage.js +141 -0
  69. package/dist/web-dashboard/src/components/CostDashboard.js +105 -0
  70. package/dist/web-dashboard/src/components/DAGView.js +477 -0
  71. package/dist/web-dashboard/src/components/DAGView.test.js +147 -0
  72. package/dist/web-dashboard/src/components/EnvVarEditor.js +138 -0
  73. package/dist/web-dashboard/src/components/EvalsPage.js +73 -0
  74. package/dist/web-dashboard/src/components/EvalsPage.test.js +120 -0
  75. package/dist/web-dashboard/src/components/ExecutionHistory.js +201 -0
  76. package/dist/web-dashboard/src/components/GatewayPage.js +40 -0
  77. package/dist/web-dashboard/src/components/GatewayPage.test.js +74 -0
  78. package/dist/web-dashboard/src/components/HealthPanel.js +68 -0
  79. package/dist/web-dashboard/src/components/HistoryBrowser.js +34 -0
  80. package/dist/web-dashboard/src/components/Layout.js +50 -0
  81. package/dist/web-dashboard/src/components/Markdown.js +152 -0
  82. package/dist/web-dashboard/src/components/Markdown.test.js +126 -0
  83. package/dist/web-dashboard/src/components/MarkdownZeroDep.js +237 -0
  84. package/dist/web-dashboard/src/components/MemoryPanel.js +81 -0
  85. package/dist/web-dashboard/src/components/ModelTimeline.js +298 -0
  86. package/dist/web-dashboard/src/components/ModelsPanel.js +1484 -0
  87. package/dist/web-dashboard/src/components/ModelsPanel.test.js +460 -0
  88. package/dist/web-dashboard/src/components/Overview.js +123 -0
  89. package/dist/web-dashboard/src/components/PhaseTimeline.js +359 -0
  90. package/dist/web-dashboard/src/components/PhaseTimeline.test.js +234 -0
  91. package/dist/web-dashboard/src/components/PlatformConfigSection.js +384 -0
  92. package/dist/web-dashboard/src/components/PlatformConfigSection.test.js +106 -0
  93. package/dist/web-dashboard/src/components/PlatformsPage.js +250 -0
  94. package/dist/web-dashboard/src/components/QuotaPanel.js +159 -0
  95. package/dist/web-dashboard/src/components/QuotaPanel.test.js +85 -0
  96. package/dist/web-dashboard/src/components/RequestsPanel.js +229 -0
  97. package/dist/web-dashboard/src/components/RequestsPanel.test.js +103 -0
  98. package/dist/web-dashboard/src/components/RoutingInsightsPanel.js +938 -0
  99. package/dist/web-dashboard/src/components/RoutingInsightsPanel.test.js +339 -0
  100. package/dist/web-dashboard/src/components/RoutingWalkthrough.js +408 -0
  101. package/dist/web-dashboard/src/components/RoutingWalkthrough.test.js +209 -0
  102. package/dist/web-dashboard/src/components/TaskConsole.js +119 -0
  103. package/dist/web-dashboard/src/components/TaskConsole.test.js +123 -0
  104. package/dist/web-dashboard/src/components/TasksPage.js +256 -0
  105. package/dist/web-dashboard/src/components/TasksPage.test.js +103 -0
  106. package/dist/web-dashboard/src/components/TracePanel.js +306 -0
  107. package/dist/web-dashboard/src/components/WhatsAppPanel.js +195 -0
  108. package/dist/web-dashboard/src/components/WhatsAppPanel.test.js +122 -0
  109. package/dist/web-dashboard/src/jsonOrNull.js +37 -0
  110. package/dist/web-dashboard/src/main.js +15 -0
  111. package/dist/web-dashboard/src/mask.js +11 -0
  112. package/dist/web-dashboard/vite.config.js +23 -0
  113. package/dist/web-dashboard/vitest.config.js +15 -0
  114. package/package.json +5 -3
@@ -0,0 +1,408 @@
1
+ "use strict";
2
+ /**
3
+ * RoutingWalkthrough — "Why did the router pick this?"
4
+ *
5
+ * A narrated, step-by-step playback of a single routing decision, inspired by
6
+ * llm-viz's guided walkthrough. Replays a decision in 4 steps:
7
+ *
8
+ * 1. 🎯 Request — the task, its complexity, and who asked
9
+ * 2. ⚖️ Candidates — every provider the router scored, with score + reason
10
+ * 3. 🚫 Exclusions — providers the router skipped, and why
11
+ * 4. ✅ Pick — the winner, its score, and the reason it won
12
+ *
13
+ * Decisions come from two sources:
14
+ * - Real decisions from `routing.history` (the audit trail) — what actually
15
+ * happened in live chat / orchestrator / explain / benchmark / eval runs.
16
+ * - Complexity profiles from `routing.preference` — what the router WOULD
17
+ * pick right now for each complexity, when no real history exists yet.
18
+ *
19
+ * The playback is scrubbable (Prev/Next + step dots) and can auto-play.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.buildWalkthroughDecisions = buildWalkthroughDecisions;
23
+ exports.default = RoutingWalkthroughSection;
24
+ const react_1 = require("react");
25
+ // ─── Helpers ────────────────────────────────────────────────────────────────
26
+ const PROVIDER_ICONS = {
27
+ local: '💻', groq: '🟢', nim: '🔶', gemini: '🔷', openrouter: '🟣',
28
+ };
29
+ const PROVIDER_LABELS = {
30
+ local: 'Ollama (Local)', groq: 'Groq', nim: 'NVIDIA NIM',
31
+ gemini: 'Gemini', openrouter: 'OpenRouter',
32
+ };
33
+ const COMPLEXITY_ICONS = {
34
+ trivial: '🟢', simple: '🔵', moderate: '🟡', complex: '🟠', critical: '🔴',
35
+ };
36
+ const COMPLEXITY_LABELS = {
37
+ trivial: '🟢 trivial', simple: '🔵 simple', moderate: '🟡 moderate',
38
+ complex: '🟠 complex', critical: '🔴 critical',
39
+ };
40
+ const SOURCE_LABELS = {
41
+ chat: '💬 chat', orchestrator: '🔀 orchestrator', explain: '🔍 explain',
42
+ benchmark: '📈 benchmark', eval: '🎯 eval',
43
+ };
44
+ const STEP_ORDER = [
45
+ { key: 'request', icon: '🎯', title: 'Request' },
46
+ { key: 'candidates', icon: '⚖️', title: 'Candidates' },
47
+ { key: 'exclusions', icon: '🚫', title: 'Exclusions' },
48
+ { key: 'pick', icon: '✅', title: 'Pick' },
49
+ ];
50
+ /** Auto-play advance interval (ms per step). */
51
+ const PLAY_STEP_MS = 2400;
52
+ function providerIcon(provider) {
53
+ return PROVIDER_ICONS[provider] || '🔌';
54
+ }
55
+ function providerLabel(provider) {
56
+ return PROVIDER_LABELS[provider] || provider;
57
+ }
58
+ function sourceLabel(source) {
59
+ return (source && SOURCE_LABELS[source]) || source || 'unknown';
60
+ }
61
+ function timeAgo(ts) {
62
+ const diff = Date.now() - ts;
63
+ if (diff < 60_000)
64
+ return 'just now';
65
+ if (diff < 3_600_000)
66
+ return `${Math.floor(diff / 60_000)}m ago`;
67
+ if (diff < 86_400_000)
68
+ return `${Math.floor(diff / 3_600_000)}h ago`;
69
+ return `${Math.floor(diff / 86_400_000)}d ago`;
70
+ }
71
+ function fmtCooldown(ms) {
72
+ const mins = Math.ceil(ms / 60_000);
73
+ if (mins >= 60)
74
+ return `${Math.floor(mins / 60)}h ${mins % 60}m`;
75
+ return `${mins}m`;
76
+ }
77
+ /** Cap on how many real decisions we replay (most recent first). */
78
+ const MAX_REAL_DECISIONS = 8;
79
+ /** Why a provider was excluded from a decision's candidate list. */
80
+ function exclusionReason(provider, routing) {
81
+ const quota = routing.quota;
82
+ const entry = quota?.entries.find((e) => e.provider === provider);
83
+ if (entry?.parked) {
84
+ return entry.resetsInMs > 0
85
+ ? `quota exhausted — auto re-enables in ${fmtCooldown(entry.resetsInMs)}`
86
+ : 'quota exhausted — parked';
87
+ }
88
+ if (entry && entry.cooldownRemaining > 0) {
89
+ return `circuit-breaker cooldown — ${fmtCooldown(entry.cooldownRemaining)} remaining`;
90
+ }
91
+ const registered = routing.providers.some((p) => p.provider === provider);
92
+ if (!registered) {
93
+ return 'not scored for this complexity (no benchmark quality data)';
94
+ }
95
+ return 'ranked below the candidate cutoff for this complexity';
96
+ }
97
+ /**
98
+ * Build the replayable decision list.
99
+ * 1. Real decisions from routing history (most recent first, capped).
100
+ * 2. If no history exists, fall back to complexity profiles from preference
101
+ * (what the router would pick right now).
102
+ */
103
+ function buildWalkthroughDecisions(routing) {
104
+ const knownProviders = new Set();
105
+ for (const p of routing.providers)
106
+ knownProviders.add(p.provider);
107
+ if (routing.usage)
108
+ for (const p of Object.keys(routing.usage.byProvider))
109
+ knownProviders.add(p);
110
+ if (routing.bestModels)
111
+ for (const b of routing.bestModels)
112
+ knownProviders.add(b.model.split('/')[0]);
113
+ if (routing.quota)
114
+ for (const e of routing.quota.entries)
115
+ knownProviders.add(e.provider);
116
+ const prefByComplexity = new Map(routing.preference.map((p) => [p.complexity, p]));
117
+ const decisions = [];
118
+ // 1. Real recorded decisions (most recent first, capped)
119
+ const history = [...(routing.history ?? [])]
120
+ .sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
121
+ for (const h of history.slice(0, MAX_REAL_DECISIONS)) {
122
+ const profile = prefByComplexity.get(h.complexity);
123
+ const candidates = profile?.providers?.length
124
+ ? profile.providers.map((c) => ({ provider: c.provider, score: c.score, reason: c.reason }))
125
+ : [{ provider: h.provider, score: h.score, reason: 'actual pick recorded' }];
126
+ // The walkthrough must show the winner in the scored list — if the real
127
+ // pick's provider isn't in the current profile candidates (profile drifted
128
+ // since the decision), append it so the replay stays coherent.
129
+ if (!candidates.some((c) => c.provider === h.provider)) {
130
+ candidates.push({ provider: h.provider, score: h.score, reason: 'actual pick recorded' });
131
+ }
132
+ const candidateSet = new Set(candidates.map((c) => c.provider));
133
+ const exclusions = [...knownProviders]
134
+ .filter((p) => !candidateSet.has(p))
135
+ .map((p) => ({ provider: p, reason: exclusionReason(p, routing) }));
136
+ const winnerReason = candidates.find((c) => c.provider === h.provider)?.reason;
137
+ decisions.push({
138
+ id: h.id,
139
+ label: `${sourceLabel(h.source)} · ${h.complexity} · ${(h.task || h.agentType).slice(0, 26)}`,
140
+ real: true,
141
+ complexity: h.complexity,
142
+ task: h.task || h.agentType,
143
+ agentType: h.agentType,
144
+ source: h.source,
145
+ timestamp: h.timestamp,
146
+ winner: `${h.provider}/${h.model}`,
147
+ winnerScore: h.score,
148
+ winnerReason,
149
+ candidates,
150
+ exclusions,
151
+ });
152
+ }
153
+ // 2. Fallback: complexity profiles (what the router would pick now)
154
+ if (decisions.length === 0) {
155
+ for (const p of routing.preference) {
156
+ const winnerParts = p.winner.split('/');
157
+ const candidates = p.providers.map((c) => ({
158
+ provider: c.provider, score: c.score, reason: c.reason,
159
+ }));
160
+ const candidateSet = new Set(candidates.map((c) => c.provider));
161
+ const exclusions = [...knownProviders]
162
+ .filter((prov) => !candidateSet.has(prov))
163
+ .map((prov) => ({ provider: prov, reason: exclusionReason(prov, routing) }));
164
+ const winnerProvider = winnerParts[0];
165
+ decisions.push({
166
+ id: `profile-${p.complexity}`,
167
+ label: `${COMPLEXITY_LABELS[p.complexity] || p.complexity} — what the router would pick`,
168
+ real: false,
169
+ complexity: p.complexity,
170
+ task: winnerProvider ? `A ${p.complexity} task (auto-routed)` : `A ${p.complexity} task`,
171
+ agentType: 'chat',
172
+ winner: p.winner,
173
+ winnerScore: p.score,
174
+ winnerReason: candidates.find((c) => c.provider === winnerProvider)?.reason,
175
+ candidates,
176
+ exclusions,
177
+ });
178
+ }
179
+ }
180
+ return decisions;
181
+ }
182
+ // ─── Step narration ─────────────────────────────────────────────────────────
183
+ function narration(step, d) {
184
+ switch (step) {
185
+ case 'request':
186
+ return `A ${d.complexity} task arrives${d.source ? ` from ${sourceLabel(d.source)}` : ''} and must be routed.`;
187
+ case 'candidates':
188
+ return `${d.candidates.length} provider${d.candidates.length !== 1 ? 's' : ''} are scored against the ${d.complexity} weight profile.`;
189
+ case 'exclusions':
190
+ return d.exclusions.length > 0
191
+ ? `${d.exclusions.length} provider${d.exclusions.length !== 1 ? 's' : ''} known to the system are skipped before scoring.`
192
+ : 'Every known provider was scored — nothing was skipped.';
193
+ case 'pick':
194
+ return d.winnerReason
195
+ ? `The top score wins: ${d.winner.split('/')[0]} ranked first — ${d.winnerReason}.`
196
+ : `The top score wins: ${d.winner}.`;
197
+ }
198
+ }
199
+ // ─── Small render pieces ────────────────────────────────────────────────────
200
+ function ScoreBar({ value, color }) {
201
+ const pctWidth = Math.min(100, Math.max(0, value * 100));
202
+ return (<div style={{
203
+ flex: 1, background: '#0d1117', borderRadius: 4, height: 6,
204
+ overflow: 'hidden', border: '1px solid #21262d',
205
+ }}>
206
+ <div style={{ width: `${pctWidth}%`, background: color, height: '100%', transition: 'width 0.4s ease' }}/>
207
+ </div>);
208
+ }
209
+ function StepHeader({ icon, title, narration }) {
210
+ return (<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginBottom: 14 }}>
211
+ <span style={{ fontSize: 20, lineHeight: '24px' }}>{icon}</span>
212
+ <div>
213
+ <div style={{ fontSize: 14, fontWeight: 600, color: '#e6edf3' }}>{title}</div>
214
+ <div style={{ fontSize: 12, color: '#8b949e', marginTop: 2 }}>{narration}</div>
215
+ </div>
216
+ </div>);
217
+ }
218
+ // ─── Walkthrough section ────────────────────────────────────────────────────
219
+ function RoutingWalkthroughSection({ routing }) {
220
+ const decisions = (0, react_1.useMemo)(() => buildWalkthroughDecisions(routing), [routing]);
221
+ const [activeId, setActiveId] = (0, react_1.useState)('');
222
+ const [stepIdx, setStepIdx] = (0, react_1.useState)(0);
223
+ const [playing, setPlaying] = (0, react_1.useState)(false);
224
+ // Select the first decision by default (or when the active one disappears).
225
+ (0, react_1.useEffect)(() => {
226
+ if (decisions.length === 0)
227
+ return;
228
+ if (!decisions.some((d) => d.id === activeId)) {
229
+ setActiveId(decisions[0].id);
230
+ setStepIdx(0);
231
+ }
232
+ }, [decisions, activeId]);
233
+ // Auto-play loop — advances one step per PLAY_STEP_MS and loops.
234
+ (0, react_1.useEffect)(() => {
235
+ if (!playing || decisions.length === 0)
236
+ return;
237
+ const timer = setInterval(() => {
238
+ setStepIdx((s) => (s + 1) % STEP_ORDER.length);
239
+ }, PLAY_STEP_MS);
240
+ return () => clearInterval(timer);
241
+ }, [playing, decisions.length]);
242
+ if (decisions.length === 0)
243
+ return null;
244
+ const active = decisions.find((d) => d.id === activeId) ?? decisions[0];
245
+ const step = STEP_ORDER[stepIdx];
246
+ const prevStep = () => {
247
+ setStepIdx((s) => (s + STEP_ORDER.length - 1) % STEP_ORDER.length);
248
+ setPlaying(false);
249
+ };
250
+ const nextStep = () => {
251
+ setStepIdx((s) => (s + 1) % STEP_ORDER.length);
252
+ setPlaying(false);
253
+ };
254
+ const togglePlay = () => setPlaying((p) => !p);
255
+ return (<div style={{
256
+ background: '#161b22', borderRadius: 12, border: '1px solid #21262d',
257
+ padding: '18px 20px', marginBottom: 16,
258
+ }}>
259
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
260
+ <span style={{ fontSize: 20 }}>🎬</span>
261
+ <h3 style={{ fontSize: 15, fontWeight: 600, color: '#e6edf3', margin: 0 }}>
262
+ Why did the router pick this?
263
+ </h3>
264
+ {active.real && (<span style={{
265
+ fontSize: 11, padding: '1px 8px', borderRadius: 10,
266
+ background: '#12291a', border: '1px solid #238636', color: '#3fb950',
267
+ }}>
268
+ ✓ real decision
269
+ </span>)}
270
+ </div>
271
+ <p style={{ fontSize: 12, color: '#8b949e', margin: '2px 0 12px 0' }}>
272
+ Narrated step-by-step replay of a routing decision — request, candidates,
273
+ exclusions, and the pick. Scrubbable, or hit play.
274
+ </p>
275
+
276
+ {/* Decision selector */}
277
+ <select aria-label="Routing decision to replay" value={activeId} onChange={(e) => { setActiveId(e.target.value); setStepIdx(0); setPlaying(false); }} style={{
278
+ width: '100%', marginBottom: 16, padding: '8px 10px',
279
+ background: '#0d1117', color: '#e6edf3', fontSize: 12,
280
+ border: '1px solid #30363d', borderRadius: 8, outline: 'none',
281
+ }}>
282
+ {decisions.map((d) => (<option key={d.id} value={d.id} style={{ background: '#0d1117', color: '#e6edf3' }}>
283
+ {d.real ? '● ' : '○ '}{d.label}
284
+ </option>))}
285
+ </select>
286
+
287
+ {/* Step header + narration */}
288
+ <StepHeader icon={step.icon} title={`${stepIdx + 1}. ${step.title}${stepIdx > 0 ? ` — ${active.complexity}` : ''}`} narration={narration(step.key, active)}/>
289
+
290
+ {/* Step content */}
291
+ {step.key === 'request' && (<div style={{ background: '#0d1117', border: '1px solid #21262d', borderRadius: 10, padding: '14px 16px' }}>
292
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 8 }}>
293
+ <span style={{ fontSize: 12, padding: '2px 10px', borderRadius: 12, background: '#161b22', border: '1px solid #30363d', color: '#e6edf3' }}>
294
+ {COMPLEXITY_ICONS[active.complexity] || '•'} {COMPLEXITY_LABELS[active.complexity] || active.complexity}
295
+ </span>
296
+ <span style={{ fontSize: 12, padding: '2px 10px', borderRadius: 12, background: '#161b22', border: '1px solid #30363d', color: '#8b949e' }}>
297
+ 🤖 {active.agentType}
298
+ </span>
299
+ {active.source && (<span style={{ fontSize: 12, padding: '2px 10px', borderRadius: 12, background: '#161b22', border: '1px solid #30363d', color: '#8b949e' }}>
300
+ {sourceLabel(active.source)}
301
+ </span>)}
302
+ {active.timestamp && (<span style={{ fontSize: 11, color: '#6e7681', marginLeft: 'auto' }}>
303
+ {timeAgo(active.timestamp)}
304
+ </span>)}
305
+ </div>
306
+ <div style={{ fontSize: 13, color: '#e6edf3' }}>{active.task}</div>
307
+ </div>)}
308
+
309
+ {step.key === 'candidates' && (<div>
310
+ {active.candidates.map((c, i) => {
311
+ const isWinner = c.provider === active.winner.split('/')[0];
312
+ return (<div key={c.provider} style={{
313
+ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px',
314
+ background: isWinner ? '#12291a' : '#0d1117',
315
+ border: `1px solid ${isWinner ? '#238636' : '#21262d'}`,
316
+ borderRadius: 8, marginBottom: 6,
317
+ }}>
318
+ <span style={{ width: 18, fontSize: 11, color: '#6e7681', textAlign: 'right' }}>{i + 1}.</span>
319
+ <span style={{ width: 104, color: isWinner ? '#3fb950' : '#e6edf3', fontWeight: isWinner ? 600 : 400, whiteSpace: 'nowrap', fontSize: 12 }}>
320
+ {providerIcon(c.provider)} {providerLabel(c.provider).split(' ')[0]}
321
+ {isWinner && ' 👑'}
322
+ </span>
323
+ <ScoreBar value={c.score} color={isWinner ? '#3fb950' : '#58a6ff'}/>
324
+ <span style={{ width: 44, textAlign: 'right', fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 11, color: '#8b949e' }}>
325
+ {c.score.toFixed(3)}
326
+ </span>
327
+ </div>);
328
+ })}
329
+ {active.candidates.length > 0 && (<div style={{ marginTop: 10 }}>
330
+ <div style={{ fontSize: 11, color: '#6e7681', marginBottom: 4 }}>Why each ranked where it did</div>
331
+ {active.candidates.map((c) => (<div key={c.provider} style={{ fontSize: 12, color: '#8b949e', marginBottom: 3 }}>
332
+ <span style={{ color: '#e6edf3' }}>{c.provider}</span>: {c.reason || 'scored'}
333
+ </div>))}
334
+ </div>)}
335
+ </div>)}
336
+
337
+ {step.key === 'exclusions' && (active.exclusions.length > 0 ? (<div>
338
+ {active.exclusions.map((ex) => (<div key={ex.provider} style={{
339
+ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px',
340
+ background: '#0d1117', border: '1px solid #21262d', borderRadius: 8, marginBottom: 6,
341
+ }}>
342
+ <span style={{ fontSize: 13 }}>🚫</span>
343
+ <span style={{ width: 110, color: '#e6edf3', whiteSpace: 'nowrap', fontSize: 12 }}>
344
+ {providerIcon(ex.provider)} {providerLabel(ex.provider).split(' ')[0]}
345
+ </span>
346
+ <span style={{ flex: 1, fontSize: 12, color: '#8b949e' }}>{ex.reason}</span>
347
+ </div>))}
348
+ </div>) : (<div style={{
349
+ background: '#0d1117', border: '1px dashed #30363d', borderRadius: 10,
350
+ padding: '14px 16px', color: '#8b949e', fontSize: 13,
351
+ }}>
352
+ ✅ No exclusions — every provider known to the system was scored for this decision.
353
+ </div>))}
354
+
355
+ {step.key === 'pick' && (<div style={{
356
+ background: '#12291a', border: '1px solid #238636', borderRadius: 10,
357
+ padding: '16px 18px',
358
+ }}>
359
+ <div style={{ fontSize: 12, color: '#3fb950', fontWeight: 600, marginBottom: 6 }}>
360
+ 🏆 ROUTER PICK
361
+ </div>
362
+ <div style={{ fontSize: 16, fontWeight: 700, color: '#e6edf3', fontFamily: "'SFMono-Regular', Consolas, monospace", marginBottom: 4 }}>
363
+ {providerIcon(active.winner.split('/')[0])} {active.winner}
364
+ </div>
365
+ <div style={{ fontSize: 12, color: '#8b949e', marginBottom: 10 }}>
366
+ composite score <span style={{ fontFamily: "'SFMono-Regular', Consolas, monospace", color: '#3fb950' }}>{active.winnerScore.toFixed(3)}</span>
367
+ {!active.real && ' · profile preview'}
368
+ </div>
369
+ {active.winnerReason && (<div style={{ fontSize: 12, color: '#8b949e', borderTop: '1px solid #23863655', paddingTop: 8 }}>
370
+ <span style={{ color: '#e6edf3' }}>Why:</span> {active.winnerReason}
371
+ </div>)}
372
+ </div>)}
373
+
374
+ {/* Controls */}
375
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 16 }}>
376
+ <button onClick={prevStep} aria-label="Previous step" style={btnStyle}>
377
+
378
+ </button>
379
+ <button onClick={togglePlay} aria-label={playing ? 'Pause playback' : 'Play playback'} style={{ ...btnStyle, minWidth: 84, color: playing ? '#d29922' : '#3fb950' }}>
380
+ {playing ? '⏸ Pause' : '▶ Play'}
381
+ </button>
382
+ <button onClick={nextStep} aria-label="Next step" style={btnStyle}>
383
+
384
+ </button>
385
+
386
+ {/* Step dots */}
387
+ <div style={{ display: 'flex', gap: 6, marginLeft: 8 }}>
388
+ {STEP_ORDER.map((s, i) => (<button key={s.key} aria-label={`Go to step ${i + 1}: ${s.title}`} onClick={() => { setStepIdx(i); setPlaying(false); }} style={{
389
+ width: 22, height: 22, borderRadius: 11, border: 'none', cursor: 'pointer',
390
+ fontSize: 10, lineHeight: '22px', padding: 0,
391
+ background: i === stepIdx ? '#58a6ff' : '#21262d',
392
+ color: i === stepIdx ? '#0d1117' : '#8b949e',
393
+ }} title={s.title}>
394
+ {s.icon}
395
+ </button>))}
396
+ </div>
397
+
398
+ <span style={{ marginLeft: 'auto', fontSize: 11, color: '#6e7681' }}>
399
+ step {stepIdx + 1}/{STEP_ORDER.length}
400
+ </span>
401
+ </div>
402
+ </div>);
403
+ }
404
+ const btnStyle = {
405
+ background: '#21262d', color: '#e6edf3', border: '1px solid #30363d',
406
+ borderRadius: 8, padding: '6px 12px', fontSize: 13, cursor: 'pointer',
407
+ transition: 'background 0.15s',
408
+ };
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ /**
3
+ * Unit tests for the RoutingWalkthroughSection — the narrated "why did the
4
+ * router pick this?" playback rendered by RoutingInsightsPanel.
5
+ *
6
+ * Covers: empty state (no decisions → nothing rendered), the 4-step playback
7
+ * (request → candidates → exclusions → pick), prev/next + play controls, the
8
+ * decision selector, and the builder's real-history vs profile fallback.
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ const vitest_1 = require("vitest");
15
+ const react_1 = require("@testing-library/react");
16
+ const RoutingInsightsPanel_1 = __importDefault(require("./RoutingInsightsPanel"));
17
+ const RoutingWalkthrough_1 = require("./RoutingWalkthrough");
18
+ // ─── Helpers ────────────────────────────────────────────────────────────────
19
+ function makeData(routing) {
20
+ return { routing };
21
+ }
22
+ const historyEntry = {
23
+ id: 'route-test-1',
24
+ timestamp: Date.now(),
25
+ source: 'chat',
26
+ agentType: 'writer',
27
+ task: 'implement JWT auth middleware',
28
+ complexity: 'moderate',
29
+ provider: 'gemini',
30
+ model: 'gemini-2.0-flash-exp',
31
+ score: 0.87,
32
+ };
33
+ const baseRouting = {
34
+ providers: [],
35
+ bestModels: [],
36
+ preference: [
37
+ {
38
+ complexity: 'moderate',
39
+ winner: 'gemini/gemini-2.0-flash-exp',
40
+ score: 0.87,
41
+ providers: [
42
+ { provider: 'gemini', score: 0.87, reason: 'gemini: strongest reasoning' },
43
+ { provider: 'groq', score: 0.71, reason: 'groq: fastest' },
44
+ { provider: 'local', score: 0.52, reason: 'local: fully private/local' },
45
+ ],
46
+ },
47
+ ],
48
+ history: [historyEntry],
49
+ updatedAt: Date.now(),
50
+ };
51
+ // ─── Tests ──────────────────────────────────────────────────────────────────
52
+ (0, vitest_1.describe)('RoutingWalkthroughSection (via RoutingInsightsPanel)', () => {
53
+ (0, vitest_1.beforeEach)(() => {
54
+ vitest_1.vi.useFakeTimers();
55
+ });
56
+ (0, vitest_1.afterEach)(() => {
57
+ vitest_1.vi.useRealTimers();
58
+ });
59
+ (0, vitest_1.it)('renders nothing when there is no history and no preference', () => {
60
+ (0, react_1.render)(<RoutingInsightsPanel_1.default data={makeData({ providers: [], bestModels: [], preference: [], updatedAt: Date.now() })}/>);
61
+ (0, vitest_1.expect)(react_1.screen.queryByText('Why did the router pick this?')).toBeNull();
62
+ });
63
+ (0, vitest_1.it)('plays back a real decision starting with the request step', () => {
64
+ (0, react_1.render)(<RoutingInsightsPanel_1.default data={makeData(baseRouting)}/>);
65
+ (0, vitest_1.expect)(react_1.screen.getByText('Why did the router pick this?')).toBeTruthy();
66
+ (0, vitest_1.expect)(react_1.screen.getByText('✓ real decision')).toBeTruthy();
67
+ (0, vitest_1.expect)(react_1.screen.getByText(/1\. Request/)).toBeTruthy();
68
+ // Task text appears in both the walkthrough step and the panel's audit table
69
+ (0, vitest_1.expect)(react_1.screen.getAllByText('implement JWT auth middleware').length).toBeGreaterThan(0);
70
+ // "🤖 writer" — emoji and label share one text node, so match loosely
71
+ (0, vitest_1.expect)(react_1.screen.getAllByText(/writer/).length).toBeGreaterThan(0);
72
+ });
73
+ (0, vitest_1.it)('advances through candidates → exclusions → pick with the Next button', () => {
74
+ (0, react_1.render)(<RoutingInsightsPanel_1.default data={makeData(baseRouting)}/>);
75
+ // Step 2: candidates
76
+ react_1.fireEvent.click(react_1.screen.getByLabelText('Next step'));
77
+ (0, vitest_1.expect)(react_1.screen.getByText(/2\. Candidates/)).toBeTruthy();
78
+ // Gemini appears in the candidate row and in the reason list
79
+ (0, vitest_1.expect)(react_1.screen.getAllByText(/gemini/i).length).toBeGreaterThan(0);
80
+ (0, vitest_1.expect)(react_1.screen.getAllByText('0.870').length).toBeGreaterThan(0);
81
+ // Step 3: exclusions — local is scored but known providers that aren't
82
+ // candidates are excluded; with only these three scored, show the note.
83
+ react_1.fireEvent.click(react_1.screen.getByLabelText('Next step'));
84
+ (0, vitest_1.expect)(react_1.screen.getByText(/3\. Exclusions/)).toBeTruthy();
85
+ // Step 4: pick — the winner callout
86
+ react_1.fireEvent.click(react_1.screen.getByLabelText('Next step'));
87
+ (0, vitest_1.expect)(react_1.screen.getByText(/4\. Pick/)).toBeTruthy();
88
+ (0, vitest_1.expect)(react_1.screen.getByText('🏆 ROUTER PICK')).toBeTruthy();
89
+ // Winner shows in the walkthrough callout and the panel's best-models table
90
+ (0, vitest_1.expect)(react_1.screen.getAllByText(/gemini\/gemini-2\.0-flash-exp/).length).toBeGreaterThan(0);
91
+ (0, vitest_1.expect)(react_1.screen.getByText(/composite score/)).toBeTruthy();
92
+ });
93
+ (0, vitest_1.it)('wraps around at the end of the step list', () => {
94
+ (0, react_1.render)(<RoutingInsightsPanel_1.default data={makeData(baseRouting)}/>);
95
+ for (let i = 0; i < 4; i++)
96
+ react_1.fireEvent.click(react_1.screen.getByLabelText('Next step'));
97
+ (0, vitest_1.expect)(react_1.screen.getByText(/1\. Request/)).toBeTruthy();
98
+ });
99
+ (0, vitest_1.it)('auto-plays through steps while playing, then stops on pause', () => {
100
+ (0, react_1.render)(<RoutingInsightsPanel_1.default data={makeData(baseRouting)}/>);
101
+ react_1.fireEvent.click(react_1.screen.getByLabelText('Play playback'));
102
+ // First advance happens after one tick — still on step 1 right now
103
+ (0, vitest_1.expect)(react_1.screen.getByText(/1\. Request/)).toBeTruthy();
104
+ (0, react_1.act)(() => { vitest_1.vi.advanceTimersByTime(2400); });
105
+ (0, vitest_1.expect)(react_1.screen.getByText(/2\. Candidates/)).toBeTruthy();
106
+ (0, react_1.act)(() => { vitest_1.vi.advanceTimersByTime(2400); });
107
+ (0, vitest_1.expect)(react_1.screen.getByText(/3\. Exclusions/)).toBeTruthy();
108
+ react_1.fireEvent.click(react_1.screen.getByLabelText('Pause playback'));
109
+ (0, react_1.act)(() => { vitest_1.vi.advanceTimersByTime(2400); });
110
+ // Still on the same step after pause
111
+ (0, vitest_1.expect)(react_1.screen.getByText(/3\. Exclusions/)).toBeTruthy();
112
+ });
113
+ (0, vitest_1.it)('switches decisions via the selector', () => {
114
+ const second = {
115
+ ...historyEntry,
116
+ id: 'route-test-2',
117
+ source: 'orchestrator',
118
+ agentType: 'planner',
119
+ task: 'design a distributed architecture',
120
+ complexity: 'complex',
121
+ provider: 'openrouter',
122
+ model: 'openai/gpt-4o',
123
+ score: 0.92,
124
+ };
125
+ (0, react_1.render)(<RoutingInsightsPanel_1.default data={makeData({ ...baseRouting, history: [second, historyEntry] })}/>);
126
+ // Second entry is first in history (most recent first)
127
+ (0, vitest_1.expect)(react_1.screen.getAllByText('design a distributed architecture').length).toBeGreaterThan(0);
128
+ react_1.fireEvent.change(react_1.screen.getByLabelText('Routing decision to replay'), {
129
+ target: { value: 'route-test-1' },
130
+ });
131
+ (0, vitest_1.expect)(react_1.screen.getAllByText('implement JWT auth middleware').length).toBeGreaterThan(0);
132
+ });
133
+ (0, vitest_1.it)('falls back to complexity profiles when no real history exists', () => {
134
+ const noHistory = {
135
+ ...baseRouting,
136
+ history: [],
137
+ };
138
+ (0, react_1.render)(<RoutingInsightsPanel_1.default data={makeData(noHistory)}/>);
139
+ (0, vitest_1.expect)(react_1.screen.getByText('Why did the router pick this?')).toBeTruthy();
140
+ (0, vitest_1.expect)(react_1.screen.queryByText('✓ real decision')).toBeNull();
141
+ (0, vitest_1.expect)(react_1.screen.getByText(/moderate — what the router would pick/)).toBeTruthy();
142
+ // Pick step shows profile preview marker
143
+ react_1.fireEvent.click(react_1.screen.getByLabelText('Next step'));
144
+ react_1.fireEvent.click(react_1.screen.getByLabelText('Next step'));
145
+ react_1.fireEvent.click(react_1.screen.getByLabelText('Next step'));
146
+ (0, vitest_1.expect)(react_1.screen.getByText(/profile preview/)).toBeTruthy();
147
+ });
148
+ });
149
+ // ─── Builder unit tests ─────────────────────────────────────────────────────
150
+ (0, vitest_1.describe)('buildWalkthroughDecisions', () => {
151
+ (0, vitest_1.it)('builds a real decision from history with candidates from the matching profile', () => {
152
+ const decisions = (0, RoutingWalkthrough_1.buildWalkthroughDecisions)(baseRouting);
153
+ (0, vitest_1.expect)(decisions).toHaveLength(1);
154
+ (0, vitest_1.expect)(decisions[0].real).toBe(true);
155
+ (0, vitest_1.expect)(decisions[0].winner).toBe('gemini/gemini-2.0-flash-exp');
156
+ (0, vitest_1.expect)(decisions[0].candidates.map((c) => c.provider)).toEqual(['gemini', 'groq', 'local']);
157
+ });
158
+ (0, vitest_1.it)('derives exclusions from providers known to the system but not scored', () => {
159
+ const routing = {
160
+ ...baseRouting,
161
+ // A provider known via usage that is NOT among the scored candidates
162
+ usage: {
163
+ total: 5, last24h: 2,
164
+ byProvider: { gemini: 3, nim: 2 },
165
+ byModel: {}, bySource: {}, byComplexity: {},
166
+ updatedAt: Date.now(),
167
+ },
168
+ quota: {
169
+ enabled: true,
170
+ entries: [{ provider: 'nim', model: 'x', tokensConsumed: 0, requests: 0, windowLengthMs: 0, resetsInMs: 0, parked: true, cooldownRemaining: 0 }],
171
+ updatedAt: Date.now(),
172
+ },
173
+ };
174
+ const decisions = (0, RoutingWalkthrough_1.buildWalkthroughDecisions)(routing);
175
+ (0, vitest_1.expect)(decisions[0].exclusions.map((e) => e.provider)).toContain('nim');
176
+ (0, vitest_1.expect)(decisions[0].exclusions.find((e) => e.provider === 'nim')?.reason).toMatch(/quota/);
177
+ });
178
+ (0, vitest_1.it)('appends the real winner to candidates when it is missing from the profile', () => {
179
+ // History pick is openrouter, but the moderate profile only scores
180
+ // gemini/groq/local — the winner must still appear in the scored list.
181
+ const drifted = {
182
+ ...baseRouting,
183
+ history: [{ ...historyEntry, provider: 'openrouter', model: 'openai/gpt-4o' }],
184
+ };
185
+ const decisions = (0, RoutingWalkthrough_1.buildWalkthroughDecisions)(drifted);
186
+ (0, vitest_1.expect)(decisions[0].winner).toBe('openrouter/openai/gpt-4o');
187
+ (0, vitest_1.expect)(decisions[0].candidates.map((c) => c.provider)).toContain('openrouter');
188
+ // The appended winner is not double-listed and uses the recorded score/reason
189
+ (0, vitest_1.expect)(decisions[0].candidates.filter((c) => c.provider === 'openrouter')).toHaveLength(1);
190
+ (0, vitest_1.expect)(decisions[0].winnerReason).toBe('actual pick recorded');
191
+ });
192
+ (0, vitest_1.it)('returns profile decisions when history is empty and caps real decisions', () => {
193
+ const empty = { ...baseRouting, history: [] };
194
+ const profileDecisions = (0, RoutingWalkthrough_1.buildWalkthroughDecisions)(empty);
195
+ (0, vitest_1.expect)(profileDecisions).toHaveLength(1);
196
+ (0, vitest_1.expect)(profileDecisions[0].real).toBe(false);
197
+ // Cap: only the 8 most recent history entries become replayable decisions
198
+ const many = Array.from({ length: 12 }, (_, i) => ({
199
+ ...historyEntry,
200
+ id: `route-${i}`,
201
+ task: `task ${i}`,
202
+ // Distinct timestamps so "most recent first" ordering is deterministic
203
+ timestamp: Date.now() - (11 - i) * 60_000,
204
+ }));
205
+ const capped = (0, RoutingWalkthrough_1.buildWalkthroughDecisions)({ ...baseRouting, history: many });
206
+ (0, vitest_1.expect)(capped).toHaveLength(8);
207
+ (0, vitest_1.expect)(capped[0].task).toBe('task 11'); // most recent first
208
+ });
209
+ });