adaptive-memory-multi-model-router 2.13.18 → 2.13.22

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 (91) hide show
  1. package/.dockerignore +82 -0
  2. package/.env.example +303 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +83 -12
  4. package/.github/ISSUE_TEMPLATE/config.yml +12 -6
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +61 -10
  6. package/.github/PULL_REQUEST_TEMPLATE.md +53 -26
  7. package/.github/dependabot.yml +9 -0
  8. package/.github/workflows/codeql.yml +38 -0
  9. package/.github/workflows/npm-publish.yml +20 -0
  10. package/.github/workflows/stale.yml +56 -0
  11. package/ARCHITECTURE.md +346 -0
  12. package/AUDIT_REPORT.md +28 -0
  13. package/CHANGELOG.md +386 -22
  14. package/CONTRIBUTORS.md +20 -0
  15. package/Dockerfile +53 -0
  16. package/Dockerfile.proxy +33 -0
  17. package/PR_STATUS_REPORT.md +148 -0
  18. package/README.md +22 -0
  19. package/RUNKIT.md +83 -0
  20. package/_schema.html +61 -15
  21. package/articles/AI_AGENT_LLM_ROUTING.md +150 -0
  22. package/articles/FROM_ZERO_TO_10K.md +107 -0
  23. package/articles/LLM_BENCHMARK_DEEP_DIVE.md +153 -0
  24. package/articles/TWEETS_10K_DOWNLOADS.md +47 -0
  25. package/articles/TWEETS_BENCHMARK_FIRST.md +46 -0
  26. package/articles/TWEETS_MCP_PLAY.md +51 -0
  27. package/articles/TWEETS_SEQUENTIAL_BROKEN.md +49 -0
  28. package/articles/TWEETS_WHY_BUILD.md +54 -0
  29. package/benchmark-results.json +26 -45
  30. package/cli/a3m +840 -0
  31. package/demo/package.json +13 -0
  32. package/demo/public/index.html +762 -0
  33. package/demo/server.js +405 -0
  34. package/dist/cli.js +4 -0
  35. package/docker-compose.yml +74 -0
  36. package/docs/.nojekyll +0 -0
  37. package/docs/BENCHMARK.md +96 -22
  38. package/docs/_config.yml +49 -0
  39. package/docs/api.html +513 -0
  40. package/docs/benchmark.html +387 -0
  41. package/docs/cli-cheatsheet.md +339 -0
  42. package/docs/comparison.md +108 -0
  43. package/docs/curl-examples.md +247 -0
  44. package/docs/index.html +390 -99
  45. package/docs/openapi.yaml +1318 -0
  46. package/docs/quick-start.html +366 -0
  47. package/docs/robots.txt +1 -1
  48. package/docs/sitemap.xml +23 -5
  49. package/docs/styles.css +682 -0
  50. package/examples/README.md +61 -0
  51. package/examples/a3m-sdk.js +124 -0
  52. package/examples/basic-route.js +54 -0
  53. package/examples/chat-loop.js +202 -0
  54. package/examples/classify-then-route.js +102 -0
  55. package/examples/cost-compare.js +120 -0
  56. package/examples/ensemble.js +160 -0
  57. package/integrations/langchain/README.md +216 -0
  58. package/integrations/langchain/a3m_langchain.ts +1360 -0
  59. package/integrations/langchain/example.ts +287 -0
  60. package/integrations/vercel-ai-sdk/README.md +49 -0
  61. package/integrations/vercel-ai-sdk/a3m_provider.ts +78 -0
  62. package/integrations/vercel-ai-sdk/example.ts +25 -0
  63. package/llms-full.txt +43 -0
  64. package/llms.txt +9 -0
  65. package/mcp-server/README.md +188 -0
  66. package/mcp-server/package.json +29 -0
  67. package/mcp-server/src/index.ts +744 -0
  68. package/mcp-server/tsconfig.json +19 -0
  69. package/package.json +3 -3
  70. package/proxy/README.md +227 -0
  71. package/proxy/package-lock.json +831 -0
  72. package/proxy/package.json +17 -0
  73. package/proxy/rate-limit.js +145 -0
  74. package/proxy/rate-limit.test.js +311 -0
  75. package/proxy/server.js +970 -0
  76. package/scripts/banner.js +29 -0
  77. package/scripts/compare-providers.sh +230 -0
  78. package/scripts/cross_post.py +443 -0
  79. package/scripts/publish_fcc.py +106 -0
  80. package/scripts/push-to-gitee.sh +52 -0
  81. package/src/tui/dashboard.ts +13 -0
  82. package/tests/__mocks__/tokenUtils.ts +22 -0
  83. package/tests/memory/episodicMemory.test.ts +227 -0
  84. package/tests/package-lock.json +1628 -0
  85. package/tests/package.json +18 -0
  86. package/tests/routing/ensembleVoting.test.ts +236 -0
  87. package/tests/routing/providerRetry.test.ts +360 -0
  88. package/tests/routing/queryTypePresets.test.ts +206 -0
  89. package/tests/tsconfig.json +21 -0
  90. package/tests/vitest.config.ts +18 -0
  91. package/.env +0 -2
@@ -0,0 +1,762 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>A3M Router — Interactive Demo</title>
7
+ <meta name="description" content="Interactive demo of A3M Router - parallel multi-LLM execution with intelligent routing" />
8
+ <style>
9
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10
+
11
+ :root {
12
+ --bg: #0f0f1a;
13
+ --surface: #1a1a2e;
14
+ --surface2: #242440;
15
+ --border: #2d2d4a;
16
+ --text: #e4e4f0;
17
+ --text2: #9090b0;
18
+ --accent1: #7c3aed;
19
+ --accent2: #4f46e5;
20
+ --accent3: #06b6d4;
21
+ --gradient: linear-gradient(135deg, #7c3aed, #4f46e5, #06b6d4);
22
+ --green: #22c55e;
23
+ --yellow: #eab308;
24
+ --red: #ef4444;
25
+ --radius: 12px;
26
+ --radius-sm: 8px;
27
+ }
28
+
29
+ body {
30
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
31
+ background: var(--bg);
32
+ color: var(--text);
33
+ min-height: 100vh;
34
+ line-height: 1.6;
35
+ }
36
+
37
+ /* ── Header ─────────────────────────────────────── */
38
+ header {
39
+ background: var(--surface);
40
+ border-bottom: 1px solid var(--border);
41
+ padding: 16px 24px;
42
+ position: sticky;
43
+ top: 0;
44
+ z-index: 100;
45
+ backdrop-filter: blur(12px);
46
+ }
47
+
48
+ .header-inner {
49
+ max-width: 960px;
50
+ margin: 0 auto;
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 16px;
54
+ }
55
+
56
+ .logo {
57
+ font-size: 28px;
58
+ font-weight: 800;
59
+ background: var(--gradient);
60
+ -webkit-background-clip: text;
61
+ -webkit-text-fill-color: transparent;
62
+ background-clip: text;
63
+ letter-spacing: -0.5px;
64
+ }
65
+
66
+ .logo-sub {
67
+ font-size: 13px;
68
+ color: var(--text2);
69
+ -webkit-text-fill-color: var(--text2);
70
+ font-weight: 400;
71
+ }
72
+
73
+ .badge {
74
+ margin-left: auto;
75
+ font-size: 12px;
76
+ padding: 4px 12px;
77
+ border-radius: 20px;
78
+ font-weight: 600;
79
+ }
80
+ .badge.live { background: #065f4620; color: var(--green); border: 1px solid var(--green); }
81
+ .badge.demo { background: #7c3aed20; color: var(--accent1); border: 1px solid var(--accent1); }
82
+
83
+ /* ── Main ───────────────────────────────────────── */
84
+ main {
85
+ max-width: 960px;
86
+ margin: 0 auto;
87
+ padding: 32px 24px;
88
+ }
89
+
90
+ section {
91
+ margin-bottom: 32px;
92
+ }
93
+
94
+ h2 {
95
+ font-size: 20px;
96
+ font-weight: 700;
97
+ margin-bottom: 16px;
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 8px;
101
+ }
102
+
103
+ h3 {
104
+ font-size: 15px;
105
+ font-weight: 600;
106
+ margin-bottom: 8px;
107
+ color: var(--text2);
108
+ }
109
+
110
+ /* ── Input Area ─────────────────────────────────── */
111
+ .input-area {
112
+ background: var(--surface);
113
+ border: 1px solid var(--border);
114
+ border-radius: var(--radius);
115
+ padding: 24px;
116
+ }
117
+
118
+ .prompt-row {
119
+ display: flex;
120
+ gap: 12px;
121
+ margin-bottom: 12px;
122
+ }
123
+
124
+ .prompt-row textarea {
125
+ flex: 1;
126
+ background: var(--surface2);
127
+ border: 1px solid var(--border);
128
+ border-radius: var(--radius-sm);
129
+ padding: 14px 16px;
130
+ font-size: 15px;
131
+ color: var(--text);
132
+ resize: vertical;
133
+ min-height: 70px;
134
+ font-family: inherit;
135
+ line-height: 1.5;
136
+ transition: border-color 0.2s;
137
+ }
138
+ .prompt-row textarea:focus {
139
+ outline: none;
140
+ border-color: var(--accent1);
141
+ box-shadow: 0 0 0 3px #7c3aed20;
142
+ }
143
+ .prompt-row textarea::placeholder { color: var(--text2); }
144
+
145
+ .controls {
146
+ display: flex;
147
+ gap: 12px;
148
+ align-items: center;
149
+ flex-wrap: wrap;
150
+ }
151
+
152
+ .mode-select {
153
+ background: var(--surface2);
154
+ border: 1px solid var(--border);
155
+ border-radius: var(--radius-sm);
156
+ padding: 10px 14px;
157
+ color: var(--text);
158
+ font-size: 14px;
159
+ cursor: pointer;
160
+ }
161
+ .mode-select:focus { outline: none; border-color: var(--accent1); }
162
+
163
+ .btn {
164
+ background: var(--gradient);
165
+ border: none;
166
+ border-radius: var(--radius-sm);
167
+ padding: 10px 28px;
168
+ color: #fff;
169
+ font-weight: 600;
170
+ font-size: 15px;
171
+ cursor: pointer;
172
+ transition: opacity 0.2s, transform 0.1s;
173
+ }
174
+ .btn:hover { opacity: 0.9; }
175
+ .btn:active { transform: scale(0.97); }
176
+ .btn:disabled { opacity: 0.4; cursor: not-allowed; }
177
+
178
+ .btn-clear {
179
+ background: transparent;
180
+ border: 1px solid var(--border);
181
+ border-radius: var(--radius-sm);
182
+ padding: 10px 18px;
183
+ color: var(--text2);
184
+ font-size: 14px;
185
+ cursor: pointer;
186
+ transition: border-color 0.2s;
187
+ }
188
+ .btn-clear:hover { border-color: var(--text2); }
189
+
190
+ .preset-chips {
191
+ display: flex;
192
+ gap: 8px;
193
+ flex-wrap: wrap;
194
+ margin-top: 12px;
195
+ }
196
+
197
+ .chip {
198
+ background: var(--surface2);
199
+ border: 1px solid var(--border);
200
+ border-radius: 20px;
201
+ padding: 6px 16px;
202
+ font-size: 13px;
203
+ color: var(--text2);
204
+ cursor: pointer;
205
+ transition: all 0.2s;
206
+ white-space: nowrap;
207
+ }
208
+ .chip:hover {
209
+ border-color: var(--accent1);
210
+ color: var(--accent1);
211
+ }
212
+
213
+ /* ── Provider Status ────────────────────────────── */
214
+ .provider-bar {
215
+ background: var(--surface);
216
+ border: 1px solid var(--border);
217
+ border-radius: var(--radius);
218
+ padding: 16px 20px;
219
+ }
220
+
221
+ #provider-count {
222
+ font-size: 14px;
223
+ color: var(--text2);
224
+ margin-bottom: 12px;
225
+ }
226
+
227
+ .provider-grid {
228
+ display: grid;
229
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
230
+ gap: 8px;
231
+ }
232
+
233
+ .provider-item {
234
+ background: var(--surface2);
235
+ border-radius: var(--radius-sm);
236
+ padding: 8px 12px;
237
+ font-size: 13px;
238
+ display: flex;
239
+ align-items: center;
240
+ gap: 8px;
241
+ }
242
+ .provider-item .dot {
243
+ width: 8px;
244
+ height: 8px;
245
+ border-radius: 50%;
246
+ flex-shrink: 0;
247
+ }
248
+ .provider-item .dot.configured { background: var(--green); }
249
+ .provider-item .dot.unconfigured { background: var(--text2); opacity: 0.3; }
250
+ .provider-item .name { flex: 1; }
251
+ .provider-item .tier {
252
+ font-size: 10px;
253
+ padding: 2px 6px;
254
+ border-radius: 4px;
255
+ text-transform: uppercase;
256
+ font-weight: 700;
257
+ }
258
+ .provider-item .tier.free { background: #22c55e20; color: var(--green); }
259
+ .provider-item .tier.cheap { background: #06b6d420; color: var(--accent3); }
260
+ .provider-item .tier.mid { background: #eab30820; color: var(--yellow); }
261
+ .provider-item .tier.premium { background: #7c3aed20; color: var(--accent1); }
262
+
263
+ /* ── Response ───────────────────────────────────── */
264
+ .response-area {
265
+ background: var(--surface);
266
+ border: 1px solid var(--border);
267
+ border-radius: var(--radius);
268
+ padding: 24px;
269
+ display: none;
270
+ }
271
+ .response-area.visible { display: block; }
272
+
273
+ .response-header {
274
+ display: flex;
275
+ justify-content: space-between;
276
+ align-items: center;
277
+ flex-wrap: wrap;
278
+ gap: 8px;
279
+ margin-bottom: 16px;
280
+ padding-bottom: 12px;
281
+ border-bottom: 1px solid var(--border);
282
+ }
283
+
284
+ .routing-badge {
285
+ display: flex;
286
+ align-items: center;
287
+ gap: 6px;
288
+ font-size: 13px;
289
+ }
290
+ .routing-badge .provider-label {
291
+ background: var(--surface2);
292
+ padding: 4px 10px;
293
+ border-radius: 6px;
294
+ font-weight: 600;
295
+ }
296
+ .routing-badge .mode-label {
297
+ background: #4f46e520;
298
+ padding: 4px 10px;
299
+ border-radius: 6px;
300
+ color: var(--accent2);
301
+ font-weight: 600;
302
+ }
303
+
304
+ .routing-stats {
305
+ display: flex;
306
+ gap: 16px;
307
+ font-size: 13px;
308
+ color: var(--text2);
309
+ }
310
+ .routing-stats span { white-space: nowrap; }
311
+
312
+ .response-content {
313
+ font-size: 15px;
314
+ line-height: 1.7;
315
+ white-space: pre-wrap;
316
+ word-break: break-word;
317
+ }
318
+ .response-content code {
319
+ background: var(--surface2);
320
+ padding: 2px 6px;
321
+ border-radius: 4px;
322
+ font-size: 13px;
323
+ }
324
+ .response-content pre {
325
+ background: var(--surface2);
326
+ border: 1px solid var(--border);
327
+ border-radius: var(--radius-sm);
328
+ padding: 16px;
329
+ overflow-x: auto;
330
+ margin: 12px 0;
331
+ font-size: 14px;
332
+ }
333
+
334
+ /* ── Analysis Panel ─────────────────────────────── */
335
+ .analysis-panel {
336
+ background: var(--surface2);
337
+ border-radius: var(--radius-sm);
338
+ padding: 16px;
339
+ margin-top: 16px;
340
+ font-size: 13px;
341
+ }
342
+ .analysis-grid {
343
+ display: grid;
344
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
345
+ gap: 8px;
346
+ margin-top: 8px;
347
+ }
348
+ .analysis-item {
349
+ display: flex;
350
+ justify-content: space-between;
351
+ padding: 4px 0;
352
+ }
353
+ .analysis-item .label { color: var(--text2); }
354
+ .analysis-item .value { font-weight: 600; }
355
+ .analysis-item .value.yes { color: var(--green); }
356
+ .analysis-item .value.no { color: var(--red); }
357
+
358
+ .confidence-bar {
359
+ height: 6px;
360
+ background: var(--border);
361
+ border-radius: 3px;
362
+ margin: 8px 0;
363
+ overflow: hidden;
364
+ }
365
+ .confidence-fill {
366
+ height: 100%;
367
+ border-radius: 3px;
368
+ background: var(--gradient);
369
+ transition: width 0.5s ease;
370
+ }
371
+
372
+ /* ── Ensemble Mode ──────────────────────────────── */
373
+ .ensemble-results {
374
+ display: grid;
375
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
376
+ gap: 12px;
377
+ margin-top: 16px;
378
+ }
379
+
380
+ .ensemble-card {
381
+ background: var(--surface2);
382
+ border: 1px solid var(--border);
383
+ border-radius: var(--radius-sm);
384
+ padding: 16px;
385
+ }
386
+ .ensemble-card h4 {
387
+ font-size: 13px;
388
+ color: var(--text2);
389
+ margin-bottom: 8px;
390
+ }
391
+ .ensemble-card .card-content {
392
+ font-size: 14px;
393
+ line-height: 1.6;
394
+ color: var(--text);
395
+ }
396
+ .ensemble-card .card-stats {
397
+ display: flex;
398
+ gap: 12px;
399
+ margin-top: 8px;
400
+ font-size: 12px;
401
+ color: var(--text2);
402
+ }
403
+
404
+ /* ── Loading ────────────────────────────────────── */
405
+ .loading {
406
+ display: none;
407
+ text-align: center;
408
+ padding: 32px;
409
+ }
410
+ .loading.visible { display: block; }
411
+
412
+ .spinner {
413
+ width: 40px;
414
+ height: 40px;
415
+ border: 3px solid var(--border);
416
+ border-top-color: var(--accent1);
417
+ border-radius: 50%;
418
+ animation: spin 0.8s linear infinite;
419
+ margin: 0 auto 12px;
420
+ }
421
+ @keyframes spin { to { transform: rotate(360deg); } }
422
+
423
+ /* ── Footer ─────────────────────────────────────── */
424
+ footer {
425
+ text-align: center;
426
+ padding: 32px 24px;
427
+ color: var(--text2);
428
+ font-size: 13px;
429
+ border-top: 1px solid var(--border);
430
+ }
431
+ footer a {
432
+ color: var(--accent1);
433
+ text-decoration: none;
434
+ }
435
+ footer a:hover { text-decoration: underline; }
436
+
437
+ /* ── Responsive ─────────────────────────────────── */
438
+ @media (max-width: 640px) {
439
+ main { padding: 16px; }
440
+ .prompt-row { flex-direction: column; }
441
+ .controls { flex-direction: column; align-items: stretch; }
442
+ .provider-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); }
443
+ .ensemble-results { grid-template-columns: 1fr; }
444
+ .response-header { flex-direction: column; align-items: stretch; }
445
+ .routing-stats { flex-wrap: wrap; }
446
+ }
447
+
448
+ /* ── Scrollbar ──────────────────────────────────── */
449
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
450
+ ::-webkit-scrollbar-track { background: transparent; }
451
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
452
+ </style>
453
+ </head>
454
+ <body>
455
+
456
+ <!-- ── Header ────────────────────────────────────── -->
457
+ <header>
458
+ <div class="header-inner">
459
+ <div>
460
+ <span class="logo">A3M</span>
461
+ <span class="logo-sub">Router</span>
462
+ </div>
463
+ <span class="badge demo" id="status-badge">Demo Mode</span>
464
+ </div>
465
+ </header>
466
+
467
+ <!-- ── Main ──────────────────────────────────────── -->
468
+ <main>
469
+
470
+ <!-- Input -->
471
+ <section>
472
+ <div class="input-area">
473
+ <div class="prompt-row">
474
+ <textarea id="prompt" placeholder="Enter your prompt here..." rows="3">Write a haiku about Kubernetes</textarea>
475
+ </div>
476
+ <div class="controls">
477
+ <select id="mode" class="mode-select">
478
+ <option value="single">Single Provider</option>
479
+ <option value="ensemble">Ensemble (Parallel)</option>
480
+ </select>
481
+ <button class="btn" id="run-btn" onclick="runQuery()">Run</button>
482
+ <button class="btn-clear" onclick="clearAll()">Clear</button>
483
+ </div>
484
+ <div class="preset-chips">
485
+ <span class="chip" onclick="setPreset('Write a haiku about Kubernetes')">Haiku</span>
486
+ <span class="chip" onclick="setPreset('Explain quantum computing to a 5-year-old')">Quantum</span>
487
+ <span class="chip" onclick="setPreset('Write a React component for a search bar')">React</span>
488
+ </div>
489
+ </div>
490
+ </section>
491
+
492
+ <!-- Loading -->
493
+ <div class="loading" id="loading">
494
+ <div class="spinner"></div>
495
+ <p style="color: var(--text2); font-size: 14px;" id="loading-text">Routing query...</p>
496
+ </div>
497
+
498
+ <!-- Response -->
499
+ <section>
500
+ <div class="response-area" id="response">
501
+ <div class="response-header">
502
+ <div class="routing-badge">
503
+ <span class="provider-label" id="routed-provider">groq/llama-3.3-70b</span>
504
+ <span class="mode-label" id="routed-mode">Single</span>
505
+ </div>
506
+ <div class="routing-stats">
507
+ <span id="stat-latency">0ms</span>
508
+ <span id="stat-cost">$0</span>
509
+ <span id="stat-mock" style="display:none;">Simulated</span>
510
+ </div>
511
+ </div>
512
+ <div class="response-content" id="response-content"></div>
513
+
514
+ <!-- Analysis panel -->
515
+ <div class="analysis-panel" id="analysis-panel" style="display:none;">
516
+ <h3 style="margin-bottom:4px;">Routing Analysis</h3>
517
+ <div class="confidence-bar">
518
+ <div class="confidence-fill" id="confidence-fill" style="width:0%"></div>
519
+ </div>
520
+ <p style="font-size:12px;color:var(--text2);margin-bottom:8px;" id="confidence-text">Confidence: 0%</p>
521
+ <p style="font-size:13px;color:var(--text2);margin-bottom:8px;" id="routing-reasoning"></p>
522
+ <div class="analysis-grid" id="analysis-grid"></div>
523
+ </div>
524
+
525
+ <!-- Ensemble results -->
526
+ <div class="ensemble-results" id="ensemble-results"></div>
527
+ </div>
528
+ </section>
529
+
530
+ <!-- Provider Status -->
531
+ <section>
532
+ <h2>Available Providers</h2>
533
+ <div class="provider-bar">
534
+ <p id="provider-count">Loading providers...</p>
535
+ <div class="provider-grid" id="provider-grid"></div>
536
+ </div>
537
+ </section>
538
+ </main>
539
+
540
+ <!-- ── Footer ────────────────────────────────────── -->
541
+ <footer>
542
+ <p>
543
+ <a href="https://github.com/Das-rebel/a3m-router" target="_blank" rel="noopener">A3M Router</a>
544
+ &mdash; Parallel multi-LLM execution with intelligent routing.
545
+ <br/>
546
+ No data tracked. All processing happens client-side via the A3M Router API.
547
+ </p>
548
+ </footer>
549
+
550
+ <script>
551
+ // ============================================================
552
+ // Load providers on page load
553
+ // ============================================================
554
+ window.addEventListener('DOMContentLoaded', async () => {
555
+ try {
556
+ const resp = await fetch('/api/providers');
557
+ const data = await resp.json();
558
+
559
+ const badge = document.getElementById('status-badge');
560
+ if (data.hasApiKeys) {
561
+ badge.textContent = 'Live Mode';
562
+ badge.className = 'badge live';
563
+ } else {
564
+ badge.textContent = 'Demo Mode';
565
+ badge.className = 'badge demo';
566
+ }
567
+
568
+ document.getElementById('provider-count').textContent =
569
+ data.count > 0
570
+ ? `${data.count} providers configured (${data.note})`
571
+ : `No providers configured — showing simulated responses`;
572
+
573
+ const grid = document.getElementById('provider-grid');
574
+ grid.innerHTML = '';
575
+
576
+ if (data.providers.length === 0) {
577
+ // Show default providers even when unconfigured
578
+ const defaults = [
579
+ { name: 'Groq', tier: 'cheap' },
580
+ { name: 'OpenAI', tier: 'premium' },
581
+ { name: 'Anthropic', tier: 'premium' },
582
+ { name: 'Mistral', tier: 'mid' },
583
+ { name: 'DeepSeek', tier: 'mid' },
584
+ { name: 'Google', tier: 'free' },
585
+ ];
586
+ defaults.forEach(p => {
587
+ const div = document.createElement('div');
588
+ div.className = 'provider-item';
589
+ div.innerHTML = \`
590
+ <span class="dot unconfigured"></span>
591
+ <span class="name">\${p.name}</span>
592
+ <span class="tier \${p.tier}">\${p.tier}</span>
593
+ \`;
594
+ grid.appendChild(div);
595
+ });
596
+ return;
597
+ }
598
+
599
+ data.providers.forEach(p => {
600
+ const div = document.createElement('div');
601
+ div.className = 'provider-item';
602
+ const configured = p.type !== 'api' || true; // always show as configured for display
603
+ div.innerHTML = \`
604
+ <span class="dot \${configured ? 'configured' : 'unconfigured'}"></span>
605
+ <span class="name">\${p.name}</span>
606
+ <span class="tier \${p.tier}">\${p.tier}</span>
607
+ \`;
608
+ grid.appendChild(div);
609
+ });
610
+ } catch (e) {
611
+ document.getElementById('provider-count').textContent = 'Failed to load providers';
612
+ }
613
+ });
614
+
615
+ // ============================================================
616
+ // Set preset prompt
617
+ // ============================================================
618
+ function setPreset(text) {
619
+ document.getElementById('prompt').value = text;
620
+ }
621
+
622
+ // ============================================================
623
+ // Clear all
624
+ // ============================================================
625
+ function clearAll() {
626
+ document.getElementById('prompt').value = '';
627
+ document.getElementById('response').className = 'response-area';
628
+ document.getElementById('response-content').textContent = '';
629
+ document.getElementById('ensemble-results').innerHTML = '';
630
+ document.getElementById('analysis-panel').style.display = 'none';
631
+ }
632
+
633
+ // ============================================================
634
+ // Run query
635
+ // ============================================================
636
+ async function runQuery() {
637
+ const prompt = document.getElementById('prompt').value.trim();
638
+ if (!prompt) return;
639
+
640
+ const mode = document.getElementById('mode').value;
641
+ const btn = document.getElementById('run-btn');
642
+ const loading = document.getElementById('loading');
643
+ const response = document.getElementById('response');
644
+
645
+ btn.disabled = true;
646
+ loading.className = 'loading visible';
647
+ response.className = 'response-area';
648
+
649
+ document.getElementById('loading-text').textContent =
650
+ mode === 'ensemble' ? 'Running parallel ensemble...' : 'Routing query...';
651
+
652
+ try {
653
+ const resp = await fetch('/api/route', {
654
+ method: 'POST',
655
+ headers: { 'Content-Type': 'application/json' },
656
+ body: JSON.stringify({ prompt, mode }),
657
+ });
658
+ const data = await resp.json();
659
+
660
+ loading.className = 'loading';
661
+ response.className = 'response-area visible';
662
+
663
+ // Route info
664
+ document.getElementById('routed-provider').textContent =
665
+ data.provider + '/' + (data.model || 'auto');
666
+ document.getElementById('routed-mode').textContent =
667
+ data.mode === 'ensemble' ? 'Ensemble' : 'Single';
668
+ document.getElementById('stat-latency').textContent = data.latency + 'ms';
669
+ document.getElementById('stat-cost').textContent = '$' + data.cost;
670
+
671
+ const mockEl = document.getElementById('stat-mock');
672
+ if (data.usedMock) {
673
+ mockEl.style.display = 'inline';
674
+ } else {
675
+ mockEl.style.display = 'none';
676
+ }
677
+
678
+ // Content
679
+ document.getElementById('response-content').textContent = data.result;
680
+
681
+ // Analysis
682
+ if (data.features || data.routing) {
683
+ const panel = document.getElementById('analysis-panel');
684
+ panel.style.display = 'block';
685
+
686
+ const features = data.features || {};
687
+ const routing = data.routing || {};
688
+ const confidence = routing.confidence || 0.5;
689
+
690
+ document.getElementById('confidence-fill').style.width = (confidence * 100) + '%';
691
+ document.getElementById('confidence-text').textContent =
692
+ 'Confidence: ' + Math.round(confidence * 100) + '%';
693
+ document.getElementById('routing-reasoning').textContent =
694
+ routing.reasoning || 'Intelligent routing based on query analysis';
695
+
696
+ const grid = document.getElementById('analysis-grid');
697
+ grid.innerHTML = '';
698
+
699
+ const items = [
700
+ { label: 'Complexity', value: (features.complexity || 0).toFixed(2) },
701
+ { label: 'Length', value: features.length + ' words' },
702
+ { label: 'Has Code', value: features.has_code },
703
+ { label: 'Has Math', value: features.has_math },
704
+ { label: 'Reasoning', value: features.requires_reasoning },
705
+ { label: 'Creative', value: features.is_creative },
706
+ { label: 'Domain', value: features.detected_domain || 'general' },
707
+ ];
708
+
709
+ items.forEach(item => {
710
+ const div = document.createElement('div');
711
+ div.className = 'analysis-item';
712
+ const isBool = typeof item.value === 'boolean';
713
+ div.innerHTML = \`
714
+ <span class="label">\${item.label}</span>
715
+ <span class="value \${isBool ? (item.value ? 'yes' : 'no') : ''}">
716
+ \${isBool ? (item.value ? 'Yes' : 'No') : item.value}
717
+ </span>
718
+ \`;
719
+ grid.appendChild(div);
720
+ });
721
+ }
722
+
723
+ // Ensemble results
724
+ const ensembleEl = document.getElementById('ensemble-results');
725
+ if (data.responses && data.responses.length > 0) {
726
+ ensembleEl.innerHTML = '';
727
+ data.responses.forEach(r => {
728
+ const card = document.createElement('div');
729
+ card.className = 'ensemble-card';
730
+ card.innerHTML = \`
731
+ <h4>\${r.provider} — \${r.model}</h4>
732
+ <div class="card-content">\${r.content}</div>
733
+ <div class="card-stats">
734
+ <span>\${r.latency}ms</span>
735
+ <span>\$\${r.cost}</span>
736
+ </div>
737
+ \`;
738
+ ensembleEl.appendChild(card);
739
+ });
740
+ } else {
741
+ ensembleEl.innerHTML = '';
742
+ }
743
+ } catch (e) {
744
+ loading.className = 'loading';
745
+ response.className = 'response-area visible';
746
+ document.getElementById('response-content').textContent =
747
+ 'Error: ' + e.message + '\n\nPlease check that the server is running.';
748
+ } finally {
749
+ btn.disabled = false;
750
+ }
751
+ }
752
+
753
+ // Allow Ctrl+Enter to submit
754
+ document.getElementById('prompt').addEventListener('keydown', (e) => {
755
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
756
+ e.preventDefault();
757
+ runQuery();
758
+ }
759
+ });
760
+ </script>
761
+ </body>
762
+ </html>