@data-club/ai-hub 0.0.1 → 0.0.3
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/auth/locales/cs.d.ts +16 -0
- package/dist/auth/locales/cs.d.ts.map +1 -0
- package/dist/auth/locales/cs.js +394 -0
- package/dist/auth/locales/cs.js.map +1 -0
- package/dist/auth/locales/index.d.ts +24 -0
- package/dist/auth/locales/index.d.ts.map +1 -0
- package/dist/auth/locales/index.js +31 -0
- package/dist/auth/locales/index.js.map +1 -0
- package/dist/auth/locales/sk.d.ts +16 -0
- package/dist/auth/locales/sk.d.ts.map +1 -0
- package/dist/auth/locales/sk.js +394 -0
- package/dist/auth/locales/sk.js.map +1 -0
- package/dist/auth/texts.d.ts +24 -2
- package/dist/auth/texts.d.ts.map +1 -1
- package/dist/auth/texts.js +20 -7
- package/dist/auth/texts.js.map +1 -1
- package/dist/client/AIHubClient.d.ts +11 -0
- package/dist/client/AIHubClient.d.ts.map +1 -1
- package/dist/client/AIHubClient.js +10 -0
- package/dist/client/AIHubClient.js.map +1 -1
- package/dist/components/AgentsPicker/index.d.ts.map +1 -1
- package/dist/components/AgentsPicker/index.js +3 -2
- package/dist/components/AgentsPicker/index.js.map +1 -1
- package/dist/components/ChangePassword/index.d.ts.map +1 -1
- package/dist/components/ChangePassword/index.js +3 -2
- package/dist/components/ChangePassword/index.js.map +1 -1
- package/dist/components/ChatShell/ShellHeader.d.ts.map +1 -1
- package/dist/components/ChatShell/ShellHeader.js +4 -3
- package/dist/components/ChatShell/ShellHeader.js.map +1 -1
- package/dist/components/ChatShell/UserMenu.d.ts.map +1 -1
- package/dist/components/ChatShell/UserMenu.js +13 -1
- package/dist/components/ChatShell/UserMenu.js.map +1 -1
- package/dist/components/ChatShell/index.d.ts.map +1 -1
- package/dist/components/ChatShell/index.js +3 -2
- package/dist/components/ChatShell/index.js.map +1 -1
- package/dist/components/DataClubAIHub/index.d.ts +7 -1
- package/dist/components/DataClubAIHub/index.d.ts.map +1 -1
- package/dist/components/DataClubAIHub/index.js +3 -1
- package/dist/components/DataClubAIHub/index.js.map +1 -1
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.d.ts.map +1 -1
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.js +5 -23
- package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.js.map +1 -1
- package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts.map +1 -1
- package/dist/components/DataClubAIHubAdmin/SettingsPage.js +45 -16
- package/dist/components/DataClubAIHubAdmin/SettingsPage.js.map +1 -1
- package/dist/components/DataClubAIHubAdmin/index.d.ts.map +1 -1
- package/dist/components/DataClubAIHubAdmin/index.js +3 -2
- package/dist/components/DataClubAIHubAdmin/index.js.map +1 -1
- package/dist/components/DataClubAIHubLogin/index.d.ts.map +1 -1
- package/dist/components/DataClubAIHubLogin/index.js +3 -2
- package/dist/components/DataClubAIHubLogin/index.js.map +1 -1
- package/dist/components/EffortControl/index.d.ts.map +1 -1
- package/dist/components/EffortControl/index.js +3 -2
- package/dist/components/EffortControl/index.js.map +1 -1
- package/dist/components/LanguageSwitcher/index.d.ts +25 -0
- package/dist/components/LanguageSwitcher/index.d.ts.map +1 -0
- package/dist/components/LanguageSwitcher/index.js +73 -0
- package/dist/components/LanguageSwitcher/index.js.map +1 -0
- package/dist/components/MainColumn/index.d.ts.map +1 -1
- package/dist/components/MainColumn/index.js +3 -2
- package/dist/components/MainColumn/index.js.map +1 -1
- package/dist/components/MessageInput/index.d.ts.map +1 -1
- package/dist/components/MessageInput/index.js +3 -2
- package/dist/components/MessageInput/index.js.map +1 -1
- package/dist/components/MessageList/index.d.ts.map +1 -1
- package/dist/components/MessageList/index.js +3 -2
- package/dist/components/MessageList/index.js.map +1 -1
- package/dist/components/ReasoningView/index.d.ts.map +1 -1
- package/dist/components/ReasoningView/index.js +3 -2
- package/dist/components/ReasoningView/index.js.map +1 -1
- package/dist/components/ScrollToPresentButton/index.d.ts.map +1 -1
- package/dist/components/ScrollToPresentButton/index.js +3 -2
- package/dist/components/ScrollToPresentButton/index.js.map +1 -1
- package/dist/components/SessionsPanel/index.d.ts.map +1 -1
- package/dist/components/SessionsPanel/index.js +6 -34
- package/dist/components/SessionsPanel/index.js.map +1 -1
- package/dist/components/ThemeSwitcher/index.d.ts.map +1 -1
- package/dist/components/ThemeSwitcher/index.js +3 -2
- package/dist/components/ThemeSwitcher/index.js.map +1 -1
- package/dist/components/ToolCallView/index.d.ts.map +1 -1
- package/dist/components/ToolCallView/index.js +3 -2
- package/dist/components/ToolCallView/index.js.map +1 -1
- package/dist/components/TpmBanner/index.d.ts.map +1 -1
- package/dist/components/TpmBanner/index.js +3 -2
- package/dist/components/TpmBanner/index.js.map +1 -1
- package/dist/context/AIHubContext.d.ts +23 -0
- package/dist/context/AIHubContext.d.ts.map +1 -1
- package/dist/context/AIHubContext.js.map +1 -1
- package/dist/context/AIHubProvider.d.ts +13 -0
- package/dist/context/AIHubProvider.d.ts.map +1 -1
- package/dist/context/AIHubProvider.js +39 -2
- package/dist/context/AIHubProvider.js.map +1 -1
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +1 -0
- package/dist/context/index.js.map +1 -1
- package/dist/context/language.d.ts +44 -0
- package/dist/context/language.d.ts.map +1 -0
- package/dist/context/language.js +74 -0
- package/dist/context/language.js.map +1 -0
- package/dist/context/useAIHub.d.ts +8 -0
- package/dist/context/useAIHub.d.ts.map +1 -1
- package/dist/context/useAIHub.js +4 -0
- package/dist/context/useAIHub.js.map +1 -1
- package/dist/context/useAIHubTexts.d.ts +40 -0
- package/dist/context/useAIHubTexts.d.ts.map +1 -0
- package/dist/context/useAIHubTexts.js +52 -0
- package/dist/context/useAIHubTexts.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/primitives/index.d.ts +3 -0
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +3 -0
- package/dist/primitives/index.js.map +1 -1
- package/dist/stories/_mockProvider.d.ts.map +1 -1
- package/dist/stories/_mockProvider.js +5 -0
- package/dist/stories/_mockProvider.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/util/relativeTime.d.ts +49 -0
- package/dist/util/relativeTime.d.ts.map +1 -0
- package/dist/util/relativeTime.js +78 -0
- package/dist/util/relativeTime.js.map +1 -0
- package/package.json +1 -1
- package/src/auth/locales/cs.ts +452 -0
- package/src/auth/locales/index.ts +38 -0
- package/src/auth/locales/sk.ts +452 -0
- package/src/auth/texts.ts +45 -6
- package/src/client/AIHubClient.ts +22 -0
- package/src/components/AgentsPicker/index.tsx +3 -2
- package/src/components/ChangePassword/index.tsx +2 -2
- package/src/components/ChatShell/ShellHeader.tsx +2 -0
- package/src/components/ChatShell/UserMenu.tsx +14 -1
- package/src/components/ChatShell/index.tsx +2 -2
- package/src/components/DataClubAIHub/index.tsx +11 -1
- package/src/components/DataClubAIHubAdmin/SessionsOverviewPage.tsx +5 -19
- package/src/components/DataClubAIHubAdmin/SettingsPage.tsx +138 -13
- package/src/components/DataClubAIHubAdmin/index.tsx +2 -2
- package/src/components/DataClubAIHubLogin/index.tsx +3 -2
- package/src/components/EffortControl/index.tsx +2 -2
- package/src/components/LanguageSwitcher/index.tsx +127 -0
- package/src/components/MainColumn/index.tsx +2 -2
- package/src/components/MessageInput/index.tsx +2 -2
- package/src/components/MessageList/index.tsx +2 -2
- package/src/components/ReasoningView/index.tsx +2 -2
- package/src/components/ScrollToPresentButton/index.tsx +2 -2
- package/src/components/SessionsPanel/index.tsx +5 -27
- package/src/components/ThemeSwitcher/index.tsx +3 -2
- package/src/components/ToolCallView/index.tsx +2 -2
- package/src/components/TpmBanner/index.tsx +2 -2
- package/src/context/AIHubContext.ts +25 -0
- package/src/context/AIHubProvider.tsx +68 -1
- package/src/context/index.ts +1 -0
- package/src/context/language.ts +81 -0
- package/src/context/useAIHub.ts +12 -0
- package/src/context/useAIHubTexts.ts +64 -0
- package/src/index.ts +13 -0
- package/src/primitives/index.ts +3 -0
- package/src/stories/_mockProvider.tsx +5 -0
- package/src/styles/default.css +79 -0
- package/src/types.ts +14 -0
- package/src/util/relativeTime.ts +90 -0
|
@@ -30,10 +30,11 @@
|
|
|
30
30
|
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
31
31
|
|
|
32
32
|
import {
|
|
33
|
-
resolveAIHubTexts,
|
|
34
33
|
type AIHubTexts,
|
|
35
34
|
type DeepPartial,
|
|
36
35
|
} from "../../auth/texts.js";
|
|
36
|
+
import { useResolvedTexts } from "../../context/useAIHubTexts.js";
|
|
37
|
+
import { formatRelativeTime } from "../../util/relativeTime.js";
|
|
37
38
|
import { useAIHub } from "../../context/useAIHub.js";
|
|
38
39
|
import type { Session } from "../../types.js";
|
|
39
40
|
|
|
@@ -125,30 +126,6 @@ export interface SessionsPanelProps {
|
|
|
125
126
|
texts?: DeepPartial<AIHubTexts>;
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
/**
|
|
129
|
-
* Formats an ISO-8601 timestamp into a short "N min ago" / "yesterday" /
|
|
130
|
-
* "N days ago" line. Intentionally minimal — i18n libraries can take this
|
|
131
|
-
* over later via the `texts.*` map if needed.
|
|
132
|
-
*
|
|
133
|
-
* @param iso - ISO timestamp.
|
|
134
|
-
* @returns the short relative-time string.
|
|
135
|
-
*/
|
|
136
|
-
function formatRelative(iso: string): string {
|
|
137
|
-
const then = Date.parse(iso);
|
|
138
|
-
if (Number.isNaN(then)) return "";
|
|
139
|
-
const diffMs = Date.now() - then;
|
|
140
|
-
const minutes = Math.round(diffMs / 60_000);
|
|
141
|
-
if (minutes < 1) return "just now";
|
|
142
|
-
if (minutes === 1) return "1 min ago";
|
|
143
|
-
if (minutes < 60) return `${minutes} min ago`;
|
|
144
|
-
const hours = Math.round(minutes / 60);
|
|
145
|
-
if (hours === 1) return "1 hr ago";
|
|
146
|
-
if (hours < 24) return `${hours} hr ago`;
|
|
147
|
-
const days = Math.round(hours / 24);
|
|
148
|
-
if (days === 1) return "yesterday";
|
|
149
|
-
return `${days} days ago`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
129
|
/**
|
|
153
130
|
* Renders the sessions panel.
|
|
154
131
|
*
|
|
@@ -165,8 +142,9 @@ export function SessionsPanel(props: SessionsPanelProps): ReactNode {
|
|
|
165
142
|
patchSession,
|
|
166
143
|
includeArchivedSessions,
|
|
167
144
|
setIncludeArchivedSessions,
|
|
145
|
+
language,
|
|
168
146
|
} = useAIHub();
|
|
169
|
-
const texts =
|
|
147
|
+
const texts = useResolvedTexts(props.texts);
|
|
170
148
|
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
171
149
|
const showCreate = props.showCreateButton ?? true;
|
|
172
150
|
const collapsible = props.collapsible ?? true;
|
|
@@ -313,7 +291,7 @@ export function SessionsPanel(props: SessionsPanelProps): ReactNode {
|
|
|
313
291
|
>
|
|
314
292
|
<span>{displayTitle}</span>
|
|
315
293
|
<span className={cls.itemMeta}>
|
|
316
|
-
{
|
|
294
|
+
{formatRelativeTime(session.updated_at, language)}
|
|
317
295
|
</span>
|
|
318
296
|
</button>
|
|
319
297
|
<div className={cls.itemActions} data-testid="session-item-actions">
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
import { type ReactNode } from "react";
|
|
25
25
|
|
|
26
26
|
import type { AIHubChatTexts } from "../../auth/texts.js";
|
|
27
|
-
import {
|
|
27
|
+
import { useResolvedTexts } from "../../context/useAIHubTexts.js";
|
|
28
|
+
import { type AIHubTexts, type DeepPartial } from "../../auth/texts.js";
|
|
28
29
|
import { useAIHub } from "../../context/useAIHub.js";
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -79,7 +80,7 @@ export interface ThemeSwitcherProps {
|
|
|
79
80
|
*/
|
|
80
81
|
export function ThemeSwitcher(props: ThemeSwitcherProps): ReactNode {
|
|
81
82
|
const { theme, setTheme, themeSwitcherEnabled } = useAIHub();
|
|
82
|
-
const texts =
|
|
83
|
+
const texts = useResolvedTexts(props.texts);
|
|
83
84
|
const chatTexts: AIHubChatTexts = texts.chat;
|
|
84
85
|
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
85
86
|
const showLabel = props.showLabel ?? true;
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
import { useState, type ReactNode } from "react";
|
|
21
21
|
|
|
22
22
|
import {
|
|
23
|
-
resolveAIHubTexts,
|
|
24
23
|
type AIHubTexts,
|
|
25
24
|
type DeepPartial,
|
|
26
25
|
} from "../../auth/texts.js";
|
|
26
|
+
import { useResolvedTexts } from "../../context/useAIHubTexts.js";
|
|
27
27
|
import { formatToolContentForDisplay } from "./formatContent.js";
|
|
28
28
|
|
|
29
29
|
/** Status of a tool call. */
|
|
@@ -182,7 +182,7 @@ function statusText(
|
|
|
182
182
|
* @returns the rendered card.
|
|
183
183
|
*/
|
|
184
184
|
export function ToolCallView(props: ToolCallViewProps): ReactNode {
|
|
185
|
-
const texts =
|
|
185
|
+
const texts = useResolvedTexts(props.texts);
|
|
186
186
|
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
187
187
|
const [uncontrolledExpanded, setUncontrolledExpanded] = useState(
|
|
188
188
|
props.defaultExpanded ?? false,
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
import { useEffect, useState, type ReactNode } from "react";
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
|
-
resolveAIHubTexts,
|
|
30
29
|
type AIHubTexts,
|
|
31
30
|
type DeepPartial,
|
|
32
31
|
} from "../../auth/texts.js";
|
|
32
|
+
import { useResolvedTexts } from "../../context/useAIHubTexts.js";
|
|
33
33
|
import { useAIHub } from "../../context/useAIHub.js";
|
|
34
34
|
|
|
35
35
|
import { SystemNotice } from "../SystemNotice/index.js";
|
|
@@ -90,7 +90,7 @@ export interface TpmBannerProps {
|
|
|
90
90
|
*/
|
|
91
91
|
export function TpmBanner(props: TpmBannerProps): ReactNode {
|
|
92
92
|
const { state, lastTurnError } = useAIHub();
|
|
93
|
-
const texts =
|
|
93
|
+
const texts = useResolvedTexts(props.texts);
|
|
94
94
|
|
|
95
95
|
// Tick at 1Hz while we're in the wait phase. The interval is
|
|
96
96
|
// re-installed when the deadline changes (new wait) or removed when
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
PatchSessionOptions,
|
|
19
19
|
} from "../client/AIHubClient.js";
|
|
20
20
|
import type { AIHubEffort } from "../client/AIHubClient.js";
|
|
21
|
+
import type { AIHubLanguage, AIHubTexts } from "../auth/texts.js";
|
|
21
22
|
import type {
|
|
22
23
|
AIHubAuthContext,
|
|
23
24
|
AIHubFeatureToggles,
|
|
@@ -219,6 +220,30 @@ export interface AIHubContextValue {
|
|
|
219
220
|
*/
|
|
220
221
|
themeSwitcherEnabled: boolean;
|
|
221
222
|
|
|
223
|
+
/* ── language / i18n ────────────────────────────────────────────── */
|
|
224
|
+
/**
|
|
225
|
+
* Resolved UI language (`en` | `cs` | `sk`) — computed per the same
|
|
226
|
+
* precedence as theme: when switching is unlocked, the user's cached
|
|
227
|
+
* choice wins; otherwise the deployment default. Drives `texts`.
|
|
228
|
+
*/
|
|
229
|
+
language: AIHubLanguage;
|
|
230
|
+
/**
|
|
231
|
+
* Switches the UI language and caches the choice on this device. No-op
|
|
232
|
+
* (and the switcher is hidden) when `languageSwitcherEnabled` is false.
|
|
233
|
+
*/
|
|
234
|
+
setLanguage: (next: AIHubLanguage) => void;
|
|
235
|
+
/**
|
|
236
|
+
* Mirror of `!deployment.user_language_locked`. When false, the
|
|
237
|
+
* `<LanguageSwitcher />` renders nothing and every user sees the default.
|
|
238
|
+
*/
|
|
239
|
+
languageSwitcherEnabled: boolean;
|
|
240
|
+
/**
|
|
241
|
+
* The fully-resolved, language-aware `AIHubTexts` — the selected built-in
|
|
242
|
+
* locale with any wrapper `texts` override merged on top. Components read
|
|
243
|
+
* this via `useAIHubTexts()` so a `setLanguage` re-renders the whole tree.
|
|
244
|
+
*/
|
|
245
|
+
texts: AIHubTexts;
|
|
246
|
+
|
|
222
247
|
/* ── feature toggles (§8.4, wired in Step 10) ──────────────────── */
|
|
223
248
|
/**
|
|
224
249
|
* The deployment-wide UI feature toggles fetched from public
|
|
@@ -57,6 +57,14 @@ import type {
|
|
|
57
57
|
Session,
|
|
58
58
|
} from "../types.js";
|
|
59
59
|
|
|
60
|
+
import {
|
|
61
|
+
resolveAIHubTexts,
|
|
62
|
+
type AIHubLanguage,
|
|
63
|
+
type AIHubTexts,
|
|
64
|
+
type DeepPartial,
|
|
65
|
+
} from "../auth/texts.js";
|
|
66
|
+
import { AIHUB_LOCALES } from "../auth/locales/index.js";
|
|
67
|
+
|
|
60
68
|
import { aiHubContext, type AIHubContextValue } from "./AIHubContext.js";
|
|
61
69
|
import {
|
|
62
70
|
readCachedTheme,
|
|
@@ -64,6 +72,11 @@ import {
|
|
|
64
72
|
writeCachedTheme,
|
|
65
73
|
type ResolvedTheme,
|
|
66
74
|
} from "./theme.js";
|
|
75
|
+
import {
|
|
76
|
+
readCachedLanguage,
|
|
77
|
+
resolveLanguage,
|
|
78
|
+
writeCachedLanguage,
|
|
79
|
+
} from "./language.js";
|
|
67
80
|
|
|
68
81
|
/**
|
|
69
82
|
* When the post-stream canonicalize refetches messages from the server,
|
|
@@ -154,6 +167,18 @@ export interface AIHubProviderProps {
|
|
|
154
167
|
* is empty. Server config (`theme_default`) wins after hydration.
|
|
155
168
|
*/
|
|
156
169
|
defaultTheme?: ResolvedTheme;
|
|
170
|
+
/**
|
|
171
|
+
* Fallback UI language used when no `/auth/me` has resolved yet and the
|
|
172
|
+
* cache is empty. Server config (`language_default`) wins after hydration.
|
|
173
|
+
* Defaults to `"en"`.
|
|
174
|
+
*/
|
|
175
|
+
defaultLanguage?: AIHubLanguage;
|
|
176
|
+
/**
|
|
177
|
+
* Wrapper-supplied `texts` override, merged on top of the selected
|
|
178
|
+
* built-in locale (`en` | `cs` | `sk`). Any key the wrapper sets wins over
|
|
179
|
+
* the locale; omitted keys fall back to the locale, then English.
|
|
180
|
+
*/
|
|
181
|
+
texts?: DeepPartial<AIHubTexts>;
|
|
157
182
|
/** The tree under the provider. */
|
|
158
183
|
children: ReactNode;
|
|
159
184
|
}
|
|
@@ -171,6 +196,8 @@ export function AIHubProvider(props: AIHubProviderProps): ReactNode {
|
|
|
171
196
|
children,
|
|
172
197
|
client: clientProp,
|
|
173
198
|
defaultTheme = "light",
|
|
199
|
+
defaultLanguage = "en",
|
|
200
|
+
texts: textsOverride,
|
|
174
201
|
fetch: fetchProp,
|
|
175
202
|
onError,
|
|
176
203
|
tokenStorage = "localStorage",
|
|
@@ -207,6 +234,12 @@ export function AIHubProvider(props: AIHubProviderProps): ReactNode {
|
|
|
207
234
|
const [theme, setThemeState] = useState<ResolvedTheme>(() => {
|
|
208
235
|
return readCachedTheme() ?? defaultTheme;
|
|
209
236
|
});
|
|
237
|
+
// ── language / i18n ──────────────────────────────────────────────────
|
|
238
|
+
// Seed from the device cache (FOUC-free first paint); reconciled against
|
|
239
|
+
// the deployment default once /auth/me resolves (see effect below).
|
|
240
|
+
const [language, setLanguageState] = useState<AIHubLanguage>(() => {
|
|
241
|
+
return readCachedLanguage() ?? defaultLanguage;
|
|
242
|
+
});
|
|
210
243
|
|
|
211
244
|
// ── chat state (Step 9) ─────────────────────────────────────────────
|
|
212
245
|
const [agents, setAgents] = useState<AgentSummary[]>([]);
|
|
@@ -381,6 +414,21 @@ export function AIHubProvider(props: AIHubProviderProps): ReactNode {
|
|
|
381
414
|
writeCachedTheme(resolved);
|
|
382
415
|
}, [user, deployment, defaultTheme]);
|
|
383
416
|
|
|
417
|
+
// ── reconcile resolved language once the deployment default lands ──
|
|
418
|
+
// Seed-only: when switching is unlocked the device-cached choice wins and
|
|
419
|
+
// we keep it; when locked we force the deployment default WITHOUT touching
|
|
420
|
+
// the cache, so unlocking later restores the user's prior choice.
|
|
421
|
+
useEffect(() => {
|
|
422
|
+
if (!deployment) return;
|
|
423
|
+
const resolved = resolveLanguage(
|
|
424
|
+
readCachedLanguage(),
|
|
425
|
+
deployment,
|
|
426
|
+
defaultLanguage,
|
|
427
|
+
);
|
|
428
|
+
setLanguageState(resolved);
|
|
429
|
+
if (!deployment.user_language_locked) writeCachedLanguage(resolved);
|
|
430
|
+
}, [deployment, defaultLanguage]);
|
|
431
|
+
|
|
384
432
|
// ── load /agents whenever an authenticated user becomes available ───
|
|
385
433
|
const refreshAgents = useCallback(async (): Promise<void> => {
|
|
386
434
|
setAgentsLoading(true);
|
|
@@ -526,6 +574,14 @@ export function AIHubProvider(props: AIHubProviderProps): ReactNode {
|
|
|
526
574
|
}
|
|
527
575
|
};
|
|
528
576
|
|
|
577
|
+
// Language is a device-local preference (no server round-trip): set state
|
|
578
|
+
// and cache it. No-op when the deployment has locked the language.
|
|
579
|
+
const setLanguage: AIHubContextValue["setLanguage"] = (next) => {
|
|
580
|
+
if (deployment?.user_language_locked) return;
|
|
581
|
+
setLanguageState(next);
|
|
582
|
+
writeCachedLanguage(next);
|
|
583
|
+
};
|
|
584
|
+
|
|
529
585
|
// ── chat actions ───────────────────────────────────────────────────
|
|
530
586
|
const setActiveAgent: AIHubContextValue["setActiveAgent"] = (id) => {
|
|
531
587
|
streamRef.current?.controller.abort();
|
|
@@ -1003,6 +1059,13 @@ export function AIHubProvider(props: AIHubProviderProps): ReactNode {
|
|
|
1003
1059
|
};
|
|
1004
1060
|
|
|
1005
1061
|
const themeSwitcherEnabled = deployment?.theme_switcher_enabled ?? true;
|
|
1062
|
+
const languageSwitcherEnabled = !(deployment?.user_language_locked ?? false);
|
|
1063
|
+
// Resolve language-aware texts: the selected built-in locale with the
|
|
1064
|
+
// wrapper override merged on top. Recomputes when `language` flips.
|
|
1065
|
+
const texts: AIHubTexts = useMemo(
|
|
1066
|
+
() => resolveAIHubTexts(textsOverride, AIHUB_LOCALES[language]),
|
|
1067
|
+
[textsOverride, language],
|
|
1068
|
+
);
|
|
1006
1069
|
const role: AIHubRole | null = user?.role ?? null;
|
|
1007
1070
|
|
|
1008
1071
|
// Refresh the feature toggles on demand — used after the super-admin
|
|
@@ -1039,6 +1102,10 @@ export function AIHubProvider(props: AIHubProviderProps): ReactNode {
|
|
|
1039
1102
|
theme,
|
|
1040
1103
|
setTheme,
|
|
1041
1104
|
themeSwitcherEnabled,
|
|
1105
|
+
language,
|
|
1106
|
+
setLanguage,
|
|
1107
|
+
languageSwitcherEnabled,
|
|
1108
|
+
texts,
|
|
1042
1109
|
agents,
|
|
1043
1110
|
agentsLoading,
|
|
1044
1111
|
refreshAgents,
|
|
@@ -1074,7 +1141,7 @@ export function AIHubProvider(props: AIHubProviderProps): ReactNode {
|
|
|
1074
1141
|
|
|
1075
1142
|
return (
|
|
1076
1143
|
<aiHubContext.Provider value={value}>
|
|
1077
|
-
<div className="data-club-ai-hub" data-theme={theme}>
|
|
1144
|
+
<div className="data-club-ai-hub" data-theme={theme} lang={language}>
|
|
1078
1145
|
{children}
|
|
1079
1146
|
</div>
|
|
1080
1147
|
</aiHubContext.Provider>
|
package/src/context/index.ts
CHANGED
|
@@ -7,3 +7,4 @@
|
|
|
7
7
|
export { AIHubProvider, type AIHubProviderProps } from "./AIHubProvider.js";
|
|
8
8
|
export { useAIHub, type UseAIHubResult } from "./useAIHub.js";
|
|
9
9
|
export { useAIHubAuth, type UseAIHubAuthResult } from "./useAIHubAuth.js";
|
|
10
|
+
export { useAIHubTexts, useResolvedTexts } from "./useAIHubTexts.js";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language resolution helpers used by `<AIHubProvider>` — the i18n analogue
|
|
3
|
+
* of `theme.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Precedence (mirrors §7.6 theming):
|
|
6
|
+
*
|
|
7
|
+
* 1. When `deployment.user_language_locked` is true → `deployment.language_default`
|
|
8
|
+
* (the user's cached choice is ignored and the switcher is hidden).
|
|
9
|
+
* 2. Otherwise the value cached in `localStorage` (the user's last pick on
|
|
10
|
+
* this device), then `deployment.language_default`, then `fallback`.
|
|
11
|
+
*
|
|
12
|
+
* The cache also gives a FOUC-free first paint: before `/auth/me` resolves we
|
|
13
|
+
* render in the last-known language rather than flashing English.
|
|
14
|
+
*
|
|
15
|
+
* @see ./theme.ts — the theme counterpart this mirrors
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { AIHubLanguage } from "../auth/texts.js";
|
|
19
|
+
import type { AIHubDeployment } from "../types.js";
|
|
20
|
+
|
|
21
|
+
/** localStorage key for the last-chosen language. Namespaced to ai-hub. */
|
|
22
|
+
const CACHE_KEY = "data-club-ai-hub:language";
|
|
23
|
+
|
|
24
|
+
/** The languages the library ships built-in locales for. */
|
|
25
|
+
const KNOWN: ReadonlyArray<AIHubLanguage> = ["en", "cs", "sk"];
|
|
26
|
+
|
|
27
|
+
/** Type guard: is `value` one of the known language codes? */
|
|
28
|
+
function isLanguage(value: unknown): value is AIHubLanguage {
|
|
29
|
+
return typeof value === "string" && (KNOWN as string[]).includes(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Computes the resolved language from the `/auth/me` deployment context, the
|
|
34
|
+
* cached user choice, and a fallback.
|
|
35
|
+
*
|
|
36
|
+
* @param cached - the user's cached choice (or null).
|
|
37
|
+
* @param deployment - deployment context, or null when unauthenticated.
|
|
38
|
+
* @param fallback - default applied when nothing else resolves (usually `"en"`).
|
|
39
|
+
* @returns the resolved language.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveLanguage(
|
|
42
|
+
cached: AIHubLanguage | null,
|
|
43
|
+
deployment: AIHubDeployment | null,
|
|
44
|
+
fallback: AIHubLanguage,
|
|
45
|
+
): AIHubLanguage {
|
|
46
|
+
const deploymentDefault = deployment?.language_default ?? fallback;
|
|
47
|
+
if (deployment?.user_language_locked) return deploymentDefault;
|
|
48
|
+
return cached ?? deploymentDefault;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Reads the cached language from `localStorage`. Returns null when no cache
|
|
53
|
+
* exists, the value is malformed, or storage is unavailable (Safari private
|
|
54
|
+
* mode, SSR, etc.).
|
|
55
|
+
*
|
|
56
|
+
* @returns the cached language or null.
|
|
57
|
+
*/
|
|
58
|
+
export function readCachedLanguage(): AIHubLanguage | null {
|
|
59
|
+
try {
|
|
60
|
+
if (typeof window === "undefined" || !window.localStorage) return null;
|
|
61
|
+
const raw = window.localStorage.getItem(CACHE_KEY);
|
|
62
|
+
return isLanguage(raw) ? raw : null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Persists the chosen language into `localStorage` for FOUC avoidance on the
|
|
70
|
+
* next page load. Silent no-op on storage failures.
|
|
71
|
+
*
|
|
72
|
+
* @param language - the language to cache.
|
|
73
|
+
*/
|
|
74
|
+
export function writeCachedLanguage(language: AIHubLanguage): void {
|
|
75
|
+
try {
|
|
76
|
+
if (typeof window === "undefined" || !window.localStorage) return;
|
|
77
|
+
window.localStorage.setItem(CACHE_KEY, language);
|
|
78
|
+
} catch {
|
|
79
|
+
/* ignore */
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/context/useAIHub.ts
CHANGED
|
@@ -36,6 +36,14 @@ export interface UseAIHubResult {
|
|
|
36
36
|
setTheme: AIHubContextValue["setTheme"];
|
|
37
37
|
/** Mirror of `deployment.theme_switcher_enabled`. */
|
|
38
38
|
themeSwitcherEnabled: AIHubContextValue["themeSwitcherEnabled"];
|
|
39
|
+
/** Resolved UI language (`en` | `cs` | `sk`). */
|
|
40
|
+
language: AIHubContextValue["language"];
|
|
41
|
+
/** Switches the UI language (device-cached). No-op when locked. */
|
|
42
|
+
setLanguage: AIHubContextValue["setLanguage"];
|
|
43
|
+
/** Mirror of `!deployment.user_language_locked`. */
|
|
44
|
+
languageSwitcherEnabled: AIHubContextValue["languageSwitcherEnabled"];
|
|
45
|
+
/** Fully-resolved, language-aware texts. Prefer `useAIHubTexts()`. */
|
|
46
|
+
texts: AIHubContextValue["texts"];
|
|
39
47
|
/** Non-archived agents available to the caller. */
|
|
40
48
|
agents: AIHubContextValue["agents"];
|
|
41
49
|
/** True while the initial `/agents` request is in flight. */
|
|
@@ -120,6 +128,10 @@ export function useAIHub(): UseAIHubResult {
|
|
|
120
128
|
theme: ctx.theme,
|
|
121
129
|
setTheme: ctx.setTheme,
|
|
122
130
|
themeSwitcherEnabled: ctx.themeSwitcherEnabled,
|
|
131
|
+
language: ctx.language,
|
|
132
|
+
setLanguage: ctx.setLanguage,
|
|
133
|
+
languageSwitcherEnabled: ctx.languageSwitcherEnabled,
|
|
134
|
+
texts: ctx.texts,
|
|
123
135
|
agents: ctx.agents,
|
|
124
136
|
agentsLoading: ctx.agentsLoading,
|
|
125
137
|
refreshAgents: ctx.refreshAgents,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useAIHubTexts()` — returns the fully-resolved, language-aware
|
|
3
|
+
* {@link AIHubTexts} from the nearest `<AIHubProvider>`.
|
|
4
|
+
*
|
|
5
|
+
* This is the supported way for built-in screens to read user-facing
|
|
6
|
+
* strings: the provider has already merged the active built-in locale
|
|
7
|
+
* (`en` | `cs` | `sk`) with any wrapper `texts` override, so flipping the
|
|
8
|
+
* language via `setLanguage` re-renders every consumer.
|
|
9
|
+
*
|
|
10
|
+
* @see ./AIHubProvider.tsx — where `texts` is computed
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useContext, useMemo } from "react";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
defaultAIHubTexts,
|
|
17
|
+
resolveAIHubTexts,
|
|
18
|
+
type AIHubTexts,
|
|
19
|
+
type DeepPartial,
|
|
20
|
+
} from "../auth/texts.js";
|
|
21
|
+
import { aiHubContext } from "./AIHubContext.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns the resolved texts map.
|
|
25
|
+
*
|
|
26
|
+
* Throws when called outside a provider — surfaces mounting bugs early
|
|
27
|
+
* instead of cascading through context-null deref.
|
|
28
|
+
*
|
|
29
|
+
* @returns the resolved `AIHubTexts`.
|
|
30
|
+
*/
|
|
31
|
+
export function useAIHubTexts(): AIHubTexts {
|
|
32
|
+
const ctx = useContext(aiHubContext);
|
|
33
|
+
if (!ctx) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"useAIHubTexts() was called outside <AIHubProvider>. Wrap the tree in <AIHubProvider> first.",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return ctx.texts;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolves a component's optional `texts` override against the **active
|
|
43
|
+
* language locale** from the nearest `<AIHubProvider>` (the supported way
|
|
44
|
+
* for built-in components to be language-aware).
|
|
45
|
+
*
|
|
46
|
+
* - Inside a provider: the provider's already-resolved, language-aware
|
|
47
|
+
* `texts` is the base; the component's own `override` (if any) is merged on
|
|
48
|
+
* top. Flipping `setLanguage` re-renders the provider, so every consumer
|
|
49
|
+
* re-resolves automatically.
|
|
50
|
+
* - Outside a provider (standalone component / Storybook / tests): falls back
|
|
51
|
+
* to the English `defaultAIHubTexts` as the base, preserving the original
|
|
52
|
+
* "merge over English defaults" behavior.
|
|
53
|
+
*
|
|
54
|
+
* @param override - the component's wrapper-supplied partial (usually
|
|
55
|
+
* `props.texts`). `undefined` is fine.
|
|
56
|
+
* @returns the fully resolved `AIHubTexts`.
|
|
57
|
+
*/
|
|
58
|
+
export function useResolvedTexts(
|
|
59
|
+
override: DeepPartial<AIHubTexts> | undefined,
|
|
60
|
+
): AIHubTexts {
|
|
61
|
+
const ctx = useContext(aiHubContext);
|
|
62
|
+
const base = ctx?.texts ?? defaultAIHubTexts;
|
|
63
|
+
return useMemo(() => resolveAIHubTexts(override, base), [override, base]);
|
|
64
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -90,6 +90,8 @@ export {
|
|
|
90
90
|
AIHubProvider,
|
|
91
91
|
useAIHub,
|
|
92
92
|
useAIHubAuth,
|
|
93
|
+
useAIHubTexts,
|
|
94
|
+
useResolvedTexts,
|
|
93
95
|
} from "./context/index.js";
|
|
94
96
|
export type {
|
|
95
97
|
AIHubProviderProps,
|
|
@@ -205,6 +207,8 @@ export type {
|
|
|
205
207
|
ThemeSwitcherClassNames,
|
|
206
208
|
} from "./components/ThemeSwitcher/index.js";
|
|
207
209
|
|
|
210
|
+
export { LanguageSwitcher } from "./components/LanguageSwitcher/index.js";
|
|
211
|
+
|
|
208
212
|
export { SystemNotice } from "./components/SystemNotice/index.js";
|
|
209
213
|
export type {
|
|
210
214
|
SystemNoticeProps,
|
|
@@ -249,6 +253,7 @@ export {
|
|
|
249
253
|
} from "./auth/texts.js";
|
|
250
254
|
export type {
|
|
251
255
|
AIHubTexts,
|
|
256
|
+
AIHubLanguage,
|
|
252
257
|
AIHubLoginTexts,
|
|
253
258
|
AIHubChangePasswordTexts,
|
|
254
259
|
AIHubChatTexts,
|
|
@@ -256,6 +261,14 @@ export type {
|
|
|
256
261
|
DeepPartial,
|
|
257
262
|
} from "./auth/texts.js";
|
|
258
263
|
|
|
264
|
+
/* Built-in locales (Czech / Slovak) + the switcher's language metadata. */
|
|
265
|
+
export {
|
|
266
|
+
AIHUB_LOCALES,
|
|
267
|
+
AIHUB_LANGUAGE_OPTIONS,
|
|
268
|
+
csTexts,
|
|
269
|
+
skTexts,
|
|
270
|
+
} from "./auth/locales/index.js";
|
|
271
|
+
|
|
259
272
|
/* ── Password policy (client-side mirror of §8.5) ────────────────── */
|
|
260
273
|
|
|
261
274
|
export {
|
package/src/primitives/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { ReasoningView } from "../components/ReasoningView/index.js";
|
|
|
29
29
|
import { ScrollToPresentButton } from "../components/ScrollToPresentButton/index.js";
|
|
30
30
|
import { SessionsPanel } from "../components/SessionsPanel/index.js";
|
|
31
31
|
import { SystemNotice } from "../components/SystemNotice/index.js";
|
|
32
|
+
import { LanguageSwitcher } from "../components/LanguageSwitcher/index.js";
|
|
32
33
|
import { ThemeSwitcher } from "../components/ThemeSwitcher/index.js";
|
|
33
34
|
import { ToolCallView } from "../components/ToolCallView/index.js";
|
|
34
35
|
import { TpmBanner } from "../components/TpmBanner/index.js";
|
|
@@ -87,6 +88,8 @@ export const AIHub = {
|
|
|
87
88
|
TurnStatusIndicator,
|
|
88
89
|
/** Sun/moon theme toggle. */
|
|
89
90
|
ThemeSwitcher,
|
|
91
|
+
/** Globe language switcher (en/cs/sk). */
|
|
92
|
+
LanguageSwitcher,
|
|
90
93
|
/** Floating "↓ Jump to latest" button. */
|
|
91
94
|
ScrollToPresentButton,
|
|
92
95
|
/** Collapsible reasoning block inside an assistant bubble. */
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { type ReactNode } from "react";
|
|
16
16
|
|
|
17
17
|
import { aiHubContext, type AIHubContextValue } from "../context/AIHubContext.js";
|
|
18
|
+
import { defaultAIHubTexts } from "../auth/texts.js";
|
|
18
19
|
import type { AIHubEffort } from "../client/AIHubClient.js";
|
|
19
20
|
import type {
|
|
20
21
|
AIHubFeatureToggles,
|
|
@@ -82,6 +83,10 @@ export function MockProvider(props: {
|
|
|
82
83
|
theme: opts.theme ?? "light",
|
|
83
84
|
setTheme: noopAsync,
|
|
84
85
|
themeSwitcherEnabled: opts.themeSwitcherEnabled ?? true,
|
|
86
|
+
language: "en",
|
|
87
|
+
setLanguage: () => undefined,
|
|
88
|
+
languageSwitcherEnabled: true,
|
|
89
|
+
texts: defaultAIHubTexts,
|
|
85
90
|
agents: [],
|
|
86
91
|
agentsLoading: false,
|
|
87
92
|
refreshAgents: noopAsync,
|
package/src/styles/default.css
CHANGED
|
@@ -664,6 +664,85 @@
|
|
|
664
664
|
text-decoration: none;
|
|
665
665
|
}
|
|
666
666
|
|
|
667
|
+
/* ── Language switcher (left of the user menu in the shell header) ────── */
|
|
668
|
+
|
|
669
|
+
.data-club-ai-hub__language-switcher {
|
|
670
|
+
position: relative;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.data-club-ai-hub__language-switcher__button {
|
|
674
|
+
display: inline-flex;
|
|
675
|
+
align-items: center;
|
|
676
|
+
gap: var(--dc-aih-space-xs);
|
|
677
|
+
padding: var(--dc-aih-space-xs) var(--dc-aih-space-sm);
|
|
678
|
+
font: inherit;
|
|
679
|
+
color: var(--dc-aih-fg);
|
|
680
|
+
background: transparent;
|
|
681
|
+
border: 1px solid transparent;
|
|
682
|
+
border-radius: var(--dc-aih-radius-md);
|
|
683
|
+
transition: background-color var(--dc-aih-transition),
|
|
684
|
+
border-color var(--dc-aih-transition);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.data-club-ai-hub__language-switcher__button:hover {
|
|
688
|
+
background-color: color-mix(in srgb, var(--dc-aih-fg) 6%, transparent);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.data-club-ai-hub__language-switcher__button:focus-visible {
|
|
692
|
+
outline: none;
|
|
693
|
+
border-color: var(--dc-aih-accent);
|
|
694
|
+
box-shadow: 0 0 0 3px var(--dc-aih-focus-ring);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.data-club-ai-hub__language-switcher__icon {
|
|
698
|
+
font-size: var(--dc-aih-font-size-md);
|
|
699
|
+
line-height: 1;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.data-club-ai-hub__language-switcher__code {
|
|
703
|
+
font-size: var(--dc-aih-font-size-sm);
|
|
704
|
+
font-weight: 600;
|
|
705
|
+
letter-spacing: 0.03em;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.data-club-ai-hub__language-switcher__dropdown {
|
|
709
|
+
position: absolute;
|
|
710
|
+
right: 0;
|
|
711
|
+
top: calc(100% + var(--dc-aih-space-xs));
|
|
712
|
+
min-width: 160px;
|
|
713
|
+
padding: var(--dc-aih-space-sm) 0;
|
|
714
|
+
background-color: var(--dc-aih-bg-elevated);
|
|
715
|
+
border: 1px solid var(--dc-aih-border);
|
|
716
|
+
border-radius: var(--dc-aih-radius-md);
|
|
717
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
|
718
|
+
z-index: 30;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.data-club-ai-hub__language-switcher__item {
|
|
722
|
+
display: block;
|
|
723
|
+
width: 100%;
|
|
724
|
+
padding: var(--dc-aih-space-sm) var(--dc-aih-space-md);
|
|
725
|
+
font: inherit;
|
|
726
|
+
text-align: left;
|
|
727
|
+
color: var(--dc-aih-fg);
|
|
728
|
+
background: transparent;
|
|
729
|
+
border: none;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.data-club-ai-hub__language-switcher__item:hover {
|
|
733
|
+
background-color: color-mix(in srgb, var(--dc-aih-fg) 6%, transparent);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.data-club-ai-hub__language-switcher__item:focus-visible {
|
|
737
|
+
outline: none;
|
|
738
|
+
background-color: color-mix(in srgb, var(--dc-aih-accent) 12%, transparent);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.data-club-ai-hub__language-switcher__item--active {
|
|
742
|
+
font-weight: 600;
|
|
743
|
+
color: var(--dc-aih-accent);
|
|
744
|
+
}
|
|
745
|
+
|
|
667
746
|
/* ────────────────────────────────────────────────────────────────────── */
|
|
668
747
|
/* Admin shell + Users sub-page — Step 11 */
|
|
669
748
|
/* ui-sketches/04-admin-overview.md, ui-sketches/05-admin-users.md */
|
package/src/types.ts
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* @see SPECIFICATIONS.md §11 — data model
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import type { AIHubLanguage } from "./auth/texts.js";
|
|
20
|
+
|
|
19
21
|
/* ────────────────────────────────────────────────────────────────────────── */
|
|
20
22
|
/* Roles + users */
|
|
21
23
|
/* ────────────────────────────────────────────────────────────────────────── */
|
|
@@ -98,6 +100,18 @@ export interface AIHubDeployment {
|
|
|
98
100
|
* `user.theme_preference`.
|
|
99
101
|
*/
|
|
100
102
|
theme_switcher_enabled: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Default UI language for the deployment (`en` | `cs` | `sk`). Seeds a
|
|
105
|
+
* user's language when they have no per-device choice cached, and is the
|
|
106
|
+
* only language used when `user_language_locked` is true.
|
|
107
|
+
*/
|
|
108
|
+
language_default: AIHubLanguage;
|
|
109
|
+
/**
|
|
110
|
+
* When `true`, the deployment is single-language — `AIHub.LanguageSwitcher`
|
|
111
|
+
* is hidden and every user sees `language_default` regardless of any cached
|
|
112
|
+
* per-device choice.
|
|
113
|
+
*/
|
|
114
|
+
user_language_locked: boolean;
|
|
101
115
|
/**
|
|
102
116
|
* Browser-facing URL of the self-hosted Letta ADE — only present when the
|
|
103
117
|
* caller's role is `power_user` or higher. Absent for `user`-role callers.
|