@data-club/ai-hub 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/auth/locales/cs.d.ts.map +1 -1
  2. package/dist/auth/locales/cs.js +12 -0
  3. package/dist/auth/locales/cs.js.map +1 -1
  4. package/dist/auth/locales/sk.d.ts.map +1 -1
  5. package/dist/auth/locales/sk.js +12 -0
  6. package/dist/auth/locales/sk.js.map +1 -1
  7. package/dist/auth/texts.d.ts +18 -0
  8. package/dist/auth/texts.d.ts.map +1 -1
  9. package/dist/auth/texts.js +12 -0
  10. package/dist/auth/texts.js.map +1 -1
  11. package/dist/client/AIHubClient.d.ts +27 -0
  12. package/dist/client/AIHubClient.d.ts.map +1 -1
  13. package/dist/client/AIHubClient.js +18 -0
  14. package/dist/client/AIHubClient.js.map +1 -1
  15. package/dist/components/ChatShell/index.d.ts.map +1 -1
  16. package/dist/components/ChatShell/index.js +35 -2
  17. package/dist/components/ChatShell/index.js.map +1 -1
  18. package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.d.ts +4 -0
  19. package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.d.ts.map +1 -1
  20. package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.js +6 -1
  21. package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.js.map +1 -1
  22. package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.d.ts.map +1 -1
  23. package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.js +6 -0
  24. package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.js.map +1 -1
  25. package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts.map +1 -1
  26. package/dist/components/DataClubAIHubAdmin/SettingsPage.js +56 -1
  27. package/dist/components/DataClubAIHubAdmin/SettingsPage.js.map +1 -1
  28. package/dist/types.d.ts +11 -0
  29. package/dist/types.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/auth/locales/cs.ts +15 -0
  32. package/src/auth/locales/sk.ts +15 -0
  33. package/src/auth/texts.ts +35 -0
  34. package/src/client/AIHubClient.ts +41 -0
  35. package/src/components/ChatShell/index.tsx +34 -2
  36. package/src/components/DataClubAIHubAdmin/RotatingMessageEditorModal.tsx +51 -0
  37. package/src/components/DataClubAIHubAdmin/RotatingMessagesPage.tsx +6 -0
  38. package/src/components/DataClubAIHubAdmin/SettingsPage.tsx +145 -1
  39. package/src/types.ts +11 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-club/ai-hub",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "React package for the data-club ai-hub: chat UI, login + change-password, admin UI, AIHub.* primitives, defineWebComponent helper, client SDK. See docs/SPECIFICATIONS.md §7.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -340,6 +340,12 @@ export const csTexts: AIHubTexts = {
340
340
  rotatingModalCreateButton: "Vytvořit",
341
341
  rotatingModalSaveButton: "Uložit",
342
342
  rotatingModalSubmittingButton: "Ukládání…",
343
+ rotatingDisplaySectionLabel: "Zobrazené části výstupu",
344
+ rotatingDisplayHelper:
345
+ "Vyberte, které části připravené odpovědi agenta uživatelé uvidí. Skryté části se z Letty nemažou — jsou jen skryté v ai-hubu.",
346
+ rotatingDisplayNormalAlways: "Běžný výstup (vždy zobrazen)",
347
+ rotatingDisplayReasoning: "Uvažování (thinking)",
348
+ rotatingDisplayToolCalls: "Volání nástrojů",
343
349
  /* Sessions overview. */
344
350
  sessionsOverviewHeading: "Konverzace",
345
351
  sessionsOverviewSearchPlaceholder: "Hledat v názvech…",
@@ -425,6 +431,15 @@ export const csTexts: AIHubTexts = {
425
431
  settingsLanguageLockedHelper:
426
432
  "Když je zapnuto, přepínač jazyka je skrytý a všichni uživatelé vidí výchozí jazyk. Když je vypnuto, uživatelé si mohou zvolit vlastní jazyk a jejich volba se zapamatuje na jejich zařízení.",
427
433
  settingsSaveLanguageButton: "Uložit jazyk",
434
+ settingsAgentsSectionHeading: "Agenti",
435
+ settingsAgentsDefaultLabel: "Výchozí agent",
436
+ settingsAgentsDefaultNone: "Žádný — zobrazit seznam agentů",
437
+ settingsAgentsDefaultHelper:
438
+ "Když je nastaven, uživatelům se rovnou otevře tento agent místo výběru. Prostředí s jediným agentem vždy otevře tohoto jednoho agenta bez ohledu na toto nastavení.",
439
+ settingsAgentsSwitcherEnabledLabel: "Povolit uživatelům přepínat agenty",
440
+ settingsAgentsSwitcherEnabledHelper:
441
+ "Když je vypnuto, odkaz „← Agenti“ je skrytý a uživatelé zůstanou u svého agenta. Skrytý je také vždy, když existuje jen jeden agent.",
442
+ settingsSaveAgentsButton: "Uložit agenty",
428
443
  settingsTogglesSectionHeading: "Přepínače funkcí UI",
429
444
  settingsTogglesGroupSessions: "Panel konverzací",
430
445
  settingsTogglesGroupInput: "Vstup",
@@ -340,6 +340,12 @@ export const skTexts: AIHubTexts = {
340
340
  rotatingModalCreateButton: "Vytvoriť",
341
341
  rotatingModalSaveButton: "Uložiť",
342
342
  rotatingModalSubmittingButton: "Ukladanie…",
343
+ rotatingDisplaySectionLabel: "Zobrazené časti výstupu",
344
+ rotatingDisplayHelper:
345
+ "Vyberte, ktoré časti pripravenej odpovede agenta používatelia uvidia. Skryté časti sa z Letty nemažú — sú len skryté v ai-hube.",
346
+ rotatingDisplayNormalAlways: "Bežný výstup (vždy zobrazený)",
347
+ rotatingDisplayReasoning: "Uvažovanie (thinking)",
348
+ rotatingDisplayToolCalls: "Volania nástrojov",
343
349
  /* Sessions overview. */
344
350
  sessionsOverviewHeading: "Konverzácie",
345
351
  sessionsOverviewSearchPlaceholder: "Hľadať v názvoch…",
@@ -425,6 +431,15 @@ export const skTexts: AIHubTexts = {
425
431
  settingsLanguageLockedHelper:
426
432
  "Keď je zapnuté, prepínač jazyka je skrytý a všetci používatelia vidia predvolený jazyk. Keď je vypnuté, používatelia si môžu zvoliť vlastný jazyk a ich voľba sa zapamätá na ich zariadení.",
427
433
  settingsSaveLanguageButton: "Uložiť jazyk",
434
+ settingsAgentsSectionHeading: "Agenti",
435
+ settingsAgentsDefaultLabel: "Predvolený agent",
436
+ settingsAgentsDefaultNone: "Žiadny — zobraziť zoznam agentov",
437
+ settingsAgentsDefaultHelper:
438
+ "Keď je nastavený, používateľom sa rovno otvorí tento agent namiesto výberu. Prostredie s jediným agentom vždy otvorí tohto jedného agenta bez ohľadu na toto nastavenie.",
439
+ settingsAgentsSwitcherEnabledLabel: "Povoliť používateľom prepínať agentov",
440
+ settingsAgentsSwitcherEnabledHelper:
441
+ "Keď je vypnuté, odkaz „← Agenti“ je skrytý a používatelia zostanú pri svojom agentovi. Skrytý je tiež vždy, keď existuje iba jeden agent.",
442
+ settingsSaveAgentsButton: "Uložiť agentov",
428
443
  settingsTogglesSectionHeading: "Prepínače funkcií UI",
429
444
  settingsTogglesGroupSessions: "Panel konverzácií",
430
445
  settingsTogglesGroupInput: "Vstup",
package/src/auth/texts.ts CHANGED
@@ -688,6 +688,16 @@ export interface AIHubAdminTexts {
688
688
  rotatingModalSaveButton: string;
689
689
  /** In-flight submit button label. */
690
690
  rotatingModalSubmittingButton: string;
691
+ /** Heading for the "which output parts to display" group in the editor. */
692
+ rotatingDisplaySectionLabel: string;
693
+ /** Helper under the group — explains hide-not-delete. */
694
+ rotatingDisplayHelper: string;
695
+ /** Always-on row label for normal assistant text. */
696
+ rotatingDisplayNormalAlways: string;
697
+ /** Toggle label — show the agent's reasoning / thinking. */
698
+ rotatingDisplayReasoning: string;
699
+ /** Toggle label — show tool calls. */
700
+ rotatingDisplayToolCalls: string;
691
701
 
692
702
  /* ── Step 12 — Sessions overview ([07-admin-sessions-overview.md]) ─ */
693
703
  /** Page heading. */
@@ -814,6 +824,16 @@ export interface AIHubAdminTexts {
814
824
  settingsLanguageLockedHelper: string;
815
825
  settingsSaveLanguageButton: string;
816
826
 
827
+ /* Agents section. */
828
+ settingsAgentsSectionHeading: string;
829
+ settingsAgentsDefaultLabel: string;
830
+ /** First `<option>` — no default agent (show the picker). */
831
+ settingsAgentsDefaultNone: string;
832
+ settingsAgentsDefaultHelper: string;
833
+ settingsAgentsSwitcherEnabledLabel: string;
834
+ settingsAgentsSwitcherEnabledHelper: string;
835
+ settingsSaveAgentsButton: string;
836
+
817
837
  /* Feature toggles section. */
818
838
  settingsTogglesSectionHeading: string;
819
839
  settingsTogglesGroupSessions: string;
@@ -1194,6 +1214,12 @@ export const defaultAIHubTexts: AIHubTexts = Object.freeze({
1194
1214
  rotatingModalCreateButton: "Create",
1195
1215
  rotatingModalSaveButton: "Save",
1196
1216
  rotatingModalSubmittingButton: "Saving…",
1217
+ rotatingDisplaySectionLabel: "Displayed output parts",
1218
+ rotatingDisplayHelper:
1219
+ "Choose which parts of the agent's prepared reply users see. Hidden parts are not deleted from Letta — only hidden in ai-hub.",
1220
+ rotatingDisplayNormalAlways: "Normal output (always shown)",
1221
+ rotatingDisplayReasoning: "Thinking (reasoning)",
1222
+ rotatingDisplayToolCalls: "Tool calls",
1197
1223
  /* Step 12 — Sessions overview. */
1198
1224
  sessionsOverviewHeading: "Sessions",
1199
1225
  sessionsOverviewSearchPlaceholder: "Search titles…",
@@ -1280,6 +1306,15 @@ export const defaultAIHubTexts: AIHubTexts = Object.freeze({
1280
1306
  settingsLanguageLockedHelper:
1281
1307
  "When on, the language switcher is hidden and every user sees the default language. When off, users can pick their own language and their choice is remembered on their device.",
1282
1308
  settingsSaveLanguageButton: "Save language",
1309
+ settingsAgentsSectionHeading: "Agents",
1310
+ settingsAgentsDefaultLabel: "Default agent",
1311
+ settingsAgentsDefaultNone: "No default — show the agent list",
1312
+ settingsAgentsDefaultHelper:
1313
+ "When set, users open straight into this agent instead of the picker. A deployment with a single agent always opens that one regardless of this setting.",
1314
+ settingsAgentsSwitcherEnabledLabel: "Allow users to switch agents",
1315
+ settingsAgentsSwitcherEnabledHelper:
1316
+ "When off, the “← Agents” back link is hidden so users stay on their agent. It is also hidden whenever only one agent exists.",
1317
+ settingsSaveAgentsButton: "Save agents",
1283
1318
  settingsTogglesSectionHeading: "UI feature toggles",
1284
1319
  settingsTogglesGroupSessions: "Sessions panel",
1285
1320
  settingsTogglesGroupInput: "Input",
@@ -428,6 +428,14 @@ export interface AdminRotatingMessageView {
428
428
  next_rotation_at: string;
429
429
  /** Soft-hide flag. */
430
430
  is_archived: boolean;
431
+ /**
432
+ * Whether the agent's reasoning is shown in the seeded opening of sessions
433
+ * forked from this rotating message. When false, ai-hub hides it at display
434
+ * time (not deleted from Letta). Normal assistant text is always shown.
435
+ */
436
+ reasoning_visible: boolean;
437
+ /** Same, for the seeded opening's tool calls. */
438
+ tool_calls_visible: boolean;
431
439
  /** ISO-8601 timestamp. */
432
440
  created_at: string;
433
441
  /** ISO-8601 timestamp. */
@@ -446,6 +454,10 @@ export interface CreateAdminRotatingMessageBody {
446
454
  dailyTime?: string | null;
447
455
  /** Required when `scheduleKind === "cron"` (5-field cron). */
448
456
  cronExpression?: string | null;
457
+ /** Show reasoning in the seeded opening. Defaults to true server-side. */
458
+ reasoningVisible?: boolean;
459
+ /** Show tool calls in the seeded opening. Defaults to true server-side. */
460
+ toolCallsVisible?: boolean;
449
461
  }
450
462
 
451
463
  /** Body for `patchAdminRotatingMessage`. Each field independently optional. */
@@ -458,6 +470,8 @@ export interface PatchAdminRotatingMessageBody {
458
470
  dailyTime?: string | null;
459
471
  cronExpression?: string | null;
460
472
  isArchived?: boolean;
473
+ reasoningVisible?: boolean;
474
+ toolCallsVisible?: boolean;
461
475
  }
462
476
 
463
477
  /** Response from `rotateAdminRotatingMessageNow`. */
@@ -583,6 +597,13 @@ export interface AdminSettingsView {
583
597
  default: AIHubLanguage;
584
598
  user_locked: boolean;
585
599
  };
600
+ /** Agent-picker settings, stored as separate columns server-side. */
601
+ agents: {
602
+ /** Default ai-hub agent id auto-opened on load, or `null` for the picker. */
603
+ default_id: string | null;
604
+ /** Whether users may switch agents (the "← Agents" back link). */
605
+ switcher_enabled: boolean;
606
+ };
586
607
  /** The flat §8.4 feature-toggles blob. */
587
608
  feature_toggles: AIHubFeatureToggles;
588
609
  /** ISO-8601 of the last signing-key rotation. `null` until first rotation. */
@@ -611,6 +632,12 @@ export interface PatchAdminSettingsBody {
611
632
  default?: AIHubLanguage;
612
633
  userLocked?: boolean;
613
634
  };
635
+ /** Agent-picker partial — either or both fields. */
636
+ agents?: {
637
+ /** Agent id to default to; `null` / `""` clears (→ show the picker). */
638
+ defaultId?: string | null;
639
+ switcherEnabled?: boolean;
640
+ };
614
641
  }
615
642
 
616
643
  /** Response from `rotateAdminMetricsToken` — the new token returned **once**. */
@@ -1330,6 +1357,8 @@ export class AIHubClient {
1330
1357
  if (body.intervalSeconds !== undefined) wire.interval_seconds = body.intervalSeconds;
1331
1358
  if (body.dailyTime !== undefined) wire.daily_time = body.dailyTime;
1332
1359
  if (body.cronExpression !== undefined) wire.cron_expression = body.cronExpression;
1360
+ if (body.reasoningVisible !== undefined) wire.reasoning_visible = body.reasoningVisible;
1361
+ if (body.toolCallsVisible !== undefined) wire.tool_calls_visible = body.toolCallsVisible;
1333
1362
  return this.requestJson<AdminRotatingMessageView>(
1334
1363
  "POST",
1335
1364
  "/admin/rotating-messages",
@@ -1360,6 +1389,8 @@ export class AIHubClient {
1360
1389
  if (body.dailyTime !== undefined) wire.daily_time = body.dailyTime;
1361
1390
  if (body.cronExpression !== undefined) wire.cron_expression = body.cronExpression;
1362
1391
  if (body.isArchived !== undefined) wire.is_archived = body.isArchived;
1392
+ if (body.reasoningVisible !== undefined) wire.reasoning_visible = body.reasoningVisible;
1393
+ if (body.toolCallsVisible !== undefined) wire.tool_calls_visible = body.toolCallsVisible;
1363
1394
  return this.requestJson<AdminRotatingMessageView>(
1364
1395
  "PATCH",
1365
1396
  `/admin/rotating-messages/${encodeURIComponent(id)}`,
@@ -1505,6 +1536,16 @@ export class AIHubClient {
1505
1536
  }
1506
1537
  wire.language = languageWire;
1507
1538
  }
1539
+ if (body.agents !== undefined) {
1540
+ const agentsWire: Record<string, unknown> = {};
1541
+ if (body.agents.defaultId !== undefined) {
1542
+ agentsWire.default_id = body.agents.defaultId;
1543
+ }
1544
+ if (body.agents.switcherEnabled !== undefined) {
1545
+ agentsWire.switcher_enabled = body.agents.switcherEnabled;
1546
+ }
1547
+ wire.agents = agentsWire;
1548
+ }
1508
1549
  return this.requestJson<AdminSettingsView>("PATCH", "/admin/settings", {
1509
1550
  body: wire,
1510
1551
  });
@@ -13,7 +13,7 @@
13
13
  * @see ui-sketches/00-shell.md, ui-sketches/03-chat.md
14
14
  */
15
15
 
16
- import { useEffect, useMemo, type ReactNode } from "react";
16
+ import { useEffect, useMemo, useRef, type ReactNode } from "react";
17
17
 
18
18
  import {
19
19
  type AIHubTexts,
@@ -21,6 +21,7 @@ import {
21
21
  } from "../../auth/texts.js";
22
22
  import { useResolvedTexts } from "../../context/useAIHubTexts.js";
23
23
  import { useAIHub } from "../../context/useAIHub.js";
24
+ import { useAIHubAuth } from "../../context/useAIHubAuth.js";
24
25
  import {
25
26
  resolveAffordanceFlags,
26
27
  type AIHubAffordanceFlags,
@@ -171,9 +172,13 @@ export function ChatShell(props: ChatShellProps): ReactNode {
171
172
  setActiveSession,
172
173
  featureToggles,
173
174
  } = useAIHub();
175
+ const { deployment } = useAIHubAuth();
174
176
  const texts = useResolvedTexts(props.texts);
175
177
  const productName = props.productName ?? "ai-hub";
176
178
  const showAgentsPicker = props.showAgentsPicker ?? true;
179
+ // §7.x agent-picker deployment config: when switching is off the "← Agents"
180
+ // back link is hidden; a single-agent deployment hides it too (see below).
181
+ const agentSwitcherEnabled = deployment?.agent_switcher_enabled ?? true;
177
182
  const sessionsPanelPosition = props.sessionsPanelPosition ?? "left";
178
183
  const sessionsPanelWidthPercent = props.sessionsPanelWidthPercent ?? 25;
179
184
 
@@ -214,6 +219,31 @@ export function ChatShell(props: ChatShellProps): ReactNode {
214
219
  }
215
220
  }, [autoSelectOnlyAgent, agents, setActiveAgent]);
216
221
 
222
+ // Deployment default agent (multi-agent deployments): open it once on first
223
+ // agents-load instead of showing the picker. A deep-link `initialAgentId`
224
+ // wins (handled by its own effect above). Guarded by a ref so that, when
225
+ // switching is enabled, clicking "← Agents" genuinely returns to the picker
226
+ // rather than snapping straight back to the default.
227
+ const appliedDefaultAgentRef = useRef(false);
228
+ useEffect(() => {
229
+ if (appliedDefaultAgentRef.current) return;
230
+ if (props.initialAgentId) return;
231
+ if (agentsLoading || agents.length <= 1) return;
232
+ appliedDefaultAgentRef.current = true;
233
+ if (activeAgentId) return;
234
+ const defaultId = deployment?.default_agent_id ?? null;
235
+ if (defaultId && agents.some((a) => a.id === defaultId)) {
236
+ setActiveAgent(defaultId);
237
+ }
238
+ }, [
239
+ agents,
240
+ agentsLoading,
241
+ activeAgentId,
242
+ props.initialAgentId,
243
+ deployment,
244
+ setActiveAgent,
245
+ ]);
246
+
217
247
  const inPicker = showAgentsPicker && !activeAgentId;
218
248
 
219
249
  const rootClass = props.className
@@ -259,7 +289,9 @@ export function ChatShell(props: ChatShellProps): ReactNode {
259
289
  />
260
290
  ) : null}
261
291
  <MainColumn
262
- showBackToAgents={showAgentsPicker}
292
+ showBackToAgents={
293
+ showAgentsPicker && agentSwitcherEnabled && agents.length > 1
294
+ }
263
295
  showTurnStatus={flags.turnStatusVisible}
264
296
  {...(props.texts !== undefined ? { texts: props.texts } : {})}
265
297
  {...(props.customHeaderContent !== undefined
@@ -33,6 +33,8 @@ export interface RotatingEditorInitial {
33
33
  intervalSeconds: number | null;
34
34
  dailyTime: string | null;
35
35
  cronExpression: string | null;
36
+ reasoningVisible: boolean;
37
+ toolCallsVisible: boolean;
36
38
  }
37
39
 
38
40
  /** Submit body passed up to the parent page. */
@@ -44,6 +46,8 @@ export interface RotatingEditorSubmitBody {
44
46
  intervalSeconds: number | null;
45
47
  dailyTime: string | null;
46
48
  cronExpression: string | null;
49
+ reasoningVisible: boolean;
50
+ toolCallsVisible: boolean;
47
51
  }
48
52
 
49
53
  /** Props for `<RotatingMessageEditorModal />`. */
@@ -90,6 +94,13 @@ export function RotatingMessageEditorModal(
90
94
  const [intervalUnit, setIntervalUnit] = useState<"hours" | "days">(initialInterval.unit);
91
95
  const [dailyTime, setDailyTime] = useState(props.initial?.dailyTime ?? "06:00");
92
96
  const [cronExpression, setCronExpression] = useState(props.initial?.cronExpression ?? "0 6 * * *");
97
+ // Which output parts the seeded opening shows. Normal text is always shown.
98
+ const [reasoningVisible, setReasoningVisible] = useState(
99
+ props.initial?.reasoningVisible ?? true,
100
+ );
101
+ const [toolCallsVisible, setToolCallsVisible] = useState(
102
+ props.initial?.toolCallsVisible ?? true,
103
+ );
93
104
 
94
105
  const [nameError, setNameError] = useState<string | null>(null);
95
106
  const [promptError, setPromptError] = useState<string | null>(null);
@@ -172,6 +183,8 @@ export function RotatingMessageEditorModal(
172
183
  prompt: trimmedPrompt,
173
184
  lettaAgentId: trimmedAgent,
174
185
  ...schedule.body,
186
+ reasoningVisible,
187
+ toolCallsVisible,
175
188
  });
176
189
  } catch (err) {
177
190
  if (err instanceof AIHubRequestError) {
@@ -377,6 +390,44 @@ export function RotatingMessageEditorModal(
377
390
  ) : null}
378
391
  </fieldset>
379
392
 
393
+ <fieldset className="data-club-ai-hub-admin__radio-group">
394
+ <legend className="data-club-ai-hub-admin__form-label">
395
+ {props.texts.rotatingDisplaySectionLabel}
396
+ </legend>
397
+ <label className="data-club-ai-hub-admin__checkbox">
398
+ <input
399
+ type="checkbox"
400
+ checked
401
+ disabled
402
+ data-testid="admin-rotating-display-normal"
403
+ />
404
+ <span>{props.texts.rotatingDisplayNormalAlways}</span>
405
+ </label>
406
+ <label className="data-club-ai-hub-admin__checkbox">
407
+ <input
408
+ type="checkbox"
409
+ data-testid="admin-rotating-display-reasoning"
410
+ checked={reasoningVisible}
411
+ onChange={(event) => setReasoningVisible(event.target.checked)}
412
+ disabled={submitting}
413
+ />
414
+ <span>{props.texts.rotatingDisplayReasoning}</span>
415
+ </label>
416
+ <label className="data-club-ai-hub-admin__checkbox">
417
+ <input
418
+ type="checkbox"
419
+ data-testid="admin-rotating-display-tool-calls"
420
+ checked={toolCallsVisible}
421
+ onChange={(event) => setToolCallsVisible(event.target.checked)}
422
+ disabled={submitting}
423
+ />
424
+ <span>{props.texts.rotatingDisplayToolCalls}</span>
425
+ </label>
426
+ <span className="data-club-ai-hub-admin__form-helper">
427
+ {props.texts.rotatingDisplayHelper}
428
+ </span>
429
+ </fieldset>
430
+
380
431
  {banner !== null ? (
381
432
  <div className="data-club-ai-hub-admin__error" role="alert" data-testid="admin-rotating-modal-banner">
382
433
  {banner}
@@ -135,6 +135,8 @@ export function AdminRotatingMessagesPage(
135
135
  intervalSeconds: body.intervalSeconds,
136
136
  dailyTime: body.dailyTime,
137
137
  cronExpression: body.cronExpression,
138
+ reasoningVisible: body.reasoningVisible,
139
+ toolCallsVisible: body.toolCallsVisible,
138
140
  });
139
141
  upsert(created);
140
142
  setEditor({ kind: "closed" });
@@ -152,6 +154,8 @@ export function AdminRotatingMessagesPage(
152
154
  intervalSeconds: body.intervalSeconds,
153
155
  dailyTime: body.dailyTime,
154
156
  cronExpression: body.cronExpression,
157
+ reasoningVisible: body.reasoningVisible,
158
+ toolCallsVisible: body.toolCallsVisible,
155
159
  });
156
160
  upsert(updated);
157
161
  setEditor({ kind: "closed" });
@@ -362,6 +366,8 @@ export function AdminRotatingMessagesPage(
362
366
  intervalSeconds: editor.row.interval_seconds,
363
367
  dailyTime: editor.row.daily_time,
364
368
  cronExpression: editor.row.cron_expression,
369
+ reasoningVisible: editor.row.reasoning_visible,
370
+ toolCallsVisible: editor.row.tool_calls_visible,
365
371
  }}
366
372
  texts={props.texts}
367
373
  onSubmit={(body) => handleEditSubmit(editor.row, body)}
@@ -27,7 +27,10 @@
27
27
  import { useEffect, useState, type ReactNode } from "react";
28
28
 
29
29
  import type { AIHubAdminTexts, AIHubLanguage } from "../../auth/texts.js";
30
- import type { AdminSettingsView } from "../../client/AIHubClient.js";
30
+ import type {
31
+ AdminAgentView,
32
+ AdminSettingsView,
33
+ } from "../../client/AIHubClient.js";
31
34
  import { useAIHub } from "../../context/useAIHub.js";
32
35
  import { useAIHubAuth } from "../../context/useAIHubAuth.js";
33
36
  import {
@@ -188,6 +191,11 @@ export function AdminSettingsPage(props: AdminSettingsPageProps): ReactNode {
188
191
  texts={props.texts}
189
192
  onSaved={handleSectionSaved}
190
193
  />
194
+ <AgentsSection
195
+ settings={settings}
196
+ texts={props.texts}
197
+ onSaved={handleSectionSaved}
198
+ />
191
199
  <FeatureTogglesSection
192
200
  settings={settings}
193
201
  texts={props.texts}
@@ -646,6 +654,142 @@ function LanguageSection(props: LanguageSectionProps): ReactNode {
646
654
  );
647
655
  }
648
656
 
657
+ /* ────────────────────────────────────────────────────────────────────────── */
658
+ /* Section: Agents */
659
+ /* ────────────────────────────────────────────────────────────────────────── */
660
+
661
+ interface AgentsSectionProps extends SectionProps {
662
+ onSaved: (next: AdminSettingsView) => void;
663
+ }
664
+
665
+ function AgentsSection(props: AgentsSectionProps): ReactNode {
666
+ const { client } = useAIHub();
667
+ const [defaultAgentId, setDefaultAgentId] = useState<string>(
668
+ props.settings.agents.default_id ?? "",
669
+ );
670
+ const [switcherEnabled, setSwitcherEnabled] = useState(
671
+ props.settings.agents.switcher_enabled,
672
+ );
673
+ // Non-archived agents to populate the default-agent <select>.
674
+ const [agents, setAgents] = useState<AdminAgentView[] | null>(null);
675
+ const [saving, setSaving] = useState(false);
676
+ const [flash, setFlash] = useState<{ tone: "success" | "error"; text: string } | null>(
677
+ null,
678
+ );
679
+
680
+ useEffect(() => {
681
+ let cancelled = false;
682
+ void (async () => {
683
+ try {
684
+ const rows = await client.listAdminAgents();
685
+ if (cancelled) return;
686
+ setAgents(rows.filter((a) => !a.is_archived));
687
+ } catch {
688
+ if (!cancelled) setAgents([]);
689
+ }
690
+ })();
691
+ return () => {
692
+ cancelled = true;
693
+ };
694
+ }, [client]);
695
+
696
+ async function handleSave(event: React.FormEvent): Promise<void> {
697
+ event.preventDefault();
698
+ setSaving(true);
699
+ setFlash(null);
700
+ try {
701
+ const next = await client.patchAdminSettings({
702
+ agents: { defaultId: defaultAgentId, switcherEnabled },
703
+ });
704
+ props.onSaved(next);
705
+ setFlash({ tone: "success", text: props.texts.settingsSaveSuccessFlash });
706
+ } catch (err) {
707
+ setFlash({
708
+ tone: "error",
709
+ text:
710
+ err instanceof Error && err.message.length > 0
711
+ ? err.message
712
+ : props.texts.settingsSaveGenericError,
713
+ });
714
+ } finally {
715
+ setSaving(false);
716
+ }
717
+ }
718
+
719
+ return (
720
+ <section
721
+ className="data-club-ai-hub-admin__settings-section"
722
+ data-testid="admin-settings-agents-section"
723
+ >
724
+ <h2 className="data-club-ai-hub-admin__settings-section-heading">
725
+ {props.texts.settingsAgentsSectionHeading}
726
+ </h2>
727
+ <form onSubmit={(event) => void handleSave(event)} noValidate>
728
+ <label
729
+ className="data-club-ai-hub-admin__form-label"
730
+ htmlFor="admin-settings-default-agent"
731
+ >
732
+ {props.texts.settingsAgentsDefaultLabel}
733
+ </label>
734
+ <select
735
+ id="admin-settings-default-agent"
736
+ className="data-club-ai-hub-admin__form-input"
737
+ data-testid="admin-settings-default-agent"
738
+ value={defaultAgentId}
739
+ onChange={(event) => setDefaultAgentId(event.target.value)}
740
+ disabled={saving || agents === null}
741
+ >
742
+ <option value="">{props.texts.settingsAgentsDefaultNone}</option>
743
+ {(agents ?? []).map((agent) => (
744
+ <option key={agent.id} value={agent.id}>
745
+ {agent.display_name}
746
+ </option>
747
+ ))}
748
+ </select>
749
+ <p className="data-club-ai-hub-admin__form-helper">
750
+ {props.texts.settingsAgentsDefaultHelper}
751
+ </p>
752
+ <label className="data-club-ai-hub-admin__checkbox">
753
+ <input
754
+ type="checkbox"
755
+ data-testid="admin-settings-agent-switcher-enabled"
756
+ checked={switcherEnabled}
757
+ onChange={(event) => setSwitcherEnabled(event.target.checked)}
758
+ disabled={saving}
759
+ />
760
+ <span>{props.texts.settingsAgentsSwitcherEnabledLabel}</span>
761
+ </label>
762
+ <p className="data-club-ai-hub-admin__form-helper">
763
+ {props.texts.settingsAgentsSwitcherEnabledHelper}
764
+ </p>
765
+ {flash !== null ? (
766
+ <div
767
+ className={
768
+ flash.tone === "success"
769
+ ? "data-club-ai-hub-admin__flash data-club-ai-hub-admin__flash--success"
770
+ : "data-club-ai-hub-admin__flash data-club-ai-hub-admin__flash--error"
771
+ }
772
+ data-testid="admin-settings-agents-flash"
773
+ role={flash.tone === "error" ? "alert" : "status"}
774
+ >
775
+ {flash.text}
776
+ </div>
777
+ ) : null}
778
+ <button
779
+ type="submit"
780
+ className="data-club-ai-hub-admin__button data-club-ai-hub-admin__button--primary"
781
+ data-testid="admin-settings-agents-save"
782
+ disabled={saving}
783
+ >
784
+ {saving
785
+ ? props.texts.settingsSaveSavingButton
786
+ : props.texts.settingsSaveAgentsButton}
787
+ </button>
788
+ </form>
789
+ </section>
790
+ );
791
+ }
792
+
649
793
  /* ────────────────────────────────────────────────────────────────────────── */
650
794
  /* Section: Feature toggles */
651
795
  /* ────────────────────────────────────────────────────────────────────────── */
package/src/types.ts CHANGED
@@ -112,6 +112,17 @@ export interface AIHubDeployment {
112
112
  * per-device choice.
113
113
  */
114
114
  user_language_locked: boolean;
115
+ /**
116
+ * ai-hub agent id auto-opened on load (skipping the agents picker). `null` =
117
+ * no default → the picker is shown. Ignored when only one agent exists (that
118
+ * one always opens). A dangling id falls back to the picker.
119
+ */
120
+ default_agent_id: string | null;
121
+ /**
122
+ * When `false`, users can't switch agents — the "← Agents" back link is
123
+ * hidden. (Also hidden whenever only one agent exists, regardless of this.)
124
+ */
125
+ agent_switcher_enabled: boolean;
115
126
  /**
116
127
  * Browser-facing URL of the self-hosted Letta ADE — only present when the
117
128
  * caller's role is `power_user` or higher. Absent for `user`-role callers.