@btst/stack 4.1.1 → 4.3.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.
- package/README.md +2 -2
- package/dist/packages/stack/src/plugins/ai-chat/api/operations.cjs +15 -9
- package/dist/packages/stack/src/plugins/ai-chat/api/operations.mjs +16 -10
- package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.cjs +28 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.mjs +29 -2
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +5 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +5 -0
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +91 -8
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +91 -8
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +242 -68
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +246 -72
- package/dist/packages/stack/src/plugins/ai-chat/client/components/tool-call-display.cjs +17 -0
- package/dist/packages/stack/src/plugins/ai-chat/client/components/tool-call-display.mjs +18 -1
- package/dist/packages/stack/src/plugins/ai-chat/client/context/chat-analytics.cjs +42 -0
- package/dist/packages/stack/src/plugins/ai-chat/client/context/chat-analytics.mjs +39 -0
- package/dist/packages/stack/src/plugins/ai-chat/client/plugin.cjs +2 -1
- package/dist/packages/stack/src/plugins/ai-chat/client/plugin.mjs +2 -1
- package/dist/plugins/ai-chat/api/index.d.cts +2 -2
- package/dist/plugins/ai-chat/api/index.d.mts +2 -2
- package/dist/plugins/ai-chat/api/index.d.ts +2 -2
- package/dist/plugins/ai-chat/client/components/index.d.cts +2 -1
- package/dist/plugins/ai-chat/client/components/index.d.mts +2 -1
- package/dist/plugins/ai-chat/client/components/index.d.ts +2 -1
- package/dist/plugins/ai-chat/client/context/page-ai-context.cjs +4 -0
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.cts +6 -2
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.mts +6 -2
- package/dist/plugins/ai-chat/client/context/page-ai-context.d.ts +6 -2
- package/dist/plugins/ai-chat/client/context/page-ai-context.mjs +4 -1
- package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
- package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
- package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
- package/dist/plugins/ai-chat/client/index.d.cts +7 -3
- package/dist/plugins/ai-chat/client/index.d.mts +7 -3
- package/dist/plugins/ai-chat/client/index.d.ts +7 -3
- package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
- package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
- package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
- package/dist/plugins/blog/api/index.d.cts +2 -2
- package/dist/plugins/blog/api/index.d.mts +2 -2
- package/dist/plugins/blog/api/index.d.ts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +2 -2
- package/dist/plugins/blog/client/index.d.mts +2 -2
- package/dist/plugins/blog/client/index.d.ts +2 -2
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/form-builder/api/index.d.cts +1 -1
- package/dist/plugins/form-builder/api/index.d.mts +1 -1
- package/dist/plugins/form-builder/api/index.d.ts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/form-builder/query-keys.d.cts +1 -1
- package/dist/plugins/form-builder/query-keys.d.mts +1 -1
- package/dist/plugins/form-builder/query-keys.d.ts +1 -1
- package/dist/shared/{stack.B5yPgHsp.d.mts → stack.B7Mj74BQ.d.mts} +18 -2
- package/dist/shared/{stack.rVF0f509.d.ts → stack.CHrtGKLz.d.mts} +66 -4
- package/dist/shared/{stack.BggTHKlo.d.mts → stack.CZ1h_T4k.d.mts} +90 -90
- package/dist/shared/{stack.DfrRYOIl.d.ts → stack.CnDMBj4z.d.ts} +5 -5
- package/dist/shared/{stack.94kN3pEK.d.mts → stack.D2y4gl03.d.mts} +5 -5
- package/dist/shared/{stack.3Sr6qRtT.d.ts → stack.DBH1Bjko.d.ts} +18 -2
- package/dist/shared/{stack.lPKwd_v0.d.ts → stack.DQpyA3Cq.d.ts} +90 -90
- package/dist/shared/{stack.C4oVW2qK.d.cts → stack.Dgt3MCtW.d.cts} +18 -2
- package/dist/shared/{stack.CtrnoQgH.d.cts → stack.DiYg7Jw1.d.cts} +90 -90
- package/dist/shared/{stack.rVF0f509.d.cts → stack.DzJgoNqJ.d.cts} +66 -4
- package/dist/shared/{stack.rVF0f509.d.mts → stack.qMp2VzNw.d.ts} +66 -4
- package/dist/shared/{stack.BTStHVTy.d.cts → stack.rmWRtSjs.d.cts} +5 -5
- package/package.json +2 -2
- package/src/plugins/ai-chat/__tests__/authorization.test.ts +106 -1
- package/src/plugins/ai-chat/__tests__/classnames-overrides.test.tsx +26 -3
- package/src/plugins/ai-chat/__tests__/page-content.test.ts +177 -0
- package/src/plugins/ai-chat/__tests__/page-widget.test.tsx +675 -0
- package/src/plugins/ai-chat/api/operations.ts +29 -13
- package/src/plugins/ai-chat/api/page-tools.ts +56 -2
- package/src/plugins/ai-chat/api/plugin.ts +13 -0
- package/src/plugins/ai-chat/client/analytics.ts +35 -0
- package/src/plugins/ai-chat/client/components/chat-interface.tsx +113 -10
- package/src/plugins/ai-chat/client/components/chat-layout.tsx +304 -68
- package/src/plugins/ai-chat/client/components/tool-call-display.tsx +29 -1
- package/src/plugins/ai-chat/client/context/chat-analytics.tsx +46 -0
- package/src/plugins/ai-chat/client/context/page-ai-context.tsx +9 -1
- package/src/plugins/ai-chat/client/index.ts +2 -0
- package/src/plugins/ai-chat/client/localization/index.ts +12 -0
- package/src/plugins/ai-chat/client/overrides.ts +5 -0
- package/src/plugins/ai-chat/client/plugin.tsx +3 -0
- package/dist/shared/{stack.BuxUQqL8.d.ts → stack.CGmh-Lpe.d.ts} +12 -12
- package/dist/shared/{stack.Egr3RWhA.d.mts → stack.Doy2_wgl.d.mts} +12 -12
- package/dist/shared/{stack.CKc_Bais.d.cts → stack.DtZgEX84.d.cts} +6 -6
- package/dist/shared/{stack.CKc_Bais.d.mts → stack.DtZgEX84.d.mts} +6 -6
- package/dist/shared/{stack.CKc_Bais.d.ts → stack.DtZgEX84.d.ts} +6 -6
- package/dist/shared/{stack.CX83YNuS.d.cts → stack.zch70Loj.d.cts} +12 -12
|
@@ -17,6 +17,7 @@ import { createApiClient } from '@btst/stack/plugins/client';
|
|
|
17
17
|
import { aiChatIdentityKey, createAiChatQueryKeys } from '../../../../../../../plugins/ai-chat/query-keys.mjs';
|
|
18
18
|
import { useAiChatIdentityPartition, useConversation, useConversations } from '../hooks/chat-hooks.mjs';
|
|
19
19
|
import { usePageAIContext } from '../../../../../../../plugins/ai-chat/client/context/page-ai-context.mjs';
|
|
20
|
+
import { useChatAnalytics } from '../context/chat-analytics.mjs';
|
|
20
21
|
import { navigateAiChatCrossOrigin } from '../navigation.mjs';
|
|
21
22
|
|
|
22
23
|
function reconcilePersistedMessageIds(messages, persistedMessages) {
|
|
@@ -129,6 +130,8 @@ function PermissionedChatMessage({
|
|
|
129
130
|
conversationId,
|
|
130
131
|
ownerId,
|
|
131
132
|
messageId,
|
|
133
|
+
toolNames,
|
|
134
|
+
routeName,
|
|
132
135
|
...messageProps
|
|
133
136
|
}) {
|
|
134
137
|
if (!action) return /* @__PURE__ */ jsx(ChatMessage, { ...messageProps });
|
|
@@ -140,6 +143,8 @@ function PermissionedChatMessage({
|
|
|
140
143
|
conversationId,
|
|
141
144
|
ownerId,
|
|
142
145
|
messageId,
|
|
146
|
+
toolNames,
|
|
147
|
+
routeName,
|
|
143
148
|
children: (allowed) => /* @__PURE__ */ jsx(
|
|
144
149
|
ChatMessage,
|
|
145
150
|
{
|
|
@@ -184,6 +189,10 @@ function ChatInterface({
|
|
|
184
189
|
const siteBasePath = plugins?.aiChat?.site.basePath ?? stackBasePath;
|
|
185
190
|
const isPublicMode = mode === "public";
|
|
186
191
|
const pageAIContext = usePageAIContext();
|
|
192
|
+
const effectiveToolNames = [
|
|
193
|
+
...Object.keys(pageAIContext?.clientTools ?? {}),
|
|
194
|
+
...plugins?.aiChat?.config?.pageContent ? ["readPage"] : []
|
|
195
|
+
];
|
|
187
196
|
const tr = useAiChatTranslation(customLocalization);
|
|
188
197
|
const queryClient = useQueryClient(stackQueryClient);
|
|
189
198
|
const identityPartition = useAiChatIdentityPartition();
|
|
@@ -328,6 +337,29 @@ function ChatInterface({
|
|
|
328
337
|
});
|
|
329
338
|
}, [browserHeaders, credentials, isPublicMode, resolvedApiPath]);
|
|
330
339
|
const addToolOutputRef = useRef(null);
|
|
340
|
+
const mounted = useRef(true);
|
|
341
|
+
const track = useChatAnalytics();
|
|
342
|
+
const inputKind = useRef("typed");
|
|
343
|
+
const responseStarted = useRef(null);
|
|
344
|
+
const trackSubmission = useCallback(
|
|
345
|
+
(kind, inputLength, messageCount, attachmentCount = 0) => {
|
|
346
|
+
responseStarted.current = Date.now();
|
|
347
|
+
track({
|
|
348
|
+
type: "message_submitted",
|
|
349
|
+
inputKind: kind,
|
|
350
|
+
inputLength,
|
|
351
|
+
messageCount,
|
|
352
|
+
attachmentCount
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
[track]
|
|
356
|
+
);
|
|
357
|
+
const trackFailure = useCallback(() => {
|
|
358
|
+
if (!mounted.current || responseStarted.current === null) return;
|
|
359
|
+
const durationMs = Math.max(0, Date.now() - responseStarted.current);
|
|
360
|
+
responseStarted.current = null;
|
|
361
|
+
track({ type: "response_failed", durationMs });
|
|
362
|
+
}, [track]);
|
|
331
363
|
const {
|
|
332
364
|
messages,
|
|
333
365
|
sendMessage,
|
|
@@ -341,15 +373,16 @@ function ChatInterface({
|
|
|
341
373
|
id: `${chatInstanceId}:${isPublicMode ? "public" : identitySessionVersion}`,
|
|
342
374
|
transport,
|
|
343
375
|
// Automatically resubmit after all client-side tool results are provided
|
|
344
|
-
sendAutomaticallyWhen: (options) => (isPublicMode || identitySessionVersion === identitySessionGeneration.current) && lastAssistantMessageIsCompleteWithToolCalls(options),
|
|
376
|
+
sendAutomaticallyWhen: (options) => mounted.current && (isPublicMode || identitySessionVersion === identitySessionGeneration.current) && lastAssistantMessageIsCompleteWithToolCalls(options),
|
|
345
377
|
onToolCall: async ({ toolCall }) => {
|
|
378
|
+
if (!mounted.current) return;
|
|
346
379
|
const toolRequest = pendingStreamRequests.current.at(-1);
|
|
347
380
|
if (!isPublicMode && (identitySessionVersion !== identitySessionGeneration.current || !toolRequest || toolRequest.identityPartitionKey !== latestIdentityPartitionKey.current || toolRequest.generation !== latestStreamRequestGeneration.current || activeStreamPartitionKey.current !== toolRequest.identityPartitionKey)) {
|
|
348
381
|
return;
|
|
349
382
|
}
|
|
350
383
|
const toolIdentityPartitionKey = toolRequest?.identityPartitionKey ?? latestIdentityPartitionKey.current;
|
|
351
384
|
const toolStreamRequestGeneration = toolRequest?.generation ?? latestStreamRequestGeneration.current;
|
|
352
|
-
const toolRequestIsCurrent = () => isPublicMode || identitySessionVersion === identitySessionGeneration.current && toolIdentityPartitionKey === latestIdentityPartitionKey.current && toolStreamRequestGeneration === latestStreamRequestGeneration.current;
|
|
385
|
+
const toolRequestIsCurrent = () => mounted.current && (isPublicMode || identitySessionVersion === identitySessionGeneration.current && toolIdentityPartitionKey === latestIdentityPartitionKey.current && toolStreamRequestGeneration === latestStreamRequestGeneration.current);
|
|
353
386
|
const toolName = toolCall.toolName;
|
|
354
387
|
const handler = pageAIContextRef.current?.clientTools?.[toolName];
|
|
355
388
|
if (handler) {
|
|
@@ -389,6 +422,7 @@ function ChatInterface({
|
|
|
389
422
|
}
|
|
390
423
|
},
|
|
391
424
|
onError: (err) => {
|
|
425
|
+
if (!mounted.current) return;
|
|
392
426
|
console.error("useChat onError:", err);
|
|
393
427
|
if (!isPublicMode && identitySessionVersion !== identitySessionGeneration.current) {
|
|
394
428
|
return;
|
|
@@ -398,8 +432,30 @@ function ChatInterface({
|
|
|
398
432
|
if (!isPublicMode && failedPartitionKey !== latestIdentityPartitionKey.current) {
|
|
399
433
|
return;
|
|
400
434
|
}
|
|
435
|
+
trackFailure();
|
|
401
436
|
},
|
|
402
|
-
onFinish: async () => {
|
|
437
|
+
onFinish: async (completion) => {
|
|
438
|
+
if (!mounted.current || !isPublicMode && identitySessionVersion !== identitySessionGeneration.current)
|
|
439
|
+
return;
|
|
440
|
+
if (completion?.isError || completion?.isDisconnect) trackFailure();
|
|
441
|
+
else if (completion?.isAbort) responseStarted.current = null;
|
|
442
|
+
else if (completion && responseStarted.current !== null && !lastAssistantMessageIsCompleteWithToolCalls({
|
|
443
|
+
messages: completion.messages
|
|
444
|
+
})) {
|
|
445
|
+
const durationMs = Math.max(0, Date.now() - responseStarted.current);
|
|
446
|
+
responseStarted.current = null;
|
|
447
|
+
track({
|
|
448
|
+
type: "response_completed",
|
|
449
|
+
durationMs,
|
|
450
|
+
outputLength: completion.message.parts.reduce(
|
|
451
|
+
(count, part) => count + (part.type === "text" ? part.text.length : 0),
|
|
452
|
+
0
|
|
453
|
+
),
|
|
454
|
+
toolCallCount: completion.message.parts.filter(
|
|
455
|
+
(part) => part.type === "dynamic-tool" || part.type.startsWith("tool-")
|
|
456
|
+
).length
|
|
457
|
+
});
|
|
458
|
+
}
|
|
403
459
|
if (isPublicMode) {
|
|
404
460
|
activeStreamPartitionKey.current = void 0;
|
|
405
461
|
return;
|
|
@@ -415,7 +471,7 @@ function ChatInterface({
|
|
|
415
471
|
if (!finishedRequest || finishedRequest.generation === latestStreamRequestGeneration.current) {
|
|
416
472
|
activeStreamPartitionKey.current = void 0;
|
|
417
473
|
}
|
|
418
|
-
const identityIsCurrent = () => finishingPartitionKey === latestIdentityPartitionKey.current && (!finishedRequest || finishedRequest.generation === latestStreamRequestGeneration.current);
|
|
474
|
+
const identityIsCurrent = () => mounted.current && finishingPartitionKey === latestIdentityPartitionKey.current && (!finishedRequest || finishedRequest.generation === latestStreamRequestGeneration.current);
|
|
419
475
|
const finishingIdentityPartition = latestIdentityPartition.current;
|
|
420
476
|
const finishingConversationsListQueryKey = latestConversationsListQueryKey.current;
|
|
421
477
|
const finishingHeaders = latestHeaders.current;
|
|
@@ -596,6 +652,15 @@ function ChatInterface({
|
|
|
596
652
|
useLayoutEffect(() => {
|
|
597
653
|
latestMessages.current = messages;
|
|
598
654
|
}, [messages]);
|
|
655
|
+
const stopRef = useRef(stop);
|
|
656
|
+
stopRef.current = stop;
|
|
657
|
+
useEffect(() => {
|
|
658
|
+
mounted.current = true;
|
|
659
|
+
return () => {
|
|
660
|
+
mounted.current = false;
|
|
661
|
+
void stopRef.current();
|
|
662
|
+
};
|
|
663
|
+
}, []);
|
|
599
664
|
const previousIdentityPartition = useRef(identityPartitionKey);
|
|
600
665
|
useLayoutEffect(() => {
|
|
601
666
|
const nextPartition = identityPartitionKey;
|
|
@@ -609,6 +674,7 @@ function ChatInterface({
|
|
|
609
674
|
activeStreamPartitionKey.current = void 0;
|
|
610
675
|
pendingStreamRequests.current = [];
|
|
611
676
|
identitySessionGeneration.current += 1;
|
|
677
|
+
responseStarted.current = null;
|
|
612
678
|
setIdentitySessionVersion(identitySessionGeneration.current);
|
|
613
679
|
latestStreamRequestGeneration.current = ++nextStreamRequestGeneration.current;
|
|
614
680
|
void stop();
|
|
@@ -691,12 +757,20 @@ function ChatInterface({
|
|
|
691
757
|
}
|
|
692
758
|
}, [messages, isPublicMode, onMessagesChange, isMessagesInitialized]);
|
|
693
759
|
const handleInputChange = (e) => {
|
|
760
|
+
inputKind.current = "typed";
|
|
694
761
|
setInput(e.target.value);
|
|
695
762
|
};
|
|
696
763
|
const handleSubmit = async (e, files) => {
|
|
697
764
|
e.preventDefault();
|
|
698
765
|
const text = input.trim();
|
|
699
766
|
if (!text && (!files || files.length === 0)) return;
|
|
767
|
+
trackSubmission(
|
|
768
|
+
inputKind.current,
|
|
769
|
+
text.length,
|
|
770
|
+
messages.length + 1,
|
|
771
|
+
files?.length ?? 0
|
|
772
|
+
);
|
|
773
|
+
inputKind.current = "typed";
|
|
700
774
|
if (!isPublicMode && !id && messages.length === 0) {
|
|
701
775
|
isFirstMessageSentRef.current = true;
|
|
702
776
|
}
|
|
@@ -735,6 +809,7 @@ function ChatInterface({
|
|
|
735
809
|
if (!isPublicMode && (sendIdentityPartitionKey !== latestIdentityPartitionKey.current || sendIdentitySessionGeneration !== identitySessionGeneration.current)) {
|
|
736
810
|
return;
|
|
737
811
|
}
|
|
812
|
+
trackFailure();
|
|
738
813
|
setInput(savedInput);
|
|
739
814
|
setAttachedFiles(savedFiles);
|
|
740
815
|
if (isFirstMessageSentRef.current && !hasNavigatedRef.current) {
|
|
@@ -748,17 +823,19 @@ function ChatInterface({
|
|
|
748
823
|
const handleRetry = useCallback(() => {
|
|
749
824
|
setHistorySyncError(null);
|
|
750
825
|
activeStreamPartitionKey.current = latestIdentityPartitionKey.current;
|
|
826
|
+
trackSubmission("retry", 0, messages.length);
|
|
751
827
|
regenerate();
|
|
752
|
-
}, [regenerate]);
|
|
828
|
+
}, [regenerate, trackSubmission, messages.length]);
|
|
753
829
|
useEffect(() => {
|
|
754
830
|
if (pendingEdit && pendingEdit.identityPartitionKey === latestIdentityPartitionKey.current && pendingEdit.streamRequestGeneration === latestStreamRequestGeneration.current && messages.length === pendingEdit.expectedLength) {
|
|
755
831
|
const textToSend = pendingEdit.text;
|
|
756
832
|
setPendingEdit(null);
|
|
757
833
|
isEditInProgressRef.current = false;
|
|
758
834
|
activeStreamPartitionKey.current = latestIdentityPartitionKey.current;
|
|
835
|
+
trackSubmission("edit", textToSend.length, messages.length + 1);
|
|
759
836
|
sendMessage({ text: textToSend });
|
|
760
837
|
}
|
|
761
|
-
}, [messages.length, pendingEdit, sendMessage]);
|
|
838
|
+
}, [messages.length, pendingEdit, sendMessage, trackSubmission]);
|
|
762
839
|
const handleEditMessage = useCallback(
|
|
763
840
|
(messageId, newText) => {
|
|
764
841
|
setHistorySyncError(null);
|
|
@@ -817,7 +894,11 @@ function ChatInterface({
|
|
|
817
894
|
"button",
|
|
818
895
|
{
|
|
819
896
|
type: "button",
|
|
820
|
-
onClick: () =>
|
|
897
|
+
onClick: () => {
|
|
898
|
+
inputKind.current = "suggestion";
|
|
899
|
+
track({ type: "suggestion_selected", index });
|
|
900
|
+
setInput(suggestion);
|
|
901
|
+
},
|
|
821
902
|
className: "px-3 py-2 text-sm rounded-lg border border-border bg-background hover:bg-accent hover:text-accent-foreground transition-colors text-foreground",
|
|
822
903
|
children: suggestion
|
|
823
904
|
},
|
|
@@ -828,6 +909,8 @@ function ChatInterface({
|
|
|
828
909
|
PermissionedChatMessage,
|
|
829
910
|
{
|
|
830
911
|
publicMode: isPublicMode,
|
|
912
|
+
toolNames: effectiveToolNames,
|
|
913
|
+
routeName: pageAIContext?.routeName,
|
|
831
914
|
action: m.role === "user" ? "edit" : m.role === "assistant" && index === messages.length - 1 ? "retry" : void 0,
|
|
832
915
|
conversationId: currentConversationId,
|
|
833
916
|
ownerId: currentConversationOwnerId,
|
|
@@ -878,7 +961,7 @@ function ChatInterface({
|
|
|
878
961
|
action: "send",
|
|
879
962
|
conversationId: currentConversationId,
|
|
880
963
|
ownerId: currentConversationOwnerId,
|
|
881
|
-
toolNames:
|
|
964
|
+
toolNames: effectiveToolNames,
|
|
882
965
|
routeName: pageAIContext?.routeName,
|
|
883
966
|
children: (allowed) => allowed && !historySyncError ? /* @__PURE__ */ jsx(
|
|
884
967
|
"div",
|
|
@@ -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');
|
|
@@ -13,13 +15,19 @@ const chatInterface = require('./chat-interface.cjs');
|
|
|
13
15
|
const plugins_aiChat_client_context_pageAiContext = require('../../../../../../../plugins/ai-chat/client/context/page-ai-context.cjs');
|
|
14
16
|
const context = require('@btst/stack/context');
|
|
15
17
|
const overrides = require('../overrides.cjs');
|
|
18
|
+
const chatAnalytics = require('../context/chat-analytics.cjs');
|
|
16
19
|
const index = require('../localization/index.cjs');
|
|
17
20
|
|
|
18
21
|
function ChatLayout(props) {
|
|
22
|
+
return /* @__PURE__ */ jsxRuntime.jsx(plugins_aiChat_client_context_pageAiContext.PageAIContextScope.Provider, { value: props.pageContext, children: /* @__PURE__ */ jsxRuntime.jsx(chatAnalytics.ChatAnalyticsPageKey.Provider, { value: props.pageKey, children: /* @__PURE__ */ jsxRuntime.jsx(ChatLayoutContent, { ...props }, props.pageKey) }) });
|
|
23
|
+
}
|
|
24
|
+
function ChatLayoutContent(props) {
|
|
25
|
+
const track = chatAnalytics.useChatAnalytics();
|
|
19
26
|
const {
|
|
20
27
|
conversationId,
|
|
21
28
|
layout = "full",
|
|
22
29
|
className,
|
|
30
|
+
style,
|
|
23
31
|
showSidebar: requestedShowSidebar = true,
|
|
24
32
|
initialMessages,
|
|
25
33
|
onMessagesChange,
|
|
@@ -30,16 +38,77 @@ function ChatLayout(props) {
|
|
|
30
38
|
const resolvedMode = overrides.resolveAiChatMode(plugins?.aiChat?.config);
|
|
31
39
|
const showSidebar = requestedShowSidebar && resolvedMode !== "public";
|
|
32
40
|
const tr = index.useAiChatTranslation(localization);
|
|
33
|
-
const widgetHeight = props.layout === "widget" ? props.widgetHeight ?? "
|
|
34
|
-
const widgetWidth = props.layout === "widget" ? props.widgetWidth ?? "
|
|
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;
|
|
35
43
|
const defaultOpen = props.layout === "widget" ? props.defaultOpen ?? false : false;
|
|
36
44
|
const showTrigger = props.layout === "widget" ? props.showTrigger ?? true : true;
|
|
45
|
+
const triggerIcon = props.layout === "widget" ? props.triggerIcon : void 0;
|
|
37
46
|
const [sidebarOpen, setSidebarOpen] = React.useState(true);
|
|
38
47
|
const [mobileSidebarOpen, setMobileSidebarOpen] = React.useState(false);
|
|
39
48
|
const [chatResetKey, setChatResetKey] = React.useState(0);
|
|
40
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
|
+
);
|
|
41
79
|
const [widgetResetKey, setWidgetResetKey] = React.useState(0);
|
|
42
80
|
const [widgetEverOpened, setWidgetEverOpened] = React.useState(defaultOpen);
|
|
81
|
+
const [tipDismissed, setTipDismissed] = React.useState(defaultOpen);
|
|
82
|
+
const introTip = props.layout === "widget" ? props.introTip : void 0;
|
|
83
|
+
const introTipDuration = props.layout === "widget" ? props.introTipDuration ?? 6e3 : 6e3;
|
|
84
|
+
const tipShown = React.useRef(false);
|
|
85
|
+
const tipDismissedRef = React.useRef(defaultOpen);
|
|
86
|
+
const dismissTip = React.useCallback(
|
|
87
|
+
(reason) => {
|
|
88
|
+
if (tipShown.current && !tipDismissedRef.current) {
|
|
89
|
+
tipDismissedRef.current = true;
|
|
90
|
+
track({ type: "intro_tip_dismissed", reason });
|
|
91
|
+
}
|
|
92
|
+
setTipDismissed(true);
|
|
93
|
+
},
|
|
94
|
+
[track]
|
|
95
|
+
);
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
if (!introTip || !showTrigger || tipDismissed) return;
|
|
98
|
+
if (!tipShown.current) {
|
|
99
|
+
tipShown.current = true;
|
|
100
|
+
track({ type: "intro_tip_shown" });
|
|
101
|
+
}
|
|
102
|
+
const timeout = setTimeout(() => dismissTip("timeout"), introTipDuration);
|
|
103
|
+
return () => clearTimeout(timeout);
|
|
104
|
+
}, [
|
|
105
|
+
introTip,
|
|
106
|
+
introTipDuration,
|
|
107
|
+
showTrigger,
|
|
108
|
+
tipDismissed,
|
|
109
|
+
dismissTip,
|
|
110
|
+
track
|
|
111
|
+
]);
|
|
43
112
|
const pageAIContext = plugins_aiChat_client_context_pageAiContext.usePageAIContext();
|
|
44
113
|
const handleNewChat = React.useCallback(() => {
|
|
45
114
|
if (!conversationId) {
|
|
@@ -52,95 +121,198 @@ function ChatLayout(props) {
|
|
|
52
121
|
{
|
|
53
122
|
className: utils.cn(
|
|
54
123
|
"flex flex-col items-end gap-3",
|
|
124
|
+
props.layout === "widget" && props.floating && "fixed right-4 bottom-[max(1rem,env(safe-area-inset-bottom))] z-50",
|
|
55
125
|
className,
|
|
56
126
|
classNames?.container
|
|
57
127
|
),
|
|
128
|
+
style,
|
|
58
129
|
children: [
|
|
59
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
130
|
+
introTip && showTrigger && !tipDismissed && !widgetOpen && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
60
131
|
"div",
|
|
61
132
|
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
widgetOpen ? "flex" : "hidden"
|
|
65
|
-
),
|
|
66
|
-
style: { height: widgetHeight, width: widgetWidth },
|
|
133
|
+
role: "status",
|
|
134
|
+
className: "flex max-w-[min(280px,calc(100vw-32px))] items-center gap-2 rounded-lg border bg-background p-3 text-sm text-foreground shadow-lg",
|
|
67
135
|
children: [
|
|
68
|
-
/* @__PURE__ */ jsxRuntime.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
badge.Badge,
|
|
72
|
-
{
|
|
73
|
-
variant: "secondary",
|
|
74
|
-
className: "text-xs",
|
|
75
|
-
"data-testid": "page-context-badge",
|
|
76
|
-
children: pageAIContext.routeName
|
|
77
|
-
}
|
|
78
|
-
) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground font-medium", children: tr("A11Y_CHAT_TITLE", "aiChat.a11y.title", "AI Chat") }),
|
|
79
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1" }),
|
|
80
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
81
|
-
button.Button,
|
|
82
|
-
{
|
|
83
|
-
variant: "ghost",
|
|
84
|
-
size: "icon",
|
|
85
|
-
className: "h-5 w-5",
|
|
86
|
-
onClick: () => {
|
|
87
|
-
onClear?.();
|
|
88
|
-
setWidgetResetKey((prev) => prev + 1);
|
|
89
|
-
},
|
|
90
|
-
"aria-label": tr(
|
|
91
|
-
"A11Y_CLEAR_CHAT",
|
|
92
|
-
"aiChat.a11y.clearChat",
|
|
93
|
-
"Clear chat"
|
|
94
|
-
),
|
|
95
|
-
title: tr(
|
|
96
|
-
"A11Y_CLEAR_CHAT",
|
|
97
|
-
"aiChat.a11y.clearChat",
|
|
98
|
-
"Clear chat"
|
|
99
|
-
),
|
|
100
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Trash2, { className: "h-3.5 w-3.5" })
|
|
101
|
-
}
|
|
102
|
-
),
|
|
103
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
104
|
-
button.Button,
|
|
105
|
-
{
|
|
106
|
-
variant: "ghost",
|
|
107
|
-
size: "icon",
|
|
108
|
-
className: "h-5 w-5",
|
|
109
|
-
onClick: () => setWidgetOpen(false),
|
|
110
|
-
"aria-label": tr(
|
|
111
|
-
"A11Y_CLOSE_CHAT",
|
|
112
|
-
"aiChat.a11y.closeChat",
|
|
113
|
-
"Close chat"
|
|
114
|
-
),
|
|
115
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.X, { className: "h-3.5 w-3.5" })
|
|
116
|
-
}
|
|
117
|
-
)
|
|
118
|
-
] }),
|
|
119
|
-
widgetEverOpened && /* @__PURE__ */ jsxRuntime.jsx(
|
|
120
|
-
chatInterface.ChatInterface,
|
|
136
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: introTip }),
|
|
137
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
138
|
+
button.Button,
|
|
121
139
|
{
|
|
122
|
-
|
|
123
|
-
variant: "
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
type: "button",
|
|
141
|
+
variant: "ghost",
|
|
142
|
+
size: "icon",
|
|
143
|
+
className: "h-7 w-7 shrink-0",
|
|
144
|
+
onClick: () => dismissTip("manual"),
|
|
145
|
+
"aria-label": tr(
|
|
146
|
+
"A11Y_DISMISS_CHAT_TIP",
|
|
147
|
+
"aiChat.a11y.dismissChatTip",
|
|
148
|
+
"Dismiss chat tip"
|
|
149
|
+
),
|
|
150
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.X, { className: "h-4 w-4", "aria-hidden": "true" })
|
|
151
|
+
}
|
|
128
152
|
)
|
|
129
153
|
]
|
|
130
154
|
}
|
|
131
155
|
),
|
|
156
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
157
|
+
"div",
|
|
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,
|
|
195
|
+
className: utils.cn(
|
|
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)]"
|
|
199
|
+
),
|
|
200
|
+
onCloseAutoFocus: (event) => {
|
|
201
|
+
const target = widgetOpen ? expandRef.current ?? compactPanelRef.current : triggerRef.current;
|
|
202
|
+
if (target) {
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
target.focus();
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
children: [
|
|
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" })
|
|
210
|
+
]
|
|
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
|
|
299
|
+
),
|
|
132
300
|
showTrigger && /* @__PURE__ */ jsxRuntime.jsx(
|
|
133
301
|
button.Button,
|
|
134
302
|
{
|
|
303
|
+
ref: triggerRef,
|
|
135
304
|
size: "icon",
|
|
136
305
|
className: "h-12 w-12 rounded-full shadow-lg",
|
|
137
306
|
onClick: () => {
|
|
307
|
+
dismissTip("open");
|
|
308
|
+
track({ type: widgetOpen ? "widget_closed" : "widget_opened" });
|
|
138
309
|
setWidgetOpen((prev) => !prev);
|
|
310
|
+
setExpanded(false);
|
|
139
311
|
setWidgetEverOpened(true);
|
|
140
312
|
},
|
|
141
313
|
"aria-label": widgetOpen ? tr("A11Y_CLOSE_CHAT", "aiChat.a11y.closeChat", "Close chat") : tr("A11Y_OPEN_CHAT", "aiChat.a11y.openChat", "Open chat"),
|
|
142
314
|
"data-testid": "widget-trigger",
|
|
143
|
-
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" })
|
|
144
316
|
}
|
|
145
317
|
)
|
|
146
318
|
]
|
|
@@ -156,6 +328,7 @@ function ChatLayout(props) {
|
|
|
156
328
|
classNames?.container
|
|
157
329
|
),
|
|
158
330
|
"data-testid": "chat-layout",
|
|
331
|
+
style,
|
|
159
332
|
children: [
|
|
160
333
|
showSidebar && /* @__PURE__ */ jsxRuntime.jsx(
|
|
161
334
|
"div",
|
|
@@ -240,6 +413,7 @@ function ChatLayout(props) {
|
|
|
240
413
|
variant: "ghost",
|
|
241
414
|
size: "icon",
|
|
242
415
|
onClick: () => {
|
|
416
|
+
track({ type: "conversation_cleared" });
|
|
243
417
|
onClear();
|
|
244
418
|
setChatResetKey((prev) => prev + 1);
|
|
245
419
|
},
|