@btst/stack 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +2 -2
  2. package/dist/packages/stack/src/plugins/ai-chat/api/compaction.cjs +249 -0
  3. package/dist/packages/stack/src/plugins/ai-chat/api/compaction.mjs +241 -0
  4. package/dist/packages/stack/src/plugins/ai-chat/api/operations.cjs +20 -0
  5. package/dist/packages/stack/src/plugins/ai-chat/api/operations.mjs +20 -0
  6. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +5 -0
  7. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +5 -0
  8. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +38 -3
  9. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +38 -3
  10. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +173 -72
  11. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +176 -75
  12. package/dist/packages/stack/src/plugins/ai-chat/client/context-checkpoint.cjs +30 -0
  13. package/dist/packages/stack/src/plugins/ai-chat/client/context-checkpoint.mjs +27 -0
  14. package/dist/packages/stack/src/plugins/ai-chat/schemas.cjs +2 -0
  15. package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +2 -0
  16. package/dist/packages/ui/src/components/markdown-content.cjs +9 -9
  17. package/dist/packages/ui/src/components/markdown-content.mjs +9 -9
  18. package/dist/plugins/ai-chat/api/index.d.cts +2 -2
  19. package/dist/plugins/ai-chat/api/index.d.mts +2 -2
  20. package/dist/plugins/ai-chat/api/index.d.ts +2 -2
  21. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  22. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  23. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  24. package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
  25. package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
  26. package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
  27. package/dist/plugins/ai-chat/client/index.d.cts +3 -3
  28. package/dist/plugins/ai-chat/client/index.d.mts +3 -3
  29. package/dist/plugins/ai-chat/client/index.d.ts +3 -3
  30. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  31. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  32. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  33. package/dist/plugins/blog/api/index.d.cts +2 -2
  34. package/dist/plugins/blog/api/index.d.mts +2 -2
  35. package/dist/plugins/blog/api/index.d.ts +2 -2
  36. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  37. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  38. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  39. package/dist/plugins/blog/client/index.d.cts +2 -2
  40. package/dist/plugins/blog/client/index.d.mts +2 -2
  41. package/dist/plugins/blog/client/index.d.ts +2 -2
  42. package/dist/plugins/blog/query-keys.d.cts +2 -2
  43. package/dist/plugins/blog/query-keys.d.mts +2 -2
  44. package/dist/plugins/blog/query-keys.d.ts +2 -2
  45. package/dist/shared/{stack.BTStHVTy.d.cts → stack.BGzgFx3R.d.cts} +5 -5
  46. package/dist/shared/{stack.DBH1Bjko.d.ts → stack.C4zspX0T.d.ts} +13 -2
  47. package/dist/shared/{stack.94kN3pEK.d.mts → stack.CjCSDfWy.d.mts} +5 -5
  48. package/dist/shared/{stack.D5b9l24t.d.ts → stack.Clil75lW.d.ts} +35 -35
  49. package/dist/shared/{stack.xAiy2VtO.d.cts → stack.DDQlE2Pm.d.cts} +10 -3
  50. package/dist/shared/{stack.Ce6V7eDj.d.cts → stack.DNw6Hv5o.d.cts} +35 -35
  51. package/dist/shared/{stack.CTneIW0u.d.mts → stack.DhZtP2JZ.d.mts} +35 -35
  52. package/dist/shared/{stack.C-eQN_Jt.d.ts → stack.DlehgNZZ.d.ts} +10 -3
  53. package/dist/shared/{stack.DfrRYOIl.d.ts → stack.Dpg_JjIo.d.ts} +5 -5
  54. package/dist/shared/{stack.B7Mj74BQ.d.mts → stack.Ps3MDhTC.d.mts} +13 -2
  55. package/dist/shared/{stack.C5FZr-jJ.d.mts → stack.tAUeRAtd.d.mts} +10 -3
  56. package/dist/shared/{stack.Dgt3MCtW.d.cts → stack.ymWsP4HZ.d.cts} +13 -2
  57. package/package.json +2 -2
  58. package/src/__tests__/markdown-link-navigation.test.tsx +29 -0
  59. package/src/plugins/ai-chat/__tests__/classnames-overrides.test.tsx +22 -2
  60. package/src/plugins/ai-chat/__tests__/compaction.test.ts +272 -0
  61. package/src/plugins/ai-chat/__tests__/page-widget.test.tsx +103 -1
  62. package/src/plugins/ai-chat/api/compaction.ts +310 -0
  63. package/src/plugins/ai-chat/api/operations.ts +25 -0
  64. package/src/plugins/ai-chat/api/plugin.ts +14 -0
  65. package/src/plugins/ai-chat/client/components/chat-interface.tsx +58 -3
  66. package/src/plugins/ai-chat/client/components/chat-layout.tsx +197 -72
  67. package/src/plugins/ai-chat/client/context-checkpoint.ts +47 -0
  68. package/src/plugins/ai-chat/client/localization/index.ts +5 -0
  69. package/src/plugins/ai-chat/schemas.ts +2 -0
  70. package/dist/shared/{stack.CKc_Bais.d.cts → stack.Csi-4Mgz.d.cts} +7 -7
  71. package/dist/shared/{stack.CKc_Bais.d.mts → stack.Csi-4Mgz.d.mts} +7 -7
  72. package/dist/shared/{stack.CKc_Bais.d.ts → stack.Csi-4Mgz.d.ts} +7 -7
@@ -21,6 +21,7 @@ const chatHooks = require('../hooks/chat-hooks.cjs');
21
21
  const plugins_aiChat_client_context_pageAiContext = require('../../../../../../../plugins/ai-chat/client/context/page-ai-context.cjs');
22
22
  const chatAnalytics = require('../context/chat-analytics.cjs');
23
23
  const navigation = require('../navigation.cjs');
24
+ const contextCheckpoint = require('../context-checkpoint.cjs');
24
25
 
25
26
  function reconcilePersistedMessageIds(messages, persistedMessages) {
26
27
  const userMessages = messages.filter((message) => message.role === "user");
@@ -208,6 +209,9 @@ function ChatInterface({
208
209
  const latestStreamRequestGeneration = React.useRef(0);
209
210
  const pendingStreamRequests = React.useRef([]);
210
211
  const latestMessages = React.useRef([]);
212
+ const contextCheckpoint$1 = React.useRef(void 0);
213
+ const submittedMessages = React.useRef([]);
214
+ const [isCompacting, setIsCompacting] = React.useState(false);
211
215
  const conversationsListQueryKey = React.useMemo(() => {
212
216
  if (isPublicMode) return ["ai-chat", "disabled"];
213
217
  const client$1 = client.createApiClient({
@@ -307,6 +311,7 @@ function ChatInterface({
307
311
  pendingStreamRequests.current.push(request);
308
312
  activeStreamPartitionKey.current = request.identityPartitionKey;
309
313
  }
314
+ setIsCompacting(false);
310
315
  const currentPageContext = pageAIContextRef.current;
311
316
  const pageContextBody = currentPageContext?.pageDescription ? {
312
317
  pageContext: currentPageContext.pageDescription,
@@ -319,18 +324,20 @@ function ChatInterface({
319
324
  if (newUserMessage) {
320
325
  messagesToSend.push(newUserMessage);
321
326
  }
327
+ submittedMessages.current = messagesToSend;
322
328
  editMessagesRef.current = null;
323
329
  return {
324
330
  body: {
325
- messages: messagesToSend,
331
+ ...isPublicMode ? contextCheckpoint.contextRequest(messagesToSend, contextCheckpoint$1.current) : { messages: messagesToSend },
326
332
  conversationId: conversationIdRef.current,
327
333
  ...pageContextBody
328
334
  }
329
335
  };
330
336
  }
337
+ submittedMessages.current = hookMessages;
331
338
  return {
332
339
  body: {
333
- messages: hookMessages,
340
+ ...isPublicMode ? contextCheckpoint.contextRequest(hookMessages, contextCheckpoint$1.current) : { messages: hookMessages },
334
341
  conversationId: conversationIdRef.current,
335
342
  ...pageContextBody
336
343
  }
@@ -374,6 +381,20 @@ function ChatInterface({
374
381
  } = react.useChat({
375
382
  id: `${chatInstanceId}:${isPublicMode ? "public" : identitySessionVersion}`,
376
383
  transport,
384
+ onData: ({ type, data }) => {
385
+ if (!isPublicMode || !mounted.current || !data || typeof data !== "object")
386
+ return;
387
+ if (type === "data-context-status" && "compacting" in data) {
388
+ setIsCompacting(data.compacting === true);
389
+ }
390
+ if (type === "data-context-checkpoint" && "summary" in data && "throughMessageId" in data && typeof data.summary === "string" && typeof data.throughMessageId === "string") {
391
+ contextCheckpoint$1.current = contextCheckpoint.captureContextCheckpoint(
392
+ submittedMessages.current,
393
+ data.summary,
394
+ data.throughMessageId
395
+ );
396
+ }
397
+ },
377
398
  // Automatically resubmit after all client-side tool results are provided
378
399
  sendAutomaticallyWhen: (options) => mounted.current && (isPublicMode || identitySessionVersion === identitySessionGeneration.current) && ai.lastAssistantMessageIsCompleteWithToolCalls(options),
379
400
  onToolCall: async ({ toolCall }) => {
@@ -424,6 +445,7 @@ function ChatInterface({
424
445
  }
425
446
  },
426
447
  onError: (err) => {
448
+ setIsCompacting(false);
427
449
  if (!mounted.current) return;
428
450
  console.error("useChat onError:", err);
429
451
  if (!isPublicMode && identitySessionVersion !== identitySessionGeneration.current) {
@@ -437,6 +459,7 @@ function ChatInterface({
437
459
  trackFailure();
438
460
  },
439
461
  onFinish: async (completion) => {
462
+ setIsCompacting(false);
440
463
  if (!mounted.current || !isPublicMode && identitySessionVersion !== identitySessionGeneration.current)
441
464
  return;
442
465
  if (completion?.isError || completion?.isDisconnect) trackFailure();
@@ -932,7 +955,19 @@ function ChatInterface({
932
955
  },
933
956
  m.id || `msg-${index}`
934
957
  )),
935
- isLoading && messages[messages.length - 1]?.role !== "assistant" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 text-muted-foreground text-sm py-4", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "animate-pulse", children: tr("CHAT_LOADING", "aiChat.chat.loading", "Thinking...") }) }),
958
+ isLoading && isCompacting && /* @__PURE__ */ jsxRuntime.jsx(
959
+ "div",
960
+ {
961
+ role: "status",
962
+ className: "text-muted-foreground text-sm py-4",
963
+ children: tr(
964
+ "CHAT_COMPACTING",
965
+ "aiChat.chat.compacting",
966
+ "Summarizing earlier conversation\u2026"
967
+ )
968
+ }
969
+ ),
970
+ isLoading && !isCompacting && messages[messages.length - 1]?.role !== "assistant" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 text-muted-foreground text-sm py-4", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "animate-pulse", children: tr("CHAT_LOADING", "aiChat.chat.loading", "Thinking...") }) }),
936
971
  (error || historySyncError) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-destructive text-sm py-4 px-3 bg-destructive/10 rounded-md", children: [
937
972
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: tr(
938
973
  "CHAT_ERROR",
@@ -19,6 +19,7 @@ import { useAiChatIdentityPartition, useConversation, useConversations } from '.
19
19
  import { usePageAIContext } from '../../../../../../../plugins/ai-chat/client/context/page-ai-context.mjs';
20
20
  import { useChatAnalytics } from '../context/chat-analytics.mjs';
21
21
  import { navigateAiChatCrossOrigin } from '../navigation.mjs';
22
+ import { contextRequest, captureContextCheckpoint } from '../context-checkpoint.mjs';
22
23
 
23
24
  function reconcilePersistedMessageIds(messages, persistedMessages) {
24
25
  const userMessages = messages.filter((message) => message.role === "user");
@@ -206,6 +207,9 @@ function ChatInterface({
206
207
  const latestStreamRequestGeneration = useRef(0);
207
208
  const pendingStreamRequests = useRef([]);
208
209
  const latestMessages = useRef([]);
210
+ const contextCheckpoint = useRef(void 0);
211
+ const submittedMessages = useRef([]);
212
+ const [isCompacting, setIsCompacting] = useState(false);
209
213
  const conversationsListQueryKey = useMemo(() => {
210
214
  if (isPublicMode) return ["ai-chat", "disabled"];
211
215
  const client = createApiClient({
@@ -305,6 +309,7 @@ function ChatInterface({
305
309
  pendingStreamRequests.current.push(request);
306
310
  activeStreamPartitionKey.current = request.identityPartitionKey;
307
311
  }
312
+ setIsCompacting(false);
308
313
  const currentPageContext = pageAIContextRef.current;
309
314
  const pageContextBody = currentPageContext?.pageDescription ? {
310
315
  pageContext: currentPageContext.pageDescription,
@@ -317,18 +322,20 @@ function ChatInterface({
317
322
  if (newUserMessage) {
318
323
  messagesToSend.push(newUserMessage);
319
324
  }
325
+ submittedMessages.current = messagesToSend;
320
326
  editMessagesRef.current = null;
321
327
  return {
322
328
  body: {
323
- messages: messagesToSend,
329
+ ...isPublicMode ? contextRequest(messagesToSend, contextCheckpoint.current) : { messages: messagesToSend },
324
330
  conversationId: conversationIdRef.current,
325
331
  ...pageContextBody
326
332
  }
327
333
  };
328
334
  }
335
+ submittedMessages.current = hookMessages;
329
336
  return {
330
337
  body: {
331
- messages: hookMessages,
338
+ ...isPublicMode ? contextRequest(hookMessages, contextCheckpoint.current) : { messages: hookMessages },
332
339
  conversationId: conversationIdRef.current,
333
340
  ...pageContextBody
334
341
  }
@@ -372,6 +379,20 @@ function ChatInterface({
372
379
  } = useChat({
373
380
  id: `${chatInstanceId}:${isPublicMode ? "public" : identitySessionVersion}`,
374
381
  transport,
382
+ onData: ({ type, data }) => {
383
+ if (!isPublicMode || !mounted.current || !data || typeof data !== "object")
384
+ return;
385
+ if (type === "data-context-status" && "compacting" in data) {
386
+ setIsCompacting(data.compacting === true);
387
+ }
388
+ if (type === "data-context-checkpoint" && "summary" in data && "throughMessageId" in data && typeof data.summary === "string" && typeof data.throughMessageId === "string") {
389
+ contextCheckpoint.current = captureContextCheckpoint(
390
+ submittedMessages.current,
391
+ data.summary,
392
+ data.throughMessageId
393
+ );
394
+ }
395
+ },
375
396
  // Automatically resubmit after all client-side tool results are provided
376
397
  sendAutomaticallyWhen: (options) => mounted.current && (isPublicMode || identitySessionVersion === identitySessionGeneration.current) && lastAssistantMessageIsCompleteWithToolCalls(options),
377
398
  onToolCall: async ({ toolCall }) => {
@@ -422,6 +443,7 @@ function ChatInterface({
422
443
  }
423
444
  },
424
445
  onError: (err) => {
446
+ setIsCompacting(false);
425
447
  if (!mounted.current) return;
426
448
  console.error("useChat onError:", err);
427
449
  if (!isPublicMode && identitySessionVersion !== identitySessionGeneration.current) {
@@ -435,6 +457,7 @@ function ChatInterface({
435
457
  trackFailure();
436
458
  },
437
459
  onFinish: async (completion) => {
460
+ setIsCompacting(false);
438
461
  if (!mounted.current || !isPublicMode && identitySessionVersion !== identitySessionGeneration.current)
439
462
  return;
440
463
  if (completion?.isError || completion?.isDisconnect) trackFailure();
@@ -930,7 +953,19 @@ function ChatInterface({
930
953
  },
931
954
  m.id || `msg-${index}`
932
955
  )),
933
- isLoading && messages[messages.length - 1]?.role !== "assistant" && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 text-muted-foreground text-sm py-4", children: /* @__PURE__ */ jsx("div", { className: "animate-pulse", children: tr("CHAT_LOADING", "aiChat.chat.loading", "Thinking...") }) }),
956
+ isLoading && isCompacting && /* @__PURE__ */ jsx(
957
+ "div",
958
+ {
959
+ role: "status",
960
+ className: "text-muted-foreground text-sm py-4",
961
+ children: tr(
962
+ "CHAT_COMPACTING",
963
+ "aiChat.chat.compacting",
964
+ "Summarizing earlier conversation\u2026"
965
+ )
966
+ }
967
+ ),
968
+ isLoading && !isCompacting && messages[messages.length - 1]?.role !== "assistant" && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 text-muted-foreground text-sm py-4", children: /* @__PURE__ */ jsx("div", { className: "animate-pulse", children: tr("CHAT_LOADING", "aiChat.chat.loading", "Thinking...") }) }),
934
969
  (error || historySyncError) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-destructive text-sm py-4 px-3 bg-destructive/10 rounded-md", children: [
935
970
  /* @__PURE__ */ jsx("span", { className: "flex-1", children: tr(
936
971
  "CHAT_ERROR",
@@ -3,6 +3,8 @@
3
3
 
4
4
  const jsxRuntime = require('react/jsx-runtime');
5
5
  const React = require('react');
6
+ const ReactDOM = require('react-dom');
7
+ const dialog = require('../../../../../../ui/src/components/dialog.cjs');
6
8
  const button = require('../../../../../../ui/src/components/button.cjs');
7
9
  const badge = require('../../../../../../ui/src/components/badge.cjs');
8
10
  const sheet = require('../../../../../../ui/src/components/sheet.cjs');
@@ -36,14 +38,44 @@ function ChatLayoutContent(props) {
36
38
  const resolvedMode = overrides.resolveAiChatMode(plugins?.aiChat?.config);
37
39
  const showSidebar = requestedShowSidebar && resolvedMode !== "public";
38
40
  const tr = index.useAiChatTranslation(localization);
39
- const widgetHeight = props.layout === "widget" ? props.widgetHeight ?? "min(600px, calc(100dvh - 112px))" : void 0;
40
- const widgetWidth = props.layout === "widget" ? props.widgetWidth ?? "min(380px, calc(100vw - 32px))" : void 0;
41
+ const widgetHeight = props.layout === "widget" ? props.widgetHeight ?? "min(640px, calc(100dvh - 112px))" : void 0;
42
+ const widgetWidth = props.layout === "widget" ? props.widgetWidth ?? "min(440px, calc(100vw - 32px))" : void 0;
41
43
  const defaultOpen = props.layout === "widget" ? props.defaultOpen ?? false : false;
42
44
  const showTrigger = props.layout === "widget" ? props.showTrigger ?? true : true;
45
+ const triggerIcon = props.layout === "widget" ? props.triggerIcon : void 0;
43
46
  const [sidebarOpen, setSidebarOpen] = React.useState(true);
44
47
  const [mobileSidebarOpen, setMobileSidebarOpen] = React.useState(false);
45
48
  const [chatResetKey, setChatResetKey] = React.useState(0);
46
49
  const [widgetOpen, setWidgetOpen] = React.useState(defaultOpen);
50
+ const [expanded, setExpanded] = React.useState(false);
51
+ const expandable = props.layout === "widget" && props.expandable === true;
52
+ const widgetExpanded = expandable && expanded && widgetOpen;
53
+ React.useEffect(() => {
54
+ if (!expandable) setExpanded(false);
55
+ }, [expandable]);
56
+ const compactPanelRef = React.useRef(null);
57
+ const triggerRef = React.useRef(null);
58
+ const expandRef = React.useRef(null);
59
+ const [panelContainer, setPanelContainer] = React.useState(
60
+ null
61
+ );
62
+ const expandLabel = widgetExpanded ? tr("A11Y_COLLAPSE_CHAT", "aiChat.a11y.collapseChat", "Collapse chat") : tr("A11Y_EXPAND_CHAT", "aiChat.a11y.expandChat", "Expand chat");
63
+ React.useEffect(() => {
64
+ if (layout !== "widget") return;
65
+ const container = document.createElement("div");
66
+ container.className = "flex h-full min-h-0 flex-col";
67
+ compactPanelRef.current?.appendChild(container);
68
+ setPanelContainer(container);
69
+ return () => container.remove();
70
+ }, [layout]);
71
+ const mountExpandedPanel = React.useCallback(
72
+ (node) => {
73
+ if (panelContainer) {
74
+ (node ?? compactPanelRef.current)?.appendChild(panelContainer);
75
+ }
76
+ },
77
+ [panelContainer]
78
+ );
47
79
  const [widgetResetKey, setWidgetResetKey] = React.useState(0);
48
80
  const [widgetEverOpened, setWidgetEverOpened] = React.useState(defaultOpen);
49
81
  const [tipDismissed, setTipDismissed] = React.useState(defaultOpen);
@@ -121,97 +153,166 @@ function ChatLayoutContent(props) {
121
153
  ]
122
154
  }
123
155
  ),
124
- /* @__PURE__ */ jsxRuntime.jsxs(
156
+ /* @__PURE__ */ jsxRuntime.jsx(
125
157
  "div",
126
158
  {
159
+ ref: compactPanelRef,
160
+ role: "dialog",
161
+ tabIndex: -1,
162
+ "aria-label": tr("A11Y_CHAT_TITLE", "aiChat.a11y.title", "AI Chat"),
163
+ className: utils.cn(
164
+ "overflow-hidden rounded-xl border bg-background shadow-xl",
165
+ (!widgetOpen || widgetExpanded) && "hidden"
166
+ ),
167
+ style: { height: widgetHeight, width: widgetWidth }
168
+ }
169
+ ),
170
+ /* @__PURE__ */ jsxRuntime.jsx(dialog.Dialog, { open: widgetExpanded, onOpenChange: setExpanded, children: /* @__PURE__ */ jsxRuntime.jsxs(
171
+ dialog.DialogContent,
172
+ {
173
+ showCloseButton: false,
174
+ style: {
175
+ ...style,
176
+ position: "fixed",
177
+ inset: 0,
178
+ top: 0,
179
+ right: 0,
180
+ bottom: 0,
181
+ left: 0,
182
+ width: "100vw",
183
+ height: "100dvh",
184
+ minWidth: 0,
185
+ minHeight: 0,
186
+ maxWidth: "none",
187
+ maxHeight: "none",
188
+ margin: 0,
189
+ transform: "none",
190
+ translate: "none",
191
+ zIndex: 50,
192
+ animation: "none"
193
+ },
194
+ "aria-describedby": void 0,
127
195
  className: utils.cn(
128
- "flex flex-col border rounded-xl overflow-hidden bg-background shadow-xl",
129
- widgetOpen ? "flex" : "hidden"
196
+ className,
197
+ classNames?.container,
198
+ "fixed inset-0 z-50 flex h-dvh w-screen max-w-none translate-x-0 translate-y-0 flex-col gap-0 rounded-none border-0 p-0 sm:max-w-none pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]"
130
199
  ),
131
- style: { height: widgetHeight, width: widgetWidth },
200
+ onCloseAutoFocus: (event) => {
201
+ const target = widgetOpen ? expandRef.current ?? compactPanelRef.current : triggerRef.current;
202
+ if (target) {
203
+ event.preventDefault();
204
+ target.focus();
205
+ }
206
+ },
132
207
  children: [
133
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 px-3 py-1.5 border-b bg-muted/40", children: [
134
- /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Sparkles, { className: "h-3 w-3 text-muted-foreground" }),
135
- pageAIContext ? /* @__PURE__ */ jsxRuntime.jsx(
136
- badge.Badge,
137
- {
138
- variant: "secondary",
139
- className: "text-xs",
140
- "data-testid": "page-context-badge",
141
- children: pageAIContext.routeName
142
- }
143
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground font-medium", children: tr("A11Y_CHAT_TITLE", "aiChat.a11y.title", "AI Chat") }),
144
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1" }),
145
- /* @__PURE__ */ jsxRuntime.jsx(
146
- button.Button,
147
- {
148
- variant: "ghost",
149
- size: "icon",
150
- className: "h-5 w-5",
151
- onClick: () => {
152
- track({ type: "conversation_cleared" });
153
- onClear?.();
154
- setWidgetResetKey((prev) => prev + 1);
155
- },
156
- "aria-label": tr(
157
- "A11Y_CLEAR_CHAT",
158
- "aiChat.a11y.clearChat",
159
- "Clear chat"
160
- ),
161
- title: tr(
162
- "A11Y_CLEAR_CHAT",
163
- "aiChat.a11y.clearChat",
164
- "Clear chat"
165
- ),
166
- children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Trash2, { className: "h-3.5 w-3.5" })
167
- }
168
- ),
169
- /* @__PURE__ */ jsxRuntime.jsx(
170
- button.Button,
171
- {
172
- variant: "ghost",
173
- size: "icon",
174
- className: "h-5 w-5",
175
- onClick: () => {
176
- setWidgetOpen(false);
177
- track({ type: "widget_closed" });
178
- },
179
- "aria-label": tr(
180
- "A11Y_CLOSE_CHAT",
181
- "aiChat.a11y.closeChat",
182
- "Close chat"
183
- ),
184
- children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.X, { className: "h-3.5 w-3.5" })
185
- }
186
- )
187
- ] }),
188
- widgetEverOpened && /* @__PURE__ */ jsxRuntime.jsx(
189
- chatInterface.ChatInterface,
190
- {
191
- id: conversationId,
192
- variant: "widget",
193
- initialMessages,
194
- onMessagesChange
195
- },
196
- `widget-${conversationId ?? "new"}-${widgetResetKey}`
197
- )
208
+ /* @__PURE__ */ jsxRuntime.jsx(dialog.DialogTitle, { className: "sr-only", children: tr("A11Y_CHAT_TITLE", "aiChat.a11y.title", "AI Chat") }),
209
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: mountExpandedPanel, className: "min-h-0 flex-1" })
198
210
  ]
199
211
  }
212
+ ) }),
213
+ panelContainer && ReactDOM.createPortal(
214
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
215
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-1.5 px-3 py-1.5 border-b bg-muted/40", children: [
216
+ /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Sparkles, { className: "h-3 w-3 text-muted-foreground" }),
217
+ pageAIContext ? /* @__PURE__ */ jsxRuntime.jsx(
218
+ badge.Badge,
219
+ {
220
+ variant: "secondary",
221
+ className: "text-xs",
222
+ "data-testid": "page-context-badge",
223
+ children: pageAIContext.routeName
224
+ }
225
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground font-medium", children: tr("A11Y_CHAT_TITLE", "aiChat.a11y.title", "AI Chat") }),
226
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1" }),
227
+ expandable && /* @__PURE__ */ jsxRuntime.jsx(
228
+ button.Button,
229
+ {
230
+ type: "button",
231
+ variant: "ghost",
232
+ size: "icon",
233
+ className: "h-7 w-7",
234
+ ref: expandRef,
235
+ onClick: () => setExpanded((prev) => !prev),
236
+ "aria-label": expandLabel,
237
+ title: expandLabel,
238
+ "aria-expanded": widgetExpanded,
239
+ children: widgetExpanded ? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Minimize2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Maximize2, { className: "h-3.5 w-3.5", "aria-hidden": "true" })
240
+ }
241
+ ),
242
+ /* @__PURE__ */ jsxRuntime.jsx(
243
+ button.Button,
244
+ {
245
+ variant: "ghost",
246
+ size: "icon",
247
+ className: "h-5 w-5",
248
+ onClick: () => {
249
+ track({ type: "conversation_cleared" });
250
+ onClear?.();
251
+ setWidgetResetKey((prev) => prev + 1);
252
+ },
253
+ "aria-label": tr(
254
+ "A11Y_CLEAR_CHAT",
255
+ "aiChat.a11y.clearChat",
256
+ "Clear chat"
257
+ ),
258
+ title: tr(
259
+ "A11Y_CLEAR_CHAT",
260
+ "aiChat.a11y.clearChat",
261
+ "Clear chat"
262
+ ),
263
+ children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Trash2, { className: "h-3.5 w-3.5" })
264
+ }
265
+ ),
266
+ /* @__PURE__ */ jsxRuntime.jsx(
267
+ button.Button,
268
+ {
269
+ variant: "ghost",
270
+ size: "icon",
271
+ className: "h-5 w-5",
272
+ onClick: () => {
273
+ setWidgetOpen(false);
274
+ setExpanded(false);
275
+ triggerRef.current?.focus();
276
+ track({ type: "widget_closed" });
277
+ },
278
+ "aria-label": tr(
279
+ "A11Y_CLOSE_CHAT",
280
+ "aiChat.a11y.closeChat",
281
+ "Close chat"
282
+ ),
283
+ children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.X, { className: "h-3.5 w-3.5" })
284
+ }
285
+ )
286
+ ] }),
287
+ widgetEverOpened && /* @__PURE__ */ jsxRuntime.jsx(
288
+ chatInterface.ChatInterface,
289
+ {
290
+ id: conversationId,
291
+ variant: "widget",
292
+ initialMessages,
293
+ onMessagesChange
294
+ },
295
+ `widget-${conversationId ?? "new"}-${widgetResetKey}`
296
+ )
297
+ ] }),
298
+ panelContainer
200
299
  ),
201
300
  showTrigger && /* @__PURE__ */ jsxRuntime.jsx(
202
301
  button.Button,
203
302
  {
303
+ ref: triggerRef,
204
304
  size: "icon",
205
305
  className: "h-12 w-12 rounded-full shadow-lg",
206
306
  onClick: () => {
207
307
  dismissTip("open");
208
308
  track({ type: widgetOpen ? "widget_closed" : "widget_opened" });
209
309
  setWidgetOpen((prev) => !prev);
310
+ setExpanded(false);
210
311
  setWidgetEverOpened(true);
211
312
  },
212
313
  "aria-label": widgetOpen ? tr("A11Y_CLOSE_CHAT", "aiChat.a11y.closeChat", "Close chat") : tr("A11Y_OPEN_CHAT", "aiChat.a11y.openChat", "Open chat"),
213
314
  "data-testid": "widget-trigger",
214
- children: widgetOpen ? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.X, { className: "h-5 w-5" }) : /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Sparkles, { className: "h-5 w-5" })
315
+ children: widgetOpen ? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.X, { className: "h-5 w-5" }) : triggerIcon ?? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Sparkles, { className: "h-5 w-5" })
215
316
  }
216
317
  )
217
318
  ]