@luckydraw/cumulus 0.31.66 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -556
- package/LICENSE +150 -0
- package/README.md +27 -8
- package/dist/gateway/adapters/webchat.d.ts +15 -0
- package/dist/gateway/adapters/webchat.d.ts.map +1 -1
- package/dist/gateway/adapters/webchat.js +78 -5
- package/dist/gateway/adapters/webchat.js.map +1 -1
- package/dist/gateway/config.d.ts +17 -2
- package/dist/gateway/config.d.ts.map +1 -1
- package/dist/gateway/config.js +10 -3
- package/dist/gateway/config.js.map +1 -1
- package/dist/gateway/daemon.d.ts +3 -1
- package/dist/gateway/daemon.d.ts.map +1 -1
- package/dist/gateway/daemon.js +128 -39
- package/dist/gateway/daemon.js.map +1 -1
- package/dist/gateway/namespaces.d.ts +34 -0
- package/dist/gateway/namespaces.d.ts.map +1 -1
- package/dist/gateway/namespaces.js +58 -0
- package/dist/gateway/namespaces.js.map +1 -1
- package/dist/gateway/server.d.ts +8 -0
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +150 -41
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/setup.d.ts +32 -0
- package/dist/gateway/setup.d.ts.map +1 -1
- package/dist/gateway/setup.js +23 -3
- package/dist/gateway/setup.js.map +1 -1
- package/dist/gateway/static/blex-render.js +341 -0
- package/dist/gateway/static/chat.html +1 -0
- package/dist/gateway/static/widget.js +1009 -738
- package/dist/lib/gateway.d.ts +30 -8
- package/dist/lib/gateway.d.ts.map +1 -1
- package/dist/lib/gateway.js +36 -11
- package/dist/lib/gateway.js.map +1 -1
- package/dist/lib/history.d.ts +22 -0
- package/dist/lib/history.d.ts.map +1 -1
- package/dist/lib/history.js +59 -21
- package/dist/lib/history.js.map +1 -1
- package/dist/lib/huggingface-provider.d.ts.map +1 -1
- package/dist/lib/huggingface-provider.js +11 -3
- package/dist/lib/huggingface-provider.js.map +1 -1
- package/dist/lib/license.d.ts +76 -0
- package/dist/lib/license.d.ts.map +1 -0
- package/dist/lib/license.js +141 -0
- package/dist/lib/license.js.map +1 -0
- package/docs/agentic-harness-primer.md +283 -0
- package/docs/conditional-continuation.md +167 -0
- package/docs/web-app-agent-guide.md +559 -0
- package/examples/web-app-agent/README.md +334 -0
- package/examples/web-app-agent/agent/mcp-shim.js +105 -0
- package/examples/web-app-agent/gateway.config.example.json +70 -0
- package/examples/web-app-agent/package.json +13 -0
- package/examples/web-app-agent/public/agent/blex-mount.js +136 -0
- package/examples/web-app-agent/public/agent/bridge-mount.js +91 -0
- package/examples/web-app-agent/public/agent/chat-client.js +104 -0
- package/examples/web-app-agent/public/agent/commands.js +256 -0
- package/examples/web-app-agent/public/agent/device-thread.js +48 -0
- package/examples/web-app-agent/public/agent/panel.css +113 -0
- package/examples/web-app-agent/public/agent/panel.js +392 -0
- package/examples/web-app-agent/public/app.js +250 -0
- package/examples/web-app-agent/public/index.html +126 -0
- package/examples/web-app-agent/server.js +379 -0
- package/package.json +7 -3
|
@@ -177,6 +177,7 @@
|
|
|
177
177
|
[
|
|
178
178
|
{ provider: 'openai', key: 'openaiApiKey' },
|
|
179
179
|
{ provider: 'huggingface', key: 'hfApiKey' },
|
|
180
|
+
{ provider: 'license', key: 'licenseKey' },
|
|
180
181
|
].forEach(function (entry) {
|
|
181
182
|
var credential = credentials[entry.provider] || {};
|
|
182
183
|
var value = String(credential.value || '').trim();
|
|
@@ -186,6 +187,48 @@
|
|
|
186
187
|
return payload;
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
// ─── Default-application rules (task 130, Option C) ─────────────────────────
|
|
191
|
+
// One gateway default across the whole catalog. Picking an Anthropic model as
|
|
192
|
+
// the gateway default forcibly couples the Claude sub-model default to it —
|
|
193
|
+
// when the gateway default IS Anthropic, `model: 'claude'` +
|
|
194
|
+
// `claudeModels[].default` are the same knob and cannot diverge.
|
|
195
|
+
function applyGatewayDefault(catalog, modelId) {
|
|
196
|
+
var chosen = null;
|
|
197
|
+
catalog.forEach(function (model) {
|
|
198
|
+
model.gatewayDefault = model.id === modelId;
|
|
199
|
+
if (model.gatewayDefault) chosen = model;
|
|
200
|
+
});
|
|
201
|
+
if (chosen && chosen.provider === 'anthropic') {
|
|
202
|
+
catalog.forEach(function (model) {
|
|
203
|
+
if (model.provider === 'anthropic') model.providerDefault = model.id === modelId;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return chosen;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function applyClaudeSubDefault(catalog, modelId) {
|
|
210
|
+
catalog.forEach(function (model) {
|
|
211
|
+
if (model.provider === 'anthropic') model.providerDefault = model.id === modelId;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// The Claude sub-model default is a visible control ONLY when it can actually
|
|
216
|
+
// diverge from the gateway default: the gateway default is non-Anthropic
|
|
217
|
+
// (else coupling forces equality) and there are ≥2 Anthropic models (else
|
|
218
|
+
// there is no choice to make).
|
|
219
|
+
function claudeSubDefaultVisible(catalog) {
|
|
220
|
+
var anthropicCount = 0;
|
|
221
|
+
for (var i = 0; i < catalog.length; i++) {
|
|
222
|
+
if (catalog[i].provider === 'anthropic') anthropicCount++;
|
|
223
|
+
}
|
|
224
|
+
if (anthropicCount < 2) return false;
|
|
225
|
+
var gatewayDefault = null;
|
|
226
|
+
for (var j = 0; j < catalog.length; j++) {
|
|
227
|
+
if (catalog[j].gatewayDefault) gatewayDefault = catalog[j];
|
|
228
|
+
}
|
|
229
|
+
return !gatewayDefault || gatewayDefault.provider !== 'anthropic';
|
|
230
|
+
}
|
|
231
|
+
|
|
189
232
|
// ─── Styles ─────────────────────────────────────────────────────────────────
|
|
190
233
|
var STYLES = [
|
|
191
234
|
'.cumulus-widget * { box-sizing: border-box; margin: 0; padding: 0; }',
|
|
@@ -366,6 +409,19 @@
|
|
|
366
409
|
'.blex-block-container[data-blex-ready="true"] {',
|
|
367
410
|
' min-height: auto;',
|
|
368
411
|
'}',
|
|
412
|
+
/* Raw-fence fallback: shown when nothing claims the container (user message,
|
|
413
|
+
changelog, or blex.min.js failed to load). Better a visible fence than the
|
|
414
|
+
blank rectangle min-height used to produce. */
|
|
415
|
+
'.blex-fallback {',
|
|
416
|
+
' margin: 0; padding: 0.6em 0.8em;',
|
|
417
|
+
' background: rgba(255,255,255,0.04);',
|
|
418
|
+
' border: 1px solid rgba(255,255,255,0.08);',
|
|
419
|
+
' border-radius: 8px;',
|
|
420
|
+
' font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;',
|
|
421
|
+
' font-size: 0.82em; line-height: 1.45;',
|
|
422
|
+
' white-space: pre-wrap; word-break: break-word;',
|
|
423
|
+
' opacity: 0.85;',
|
|
424
|
+
'}',
|
|
369
425
|
/* Blex CSS isolation — revert widget resets inside blex blocks */
|
|
370
426
|
'.blex-block-container * { margin: revert; padding: revert; line-height: revert; list-style: revert; border-spacing: revert; border: revert; }',
|
|
371
427
|
'.blex-block-container { isolation: isolate; }',
|
|
@@ -1197,6 +1253,12 @@
|
|
|
1197
1253
|
' text-decoration: underline; text-underline-offset: 2px;',
|
|
1198
1254
|
'}',
|
|
1199
1255
|
'.cumulus-topbar-changelog:hover { color: #ccc; background: #3a3a3a; }',
|
|
1256
|
+
'.cumulus-topbar-settings {',
|
|
1257
|
+
' background: none; border: none; color: #888; cursor: pointer;',
|
|
1258
|
+
' font-size: 15px; padding: 2px 5px; line-height: 1;',
|
|
1259
|
+
' border-radius: 3px; transition: color 0.2s, background 0.2s;',
|
|
1260
|
+
'}',
|
|
1261
|
+
'.cumulus-topbar-settings:hover { color: #ccc; background: #3a3a3a; }',
|
|
1200
1262
|
'.cumulus-changelog-popover {',
|
|
1201
1263
|
' position: absolute; top: 44px; left: 12px; z-index: 1000;',
|
|
1202
1264
|
' width: min(480px, calc(100vw - 24px)); max-height: 70vh; overflow-y: auto;',
|
|
@@ -1315,137 +1377,188 @@
|
|
|
1315
1377
|
'.cumulus-model-popover select { max-width: 152px; margin: 0; }',
|
|
1316
1378
|
|
|
1317
1379
|
/* ── Settings modal (provider-organized model catalog + credentials) ── */
|
|
1380
|
+
/* These three roots mount on document.body, OUTSIDE .cumulus-widget, so they
|
|
1381
|
+
must carry the font stack themselves or the browser falls back to serif. */
|
|
1382
|
+
'.cumulus-settings-backdrop, .cumulus-context-menu, .cumulus-confirm-dialog {',
|
|
1383
|
+
' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;',
|
|
1384
|
+
' -webkit-font-smoothing: antialiased;',
|
|
1385
|
+
'}',
|
|
1318
1386
|
'.cumulus-settings-backdrop {',
|
|
1319
|
-
' position: fixed; inset: 0; z-index: 1000; background: rgba(0,0,0,0.
|
|
1387
|
+
' position: fixed; inset: 0; z-index: 1000; background: rgba(0,0,0,0.6);',
|
|
1388
|
+
' backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);',
|
|
1320
1389
|
' display: flex; align-items: center; justify-content: center; padding: 20px;',
|
|
1390
|
+
' font-size: 13px; line-height: 1.5; color: #e3e5ea;',
|
|
1321
1391
|
'}',
|
|
1322
1392
|
'.cumulus-settings-modal {',
|
|
1323
|
-
' background: #
|
|
1393
|
+
' background: #1f2023; border: 1px solid #313338; border-radius: 14px;',
|
|
1324
1394
|
' width: min(720px, 100%); max-height: 86vh; display: flex; flex-direction: column;',
|
|
1325
|
-
' box-shadow: 0
|
|
1395
|
+
' box-shadow: 0 24px 64px rgba(0,0,0,0.55); overflow: hidden;',
|
|
1326
1396
|
'}',
|
|
1327
1397
|
'.cumulus-settings-header {',
|
|
1328
1398
|
' display: flex; align-items: flex-start; justify-content: space-between;',
|
|
1329
|
-
' padding: 14px
|
|
1399
|
+
' padding: 16px 20px 14px; border-bottom: 1px solid #2a2b30; flex-shrink: 0;',
|
|
1330
1400
|
'}',
|
|
1331
|
-
'.cumulus-settings-title { font-weight: 600; font-size: 15px; color: #
|
|
1332
|
-
'.cumulus-settings-subtitle { font-size: 12px; color: #
|
|
1333
|
-
'.cumulus-settings-body { padding: 6px
|
|
1401
|
+
'.cumulus-settings-title { font-weight: 600; font-size: 15px; letter-spacing: -0.01em; color: #f2f3f5; }',
|
|
1402
|
+
'.cumulus-settings-subtitle { font-size: 12px; color: #8b8f96; margin-top: 3px; }',
|
|
1403
|
+
'.cumulus-settings-body { padding: 6px 20px 14px; overflow-y: auto; flex: 1; }',
|
|
1334
1404
|
'.cumulus-settings-footer {',
|
|
1335
|
-
' display: flex; align-items: center; gap: 12px; padding: 13px
|
|
1336
|
-
' border-top: 1px solid #
|
|
1405
|
+
' display: flex; align-items: center; gap: 12px; padding: 13px 20px;',
|
|
1406
|
+
' border-top: 1px solid #2a2b30; flex-shrink: 0; background: #1b1c1f;',
|
|
1337
1407
|
'}',
|
|
1338
1408
|
'.cumulus-settings-section-label {',
|
|
1339
|
-
' font-size:
|
|
1340
|
-
' color: #
|
|
1409
|
+
' font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;',
|
|
1410
|
+
' color: #6e7278; margin: 18px 0 8px;',
|
|
1411
|
+
'}',
|
|
1412
|
+
'.cumulus-settings-body > .cumulus-settings-section-label:first-child { margin-top: 12px; }',
|
|
1413
|
+
'.cumulus-settings-default-card {',
|
|
1414
|
+
' display: flex; align-items: center; gap: 12px; padding: 12px 14px; margin: 4px 0 2px;',
|
|
1415
|
+
' border: 1px solid rgba(79,140,255,0.35); border-radius: 12px; background: rgba(79,140,255,0.07);',
|
|
1341
1416
|
'}',
|
|
1342
|
-
'.cumulus-settings-
|
|
1417
|
+
'.cumulus-settings-default-card label { color: #c8d4e8; font-size: 12.5px; font-weight: 500; flex-shrink: 0; }',
|
|
1418
|
+
'.cumulus-settings-default-card select { flex: 1; min-width: 0; }',
|
|
1343
1419
|
'.cumulus-settings-model-row {',
|
|
1344
|
-
' display:
|
|
1345
|
-
'
|
|
1346
|
-
'
|
|
1420
|
+
' display: flex; align-items: center; gap: 10px; padding: 9px 12px;',
|
|
1421
|
+
' border: 1px solid #2c2d32; border-radius: 10px; margin-bottom: 6px; background: #1e1f23;',
|
|
1422
|
+
' transition: border-color 0.12s;',
|
|
1347
1423
|
'}',
|
|
1348
|
-
'.cumulus-settings-model-row
|
|
1349
|
-
'.cumulus-settings-radio-label { color: #777; font-size: 9px; text-align: center; line-height: 1.15; }',
|
|
1424
|
+
'.cumulus-settings-model-row:hover { border-color: #3a3c42; }',
|
|
1350
1425
|
'.cumulus-settings-row-input { min-width: 0; width: 100%; }',
|
|
1351
|
-
'.cumulus-settings-model-row.is-default { border-color:
|
|
1352
|
-
'.cumulus-settings-default-toggle { display: flex; align-items: center; cursor: pointer; flex-shrink: 0; }',
|
|
1353
|
-
'.cumulus-settings-default-toggle input { cursor: pointer; accent-color: #3b82f6; margin: 0; }',
|
|
1426
|
+
'.cumulus-settings-model-row.is-default { border-color: rgba(79,140,255,0.45); background: rgba(79,140,255,0.08); }',
|
|
1354
1427
|
'.cumulus-settings-model-name { flex: 1; min-width: 0; }',
|
|
1355
1428
|
'.cumulus-settings-model-label {',
|
|
1356
|
-
' color: #
|
|
1429
|
+
' color: #e7e8ea; font-size: 13px; font-weight: 500;',
|
|
1430
|
+
' overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
|
1357
1431
|
'}',
|
|
1358
1432
|
'.cumulus-settings-model-id {',
|
|
1359
|
-
' color: #
|
|
1433
|
+
' color: #7d808a; font-size: 11px;',
|
|
1434
|
+
' font-family: ui-monospace, "SF Mono", "Cascadia Code", Consolas, monospace;',
|
|
1360
1435
|
' overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 1px;',
|
|
1361
1436
|
'}',
|
|
1437
|
+
'.cumulus-settings-context-chip {',
|
|
1438
|
+
' color: #9a9da5; font-size: 10.5px; font-variant-numeric: tabular-nums;',
|
|
1439
|
+
' border: 1px solid #383a40; border-radius: 999px; background: #1b1c1f;',
|
|
1440
|
+
' padding: 1px 8px; white-space: nowrap; flex-shrink: 0;',
|
|
1441
|
+
'}',
|
|
1362
1442
|
'.cumulus-settings-default-badge {',
|
|
1363
|
-
' display: none; font-size:
|
|
1364
|
-
' background: rgba(
|
|
1365
|
-
' border-radius:
|
|
1443
|
+
' display: none; font-size: 10.5px; font-weight: 600; color: #8ab4f8;',
|
|
1444
|
+
' background: rgba(79,140,255,0.14); border: 1px solid rgba(79,140,255,0.35);',
|
|
1445
|
+
' border-radius: 999px; padding: 1px 9px; flex-shrink: 0;',
|
|
1366
1446
|
'}',
|
|
1367
1447
|
'.cumulus-settings-model-row.is-default .cumulus-settings-default-badge { display: inline-block; }',
|
|
1448
|
+
'.cumulus-settings-row-actions {',
|
|
1449
|
+
' display: flex; align-items: center; gap: 2px; flex-shrink: 0; opacity: 0;',
|
|
1450
|
+
' transition: opacity 0.12s;',
|
|
1451
|
+
'}',
|
|
1452
|
+
'.cumulus-settings-model-row:hover .cumulus-settings-row-actions,',
|
|
1453
|
+
'.cumulus-settings-model-row:focus-within .cumulus-settings-row-actions { opacity: 1; }',
|
|
1454
|
+
'@media (hover: none) { .cumulus-settings-row-actions { opacity: 1; } }',
|
|
1455
|
+
'.cumulus-settings-row-edit {',
|
|
1456
|
+
' background: none; border: none; color: #6e7278; cursor: pointer; font-size: 13px;',
|
|
1457
|
+
' line-height: 1; padding: 2px 4px; flex-shrink: 0; font-family: inherit;',
|
|
1458
|
+
'}',
|
|
1459
|
+
'.cumulus-settings-row-edit:hover { color: #8ab4f8; }',
|
|
1368
1460
|
'.cumulus-settings-model-remove {',
|
|
1369
|
-
' background: none; border: none; color: #
|
|
1370
|
-
' line-height: 1; padding: 0 2px; flex-shrink: 0;',
|
|
1461
|
+
' background: none; border: none; color: #6e7278; cursor: pointer; font-size: 17px;',
|
|
1462
|
+
' line-height: 1; padding: 0 2px; flex-shrink: 0; font-family: inherit;',
|
|
1371
1463
|
'}',
|
|
1372
1464
|
'.cumulus-settings-model-remove:hover { color: #ef4444; }',
|
|
1465
|
+
'.cumulus-settings-model-row.editing { display: block; }',
|
|
1466
|
+
'.cumulus-settings-edit-grid {',
|
|
1467
|
+
' display: grid; grid-template-columns: minmax(140px,1.3fr) minmax(110px,1fr) 105px auto;',
|
|
1468
|
+
' gap: 8px; align-items: end;',
|
|
1469
|
+
'}',
|
|
1470
|
+
'.cumulus-settings-edit-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }',
|
|
1471
|
+
'.cumulus-settings-edit-field > span {',
|
|
1472
|
+
' color: #6e7278; font-size: 10px; font-weight: 600;',
|
|
1473
|
+
' text-transform: uppercase; letter-spacing: 0.05em;',
|
|
1474
|
+
'}',
|
|
1475
|
+
'.cumulus-settings-claude-default-row {',
|
|
1476
|
+
' display: flex; align-items: center; gap: 10px; padding: 4px 12px 10px;',
|
|
1477
|
+
'}',
|
|
1478
|
+
'.cumulus-settings-claude-default-row label { color: #9a9da5; font-size: 11.5px; flex-shrink: 0; }',
|
|
1479
|
+
'.cumulus-settings-claude-default-row select { flex: 1; min-width: 0; }',
|
|
1373
1480
|
'.cumulus-settings-provider-card {',
|
|
1374
|
-
' border: 1px solid #
|
|
1481
|
+
' border: 1px solid #2c2d32; border-radius: 12px; background: #232428; margin: 10px 0;',
|
|
1375
1482
|
' overflow: hidden;',
|
|
1376
1483
|
'}',
|
|
1377
1484
|
'.cumulus-settings-provider-head {',
|
|
1378
1485
|
' display: flex; align-items: center; justify-content: space-between; gap: 14px;',
|
|
1379
|
-
' padding:
|
|
1486
|
+
' padding: 13px 14px; background: #26272c; border-bottom: 1px solid #2c2d32;',
|
|
1380
1487
|
'}',
|
|
1381
|
-
'.cumulus-settings-provider-title { color: #
|
|
1382
|
-
'.cumulus-settings-provider-desc { color: #
|
|
1488
|
+
'.cumulus-settings-provider-title { color: #f2f3f5; font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em; }',
|
|
1489
|
+
'.cumulus-settings-provider-desc { color: #7d808a; font-size: 11.5px; margin-top: 2px; }',
|
|
1383
1490
|
'.cumulus-settings-provider-count {',
|
|
1384
|
-
' color: #
|
|
1385
|
-
'
|
|
1491
|
+
' color: #9a9da5; font-size: 10.5px; font-variant-numeric: tabular-nums;',
|
|
1492
|
+
' border: 1px solid #383a40; border-radius: 999px; background: #1b1c1f;',
|
|
1493
|
+
' padding: 2px 9px; white-space: nowrap;',
|
|
1386
1494
|
'}',
|
|
1387
|
-
'.cumulus-settings-provider-models { padding: 10px 10px
|
|
1388
|
-
'.cumulus-settings-provider-models .cumulus-settings-model-row { margin-bottom:
|
|
1389
|
-
'.cumulus-settings-provider-empty { color: #
|
|
1495
|
+
'.cumulus-settings-provider-models { padding: 10px 10px 4px; }',
|
|
1496
|
+
'.cumulus-settings-provider-models .cumulus-settings-model-row { margin-bottom: 6px; }',
|
|
1497
|
+
'.cumulus-settings-provider-empty { color: #6e7278; font-size: 12px; padding: 8px 4px 14px; }',
|
|
1390
1498
|
'.cumulus-settings-provider-credential {',
|
|
1391
1499
|
' display: grid; grid-template-columns: 120px minmax(0,1fr) auto; align-items: center; gap: 10px;',
|
|
1392
|
-
' padding:
|
|
1500
|
+
' padding: 11px 14px; background: #1b1c1f; border-top: 1px solid #2a2b30;',
|
|
1501
|
+
'}',
|
|
1502
|
+
'.cumulus-settings-credential-note {',
|
|
1503
|
+
' color: #7d808a; font-size: 11.5px; padding: 11px 14px; background: #1b1c1f; border-top: 1px solid #2a2b30;',
|
|
1393
1504
|
'}',
|
|
1394
|
-
'.cumulus-settings-credential-note { color: #777; font-size: 11px; padding: 0 10px 12px; }',
|
|
1395
1505
|
'.cumulus-settings-clear-btn {',
|
|
1396
|
-
' background:
|
|
1397
|
-
' padding: 6px
|
|
1506
|
+
' background: transparent; border: 1px solid #3d3f45; color: #9a9da5; cursor: pointer;',
|
|
1507
|
+
' padding: 6px 10px; border-radius: 8px; font-size: 11.5px; font-family: inherit;',
|
|
1508
|
+
' transition: color 0.12s, border-color 0.12s;',
|
|
1398
1509
|
'}',
|
|
1399
|
-
'.cumulus-settings-clear-btn:hover { color: #ef7777; border-color: #
|
|
1400
|
-
'.cumulus-settings-provider-credential label { color: #
|
|
1510
|
+
'.cumulus-settings-clear-btn:hover { color: #ef7777; border-color: #7a4444; }',
|
|
1511
|
+
'.cumulus-settings-provider-credential label { color: #9a9da5; font-size: 11.5px; }',
|
|
1401
1512
|
'.cumulus-settings-add-card {',
|
|
1402
|
-
' border: 1px dashed #
|
|
1513
|
+
' border: 1px dashed #3d3f45; border-radius: 12px; padding: 13px 14px; margin: 12px 0 8px;',
|
|
1403
1514
|
' background: rgba(255,255,255,0.015);',
|
|
1404
1515
|
'}',
|
|
1405
|
-
'.cumulus-settings-add-title { color: #
|
|
1516
|
+
'.cumulus-settings-add-title { color: #d4d6da; font-size: 12px; font-weight: 600; margin-bottom: 9px; }',
|
|
1406
1517
|
'.cumulus-settings-add-row {',
|
|
1407
1518
|
' display: grid; grid-template-columns: 130px minmax(150px,1.4fr) minmax(120px,1fr) 110px auto;',
|
|
1408
|
-
' gap:
|
|
1519
|
+
' gap: 8px; align-items: center;',
|
|
1520
|
+
'}',
|
|
1521
|
+
'.cumulus-settings-select, .cumulus-settings-input {',
|
|
1522
|
+
' background: #17181b; border: 1px solid #33353b; color: #e3e5ea;',
|
|
1523
|
+
' padding: 7px 10px; border-radius: 8px; font-size: 12.5px; font-family: inherit; outline: none;',
|
|
1524
|
+
' transition: border-color 0.12s, box-shadow 0.12s;',
|
|
1409
1525
|
'}',
|
|
1410
1526
|
'.cumulus-settings-select {',
|
|
1411
|
-
'
|
|
1412
|
-
'
|
|
1527
|
+
' -webkit-appearance: none; appearance: none; padding-right: 26px; cursor: pointer;',
|
|
1528
|
+
' background-image: url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2710%27 height=%276%27%3E%3Cpath d=%27M1 1l4 4 4-4%27 stroke=%27%238b8f96%27 stroke-width=%271.5%27 fill=%27none%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27/%3E%3C/svg%3E");',
|
|
1529
|
+
' background-repeat: no-repeat; background-position: right 10px center;',
|
|
1413
1530
|
'}',
|
|
1414
|
-
'.cumulus-settings-input {',
|
|
1415
|
-
'
|
|
1416
|
-
' padding: 7px 9px; border-radius: 6px; font-size: 12px; font-family: inherit; outline: none;',
|
|
1531
|
+
'.cumulus-settings-select:focus, .cumulus-settings-input:focus {',
|
|
1532
|
+
' border-color: #4f8cff; box-shadow: 0 0 0 3px rgba(79,140,255,0.18);',
|
|
1417
1533
|
'}',
|
|
1418
|
-
'.cumulus-settings-input
|
|
1419
|
-
'.cumulus-settings-input::placeholder { color: #5a5a5a; }',
|
|
1534
|
+
'.cumulus-settings-input::placeholder { color: #5c5f66; }',
|
|
1420
1535
|
'.cumulus-settings-cred-row { display: flex; align-items: center; gap: 12px; }',
|
|
1421
|
-
'.cumulus-settings-cred-row label { font-size: 12px; color: #
|
|
1536
|
+
'.cumulus-settings-cred-row label { font-size: 12px; color: #b8bac0; flex-shrink: 0; }',
|
|
1422
1537
|
'.cumulus-settings-btn {',
|
|
1423
|
-
' background: #
|
|
1424
|
-
' padding: 8px
|
|
1538
|
+
' background: #3b82f6; border: none; color: #fff; font-weight: 600;',
|
|
1539
|
+
' padding: 8px 20px; border-radius: 8px; font-size: 13px; cursor: pointer; font-family: inherit;',
|
|
1540
|
+
' transition: background 0.12s;',
|
|
1425
1541
|
'}',
|
|
1426
|
-
'.cumulus-settings-btn:hover { background: #
|
|
1427
|
-
'.cumulus-settings-btn:disabled { background: #
|
|
1542
|
+
'.cumulus-settings-btn:hover { background: #2f6fe0; }',
|
|
1543
|
+
'.cumulus-settings-btn:disabled { background: #2c2d32; color: #6e7278; cursor: not-allowed; }',
|
|
1428
1544
|
'.cumulus-settings-add-btn {',
|
|
1429
|
-
' background:
|
|
1430
|
-
' padding: 7px
|
|
1431
|
-
' font-family: inherit; white-space: nowrap;',
|
|
1545
|
+
' background: transparent; border: 1px solid #3d3f45; color: #d4d6da;',
|
|
1546
|
+
' padding: 7px 14px; border-radius: 8px; font-size: 12.5px; cursor: pointer;',
|
|
1547
|
+
' font-family: inherit; white-space: nowrap; transition: background 0.12s, border-color 0.12s;',
|
|
1432
1548
|
'}',
|
|
1433
|
-
'.cumulus-settings-add-btn:hover { background: #
|
|
1434
|
-
'.cumulus-settings-status { font-size: 12px; color: #
|
|
1549
|
+
'.cumulus-settings-add-btn:hover { background: #2a2b30; border-color: #4a4c53; }',
|
|
1550
|
+
'.cumulus-settings-status { font-size: 12px; color: #9a9da5; }',
|
|
1435
1551
|
'.cumulus-settings-status.success { color: #22c55e; }',
|
|
1436
1552
|
'.cumulus-settings-status.error { color: #ef4444; }',
|
|
1437
1553
|
'@media (max-width: 720px) {',
|
|
1438
1554
|
' .cumulus-settings-backdrop { padding: 8px; align-items: stretch; }',
|
|
1439
|
-
' .cumulus-settings-modal { max-height: 100%; border-radius:
|
|
1555
|
+
' .cumulus-settings-modal { max-height: 100%; border-radius: 12px; }',
|
|
1440
1556
|
' .cumulus-settings-add-row { grid-template-columns: 1fr 1fr; }',
|
|
1441
1557
|
' .cumulus-settings-add-row .cumulus-settings-add-btn { grid-column: 1 / -1; }',
|
|
1442
1558
|
' .cumulus-settings-provider-credential { grid-template-columns: 1fr auto; gap: 5px; }',
|
|
1443
1559
|
' .cumulus-settings-provider-credential label { grid-column: 1 / -1; }',
|
|
1444
|
-
' .cumulus-settings-
|
|
1445
|
-
'
|
|
1446
|
-
' }',
|
|
1447
|
-
' .cumulus-settings-model-row .cumulus-settings-row-input { grid-column: 3 / 4; }',
|
|
1448
|
-
' .cumulus-settings-model-row .cumulus-settings-model-remove { grid-column: 4; grid-row: 1 / 3; }',
|
|
1560
|
+
' .cumulus-settings-default-card { flex-direction: column; align-items: stretch; gap: 6px; }',
|
|
1561
|
+
' .cumulus-settings-edit-grid { grid-template-columns: 1fr 1fr; }',
|
|
1449
1562
|
'}',
|
|
1450
1563
|
|
|
1451
1564
|
/* ── Context menu (right-click on thread) ── */
|
|
@@ -1937,8 +2050,8 @@
|
|
|
1937
2050
|
|
|
1938
2051
|
// ── Blex Integration ─────────────────────────────────────────────────────
|
|
1939
2052
|
|
|
1940
|
-
//
|
|
1941
|
-
|
|
2053
|
+
// Handle lifecycle (element -> BlockHandle[]) lives in blex-render.js — one
|
|
2054
|
+
// owner, so destroy() cannot miss handles the other side created.
|
|
1942
2055
|
|
|
1943
2056
|
// Per-thread store for blex interaction values (poll answers, confirm clicks, etc.)
|
|
1944
2057
|
// Key: "threadName" → Map<"msgTimestamp:blockIdx", interactionValue>
|
|
@@ -2089,136 +2202,99 @@
|
|
|
2089
2202
|
});
|
|
2090
2203
|
}
|
|
2091
2204
|
|
|
2092
|
-
//
|
|
2093
|
-
//
|
|
2205
|
+
// ── Shared renderer seam (task 131) ────────────────────────────────────────
|
|
2206
|
+
//
|
|
2207
|
+
// Extraction, placeholder markup, block rendering and handle lifecycle live in
|
|
2208
|
+
// /blex-render.js — ONE implementation shared with the web-app-agent kit and
|
|
2209
|
+
// any embedding app. chat.html loads it before this file.
|
|
2210
|
+
//
|
|
2211
|
+
// The EMBEDDED widget (a bare <script src="/widget.js"> on a third-party page)
|
|
2212
|
+
// loads neither blex.min.js nor blex-render.js, and never has — blex has only
|
|
2213
|
+
// ever worked on /chat. There, fences degrade to literal text, which is what
|
|
2214
|
+
// an author who typed one would expect to see.
|
|
2215
|
+
function blexCore() {
|
|
2216
|
+
return typeof CumulusBlexRender !== 'undefined' ? CumulusBlexRender : null;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// Fenced-code regex, sourced from the shared module so the blex extractor and
|
|
2220
|
+
// Phase 1 of renderMarkdown cannot disagree about where a code block starts —
|
|
2221
|
+
// a fence falling through that gap is exactly the bug this seam closes.
|
|
2222
|
+
function codeFenceRe() {
|
|
2223
|
+
var core = blexCore();
|
|
2224
|
+
return new RegExp(core ? core.CODE_FENCE_SRC : '```([^\\n`]*)\\n([\\s\\S]*?)```', 'g');
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2094
2227
|
function extractBlexBlocks(text) {
|
|
2095
|
-
var
|
|
2096
|
-
|
|
2097
|
-
var replaced = text.replace(/~~~blex:(\w[\w-]*)\n([\s\S]*?)\n~~~/g, function (_, type, json) {
|
|
2098
|
-
var idx = blocks.length;
|
|
2099
|
-
blocks.push({ type: type, json: json.trim(), idx: idx });
|
|
2100
|
-
return BLEX_PREFIX + idx + '\x00';
|
|
2101
|
-
});
|
|
2102
|
-
return { text: replaced, blocks: blocks };
|
|
2228
|
+
var core = blexCore();
|
|
2229
|
+
return core ? core.extractBlocks(text) : { text: text, blocks: [] };
|
|
2103
2230
|
}
|
|
2104
2231
|
|
|
2105
|
-
//
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
// Get the interaction store for this thread
|
|
2232
|
+
// The widget's adapter over the shared renderer. It supplies all five members;
|
|
2233
|
+
// a render-only surface (the web-app-agent kit's panel) supplies the three
|
|
2234
|
+
// required ones and lets addChip/persist default to no-ops.
|
|
2235
|
+
//
|
|
2236
|
+
// `el` is captured because it may be DETACHED by the time an interaction fires
|
|
2237
|
+
// — renderPanelMessages rebuilds the message list — which is why the panel
|
|
2238
|
+
// lookup falls back to a document query.
|
|
2239
|
+
function widgetBlexAdapter(el, threadName) {
|
|
2114
2240
|
var store = threadName
|
|
2115
2241
|
? blexInteractionStore[threadName] || (blexInteractionStore[threadName] = {})
|
|
2116
2242
|
: null;
|
|
2117
2243
|
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
var blockObj = { type: block.type, id: block.type + '-' + idx, data: data };
|
|
2127
|
-
|
|
2128
|
-
if (isStreaming) {
|
|
2129
|
-
// During streaming, show placeholder skeleton
|
|
2130
|
-
Blex.renderPlaceholder(block.type, container);
|
|
2131
|
-
} else {
|
|
2132
|
-
// Check for previously stored interaction value
|
|
2133
|
-
var renderOpts = {};
|
|
2134
|
-
if (store && storeKey && store[storeKey]) {
|
|
2135
|
-
renderOpts.previousValue = store[storeKey];
|
|
2136
|
-
}
|
|
2244
|
+
function panelFor() {
|
|
2245
|
+
return (
|
|
2246
|
+
el.closest('.cumulus-thread-panel') ||
|
|
2247
|
+
el.closest('.cumulus-panel') ||
|
|
2248
|
+
document.querySelector('.cumulus-thread-panel') ||
|
|
2249
|
+
document.querySelector('.cumulus-panel')
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2137
2252
|
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2253
|
+
return {
|
|
2254
|
+
// The widget is the interactive surface: every type is allowed here.
|
|
2255
|
+
allowType: function () {
|
|
2256
|
+
return true;
|
|
2257
|
+
},
|
|
2258
|
+
getInput: function () {
|
|
2259
|
+
var panel = panelFor();
|
|
2260
|
+
return panel
|
|
2261
|
+
? panel.querySelector('[data-testid="webchat-input"]')
|
|
2262
|
+
: document.querySelector('[data-testid="webchat-input"]');
|
|
2263
|
+
},
|
|
2264
|
+
getSendButton: function () {
|
|
2265
|
+
var panel = panelFor();
|
|
2266
|
+
return panel ? panel.querySelector('[data-testid*="send"]') : null;
|
|
2267
|
+
},
|
|
2268
|
+
addChip: function (interaction, handle, sourceId) {
|
|
2269
|
+
addInteractionChip(panelFor(), interaction, handle, sourceId);
|
|
2270
|
+
},
|
|
2271
|
+
readPersisted: function (storeKey) {
|
|
2272
|
+
if (!store || !storeKey) return undefined;
|
|
2273
|
+
return store[storeKey] || undefined;
|
|
2274
|
+
},
|
|
2275
|
+
persist: function (storeKey, value) {
|
|
2276
|
+
if (!store || !storeKey) return;
|
|
2277
|
+
store[storeKey] = value;
|
|
2278
|
+
_flushBlexStore();
|
|
2279
|
+
},
|
|
2280
|
+
onRendered: function (container, storeKey) {
|
|
2281
|
+
attachBlexResize(container, threadName, storeKey);
|
|
2282
|
+
},
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2157
2285
|
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
document.querySelector('.cumulus-panel');
|
|
2165
|
-
}
|
|
2166
|
-
var inputEl = panel
|
|
2167
|
-
? panel.querySelector('[data-testid="webchat-input"]')
|
|
2168
|
-
: document.querySelector('[data-testid="webchat-input"]');
|
|
2169
|
-
|
|
2170
|
-
if (inputEl && interaction.serialized) {
|
|
2171
|
-
if (interaction.immediate) {
|
|
2172
|
-
// Immediate: prepend any existing input text, then auto-send
|
|
2173
|
-
var existingText = inputEl.value.trim();
|
|
2174
|
-
inputEl.value = existingText
|
|
2175
|
-
? existingText + '\n\n' + interaction.serialized
|
|
2176
|
-
: interaction.serialized;
|
|
2177
|
-
inputEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2178
|
-
// Find and click the send button
|
|
2179
|
-
var sendBtn = panel ? panel.querySelector('[data-testid*="send"]') : null;
|
|
2180
|
-
if (sendBtn) {
|
|
2181
|
-
sendBtn.click();
|
|
2182
|
-
}
|
|
2183
|
-
} else {
|
|
2184
|
-
// Deferred: add to chip tray (sourceId deduplicates rapid-fire from same block)
|
|
2185
|
-
addInteractionChip(panel, interaction, handle, block.type + '-' + idx);
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
});
|
|
2189
|
-
}
|
|
2190
|
-
})
|
|
2191
|
-
.catch(function (err) {
|
|
2192
|
-
container.innerHTML =
|
|
2193
|
-
'<pre style="color:#f66;font-size:0.85em;padding:0.5em;">Blex error: ' +
|
|
2194
|
-
(err.message || err) +
|
|
2195
|
-
'</pre>';
|
|
2196
|
-
});
|
|
2197
|
-
}
|
|
2198
|
-
} catch (e) {
|
|
2199
|
-
container.innerHTML =
|
|
2200
|
-
'<pre style="color:#f66;font-size:0.85em;padding:0.5em;">Invalid blex JSON: ' +
|
|
2201
|
-
e.message +
|
|
2202
|
-
'</pre>';
|
|
2203
|
-
}
|
|
2286
|
+
function renderBlexBlocks(el, blexBlocks, isStreaming, msgKey, threadName) {
|
|
2287
|
+
var core = blexCore();
|
|
2288
|
+
if (!core) return;
|
|
2289
|
+
core.render(el, blexBlocks, widgetBlexAdapter(el, threadName), {
|
|
2290
|
+
msgKey: msgKey,
|
|
2291
|
+
streaming: isStreaming,
|
|
2204
2292
|
});
|
|
2205
|
-
|
|
2206
|
-
// Note: handles are stored inside the async .then() callback above,
|
|
2207
|
-
// not here — by this point the promises haven't resolved yet.
|
|
2208
2293
|
}
|
|
2209
2294
|
|
|
2210
|
-
// Destroy blex block handles for a message element
|
|
2211
2295
|
function destroyBlexBlocks(el) {
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
if (handles) {
|
|
2215
|
-
handles.forEach(function (h) {
|
|
2216
|
-
try {
|
|
2217
|
-
h.destroy();
|
|
2218
|
-
} catch (e) {}
|
|
2219
|
-
});
|
|
2220
|
-
blexHandles.delete(el);
|
|
2221
|
-
}
|
|
2296
|
+
var core = blexCore();
|
|
2297
|
+
if (core) core.destroy(el);
|
|
2222
2298
|
}
|
|
2223
2299
|
|
|
2224
2300
|
// Add a deferred interaction chip to the chip tray
|
|
@@ -2581,7 +2657,12 @@
|
|
|
2581
2657
|
return out;
|
|
2582
2658
|
}
|
|
2583
2659
|
|
|
2584
|
-
|
|
2660
|
+
// opts.streaming — suppress the raw-fence fallback body while a turn is still
|
|
2661
|
+
// streaming. A completed-but-not-yet-rendered fence would otherwise flash its
|
|
2662
|
+
// JSON before flipping to the rendered block at `done`.
|
|
2663
|
+
function renderMarkdown(text, opts) {
|
|
2664
|
+
var streaming = !!(opts && opts.streaming);
|
|
2665
|
+
|
|
2585
2666
|
// Phase 0.5: Extract blex fences BEFORE code blocks
|
|
2586
2667
|
var blexResult = extractBlexBlocks(text);
|
|
2587
2668
|
text = blexResult.text;
|
|
@@ -2590,7 +2671,7 @@
|
|
|
2590
2671
|
// Phase 1: Extract fenced code blocks — replace with unique tokens
|
|
2591
2672
|
var codeBlocks = [];
|
|
2592
2673
|
var TOKEN_PREFIX = '\x00CODEBLOCK_';
|
|
2593
|
-
var processed = text.replace(
|
|
2674
|
+
var processed = text.replace(codeFenceRe(), function (_, lang, code) {
|
|
2594
2675
|
var idx = codeBlocks.length;
|
|
2595
2676
|
var langLabel = lang.trim() || 'text';
|
|
2596
2677
|
var escapedCode = escapeHtml(code.replace(/\n$/, '')); // trim trailing newline
|
|
@@ -2667,10 +2748,14 @@
|
|
|
2667
2748
|
return codeBlocks[parseInt(idx, 10)];
|
|
2668
2749
|
});
|
|
2669
2750
|
|
|
2670
|
-
// Phase 7.5: Reinsert blex block placeholders
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2751
|
+
// Phase 7.5: Reinsert blex block placeholders — shared markup, so the widget
|
|
2752
|
+
// and any embedding app produce byte-identical containers and one renderer
|
|
2753
|
+
// can claim either. See blex-render.js for why the placeholder carries the
|
|
2754
|
+
// raw fence as its body.
|
|
2755
|
+
var core = blexCore();
|
|
2756
|
+
if (core) {
|
|
2757
|
+
html = core.insertPlaceholders(html, blexBlocks, { streaming: streaming });
|
|
2758
|
+
}
|
|
2674
2759
|
|
|
2675
2760
|
return { html: html, blexBlocks: blexBlocks };
|
|
2676
2761
|
}
|
|
@@ -3660,7 +3745,7 @@
|
|
|
3660
3745
|
el.className = 'cumulus-msg assistant';
|
|
3661
3746
|
if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
|
|
3662
3747
|
if (content) {
|
|
3663
|
-
var mdResult = renderMarkdown(content);
|
|
3748
|
+
var mdResult = renderMarkdown(content, { streaming: isStreaming });
|
|
3664
3749
|
el.innerHTML = mdResult.html;
|
|
3665
3750
|
if (isStreaming) {
|
|
3666
3751
|
el.innerHTML += '<span class="cumulus-cursor"></span>';
|
|
@@ -4500,6 +4585,7 @@
|
|
|
4500
4585
|
'<button class="cumulus-topbar-check-update" data-testid="webchat-check-update" title="Check for update">↻</button>' +
|
|
4501
4586
|
'<span class="cumulus-topbar-check-result" data-testid="webchat-check-result" style="display:none"></span></span>' +
|
|
4502
4587
|
'<div class="cumulus-topbar-right">' +
|
|
4588
|
+
'<button class="cumulus-topbar-settings" data-testid="webchat-settings-btn" title="Gateway settings — models, providers, API keys">⚙</button>' +
|
|
4503
4589
|
'<span class="cumulus-header-status">' +
|
|
4504
4590
|
'<span class="cumulus-status-dot" data-testid="webchat-status-dot-app"></span>' +
|
|
4505
4591
|
'<span data-testid="webchat-status-text-app">Disconnected</span>' +
|
|
@@ -4689,6 +4775,14 @@
|
|
|
4689
4775
|
});
|
|
4690
4776
|
}
|
|
4691
4777
|
|
|
4778
|
+
// ── Topbar settings gear → gateway settings modal (task 128) ──
|
|
4779
|
+
var topbarSettingsBtn = topbar.querySelector('[data-testid="webchat-settings-btn"]');
|
|
4780
|
+
if (topbarSettingsBtn) {
|
|
4781
|
+
topbarSettingsBtn.addEventListener('click', function () {
|
|
4782
|
+
openSettingsModal();
|
|
4783
|
+
});
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4692
4786
|
// ── Click status indicator to force reconnect ──
|
|
4693
4787
|
var appStatusEl = topbar.querySelector('.cumulus-header-status');
|
|
4694
4788
|
if (appStatusEl) {
|
|
@@ -5846,6 +5940,715 @@
|
|
|
5846
5940
|
};
|
|
5847
5941
|
}
|
|
5848
5942
|
|
|
5943
|
+
// ── Settings modal (provider-organized model catalog + credentials) ──
|
|
5944
|
+
// Live-editable via PUT /api/config (task 104) — takes effect on the NEXT turn
|
|
5945
|
+
// with no restart, no JSON edit, no republish. Any valid gateway key.
|
|
5946
|
+
// Gateway-level config, opened from the topbar gear next to the connection
|
|
5947
|
+
// status — not tied to any thread panel (task 128).
|
|
5948
|
+
function openSettingsModal() {
|
|
5949
|
+
var backdrop = document.createElement('div');
|
|
5950
|
+
backdrop.className = 'cumulus-settings-backdrop';
|
|
5951
|
+
backdrop.setAttribute('data-testid', 'webchat-settings-modal');
|
|
5952
|
+
var modal = document.createElement('div');
|
|
5953
|
+
modal.className = 'cumulus-settings-modal';
|
|
5954
|
+
|
|
5955
|
+
var mHeader = document.createElement('div');
|
|
5956
|
+
mHeader.className = 'cumulus-settings-header';
|
|
5957
|
+
var mHeaderText = document.createElement('div');
|
|
5958
|
+
var mTitle = document.createElement('div');
|
|
5959
|
+
mTitle.className = 'cumulus-settings-title';
|
|
5960
|
+
mTitle.textContent = 'Gateway settings';
|
|
5961
|
+
var mSub = document.createElement('div');
|
|
5962
|
+
mSub.className = 'cumulus-settings-subtitle';
|
|
5963
|
+
mSub.textContent =
|
|
5964
|
+
'Manage Anthropic, OpenAI, and HuggingFace models — changes apply to new turns instantly';
|
|
5965
|
+
mHeaderText.appendChild(mTitle);
|
|
5966
|
+
mHeaderText.appendChild(mSub);
|
|
5967
|
+
var mClose = document.createElement('button');
|
|
5968
|
+
mClose.className = 'cumulus-overlay-close';
|
|
5969
|
+
mClose.textContent = '\xD7';
|
|
5970
|
+
mClose.setAttribute('data-testid', 'webchat-settings-close');
|
|
5971
|
+
mClose.addEventListener('click', function () {
|
|
5972
|
+
backdrop.remove();
|
|
5973
|
+
});
|
|
5974
|
+
mHeader.appendChild(mHeaderText);
|
|
5975
|
+
mHeader.appendChild(mClose);
|
|
5976
|
+
modal.appendChild(mHeader);
|
|
5977
|
+
|
|
5978
|
+
var body = document.createElement('div');
|
|
5979
|
+
body.className = 'cumulus-settings-body';
|
|
5980
|
+
|
|
5981
|
+
// Task 130 (Option C): ONE gateway-default control, up top — answers "what
|
|
5982
|
+
// will a new thread use?" before the catalog. The per-row radios are gone.
|
|
5983
|
+
var defaultLabelSection = document.createElement('div');
|
|
5984
|
+
defaultLabelSection.className = 'cumulus-settings-section-label';
|
|
5985
|
+
defaultLabelSection.textContent = 'Default model';
|
|
5986
|
+
body.appendChild(defaultLabelSection);
|
|
5987
|
+
var defaultCard = document.createElement('div');
|
|
5988
|
+
defaultCard.className = 'cumulus-settings-default-card';
|
|
5989
|
+
var defaultCardLabel = document.createElement('label');
|
|
5990
|
+
defaultCardLabel.textContent = 'New threads use';
|
|
5991
|
+
var defaultSelect = document.createElement('select');
|
|
5992
|
+
defaultSelect.className = 'cumulus-settings-select';
|
|
5993
|
+
defaultSelect.setAttribute('data-testid', 'webchat-gw-default-select');
|
|
5994
|
+
defaultSelect.addEventListener('change', function () {
|
|
5995
|
+
applyGatewayDefault(gwState.catalog, defaultSelect.value);
|
|
5996
|
+
renderAll();
|
|
5997
|
+
});
|
|
5998
|
+
defaultCard.appendChild(defaultCardLabel);
|
|
5999
|
+
defaultCard.appendChild(defaultSelect);
|
|
6000
|
+
body.appendChild(defaultCard);
|
|
6001
|
+
|
|
6002
|
+
var modelsLabel = document.createElement('div');
|
|
6003
|
+
modelsLabel.className = 'cumulus-settings-section-label';
|
|
6004
|
+
modelsLabel.textContent = 'Model catalog';
|
|
6005
|
+
body.appendChild(modelsLabel);
|
|
6006
|
+
|
|
6007
|
+
var gwList = document.createElement('div');
|
|
6008
|
+
gwList.setAttribute('data-testid', 'webchat-gateway-models');
|
|
6009
|
+
body.appendChild(gwList);
|
|
6010
|
+
|
|
6011
|
+
var addCard = document.createElement('div');
|
|
6012
|
+
addCard.className = 'cumulus-settings-add-card';
|
|
6013
|
+
var addTitle = document.createElement('div');
|
|
6014
|
+
addTitle.className = 'cumulus-settings-add-title';
|
|
6015
|
+
addTitle.textContent = 'Add a model';
|
|
6016
|
+
addCard.appendChild(addTitle);
|
|
6017
|
+
var addWrap = document.createElement('div');
|
|
6018
|
+
addWrap.className = 'cumulus-settings-add-row';
|
|
6019
|
+
var addProvider = document.createElement('select');
|
|
6020
|
+
addProvider.className = 'cumulus-settings-select';
|
|
6021
|
+
addProvider.setAttribute('data-testid', 'webchat-gw-add-provider');
|
|
6022
|
+
[
|
|
6023
|
+
{ value: 'anthropic', label: 'Anthropic' },
|
|
6024
|
+
{ value: 'openai', label: 'OpenAI' },
|
|
6025
|
+
{ value: 'huggingface', label: 'HuggingFace' },
|
|
6026
|
+
].forEach(function (provider) {
|
|
6027
|
+
var option = document.createElement('option');
|
|
6028
|
+
option.value = provider.value;
|
|
6029
|
+
option.textContent = provider.label;
|
|
6030
|
+
addProvider.appendChild(option);
|
|
6031
|
+
});
|
|
6032
|
+
var addId = document.createElement('input');
|
|
6033
|
+
addId.type = 'text';
|
|
6034
|
+
addId.className = 'cumulus-settings-input';
|
|
6035
|
+
addId.placeholder = 'Model ID / name';
|
|
6036
|
+
addId.setAttribute('data-testid', 'webchat-gw-add-id');
|
|
6037
|
+
var addLabel = document.createElement('input');
|
|
6038
|
+
addLabel.type = 'text';
|
|
6039
|
+
addLabel.className = 'cumulus-settings-input';
|
|
6040
|
+
addLabel.placeholder = 'Display label (optional)';
|
|
6041
|
+
addLabel.setAttribute('data-testid', 'webchat-gw-add-label');
|
|
6042
|
+
var addContext = document.createElement('input');
|
|
6043
|
+
addContext.type = 'number';
|
|
6044
|
+
addContext.min = '1';
|
|
6045
|
+
addContext.step = '1';
|
|
6046
|
+
addContext.className = 'cumulus-settings-input';
|
|
6047
|
+
addContext.placeholder = 'Context tokens';
|
|
6048
|
+
addContext.setAttribute('data-testid', 'webchat-gw-add-context');
|
|
6049
|
+
var addBtn = document.createElement('button');
|
|
6050
|
+
addBtn.type = 'button';
|
|
6051
|
+
addBtn.textContent = '+ Add';
|
|
6052
|
+
addBtn.className = 'cumulus-settings-add-btn';
|
|
6053
|
+
addBtn.setAttribute('data-testid', 'webchat-gw-add-btn');
|
|
6054
|
+
addWrap.appendChild(addProvider);
|
|
6055
|
+
addWrap.appendChild(addId);
|
|
6056
|
+
addWrap.appendChild(addLabel);
|
|
6057
|
+
addWrap.appendChild(addContext);
|
|
6058
|
+
addWrap.appendChild(addBtn);
|
|
6059
|
+
addCard.appendChild(addWrap);
|
|
6060
|
+
body.appendChild(addCard);
|
|
6061
|
+
|
|
6062
|
+
// ── License (task 129) — persist-only: the key lands in gateway.config.json
|
|
6063
|
+
// but enforcement re-verifies at startup, so it takes effect on reload (127).
|
|
6064
|
+
var licenseLabel = document.createElement('div');
|
|
6065
|
+
licenseLabel.className = 'cumulus-settings-section-label';
|
|
6066
|
+
licenseLabel.textContent = 'License';
|
|
6067
|
+
body.appendChild(licenseLabel);
|
|
6068
|
+
var licenseCard = document.createElement('div');
|
|
6069
|
+
licenseCard.className = 'cumulus-settings-add-card';
|
|
6070
|
+
var licenseStatusLine = document.createElement('div');
|
|
6071
|
+
licenseStatusLine.className = 'cumulus-settings-add-title';
|
|
6072
|
+
licenseStatusLine.setAttribute('data-testid', 'webchat-gw-license-status');
|
|
6073
|
+
licenseStatusLine.textContent = 'License status unknown';
|
|
6074
|
+
licenseCard.appendChild(licenseStatusLine);
|
|
6075
|
+
var licensePurpose = document.createElement('div');
|
|
6076
|
+
licensePurpose.className = 'cumulus-settings-subtitle';
|
|
6077
|
+
licensePurpose.setAttribute('data-testid', 'webchat-gw-license-purpose');
|
|
6078
|
+
licensePurpose.textContent =
|
|
6079
|
+
'Required to run web-app agents in production. Unlicensed, each thread ' +
|
|
6080
|
+
'namespace is capped at 5 visitor threads (demo mode); your own threads, ' +
|
|
6081
|
+
'the CLI and the TUI are never limited. Keys are verified offline and ' +
|
|
6082
|
+
'cover one whole-number release. Contact ops@luckydrawdesign.com.';
|
|
6083
|
+
licenseCard.appendChild(licensePurpose);
|
|
6084
|
+
var licenseRow = document.createElement('div');
|
|
6085
|
+
licenseRow.className = 'cumulus-settings-add-row';
|
|
6086
|
+
var licenseInput = document.createElement('input');
|
|
6087
|
+
licenseInput.type = 'text';
|
|
6088
|
+
licenseInput.className = 'cumulus-settings-input';
|
|
6089
|
+
licenseInput.placeholder = 'License key (cumulus-lic-v1.…)';
|
|
6090
|
+
licenseInput.setAttribute('data-testid', 'webchat-gw-license-input');
|
|
6091
|
+
var licenseClearBtn = document.createElement('button');
|
|
6092
|
+
licenseClearBtn.type = 'button';
|
|
6093
|
+
licenseClearBtn.textContent = 'Clear';
|
|
6094
|
+
licenseClearBtn.className = 'cumulus-settings-add-btn';
|
|
6095
|
+
licenseClearBtn.setAttribute('data-testid', 'webchat-gw-license-clear');
|
|
6096
|
+
licenseRow.appendChild(licenseInput);
|
|
6097
|
+
licenseRow.appendChild(licenseClearBtn);
|
|
6098
|
+
licenseCard.appendChild(licenseRow);
|
|
6099
|
+
var licenseNote = document.createElement('div');
|
|
6100
|
+
licenseNote.className = 'cumulus-settings-subtitle';
|
|
6101
|
+
licenseNote.textContent = 'Takes effect on gateway reload';
|
|
6102
|
+
licenseCard.appendChild(licenseNote);
|
|
6103
|
+
body.appendChild(licenseCard);
|
|
6104
|
+
modal.appendChild(body);
|
|
6105
|
+
|
|
6106
|
+
var footer = document.createElement('div');
|
|
6107
|
+
footer.className = 'cumulus-settings-footer';
|
|
6108
|
+
var saveBtn = document.createElement('button');
|
|
6109
|
+
saveBtn.textContent = 'Save';
|
|
6110
|
+
saveBtn.className = 'cumulus-settings-btn';
|
|
6111
|
+
saveBtn.setAttribute('data-testid', 'webchat-gw-save');
|
|
6112
|
+
var gwStatus = document.createElement('span');
|
|
6113
|
+
gwStatus.className = 'cumulus-settings-status';
|
|
6114
|
+
footer.appendChild(saveBtn);
|
|
6115
|
+
footer.appendChild(gwStatus);
|
|
6116
|
+
modal.appendChild(footer);
|
|
6117
|
+
|
|
6118
|
+
backdrop.appendChild(modal);
|
|
6119
|
+
backdrop.addEventListener('click', function (e) {
|
|
6120
|
+
if (e.target === backdrop) backdrop.remove();
|
|
6121
|
+
});
|
|
6122
|
+
document.body.appendChild(backdrop);
|
|
6123
|
+
|
|
6124
|
+
var gwState = { catalog: [], sentinel: null };
|
|
6125
|
+
var credentialInputs = {};
|
|
6126
|
+
var credentialValues = { openai: '', huggingface: '', license: '' };
|
|
6127
|
+
var credentialClears = { openai: false, huggingface: false, license: false };
|
|
6128
|
+
function renderLicenseState(masked, info) {
|
|
6129
|
+
licenseInput.value = credentialValues.license || '';
|
|
6130
|
+
licenseInput.placeholder = credentialClears.license
|
|
6131
|
+
? 'Will be cleared on Save'
|
|
6132
|
+
: masked || 'License key (cumulus-lic-v1.…)';
|
|
6133
|
+
if (!info) {
|
|
6134
|
+
licenseStatusLine.textContent = masked
|
|
6135
|
+
? 'License key configured'
|
|
6136
|
+
: 'No license key — demo mode';
|
|
6137
|
+
return;
|
|
6138
|
+
}
|
|
6139
|
+
var text = info.licensed
|
|
6140
|
+
? 'Licensed to ' + (info.licensee || 'unknown')
|
|
6141
|
+
: 'Demo mode' + (info.reason && info.reason !== 'absent' ? ' (' + info.reason + ')' : '');
|
|
6142
|
+
if (info.pendingReload) text += ' · key saved — takes effect on reload';
|
|
6143
|
+
licenseStatusLine.textContent = text;
|
|
6144
|
+
}
|
|
6145
|
+
licenseInput.addEventListener('input', function () {
|
|
6146
|
+
credentialValues.license = licenseInput.value;
|
|
6147
|
+
credentialClears.license = false;
|
|
6148
|
+
});
|
|
6149
|
+
licenseClearBtn.addEventListener('click', function () {
|
|
6150
|
+
credentialClears.license = true;
|
|
6151
|
+
credentialValues.license = '';
|
|
6152
|
+
renderLicenseState('', gwLicenseInfo);
|
|
6153
|
+
});
|
|
6154
|
+
var gwLicenseMasked = '';
|
|
6155
|
+
var gwLicenseInfo = null;
|
|
6156
|
+
var providerDefs = [
|
|
6157
|
+
{
|
|
6158
|
+
id: 'anthropic',
|
|
6159
|
+
title: 'Anthropic',
|
|
6160
|
+
description: 'Claude CLI models · authentication is managed by the Claude CLI',
|
|
6161
|
+
},
|
|
6162
|
+
{
|
|
6163
|
+
id: 'openai',
|
|
6164
|
+
title: 'OpenAI',
|
|
6165
|
+
description: 'Responses API models with direct tool use',
|
|
6166
|
+
credential: 'openaiApiKey',
|
|
6167
|
+
credentialTestId: 'webchat-gw-openaikey',
|
|
6168
|
+
},
|
|
6169
|
+
{
|
|
6170
|
+
id: 'huggingface',
|
|
6171
|
+
title: 'HuggingFace',
|
|
6172
|
+
description: 'OpenAI-compatible models served through the HuggingFace router',
|
|
6173
|
+
credential: 'hfApiKey',
|
|
6174
|
+
credentialTestId: 'webchat-gw-hfkey',
|
|
6175
|
+
},
|
|
6176
|
+
];
|
|
6177
|
+
|
|
6178
|
+
function setSettingsStatus(message, kind) {
|
|
6179
|
+
gwStatus.textContent = message || '';
|
|
6180
|
+
gwStatus.className = 'cumulus-settings-status' + (kind ? ' ' + kind : '');
|
|
6181
|
+
}
|
|
6182
|
+
|
|
6183
|
+
function formatContextTokens(n) {
|
|
6184
|
+
return n >= 1000 ? Math.round(n / 1000) + 'k' : String(n);
|
|
6185
|
+
}
|
|
6186
|
+
|
|
6187
|
+
function makeEditField(labelText, input) {
|
|
6188
|
+
var field = document.createElement('div');
|
|
6189
|
+
field.className = 'cumulus-settings-edit-field';
|
|
6190
|
+
var caption = document.createElement('span');
|
|
6191
|
+
caption.textContent = labelText;
|
|
6192
|
+
field.appendChild(caption);
|
|
6193
|
+
field.appendChild(input);
|
|
6194
|
+
return field;
|
|
6195
|
+
}
|
|
6196
|
+
|
|
6197
|
+
// Task 130: rows are read-only text at rest (label + mono id + ctx chip +
|
|
6198
|
+
// default badge); ✎ flips one row into labeled inputs. `model.editing` is
|
|
6199
|
+
// UI-only state — serialization rebuilds entries from `model.source` +
|
|
6200
|
+
// explicit fields, so it never reaches the PUT payload.
|
|
6201
|
+
function renderModelRow(model, providerDef, index) {
|
|
6202
|
+
var row = document.createElement('div');
|
|
6203
|
+
row.className =
|
|
6204
|
+
'cumulus-settings-model-row ' +
|
|
6205
|
+
model.provider +
|
|
6206
|
+
(model.gatewayDefault ? ' is-default' : '') +
|
|
6207
|
+
(model.editing ? ' editing' : '');
|
|
6208
|
+
row.setAttribute('data-gw-model', model.id);
|
|
6209
|
+
row.setAttribute('data-testid', 'webchat-gw-row-' + model.provider + '-' + index);
|
|
6210
|
+
|
|
6211
|
+
if (model.editing) {
|
|
6212
|
+
var grid = document.createElement('div');
|
|
6213
|
+
grid.className = 'cumulus-settings-edit-grid';
|
|
6214
|
+
|
|
6215
|
+
var idInput = document.createElement('input');
|
|
6216
|
+
idInput.type = 'text';
|
|
6217
|
+
idInput.className = 'cumulus-settings-input cumulus-settings-row-input';
|
|
6218
|
+
idInput.value = model.id;
|
|
6219
|
+
idInput.setAttribute('aria-label', providerDef.title + ' model ID');
|
|
6220
|
+
idInput.setAttribute('data-testid', 'webchat-gw-id-' + model.provider + '-' + index);
|
|
6221
|
+
idInput.addEventListener('input', function () {
|
|
6222
|
+
model.id = idInput.value;
|
|
6223
|
+
});
|
|
6224
|
+
grid.appendChild(makeEditField('Model ID', idInput));
|
|
6225
|
+
|
|
6226
|
+
var labelInput = document.createElement('input');
|
|
6227
|
+
labelInput.type = 'text';
|
|
6228
|
+
labelInput.className = 'cumulus-settings-input cumulus-settings-row-input';
|
|
6229
|
+
labelInput.value = model.label || model.id;
|
|
6230
|
+
labelInput.setAttribute('aria-label', providerDef.title + ' display label');
|
|
6231
|
+
labelInput.setAttribute(
|
|
6232
|
+
'data-testid',
|
|
6233
|
+
'webchat-gw-label-' + model.provider + '-' + index
|
|
6234
|
+
);
|
|
6235
|
+
labelInput.addEventListener('input', function () {
|
|
6236
|
+
model.label = labelInput.value;
|
|
6237
|
+
});
|
|
6238
|
+
grid.appendChild(makeEditField('Display label', labelInput));
|
|
6239
|
+
|
|
6240
|
+
if (model.provider !== 'anthropic') {
|
|
6241
|
+
var contextInput = document.createElement('input');
|
|
6242
|
+
contextInput.type = 'number';
|
|
6243
|
+
contextInput.min = '1';
|
|
6244
|
+
contextInput.step = '1';
|
|
6245
|
+
contextInput.className = 'cumulus-settings-input cumulus-settings-row-input';
|
|
6246
|
+
contextInput.placeholder = '(default)';
|
|
6247
|
+
contextInput.value = model.contextWindow || '';
|
|
6248
|
+
contextInput.setAttribute('aria-label', providerDef.title + ' context window');
|
|
6249
|
+
contextInput.setAttribute(
|
|
6250
|
+
'data-testid',
|
|
6251
|
+
'webchat-gw-context-' + model.provider + '-' + index
|
|
6252
|
+
);
|
|
6253
|
+
contextInput.addEventListener('input', function () {
|
|
6254
|
+
model.contextWindow = contextInput.value ? Number(contextInput.value) : undefined;
|
|
6255
|
+
});
|
|
6256
|
+
grid.appendChild(makeEditField('Context tokens', contextInput));
|
|
6257
|
+
}
|
|
6258
|
+
|
|
6259
|
+
var doneBtn = document.createElement('button');
|
|
6260
|
+
doneBtn.type = 'button';
|
|
6261
|
+
doneBtn.textContent = 'Done';
|
|
6262
|
+
doneBtn.className = 'cumulus-settings-add-btn';
|
|
6263
|
+
doneBtn.setAttribute('data-testid', 'webchat-gw-done-' + model.provider + '-' + index);
|
|
6264
|
+
doneBtn.addEventListener('click', function () {
|
|
6265
|
+
model.editing = false;
|
|
6266
|
+
renderAll();
|
|
6267
|
+
});
|
|
6268
|
+
grid.appendChild(doneBtn);
|
|
6269
|
+
row.appendChild(grid);
|
|
6270
|
+
return row;
|
|
6271
|
+
}
|
|
6272
|
+
|
|
6273
|
+
var name = document.createElement('div');
|
|
6274
|
+
name.className = 'cumulus-settings-model-name';
|
|
6275
|
+
var labelEl = document.createElement('div');
|
|
6276
|
+
labelEl.className = 'cumulus-settings-model-label';
|
|
6277
|
+
labelEl.textContent = model.label || model.id;
|
|
6278
|
+
var idEl = document.createElement('div');
|
|
6279
|
+
idEl.className = 'cumulus-settings-model-id';
|
|
6280
|
+
idEl.textContent = model.id;
|
|
6281
|
+
name.appendChild(labelEl);
|
|
6282
|
+
name.appendChild(idEl);
|
|
6283
|
+
row.appendChild(name);
|
|
6284
|
+
|
|
6285
|
+
if (model.provider !== 'anthropic' && model.contextWindow) {
|
|
6286
|
+
var chip = document.createElement('span');
|
|
6287
|
+
chip.className = 'cumulus-settings-context-chip';
|
|
6288
|
+
chip.textContent = 'ctx ' + formatContextTokens(model.contextWindow);
|
|
6289
|
+
row.appendChild(chip);
|
|
6290
|
+
}
|
|
6291
|
+
|
|
6292
|
+
var badge = document.createElement('span');
|
|
6293
|
+
badge.className = 'cumulus-settings-default-badge';
|
|
6294
|
+
badge.textContent = 'default';
|
|
6295
|
+
row.appendChild(badge);
|
|
6296
|
+
|
|
6297
|
+
var actions = document.createElement('div');
|
|
6298
|
+
actions.className = 'cumulus-settings-row-actions';
|
|
6299
|
+
|
|
6300
|
+
var edit = document.createElement('button');
|
|
6301
|
+
edit.type = 'button';
|
|
6302
|
+
edit.className = 'cumulus-settings-row-edit';
|
|
6303
|
+
edit.textContent = '✎';
|
|
6304
|
+
edit.title = 'Edit ' + model.id;
|
|
6305
|
+
edit.setAttribute('data-testid', 'webchat-gw-edit-' + model.provider + '-' + index);
|
|
6306
|
+
edit.addEventListener('click', function () {
|
|
6307
|
+
model.editing = true;
|
|
6308
|
+
renderAll();
|
|
6309
|
+
});
|
|
6310
|
+
actions.appendChild(edit);
|
|
6311
|
+
|
|
6312
|
+
var remove = document.createElement('button');
|
|
6313
|
+
remove.type = 'button';
|
|
6314
|
+
remove.className = 'cumulus-settings-model-remove';
|
|
6315
|
+
remove.textContent = '\xD7';
|
|
6316
|
+
remove.title = 'Remove ' + model.id;
|
|
6317
|
+
remove.setAttribute('data-testid', 'webchat-gw-remove-' + model.provider + '-' + index);
|
|
6318
|
+
remove.addEventListener('click', function () {
|
|
6319
|
+
var absoluteIndex = gwState.catalog.indexOf(model);
|
|
6320
|
+
var wasGatewayDefault = model.gatewayDefault;
|
|
6321
|
+
var wasProviderDefault = model.providerDefault;
|
|
6322
|
+
if (absoluteIndex >= 0) gwState.catalog.splice(absoluteIndex, 1);
|
|
6323
|
+
if (wasGatewayDefault && gwState.catalog.length) {
|
|
6324
|
+
applyGatewayDefault(gwState.catalog, gwState.catalog[0].id);
|
|
6325
|
+
}
|
|
6326
|
+
if (
|
|
6327
|
+
wasProviderDefault &&
|
|
6328
|
+
!gwState.catalog.some(function (item) {
|
|
6329
|
+
return item.provider === 'anthropic' && item.providerDefault;
|
|
6330
|
+
})
|
|
6331
|
+
) {
|
|
6332
|
+
var nextClaude = gwState.catalog.find(function (item) {
|
|
6333
|
+
return item.provider === 'anthropic';
|
|
6334
|
+
});
|
|
6335
|
+
if (nextClaude) nextClaude.providerDefault = true;
|
|
6336
|
+
}
|
|
6337
|
+
renderAll();
|
|
6338
|
+
});
|
|
6339
|
+
actions.appendChild(remove);
|
|
6340
|
+
row.appendChild(actions);
|
|
6341
|
+
return row;
|
|
6342
|
+
}
|
|
6343
|
+
|
|
6344
|
+
function renderCredential(card, provider) {
|
|
6345
|
+
if (!provider.credential) {
|
|
6346
|
+
var note = document.createElement('div');
|
|
6347
|
+
note.className = 'cumulus-settings-credential-note';
|
|
6348
|
+
note.textContent = 'Authenticate with `claude login` on the gateway host.';
|
|
6349
|
+
card.appendChild(note);
|
|
6350
|
+
return;
|
|
6351
|
+
}
|
|
6352
|
+
var credentialRow = document.createElement('div');
|
|
6353
|
+
credentialRow.className = 'cumulus-settings-provider-credential';
|
|
6354
|
+
var credentialLabel = document.createElement('label');
|
|
6355
|
+
credentialLabel.textContent = 'API key';
|
|
6356
|
+
var credentialInput = document.createElement('input');
|
|
6357
|
+
credentialInput.type = 'password';
|
|
6358
|
+
credentialInput.autocomplete = 'off';
|
|
6359
|
+
credentialInput.className = 'cumulus-settings-input';
|
|
6360
|
+
credentialInput.placeholder = credentialClears[provider.id]
|
|
6361
|
+
? 'Will be cleared on save'
|
|
6362
|
+
: '(unset)';
|
|
6363
|
+
credentialInput.value = credentialValues[provider.id] || '';
|
|
6364
|
+
credentialInput.setAttribute('data-testid', provider.credentialTestId);
|
|
6365
|
+
credentialInput.addEventListener('input', function () {
|
|
6366
|
+
credentialValues[provider.id] = credentialInput.value;
|
|
6367
|
+
credentialClears[provider.id] = false;
|
|
6368
|
+
credentialInput.placeholder = '(unset)';
|
|
6369
|
+
});
|
|
6370
|
+
credentialInputs[provider.id] = credentialInput;
|
|
6371
|
+
var clear = document.createElement('button');
|
|
6372
|
+
clear.type = 'button';
|
|
6373
|
+
clear.className = 'cumulus-settings-clear-btn';
|
|
6374
|
+
clear.textContent = 'Clear';
|
|
6375
|
+
clear.setAttribute('data-testid', 'webchat-gw-clear-' + provider.id + '-key');
|
|
6376
|
+
clear.addEventListener('click', function () {
|
|
6377
|
+
credentialClears[provider.id] = true;
|
|
6378
|
+
credentialValues[provider.id] = '';
|
|
6379
|
+
credentialInput.value = '';
|
|
6380
|
+
credentialInput.placeholder = 'Will be cleared on save';
|
|
6381
|
+
setSettingsStatus('Credential marked for removal', '');
|
|
6382
|
+
});
|
|
6383
|
+
credentialRow.appendChild(credentialLabel);
|
|
6384
|
+
credentialRow.appendChild(credentialInput);
|
|
6385
|
+
credentialRow.appendChild(clear);
|
|
6386
|
+
card.appendChild(credentialRow);
|
|
6387
|
+
}
|
|
6388
|
+
|
|
6389
|
+
function renderGwModels() {
|
|
6390
|
+
gwList.innerHTML = '';
|
|
6391
|
+
credentialInputs = {};
|
|
6392
|
+
providerDefs.forEach(function (provider) {
|
|
6393
|
+
var providerModels = gwState.catalog.filter(function (model) {
|
|
6394
|
+
return model.provider === provider.id;
|
|
6395
|
+
});
|
|
6396
|
+
var card = document.createElement('section');
|
|
6397
|
+
card.className = 'cumulus-settings-provider-card';
|
|
6398
|
+
card.setAttribute('data-testid', 'webchat-gw-provider-' + provider.id);
|
|
6399
|
+
var head = document.createElement('div');
|
|
6400
|
+
head.className = 'cumulus-settings-provider-head';
|
|
6401
|
+
var heading = document.createElement('div');
|
|
6402
|
+
var title = document.createElement('div');
|
|
6403
|
+
title.className = 'cumulus-settings-provider-title';
|
|
6404
|
+
title.textContent = provider.title;
|
|
6405
|
+
var description = document.createElement('div');
|
|
6406
|
+
description.className = 'cumulus-settings-provider-desc';
|
|
6407
|
+
description.textContent = provider.description;
|
|
6408
|
+
heading.appendChild(title);
|
|
6409
|
+
heading.appendChild(description);
|
|
6410
|
+
var count = document.createElement('span');
|
|
6411
|
+
count.className = 'cumulus-settings-provider-count';
|
|
6412
|
+
count.textContent =
|
|
6413
|
+
providerModels.length + (providerModels.length === 1 ? ' model' : ' models');
|
|
6414
|
+
head.appendChild(heading);
|
|
6415
|
+
head.appendChild(count);
|
|
6416
|
+
card.appendChild(head);
|
|
6417
|
+
var list = document.createElement('div');
|
|
6418
|
+
list.className = 'cumulus-settings-provider-models';
|
|
6419
|
+
if (!providerModels.length) {
|
|
6420
|
+
var empty = document.createElement('div');
|
|
6421
|
+
empty.className = 'cumulus-settings-provider-empty';
|
|
6422
|
+
empty.textContent = 'No ' + provider.title + ' models configured.';
|
|
6423
|
+
list.appendChild(empty);
|
|
6424
|
+
} else {
|
|
6425
|
+
providerModels.forEach(function (model, index) {
|
|
6426
|
+
list.appendChild(renderModelRow(model, provider, index));
|
|
6427
|
+
});
|
|
6428
|
+
}
|
|
6429
|
+
card.appendChild(list);
|
|
6430
|
+
// The Claude sub-model default only surfaces when it can actually
|
|
6431
|
+
// diverge from the gateway default (task 130) — gateway default on a
|
|
6432
|
+
// direct provider, ≥2 Anthropic models. Otherwise coupling makes it
|
|
6433
|
+
// redundant and it stays out of the way.
|
|
6434
|
+
if (provider.id === 'anthropic' && claudeSubDefaultVisible(gwState.catalog)) {
|
|
6435
|
+
var subRow = document.createElement('div');
|
|
6436
|
+
subRow.className = 'cumulus-settings-claude-default-row';
|
|
6437
|
+
var subLabel = document.createElement('label');
|
|
6438
|
+
subLabel.textContent = 'When a thread picks Claude, use';
|
|
6439
|
+
var subSelect = document.createElement('select');
|
|
6440
|
+
subSelect.className = 'cumulus-settings-select';
|
|
6441
|
+
subSelect.setAttribute('data-testid', 'webchat-gw-claude-default-select');
|
|
6442
|
+
var subDefault = '';
|
|
6443
|
+
providerModels.forEach(function (model) {
|
|
6444
|
+
var opt = document.createElement('option');
|
|
6445
|
+
opt.value = model.id;
|
|
6446
|
+
opt.textContent = model.label || model.id;
|
|
6447
|
+
subSelect.appendChild(opt);
|
|
6448
|
+
if (model.providerDefault) subDefault = model.id;
|
|
6449
|
+
});
|
|
6450
|
+
subSelect.value = subDefault || (providerModels[0] && providerModels[0].id) || '';
|
|
6451
|
+
subSelect.addEventListener('change', function () {
|
|
6452
|
+
applyClaudeSubDefault(gwState.catalog, subSelect.value);
|
|
6453
|
+
});
|
|
6454
|
+
subRow.appendChild(subLabel);
|
|
6455
|
+
subRow.appendChild(subSelect);
|
|
6456
|
+
card.appendChild(subRow);
|
|
6457
|
+
}
|
|
6458
|
+
renderCredential(card, provider);
|
|
6459
|
+
gwList.appendChild(card);
|
|
6460
|
+
});
|
|
6461
|
+
}
|
|
6462
|
+
|
|
6463
|
+
function renderDefaultSelect() {
|
|
6464
|
+
defaultSelect.innerHTML = '';
|
|
6465
|
+
if (!gwState.catalog.length) {
|
|
6466
|
+
var none = document.createElement('option');
|
|
6467
|
+
none.value = '';
|
|
6468
|
+
none.textContent = 'No models configured';
|
|
6469
|
+
defaultSelect.appendChild(none);
|
|
6470
|
+
defaultSelect.disabled = true;
|
|
6471
|
+
return;
|
|
6472
|
+
}
|
|
6473
|
+
defaultSelect.disabled = false;
|
|
6474
|
+
providerDefs.forEach(function (provider) {
|
|
6475
|
+
var group = null;
|
|
6476
|
+
gwState.catalog.forEach(function (model) {
|
|
6477
|
+
if (model.provider !== provider.id) return;
|
|
6478
|
+
if (!group) {
|
|
6479
|
+
group = document.createElement('optgroup');
|
|
6480
|
+
group.label = provider.title;
|
|
6481
|
+
defaultSelect.appendChild(group);
|
|
6482
|
+
}
|
|
6483
|
+
var opt = document.createElement('option');
|
|
6484
|
+
opt.value = model.id;
|
|
6485
|
+
opt.textContent = model.label || model.id;
|
|
6486
|
+
group.appendChild(opt);
|
|
6487
|
+
});
|
|
6488
|
+
});
|
|
6489
|
+
var current = gwState.catalog.find(function (model) {
|
|
6490
|
+
return model.gatewayDefault;
|
|
6491
|
+
});
|
|
6492
|
+
defaultSelect.value = current ? current.id : gwState.catalog[0].id;
|
|
6493
|
+
}
|
|
6494
|
+
|
|
6495
|
+
function renderAll() {
|
|
6496
|
+
renderDefaultSelect();
|
|
6497
|
+
renderGwModels();
|
|
6498
|
+
}
|
|
6499
|
+
|
|
6500
|
+
addProvider.addEventListener('change', function () {
|
|
6501
|
+
addContext.disabled = addProvider.value === 'anthropic';
|
|
6502
|
+
if (addContext.disabled) addContext.value = '';
|
|
6503
|
+
});
|
|
6504
|
+
addBtn.addEventListener('click', function () {
|
|
6505
|
+
var id = (addId.value || '').trim();
|
|
6506
|
+
var provider = addProvider.value;
|
|
6507
|
+
var context = addContext.value ? Number(addContext.value) : undefined;
|
|
6508
|
+
if (!id) {
|
|
6509
|
+
setSettingsStatus('Model ID is required', 'error');
|
|
6510
|
+
return;
|
|
6511
|
+
}
|
|
6512
|
+
if (
|
|
6513
|
+
id === 'claude' ||
|
|
6514
|
+
gwState.catalog.some(function (item) {
|
|
6515
|
+
return item.id === id;
|
|
6516
|
+
})
|
|
6517
|
+
) {
|
|
6518
|
+
setSettingsStatus('Duplicate or reserved model ID: ' + id, 'error');
|
|
6519
|
+
return;
|
|
6520
|
+
}
|
|
6521
|
+
if (context !== undefined && (!Number.isInteger(context) || context <= 0)) {
|
|
6522
|
+
setSettingsStatus('Context window must be a positive whole number', 'error');
|
|
6523
|
+
return;
|
|
6524
|
+
}
|
|
6525
|
+
var model = {
|
|
6526
|
+
id: id,
|
|
6527
|
+
label: (addLabel.value || '').trim() || id,
|
|
6528
|
+
provider: provider,
|
|
6529
|
+
contextWindow: provider === 'anthropic' ? undefined : context,
|
|
6530
|
+
gatewayDefault: gwState.catalog.length === 0,
|
|
6531
|
+
providerDefault:
|
|
6532
|
+
provider === 'anthropic' &&
|
|
6533
|
+
!gwState.catalog.some(function (item) {
|
|
6534
|
+
return item.provider === 'anthropic';
|
|
6535
|
+
}),
|
|
6536
|
+
};
|
|
6537
|
+
gwState.catalog.push(model);
|
|
6538
|
+
addId.value = '';
|
|
6539
|
+
addLabel.value = '';
|
|
6540
|
+
addContext.value = '';
|
|
6541
|
+
setSettingsStatus('', '');
|
|
6542
|
+
renderAll();
|
|
6543
|
+
});
|
|
6544
|
+
|
|
6545
|
+
function refreshOpenModelCatalogs() {
|
|
6546
|
+
var refresh = new XMLHttpRequest();
|
|
6547
|
+
refresh.open('GET', '/api/models');
|
|
6548
|
+
refresh.setRequestHeader('X-API-Key', activeApiKey);
|
|
6549
|
+
refresh.onreadystatechange = function () {
|
|
6550
|
+
if (refresh.readyState !== 4 || refresh.status !== 200) return;
|
|
6551
|
+
try {
|
|
6552
|
+
window.dispatchEvent(
|
|
6553
|
+
new CustomEvent('cumulus-model-catalog-updated', {
|
|
6554
|
+
detail: JSON.parse(refresh.responseText),
|
|
6555
|
+
})
|
|
6556
|
+
);
|
|
6557
|
+
} catch (e) {
|
|
6558
|
+
/* keep the saved config even if a panel refresh fails */
|
|
6559
|
+
}
|
|
6560
|
+
};
|
|
6561
|
+
refresh.send();
|
|
6562
|
+
}
|
|
6563
|
+
|
|
6564
|
+
saveBtn.addEventListener('click', function () {
|
|
6565
|
+
var validationError = validateModelCatalog(gwState.catalog);
|
|
6566
|
+
if (validationError) {
|
|
6567
|
+
setSettingsStatus(validationError, 'error');
|
|
6568
|
+
return;
|
|
6569
|
+
}
|
|
6570
|
+
saveBtn.disabled = true;
|
|
6571
|
+
setSettingsStatus('Saving…', '');
|
|
6572
|
+
var payload = addCredentialPatch(modelCatalogToGatewayConfig(gwState), {
|
|
6573
|
+
openai: {
|
|
6574
|
+
value: credentialValues.openai,
|
|
6575
|
+
clear: credentialClears.openai,
|
|
6576
|
+
},
|
|
6577
|
+
huggingface: {
|
|
6578
|
+
value: credentialValues.huggingface,
|
|
6579
|
+
clear: credentialClears.huggingface,
|
|
6580
|
+
},
|
|
6581
|
+
license: {
|
|
6582
|
+
value: credentialValues.license,
|
|
6583
|
+
clear: credentialClears.license,
|
|
6584
|
+
},
|
|
6585
|
+
});
|
|
6586
|
+
var save = new XMLHttpRequest();
|
|
6587
|
+
save.open('PUT', '/api/config');
|
|
6588
|
+
save.setRequestHeader('Content-Type', 'application/json');
|
|
6589
|
+
save.setRequestHeader('X-API-Key', activeApiKey);
|
|
6590
|
+
save.onreadystatechange = function () {
|
|
6591
|
+
if (save.readyState !== 4) return;
|
|
6592
|
+
saveBtn.disabled = false;
|
|
6593
|
+
if (save.status === 200) {
|
|
6594
|
+
try {
|
|
6595
|
+
var response = JSON.parse(save.responseText);
|
|
6596
|
+
gwState = gatewayConfigToModelCatalog(response);
|
|
6597
|
+
credentialClears.openai = false;
|
|
6598
|
+
credentialClears.huggingface = false;
|
|
6599
|
+
credentialClears.license = false;
|
|
6600
|
+
credentialValues.openai = response.openaiApiKey || '';
|
|
6601
|
+
credentialValues.huggingface = response.hfApiKey || '';
|
|
6602
|
+
credentialValues.license = response.licenseKey || '';
|
|
6603
|
+
gwLicenseMasked = response.licenseKey || '';
|
|
6604
|
+
gwLicenseInfo = response.license || null;
|
|
6605
|
+
renderAll();
|
|
6606
|
+
renderLicenseState(gwLicenseMasked, gwLicenseInfo);
|
|
6607
|
+
setSettingsStatus(
|
|
6608
|
+
gwLicenseInfo && gwLicenseInfo.pendingReload
|
|
6609
|
+
? 'Saved — license takes effect on gateway reload'
|
|
6610
|
+
: 'Saved — applies to new turns immediately',
|
|
6611
|
+
'success'
|
|
6612
|
+
);
|
|
6613
|
+
refreshOpenModelCatalogs();
|
|
6614
|
+
} catch (e) {
|
|
6615
|
+
setSettingsStatus('Saved, but the response could not be rendered', 'error');
|
|
6616
|
+
}
|
|
6617
|
+
} else {
|
|
6618
|
+
setSettingsStatus('Error ' + save.status, 'error');
|
|
6619
|
+
}
|
|
6620
|
+
};
|
|
6621
|
+
save.send(JSON.stringify(payload));
|
|
6622
|
+
});
|
|
6623
|
+
|
|
6624
|
+
(function loadGatewayConfig() {
|
|
6625
|
+
var load = new XMLHttpRequest();
|
|
6626
|
+
load.open('GET', '/api/config');
|
|
6627
|
+
load.setRequestHeader('X-API-Key', activeApiKey);
|
|
6628
|
+
load.onreadystatechange = function () {
|
|
6629
|
+
if (load.readyState !== 4) return;
|
|
6630
|
+
if (load.status === 200) {
|
|
6631
|
+
try {
|
|
6632
|
+
var response = JSON.parse(load.responseText);
|
|
6633
|
+
gwState = gatewayConfigToModelCatalog(response);
|
|
6634
|
+
credentialValues.openai = response.openaiApiKey || '';
|
|
6635
|
+
credentialValues.huggingface = response.hfApiKey || '';
|
|
6636
|
+
credentialValues.license = response.licenseKey || '';
|
|
6637
|
+
gwLicenseMasked = response.licenseKey || '';
|
|
6638
|
+
gwLicenseInfo = response.license || null;
|
|
6639
|
+
renderAll();
|
|
6640
|
+
renderLicenseState(gwLicenseMasked, gwLicenseInfo);
|
|
6641
|
+
} catch (e) {
|
|
6642
|
+
setSettingsStatus('Gateway settings response was invalid', 'error');
|
|
6643
|
+
}
|
|
6644
|
+
} else {
|
|
6645
|
+
setSettingsStatus('Load failed (' + load.status + ')', 'error');
|
|
6646
|
+
}
|
|
6647
|
+
};
|
|
6648
|
+
load.send();
|
|
6649
|
+
})();
|
|
6650
|
+
}
|
|
6651
|
+
|
|
5849
6652
|
function buildThreadPanel(threadName) {
|
|
5850
6653
|
var state = getThreadState(threadName);
|
|
5851
6654
|
|
|
@@ -5907,14 +6710,14 @@
|
|
|
5907
6710
|
claudeModelSelect.setAttribute('data-testid', 'webchat-claude-model-select');
|
|
5908
6711
|
claudeModelSelect.setAttribute('data-thread-claude-model', threadName);
|
|
5909
6712
|
claudeModelSelect.setAttribute('title', 'Claude model');
|
|
5910
|
-
//
|
|
5911
|
-
//
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
];
|
|
6713
|
+
// Populated ONLY from /api/models (resp.claudeModels) — task 129 deleted
|
|
6714
|
+
// the hardcoded fallback list. An empty catalog hides the select entirely
|
|
6715
|
+
// (visibility is re-derived in updateEffortVisibility, which also gates
|
|
6716
|
+
// on model === 'claude'); the Settings gear is where models get added.
|
|
6717
|
+
var claudeModelListEmpty = true;
|
|
5916
6718
|
function populateClaudeModels(list) {
|
|
5917
|
-
var models = list && list.length ? list :
|
|
6719
|
+
var models = list && list.length ? list : [];
|
|
6720
|
+
claudeModelListEmpty = models.length === 0;
|
|
5918
6721
|
claudeModelSelect.innerHTML = '';
|
|
5919
6722
|
var def = null;
|
|
5920
6723
|
models.forEach(function (m) {
|
|
@@ -5925,6 +6728,7 @@
|
|
|
5925
6728
|
if (m.default) def = m.id;
|
|
5926
6729
|
});
|
|
5927
6730
|
claudeModelSelect.value = def || (models[0] && models[0].id) || '';
|
|
6731
|
+
claudeModelSelect.style.display = claudeModelListEmpty ? 'none' : '';
|
|
5928
6732
|
}
|
|
5929
6733
|
populateClaudeModels(null);
|
|
5930
6734
|
|
|
@@ -6024,7 +6828,7 @@
|
|
|
6024
6828
|
function updateEffortVisibility() {
|
|
6025
6829
|
var isClaude = modelSelect.value === 'claude';
|
|
6026
6830
|
effortSelect.style.display = isClaude ? '' : 'none';
|
|
6027
|
-
claudeModelSelect.style.display = isClaude ? '' : 'none';
|
|
6831
|
+
claudeModelSelect.style.display = isClaude && !claudeModelListEmpty ? '' : 'none';
|
|
6028
6832
|
// Hide the whole Model/Effort popover rows (label included) for non-Claude providers.
|
|
6029
6833
|
var mr = panel.querySelector('[data-config-row="model"]');
|
|
6030
6834
|
var er = panel.querySelector('[data-config-row="effort"]');
|
|
@@ -6185,546 +6989,13 @@
|
|
|
6185
6989
|
modelPicker.appendChild(configBreadcrumb);
|
|
6186
6990
|
modelPicker.appendChild(modelPopover);
|
|
6187
6991
|
|
|
6188
|
-
// ── Settings modal (provider-organized model catalog + credentials) ──
|
|
6189
|
-
// Live-editable via PUT /api/config (task 104) — takes effect on the NEXT turn
|
|
6190
|
-
// with no restart, no JSON edit, no republish. Any valid gateway key.
|
|
6191
|
-
function openSettingsModal() {
|
|
6192
|
-
var backdrop = document.createElement('div');
|
|
6193
|
-
backdrop.className = 'cumulus-settings-backdrop';
|
|
6194
|
-
backdrop.setAttribute('data-testid', 'webchat-settings-modal');
|
|
6195
|
-
var modal = document.createElement('div');
|
|
6196
|
-
modal.className = 'cumulus-settings-modal';
|
|
6197
|
-
|
|
6198
|
-
var mHeader = document.createElement('div');
|
|
6199
|
-
mHeader.className = 'cumulus-settings-header';
|
|
6200
|
-
var mHeaderText = document.createElement('div');
|
|
6201
|
-
var mTitle = document.createElement('div');
|
|
6202
|
-
mTitle.className = 'cumulus-settings-title';
|
|
6203
|
-
mTitle.textContent = 'Gateway settings';
|
|
6204
|
-
var mSub = document.createElement('div');
|
|
6205
|
-
mSub.className = 'cumulus-settings-subtitle';
|
|
6206
|
-
mSub.textContent =
|
|
6207
|
-
'Manage Anthropic, OpenAI, and HuggingFace models — changes apply to new turns instantly';
|
|
6208
|
-
mHeaderText.appendChild(mTitle);
|
|
6209
|
-
mHeaderText.appendChild(mSub);
|
|
6210
|
-
var mClose = document.createElement('button');
|
|
6211
|
-
mClose.className = 'cumulus-overlay-close';
|
|
6212
|
-
mClose.textContent = '\xD7';
|
|
6213
|
-
mClose.setAttribute('data-testid', 'webchat-settings-close');
|
|
6214
|
-
mClose.addEventListener('click', function () {
|
|
6215
|
-
backdrop.remove();
|
|
6216
|
-
});
|
|
6217
|
-
mHeader.appendChild(mHeaderText);
|
|
6218
|
-
mHeader.appendChild(mClose);
|
|
6219
|
-
modal.appendChild(mHeader);
|
|
6220
|
-
|
|
6221
|
-
var body = document.createElement('div');
|
|
6222
|
-
body.className = 'cumulus-settings-body';
|
|
6223
|
-
|
|
6224
|
-
var modelsLabel = document.createElement('div');
|
|
6225
|
-
modelsLabel.className = 'cumulus-settings-section-label';
|
|
6226
|
-
modelsLabel.textContent = 'Models by provider';
|
|
6227
|
-
body.appendChild(modelsLabel);
|
|
6228
|
-
|
|
6229
|
-
var gwList = document.createElement('div');
|
|
6230
|
-
gwList.setAttribute('data-testid', 'webchat-gateway-models');
|
|
6231
|
-
body.appendChild(gwList);
|
|
6232
|
-
|
|
6233
|
-
var addCard = document.createElement('div');
|
|
6234
|
-
addCard.className = 'cumulus-settings-add-card';
|
|
6235
|
-
var addTitle = document.createElement('div');
|
|
6236
|
-
addTitle.className = 'cumulus-settings-add-title';
|
|
6237
|
-
addTitle.textContent = 'Add a model';
|
|
6238
|
-
addCard.appendChild(addTitle);
|
|
6239
|
-
var addWrap = document.createElement('div');
|
|
6240
|
-
addWrap.className = 'cumulus-settings-add-row';
|
|
6241
|
-
var addProvider = document.createElement('select');
|
|
6242
|
-
addProvider.className = 'cumulus-settings-select';
|
|
6243
|
-
addProvider.setAttribute('data-testid', 'webchat-gw-add-provider');
|
|
6244
|
-
[
|
|
6245
|
-
{ value: 'anthropic', label: 'Anthropic' },
|
|
6246
|
-
{ value: 'openai', label: 'OpenAI' },
|
|
6247
|
-
{ value: 'huggingface', label: 'HuggingFace' },
|
|
6248
|
-
].forEach(function (provider) {
|
|
6249
|
-
var option = document.createElement('option');
|
|
6250
|
-
option.value = provider.value;
|
|
6251
|
-
option.textContent = provider.label;
|
|
6252
|
-
addProvider.appendChild(option);
|
|
6253
|
-
});
|
|
6254
|
-
var addId = document.createElement('input');
|
|
6255
|
-
addId.type = 'text';
|
|
6256
|
-
addId.className = 'cumulus-settings-input';
|
|
6257
|
-
addId.placeholder = 'Model ID / name';
|
|
6258
|
-
addId.setAttribute('data-testid', 'webchat-gw-add-id');
|
|
6259
|
-
var addLabel = document.createElement('input');
|
|
6260
|
-
addLabel.type = 'text';
|
|
6261
|
-
addLabel.className = 'cumulus-settings-input';
|
|
6262
|
-
addLabel.placeholder = 'Display label (optional)';
|
|
6263
|
-
addLabel.setAttribute('data-testid', 'webchat-gw-add-label');
|
|
6264
|
-
var addContext = document.createElement('input');
|
|
6265
|
-
addContext.type = 'number';
|
|
6266
|
-
addContext.min = '1';
|
|
6267
|
-
addContext.step = '1';
|
|
6268
|
-
addContext.className = 'cumulus-settings-input';
|
|
6269
|
-
addContext.placeholder = 'Context tokens';
|
|
6270
|
-
addContext.setAttribute('data-testid', 'webchat-gw-add-context');
|
|
6271
|
-
var addBtn = document.createElement('button');
|
|
6272
|
-
addBtn.type = 'button';
|
|
6273
|
-
addBtn.textContent = '+ Add';
|
|
6274
|
-
addBtn.className = 'cumulus-settings-add-btn';
|
|
6275
|
-
addBtn.setAttribute('data-testid', 'webchat-gw-add-btn');
|
|
6276
|
-
addWrap.appendChild(addProvider);
|
|
6277
|
-
addWrap.appendChild(addId);
|
|
6278
|
-
addWrap.appendChild(addLabel);
|
|
6279
|
-
addWrap.appendChild(addContext);
|
|
6280
|
-
addWrap.appendChild(addBtn);
|
|
6281
|
-
addCard.appendChild(addWrap);
|
|
6282
|
-
body.appendChild(addCard);
|
|
6283
|
-
modal.appendChild(body);
|
|
6284
|
-
|
|
6285
|
-
var footer = document.createElement('div');
|
|
6286
|
-
footer.className = 'cumulus-settings-footer';
|
|
6287
|
-
var saveBtn = document.createElement('button');
|
|
6288
|
-
saveBtn.textContent = 'Save';
|
|
6289
|
-
saveBtn.className = 'cumulus-settings-btn';
|
|
6290
|
-
saveBtn.setAttribute('data-testid', 'webchat-gw-save');
|
|
6291
|
-
var gwStatus = document.createElement('span');
|
|
6292
|
-
gwStatus.className = 'cumulus-settings-status';
|
|
6293
|
-
footer.appendChild(saveBtn);
|
|
6294
|
-
footer.appendChild(gwStatus);
|
|
6295
|
-
modal.appendChild(footer);
|
|
6296
|
-
|
|
6297
|
-
backdrop.appendChild(modal);
|
|
6298
|
-
backdrop.addEventListener('click', function (e) {
|
|
6299
|
-
if (e.target === backdrop) backdrop.remove();
|
|
6300
|
-
});
|
|
6301
|
-
document.body.appendChild(backdrop);
|
|
6302
|
-
|
|
6303
|
-
var gwState = { catalog: [], sentinel: null };
|
|
6304
|
-
var credentialInputs = {};
|
|
6305
|
-
var credentialValues = { openai: '', huggingface: '' };
|
|
6306
|
-
var credentialClears = { openai: false, huggingface: false };
|
|
6307
|
-
var providerDefs = [
|
|
6308
|
-
{
|
|
6309
|
-
id: 'anthropic',
|
|
6310
|
-
title: 'Anthropic',
|
|
6311
|
-
description: 'Claude CLI models · authentication is managed by the Claude CLI',
|
|
6312
|
-
},
|
|
6313
|
-
{
|
|
6314
|
-
id: 'openai',
|
|
6315
|
-
title: 'OpenAI',
|
|
6316
|
-
description: 'Responses API models with direct tool use',
|
|
6317
|
-
credential: 'openaiApiKey',
|
|
6318
|
-
credentialTestId: 'webchat-gw-openaikey',
|
|
6319
|
-
},
|
|
6320
|
-
{
|
|
6321
|
-
id: 'huggingface',
|
|
6322
|
-
title: 'HuggingFace',
|
|
6323
|
-
description: 'OpenAI-compatible models served through the HuggingFace router',
|
|
6324
|
-
credential: 'hfApiKey',
|
|
6325
|
-
credentialTestId: 'webchat-gw-hfkey',
|
|
6326
|
-
},
|
|
6327
|
-
];
|
|
6328
|
-
|
|
6329
|
-
function setSettingsStatus(message, kind) {
|
|
6330
|
-
gwStatus.textContent = message || '';
|
|
6331
|
-
gwStatus.className = 'cumulus-settings-status' + (kind ? ' ' + kind : '');
|
|
6332
|
-
}
|
|
6333
|
-
|
|
6334
|
-
function makeRadioLabel(text, input) {
|
|
6335
|
-
var wrap = document.createElement('label');
|
|
6336
|
-
wrap.className = 'cumulus-settings-radio-label';
|
|
6337
|
-
wrap.appendChild(input);
|
|
6338
|
-
wrap.appendChild(document.createElement('br'));
|
|
6339
|
-
wrap.appendChild(document.createTextNode(text));
|
|
6340
|
-
return wrap;
|
|
6341
|
-
}
|
|
6342
|
-
|
|
6343
|
-
function renderModelRow(model, providerModels, index) {
|
|
6344
|
-
var row = document.createElement('div');
|
|
6345
|
-
row.className =
|
|
6346
|
-
'cumulus-settings-model-row ' +
|
|
6347
|
-
model.provider +
|
|
6348
|
-
(model.gatewayDefault ? ' is-default' : '');
|
|
6349
|
-
row.setAttribute('data-gw-model', model.id);
|
|
6350
|
-
row.setAttribute('data-testid', 'webchat-gw-row-' + model.provider + '-' + index);
|
|
6351
|
-
|
|
6352
|
-
var gatewayRadio = document.createElement('input');
|
|
6353
|
-
gatewayRadio.type = 'radio';
|
|
6354
|
-
gatewayRadio.name = 'gw-default';
|
|
6355
|
-
gatewayRadio.checked = !!model.gatewayDefault;
|
|
6356
|
-
gatewayRadio.setAttribute(
|
|
6357
|
-
'data-testid',
|
|
6358
|
-
'webchat-gw-default-' + model.provider + '-' + index
|
|
6359
|
-
);
|
|
6360
|
-
gatewayRadio.addEventListener('change', function () {
|
|
6361
|
-
gwState.catalog.forEach(function (item) {
|
|
6362
|
-
item.gatewayDefault = false;
|
|
6363
|
-
});
|
|
6364
|
-
model.gatewayDefault = true;
|
|
6365
|
-
if (model.provider === 'anthropic') {
|
|
6366
|
-
gwState.catalog.forEach(function (item) {
|
|
6367
|
-
if (item.provider === 'anthropic') item.providerDefault = false;
|
|
6368
|
-
});
|
|
6369
|
-
model.providerDefault = true;
|
|
6370
|
-
}
|
|
6371
|
-
renderGwModels();
|
|
6372
|
-
});
|
|
6373
|
-
row.appendChild(makeRadioLabel('gateway', gatewayRadio));
|
|
6374
|
-
|
|
6375
|
-
if (model.provider === 'anthropic') {
|
|
6376
|
-
var providerRadio = document.createElement('input');
|
|
6377
|
-
providerRadio.type = 'radio';
|
|
6378
|
-
providerRadio.name = 'gw-anthropic-default';
|
|
6379
|
-
providerRadio.checked = !!model.providerDefault;
|
|
6380
|
-
providerRadio.setAttribute(
|
|
6381
|
-
'data-testid',
|
|
6382
|
-
'webchat-gw-provider-default-anthropic-' + index
|
|
6383
|
-
);
|
|
6384
|
-
providerRadio.addEventListener('change', function () {
|
|
6385
|
-
gwState.catalog.forEach(function (item) {
|
|
6386
|
-
if (item.provider === 'anthropic') item.providerDefault = false;
|
|
6387
|
-
});
|
|
6388
|
-
model.providerDefault = true;
|
|
6389
|
-
if (
|
|
6390
|
-
gwState.catalog.some(function (item) {
|
|
6391
|
-
return item.provider === 'anthropic' && item.gatewayDefault;
|
|
6392
|
-
})
|
|
6393
|
-
) {
|
|
6394
|
-
gwState.catalog.forEach(function (item) {
|
|
6395
|
-
item.gatewayDefault = false;
|
|
6396
|
-
});
|
|
6397
|
-
model.gatewayDefault = true;
|
|
6398
|
-
}
|
|
6399
|
-
renderGwModels();
|
|
6400
|
-
});
|
|
6401
|
-
row.appendChild(makeRadioLabel('Claude', providerRadio));
|
|
6402
|
-
} else {
|
|
6403
|
-
var spacer = document.createElement('span');
|
|
6404
|
-
row.appendChild(spacer);
|
|
6405
|
-
}
|
|
6406
|
-
|
|
6407
|
-
var idInput = document.createElement('input');
|
|
6408
|
-
idInput.type = 'text';
|
|
6409
|
-
idInput.className = 'cumulus-settings-input cumulus-settings-row-input';
|
|
6410
|
-
idInput.value = model.id;
|
|
6411
|
-
idInput.setAttribute('aria-label', providerModels.title + ' model ID');
|
|
6412
|
-
idInput.setAttribute('data-testid', 'webchat-gw-id-' + model.provider + '-' + index);
|
|
6413
|
-
idInput.addEventListener('input', function () {
|
|
6414
|
-
model.id = idInput.value;
|
|
6415
|
-
});
|
|
6416
|
-
row.appendChild(idInput);
|
|
6417
|
-
|
|
6418
|
-
var labelInput = document.createElement('input');
|
|
6419
|
-
labelInput.type = 'text';
|
|
6420
|
-
labelInput.className = 'cumulus-settings-input cumulus-settings-row-input';
|
|
6421
|
-
labelInput.value = model.label || model.id;
|
|
6422
|
-
labelInput.setAttribute('aria-label', providerModels.title + ' display label');
|
|
6423
|
-
labelInput.setAttribute(
|
|
6424
|
-
'data-testid',
|
|
6425
|
-
'webchat-gw-label-' + model.provider + '-' + index
|
|
6426
|
-
);
|
|
6427
|
-
labelInput.addEventListener('input', function () {
|
|
6428
|
-
model.label = labelInput.value;
|
|
6429
|
-
});
|
|
6430
|
-
row.appendChild(labelInput);
|
|
6431
|
-
|
|
6432
|
-
if (model.provider !== 'anthropic') {
|
|
6433
|
-
var contextInput = document.createElement('input');
|
|
6434
|
-
contextInput.type = 'number';
|
|
6435
|
-
contextInput.min = '1';
|
|
6436
|
-
contextInput.step = '1';
|
|
6437
|
-
contextInput.className = 'cumulus-settings-input cumulus-settings-row-input';
|
|
6438
|
-
contextInput.placeholder = 'Context';
|
|
6439
|
-
contextInput.value = model.contextWindow || '';
|
|
6440
|
-
contextInput.setAttribute('aria-label', providerModels.title + ' context window');
|
|
6441
|
-
contextInput.setAttribute(
|
|
6442
|
-
'data-testid',
|
|
6443
|
-
'webchat-gw-context-' + model.provider + '-' + index
|
|
6444
|
-
);
|
|
6445
|
-
contextInput.addEventListener('input', function () {
|
|
6446
|
-
model.contextWindow = contextInput.value ? Number(contextInput.value) : undefined;
|
|
6447
|
-
});
|
|
6448
|
-
row.appendChild(contextInput);
|
|
6449
|
-
}
|
|
6450
|
-
|
|
6451
|
-
var remove = document.createElement('button');
|
|
6452
|
-
remove.type = 'button';
|
|
6453
|
-
remove.className = 'cumulus-settings-model-remove';
|
|
6454
|
-
remove.textContent = '\xD7';
|
|
6455
|
-
remove.title = 'Remove ' + model.id;
|
|
6456
|
-
remove.setAttribute('data-testid', 'webchat-gw-remove-' + model.provider + '-' + index);
|
|
6457
|
-
remove.addEventListener('click', function () {
|
|
6458
|
-
var absoluteIndex = gwState.catalog.indexOf(model);
|
|
6459
|
-
var wasGatewayDefault = model.gatewayDefault;
|
|
6460
|
-
var wasProviderDefault = model.providerDefault;
|
|
6461
|
-
if (absoluteIndex >= 0) gwState.catalog.splice(absoluteIndex, 1);
|
|
6462
|
-
if (wasGatewayDefault && gwState.catalog.length) {
|
|
6463
|
-
var nextGatewayDefault = gwState.catalog[0];
|
|
6464
|
-
nextGatewayDefault.gatewayDefault = true;
|
|
6465
|
-
if (nextGatewayDefault.provider === 'anthropic') {
|
|
6466
|
-
gwState.catalog.forEach(function (item) {
|
|
6467
|
-
if (item.provider === 'anthropic') item.providerDefault = false;
|
|
6468
|
-
});
|
|
6469
|
-
nextGatewayDefault.providerDefault = true;
|
|
6470
|
-
}
|
|
6471
|
-
}
|
|
6472
|
-
if (
|
|
6473
|
-
wasProviderDefault &&
|
|
6474
|
-
!gwState.catalog.some(function (item) {
|
|
6475
|
-
return item.provider === 'anthropic' && item.providerDefault;
|
|
6476
|
-
})
|
|
6477
|
-
) {
|
|
6478
|
-
var nextClaude = gwState.catalog.find(function (item) {
|
|
6479
|
-
return item.provider === 'anthropic';
|
|
6480
|
-
});
|
|
6481
|
-
if (nextClaude) nextClaude.providerDefault = true;
|
|
6482
|
-
}
|
|
6483
|
-
renderGwModels();
|
|
6484
|
-
});
|
|
6485
|
-
row.appendChild(remove);
|
|
6486
|
-
return row;
|
|
6487
|
-
}
|
|
6488
|
-
|
|
6489
|
-
function renderCredential(card, provider) {
|
|
6490
|
-
if (!provider.credential) {
|
|
6491
|
-
var note = document.createElement('div');
|
|
6492
|
-
note.className = 'cumulus-settings-credential-note';
|
|
6493
|
-
note.textContent = 'Authenticate with `claude login` on the gateway host.';
|
|
6494
|
-
card.appendChild(note);
|
|
6495
|
-
return;
|
|
6496
|
-
}
|
|
6497
|
-
var credentialRow = document.createElement('div');
|
|
6498
|
-
credentialRow.className = 'cumulus-settings-provider-credential';
|
|
6499
|
-
var credentialLabel = document.createElement('label');
|
|
6500
|
-
credentialLabel.textContent = 'API key';
|
|
6501
|
-
var credentialInput = document.createElement('input');
|
|
6502
|
-
credentialInput.type = 'password';
|
|
6503
|
-
credentialInput.autocomplete = 'off';
|
|
6504
|
-
credentialInput.className = 'cumulus-settings-input';
|
|
6505
|
-
credentialInput.placeholder = credentialClears[provider.id]
|
|
6506
|
-
? 'Will be cleared on save'
|
|
6507
|
-
: '(unset)';
|
|
6508
|
-
credentialInput.value = credentialValues[provider.id] || '';
|
|
6509
|
-
credentialInput.setAttribute('data-testid', provider.credentialTestId);
|
|
6510
|
-
credentialInput.addEventListener('input', function () {
|
|
6511
|
-
credentialValues[provider.id] = credentialInput.value;
|
|
6512
|
-
credentialClears[provider.id] = false;
|
|
6513
|
-
credentialInput.placeholder = '(unset)';
|
|
6514
|
-
});
|
|
6515
|
-
credentialInputs[provider.id] = credentialInput;
|
|
6516
|
-
var clear = document.createElement('button');
|
|
6517
|
-
clear.type = 'button';
|
|
6518
|
-
clear.className = 'cumulus-settings-clear-btn';
|
|
6519
|
-
clear.textContent = 'Clear';
|
|
6520
|
-
clear.setAttribute('data-testid', 'webchat-gw-clear-' + provider.id + '-key');
|
|
6521
|
-
clear.addEventListener('click', function () {
|
|
6522
|
-
credentialClears[provider.id] = true;
|
|
6523
|
-
credentialValues[provider.id] = '';
|
|
6524
|
-
credentialInput.value = '';
|
|
6525
|
-
credentialInput.placeholder = 'Will be cleared on save';
|
|
6526
|
-
setSettingsStatus('Credential marked for removal', '');
|
|
6527
|
-
});
|
|
6528
|
-
credentialRow.appendChild(credentialLabel);
|
|
6529
|
-
credentialRow.appendChild(credentialInput);
|
|
6530
|
-
credentialRow.appendChild(clear);
|
|
6531
|
-
card.appendChild(credentialRow);
|
|
6532
|
-
}
|
|
6533
|
-
|
|
6534
|
-
function renderGwModels() {
|
|
6535
|
-
gwList.innerHTML = '';
|
|
6536
|
-
credentialInputs = {};
|
|
6537
|
-
providerDefs.forEach(function (provider) {
|
|
6538
|
-
var providerModels = gwState.catalog.filter(function (model) {
|
|
6539
|
-
return model.provider === provider.id;
|
|
6540
|
-
});
|
|
6541
|
-
var card = document.createElement('section');
|
|
6542
|
-
card.className = 'cumulus-settings-provider-card';
|
|
6543
|
-
card.setAttribute('data-testid', 'webchat-gw-provider-' + provider.id);
|
|
6544
|
-
var head = document.createElement('div');
|
|
6545
|
-
head.className = 'cumulus-settings-provider-head';
|
|
6546
|
-
var heading = document.createElement('div');
|
|
6547
|
-
var title = document.createElement('div');
|
|
6548
|
-
title.className = 'cumulus-settings-provider-title';
|
|
6549
|
-
title.textContent = provider.title;
|
|
6550
|
-
var description = document.createElement('div');
|
|
6551
|
-
description.className = 'cumulus-settings-provider-desc';
|
|
6552
|
-
description.textContent = provider.description;
|
|
6553
|
-
heading.appendChild(title);
|
|
6554
|
-
heading.appendChild(description);
|
|
6555
|
-
var count = document.createElement('span');
|
|
6556
|
-
count.className = 'cumulus-settings-provider-count';
|
|
6557
|
-
count.textContent =
|
|
6558
|
-
providerModels.length + (providerModels.length === 1 ? ' model' : ' models');
|
|
6559
|
-
head.appendChild(heading);
|
|
6560
|
-
head.appendChild(count);
|
|
6561
|
-
card.appendChild(head);
|
|
6562
|
-
var list = document.createElement('div');
|
|
6563
|
-
list.className = 'cumulus-settings-provider-models';
|
|
6564
|
-
if (!providerModels.length) {
|
|
6565
|
-
var empty = document.createElement('div');
|
|
6566
|
-
empty.className = 'cumulus-settings-provider-empty';
|
|
6567
|
-
empty.textContent = 'No ' + provider.title + ' models configured.';
|
|
6568
|
-
list.appendChild(empty);
|
|
6569
|
-
} else {
|
|
6570
|
-
providerModels.forEach(function (model, index) {
|
|
6571
|
-
list.appendChild(renderModelRow(model, provider, index));
|
|
6572
|
-
});
|
|
6573
|
-
}
|
|
6574
|
-
card.appendChild(list);
|
|
6575
|
-
renderCredential(card, provider);
|
|
6576
|
-
gwList.appendChild(card);
|
|
6577
|
-
});
|
|
6578
|
-
}
|
|
6579
|
-
|
|
6580
|
-
addProvider.addEventListener('change', function () {
|
|
6581
|
-
addContext.disabled = addProvider.value === 'anthropic';
|
|
6582
|
-
if (addContext.disabled) addContext.value = '';
|
|
6583
|
-
});
|
|
6584
|
-
addBtn.addEventListener('click', function () {
|
|
6585
|
-
var id = (addId.value || '').trim();
|
|
6586
|
-
var provider = addProvider.value;
|
|
6587
|
-
var context = addContext.value ? Number(addContext.value) : undefined;
|
|
6588
|
-
if (!id) {
|
|
6589
|
-
setSettingsStatus('Model ID is required', 'error');
|
|
6590
|
-
return;
|
|
6591
|
-
}
|
|
6592
|
-
if (
|
|
6593
|
-
id === 'claude' ||
|
|
6594
|
-
gwState.catalog.some(function (item) {
|
|
6595
|
-
return item.id === id;
|
|
6596
|
-
})
|
|
6597
|
-
) {
|
|
6598
|
-
setSettingsStatus('Duplicate or reserved model ID: ' + id, 'error');
|
|
6599
|
-
return;
|
|
6600
|
-
}
|
|
6601
|
-
if (context !== undefined && (!Number.isInteger(context) || context <= 0)) {
|
|
6602
|
-
setSettingsStatus('Context window must be a positive whole number', 'error');
|
|
6603
|
-
return;
|
|
6604
|
-
}
|
|
6605
|
-
var model = {
|
|
6606
|
-
id: id,
|
|
6607
|
-
label: (addLabel.value || '').trim() || id,
|
|
6608
|
-
provider: provider,
|
|
6609
|
-
contextWindow: provider === 'anthropic' ? undefined : context,
|
|
6610
|
-
gatewayDefault: gwState.catalog.length === 0,
|
|
6611
|
-
providerDefault:
|
|
6612
|
-
provider === 'anthropic' &&
|
|
6613
|
-
!gwState.catalog.some(function (item) {
|
|
6614
|
-
return item.provider === 'anthropic';
|
|
6615
|
-
}),
|
|
6616
|
-
};
|
|
6617
|
-
gwState.catalog.push(model);
|
|
6618
|
-
addId.value = '';
|
|
6619
|
-
addLabel.value = '';
|
|
6620
|
-
addContext.value = '';
|
|
6621
|
-
setSettingsStatus('', '');
|
|
6622
|
-
renderGwModels();
|
|
6623
|
-
});
|
|
6624
|
-
|
|
6625
|
-
function refreshOpenModelCatalogs() {
|
|
6626
|
-
var refresh = new XMLHttpRequest();
|
|
6627
|
-
refresh.open('GET', '/api/models');
|
|
6628
|
-
refresh.setRequestHeader('X-API-Key', activeApiKey);
|
|
6629
|
-
refresh.onreadystatechange = function () {
|
|
6630
|
-
if (refresh.readyState !== 4 || refresh.status !== 200) return;
|
|
6631
|
-
try {
|
|
6632
|
-
window.dispatchEvent(
|
|
6633
|
-
new CustomEvent('cumulus-model-catalog-updated', {
|
|
6634
|
-
detail: JSON.parse(refresh.responseText),
|
|
6635
|
-
})
|
|
6636
|
-
);
|
|
6637
|
-
} catch (e) {
|
|
6638
|
-
/* keep the saved config even if a panel refresh fails */
|
|
6639
|
-
}
|
|
6640
|
-
};
|
|
6641
|
-
refresh.send();
|
|
6642
|
-
}
|
|
6643
|
-
|
|
6644
|
-
saveBtn.addEventListener('click', function () {
|
|
6645
|
-
var validationError = validateModelCatalog(gwState.catalog);
|
|
6646
|
-
if (validationError) {
|
|
6647
|
-
setSettingsStatus(validationError, 'error');
|
|
6648
|
-
return;
|
|
6649
|
-
}
|
|
6650
|
-
saveBtn.disabled = true;
|
|
6651
|
-
setSettingsStatus('Saving…', '');
|
|
6652
|
-
var payload = addCredentialPatch(modelCatalogToGatewayConfig(gwState), {
|
|
6653
|
-
openai: {
|
|
6654
|
-
value: credentialValues.openai,
|
|
6655
|
-
clear: credentialClears.openai,
|
|
6656
|
-
},
|
|
6657
|
-
huggingface: {
|
|
6658
|
-
value: credentialValues.huggingface,
|
|
6659
|
-
clear: credentialClears.huggingface,
|
|
6660
|
-
},
|
|
6661
|
-
});
|
|
6662
|
-
var save = new XMLHttpRequest();
|
|
6663
|
-
save.open('PUT', '/api/config');
|
|
6664
|
-
save.setRequestHeader('Content-Type', 'application/json');
|
|
6665
|
-
save.setRequestHeader('X-API-Key', activeApiKey);
|
|
6666
|
-
save.onreadystatechange = function () {
|
|
6667
|
-
if (save.readyState !== 4) return;
|
|
6668
|
-
saveBtn.disabled = false;
|
|
6669
|
-
if (save.status === 200) {
|
|
6670
|
-
try {
|
|
6671
|
-
var response = JSON.parse(save.responseText);
|
|
6672
|
-
gwState = gatewayConfigToModelCatalog(response);
|
|
6673
|
-
credentialClears.openai = false;
|
|
6674
|
-
credentialClears.huggingface = false;
|
|
6675
|
-
credentialValues.openai = response.openaiApiKey || '';
|
|
6676
|
-
credentialValues.huggingface = response.hfApiKey || '';
|
|
6677
|
-
renderGwModels();
|
|
6678
|
-
setSettingsStatus('Saved — applies to new turns immediately', 'success');
|
|
6679
|
-
refreshOpenModelCatalogs();
|
|
6680
|
-
} catch (e) {
|
|
6681
|
-
setSettingsStatus('Saved, but the response could not be rendered', 'error');
|
|
6682
|
-
}
|
|
6683
|
-
} else {
|
|
6684
|
-
setSettingsStatus('Error ' + save.status, 'error');
|
|
6685
|
-
}
|
|
6686
|
-
};
|
|
6687
|
-
save.send(JSON.stringify(payload));
|
|
6688
|
-
});
|
|
6689
|
-
|
|
6690
|
-
(function loadGatewayConfig() {
|
|
6691
|
-
var load = new XMLHttpRequest();
|
|
6692
|
-
load.open('GET', '/api/config');
|
|
6693
|
-
load.setRequestHeader('X-API-Key', activeApiKey);
|
|
6694
|
-
load.onreadystatechange = function () {
|
|
6695
|
-
if (load.readyState !== 4) return;
|
|
6696
|
-
if (load.status === 200) {
|
|
6697
|
-
try {
|
|
6698
|
-
var response = JSON.parse(load.responseText);
|
|
6699
|
-
gwState = gatewayConfigToModelCatalog(response);
|
|
6700
|
-
credentialValues.openai = response.openaiApiKey || '';
|
|
6701
|
-
credentialValues.huggingface = response.hfApiKey || '';
|
|
6702
|
-
renderGwModels();
|
|
6703
|
-
} catch (e) {
|
|
6704
|
-
setSettingsStatus('Gateway settings response was invalid', 'error');
|
|
6705
|
-
}
|
|
6706
|
-
} else {
|
|
6707
|
-
setSettingsStatus('Load failed (' + load.status + ')', 'error');
|
|
6708
|
-
}
|
|
6709
|
-
};
|
|
6710
|
-
load.send();
|
|
6711
|
-
})();
|
|
6712
|
-
}
|
|
6713
|
-
|
|
6714
6992
|
panelHeader.appendChild(modelPicker);
|
|
6715
6993
|
|
|
6716
|
-
// Action buttons (
|
|
6994
|
+
// Action buttons (include + revert + select) — gateway settings moved
|
|
6995
|
+
// to the topbar gear (task 128)
|
|
6717
6996
|
var actions = document.createElement('div');
|
|
6718
6997
|
actions.className = 'cumulus-panel-actions';
|
|
6719
6998
|
|
|
6720
|
-
var settingsBtn = document.createElement('button');
|
|
6721
|
-
settingsBtn.className = 'cumulus-panel-action-btn';
|
|
6722
|
-
settingsBtn.setAttribute('data-testid', 'webchat-settings-btn');
|
|
6723
|
-
settingsBtn.setAttribute('title', 'Settings — configure available models');
|
|
6724
|
-
settingsBtn.textContent = '⚙'; // gear
|
|
6725
|
-
settingsBtn.addEventListener('click', openSettingsModal);
|
|
6726
|
-
actions.appendChild(settingsBtn);
|
|
6727
|
-
|
|
6728
6999
|
var includeBtn = document.createElement('button');
|
|
6729
7000
|
includeBtn.className = 'cumulus-panel-action-btn';
|
|
6730
7001
|
includeBtn.setAttribute('data-testid', 'webchat-include-btn');
|
|
@@ -7175,7 +7446,7 @@
|
|
|
7175
7446
|
el.className = 'cumulus-msg assistant';
|
|
7176
7447
|
if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
|
|
7177
7448
|
if (content) {
|
|
7178
|
-
var mdResult = renderMarkdown(content);
|
|
7449
|
+
var mdResult = renderMarkdown(content, { streaming: isStreaming });
|
|
7179
7450
|
el.innerHTML = mdResult.html;
|
|
7180
7451
|
if (isStreaming) {
|
|
7181
7452
|
el.innerHTML += '<span class="cumulus-cursor"></span>';
|