@data-club/ai-hub 0.0.2 → 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/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/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
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
import { useEffect, useMemo, type ReactNode } from "react";
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
|
-
resolveAIHubTexts,
|
|
20
19
|
type AIHubTexts,
|
|
21
20
|
type DeepPartial,
|
|
22
21
|
} from "../../auth/texts.js";
|
|
22
|
+
import { useResolvedTexts } from "../../context/useAIHubTexts.js";
|
|
23
23
|
import { useAIHub } from "../../context/useAIHub.js";
|
|
24
24
|
import {
|
|
25
25
|
resolveAffordanceFlags,
|
|
@@ -171,7 +171,7 @@ export function ChatShell(props: ChatShellProps): ReactNode {
|
|
|
171
171
|
setActiveSession,
|
|
172
172
|
featureToggles,
|
|
173
173
|
} = useAIHub();
|
|
174
|
-
const texts =
|
|
174
|
+
const texts = useResolvedTexts(props.texts);
|
|
175
175
|
const productName = props.productName ?? "ai-hub";
|
|
176
176
|
const showAgentsPicker = props.showAgentsPicker ?? true;
|
|
177
177
|
const sessionsPanelPosition = props.sessionsPanelPosition ?? "left";
|
|
@@ -40,7 +40,7 @@ import type {
|
|
|
40
40
|
import type {
|
|
41
41
|
AIHubChangePasswordClassNames,
|
|
42
42
|
} from "../ChangePassword/index.js";
|
|
43
|
-
import type { AIHubTexts, DeepPartial } from "../../auth/texts.js";
|
|
43
|
+
import type { AIHubLanguage, AIHubTexts, DeepPartial } from "../../auth/texts.js";
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Props for `<DataClubAIHub />`.
|
|
@@ -66,6 +66,12 @@ export interface DataClubAIHubProps {
|
|
|
66
66
|
usernamePlaceholder?: string;
|
|
67
67
|
/** Wrapper-supplied i18n. */
|
|
68
68
|
texts?: DeepPartial<AIHubTexts>;
|
|
69
|
+
/**
|
|
70
|
+
* Fallback UI language before `/auth/me` resolves and when no per-device
|
|
71
|
+
* choice is cached. The server's `language_default` wins after hydration.
|
|
72
|
+
* Built-in locales: `"en"` | `"cs"` | `"sk"`. Defaults to `"en"`.
|
|
73
|
+
*/
|
|
74
|
+
defaultLanguage?: AIHubLanguage;
|
|
69
75
|
/** Per-element class overrides for the login screen. */
|
|
70
76
|
loginClassNames?: Partial<AIHubLoginClassNames>;
|
|
71
77
|
/** Per-element class overrides for the change-password screen. */
|
|
@@ -161,6 +167,10 @@ export function DataClubAIHub(props: DataClubAIHubProps): ReactNode {
|
|
|
161
167
|
<AIHubProvider
|
|
162
168
|
baseUrl={baseUrl}
|
|
163
169
|
{...(props.defaultTheme !== undefined ? { defaultTheme: props.defaultTheme } : {})}
|
|
170
|
+
{...(props.defaultLanguage !== undefined
|
|
171
|
+
? { defaultLanguage: props.defaultLanguage }
|
|
172
|
+
: {})}
|
|
173
|
+
{...(props.texts !== undefined ? { texts: props.texts } : {})}
|
|
164
174
|
>
|
|
165
175
|
<AuthGate
|
|
166
176
|
{...(props.productName !== undefined ? { productName: props.productName } : {})}
|
|
@@ -33,6 +33,7 @@ import type {
|
|
|
33
33
|
} from "../../client/AIHubClient.js";
|
|
34
34
|
import { useAIHub } from "../../context/useAIHub.js";
|
|
35
35
|
import { useAIHubAuth } from "../../context/useAIHubAuth.js";
|
|
36
|
+
import { formatRelativeTime } from "../../util/relativeTime.js";
|
|
36
37
|
import type { AIHubMessage } from "../../types.js";
|
|
37
38
|
import { ReasoningView } from "../ReasoningView/index.js";
|
|
38
39
|
import { ToolCallView, type ToolCallStatus } from "../ToolCallView/index.js";
|
|
@@ -48,21 +49,6 @@ type StatusFilter = "active" | "archived" | "all";
|
|
|
48
49
|
/** Page size — matches the spec's default of 50. */
|
|
49
50
|
const PAGE_SIZE = 50;
|
|
50
51
|
|
|
51
|
-
/** Format an ISO date as a short relative "x ago" string. */
|
|
52
|
-
function formatLastMessage(iso: string): string {
|
|
53
|
-
const date = Date.parse(iso);
|
|
54
|
-
if (Number.isNaN(date)) return iso;
|
|
55
|
-
const diffMs = Date.now() - date;
|
|
56
|
-
if (diffMs < 60_000) return "just now";
|
|
57
|
-
const minutes = Math.floor(diffMs / 60_000);
|
|
58
|
-
if (minutes < 60) return `${minutes} min ago`;
|
|
59
|
-
const hours = Math.floor(minutes / 60);
|
|
60
|
-
if (hours < 24) return `${hours}h ago`;
|
|
61
|
-
const days = Math.floor(hours / 24);
|
|
62
|
-
if (days < 7) return `${days}d ago`;
|
|
63
|
-
return new Date(date).toISOString().slice(0, 10);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
52
|
/** Props for `<AdminSessionsOverviewPage />`. */
|
|
67
53
|
export interface AdminSessionsOverviewPageProps {
|
|
68
54
|
/** Resolved admin texts. */
|
|
@@ -78,7 +64,7 @@ export interface AdminSessionsOverviewPageProps {
|
|
|
78
64
|
export function AdminSessionsOverviewPage(
|
|
79
65
|
props: AdminSessionsOverviewPageProps,
|
|
80
66
|
): ReactNode {
|
|
81
|
-
const { client } = useAIHub();
|
|
67
|
+
const { client, language } = useAIHub();
|
|
82
68
|
|
|
83
69
|
const [view, setView] = useState<View>({ kind: "list" });
|
|
84
70
|
|
|
@@ -337,7 +323,7 @@ export function AdminSessionsOverviewPage(
|
|
|
337
323
|
<td>{s.title}</td>
|
|
338
324
|
<td>{s.owner_username}</td>
|
|
339
325
|
<td>{s.agent_display_name}</td>
|
|
340
|
-
<td>{
|
|
326
|
+
<td>{formatRelativeTime(s.updated_at, language, { absoluteAfterDays: 7 })}</td>
|
|
341
327
|
<td>
|
|
342
328
|
<button
|
|
343
329
|
type="button"
|
|
@@ -407,7 +393,7 @@ interface AdminSessionDetailProps {
|
|
|
407
393
|
* detail browses arbitrary sessions outside the chat context.
|
|
408
394
|
*/
|
|
409
395
|
function AdminSessionDetail(props: AdminSessionDetailProps): ReactNode {
|
|
410
|
-
const { client } = useAIHub();
|
|
396
|
+
const { client, language } = useAIHub();
|
|
411
397
|
const auth = useAIHubAuth();
|
|
412
398
|
const isSuperAdmin = auth.user?.role === "super_admin";
|
|
413
399
|
const { session, texts } = props;
|
|
@@ -552,7 +538,7 @@ function AdminSessionDetail(props: AdminSessionDetailProps): ReactNode {
|
|
|
552
538
|
>
|
|
553
539
|
<div className="data-club-ai-hub-admin__readonly-meta">
|
|
554
540
|
<strong>{m.role}</strong>
|
|
555
|
-
<span>{
|
|
541
|
+
<span>{formatRelativeTime(m.created_at, language, { absoluteAfterDays: 7 })}</span>
|
|
556
542
|
</div>
|
|
557
543
|
<div className="data-club-ai-hub-admin__readonly-content">
|
|
558
544
|
{m.content}
|
|
@@ -26,10 +26,14 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { useEffect, useState, type ReactNode } from "react";
|
|
28
28
|
|
|
29
|
-
import type { AIHubAdminTexts } from "../../auth/texts.js";
|
|
29
|
+
import type { AIHubAdminTexts, AIHubLanguage } from "../../auth/texts.js";
|
|
30
30
|
import type { AdminSettingsView } from "../../client/AIHubClient.js";
|
|
31
31
|
import { useAIHub } from "../../context/useAIHub.js";
|
|
32
32
|
import { useAIHubAuth } from "../../context/useAIHubAuth.js";
|
|
33
|
+
import {
|
|
34
|
+
formatAbsoluteDate,
|
|
35
|
+
formatRelativeDay,
|
|
36
|
+
} from "../../util/relativeTime.js";
|
|
33
37
|
import type { AIHubFeatureToggles } from "../../types.js";
|
|
34
38
|
|
|
35
39
|
/** All feature-toggle keys per §8.4. */
|
|
@@ -86,16 +90,14 @@ function defaultToggleValue(): boolean {
|
|
|
86
90
|
return true;
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
/** Format ISO date as
|
|
90
|
-
function formatRotationStamp(
|
|
93
|
+
/** Format ISO date as `<localized date> (<localized N days ago>)`. */
|
|
94
|
+
function formatRotationStamp(
|
|
95
|
+
iso: string | null,
|
|
96
|
+
language: AIHubLanguage,
|
|
97
|
+
): string | null {
|
|
91
98
|
if (iso === null) return null;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const dateOnly = new Date(ts).toISOString().slice(0, 10);
|
|
95
|
-
const diffDays = Math.floor((Date.now() - ts) / (24 * 60 * 60 * 1000));
|
|
96
|
-
if (diffDays <= 0) return `${dateOnly} (today)`;
|
|
97
|
-
if (diffDays === 1) return `${dateOnly} (1 day ago)`;
|
|
98
|
-
return `${dateOnly} (${diffDays} days ago)`;
|
|
99
|
+
if (Number.isNaN(Date.parse(iso))) return iso;
|
|
100
|
+
return `${formatAbsoluteDate(iso, language)} (${formatRelativeDay(iso, language)})`;
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
/** Props for `<AdminSettingsPage />`. */
|
|
@@ -181,6 +183,11 @@ export function AdminSettingsPage(props: AdminSettingsPageProps): ReactNode {
|
|
|
181
183
|
texts={props.texts}
|
|
182
184
|
onSaved={handleSectionSaved}
|
|
183
185
|
/>
|
|
186
|
+
<LanguageSection
|
|
187
|
+
settings={settings}
|
|
188
|
+
texts={props.texts}
|
|
189
|
+
onSaved={handleSectionSaved}
|
|
190
|
+
/>
|
|
184
191
|
<FeatureTogglesSection
|
|
185
192
|
settings={settings}
|
|
186
193
|
texts={props.texts}
|
|
@@ -525,6 +532,120 @@ function ThemeSection(props: ThemeSectionProps): ReactNode {
|
|
|
525
532
|
);
|
|
526
533
|
}
|
|
527
534
|
|
|
535
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
536
|
+
/* Section: Language */
|
|
537
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
538
|
+
|
|
539
|
+
interface LanguageSectionProps extends SectionProps {
|
|
540
|
+
onSaved: (next: AdminSettingsView) => void;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function LanguageSection(props: LanguageSectionProps): ReactNode {
|
|
544
|
+
const { client } = useAIHub();
|
|
545
|
+
const [languageDefault, setLanguageDefault] = useState<AIHubLanguage>(
|
|
546
|
+
props.settings.language.default,
|
|
547
|
+
);
|
|
548
|
+
const [userLocked, setUserLocked] = useState(
|
|
549
|
+
props.settings.language.user_locked,
|
|
550
|
+
);
|
|
551
|
+
const [saving, setSaving] = useState(false);
|
|
552
|
+
const [flash, setFlash] = useState<{ tone: "success" | "error"; text: string } | null>(
|
|
553
|
+
null,
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
async function handleSave(event: React.FormEvent): Promise<void> {
|
|
557
|
+
event.preventDefault();
|
|
558
|
+
setSaving(true);
|
|
559
|
+
setFlash(null);
|
|
560
|
+
try {
|
|
561
|
+
const next = await client.patchAdminSettings({
|
|
562
|
+
language: { default: languageDefault, userLocked },
|
|
563
|
+
});
|
|
564
|
+
props.onSaved(next);
|
|
565
|
+
setFlash({ tone: "success", text: props.texts.settingsSaveSuccessFlash });
|
|
566
|
+
} catch (err) {
|
|
567
|
+
setFlash({
|
|
568
|
+
tone: "error",
|
|
569
|
+
text:
|
|
570
|
+
err instanceof Error && err.message.length > 0
|
|
571
|
+
? err.message
|
|
572
|
+
: props.texts.settingsSaveGenericError,
|
|
573
|
+
});
|
|
574
|
+
} finally {
|
|
575
|
+
setSaving(false);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return (
|
|
580
|
+
<section
|
|
581
|
+
className="data-club-ai-hub-admin__settings-section"
|
|
582
|
+
data-testid="admin-settings-language-section"
|
|
583
|
+
>
|
|
584
|
+
<h2 className="data-club-ai-hub-admin__settings-section-heading">
|
|
585
|
+
{props.texts.settingsLanguageSectionHeading}
|
|
586
|
+
</h2>
|
|
587
|
+
<form onSubmit={(event) => void handleSave(event)} noValidate>
|
|
588
|
+
<label
|
|
589
|
+
className="data-club-ai-hub-admin__form-label"
|
|
590
|
+
htmlFor="admin-settings-language-default"
|
|
591
|
+
>
|
|
592
|
+
{props.texts.settingsLanguageDefaultLabel}
|
|
593
|
+
</label>
|
|
594
|
+
<select
|
|
595
|
+
id="admin-settings-language-default"
|
|
596
|
+
className="data-club-ai-hub-admin__form-input"
|
|
597
|
+
data-testid="admin-settings-language-default"
|
|
598
|
+
value={languageDefault}
|
|
599
|
+
onChange={(event) =>
|
|
600
|
+
setLanguageDefault(event.target.value as AIHubLanguage)
|
|
601
|
+
}
|
|
602
|
+
disabled={saving}
|
|
603
|
+
>
|
|
604
|
+
<option value="en">{props.texts.settingsLanguageDefaultEnglish}</option>
|
|
605
|
+
<option value="cs">{props.texts.settingsLanguageDefaultCzech}</option>
|
|
606
|
+
<option value="sk">{props.texts.settingsLanguageDefaultSlovak}</option>
|
|
607
|
+
</select>
|
|
608
|
+
<label className="data-club-ai-hub-admin__checkbox">
|
|
609
|
+
<input
|
|
610
|
+
type="checkbox"
|
|
611
|
+
data-testid="admin-settings-language-locked"
|
|
612
|
+
checked={userLocked}
|
|
613
|
+
onChange={(event) => setUserLocked(event.target.checked)}
|
|
614
|
+
disabled={saving}
|
|
615
|
+
/>
|
|
616
|
+
<span>{props.texts.settingsLanguageLockedLabel}</span>
|
|
617
|
+
</label>
|
|
618
|
+
<p className="data-club-ai-hub-admin__form-helper">
|
|
619
|
+
{props.texts.settingsLanguageLockedHelper}
|
|
620
|
+
</p>
|
|
621
|
+
{flash !== null ? (
|
|
622
|
+
<div
|
|
623
|
+
className={
|
|
624
|
+
flash.tone === "success"
|
|
625
|
+
? "data-club-ai-hub-admin__flash data-club-ai-hub-admin__flash--success"
|
|
626
|
+
: "data-club-ai-hub-admin__flash data-club-ai-hub-admin__flash--error"
|
|
627
|
+
}
|
|
628
|
+
data-testid="admin-settings-language-flash"
|
|
629
|
+
role={flash.tone === "error" ? "alert" : "status"}
|
|
630
|
+
>
|
|
631
|
+
{flash.text}
|
|
632
|
+
</div>
|
|
633
|
+
) : null}
|
|
634
|
+
<button
|
|
635
|
+
type="submit"
|
|
636
|
+
className="data-club-ai-hub-admin__button data-club-ai-hub-admin__button--primary"
|
|
637
|
+
data-testid="admin-settings-language-save"
|
|
638
|
+
disabled={saving}
|
|
639
|
+
>
|
|
640
|
+
{saving
|
|
641
|
+
? props.texts.settingsSaveSavingButton
|
|
642
|
+
: props.texts.settingsSaveLanguageButton}
|
|
643
|
+
</button>
|
|
644
|
+
</form>
|
|
645
|
+
</section>
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
528
649
|
/* ────────────────────────────────────────────────────────────────────────── */
|
|
529
650
|
/* Section: Feature toggles */
|
|
530
651
|
/* ────────────────────────────────────────────────────────────────────────── */
|
|
@@ -649,10 +770,13 @@ interface SigningKeySectionProps extends SectionProps {
|
|
|
649
770
|
}
|
|
650
771
|
|
|
651
772
|
function SigningKeySection(props: SigningKeySectionProps): ReactNode {
|
|
652
|
-
const { client } = useAIHub();
|
|
773
|
+
const { client, language } = useAIHub();
|
|
653
774
|
const auth = useAIHubAuth();
|
|
654
775
|
const [showModal, setShowModal] = useState(false);
|
|
655
|
-
const lastRotated = formatRotationStamp(
|
|
776
|
+
const lastRotated = formatRotationStamp(
|
|
777
|
+
props.settings.signing_key_rotated_at,
|
|
778
|
+
language,
|
|
779
|
+
);
|
|
656
780
|
|
|
657
781
|
async function handleRotateConfirmed(): Promise<void> {
|
|
658
782
|
await client.rotateAdminSigningKey();
|
|
@@ -716,11 +840,12 @@ interface MetricsTokenSectionProps extends SectionProps {
|
|
|
716
840
|
}
|
|
717
841
|
|
|
718
842
|
function MetricsTokenSection(props: MetricsTokenSectionProps): ReactNode {
|
|
719
|
-
const { client } = useAIHub();
|
|
843
|
+
const { client, language } = useAIHub();
|
|
720
844
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
721
845
|
const [revealedToken, setRevealedToken] = useState<string | null>(null);
|
|
722
846
|
const lastRotated = formatRotationStamp(
|
|
723
847
|
props.settings.metrics_scrape_token_rotated_at,
|
|
848
|
+
language,
|
|
724
849
|
);
|
|
725
850
|
|
|
726
851
|
async function handleRotateConfirmed(): Promise<void> {
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
import { useMemo, useState, type ReactNode } from "react";
|
|
26
26
|
|
|
27
27
|
import {
|
|
28
|
-
resolveAIHubTexts,
|
|
29
28
|
type AIHubTexts,
|
|
30
29
|
type DeepPartial,
|
|
31
30
|
} from "../../auth/texts.js";
|
|
31
|
+
import { useResolvedTexts } from "../../context/useAIHubTexts.js";
|
|
32
32
|
import { useAIHub } from "../../context/useAIHub.js";
|
|
33
33
|
import { useAIHubAuth } from "../../context/useAIHubAuth.js";
|
|
34
34
|
import type { AIHubRole } from "../../types.js";
|
|
@@ -150,7 +150,7 @@ export function DataClubAIHubAdmin(
|
|
|
150
150
|
// custom-property values and overrides the dark cascade from the
|
|
151
151
|
// provider's outer wrapper.
|
|
152
152
|
const { theme } = useAIHub();
|
|
153
|
-
const texts =
|
|
153
|
+
const texts = useResolvedTexts(props.texts);
|
|
154
154
|
const navItems = navItemsForRole(auth.role);
|
|
155
155
|
const productName = props.productName ?? "ai-hub";
|
|
156
156
|
|
|
@@ -29,7 +29,8 @@ import { useState, type FormEvent, type ReactNode } from "react";
|
|
|
29
29
|
|
|
30
30
|
import { useAIHubAuth } from "../../context/useAIHubAuth.js";
|
|
31
31
|
import { AIHubRequestError } from "../../client/AIHubClient.js";
|
|
32
|
-
import {
|
|
32
|
+
import { type AIHubTexts, type DeepPartial } from "../../auth/texts.js";
|
|
33
|
+
import { useResolvedTexts } from "../../context/useAIHubTexts.js";
|
|
33
34
|
import type { AIHubUser } from "../../types.js";
|
|
34
35
|
import { PasswordInput } from "../internal/PasswordInput.js";
|
|
35
36
|
|
|
@@ -142,7 +143,7 @@ export interface DataClubAIHubLoginProps {
|
|
|
142
143
|
export function DataClubAIHubLogin(props: DataClubAIHubLoginProps): ReactNode {
|
|
143
144
|
const auth = useAIHubAuth();
|
|
144
145
|
const classes = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
145
|
-
const texts =
|
|
146
|
+
const texts = useResolvedTexts(props.texts).login;
|
|
146
147
|
const productName = props.productName ?? "ai-hub";
|
|
147
148
|
|
|
148
149
|
const [username, setUsername] = useState("");
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
import { type ChangeEvent, type ReactNode } from "react";
|
|
23
23
|
|
|
24
24
|
import {
|
|
25
|
-
resolveAIHubTexts,
|
|
26
25
|
type AIHubTexts,
|
|
27
26
|
type DeepPartial,
|
|
28
27
|
} from "../../auth/texts.js";
|
|
28
|
+
import { useResolvedTexts } from "../../context/useAIHubTexts.js";
|
|
29
29
|
import type { AIHubEffort } from "../../client/AIHubClient.js";
|
|
30
30
|
import { useAIHub } from "../../context/useAIHub.js";
|
|
31
31
|
|
|
@@ -106,7 +106,7 @@ export interface EffortControlProps {
|
|
|
106
106
|
*/
|
|
107
107
|
export function EffortControl(props: EffortControlProps): ReactNode {
|
|
108
108
|
const { effort, setEffort } = useAIHub();
|
|
109
|
-
const texts =
|
|
109
|
+
const texts = useResolvedTexts(props.texts);
|
|
110
110
|
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
111
111
|
const rootClass = props.className
|
|
112
112
|
? `${cls.root} ${props.className}`
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<LanguageSwitcher />` — globe button in the shell header (beside the user
|
|
3
|
+
* avatar) that switches the active UI language via `setLanguage` from
|
|
4
|
+
* `useAIHub()`.
|
|
5
|
+
*
|
|
6
|
+
* Renders **nothing** when `languageSwitcherEnabled === false` (the super-
|
|
7
|
+
* admin has locked the language for this deployment) — absent from the DOM
|
|
8
|
+
* entirely, like `<ThemeSwitcher />`. So a caller can leave it in place
|
|
9
|
+
* unconditionally and the primitive self-suppresses.
|
|
10
|
+
*
|
|
11
|
+
* Switching is a device-local preference (cached in `localStorage`), no
|
|
12
|
+
* server round-trip — picking a language flips it immediately and the whole
|
|
13
|
+
* tree re-renders with the new locale.
|
|
14
|
+
*
|
|
15
|
+
* Exported under `AIHub.LanguageSwitcher`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
19
|
+
|
|
20
|
+
import { AIHUB_LANGUAGE_OPTIONS } from "../../auth/locales/index.js";
|
|
21
|
+
import { useAIHub } from "../../context/useAIHub.js";
|
|
22
|
+
import { useAIHubTexts } from "../../context/useAIHubTexts.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Renders the language switcher (globe button + popover), or `null` when the
|
|
26
|
+
* deployment has locked the language.
|
|
27
|
+
*
|
|
28
|
+
* @returns the rendered switcher, or `null` when disabled by config.
|
|
29
|
+
*/
|
|
30
|
+
export function LanguageSwitcher(): ReactNode {
|
|
31
|
+
const { language, setLanguage, languageSwitcherEnabled } = useAIHub();
|
|
32
|
+
const texts = useAIHubTexts();
|
|
33
|
+
const [open, setOpen] = useState(false);
|
|
34
|
+
const wrapRef = useRef<HTMLDivElement | null>(null);
|
|
35
|
+
|
|
36
|
+
// Close on outside-click + Escape. `composedPath()` crosses the shadow
|
|
37
|
+
// boundary so this works when packaged as a Web Component (see UserMenu for
|
|
38
|
+
// the full rationale).
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!open) return;
|
|
41
|
+
function handleClick(e: MouseEvent): void {
|
|
42
|
+
const node = wrapRef.current;
|
|
43
|
+
if (!node) return;
|
|
44
|
+
const path =
|
|
45
|
+
typeof e.composedPath === "function" ? e.composedPath() : [];
|
|
46
|
+
const clickedInside =
|
|
47
|
+
path.length > 0 ? path.includes(node) : node.contains(e.target as Node);
|
|
48
|
+
if (!clickedInside) setOpen(false);
|
|
49
|
+
}
|
|
50
|
+
function handleKey(e: KeyboardEvent): void {
|
|
51
|
+
if (e.key === "Escape") setOpen(false);
|
|
52
|
+
}
|
|
53
|
+
document.addEventListener("mousedown", handleClick);
|
|
54
|
+
document.addEventListener("keydown", handleKey);
|
|
55
|
+
return () => {
|
|
56
|
+
document.removeEventListener("mousedown", handleClick);
|
|
57
|
+
document.removeEventListener("keydown", handleKey);
|
|
58
|
+
};
|
|
59
|
+
}, [open]);
|
|
60
|
+
|
|
61
|
+
if (!languageSwitcherEnabled) return null;
|
|
62
|
+
|
|
63
|
+
const activeLabel =
|
|
64
|
+
AIHUB_LANGUAGE_OPTIONS.find((o) => o.code === language)?.label ?? language;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
className="data-club-ai-hub__language-switcher"
|
|
69
|
+
ref={wrapRef}
|
|
70
|
+
data-testid="language-switcher"
|
|
71
|
+
>
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
className="data-club-ai-hub__language-switcher__button"
|
|
75
|
+
aria-haspopup="menu"
|
|
76
|
+
aria-expanded={open}
|
|
77
|
+
aria-label={texts.chat.languageSwitcherAriaLabel}
|
|
78
|
+
title={texts.chat.languageSwitcherAriaLabel}
|
|
79
|
+
onClick={() => setOpen((o) => !o)}
|
|
80
|
+
data-testid="language-switcher-button"
|
|
81
|
+
>
|
|
82
|
+
<span
|
|
83
|
+
className="data-club-ai-hub__language-switcher__icon"
|
|
84
|
+
aria-hidden="true"
|
|
85
|
+
>
|
|
86
|
+
🌐
|
|
87
|
+
</span>
|
|
88
|
+
<span
|
|
89
|
+
className="data-club-ai-hub__language-switcher__code"
|
|
90
|
+
data-testid="language-switcher-code"
|
|
91
|
+
>
|
|
92
|
+
{language.toUpperCase()}
|
|
93
|
+
</span>
|
|
94
|
+
</button>
|
|
95
|
+
{open ? (
|
|
96
|
+
<div
|
|
97
|
+
className="data-club-ai-hub__language-switcher__dropdown"
|
|
98
|
+
role="menu"
|
|
99
|
+
data-testid="language-switcher-dropdown"
|
|
100
|
+
>
|
|
101
|
+
{AIHUB_LANGUAGE_OPTIONS.map((opt) => {
|
|
102
|
+
const isActive = opt.code === language;
|
|
103
|
+
const itemClass = isActive
|
|
104
|
+
? "data-club-ai-hub__language-switcher__item data-club-ai-hub__language-switcher__item--active"
|
|
105
|
+
: "data-club-ai-hub__language-switcher__item";
|
|
106
|
+
return (
|
|
107
|
+
<button
|
|
108
|
+
key={opt.code}
|
|
109
|
+
type="button"
|
|
110
|
+
role="menuitemradio"
|
|
111
|
+
aria-checked={isActive}
|
|
112
|
+
className={itemClass}
|
|
113
|
+
data-testid={`language-switcher-option-${opt.code}`}
|
|
114
|
+
onClick={() => {
|
|
115
|
+
setLanguage(opt.code);
|
|
116
|
+
setOpen(false);
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{opt.label}
|
|
120
|
+
</button>
|
|
121
|
+
);
|
|
122
|
+
})}
|
|
123
|
+
</div>
|
|
124
|
+
) : null}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
import { useMemo, type ReactNode } from "react";
|
|
18
18
|
|
|
19
19
|
import {
|
|
20
|
-
resolveAIHubTexts,
|
|
21
20
|
type AIHubTexts,
|
|
22
21
|
type DeepPartial,
|
|
23
22
|
} from "../../auth/texts.js";
|
|
23
|
+
import { useResolvedTexts } from "../../context/useAIHubTexts.js";
|
|
24
24
|
import { useAIHub } from "../../context/useAIHub.js";
|
|
25
25
|
|
|
26
26
|
import { TurnStatusIndicator } from "../TurnStatusIndicator/index.js";
|
|
@@ -88,7 +88,7 @@ export interface MainColumnProps {
|
|
|
88
88
|
*/
|
|
89
89
|
export function MainColumn(props: MainColumnProps): ReactNode {
|
|
90
90
|
const { agents, activeAgentId, setActiveAgent } = useAIHub();
|
|
91
|
-
const texts =
|
|
91
|
+
const texts = useResolvedTexts(props.texts);
|
|
92
92
|
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
93
93
|
const showBack = props.showBackToAgents ?? true;
|
|
94
94
|
const showTurnStatus = props.showTurnStatus ?? true;
|
|
@@ -32,10 +32,10 @@ import {
|
|
|
32
32
|
} from "react";
|
|
33
33
|
|
|
34
34
|
import {
|
|
35
|
-
resolveAIHubTexts,
|
|
36
35
|
type AIHubTexts,
|
|
37
36
|
type DeepPartial,
|
|
38
37
|
} from "../../auth/texts.js";
|
|
38
|
+
import { useResolvedTexts } from "../../context/useAIHubTexts.js";
|
|
39
39
|
import { useAIHub } from "../../context/useAIHub.js";
|
|
40
40
|
import { chatAffordances } from "../../state/affordances.js";
|
|
41
41
|
|
|
@@ -124,7 +124,7 @@ export function MessageInput(props: MessageInputProps): ReactNode {
|
|
|
124
124
|
pendingDraft,
|
|
125
125
|
consumePendingDraft,
|
|
126
126
|
} = useAIHub();
|
|
127
|
-
const texts =
|
|
127
|
+
const texts = useResolvedTexts(props.texts);
|
|
128
128
|
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
129
129
|
const showToolbar = props.showToolbar ?? true;
|
|
130
130
|
const showEffortControl = props.showEffortControl ?? true;
|
|
@@ -42,10 +42,10 @@ import {
|
|
|
42
42
|
} from "react";
|
|
43
43
|
|
|
44
44
|
import {
|
|
45
|
-
resolveAIHubTexts,
|
|
46
45
|
type AIHubTexts,
|
|
47
46
|
type DeepPartial,
|
|
48
47
|
} from "../../auth/texts.js";
|
|
48
|
+
import { useResolvedTexts } from "../../context/useAIHubTexts.js";
|
|
49
49
|
import { useAIHub } from "../../context/useAIHub.js";
|
|
50
50
|
import { chatAffordances } from "../../state/affordances.js";
|
|
51
51
|
import type {
|
|
@@ -253,7 +253,7 @@ export function MessageList(props: MessageListProps): ReactNode {
|
|
|
253
253
|
state,
|
|
254
254
|
sendMessage,
|
|
255
255
|
} = useAIHub();
|
|
256
|
-
const texts =
|
|
256
|
+
const texts = useResolvedTexts(props.texts);
|
|
257
257
|
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
258
258
|
const showSystemMessages = props.showSystemMessages ?? true;
|
|
259
259
|
const showTpmNotice = props.showTpmNotice ?? true;
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
import { useState, type ReactNode } from "react";
|
|
26
26
|
|
|
27
27
|
import {
|
|
28
|
-
resolveAIHubTexts,
|
|
29
28
|
type AIHubTexts,
|
|
30
29
|
type DeepPartial,
|
|
31
30
|
} from "../../auth/texts.js";
|
|
31
|
+
import { useResolvedTexts } from "../../context/useAIHubTexts.js";
|
|
32
32
|
|
|
33
33
|
/** BEM class names emitted by `<ReasoningView />`. */
|
|
34
34
|
export interface ReasoningViewClassNames {
|
|
@@ -110,7 +110,7 @@ export interface ReasoningViewProps {
|
|
|
110
110
|
* @returns the rendered block.
|
|
111
111
|
*/
|
|
112
112
|
export function ReasoningView(props: ReasoningViewProps): ReactNode {
|
|
113
|
-
const texts =
|
|
113
|
+
const texts = useResolvedTexts(props.texts);
|
|
114
114
|
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
115
115
|
const [uncontrolledExpanded, setUncontrolledExpanded] = useState(
|
|
116
116
|
props.defaultExpanded ?? false,
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
import { type ReactNode } from "react";
|
|
24
24
|
|
|
25
25
|
import {
|
|
26
|
-
resolveAIHubTexts,
|
|
27
26
|
type AIHubTexts,
|
|
28
27
|
type DeepPartial,
|
|
29
28
|
} from "../../auth/texts.js";
|
|
29
|
+
import { useResolvedTexts } from "../../context/useAIHubTexts.js";
|
|
30
30
|
|
|
31
31
|
/** BEM class names emitted by `<ScrollToPresentButton />`. */
|
|
32
32
|
export interface ScrollToPresentButtonClassNames {
|
|
@@ -67,7 +67,7 @@ export function ScrollToPresentButton(
|
|
|
67
67
|
props: ScrollToPresentButtonProps,
|
|
68
68
|
): ReactNode {
|
|
69
69
|
if (!props.visible) return null;
|
|
70
|
-
const texts =
|
|
70
|
+
const texts = useResolvedTexts(props.texts);
|
|
71
71
|
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
72
72
|
const rootClass = props.className
|
|
73
73
|
? `${cls.root} ${props.className}`
|