privateboard 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot.js +1404 -1378
- package/dist/boot.js.map +1 -1
- package/dist/cli.js +1404 -1378
- package/dist/cli.js.map +1 -1
- package/dist/server.js +1370 -1359
- package/dist/server.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/public/agent-overlay.css +82 -0
- package/public/agent-overlay.js +459 -17
- package/public/agent-profile.js +34 -54
- package/public/app-updater.css +318 -0
- package/public/app-updater.js +247 -0
- package/public/app.js +1590 -691
- package/public/home.html +1 -1
- package/public/i18n.js +477 -52
- package/public/icons/floor.png +0 -0
- package/public/index.html +600 -213
- package/public/keys-store.js +112 -1
- package/public/mention-picker.js +573 -0
- package/public/new-agent.js +17 -7
- package/public/onboarding.js +108 -117
- package/public/themes.css +44 -0
- package/public/user-settings.css +503 -3
- package/public/user-settings.js +526 -217
- package/public/voice-replay.css +33 -20
- package/public/voice-replay.js +16 -0
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["/**\n * Single source of truth for the app version.\n *\n * Imported by `cli.ts` (CLI banner / `--version`), `server.ts` (the\n * `/health` payload + the `/api/version` endpoint), and bundled into\n * the frontend via the version endpoint. Bump alongside `package.json`\n * on every release — the existing `npm version <patch|minor|major>`\n * + commit pattern updates package.json automatically; this file\n * needs the matching manual bump.\n *\n * If two strings drift (bumped one but not the other), the wrong\n * number ends up surfaced in the user-facing footer or banner. Keep\n * this file as the canonical source — every callsite reads from here.\n */\nexport const VERSION = \"0.1.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["/**\n * Single source of truth for the app version.\n *\n * Imported by `cli.ts` (CLI banner / `--version`), `server.ts` (the\n * `/health` payload + the `/api/version` endpoint), and bundled into\n * the frontend via the version endpoint. Bump alongside `package.json`\n * on every release — the existing `npm version <patch|minor|major>`\n * + commit pattern updates package.json automatically; this file\n * needs the matching manual bump.\n *\n * If two strings drift (bumped one but not the other), the wrong\n * number ends up surfaced in the user-facing footer or banner. Keep\n * this file as the canonical source — every callsite reads from here.\n */\nexport const VERSION = \"0.1.25\";\n"],"mappings":";;;AAcO,IAAM,UAAU;","names":[]}
|
package/package.json
CHANGED
package/public/agent-overlay.css
CHANGED
|
@@ -211,6 +211,88 @@ img[data-agent]:hover { filter: brightness(1.15); }
|
|
|
211
211
|
}
|
|
212
212
|
.agent-model-provider:empty { display: none; }
|
|
213
213
|
|
|
214
|
+
/* Editable model picker · the trigger + popover reuse the
|
|
215
|
+
agent-profile model dropdown vocabulary (`.ap-model-trigger` /
|
|
216
|
+
`.ap-model-picker` / `.ap-model-group` / `.ap-model-opt`), defined
|
|
217
|
+
in agent-profile.css so the styles are shared automatically. Both
|
|
218
|
+
stylesheets are loaded on the main app page (see index.html), so
|
|
219
|
+
the classes resolve identically inside the overlay. */
|
|
220
|
+
.agent-model-edit[hidden] { display: none; }
|
|
221
|
+
/* Lift the `.ap-model-picker` popover above the overlay backdrop ·
|
|
222
|
+
the overlay is z-index 9700; the agent-profile picker default is
|
|
223
|
+
9100, which would sit behind the modal. Scoped modifier class so
|
|
224
|
+
we don't regress the profile-page stacking. */
|
|
225
|
+
.ap-model-picker.ap-model-picker-overlay { z-index: 9800; }
|
|
226
|
+
|
|
227
|
+
.agent-model-meta {
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
flex-wrap: wrap;
|
|
231
|
+
gap: 8px;
|
|
232
|
+
margin-top: 8px;
|
|
233
|
+
font-family: var(--mono);
|
|
234
|
+
font-size: 10px;
|
|
235
|
+
letter-spacing: 0.16em;
|
|
236
|
+
text-transform: uppercase;
|
|
237
|
+
color: var(--text-soft);
|
|
238
|
+
}
|
|
239
|
+
.agent-model-saving {
|
|
240
|
+
color: var(--text-faint);
|
|
241
|
+
letter-spacing: 0.14em;
|
|
242
|
+
}
|
|
243
|
+
.agent-model-saving[hidden] { display: none; }
|
|
244
|
+
.agent-model-error {
|
|
245
|
+
color: var(--red, #B5706A);
|
|
246
|
+
letter-spacing: 0.04em;
|
|
247
|
+
text-transform: none;
|
|
248
|
+
font-size: 11px;
|
|
249
|
+
flex-basis: 100%;
|
|
250
|
+
}
|
|
251
|
+
.agent-model-error[hidden] { display: none; }
|
|
252
|
+
|
|
253
|
+
/* Kick-from-room block · alert-coloured destructive action. Reads as
|
|
254
|
+
"this will remove someone" from idle state — not just on hover —
|
|
255
|
+
so the user perceives the danger BEFORE they reach for it. Uses
|
|
256
|
+
the project's `--red` token (#B5706A · the same alert tint
|
|
257
|
+
referenced in `.notes-item-unfav` and the brief-llm failure rows)
|
|
258
|
+
so the colour vocabulary matches other destructive surfaces.
|
|
259
|
+
`[data-agent-room-actions][hidden]` gates the block · the overlay
|
|
260
|
+
JS only un-hides it when the agent is kickable
|
|
261
|
+
(in-room + director + non-adjourned). */
|
|
262
|
+
.agent-room-actions[hidden] { display: none; }
|
|
263
|
+
.agent-kick-btn {
|
|
264
|
+
appearance: none;
|
|
265
|
+
display: block;
|
|
266
|
+
width: 100%;
|
|
267
|
+
background: color-mix(in srgb, var(--red, #B5706A) 6%, transparent);
|
|
268
|
+
border: 0.5px solid color-mix(in srgb, var(--red, #B5706A) 55%, var(--line-bright));
|
|
269
|
+
color: var(--red, #B5706A);
|
|
270
|
+
font-family: var(--mono);
|
|
271
|
+
font-size: 11px;
|
|
272
|
+
/* Weight 400 (was 700) · the alert tint + uppercase letter-spacing
|
|
273
|
+
already carry the "destructive" register; bold on top read as
|
|
274
|
+
shouting. Quieter weight pairs better with the calm overlay
|
|
275
|
+
surface around it. */
|
|
276
|
+
font-weight: 400;
|
|
277
|
+
letter-spacing: 0.14em;
|
|
278
|
+
text-transform: uppercase;
|
|
279
|
+
padding: 10px 14px;
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
transition: border-color 0.12s, color 0.12s, background 0.12s;
|
|
282
|
+
}
|
|
283
|
+
.agent-kick-btn:hover {
|
|
284
|
+
background: color-mix(in srgb, var(--red, #B5706A) 14%, transparent);
|
|
285
|
+
border-color: var(--red, #B5706A);
|
|
286
|
+
/* Bright white text on hover lifts the affordance from "noted
|
|
287
|
+
danger" to "click to commit". */
|
|
288
|
+
color: #FFFFFF;
|
|
289
|
+
}
|
|
290
|
+
.agent-kick-btn:active { transform: translateY(1px); }
|
|
291
|
+
.agent-kick-btn:disabled {
|
|
292
|
+
opacity: 0.45;
|
|
293
|
+
cursor: wait;
|
|
294
|
+
}
|
|
295
|
+
|
|
214
296
|
.agent-traits {
|
|
215
297
|
display: flex;
|
|
216
298
|
flex-wrap: wrap;
|
package/public/agent-overlay.js
CHANGED
|
@@ -216,10 +216,30 @@
|
|
|
216
216
|
|
|
217
217
|
<div class="agent-block agent-model-block">
|
|
218
218
|
<div class="agent-block-label" data-i18n="ao_model">Model</div>
|
|
219
|
-
<div class="agent-model-display">
|
|
219
|
+
<div class="agent-model-display agent-model-display-readonly">
|
|
220
220
|
<span class="agent-model-name"></span>
|
|
221
221
|
<span class="agent-model-provider"></span>
|
|
222
222
|
</div>
|
|
223
|
+
<!-- Interactive model picker · only mounted when window.app
|
|
224
|
+
is present (in-room view); on standalone gallery pages
|
|
225
|
+
the readonly display above shows instead. Trigger reuses
|
|
226
|
+
the agent-profile model dropdown vocabulary (ap-model-*)
|
|
227
|
+
so the in-room overlay and the full profile page share
|
|
228
|
+
ONE control treatment for model selection · same panel
|
|
229
|
+
bar, name + provider chip + caret, same popover rows. -->
|
|
230
|
+
<div class="agent-model-edit private-only" data-agent-model-edit hidden>
|
|
231
|
+
<button type="button" class="ap-model-trigger" data-agent-model-trigger>
|
|
232
|
+
<span class="ap-model-trigger-text">
|
|
233
|
+
<span class="ap-model-trigger-name" data-agent-model-value></span>
|
|
234
|
+
<span class="ap-model-trigger-provider" data-agent-model-provider-tag></span>
|
|
235
|
+
</span>
|
|
236
|
+
<span class="ap-model-trigger-caret">▾</span>
|
|
237
|
+
</button>
|
|
238
|
+
<div class="agent-model-meta">
|
|
239
|
+
<span class="agent-model-saving" data-agent-model-saving hidden data-i18n="ao_model_saving">Saving…</span>
|
|
240
|
+
<span class="agent-model-error" data-agent-model-error hidden></span>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
223
243
|
</div>
|
|
224
244
|
|
|
225
245
|
<div class="agent-block">
|
|
@@ -259,6 +279,17 @@
|
|
|
259
279
|
</div>
|
|
260
280
|
</div>
|
|
261
281
|
|
|
282
|
+
<!-- Kick-from-room block · only visible when this overlay is
|
|
283
|
+
opened from inside a live room AND the agent is a director
|
|
284
|
+
member of that room (NOT the chair · chair is structural).
|
|
285
|
+
Renders a confirm dialog before firing the PATCH so a
|
|
286
|
+
misclick can't silently boot a director mid-conversation. -->
|
|
287
|
+
<div class="agent-block agent-room-actions private-only" data-agent-room-actions hidden>
|
|
288
|
+
<button type="button" class="agent-kick-btn" data-agent-kick-btn>
|
|
289
|
+
<span data-i18n="ao_kick_button">Remove from this room</span>
|
|
290
|
+
</button>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
262
293
|
</div>
|
|
263
294
|
<footer class="agent-card-foot">
|
|
264
295
|
<div class="meta private-only"><span data-i18n="ao_tenure_meta">tenure ·</span> <span class="lime agent-tenure"></span></div>
|
|
@@ -383,22 +414,140 @@
|
|
|
383
414
|
|
|
384
415
|
// Model · resolved from the live record (catalog entries don't
|
|
385
416
|
// ship a modelV). Fall back to displaying the raw id if we don't
|
|
386
|
-
// have a friendly label for it yet.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
modelBlock
|
|
400
|
-
|
|
401
|
-
|
|
417
|
+
// have a friendly label for it yet. When `live` is present (the
|
|
418
|
+
// common case · in-room view + signed-in roster), the read-only
|
|
419
|
+
// chip swaps to an interactive `<select>` so the user can switch
|
|
420
|
+
// the director's model without leaving the conversation.
|
|
421
|
+
// Model block + kick block setup · wrapped in try/catch so a
|
|
422
|
+
// missing selector or a broken sub-call can't prevent the rest
|
|
423
|
+
// of open() from reaching `overlay.classList.add("open")`. Without
|
|
424
|
+
// this guard, ANY thrown error here would silently abort the
|
|
425
|
+
// open() flow and the user would experience "clicked the avatar
|
|
426
|
+
// but nothing happened" with no visible affordance for the bug.
|
|
427
|
+
try {
|
|
428
|
+
const modelV = live ? live.modelV : null;
|
|
429
|
+
const modelMeta = modelV ? MODEL_LABELS[modelV] : null;
|
|
430
|
+
const modelBlock = card.querySelector(".agent-model-block");
|
|
431
|
+
const modelReadonly = card.querySelector(".agent-model-display-readonly");
|
|
432
|
+
const modelEdit = card.querySelector("[data-agent-model-edit]");
|
|
433
|
+
const modelNameEl = card.querySelector(".agent-model-display-readonly .agent-model-name");
|
|
434
|
+
const modelProvEl = card.querySelector(".agent-model-display-readonly .agent-model-provider");
|
|
435
|
+
const modelTrigger = card.querySelector("[data-agent-model-trigger]");
|
|
436
|
+
const modelValueEl = card.querySelector("[data-agent-model-value]");
|
|
437
|
+
const modelProviderTagEl = card.querySelector("[data-agent-model-provider-tag]");
|
|
438
|
+
const modelSavingEl = card.querySelector("[data-agent-model-saving]");
|
|
439
|
+
const modelErrorEl = card.querySelector("[data-agent-model-error]");
|
|
440
|
+
// Reset transient state on every open so a prior save's
|
|
441
|
+
// saving/error chips don't leak into the new agent's view.
|
|
442
|
+
if (modelSavingEl) modelSavingEl.hidden = true;
|
|
443
|
+
if (modelErrorEl) { modelErrorEl.hidden = true; modelErrorEl.textContent = ""; }
|
|
444
|
+
|
|
445
|
+
if (modelBlock) {
|
|
446
|
+
if (modelMeta) {
|
|
447
|
+
if (modelNameEl) modelNameEl.textContent = modelMeta.name;
|
|
448
|
+
if (modelProvEl) modelProvEl.textContent = modelMeta.provider;
|
|
449
|
+
modelBlock.style.display = "";
|
|
450
|
+
} else if (modelV) {
|
|
451
|
+
if (modelNameEl) modelNameEl.textContent = modelV;
|
|
452
|
+
if (modelProvEl) modelProvEl.textContent = "";
|
|
453
|
+
modelBlock.style.display = "";
|
|
454
|
+
} else {
|
|
455
|
+
modelBlock.style.display = "none";
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Wire the editable picker only when this overlay is mounted in
|
|
460
|
+
// the live in-room context (window.app + live record). Otherwise
|
|
461
|
+
// keep the readonly chip · standalone gallery pages can't PATCH.
|
|
462
|
+
const canEditModel = !!(live && window.app
|
|
463
|
+
&& typeof fetch === "function"
|
|
464
|
+
&& !document.body.classList.contains("public"));
|
|
465
|
+
if (canEditModel && modelTrigger && modelEdit && modelReadonly) {
|
|
466
|
+
// Trigger · name on the left, provider chip on the right,
|
|
467
|
+
// caret last. Matches the agent-profile `.ap-model-trigger`
|
|
468
|
+
// pattern so both surfaces read as one control vocabulary.
|
|
469
|
+
if (modelValueEl) {
|
|
470
|
+
modelValueEl.textContent = modelMeta
|
|
471
|
+
? modelMeta.name
|
|
472
|
+
: (modelV || "—");
|
|
473
|
+
}
|
|
474
|
+
if (modelProviderTagEl) {
|
|
475
|
+
modelProviderTagEl.textContent = modelMeta ? modelMeta.provider : "";
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Replace any prior trigger listener (the DOM node persists
|
|
479
|
+
// across open() calls, so naïve addEventListener stacks
|
|
480
|
+
// duplicates · cloneNode wipes the listeners).
|
|
481
|
+
const newTrigger = modelTrigger.cloneNode(true);
|
|
482
|
+
modelTrigger.parentNode.replaceChild(newTrigger, modelTrigger);
|
|
483
|
+
// Re-fetch refs AFTER clone since the old references point
|
|
484
|
+
// at the detached node.
|
|
485
|
+
const triggerValEl = newTrigger.querySelector("[data-agent-model-value]");
|
|
486
|
+
const triggerProvEl = newTrigger.querySelector("[data-agent-model-provider-tag]");
|
|
487
|
+
newTrigger.addEventListener("click", (ev) => {
|
|
488
|
+
ev.stopPropagation();
|
|
489
|
+
if (newTrigger.classList.contains("open")) {
|
|
490
|
+
closeAgentModelPicker();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
openAgentModelPicker(slug, {
|
|
494
|
+
trigger: newTrigger,
|
|
495
|
+
valueEl: triggerValEl,
|
|
496
|
+
providerEl: triggerProvEl,
|
|
497
|
+
saving: modelSavingEl,
|
|
498
|
+
error: modelErrorEl,
|
|
499
|
+
readonlyName: modelNameEl,
|
|
500
|
+
readonlyProv: modelProvEl,
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Show editor, hide readonly chip.
|
|
505
|
+
modelReadonly.style.display = "none";
|
|
506
|
+
modelEdit.hidden = false;
|
|
507
|
+
} else if (modelEdit && modelReadonly) {
|
|
508
|
+
modelReadonly.style.display = "";
|
|
509
|
+
modelEdit.hidden = true;
|
|
510
|
+
}
|
|
511
|
+
} catch (e) {
|
|
512
|
+
// Don't let model-picker wiring abort the overlay open.
|
|
513
|
+
try { console.warn("[agent-overlay] model picker setup failed:", e); } catch (_) {}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Kick-from-room button · visible only when (a) we're inside a
|
|
517
|
+
// live room, (b) this agent is a director member of that room
|
|
518
|
+
// (chair excluded · the role is structural), (c) the room isn't
|
|
519
|
+
// adjourned (member changes are rejected by the server then).
|
|
520
|
+
try {
|
|
521
|
+
const kickBlock = card.querySelector("[data-agent-room-actions]");
|
|
522
|
+
const kickBtn = card.querySelector("[data-agent-kick-btn]");
|
|
523
|
+
if (kickBlock && kickBtn) {
|
|
524
|
+
const app = window.app;
|
|
525
|
+
const room = app && app.currentRoom;
|
|
526
|
+
const members = (app && app.currentMembers) || [];
|
|
527
|
+
const isMember = members.some((m) => m && m.id === slug);
|
|
528
|
+
const isChair = !!(live && live.roleKind === "moderator");
|
|
529
|
+
const adjourned = room && room.status === "adjourned";
|
|
530
|
+
const canKick = !!room && isMember && !isChair && !adjourned;
|
|
531
|
+
if (canKick) {
|
|
532
|
+
kickBlock.hidden = false;
|
|
533
|
+
kickBtn.disabled = false;
|
|
534
|
+
// Replace any prior listener to avoid stacking after re-open.
|
|
535
|
+
const newKick = kickBtn.cloneNode(true);
|
|
536
|
+
kickBtn.parentNode.replaceChild(newKick, kickBtn);
|
|
537
|
+
newKick.addEventListener("click", () => {
|
|
538
|
+
const promptKey = ovT("ao_kick_confirm", { name: a.name });
|
|
539
|
+
if (!window.confirm(promptKey)) return;
|
|
540
|
+
kickFromRoom(slug, room.id, members, {
|
|
541
|
+
btn: newKick,
|
|
542
|
+
onDone: () => close(),
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
} else {
|
|
546
|
+
kickBlock.hidden = true;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} catch (e) {
|
|
550
|
+
try { console.warn("[agent-overlay] kick button setup failed:", e); } catch (_) {}
|
|
402
551
|
}
|
|
403
552
|
|
|
404
553
|
card.querySelector(".agent-traits").innerHTML = (a.traits || [])
|
|
@@ -576,11 +725,304 @@
|
|
|
576
725
|
|
|
577
726
|
function close() {
|
|
578
727
|
overlayOpenSlug = null;
|
|
728
|
+
// Close the model picker first · it lives in document.body and
|
|
729
|
+
// wouldn't dismiss with the overlay otherwise (would leave a
|
|
730
|
+
// floating popover orphaned over the page).
|
|
731
|
+
try { closeAgentModelPicker(); } catch (_) {}
|
|
579
732
|
overlay.classList.remove("open");
|
|
580
733
|
overlay.setAttribute("aria-hidden", "true");
|
|
581
734
|
document.body.style.overflow = "";
|
|
582
735
|
}
|
|
583
736
|
|
|
737
|
+
/** Open the model picker popover · mirrors the agent-profile
|
|
738
|
+
* `.ap-model-picker` vocabulary (panel surface, hairline border,
|
|
739
|
+
* grouped by provider via `.ap-model-group` headers, `.ap-model-opt`
|
|
740
|
+
* rows with sans label + mono uppercase hint). Built fresh on each
|
|
741
|
+
* open, attached to document.body so its `position: fixed` escapes
|
|
742
|
+
* the overlay card's `overflow-y: auto` clip. z-index lifted above
|
|
743
|
+
* the overlay's 9700 via the `.ap-model-picker-overlay` modifier
|
|
744
|
+
* class so the popover floats above the modal backdrop. */
|
|
745
|
+
let _agentModelPopover = null;
|
|
746
|
+
let _agentModelPopoverOutsideHandler = null;
|
|
747
|
+
let _agentModelPopoverKeyHandler = null;
|
|
748
|
+
function closeAgentModelPicker() {
|
|
749
|
+
if (_agentModelPopover && _agentModelPopover.parentNode) {
|
|
750
|
+
_agentModelPopover.parentNode.removeChild(_agentModelPopover);
|
|
751
|
+
}
|
|
752
|
+
_agentModelPopover = null;
|
|
753
|
+
const openTrigger = card.querySelector("[data-agent-model-trigger].open");
|
|
754
|
+
if (openTrigger) openTrigger.classList.remove("open");
|
|
755
|
+
if (_agentModelPopoverOutsideHandler) {
|
|
756
|
+
document.removeEventListener("mousedown", _agentModelPopoverOutsideHandler, true);
|
|
757
|
+
_agentModelPopoverOutsideHandler = null;
|
|
758
|
+
}
|
|
759
|
+
if (_agentModelPopoverKeyHandler) {
|
|
760
|
+
document.removeEventListener("keydown", _agentModelPopoverKeyHandler, true);
|
|
761
|
+
_agentModelPopoverKeyHandler = null;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function openAgentModelPicker(slug, ui) {
|
|
765
|
+
closeAgentModelPicker();
|
|
766
|
+
const live = window.app && window.app.agentsById
|
|
767
|
+
? window.app.agentsById[slug]
|
|
768
|
+
: null;
|
|
769
|
+
if (!live) return;
|
|
770
|
+
const currentV = live.modelV || "";
|
|
771
|
+
|
|
772
|
+
// Group reachable models by provider (server filters by the
|
|
773
|
+
// user's active credential, so only that credential's family
|
|
774
|
+
// shows up). Falls back to the full MODEL_LABELS catalog only
|
|
775
|
+
// when the /api/models cache hasn't loaded yet — short cold-
|
|
776
|
+
// start window; cache lands in ~50ms.
|
|
777
|
+
const cache = (typeof window.boardroomModels === "function")
|
|
778
|
+
? window.boardroomModels()
|
|
779
|
+
: null;
|
|
780
|
+
const reachable = (cache && Array.isArray(cache.reachable) && cache.reachable.length > 0)
|
|
781
|
+
? cache.reachable
|
|
782
|
+
: null;
|
|
783
|
+
|
|
784
|
+
const byProvider = new Map();
|
|
785
|
+
if (reachable) {
|
|
786
|
+
for (const m of reachable) {
|
|
787
|
+
const meta = MODEL_LABELS[m.modelV];
|
|
788
|
+
const providerName = meta ? meta.provider : m.provider;
|
|
789
|
+
const displayName = meta ? meta.name : m.displayName;
|
|
790
|
+
if (!byProvider.has(providerName)) byProvider.set(providerName, []);
|
|
791
|
+
byProvider.get(providerName).push({ v: m.modelV, name: displayName });
|
|
792
|
+
}
|
|
793
|
+
} else {
|
|
794
|
+
for (const v of Object.keys(MODEL_LABELS)) {
|
|
795
|
+
const meta = MODEL_LABELS[v];
|
|
796
|
+
if (!byProvider.has(meta.provider)) byProvider.set(meta.provider, []);
|
|
797
|
+
byProvider.get(meta.provider).push({ v, name: meta.name });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// If the agent's currently-stored modelV isn't in the reachable
|
|
802
|
+
// set (active credential just switched, or registry retired the
|
|
803
|
+
// model), prepend a "current (unreachable)" entry so the user
|
|
804
|
+
// can still see what their agent is on — and pick a different
|
|
805
|
+
// reachable model to swap to.
|
|
806
|
+
const reachableHas = reachable
|
|
807
|
+
? reachable.some((m) => m.modelV === currentV)
|
|
808
|
+
: !!MODEL_LABELS[currentV];
|
|
809
|
+
let unknownRow = "";
|
|
810
|
+
if (currentV && !reachableHas) {
|
|
811
|
+
const meta = MODEL_LABELS[currentV];
|
|
812
|
+
const label = meta ? meta.name : currentV;
|
|
813
|
+
const hint = meta ? "unreachable · pick a model below" : "unrecognised id";
|
|
814
|
+
unknownRow =
|
|
815
|
+
`<div class="ap-model-group">Current (unreachable)</div>` +
|
|
816
|
+
`<button type="button" class="ap-model-opt active" data-agent-model-pick="${escapeHtml(currentV)}" disabled>` +
|
|
817
|
+
`<span class="ap-model-opt-label">${escapeHtml(label)}</span>` +
|
|
818
|
+
`<span class="ap-model-opt-hint">${escapeHtml(hint)}</span>` +
|
|
819
|
+
`</button>`;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const rows = [];
|
|
823
|
+
for (const [provider, items] of byProvider.entries()) {
|
|
824
|
+
rows.push(`<div class="ap-model-group">${escapeHtml(provider)}</div>`);
|
|
825
|
+
for (const it of items) {
|
|
826
|
+
const isActive = it.v === currentV;
|
|
827
|
+
rows.push(
|
|
828
|
+
`<button type="button" class="ap-model-opt${isActive ? " active" : ""}" data-agent-model-pick="${escapeHtml(it.v)}">` +
|
|
829
|
+
`<span class="ap-model-opt-label">${escapeHtml(it.name)}</span>` +
|
|
830
|
+
`<span class="ap-model-opt-hint">${escapeHtml(it.v)}</span>` +
|
|
831
|
+
`</button>`,
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Empty-reachable safety · cache loaded but no models accessible
|
|
837
|
+
// (e.g. xAI-only active provider with empty registry rows).
|
|
838
|
+
if (rows.length === 0 && !unknownRow) {
|
|
839
|
+
rows.push(
|
|
840
|
+
`<div class="ap-model-picker-loading">No models reachable with your current API key. Configure another provider in Preferences.</div>`,
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const pop = document.createElement("div");
|
|
845
|
+
pop.className = "ap-model-picker ap-model-picker-overlay";
|
|
846
|
+
pop.setAttribute("role", "listbox");
|
|
847
|
+
pop.innerHTML = unknownRow + rows.join("");
|
|
848
|
+
document.body.appendChild(pop);
|
|
849
|
+
|
|
850
|
+
// Position · anchored under the trigger, left edge aligned with
|
|
851
|
+
// the trigger's left edge. Pop width matches trigger width with
|
|
852
|
+
// a sane minimum so even short triggers get a usable menu.
|
|
853
|
+
const r = ui.trigger.getBoundingClientRect();
|
|
854
|
+
const popMinWidth = 240;
|
|
855
|
+
const popWidth = Math.max(r.width, popMinWidth);
|
|
856
|
+
const vw = window.innerWidth || document.documentElement.clientWidth;
|
|
857
|
+
const vh = window.innerHeight || document.documentElement.clientHeight;
|
|
858
|
+
let popLeft = r.left;
|
|
859
|
+
// Don't run off the right edge of the viewport.
|
|
860
|
+
if (popLeft + popWidth > vw - 8) popLeft = Math.max(8, vw - popWidth - 8);
|
|
861
|
+
let popTop = r.bottom + 4;
|
|
862
|
+
pop.style.minWidth = popWidth + "px";
|
|
863
|
+
pop.style.left = popLeft + "px";
|
|
864
|
+
pop.style.top = popTop + "px";
|
|
865
|
+
// After mount the pop has measured height; if it would overflow
|
|
866
|
+
// the viewport bottom, flip it ABOVE the trigger instead.
|
|
867
|
+
const popH = pop.getBoundingClientRect().height;
|
|
868
|
+
if (popTop + popH > vh - 8) {
|
|
869
|
+
popTop = Math.max(8, r.top - popH - 4);
|
|
870
|
+
pop.style.top = popTop + "px";
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
ui.trigger.classList.add("open");
|
|
874
|
+
_agentModelPopover = pop;
|
|
875
|
+
|
|
876
|
+
// Row pick · save through the existing fetch helper, swap chip /
|
|
877
|
+
// trigger value on success, close popover regardless of outcome
|
|
878
|
+
// (errors surface inline below the trigger via `ui.error`).
|
|
879
|
+
pop.addEventListener("click", (ev) => {
|
|
880
|
+
const btn = ev.target.closest("[data-agent-model-pick]");
|
|
881
|
+
if (!btn) return;
|
|
882
|
+
const v = btn.getAttribute("data-agent-model-pick");
|
|
883
|
+
if (!v) return;
|
|
884
|
+
// Optimistic UI · update trigger label + provider chip
|
|
885
|
+
// immediately so the user gets feedback even before the
|
|
886
|
+
// PATCH lands.
|
|
887
|
+
const meta = MODEL_LABELS[v];
|
|
888
|
+
if (ui.valueEl) ui.valueEl.textContent = meta ? meta.name : v;
|
|
889
|
+
if (ui.providerEl) ui.providerEl.textContent = meta ? meta.provider : "";
|
|
890
|
+
closeAgentModelPicker();
|
|
891
|
+
if (v === currentV) return; // no-op pick
|
|
892
|
+
saveModelForAgent(slug, v, {
|
|
893
|
+
select: null, // legacy field · saveModelForAgent only uses it to disable
|
|
894
|
+
saving: ui.saving,
|
|
895
|
+
error: ui.error,
|
|
896
|
+
providerTag: ui.providerEl,
|
|
897
|
+
readonlyName: ui.readonlyName,
|
|
898
|
+
readonlyProv: ui.readonlyProv,
|
|
899
|
+
previous: currentV,
|
|
900
|
+
// On error revert the trigger label + provider chip back to
|
|
901
|
+
// the previous model so the visible state matches what the
|
|
902
|
+
// server actually has.
|
|
903
|
+
onError: () => {
|
|
904
|
+
const prevMeta = MODEL_LABELS[currentV];
|
|
905
|
+
if (ui.valueEl) ui.valueEl.textContent = prevMeta ? prevMeta.name : (currentV || "—");
|
|
906
|
+
if (ui.providerEl) ui.providerEl.textContent = prevMeta ? prevMeta.provider : "";
|
|
907
|
+
},
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
// Outside-click + Esc close · use capture phase so the popover
|
|
912
|
+
// beats other click handlers (e.g. agent-overlay's own backdrop
|
|
913
|
+
// dismiss) when the user clicks elsewhere with the popover open.
|
|
914
|
+
_agentModelPopoverOutsideHandler = (ev) => {
|
|
915
|
+
if (pop.contains(ev.target) || ui.trigger.contains(ev.target)) return;
|
|
916
|
+
closeAgentModelPicker();
|
|
917
|
+
};
|
|
918
|
+
_agentModelPopoverKeyHandler = (ev) => {
|
|
919
|
+
if (ev.key === "Escape") {
|
|
920
|
+
ev.stopPropagation();
|
|
921
|
+
closeAgentModelPicker();
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
// defer attachment a tick so the click that opened us doesn't
|
|
925
|
+
// immediately close it via the outside-click handler.
|
|
926
|
+
setTimeout(() => {
|
|
927
|
+
document.addEventListener("mousedown", _agentModelPopoverOutsideHandler, true);
|
|
928
|
+
document.addEventListener("keydown", _agentModelPopoverKeyHandler, true);
|
|
929
|
+
}, 0);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** Persist a new modelV for the agent · PATCH /api/agents/:slug
|
|
933
|
+
* with `{ modelV }`. On success, update the live roster in place
|
|
934
|
+
* and refresh the readonly chip so a re-open shows the new model
|
|
935
|
+
* without a network round-trip. On failure, revert the select
|
|
936
|
+
* back to the previous value + surface the server error inline
|
|
937
|
+
* (mirrors the inline-error pattern from agent-profile.js so
|
|
938
|
+
* users don't have to dismiss an alert just to retry). */
|
|
939
|
+
function saveModelForAgent(slug, v, ui) {
|
|
940
|
+
const live = window.app && window.app.agentsById
|
|
941
|
+
? window.app.agentsById[slug]
|
|
942
|
+
: null;
|
|
943
|
+
if (!live) return;
|
|
944
|
+
if (ui.saving) ui.saving.hidden = false;
|
|
945
|
+
if (ui.error) { ui.error.hidden = true; ui.error.textContent = ""; }
|
|
946
|
+
if (ui.select) ui.select.disabled = true;
|
|
947
|
+
fetch("/api/agents/" + encodeURIComponent(slug), {
|
|
948
|
+
method: "PATCH",
|
|
949
|
+
headers: { "content-type": "application/json" },
|
|
950
|
+
body: JSON.stringify({ modelV: v }),
|
|
951
|
+
})
|
|
952
|
+
.then(async (r) => {
|
|
953
|
+
if (r.ok) return r.json();
|
|
954
|
+
const j = await r.json().catch(() => ({}));
|
|
955
|
+
const detail = j && typeof j.error === "string"
|
|
956
|
+
? j.error
|
|
957
|
+
: ("HTTP " + r.status);
|
|
958
|
+
throw new Error(detail);
|
|
959
|
+
})
|
|
960
|
+
.then((updated) => {
|
|
961
|
+
// Reflect the saved modelV in the in-memory roster so other
|
|
962
|
+
// surfaces (agent profile, sidebar badges) read the new value.
|
|
963
|
+
live.modelV = updated.modelV || v;
|
|
964
|
+
const meta = MODEL_LABELS[live.modelV];
|
|
965
|
+
if (ui.readonlyName) ui.readonlyName.textContent = meta ? meta.name : live.modelV;
|
|
966
|
+
if (ui.readonlyProv) ui.readonlyProv.textContent = meta ? meta.provider : "";
|
|
967
|
+
if (ui.providerTag) ui.providerTag.textContent = meta ? meta.provider : "";
|
|
968
|
+
if (typeof window.app.refreshAgents === "function") {
|
|
969
|
+
window.app.refreshAgents().catch(() => {});
|
|
970
|
+
}
|
|
971
|
+
})
|
|
972
|
+
.catch((e) => {
|
|
973
|
+
// Revert visible state to the previous value so what the
|
|
974
|
+
// user sees matches what the server has, then surface the
|
|
975
|
+
// error so they can retry.
|
|
976
|
+
if (ui.select && ui.previous) ui.select.value = ui.previous;
|
|
977
|
+
if (typeof ui.onError === "function") ui.onError(e);
|
|
978
|
+
if (ui.error) {
|
|
979
|
+
ui.error.hidden = false;
|
|
980
|
+
ui.error.textContent = (e && e.message ? e.message : String(e));
|
|
981
|
+
}
|
|
982
|
+
})
|
|
983
|
+
.finally(() => {
|
|
984
|
+
if (ui.saving) ui.saving.hidden = true;
|
|
985
|
+
if (ui.select) ui.select.disabled = false;
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** Remove an agent from the current room. PATCH
|
|
990
|
+
* /api/rooms/:roomId/members with the desired-state list (all
|
|
991
|
+
* current director ids MINUS this one). The endpoint diffs against
|
|
992
|
+
* current state, fires a chair farewell, and re-emits config-event
|
|
993
|
+
* so other clients see the change. On failure, alert the user with
|
|
994
|
+
* the server's error string (most likely "room must keep at least
|
|
995
|
+
* one director"). */
|
|
996
|
+
function kickFromRoom(slug, roomId, members, opts) {
|
|
997
|
+
const remaining = (members || [])
|
|
998
|
+
.filter((m) => m && m.id && m.id !== slug)
|
|
999
|
+
.map((m) => m.id);
|
|
1000
|
+
if (opts.btn) opts.btn.disabled = true;
|
|
1001
|
+
fetch("/api/rooms/" + encodeURIComponent(roomId) + "/members", {
|
|
1002
|
+
method: "PATCH",
|
|
1003
|
+
headers: { "content-type": "application/json" },
|
|
1004
|
+
body: JSON.stringify({ agentIds: remaining }),
|
|
1005
|
+
})
|
|
1006
|
+
.then(async (r) => {
|
|
1007
|
+
if (r.ok) return r.json();
|
|
1008
|
+
const j = await r.json().catch(() => ({}));
|
|
1009
|
+
const detail = j && typeof j.error === "string"
|
|
1010
|
+
? j.error
|
|
1011
|
+
: ("HTTP " + r.status);
|
|
1012
|
+
throw new Error(detail);
|
|
1013
|
+
})
|
|
1014
|
+
.then(() => {
|
|
1015
|
+
// Server's config-event SSE will trigger app.js to patch
|
|
1016
|
+
// currentMembers + re-render the queue / cast. Close the
|
|
1017
|
+
// overlay so the user sees the updated room state.
|
|
1018
|
+
if (typeof opts.onDone === "function") opts.onDone();
|
|
1019
|
+
})
|
|
1020
|
+
.catch((e) => {
|
|
1021
|
+
window.alert(ovT("ao_kick_failed", { detail: (e && e.message ? e.message : String(e)) }));
|
|
1022
|
+
if (opts.btn) opts.btn.disabled = false;
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
584
1026
|
// Public surface · other modules (e.g. agent-profile.js's voice
|
|
585
1027
|
// unlock CTA) need to dismiss the overlay before opening their
|
|
586
1028
|
// own modal, so we expose `close` and an `isOpen` predicate.
|