@btst/stack 4.3.0 → 4.5.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/compaction.cjs +289 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/compaction.mjs +281 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/context-checkpoint.cjs +54 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/context-checkpoint.mjs +51 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/getters.cjs +2 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/getters.mjs +2 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/operations.cjs +285 -89
- package/dist/packages/stack/src/plugins/ai-chat/api/operations.mjs +285 -89
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +6 -3
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +6 -3
- package/dist/packages/stack/src/plugins/ai-chat/api/serializers.cjs +1 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/serializers.mjs +1 -0
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +62 -17
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +62 -17
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-message.cjs +22 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-message.mjs +22 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/context-checkpoint.cjs +30 -0
- package/dist/packages/stack/src/plugins/ai-chat/client/context-checkpoint.mjs +27 -0
- package/dist/packages/stack/src/plugins/ai-chat/db.cjs +8 -0
- package/dist/packages/stack/src/plugins/ai-chat/db.mjs +8 -0
- package/dist/packages/stack/src/plugins/ai-chat/history.cjs +47 -0
- package/dist/packages/stack/src/plugins/ai-chat/history.mjs +45 -0
- package/dist/packages/stack/src/plugins/ai-chat/schemas.cjs +2 -0
- package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +2 -0
- package/dist/packages/ui/src/components/markdown-content.cjs +9 -9
- package/dist/packages/ui/src/components/markdown-content.mjs +9 -9
- 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 +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
- package/dist/plugins/ai-chat/client/components/index.d.ts +1 -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 +3 -3
- package/dist/plugins/ai-chat/client/index.d.mts +3 -3
- package/dist/plugins/ai-chat/client/index.d.ts +3 -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/cms/api/index.d.cts +1 -1
- package/dist/plugins/cms/api/index.d.mts +1 -1
- package/dist/plugins/cms/api/index.d.ts +1 -1
- package/dist/plugins/cms/query-keys.d.cts +1 -1
- package/dist/plugins/cms/query-keys.d.mts +1 -1
- package/dist/plugins/cms/query-keys.d.ts +1 -1
- 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/plugins/ui-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/ui-builder/client/index.d.cts +1 -1
- package/dist/plugins/ui-builder/client/index.d.mts +1 -1
- package/dist/plugins/ui-builder/client/index.d.ts +1 -1
- package/dist/plugins/ui-builder/query-keys.d.cts +1 -1
- package/dist/plugins/ui-builder/query-keys.d.mts +1 -1
- package/dist/plugins/ui-builder/query-keys.d.ts +1 -1
- package/dist/shared/{stack.D2y4gl03.d.mts → stack.94kN3pEK.d.mts} +5 -5
- package/dist/shared/{stack.DBH1Bjko.d.ts → stack.B2EYZyh_.d.ts} +16 -2
- package/dist/shared/{stack.rmWRtSjs.d.cts → stack.BTStHVTy.d.cts} +5 -5
- package/dist/shared/{stack.CZ1h_T4k.d.mts → stack.BggTHKlo.d.mts} +90 -90
- package/dist/shared/{stack.DzJgoNqJ.d.cts → stack.Cei2T2gc.d.cts} +2 -0
- package/dist/shared/{stack.DiYg7Jw1.d.cts → stack.CtrnoQgH.d.cts} +90 -90
- package/dist/shared/{stack.CHrtGKLz.d.mts → stack.DZRZAzcx.d.mts} +2 -0
- package/dist/shared/{stack.CnDMBj4z.d.ts → stack.DfrRYOIl.d.ts} +5 -5
- package/dist/shared/{stack.B7Mj74BQ.d.mts → stack.DivuAnNI.d.mts} +16 -2
- package/dist/shared/{stack.qMp2VzNw.d.ts → stack.byjorQi5.d.ts} +2 -0
- package/dist/shared/{stack.Dgt3MCtW.d.cts → stack.fF273jws.d.cts} +16 -2
- package/dist/shared/{stack.DQpyA3Cq.d.ts → stack.lPKwd_v0.d.ts} +90 -90
- package/package.json +1 -1
- package/src/__tests__/markdown-link-navigation.test.tsx +29 -0
- package/src/plugins/ai-chat/__tests__/authorization.test.ts +56 -30
- package/src/plugins/ai-chat/__tests__/classnames-overrides.test.tsx +25 -0
- package/src/plugins/ai-chat/__tests__/client-sweep.test.tsx +101 -0
- package/src/plugins/ai-chat/__tests__/compaction.test.ts +272 -0
- package/src/plugins/ai-chat/__tests__/persisted-compaction.test.ts +819 -0
- package/src/plugins/ai-chat/api/compaction.ts +377 -0
- package/src/plugins/ai-chat/api/context-checkpoint.ts +66 -0
- package/src/plugins/ai-chat/api/getters.ts +2 -0
- package/src/plugins/ai-chat/api/operations.ts +380 -93
- package/src/plugins/ai-chat/api/plugin.ts +15 -4
- package/src/plugins/ai-chat/api/serializers.ts +1 -0
- package/src/plugins/ai-chat/client/components/chat-interface.tsx +97 -21
- package/src/plugins/ai-chat/client/components/chat-message.tsx +32 -2
- package/src/plugins/ai-chat/client/context-checkpoint.ts +47 -0
- package/src/plugins/ai-chat/client/localization/index.ts +2 -0
- package/src/plugins/ai-chat/db.ts +8 -0
- package/src/plugins/ai-chat/history.ts +68 -0
- package/src/plugins/ai-chat/schemas.ts +2 -0
- package/src/plugins/ai-chat/types.ts +2 -0
- package/dist/shared/{stack.lsbdKRj7.d.mts → stack.B8PG21zX.d.mts} +16 -16
- package/dist/shared/{stack.CGmh-Lpe.d.ts → stack.BuxUQqL8.d.ts} +12 -12
- package/dist/shared/{stack.DtZgEX84.d.cts → stack.CKc_Bais.d.cts} +6 -6
- package/dist/shared/{stack.DtZgEX84.d.mts → stack.CKc_Bais.d.mts} +6 -6
- package/dist/shared/{stack.DtZgEX84.d.ts → stack.CKc_Bais.d.ts} +6 -6
- package/dist/shared/{stack.zch70Loj.d.cts → stack.CX83YNuS.d.cts} +12 -12
- package/dist/shared/{stack.BMlTZnZp.d.ts → stack.CyalJ0j6.d.ts} +16 -16
- package/dist/shared/{stack.BV0uNV4f.d.cts → stack.D6Pg-ufX.d.cts} +16 -16
- package/dist/shared/{stack.Doy2_wgl.d.mts → stack.Egr3RWhA.d.mts} +12 -12
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
const history = require('../../history.cjs');
|
|
5
6
|
const react = require('@ai-sdk/react');
|
|
6
7
|
const React = require('react');
|
|
7
8
|
const reactQuery = require('@tanstack/react-query');
|
|
@@ -21,6 +22,7 @@ const chatHooks = require('../hooks/chat-hooks.cjs');
|
|
|
21
22
|
const plugins_aiChat_client_context_pageAiContext = require('../../../../../../../plugins/ai-chat/client/context/page-ai-context.cjs');
|
|
22
23
|
const chatAnalytics = require('../context/chat-analytics.cjs');
|
|
23
24
|
const navigation = require('../navigation.cjs');
|
|
25
|
+
const contextCheckpoint = require('../context-checkpoint.cjs');
|
|
24
26
|
|
|
25
27
|
function reconcilePersistedMessageIds(messages, persistedMessages) {
|
|
26
28
|
const userMessages = messages.filter((message) => message.role === "user");
|
|
@@ -42,23 +44,18 @@ function reconcilePersistedMessageIds(messages, persistedMessages) {
|
|
|
42
44
|
}
|
|
43
45
|
return messages.map((message) => ({
|
|
44
46
|
...message,
|
|
45
|
-
id: persistedIds.get(message) ?? message.id
|
|
47
|
+
id: persistedIds.get(message) ?? message.id,
|
|
48
|
+
...persistedMessages.some(
|
|
49
|
+
(saved) => saved.id === message.id && saved.interrupted
|
|
50
|
+
) ? { metadata: { ...message.metadata, interrupted: true } } : {}
|
|
46
51
|
}));
|
|
47
52
|
}
|
|
48
53
|
function persistedMessagesToUiMessages(messages) {
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
} catch {
|
|
55
|
-
parts = [{ type: "text", text: message.content }];
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
id: message.id,
|
|
59
|
-
role: message.role,
|
|
60
|
-
parts
|
|
61
|
-
};
|
|
54
|
+
return history.persistedHistory(messages).map((message) => {
|
|
55
|
+
const abandoned = message.parts.some(
|
|
56
|
+
(part) => "toolCallId" in part && (part.state === "input-streaming" || part.state === "input-available")
|
|
57
|
+
);
|
|
58
|
+
return abandoned ? { ...message, metadata: { interrupted: true } } : message;
|
|
62
59
|
});
|
|
63
60
|
}
|
|
64
61
|
function ChatActionCheck({
|
|
@@ -208,6 +205,9 @@ function ChatInterface({
|
|
|
208
205
|
const latestStreamRequestGeneration = React.useRef(0);
|
|
209
206
|
const pendingStreamRequests = React.useRef([]);
|
|
210
207
|
const latestMessages = React.useRef([]);
|
|
208
|
+
const contextCheckpoint$1 = React.useRef(void 0);
|
|
209
|
+
const submittedMessages = React.useRef([]);
|
|
210
|
+
const [isCompacting, setIsCompacting] = React.useState(false);
|
|
211
211
|
const conversationsListQueryKey = React.useMemo(() => {
|
|
212
212
|
if (isPublicMode) return ["ai-chat", "disabled"];
|
|
213
213
|
const client$1 = client.createApiClient({
|
|
@@ -307,6 +307,7 @@ function ChatInterface({
|
|
|
307
307
|
pendingStreamRequests.current.push(request);
|
|
308
308
|
activeStreamPartitionKey.current = request.identityPartitionKey;
|
|
309
309
|
}
|
|
310
|
+
setIsCompacting(false);
|
|
310
311
|
const currentPageContext = pageAIContextRef.current;
|
|
311
312
|
const pageContextBody = currentPageContext?.pageDescription ? {
|
|
312
313
|
pageContext: currentPageContext.pageDescription,
|
|
@@ -319,18 +320,20 @@ function ChatInterface({
|
|
|
319
320
|
if (newUserMessage) {
|
|
320
321
|
messagesToSend.push(newUserMessage);
|
|
321
322
|
}
|
|
323
|
+
submittedMessages.current = messagesToSend;
|
|
322
324
|
editMessagesRef.current = null;
|
|
323
325
|
return {
|
|
324
326
|
body: {
|
|
325
|
-
messages: messagesToSend,
|
|
327
|
+
...isPublicMode ? contextCheckpoint.contextRequest(messagesToSend, contextCheckpoint$1.current) : { messages: messagesToSend },
|
|
326
328
|
conversationId: conversationIdRef.current,
|
|
327
329
|
...pageContextBody
|
|
328
330
|
}
|
|
329
331
|
};
|
|
330
332
|
}
|
|
333
|
+
submittedMessages.current = hookMessages;
|
|
331
334
|
return {
|
|
332
335
|
body: {
|
|
333
|
-
messages: hookMessages,
|
|
336
|
+
...isPublicMode ? contextCheckpoint.contextRequest(hookMessages, contextCheckpoint$1.current) : { messages: hookMessages },
|
|
334
337
|
conversationId: conversationIdRef.current,
|
|
335
338
|
...pageContextBody
|
|
336
339
|
}
|
|
@@ -374,6 +377,21 @@ function ChatInterface({
|
|
|
374
377
|
} = react.useChat({
|
|
375
378
|
id: `${chatInstanceId}:${isPublicMode ? "public" : identitySessionVersion}`,
|
|
376
379
|
transport,
|
|
380
|
+
onData: ({ type, data }) => {
|
|
381
|
+
if (!mounted.current || !data || typeof data !== "object") return;
|
|
382
|
+
if (!isPublicMode && (identitySessionVersion !== identitySessionGeneration.current || activeStreamPartitionKey.current !== latestIdentityPartitionKey.current))
|
|
383
|
+
return;
|
|
384
|
+
if (type === "data-context-status" && "compacting" in data) {
|
|
385
|
+
setIsCompacting(data.compacting === true);
|
|
386
|
+
}
|
|
387
|
+
if (isPublicMode && type === "data-context-checkpoint" && "summary" in data && "throughMessageId" in data && typeof data.summary === "string" && typeof data.throughMessageId === "string") {
|
|
388
|
+
contextCheckpoint$1.current = contextCheckpoint.captureContextCheckpoint(
|
|
389
|
+
submittedMessages.current,
|
|
390
|
+
data.summary,
|
|
391
|
+
data.throughMessageId
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
},
|
|
377
395
|
// Automatically resubmit after all client-side tool results are provided
|
|
378
396
|
sendAutomaticallyWhen: (options) => mounted.current && (isPublicMode || identitySessionVersion === identitySessionGeneration.current) && ai.lastAssistantMessageIsCompleteWithToolCalls(options),
|
|
379
397
|
onToolCall: async ({ toolCall }) => {
|
|
@@ -424,6 +442,7 @@ function ChatInterface({
|
|
|
424
442
|
}
|
|
425
443
|
},
|
|
426
444
|
onError: (err) => {
|
|
445
|
+
setIsCompacting(false);
|
|
427
446
|
if (!mounted.current) return;
|
|
428
447
|
console.error("useChat onError:", err);
|
|
429
448
|
if (!isPublicMode && identitySessionVersion !== identitySessionGeneration.current) {
|
|
@@ -437,6 +456,7 @@ function ChatInterface({
|
|
|
437
456
|
trackFailure();
|
|
438
457
|
},
|
|
439
458
|
onFinish: async (completion) => {
|
|
459
|
+
setIsCompacting(false);
|
|
440
460
|
if (!mounted.current || !isPublicMode && identitySessionVersion !== identitySessionGeneration.current)
|
|
441
461
|
return;
|
|
442
462
|
if (completion?.isError || completion?.isDisconnect) trackFailure();
|
|
@@ -458,6 +478,19 @@ function ChatInterface({
|
|
|
458
478
|
).length
|
|
459
479
|
});
|
|
460
480
|
}
|
|
481
|
+
if (completion?.isAbort || completion?.isError || completion?.isDisconnect) {
|
|
482
|
+
setMessages(
|
|
483
|
+
(current) => current.map(
|
|
484
|
+
(message) => message.id === completion.message.id ? {
|
|
485
|
+
...message,
|
|
486
|
+
metadata: {
|
|
487
|
+
...message.metadata,
|
|
488
|
+
interrupted: true
|
|
489
|
+
}
|
|
490
|
+
} : message
|
|
491
|
+
)
|
|
492
|
+
);
|
|
493
|
+
}
|
|
461
494
|
if (isPublicMode) {
|
|
462
495
|
activeStreamPartitionKey.current = void 0;
|
|
463
496
|
return;
|
|
@@ -932,7 +965,19 @@ function ChatInterface({
|
|
|
932
965
|
},
|
|
933
966
|
m.id || `msg-${index}`
|
|
934
967
|
)),
|
|
935
|
-
isLoading &&
|
|
968
|
+
isLoading && isCompacting && /* @__PURE__ */ jsxRuntime.jsx(
|
|
969
|
+
"div",
|
|
970
|
+
{
|
|
971
|
+
role: "status",
|
|
972
|
+
className: "text-muted-foreground text-sm py-4",
|
|
973
|
+
children: tr(
|
|
974
|
+
"CHAT_COMPACTING",
|
|
975
|
+
"aiChat.chat.compacting",
|
|
976
|
+
"Summarizing earlier conversation\u2026"
|
|
977
|
+
)
|
|
978
|
+
}
|
|
979
|
+
),
|
|
980
|
+
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
981
|
(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
982
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: tr(
|
|
938
983
|
"CHAT_ERROR",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { persistedHistory } from '../../history.mjs';
|
|
3
4
|
import { useChat } from '@ai-sdk/react';
|
|
4
5
|
import { useId, useRef, useState, useMemo, useEffect, useCallback, useLayoutEffect } from 'react';
|
|
5
6
|
import { useQueryClient, hashKey } from '@tanstack/react-query';
|
|
@@ -19,6 +20,7 @@ import { useAiChatIdentityPartition, useConversation, useConversations } from '.
|
|
|
19
20
|
import { usePageAIContext } from '../../../../../../../plugins/ai-chat/client/context/page-ai-context.mjs';
|
|
20
21
|
import { useChatAnalytics } from '../context/chat-analytics.mjs';
|
|
21
22
|
import { navigateAiChatCrossOrigin } from '../navigation.mjs';
|
|
23
|
+
import { contextRequest, captureContextCheckpoint } from '../context-checkpoint.mjs';
|
|
22
24
|
|
|
23
25
|
function reconcilePersistedMessageIds(messages, persistedMessages) {
|
|
24
26
|
const userMessages = messages.filter((message) => message.role === "user");
|
|
@@ -40,23 +42,18 @@ function reconcilePersistedMessageIds(messages, persistedMessages) {
|
|
|
40
42
|
}
|
|
41
43
|
return messages.map((message) => ({
|
|
42
44
|
...message,
|
|
43
|
-
id: persistedIds.get(message) ?? message.id
|
|
45
|
+
id: persistedIds.get(message) ?? message.id,
|
|
46
|
+
...persistedMessages.some(
|
|
47
|
+
(saved) => saved.id === message.id && saved.interrupted
|
|
48
|
+
) ? { metadata: { ...message.metadata, interrupted: true } } : {}
|
|
44
49
|
}));
|
|
45
50
|
}
|
|
46
51
|
function persistedMessagesToUiMessages(messages) {
|
|
47
|
-
return messages
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
} catch {
|
|
53
|
-
parts = [{ type: "text", text: message.content }];
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
id: message.id,
|
|
57
|
-
role: message.role,
|
|
58
|
-
parts
|
|
59
|
-
};
|
|
52
|
+
return persistedHistory(messages).map((message) => {
|
|
53
|
+
const abandoned = message.parts.some(
|
|
54
|
+
(part) => "toolCallId" in part && (part.state === "input-streaming" || part.state === "input-available")
|
|
55
|
+
);
|
|
56
|
+
return abandoned ? { ...message, metadata: { interrupted: true } } : message;
|
|
60
57
|
});
|
|
61
58
|
}
|
|
62
59
|
function ChatActionCheck({
|
|
@@ -206,6 +203,9 @@ function ChatInterface({
|
|
|
206
203
|
const latestStreamRequestGeneration = useRef(0);
|
|
207
204
|
const pendingStreamRequests = useRef([]);
|
|
208
205
|
const latestMessages = useRef([]);
|
|
206
|
+
const contextCheckpoint = useRef(void 0);
|
|
207
|
+
const submittedMessages = useRef([]);
|
|
208
|
+
const [isCompacting, setIsCompacting] = useState(false);
|
|
209
209
|
const conversationsListQueryKey = useMemo(() => {
|
|
210
210
|
if (isPublicMode) return ["ai-chat", "disabled"];
|
|
211
211
|
const client = createApiClient({
|
|
@@ -305,6 +305,7 @@ function ChatInterface({
|
|
|
305
305
|
pendingStreamRequests.current.push(request);
|
|
306
306
|
activeStreamPartitionKey.current = request.identityPartitionKey;
|
|
307
307
|
}
|
|
308
|
+
setIsCompacting(false);
|
|
308
309
|
const currentPageContext = pageAIContextRef.current;
|
|
309
310
|
const pageContextBody = currentPageContext?.pageDescription ? {
|
|
310
311
|
pageContext: currentPageContext.pageDescription,
|
|
@@ -317,18 +318,20 @@ function ChatInterface({
|
|
|
317
318
|
if (newUserMessage) {
|
|
318
319
|
messagesToSend.push(newUserMessage);
|
|
319
320
|
}
|
|
321
|
+
submittedMessages.current = messagesToSend;
|
|
320
322
|
editMessagesRef.current = null;
|
|
321
323
|
return {
|
|
322
324
|
body: {
|
|
323
|
-
messages: messagesToSend,
|
|
325
|
+
...isPublicMode ? contextRequest(messagesToSend, contextCheckpoint.current) : { messages: messagesToSend },
|
|
324
326
|
conversationId: conversationIdRef.current,
|
|
325
327
|
...pageContextBody
|
|
326
328
|
}
|
|
327
329
|
};
|
|
328
330
|
}
|
|
331
|
+
submittedMessages.current = hookMessages;
|
|
329
332
|
return {
|
|
330
333
|
body: {
|
|
331
|
-
messages: hookMessages,
|
|
334
|
+
...isPublicMode ? contextRequest(hookMessages, contextCheckpoint.current) : { messages: hookMessages },
|
|
332
335
|
conversationId: conversationIdRef.current,
|
|
333
336
|
...pageContextBody
|
|
334
337
|
}
|
|
@@ -372,6 +375,21 @@ function ChatInterface({
|
|
|
372
375
|
} = useChat({
|
|
373
376
|
id: `${chatInstanceId}:${isPublicMode ? "public" : identitySessionVersion}`,
|
|
374
377
|
transport,
|
|
378
|
+
onData: ({ type, data }) => {
|
|
379
|
+
if (!mounted.current || !data || typeof data !== "object") return;
|
|
380
|
+
if (!isPublicMode && (identitySessionVersion !== identitySessionGeneration.current || activeStreamPartitionKey.current !== latestIdentityPartitionKey.current))
|
|
381
|
+
return;
|
|
382
|
+
if (type === "data-context-status" && "compacting" in data) {
|
|
383
|
+
setIsCompacting(data.compacting === true);
|
|
384
|
+
}
|
|
385
|
+
if (isPublicMode && type === "data-context-checkpoint" && "summary" in data && "throughMessageId" in data && typeof data.summary === "string" && typeof data.throughMessageId === "string") {
|
|
386
|
+
contextCheckpoint.current = captureContextCheckpoint(
|
|
387
|
+
submittedMessages.current,
|
|
388
|
+
data.summary,
|
|
389
|
+
data.throughMessageId
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
},
|
|
375
393
|
// Automatically resubmit after all client-side tool results are provided
|
|
376
394
|
sendAutomaticallyWhen: (options) => mounted.current && (isPublicMode || identitySessionVersion === identitySessionGeneration.current) && lastAssistantMessageIsCompleteWithToolCalls(options),
|
|
377
395
|
onToolCall: async ({ toolCall }) => {
|
|
@@ -422,6 +440,7 @@ function ChatInterface({
|
|
|
422
440
|
}
|
|
423
441
|
},
|
|
424
442
|
onError: (err) => {
|
|
443
|
+
setIsCompacting(false);
|
|
425
444
|
if (!mounted.current) return;
|
|
426
445
|
console.error("useChat onError:", err);
|
|
427
446
|
if (!isPublicMode && identitySessionVersion !== identitySessionGeneration.current) {
|
|
@@ -435,6 +454,7 @@ function ChatInterface({
|
|
|
435
454
|
trackFailure();
|
|
436
455
|
},
|
|
437
456
|
onFinish: async (completion) => {
|
|
457
|
+
setIsCompacting(false);
|
|
438
458
|
if (!mounted.current || !isPublicMode && identitySessionVersion !== identitySessionGeneration.current)
|
|
439
459
|
return;
|
|
440
460
|
if (completion?.isError || completion?.isDisconnect) trackFailure();
|
|
@@ -456,6 +476,19 @@ function ChatInterface({
|
|
|
456
476
|
).length
|
|
457
477
|
});
|
|
458
478
|
}
|
|
479
|
+
if (completion?.isAbort || completion?.isError || completion?.isDisconnect) {
|
|
480
|
+
setMessages(
|
|
481
|
+
(current) => current.map(
|
|
482
|
+
(message) => message.id === completion.message.id ? {
|
|
483
|
+
...message,
|
|
484
|
+
metadata: {
|
|
485
|
+
...message.metadata,
|
|
486
|
+
interrupted: true
|
|
487
|
+
}
|
|
488
|
+
} : message
|
|
489
|
+
)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
459
492
|
if (isPublicMode) {
|
|
460
493
|
activeStreamPartitionKey.current = void 0;
|
|
461
494
|
return;
|
|
@@ -930,7 +963,19 @@ function ChatInterface({
|
|
|
930
963
|
},
|
|
931
964
|
m.id || `msg-${index}`
|
|
932
965
|
)),
|
|
933
|
-
isLoading &&
|
|
966
|
+
isLoading && isCompacting && /* @__PURE__ */ jsx(
|
|
967
|
+
"div",
|
|
968
|
+
{
|
|
969
|
+
role: "status",
|
|
970
|
+
className: "text-muted-foreground text-sm py-4",
|
|
971
|
+
children: tr(
|
|
972
|
+
"CHAT_COMPACTING",
|
|
973
|
+
"aiChat.chat.compacting",
|
|
974
|
+
"Summarizing earlier conversation\u2026"
|
|
975
|
+
)
|
|
976
|
+
}
|
|
977
|
+
),
|
|
978
|
+
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
979
|
(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
980
|
/* @__PURE__ */ jsx("span", { className: "flex-1", children: tr(
|
|
936
981
|
"CHAT_ERROR",
|
|
@@ -82,6 +82,9 @@ function ChatMessage({
|
|
|
82
82
|
isRetrying = false
|
|
83
83
|
}) {
|
|
84
84
|
const isUser = message.role === "user";
|
|
85
|
+
const interrupted = Boolean(
|
|
86
|
+
message.metadata && typeof message.metadata === "object" && "interrupted" in message.metadata && message.metadata.interrupted
|
|
87
|
+
);
|
|
85
88
|
const {
|
|
86
89
|
localization: customLocalization,
|
|
87
90
|
toolRenderers,
|
|
@@ -359,10 +362,15 @@ function ChatMessage({
|
|
|
359
362
|
toolParts.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: toolParts.map((part) => {
|
|
360
363
|
const toolName = ai.getToolName(part);
|
|
361
364
|
const toolCallId = part.toolCallId;
|
|
362
|
-
const
|
|
365
|
+
const unfinished = interrupted && (part.state === "input-streaming" || part.state === "input-available");
|
|
366
|
+
const state = unfinished ? "output-error" : part.state;
|
|
363
367
|
const input = part.input;
|
|
364
368
|
const output = part.output;
|
|
365
|
-
const errorText =
|
|
369
|
+
const errorText = unfinished ? tr(
|
|
370
|
+
"CHAT_INTERRUPTED",
|
|
371
|
+
"aiChat.interrupted",
|
|
372
|
+
"Response interrupted"
|
|
373
|
+
) : part.errorText;
|
|
366
374
|
const isLoading = state === "input-streaming" || state === "input-available";
|
|
367
375
|
const toolCallProps = {
|
|
368
376
|
toolCallId,
|
|
@@ -379,6 +387,18 @@ function ChatMessage({
|
|
|
379
387
|
}
|
|
380
388
|
return /* @__PURE__ */ jsxRuntime.jsx(toolCallDisplay.ToolCallDisplay, { ...toolCallProps }, toolCallId);
|
|
381
389
|
}) }),
|
|
390
|
+
interrupted && !isStreaming && /* @__PURE__ */ jsxRuntime.jsx(
|
|
391
|
+
"p",
|
|
392
|
+
{
|
|
393
|
+
className: "text-xs text-muted-foreground",
|
|
394
|
+
"data-testid": "chat-interrupted",
|
|
395
|
+
children: tr(
|
|
396
|
+
"CHAT_INTERRUPTED",
|
|
397
|
+
"aiChat.interrupted",
|
|
398
|
+
"Response interrupted"
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
),
|
|
382
402
|
displayContent ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
383
403
|
markdownContent.MarkdownContent,
|
|
384
404
|
{
|
|
@@ -76,6 +76,9 @@ function ChatMessage({
|
|
|
76
76
|
isRetrying = false
|
|
77
77
|
}) {
|
|
78
78
|
const isUser = message.role === "user";
|
|
79
|
+
const interrupted = Boolean(
|
|
80
|
+
message.metadata && typeof message.metadata === "object" && "interrupted" in message.metadata && message.metadata.interrupted
|
|
81
|
+
);
|
|
79
82
|
const {
|
|
80
83
|
localization: customLocalization,
|
|
81
84
|
toolRenderers,
|
|
@@ -353,10 +356,15 @@ function ChatMessage({
|
|
|
353
356
|
toolParts.length > 0 && /* @__PURE__ */ jsx("div", { className: "space-y-2", children: toolParts.map((part) => {
|
|
354
357
|
const toolName = getToolName(part);
|
|
355
358
|
const toolCallId = part.toolCallId;
|
|
356
|
-
const
|
|
359
|
+
const unfinished = interrupted && (part.state === "input-streaming" || part.state === "input-available");
|
|
360
|
+
const state = unfinished ? "output-error" : part.state;
|
|
357
361
|
const input = part.input;
|
|
358
362
|
const output = part.output;
|
|
359
|
-
const errorText =
|
|
363
|
+
const errorText = unfinished ? tr(
|
|
364
|
+
"CHAT_INTERRUPTED",
|
|
365
|
+
"aiChat.interrupted",
|
|
366
|
+
"Response interrupted"
|
|
367
|
+
) : part.errorText;
|
|
360
368
|
const isLoading = state === "input-streaming" || state === "input-available";
|
|
361
369
|
const toolCallProps = {
|
|
362
370
|
toolCallId,
|
|
@@ -373,6 +381,18 @@ function ChatMessage({
|
|
|
373
381
|
}
|
|
374
382
|
return /* @__PURE__ */ jsx(ToolCallDisplay, { ...toolCallProps }, toolCallId);
|
|
375
383
|
}) }),
|
|
384
|
+
interrupted && !isStreaming && /* @__PURE__ */ jsx(
|
|
385
|
+
"p",
|
|
386
|
+
{
|
|
387
|
+
className: "text-xs text-muted-foreground",
|
|
388
|
+
"data-testid": "chat-interrupted",
|
|
389
|
+
children: tr(
|
|
390
|
+
"CHAT_INTERRUPTED",
|
|
391
|
+
"aiChat.interrupted",
|
|
392
|
+
"Response interrupted"
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
),
|
|
376
396
|
displayContent ? /* @__PURE__ */ jsx(
|
|
377
397
|
MarkdownContent,
|
|
378
398
|
{
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function captureContextCheckpoint(messages, summary, throughMessageId) {
|
|
4
|
+
const index = messages.findIndex(
|
|
5
|
+
(message) => message.id === throughMessageId
|
|
6
|
+
);
|
|
7
|
+
if (index < 0) return void 0;
|
|
8
|
+
return {
|
|
9
|
+
summary,
|
|
10
|
+
throughMessageId,
|
|
11
|
+
prefix: JSON.stringify(messages.slice(0, index + 1))
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function contextRequest(messages, checkpoint) {
|
|
15
|
+
if (checkpoint) {
|
|
16
|
+
const index = messages.findIndex(
|
|
17
|
+
(message) => message.id === checkpoint.throughMessageId
|
|
18
|
+
);
|
|
19
|
+
if (index >= 0 && JSON.stringify(messages.slice(0, index + 1)) === checkpoint.prefix) {
|
|
20
|
+
return {
|
|
21
|
+
messages: messages.slice(index + 1),
|
|
22
|
+
contextSummary: checkpoint.summary
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { messages };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
exports.captureContextCheckpoint = captureContextCheckpoint;
|
|
30
|
+
exports.contextRequest = contextRequest;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function captureContextCheckpoint(messages, summary, throughMessageId) {
|
|
2
|
+
const index = messages.findIndex(
|
|
3
|
+
(message) => message.id === throughMessageId
|
|
4
|
+
);
|
|
5
|
+
if (index < 0) return void 0;
|
|
6
|
+
return {
|
|
7
|
+
summary,
|
|
8
|
+
throughMessageId,
|
|
9
|
+
prefix: JSON.stringify(messages.slice(0, index + 1))
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function contextRequest(messages, checkpoint) {
|
|
13
|
+
if (checkpoint) {
|
|
14
|
+
const index = messages.findIndex(
|
|
15
|
+
(message) => message.id === checkpoint.throughMessageId
|
|
16
|
+
);
|
|
17
|
+
if (index >= 0 && JSON.stringify(messages.slice(0, index + 1)) === checkpoint.prefix) {
|
|
18
|
+
return {
|
|
19
|
+
messages: messages.slice(index + 1),
|
|
20
|
+
contextSummary: checkpoint.summary
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { messages };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { captureContextCheckpoint, contextRequest };
|
|
@@ -19,6 +19,10 @@ const aiChatSchema = db.createDbPlugin("aiChat", {
|
|
|
19
19
|
type: "string",
|
|
20
20
|
required: true
|
|
21
21
|
},
|
|
22
|
+
contextCheckpoint: {
|
|
23
|
+
type: "string",
|
|
24
|
+
required: false
|
|
25
|
+
},
|
|
22
26
|
createdAt: {
|
|
23
27
|
type: "date",
|
|
24
28
|
defaultValue: () => /* @__PURE__ */ new Date()
|
|
@@ -54,6 +58,10 @@ const aiChatSchema = db.createDbPlugin("aiChat", {
|
|
|
54
58
|
type: "string",
|
|
55
59
|
required: true
|
|
56
60
|
},
|
|
61
|
+
interrupted: {
|
|
62
|
+
type: "boolean",
|
|
63
|
+
required: false
|
|
64
|
+
},
|
|
57
65
|
createdAt: {
|
|
58
66
|
type: "date",
|
|
59
67
|
defaultValue: () => /* @__PURE__ */ new Date()
|
|
@@ -17,6 +17,10 @@ const aiChatSchema = createDbPlugin("aiChat", {
|
|
|
17
17
|
type: "string",
|
|
18
18
|
required: true
|
|
19
19
|
},
|
|
20
|
+
contextCheckpoint: {
|
|
21
|
+
type: "string",
|
|
22
|
+
required: false
|
|
23
|
+
},
|
|
20
24
|
createdAt: {
|
|
21
25
|
type: "date",
|
|
22
26
|
defaultValue: () => /* @__PURE__ */ new Date()
|
|
@@ -52,6 +56,10 @@ const aiChatSchema = createDbPlugin("aiChat", {
|
|
|
52
56
|
type: "string",
|
|
53
57
|
required: true
|
|
54
58
|
},
|
|
59
|
+
interrupted: {
|
|
60
|
+
type: "boolean",
|
|
61
|
+
required: false
|
|
62
|
+
},
|
|
55
63
|
createdAt: {
|
|
56
64
|
type: "date",
|
|
57
65
|
defaultValue: () => /* @__PURE__ */ new Date()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function isMessagePart(value) {
|
|
4
|
+
if (!value || typeof value !== "object" || !("type" in value)) return false;
|
|
5
|
+
const part = value;
|
|
6
|
+
if (typeof part.type !== "string") return false;
|
|
7
|
+
switch (part.type) {
|
|
8
|
+
case "text":
|
|
9
|
+
case "reasoning":
|
|
10
|
+
return typeof part.text === "string";
|
|
11
|
+
case "file":
|
|
12
|
+
return typeof part.url === "string" && typeof part.mediaType === "string";
|
|
13
|
+
case "source-url":
|
|
14
|
+
return typeof part.sourceId === "string" && typeof part.url === "string";
|
|
15
|
+
case "source-document":
|
|
16
|
+
return typeof part.sourceId === "string" && typeof part.mediaType === "string" && typeof part.title === "string";
|
|
17
|
+
case "step-start":
|
|
18
|
+
return true;
|
|
19
|
+
default:
|
|
20
|
+
if (part.type.startsWith("data-")) return "data" in part;
|
|
21
|
+
return (part.type.startsWith("tool-") || part.type === "dynamic-tool" && typeof part.toolName === "string") && typeof part.toolCallId === "string" && [
|
|
22
|
+
"input-streaming",
|
|
23
|
+
"input-available",
|
|
24
|
+
"output-available",
|
|
25
|
+
"output-error"
|
|
26
|
+
].includes(String(part.state));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function persistedHistory(messages) {
|
|
30
|
+
return messages.filter((message) => message.role !== "data").map((message) => {
|
|
31
|
+
let parts;
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(message.content);
|
|
34
|
+
parts = Array.isArray(parsed) && (parsed.length > 0 || message.role === "assistant") && parsed.every(isMessagePart) ? parsed : [{ type: "text", text: message.content }];
|
|
35
|
+
} catch {
|
|
36
|
+
parts = [{ type: "text", text: message.content }];
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
id: message.id,
|
|
40
|
+
role: message.role,
|
|
41
|
+
parts,
|
|
42
|
+
...message.interrupted ? { metadata: { interrupted: true } } : {}
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
exports.persistedHistory = persistedHistory;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function isMessagePart(value) {
|
|
2
|
+
if (!value || typeof value !== "object" || !("type" in value)) return false;
|
|
3
|
+
const part = value;
|
|
4
|
+
if (typeof part.type !== "string") return false;
|
|
5
|
+
switch (part.type) {
|
|
6
|
+
case "text":
|
|
7
|
+
case "reasoning":
|
|
8
|
+
return typeof part.text === "string";
|
|
9
|
+
case "file":
|
|
10
|
+
return typeof part.url === "string" && typeof part.mediaType === "string";
|
|
11
|
+
case "source-url":
|
|
12
|
+
return typeof part.sourceId === "string" && typeof part.url === "string";
|
|
13
|
+
case "source-document":
|
|
14
|
+
return typeof part.sourceId === "string" && typeof part.mediaType === "string" && typeof part.title === "string";
|
|
15
|
+
case "step-start":
|
|
16
|
+
return true;
|
|
17
|
+
default:
|
|
18
|
+
if (part.type.startsWith("data-")) return "data" in part;
|
|
19
|
+
return (part.type.startsWith("tool-") || part.type === "dynamic-tool" && typeof part.toolName === "string") && typeof part.toolCallId === "string" && [
|
|
20
|
+
"input-streaming",
|
|
21
|
+
"input-available",
|
|
22
|
+
"output-available",
|
|
23
|
+
"output-error"
|
|
24
|
+
].includes(String(part.state));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function persistedHistory(messages) {
|
|
28
|
+
return messages.filter((message) => message.role !== "data").map((message) => {
|
|
29
|
+
let parts;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(message.content);
|
|
32
|
+
parts = Array.isArray(parsed) && (parsed.length > 0 || message.role === "assistant") && parsed.every(isMessagePart) ? parsed : [{ type: "text", text: message.content }];
|
|
33
|
+
} catch {
|
|
34
|
+
parts = [{ type: "text", text: message.content }];
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
id: message.id,
|
|
38
|
+
role: message.role,
|
|
39
|
+
parts,
|
|
40
|
+
...message.interrupted ? { metadata: { interrupted: true } } : {}
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { persistedHistory };
|
|
@@ -34,6 +34,8 @@ const chatRequestSchema = z.z.object({
|
|
|
34
34
|
])
|
|
35
35
|
),
|
|
36
36
|
conversationId: z.z.string().optional(),
|
|
37
|
+
/** Untrusted continuation summary for opt-in public context compaction. */
|
|
38
|
+
contextSummary: z.z.string().max(32e3).optional(),
|
|
37
39
|
model: z.z.string().optional(),
|
|
38
40
|
/**
|
|
39
41
|
* Description of the current page context, injected into the AI system prompt.
|
|
@@ -32,6 +32,8 @@ const chatRequestSchema = z.object({
|
|
|
32
32
|
])
|
|
33
33
|
),
|
|
34
34
|
conversationId: z.string().optional(),
|
|
35
|
+
/** Untrusted continuation summary for opt-in public context compaction. */
|
|
36
|
+
contextSummary: z.string().max(32e3).optional(),
|
|
35
37
|
model: z.string().optional(),
|
|
36
38
|
/**
|
|
37
39
|
* Description of the current page context, injected into the AI system prompt.
|