@data-club/ai-hub 0.0.12 → 0.0.14
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.map +1 -1
- package/dist/auth/locales/cs.js +1 -0
- package/dist/auth/locales/cs.js.map +1 -1
- package/dist/auth/locales/sk.d.ts.map +1 -1
- package/dist/auth/locales/sk.js +1 -0
- package/dist/auth/locales/sk.js.map +1 -1
- package/dist/auth/texts.d.ts +5 -0
- package/dist/auth/texts.d.ts.map +1 -1
- package/dist/auth/texts.js +1 -0
- package/dist/auth/texts.js.map +1 -1
- package/dist/components/ChatShell/index.d.ts +4 -0
- package/dist/components/ChatShell/index.d.ts.map +1 -1
- package/dist/components/ChatShell/index.js +64 -3
- package/dist/components/ChatShell/index.js.map +1 -1
- package/dist/components/DataClubAIHub/index.d.ts +4 -0
- package/dist/components/DataClubAIHub/index.d.ts.map +1 -1
- package/dist/components/DataClubAIHub/index.js +4 -0
- package/dist/components/DataClubAIHub/index.js.map +1 -1
- package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts.map +1 -1
- package/dist/components/DataClubAIHubAdmin/SettingsPage.js +18 -5
- package/dist/components/DataClubAIHubAdmin/SettingsPage.js.map +1 -1
- package/dist/components/MainColumn/index.d.ts +13 -0
- package/dist/components/MainColumn/index.d.ts.map +1 -1
- package/dist/components/MainColumn/index.js +4 -2
- package/dist/components/MainColumn/index.js.map +1 -1
- package/dist/components/SessionsPanel/index.d.ts +16 -0
- package/dist/components/SessionsPanel/index.d.ts.map +1 -1
- package/dist/components/SessionsPanel/index.js +83 -43
- package/dist/components/SessionsPanel/index.js.map +1 -1
- package/dist/context/AIHubContext.d.ts +24 -0
- package/dist/context/AIHubContext.d.ts.map +1 -1
- package/dist/context/AIHubContext.js.map +1 -1
- package/dist/context/AIHubProvider.d.ts +8 -0
- package/dist/context/AIHubProvider.d.ts.map +1 -1
- package/dist/context/AIHubProvider.js +60 -3
- package/dist/context/AIHubProvider.js.map +1 -1
- package/dist/context/useAIHub.d.ts +6 -0
- package/dist/context/useAIHub.d.ts.map +1 -1
- package/dist/context/useAIHub.js +3 -0
- package/dist/context/useAIHub.js.map +1 -1
- package/dist/state/affordances.d.ts +5 -1
- package/dist/state/affordances.d.ts.map +1 -1
- package/dist/state/affordances.js +5 -1
- package/dist/state/affordances.js.map +1 -1
- package/dist/stories/_mockProvider.d.ts.map +1 -1
- package/dist/stories/_mockProvider.js +3 -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/package.json +1 -1
- package/src/auth/locales/cs.ts +1 -0
- package/src/auth/locales/sk.ts +1 -0
- package/src/auth/texts.ts +6 -0
- package/src/components/ChatShell/index.tsx +63 -0
- package/src/components/DataClubAIHub/index.tsx +10 -0
- package/src/components/DataClubAIHubAdmin/SettingsPage.tsx +19 -5
- package/src/components/MainColumn/index.tsx +30 -0
- package/src/components/SessionsPanel/index.tsx +90 -7
- package/src/context/AIHubContext.ts +26 -0
- package/src/context/AIHubProvider.tsx +78 -1
- package/src/context/useAIHub.ts +9 -0
- package/src/state/affordances.ts +9 -1
- package/src/stories/_mockProvider.tsx +3 -0
- package/src/styles/CLASSES.md +19 -0
- package/src/styles/default.css +284 -0
- package/src/types.ts +13 -0
|
@@ -90,8 +90,12 @@ export interface DataClubAIHubProps {
|
|
|
90
90
|
sessionsPanelWidthPercent?: number;
|
|
91
91
|
/** Default for sessions_panel_collapsible. Default true. */
|
|
92
92
|
sessionsPanelCollapsible?: boolean;
|
|
93
|
+
/** Default for sessions_panel_default_collapsed. Default **false** (extended). */
|
|
94
|
+
sessionsPanelDefaultCollapsed?: boolean;
|
|
93
95
|
/** Default for create_session_button_enabled. Default true. */
|
|
94
96
|
showCreateSessionButton?: boolean;
|
|
97
|
+
/** Default for load_last_session_enabled. Default **false**. */
|
|
98
|
+
loadLastSession?: boolean;
|
|
95
99
|
|
|
96
100
|
/* ── Input controls (§8.4) ─────────────────────────────────────── */
|
|
97
101
|
/** Default for user_input_enabled. Default true. */
|
|
@@ -265,6 +269,12 @@ function AuthedShell({ props }: { props: DataClubAIHubProps }): ReactNode {
|
|
|
265
269
|
{...(props.sessionsPanelCollapsible !== undefined
|
|
266
270
|
? { sessionsPanelCollapsible: props.sessionsPanelCollapsible }
|
|
267
271
|
: {})}
|
|
272
|
+
{...(props.sessionsPanelDefaultCollapsed !== undefined
|
|
273
|
+
? { sessionsPanelDefaultCollapsed: props.sessionsPanelDefaultCollapsed }
|
|
274
|
+
: {})}
|
|
275
|
+
{...(props.loadLastSession !== undefined
|
|
276
|
+
? { loadLastSession: props.loadLastSession }
|
|
277
|
+
: {})}
|
|
268
278
|
{...(props.sessionsPanelResizable !== undefined
|
|
269
279
|
? { sessionsPanelResizable: props.sessionsPanelResizable }
|
|
270
280
|
: {})}
|
|
@@ -43,7 +43,9 @@ const ALL_TOGGLE_KEYS: ReadonlyArray<keyof AIHubFeatureToggles> = [
|
|
|
43
43
|
"sessions_panel_enabled",
|
|
44
44
|
"sessions_panel_resizable",
|
|
45
45
|
"sessions_panel_collapsible",
|
|
46
|
+
"sessions_panel_default_collapsed",
|
|
46
47
|
"create_session_button_enabled",
|
|
48
|
+
"load_last_session_enabled",
|
|
47
49
|
"user_input_enabled",
|
|
48
50
|
"stop_button_enabled",
|
|
49
51
|
"tool_calls_visible",
|
|
@@ -67,7 +69,9 @@ const TOGGLE_GROUPS: ReadonlyArray<{
|
|
|
67
69
|
"sessions_panel_enabled",
|
|
68
70
|
"sessions_panel_resizable",
|
|
69
71
|
"sessions_panel_collapsible",
|
|
72
|
+
"sessions_panel_default_collapsed",
|
|
70
73
|
"create_session_button_enabled",
|
|
74
|
+
"load_last_session_enabled",
|
|
71
75
|
],
|
|
72
76
|
},
|
|
73
77
|
{
|
|
@@ -89,9 +93,19 @@ const TOGGLE_GROUPS: ReadonlyArray<{
|
|
|
89
93
|
},
|
|
90
94
|
];
|
|
91
95
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Built-in defaults per §8.4 — most toggles are on by default; the two
|
|
98
|
+
* opt-in behaviors below default to off. Must mirror
|
|
99
|
+
* `DEFAULT_AFFORDANCE_FLAGS` in `state/affordances.ts`.
|
|
100
|
+
*/
|
|
101
|
+
const OFF_BY_DEFAULT: ReadonlySet<keyof AIHubFeatureToggles> = new Set([
|
|
102
|
+
"sessions_panel_default_collapsed",
|
|
103
|
+
"load_last_session_enabled",
|
|
104
|
+
] as const);
|
|
105
|
+
|
|
106
|
+
/** Built-in default for one toggle key. */
|
|
107
|
+
function defaultToggleValue(key: keyof AIHubFeatureToggles): boolean {
|
|
108
|
+
return !OFF_BY_DEFAULT.has(key);
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
/** Format ISO date as `<localized date> (<localized N days ago>)`. */
|
|
@@ -616,12 +630,12 @@ function FeatureTogglesSection(props: FeatureTogglesSectionProps): ReactNode {
|
|
|
616
630
|
// The server's blob may omit keys (meaning "fall through to defaults"). The
|
|
617
631
|
// UI shows the effective value for the user — fill in defaults for any
|
|
618
632
|
// missing key. Save only sends the keys the user actually has toggled (in
|
|
619
|
-
// our case all of them, since the blob always carries all
|
|
633
|
+
// our case all of them, since the blob always carries all 16 once saved).
|
|
620
634
|
const [values, setValues] = useState<Record<string, boolean>>(() => {
|
|
621
635
|
const initial: Record<string, boolean> = {};
|
|
622
636
|
for (const key of ALL_TOGGLE_KEYS) {
|
|
623
637
|
const fromServer = props.settings.feature_toggles[key];
|
|
624
|
-
initial[key] = typeof fromServer === "boolean" ? fromServer : defaultToggleValue();
|
|
638
|
+
initial[key] = typeof fromServer === "boolean" ? fromServer : defaultToggleValue(key);
|
|
625
639
|
}
|
|
626
640
|
return initial;
|
|
627
641
|
});
|
|
@@ -37,6 +37,11 @@ export interface MainColumnClassNames {
|
|
|
37
37
|
root: string;
|
|
38
38
|
/** Chat header strip at the top. */
|
|
39
39
|
header: string;
|
|
40
|
+
/**
|
|
41
|
+
* Hamburger button that opens the sessions drawer — rendered only in
|
|
42
|
+
* the narrow (mobile) layout.
|
|
43
|
+
*/
|
|
44
|
+
menu: string;
|
|
40
45
|
/** Back-to-agents-picker link. */
|
|
41
46
|
back: string;
|
|
42
47
|
/** Agent display-name text. */
|
|
@@ -52,6 +57,7 @@ export interface MainColumnClassNames {
|
|
|
52
57
|
const DEFAULT_CLASS_NAMES: MainColumnClassNames = Object.freeze({
|
|
53
58
|
root: "data-club-ai-hub__main-column",
|
|
54
59
|
header: "data-club-ai-hub__chat-header",
|
|
60
|
+
menu: "data-club-ai-hub__chat-header__menu",
|
|
55
61
|
back: "data-club-ai-hub__chat-header__back",
|
|
56
62
|
title: "data-club-ai-hub__chat-header__title",
|
|
57
63
|
export: "data-club-ai-hub__chat-header__export",
|
|
@@ -69,6 +75,14 @@ export interface MainColumnProps {
|
|
|
69
75
|
* the link in single-agent embeds). Defaults to true.
|
|
70
76
|
*/
|
|
71
77
|
showBackToAgents?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Whether the narrow-layout hamburger button (opens the sessions
|
|
80
|
+
* drawer) may render. `<ChatShell />` passes the resolved
|
|
81
|
+
* `sessions_panel_enabled` flag so the button never appears for
|
|
82
|
+
* deployments without a sessions panel. The button only renders while
|
|
83
|
+
* the container is in the narrow layout regardless. Defaults to true.
|
|
84
|
+
*/
|
|
85
|
+
showMenuButton?: boolean;
|
|
72
86
|
/**
|
|
73
87
|
* Whether to mount the turn-status indicator in the chat header.
|
|
74
88
|
* Receives the resolved §8.4 flag stack (server toggle > prop >
|
|
@@ -107,10 +121,13 @@ export function MainColumn(props: MainColumnProps): ReactNode {
|
|
|
107
121
|
sessions,
|
|
108
122
|
activeSessionId,
|
|
109
123
|
messages,
|
|
124
|
+
isNarrowLayout,
|
|
125
|
+
setSessionsDrawerOpen,
|
|
110
126
|
} = useAIHub();
|
|
111
127
|
const texts = useResolvedTexts(props.texts);
|
|
112
128
|
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
113
129
|
const showBack = props.showBackToAgents ?? true;
|
|
130
|
+
const showMenuButton = (props.showMenuButton ?? true) && isNarrowLayout;
|
|
114
131
|
const showTurnStatus = props.showTurnStatus ?? true;
|
|
115
132
|
const showExportButton = props.showExportButton ?? true;
|
|
116
133
|
|
|
@@ -176,6 +193,19 @@ export function MainColumn(props: MainColumnProps): ReactNode {
|
|
|
176
193
|
return (
|
|
177
194
|
<section className={rootClass} data-testid="main-column">
|
|
178
195
|
<header className={cls.header} data-testid="chat-header">
|
|
196
|
+
{showMenuButton ? (
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
className={cls.menu}
|
|
200
|
+
onClick={() => setSessionsDrawerOpen(true)}
|
|
201
|
+
aria-label={texts.chat.expandSessionsPanel}
|
|
202
|
+
data-testid="chat-header-menu"
|
|
203
|
+
>
|
|
204
|
+
{/* Hamburger glyph — consistent with the text-glyph icons used
|
|
205
|
+
elsewhere (≪ / ≫ / ✎); themed via currentColor. */}
|
|
206
|
+
☰
|
|
207
|
+
</button>
|
|
208
|
+
) : null}
|
|
179
209
|
{showBack ? (
|
|
180
210
|
<button
|
|
181
211
|
type="button"
|
|
@@ -48,6 +48,17 @@ export interface SessionsPanelClassNames {
|
|
|
48
48
|
root: string;
|
|
49
49
|
/** Modifier applied when the panel is collapsed. */
|
|
50
50
|
rootCollapsed: string;
|
|
51
|
+
/**
|
|
52
|
+
* Modifier applied in the narrow (mobile) layout, where the panel
|
|
53
|
+
* renders as an overlay drawer instead of an inline column.
|
|
54
|
+
*/
|
|
55
|
+
rootDrawer: string;
|
|
56
|
+
/** Modifier applied while the mobile drawer is open. */
|
|
57
|
+
rootDrawerOpen: string;
|
|
58
|
+
/** Backdrop scrim behind the open mobile drawer. */
|
|
59
|
+
drawerScrim: string;
|
|
60
|
+
/** Close (✕) button at the top of the mobile drawer. */
|
|
61
|
+
drawerClose: string;
|
|
51
62
|
/** Optional custom-header slot wrapper at the top of the panel. */
|
|
52
63
|
customHeader: string;
|
|
53
64
|
/** "+ New chat" button. */
|
|
@@ -87,6 +98,10 @@ export interface SessionsPanelClassNames {
|
|
|
87
98
|
const DEFAULT_CLASS_NAMES: SessionsPanelClassNames = Object.freeze({
|
|
88
99
|
root: "data-club-ai-hub__sessions-panel",
|
|
89
100
|
rootCollapsed: "data-club-ai-hub__sessions-panel--collapsed",
|
|
101
|
+
rootDrawer: "data-club-ai-hub__sessions-panel--drawer",
|
|
102
|
+
rootDrawerOpen: "data-club-ai-hub__sessions-panel--drawer-open",
|
|
103
|
+
drawerScrim: "data-club-ai-hub__sessions-panel__scrim",
|
|
104
|
+
drawerClose: "data-club-ai-hub__sessions-panel__drawer-close",
|
|
90
105
|
customHeader: "data-club-ai-hub__sessions-panel__custom-header",
|
|
91
106
|
newSessionButton: "data-club-ai-hub__sessions-panel__new-button",
|
|
92
107
|
reveal: "data-club-ai-hub__sessions-panel__reveal",
|
|
@@ -114,6 +129,11 @@ export interface SessionsPanelProps {
|
|
|
114
129
|
showCreateButton?: boolean;
|
|
115
130
|
/** Whether the panel can be collapsed. Defaults to true. */
|
|
116
131
|
collapsible?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Whether the panel starts collapsed (until the user toggles it).
|
|
134
|
+
* Defaults to false — panel starts extended.
|
|
135
|
+
*/
|
|
136
|
+
defaultCollapsed?: boolean;
|
|
117
137
|
/** Top-of-panel slot. */
|
|
118
138
|
customHeader?: ReactNode;
|
|
119
139
|
/** Bottom-of-panel slot, rendered above the archived-filter toggle. */
|
|
@@ -143,16 +163,42 @@ export function SessionsPanel(props: SessionsPanelProps): ReactNode {
|
|
|
143
163
|
includeArchivedSessions,
|
|
144
164
|
setIncludeArchivedSessions,
|
|
145
165
|
language,
|
|
166
|
+
isNarrowLayout,
|
|
167
|
+
sessionsDrawerOpen,
|
|
168
|
+
setSessionsDrawerOpen,
|
|
146
169
|
} = useAIHub();
|
|
147
170
|
const texts = useResolvedTexts(props.texts);
|
|
148
171
|
const cls = { ...DEFAULT_CLASS_NAMES, ...(props.classNames ?? {}) };
|
|
149
172
|
const showCreate = props.showCreateButton ?? true;
|
|
150
173
|
const collapsible = props.collapsible ?? true;
|
|
151
174
|
|
|
152
|
-
|
|
175
|
+
// `null` = the user hasn't toggled yet — follow `defaultCollapsed`. This
|
|
176
|
+
// matters because the server's feature-toggle blob resolves async: a
|
|
177
|
+
// late-arriving `sessions_panel_default_collapsed=true` still applies as
|
|
178
|
+
// long as the user hasn't made an explicit choice.
|
|
179
|
+
const [userCollapsed, setUserCollapsed] = useState<boolean | null>(null);
|
|
180
|
+
const collapsed = userCollapsed ?? props.defaultCollapsed ?? false;
|
|
153
181
|
const [modalOpen, setModalOpen] = useState(false);
|
|
154
182
|
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
|
155
183
|
|
|
184
|
+
// Narrow (mobile) layout: the panel becomes an overlay drawer driven by
|
|
185
|
+
// shared context state (the open button lives in `<MainColumn>`'s chat
|
|
186
|
+
// header). Desktop-only affordances — collapse toggle / reveal strip —
|
|
187
|
+
// are suppressed while in drawer mode.
|
|
188
|
+
const drawerMode = isNarrowLayout;
|
|
189
|
+
const drawerOpen = drawerMode && sessionsDrawerOpen;
|
|
190
|
+
|
|
191
|
+
// Escape closes the open drawer (matches the dropdown/modal conventions
|
|
192
|
+
// elsewhere in the library).
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (!drawerOpen) return undefined;
|
|
195
|
+
const onKeyDown = (event: KeyboardEvent): void => {
|
|
196
|
+
if (event.key === "Escape") setSessionsDrawerOpen(false);
|
|
197
|
+
};
|
|
198
|
+
document.addEventListener("keydown", onKeyDown);
|
|
199
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
200
|
+
}, [drawerOpen, setSessionsDrawerOpen]);
|
|
201
|
+
|
|
156
202
|
const editingSession = useMemo<Session | null>(() => {
|
|
157
203
|
if (!editingSessionId) return null;
|
|
158
204
|
return sessions.find((s) => s.id === editingSessionId) ?? null;
|
|
@@ -166,7 +212,7 @@ export function SessionsPanel(props: SessionsPanelProps): ReactNode {
|
|
|
166
212
|
}
|
|
167
213
|
}, [editingSessionId, editingSession]);
|
|
168
214
|
|
|
169
|
-
if (collapsed) {
|
|
215
|
+
if (collapsed && !drawerMode) {
|
|
170
216
|
return (
|
|
171
217
|
<div
|
|
172
218
|
className={`${cls.root} ${cls.rootCollapsed}`}
|
|
@@ -176,7 +222,7 @@ export function SessionsPanel(props: SessionsPanelProps): ReactNode {
|
|
|
176
222
|
<button
|
|
177
223
|
type="button"
|
|
178
224
|
className={cls.reveal}
|
|
179
|
-
onClick={() =>
|
|
225
|
+
onClick={() => setUserCollapsed(false)}
|
|
180
226
|
aria-label={texts.chat.expandSessionsPanel}
|
|
181
227
|
data-testid="sessions-panel-reveal"
|
|
182
228
|
>
|
|
@@ -189,7 +235,14 @@ export function SessionsPanel(props: SessionsPanelProps): ReactNode {
|
|
|
189
235
|
);
|
|
190
236
|
}
|
|
191
237
|
|
|
192
|
-
const rootClass = [
|
|
238
|
+
const rootClass = [
|
|
239
|
+
cls.root,
|
|
240
|
+
drawerMode ? cls.rootDrawer : "",
|
|
241
|
+
drawerOpen ? cls.rootDrawerOpen : "",
|
|
242
|
+
props.className,
|
|
243
|
+
]
|
|
244
|
+
.filter(Boolean)
|
|
245
|
+
.join(" ");
|
|
193
246
|
|
|
194
247
|
async function handleArchive(session: Session): Promise<void> {
|
|
195
248
|
const prompt = texts.chat.archiveConfirmPrompt.replace(
|
|
@@ -216,11 +269,35 @@ export function SessionsPanel(props: SessionsPanelProps): ReactNode {
|
|
|
216
269
|
}
|
|
217
270
|
|
|
218
271
|
return (
|
|
272
|
+
<>
|
|
273
|
+
{drawerMode ? (
|
|
274
|
+
<div
|
|
275
|
+
className={cls.drawerScrim}
|
|
276
|
+
data-open={drawerOpen ? "true" : "false"}
|
|
277
|
+
onClick={() => setSessionsDrawerOpen(false)}
|
|
278
|
+
aria-hidden="true"
|
|
279
|
+
data-testid="sessions-drawer-scrim"
|
|
280
|
+
/>
|
|
281
|
+
) : null}
|
|
219
282
|
<aside
|
|
220
283
|
className={rootClass}
|
|
221
284
|
data-testid="sessions-panel"
|
|
222
285
|
aria-label="Sessions"
|
|
286
|
+
data-drawer={drawerMode ? "true" : undefined}
|
|
287
|
+
data-open={drawerMode ? (drawerOpen ? "true" : "false") : undefined}
|
|
288
|
+
aria-hidden={drawerMode && !drawerOpen ? true : undefined}
|
|
223
289
|
>
|
|
290
|
+
{drawerMode ? (
|
|
291
|
+
<button
|
|
292
|
+
type="button"
|
|
293
|
+
className={cls.drawerClose}
|
|
294
|
+
onClick={() => setSessionsDrawerOpen(false)}
|
|
295
|
+
aria-label={texts.chat.collapseSessionsPanel}
|
|
296
|
+
data-testid="sessions-drawer-close"
|
|
297
|
+
>
|
|
298
|
+
✕
|
|
299
|
+
</button>
|
|
300
|
+
) : null}
|
|
224
301
|
{props.customHeader ? (
|
|
225
302
|
<div className={cls.customHeader} data-testid="sessions-panel-custom-header">
|
|
226
303
|
{props.customHeader}
|
|
@@ -239,11 +316,11 @@ export function SessionsPanel(props: SessionsPanelProps): ReactNode {
|
|
|
239
316
|
{texts.chat.newSessionButton}
|
|
240
317
|
</button>
|
|
241
318
|
) : null}
|
|
242
|
-
{collapsible ? (
|
|
319
|
+
{collapsible && !drawerMode ? (
|
|
243
320
|
<button
|
|
244
321
|
type="button"
|
|
245
322
|
className={cls.collapseToggle}
|
|
246
|
-
onClick={() =>
|
|
323
|
+
onClick={() => setUserCollapsed(true)}
|
|
247
324
|
aria-label={texts.chat.collapseSessionsPanel}
|
|
248
325
|
data-testid="sessions-panel-collapse"
|
|
249
326
|
>
|
|
@@ -286,7 +363,12 @@ export function SessionsPanel(props: SessionsPanelProps): ReactNode {
|
|
|
286
363
|
<button
|
|
287
364
|
type="button"
|
|
288
365
|
className={cls.itemTitle}
|
|
289
|
-
onClick={() =>
|
|
366
|
+
onClick={() => {
|
|
367
|
+
setActiveSession(session.id);
|
|
368
|
+
// Re-selecting the already-active session doesn't change
|
|
369
|
+
// provider state, so close the drawer explicitly too.
|
|
370
|
+
if (drawerMode) setSessionsDrawerOpen(false);
|
|
371
|
+
}}
|
|
290
372
|
data-testid="session-item-open"
|
|
291
373
|
>
|
|
292
374
|
<span>{displayTitle}</span>
|
|
@@ -380,5 +462,6 @@ export function SessionsPanel(props: SessionsPanelProps): ReactNode {
|
|
|
380
462
|
texts={texts.chat}
|
|
381
463
|
/>
|
|
382
464
|
</aside>
|
|
465
|
+
</>
|
|
383
466
|
);
|
|
384
467
|
}
|
|
@@ -263,6 +263,32 @@ export interface AIHubContextValue {
|
|
|
263
263
|
* effect without a page reload.
|
|
264
264
|
*/
|
|
265
265
|
refreshFeatureToggles: () => Promise<void>;
|
|
266
|
+
|
|
267
|
+
/* ── responsive layout ─────────────────────────────────────────── */
|
|
268
|
+
/**
|
|
269
|
+
* True when the component root is narrower than the narrow-layout
|
|
270
|
+
* breakpoint (default 720px). Driven by a `ResizeObserver` on the
|
|
271
|
+
* provider's root element — *container* width, not viewport width —
|
|
272
|
+
* so the library adapts identically in full-page mounts, iframes and
|
|
273
|
+
* partial-width embeds. The provider mirrors this value onto the
|
|
274
|
+
* root element as `data-layout="narrow" | "wide"`, which is what the
|
|
275
|
+
* stylesheet's responsive rules key off.
|
|
276
|
+
*
|
|
277
|
+
* In environments without `ResizeObserver` (jsdom, SSR) this stays
|
|
278
|
+
* `false` (wide).
|
|
279
|
+
*/
|
|
280
|
+
isNarrowLayout: boolean;
|
|
281
|
+
/**
|
|
282
|
+
* Whether the sessions panel is currently open as a mobile overlay
|
|
283
|
+
* drawer. Only meaningful while `isNarrowLayout` is true — the wide
|
|
284
|
+
* layout renders the panel inline and ignores this flag. The provider
|
|
285
|
+
* force-closes the drawer when the layout flips back to wide and when
|
|
286
|
+
* the active agent or session changes (picking a chat should reveal
|
|
287
|
+
* the conversation, not leave the drawer covering it).
|
|
288
|
+
*/
|
|
289
|
+
sessionsDrawerOpen: boolean;
|
|
290
|
+
/** Opens/closes the mobile sessions drawer. */
|
|
291
|
+
setSessionsDrawerOpen: (open: boolean) => void;
|
|
266
292
|
}
|
|
267
293
|
|
|
268
294
|
/**
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import {
|
|
27
27
|
useCallback,
|
|
28
28
|
useEffect,
|
|
29
|
+
useLayoutEffect,
|
|
29
30
|
useMemo,
|
|
30
31
|
useRef,
|
|
31
32
|
useState,
|
|
@@ -172,10 +173,35 @@ export interface AIHubProviderProps {
|
|
|
172
173
|
* the locale; omitted keys fall back to the locale, then English.
|
|
173
174
|
*/
|
|
174
175
|
texts?: DeepPartial<AIHubTexts>;
|
|
176
|
+
/**
|
|
177
|
+
* Container width (px) below which the UI switches to the narrow
|
|
178
|
+
* (mobile) layout — sessions panel becomes an overlay drawer, chrome
|
|
179
|
+
* paddings tighten, touch targets grow. Measured against the
|
|
180
|
+
* component's own root element, not the viewport, so partial-width
|
|
181
|
+
* embeds adapt too. Defaults to `720`.
|
|
182
|
+
*/
|
|
183
|
+
narrowLayoutBreakpoint?: number;
|
|
175
184
|
/** The tree under the provider. */
|
|
176
185
|
children: ReactNode;
|
|
177
186
|
}
|
|
178
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Default `narrowLayoutBreakpoint` — below this container width the
|
|
190
|
+
* narrow layout kicks in. 720px keeps tablets-in-portrait and phones on
|
|
191
|
+
* the drawer layout while desktop half-screen windows stay on the
|
|
192
|
+
* two-column layout.
|
|
193
|
+
*/
|
|
194
|
+
const DEFAULT_NARROW_BREAKPOINT = 720;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* `useLayoutEffect` that degrades to `useEffect` on the server so SSR
|
|
198
|
+
* renders don't warn. The narrow-layout measurement wants layout timing
|
|
199
|
+
* (classify before first paint → no wide-layout flash on phones), but
|
|
200
|
+
* must not break Next.js-style server rendering of wrapper pages.
|
|
201
|
+
*/
|
|
202
|
+
const useIsomorphicLayoutEffect =
|
|
203
|
+
typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
204
|
+
|
|
179
205
|
/**
|
|
180
206
|
* The provider component. Single instance per ai-hub mount — multiple
|
|
181
207
|
* mounts on the same page each manage their own auth state (this is the
|
|
@@ -194,6 +220,7 @@ export function AIHubProvider(props: AIHubProviderProps): ReactNode {
|
|
|
194
220
|
fetch: fetchProp,
|
|
195
221
|
onError,
|
|
196
222
|
tokenStorage = "localStorage",
|
|
223
|
+
narrowLayoutBreakpoint = DEFAULT_NARROW_BREAKPOINT,
|
|
197
224
|
} = props;
|
|
198
225
|
|
|
199
226
|
// ── client ───────────────────────────────────────────────────────────
|
|
@@ -234,6 +261,41 @@ export function AIHubProvider(props: AIHubProviderProps): ReactNode {
|
|
|
234
261
|
return readCachedLanguage() ?? defaultLanguage;
|
|
235
262
|
});
|
|
236
263
|
|
|
264
|
+
// ── responsive layout ────────────────────────────────────────────────
|
|
265
|
+
// Container-width tier, observed on the provider's own root element so
|
|
266
|
+
// the narrow layout also kicks in for partial-width embeds (widget in a
|
|
267
|
+
// sidebar, iframe, …) where viewport media queries would lie. Defaults
|
|
268
|
+
// to wide when `ResizeObserver` is unavailable (SSR, jsdom).
|
|
269
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
270
|
+
const [isNarrowLayout, setIsNarrowLayout] = useState(false);
|
|
271
|
+
const [sessionsDrawerOpen, setSessionsDrawerOpen] = useState(false);
|
|
272
|
+
|
|
273
|
+
useIsomorphicLayoutEffect(() => {
|
|
274
|
+
const el = rootRef.current;
|
|
275
|
+
if (!el || typeof ResizeObserver === "undefined") return undefined;
|
|
276
|
+
const classify = (width: number): void => {
|
|
277
|
+
// Width 0 means "not laid out" (display:none host, detached
|
|
278
|
+
// measurement) — keep the previous tier rather than flapping.
|
|
279
|
+
if (width <= 0) return;
|
|
280
|
+
setIsNarrowLayout(width < narrowLayoutBreakpoint);
|
|
281
|
+
};
|
|
282
|
+
classify(el.getBoundingClientRect().width);
|
|
283
|
+
const observer = new ResizeObserver((entries) => {
|
|
284
|
+
for (const entry of entries) {
|
|
285
|
+
classify(entry.contentRect.width);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
observer.observe(el);
|
|
289
|
+
return () => observer.disconnect();
|
|
290
|
+
}, [narrowLayoutBreakpoint]);
|
|
291
|
+
|
|
292
|
+
// The drawer only exists in the narrow layout — flip back to wide (e.g.
|
|
293
|
+
// phone rotation, window resize) and the inline panel returns, so the
|
|
294
|
+
// overlay state must reset or it would re-open stale on the next shrink.
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
if (!isNarrowLayout) setSessionsDrawerOpen(false);
|
|
297
|
+
}, [isNarrowLayout]);
|
|
298
|
+
|
|
237
299
|
// ── chat state (Step 9) ─────────────────────────────────────────────
|
|
238
300
|
const [agents, setAgents] = useState<AgentSummary[]>([]);
|
|
239
301
|
const [agentsLoading, setAgentsLoading] = useState(false);
|
|
@@ -244,6 +306,12 @@ export function AIHubProvider(props: AIHubProviderProps): ReactNode {
|
|
|
244
306
|
const [activeSessionId, setActiveSessionIdState] = useState<string | null>(
|
|
245
307
|
null,
|
|
246
308
|
);
|
|
309
|
+
|
|
310
|
+
// Picking a chat (or switching agents) should reveal the conversation —
|
|
311
|
+
// close the mobile drawer instead of leaving it covering the messages.
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
setSessionsDrawerOpen(false);
|
|
314
|
+
}, [activeAgentId, activeSessionId]);
|
|
247
315
|
const [messages, setMessages] = useState<AIHubMessage[]>([]);
|
|
248
316
|
const [messagesLoading, setMessagesLoading] = useState(false);
|
|
249
317
|
const [streamState, setStreamState] = useState<AIHubState>(() =>
|
|
@@ -1106,11 +1174,20 @@ export function AIHubProvider(props: AIHubProviderProps): ReactNode {
|
|
|
1106
1174
|
lastTurnError,
|
|
1107
1175
|
featureToggles,
|
|
1108
1176
|
refreshFeatureToggles,
|
|
1177
|
+
isNarrowLayout,
|
|
1178
|
+
sessionsDrawerOpen,
|
|
1179
|
+
setSessionsDrawerOpen,
|
|
1109
1180
|
};
|
|
1110
1181
|
|
|
1111
1182
|
return (
|
|
1112
1183
|
<aiHubContext.Provider value={value}>
|
|
1113
|
-
<div
|
|
1184
|
+
<div
|
|
1185
|
+
ref={rootRef}
|
|
1186
|
+
className="data-club-ai-hub"
|
|
1187
|
+
data-theme={theme}
|
|
1188
|
+
data-layout={isNarrowLayout ? "narrow" : "wide"}
|
|
1189
|
+
lang={language}
|
|
1190
|
+
>
|
|
1114
1191
|
{children}
|
|
1115
1192
|
</div>
|
|
1116
1193
|
</aiHubContext.Provider>
|
package/src/context/useAIHub.ts
CHANGED
|
@@ -103,6 +103,12 @@ export interface UseAIHubResult {
|
|
|
103
103
|
featureToggles: AIHubContextValue["featureToggles"];
|
|
104
104
|
/** Re-fetches the toggles (used after a super-admin save). */
|
|
105
105
|
refreshFeatureToggles: AIHubContextValue["refreshFeatureToggles"];
|
|
106
|
+
/** True while the component root is narrower than the breakpoint. */
|
|
107
|
+
isNarrowLayout: AIHubContextValue["isNarrowLayout"];
|
|
108
|
+
/** Whether the narrow-layout sessions drawer is open. */
|
|
109
|
+
sessionsDrawerOpen: AIHubContextValue["sessionsDrawerOpen"];
|
|
110
|
+
/** Opens/closes the narrow-layout sessions drawer. */
|
|
111
|
+
setSessionsDrawerOpen: AIHubContextValue["setSessionsDrawerOpen"];
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
/**
|
|
@@ -159,5 +165,8 @@ export function useAIHub(): UseAIHubResult {
|
|
|
159
165
|
lastTurnError: ctx.lastTurnError,
|
|
160
166
|
featureToggles: ctx.featureToggles,
|
|
161
167
|
refreshFeatureToggles: ctx.refreshFeatureToggles,
|
|
168
|
+
isNarrowLayout: ctx.isNarrowLayout,
|
|
169
|
+
sessionsDrawerOpen: ctx.sessionsDrawerOpen,
|
|
170
|
+
setSessionsDrawerOpen: ctx.setSessionsDrawerOpen,
|
|
162
171
|
};
|
|
163
172
|
}
|
package/src/state/affordances.ts
CHANGED
|
@@ -47,8 +47,12 @@ export interface AIHubAffordanceFlags {
|
|
|
47
47
|
sessionsPanelResizable: boolean;
|
|
48
48
|
/** Whether the sessions panel can be collapsed. Default true. */
|
|
49
49
|
sessionsPanelCollapsible: boolean;
|
|
50
|
+
/** Whether the sessions panel starts collapsed on load. Default **false**. */
|
|
51
|
+
sessionsPanelDefaultCollapsed: boolean;
|
|
50
52
|
/** Whether the "+ New chat" button renders on the panel. Default true. */
|
|
51
53
|
createSessionButtonEnabled: boolean;
|
|
54
|
+
/** Whether the last session auto-opens on load (created when none). Default **false**. */
|
|
55
|
+
loadLastSessionEnabled: boolean;
|
|
52
56
|
/** Whether the input area renders (read-only when false). Default true. */
|
|
53
57
|
userInputEnabled: boolean;
|
|
54
58
|
/** Whether the stop button is exposed while a turn is running. Default true. */
|
|
@@ -76,7 +80,9 @@ export const DEFAULT_AFFORDANCE_FLAGS: AIHubAffordanceFlags = Object.freeze({
|
|
|
76
80
|
sessionsPanelEnabled: true,
|
|
77
81
|
sessionsPanelResizable: true,
|
|
78
82
|
sessionsPanelCollapsible: true,
|
|
83
|
+
sessionsPanelDefaultCollapsed: false,
|
|
79
84
|
createSessionButtonEnabled: true,
|
|
85
|
+
loadLastSessionEnabled: false,
|
|
80
86
|
userInputEnabled: true,
|
|
81
87
|
stopButtonEnabled: true,
|
|
82
88
|
toolCallsVisible: true,
|
|
@@ -100,7 +106,9 @@ export const FEATURE_TOGGLE_KEY_MAP: ReadonlyArray<
|
|
|
100
106
|
["sessions_panel_enabled", "sessionsPanelEnabled"],
|
|
101
107
|
["sessions_panel_resizable", "sessionsPanelResizable"],
|
|
102
108
|
["sessions_panel_collapsible", "sessionsPanelCollapsible"],
|
|
109
|
+
["sessions_panel_default_collapsed", "sessionsPanelDefaultCollapsed"],
|
|
103
110
|
["create_session_button_enabled", "createSessionButtonEnabled"],
|
|
111
|
+
["load_last_session_enabled", "loadLastSessionEnabled"],
|
|
104
112
|
["user_input_enabled", "userInputEnabled"],
|
|
105
113
|
["stop_button_enabled", "stopButtonEnabled"],
|
|
106
114
|
["tool_calls_visible", "toolCallsVisible"],
|
|
@@ -138,7 +146,7 @@ function resolveOne(
|
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
/**
|
|
141
|
-
* Composes the three layers into a flat resolved record of all
|
|
149
|
+
* Composes the three layers into a flat resolved record of all 16
|
|
142
150
|
* affordance booleans.
|
|
143
151
|
*
|
|
144
152
|
* Per §8.4: server toggle wins; falling through, the React prop wins;
|
|
@@ -114,6 +114,9 @@ export function MockProvider(props: {
|
|
|
114
114
|
lastTurnError: opts.lastTurnError ?? null,
|
|
115
115
|
featureToggles: opts.featureToggles ?? null,
|
|
116
116
|
refreshFeatureToggles: noopAsync,
|
|
117
|
+
isNarrowLayout: false,
|
|
118
|
+
sessionsDrawerOpen: false,
|
|
119
|
+
setSessionsDrawerOpen: () => undefined,
|
|
117
120
|
};
|
|
118
121
|
return (
|
|
119
122
|
<aiHubContext.Provider value={value}>{props.children}</aiHubContext.Provider>
|
package/src/styles/CLASSES.md
CHANGED
|
@@ -26,6 +26,20 @@ properties only, never the attribute directly.
|
|
|
26
26
|
.data-club-ai-hub[data-theme="dark"] { --dc-aih-bg: #0f1117; }
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
### Layout attribute (responsive)
|
|
30
|
+
|
|
31
|
+
The provider observes the component root's **own width** (ResizeObserver,
|
|
32
|
+
not viewport media queries) and stamps `data-layout="narrow"` or
|
|
33
|
+
`data-layout="wide"` on the same root `<div>`. The breakpoint defaults to
|
|
34
|
+
720px and is overridable via the provider's `narrowLayoutBreakpoint` prop.
|
|
35
|
+
In the narrow layout the sessions panel renders as an overlay drawer, the
|
|
36
|
+
chat header gains a hamburger button, paddings tighten and touch targets
|
|
37
|
+
grow. Wrappers can scope their own mobile brand tweaks the same way:
|
|
38
|
+
|
|
39
|
+
```css
|
|
40
|
+
.data-club-ai-hub[data-layout="narrow"] .my-brand-footer { flex-direction: column; }
|
|
41
|
+
```
|
|
42
|
+
|
|
29
43
|
## CSS custom properties (theming tokens)
|
|
30
44
|
|
|
31
45
|
Per-customer wrappers override these to brand the UI; they don't fork the
|
|
@@ -204,6 +218,10 @@ block and modifier.
|
|
|
204
218
|
| `data-club-ai-hub__layout--panel-right` | Modifier when the panel is on the right. |
|
|
205
219
|
| `data-club-ai-hub__sessions-panel` | The sessions sidebar. |
|
|
206
220
|
| `data-club-ai-hub__sessions-panel--collapsed` | Collapsed state (thin reveal strip). |
|
|
221
|
+
| `data-club-ai-hub__sessions-panel--drawer` | Narrow-layout overlay-drawer mode (replaces the inline column). |
|
|
222
|
+
| `data-club-ai-hub__sessions-panel--drawer-open` | Drawer slid in / visible. |
|
|
223
|
+
| `data-club-ai-hub__sessions-panel__scrim` | Backdrop scrim behind the open drawer (`data-open` attribute mirrors the state). |
|
|
224
|
+
| `data-club-ai-hub__sessions-panel__drawer-close` | Close (✕) button at the top of the drawer. |
|
|
207
225
|
| `data-club-ai-hub__sessions-panel__custom-header` | Slot wrapper at the top of the panel. |
|
|
208
226
|
| `data-club-ai-hub__sessions-panel__new-button` | "+ New chat" button. |
|
|
209
227
|
| `data-club-ai-hub__sessions-panel__reveal` | Reveal-strip button shown in collapsed state. |
|
|
@@ -229,6 +247,7 @@ block and modifier.
|
|
|
229
247
|
| `data-club-ai-hub__main-column` | Right side of the layout (header + body). |
|
|
230
248
|
| `data-club-ai-hub__main-column__body` | Body region under the chat header. |
|
|
231
249
|
| `data-club-ai-hub__chat-header` | The strip above the messages with back-link + agent name. |
|
|
250
|
+
| `data-club-ai-hub__chat-header__menu` | Hamburger button opening the sessions drawer (narrow layout only). |
|
|
232
251
|
| `data-club-ai-hub__chat-header__back` | `[← Agents]` back-to-picker link. |
|
|
233
252
|
| `data-club-ai-hub__chat-header__title` | Agent display-name text. |
|
|
234
253
|
| `data-club-ai-hub__chat-header__extras` | Slot for `customHeaderContent`. |
|