@constructor-io/constructorio-ui-asa 1.9.0 → 1.10.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 +8 -210
- package/dist/constructorio-ui-asa-bundled.js +78 -20
- package/lib/cjs/components/Chat/AiMessage.css +52 -0
- package/lib/cjs/components/Chat/AiMessage.js +6 -2
- package/lib/cjs/components/Chat/Chat.js +2 -1
- package/lib/cjs/components/Chat/ChatInput.js +3 -3
- package/lib/cjs/hooks/asaStreamHandlers.js +1 -1
- package/lib/cjs/hooks/persistence/backgroundTurns.js +35 -0
- package/lib/cjs/hooks/persistence/useChatPersistence.js +35 -10
- package/lib/cjs/hooks/useAsaResults.js +23 -22
- package/lib/cjs/index.js +3 -1
- package/lib/cjs/utils/chatThreads.js +15 -1
- package/lib/cjs/utils/localStoragePersistence.js +110 -19
- package/lib/cjs/utils/markdown.js +60 -0
- package/lib/cjs/version.js +1 -1
- package/lib/mjs/components/Chat/AiMessage.css +52 -0
- package/lib/mjs/components/Chat/AiMessage.js +6 -2
- package/lib/mjs/components/Chat/Chat.js +2 -1
- package/lib/mjs/components/Chat/ChatInput.js +3 -3
- package/lib/mjs/hooks/asaStreamHandlers.js +1 -1
- package/lib/mjs/hooks/persistence/backgroundTurns.js +35 -1
- package/lib/mjs/hooks/persistence/useChatPersistence.js +35 -11
- package/lib/mjs/hooks/useAsaResults.js +23 -21
- package/lib/mjs/index.js +1 -0
- package/lib/mjs/utils/chatThreads.js +12 -0
- package/lib/mjs/utils/localStoragePersistence.js +106 -14
- package/lib/mjs/utils/markdown.js +53 -0
- package/lib/mjs/version.js +1 -1
- package/lib/types/hooks/persistence/backgroundTurns.d.ts +6 -0
- package/lib/types/hooks/persistence/useChatPersistence.d.ts +4 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/types.d.ts +21 -5
- package/lib/types/utils/chatThreads.d.ts +4 -0
- package/lib/types/utils/markdown.d.ts +8 -0
- package/lib/types/version.d.ts +1 -1
- package/package.json +5 -1
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
.cio-asa-ai-message__text {
|
|
26
|
+
width: 100%;
|
|
27
|
+
min-width: 0;
|
|
28
|
+
overflow-wrap: break-word;
|
|
26
29
|
font-weight: 400;
|
|
27
30
|
font-size: 14px;
|
|
28
31
|
line-height: 20px;
|
|
@@ -33,3 +36,52 @@
|
|
|
33
36
|
.cio-asa-ai-message__bubble--error .cio-asa-ai-message__text {
|
|
34
37
|
color: #B42318;
|
|
35
38
|
}
|
|
39
|
+
|
|
40
|
+
/* Agent markdown (see utils/markdown.ts) */
|
|
41
|
+
.cio-asa-ai-message__text > :first-child {
|
|
42
|
+
margin-top: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.cio-asa-ai-message__text > :last-child {
|
|
46
|
+
margin-bottom: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.cio-asa-ai-message__text p {
|
|
50
|
+
margin: 0 0 8px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.cio-asa-ai-message__text ul,
|
|
54
|
+
.cio-asa-ai-message__text ol {
|
|
55
|
+
margin: 0 0 8px;
|
|
56
|
+
padding-left: 20px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.cio-asa-ai-message__text a {
|
|
60
|
+
color: inherit;
|
|
61
|
+
text-decoration: underline;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Wide tables scroll inside the bubble instead of widening it. */
|
|
65
|
+
.cio-asa-ai-message__text table {
|
|
66
|
+
display: block;
|
|
67
|
+
max-width: 100%;
|
|
68
|
+
overflow-x: auto;
|
|
69
|
+
margin: 0 0 8px;
|
|
70
|
+
border-collapse: collapse;
|
|
71
|
+
font-size: 12px;
|
|
72
|
+
line-height: 16px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.cio-asa-ai-message__text th,
|
|
76
|
+
.cio-asa-ai-message__text td {
|
|
77
|
+
padding: 6px 8px;
|
|
78
|
+
border: 1px solid #D5D9E2;
|
|
79
|
+
background: #FFFFFF;
|
|
80
|
+
text-align: left;
|
|
81
|
+
vertical-align: top;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.cio-asa-ai-message__text th {
|
|
85
|
+
background: #F6F7F9;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
}
|
|
@@ -4,6 +4,7 @@ const tslib_1 = require("tslib");
|
|
|
4
4
|
const react_1 = tslib_1.__importDefault(require("react"));
|
|
5
5
|
const constructorio_ui_components_1 = require("@constructor-io/constructorio-ui-components");
|
|
6
6
|
const translate_1 = tslib_1.__importDefault(require("../../utils/translate"));
|
|
7
|
+
const markdown_1 = require("../../utils/markdown");
|
|
7
8
|
const TypingIndicator_1 = tslib_1.__importDefault(require("./TypingIndicator"));
|
|
8
9
|
function AiMessage({ message, componentOverrides, translations }) {
|
|
9
10
|
var _a, _b, _c;
|
|
@@ -15,6 +16,7 @@ function AiMessage({ message, componentOverrides, translations }) {
|
|
|
15
16
|
const errorKey = message.interrupted ? 'CioAsa.error.interrupted' : 'CioAsa.error.message';
|
|
16
17
|
const text = isError ? message.text || (0, translate_1.default)(errorKey, translations) : message.text || '';
|
|
17
18
|
const hasText = text.length > 0;
|
|
19
|
+
const html = (0, markdown_1.renderMarkdown)(text);
|
|
18
20
|
const loaderRenderProps = { translations };
|
|
19
21
|
const textRenderProps = { text };
|
|
20
22
|
return (react_1.default.createElement("div", { className: 'cio-asa-ai-message' },
|
|
@@ -26,8 +28,10 @@ function AiMessage({ message, componentOverrides, translations }) {
|
|
|
26
28
|
isError && 'cio-asa-ai-message__bubble--error',
|
|
27
29
|
]
|
|
28
30
|
.filter(Boolean)
|
|
29
|
-
.join(' '), role: isError ? 'alert' : undefined },
|
|
30
|
-
|
|
31
|
+
.join(' '), role: isError ? 'alert' : undefined }, html ? (react_1.default.createElement("div", { className: 'cio-asa-ai-message__text',
|
|
32
|
+
// Sanitized by renderMarkdown
|
|
33
|
+
// eslint-disable-next-line react/no-danger
|
|
34
|
+
dangerouslySetInnerHTML: { __html: html } })) : (react_1.default.createElement("div", { className: 'cio-asa-ai-message__text' }, text)))))));
|
|
31
35
|
}
|
|
32
36
|
exports.default = AiMessage;
|
|
33
37
|
AiMessage.displayName = 'AiMessage';
|
|
@@ -8,6 +8,7 @@ const useFocusTrap_1 = tslib_1.__importDefault(require("../../hooks/useFocusTrap
|
|
|
8
8
|
const useLatest_1 = tslib_1.__importDefault(require("../../hooks/useLatest"));
|
|
9
9
|
const productNormalizer_1 = require("../../utils/productNormalizer");
|
|
10
10
|
const translate_1 = tslib_1.__importDefault(require("../../utils/translate"));
|
|
11
|
+
const markdown_1 = require("../../utils/markdown");
|
|
11
12
|
const ChatHeader_1 = tslib_1.__importDefault(require("./ChatHeader"));
|
|
12
13
|
const WelcomeScreen_1 = tslib_1.__importDefault(require("./WelcomeScreen"));
|
|
13
14
|
const ChatMessageList_1 = tslib_1.__importDefault(require("./ChatMessageList"));
|
|
@@ -25,7 +26,7 @@ function getAnnouncement(messages, translations) {
|
|
|
25
26
|
return `${youSaid}: ${userText}. ${typing}`;
|
|
26
27
|
}
|
|
27
28
|
if (lastMessage.status === 'done' && lastMessage.text) {
|
|
28
|
-
return `${(0, translate_1.default)('CioAsa.aiMessage.ariaLabel', translations)}: ${lastMessage.text}`;
|
|
29
|
+
return `${(0, translate_1.default)('CioAsa.aiMessage.ariaLabel', translations)}: ${(0, markdown_1.markdownToText)(lastMessage.text)}`;
|
|
29
30
|
}
|
|
30
31
|
return '';
|
|
31
32
|
}
|
|
@@ -12,8 +12,7 @@ function ChatInput({ onSubmit, isDisabled = false, isStreaming = false, onAbort,
|
|
|
12
12
|
isDisabled,
|
|
13
13
|
submitOnEnterOnly: true,
|
|
14
14
|
});
|
|
15
|
-
|
|
16
|
-
const canAbort = isStreaming && showStopButton && typeof onAbort === 'function';
|
|
15
|
+
const canAbort = isStreaming && typeof onAbort === 'function';
|
|
17
16
|
const renderProps = {
|
|
18
17
|
value,
|
|
19
18
|
onChange: setValue,
|
|
@@ -21,13 +20,14 @@ function ChatInput({ onSubmit, isDisabled = false, isStreaming = false, onAbort,
|
|
|
21
20
|
placeholder: (0, translate_1.default)('CioAsa.input.placeholder', translations),
|
|
22
21
|
isDisabled,
|
|
23
22
|
isStreaming,
|
|
23
|
+
canAbort,
|
|
24
24
|
onAbort: onAbort !== null && onAbort !== void 0 ? onAbort : (() => { }),
|
|
25
25
|
};
|
|
26
26
|
return (react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { override: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.reactNode, props: renderProps },
|
|
27
27
|
react_1.default.createElement("div", { className: 'cio-asa-chat-input-wrapper' },
|
|
28
28
|
react_1.default.createElement("div", { className: 'cio-asa-chat-input' },
|
|
29
29
|
react_1.default.createElement("input", { type: 'text', className: 'cio-asa-chat-input__field', value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleKeyDown, placeholder: renderProps.placeholder, disabled: isDisabled, "aria-label": (0, translate_1.default)('CioAsa.input.ariaLabel', translations) }),
|
|
30
|
-
canAbort ? (react_1.default.createElement("button", { type: 'button', className: 'cio-asa-chat-input__send cio-asa-chat-input__stop', onClick: onAbort, "aria-label": (0, translate_1.default)('CioAsa.input.stopAriaLabel', translations) },
|
|
30
|
+
canAbort && showStopButton ? (react_1.default.createElement("button", { type: 'button', className: 'cio-asa-chat-input__send cio-asa-chat-input__stop', onClick: onAbort, "aria-label": (0, translate_1.default)('CioAsa.input.stopAriaLabel', translations) },
|
|
31
31
|
react_1.default.createElement(icons_1.StopIcon, null))) : (react_1.default.createElement("button", { type: 'button', className: 'cio-asa-chat-input__send', onClick: handleSubmit, disabled: isDisabled || !value.trim(), "aria-label": (0, translate_1.default)('CioAsa.input.sendAriaLabel', translations) },
|
|
32
32
|
react_1.default.createElement(icons_1.SendArrowIcon, null)))))));
|
|
33
33
|
}
|
|
@@ -36,7 +36,7 @@ function handleSearchResult(data, pendingGroup, assistantId, setMessages) {
|
|
|
36
36
|
}
|
|
37
37
|
exports.handleSearchResult = handleSearchResult;
|
|
38
38
|
function handleMessage(data, assistantId, setMessages) {
|
|
39
|
-
updateMessageById(setMessages, assistantId, (msg) => (Object.assign(Object.assign({}, msg), { status: 'streaming', text:
|
|
39
|
+
updateMessageById(setMessages, assistantId, (msg) => (Object.assign(Object.assign({}, msg), { status: 'streaming', text: [msg.text, data === null || data === void 0 ? void 0 : data.text].filter(Boolean).join('\n') })));
|
|
40
40
|
}
|
|
41
41
|
exports.handleMessage = handleMessage;
|
|
42
42
|
function handleFollowUpRefinement(data, assistantId, setMessages) {
|
|
@@ -31,6 +31,9 @@ class BackgroundTurn {
|
|
|
31
31
|
this.save().then(notify);
|
|
32
32
|
},
|
|
33
33
|
onDone: () => {
|
|
34
|
+
if (!this.streaming)
|
|
35
|
+
return;
|
|
36
|
+
this.release();
|
|
34
37
|
this.streaming = false;
|
|
35
38
|
this.messages = (0, chatThreads_1.normalizeHydratedMessages)(this.messages);
|
|
36
39
|
this.save().then(() => {
|
|
@@ -61,6 +64,36 @@ class BackgroundTurn {
|
|
|
61
64
|
/** Later writes go into `store`. */
|
|
62
65
|
moveTo(store) {
|
|
63
66
|
this.store = store;
|
|
67
|
+
if (this.streaming)
|
|
68
|
+
this.listen();
|
|
69
|
+
}
|
|
70
|
+
/** Stops the answer when another tab asks to, through the store it is saved into. */
|
|
71
|
+
listen() {
|
|
72
|
+
var _a, _b;
|
|
73
|
+
this.release();
|
|
74
|
+
this.stopListening = (_b = (_a = this.store).subscribeAbort) === null || _b === void 0 ? void 0 : _b.call(_a, ({ messageId }) => {
|
|
75
|
+
if (messageId === this.turn.assistantId)
|
|
76
|
+
this.abort();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
release() {
|
|
80
|
+
var _a;
|
|
81
|
+
(_a = this.stopListening) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
82
|
+
this.stopListening = undefined;
|
|
83
|
+
}
|
|
84
|
+
/** Stops the answer and stores it settled, as the stop button does on screen. */
|
|
85
|
+
abort() {
|
|
86
|
+
if (!this.streaming)
|
|
87
|
+
return;
|
|
88
|
+
this.streaming = false;
|
|
89
|
+
this.release();
|
|
90
|
+
this.turn.cancel();
|
|
91
|
+
this.messages = (0, chatThreads_1.settleCancelledReply)(this.messages, this.turn.assistantId);
|
|
92
|
+
this.save().then(() => {
|
|
93
|
+
if (this.saved)
|
|
94
|
+
turns.delete(this);
|
|
95
|
+
notify();
|
|
96
|
+
});
|
|
64
97
|
}
|
|
65
98
|
/** Stores the conversation settled, without awaiting: the page is going away. */
|
|
66
99
|
flush() {
|
|
@@ -82,6 +115,7 @@ let listeningForPagehide = false;
|
|
|
82
115
|
function continueInBackground(turn, params) {
|
|
83
116
|
const bg = new BackgroundTurn(turn, params);
|
|
84
117
|
turns.add(bg);
|
|
118
|
+
bg.listen();
|
|
85
119
|
if (!listeningForPagehide && typeof window !== 'undefined') {
|
|
86
120
|
listeningForPagehide = true;
|
|
87
121
|
window.addEventListener('pagehide', () => turns.forEach((t) => t.flush()));
|
|
@@ -98,6 +132,7 @@ function resumeBackgroundTurn(store, threadId, createOwner) {
|
|
|
98
132
|
if (!bg)
|
|
99
133
|
return null;
|
|
100
134
|
turns.delete(bg);
|
|
135
|
+
bg.release();
|
|
101
136
|
if (bg.streaming)
|
|
102
137
|
bg.turn.owner = createOwner(bg.turn);
|
|
103
138
|
return bg;
|
|
@@ -10,10 +10,12 @@ const useWriteQueue_1 = tslib_1.__importDefault(require("./useWriteQueue"));
|
|
|
10
10
|
const useThreadList_1 = tslib_1.__importDefault(require("./useThreadList"));
|
|
11
11
|
const useForeignStream_1 = tslib_1.__importDefault(require("./useForeignStream"));
|
|
12
12
|
const backgroundTurns_1 = require("./backgroundTurns");
|
|
13
|
+
/** Whether `store` can both ask another tab to stop an answer and hear such a request. */
|
|
14
|
+
const canAbortAcrossTabs = (store) => Boolean((store === null || store === void 0 ? void 0 : store.requestAbort) && store.subscribeAbort);
|
|
13
15
|
const hasUnsavedWork = (session) => session.isStreaming || session.dirty || session.orphanIds.length > 0;
|
|
14
16
|
/** Restores, saves and syncs the conversation with `store`; inert when `store` is undefined. */
|
|
15
17
|
function useChatPersistence(params) {
|
|
16
|
-
const { store, scope, index, session, initialThreadId, messages, setMessages, isStreaming, setIsStreaming, turnRef, cancelStream, } = params;
|
|
18
|
+
const { store, scope, index, session, initialThreadId, messages, setMessages, isStreaming, setIsStreaming, turnRef, cancelStream, abortTurn, } = params;
|
|
17
19
|
if (store)
|
|
18
20
|
(0, chatThreads_1.getTabId)();
|
|
19
21
|
const [isHydrating, setIsHydrating] = (0, react_1.useState)(Boolean(store));
|
|
@@ -61,6 +63,15 @@ function useChatPersistence(params) {
|
|
|
61
63
|
setIsHydrating(Boolean(store));
|
|
62
64
|
}
|
|
63
65
|
}
|
|
66
|
+
/** Settles the answer another tab left in flight, and writes it back. */
|
|
67
|
+
const settleForeign = (0, react_1.useCallback)((threadId) => {
|
|
68
|
+
if (!mountedRef.current || session.storageThreadId !== threadId)
|
|
69
|
+
return;
|
|
70
|
+
if (session.isStreaming)
|
|
71
|
+
return;
|
|
72
|
+
session.dirty = true;
|
|
73
|
+
setMessages((prev) => (0, chatThreads_1.normalizeHydratedMessages)(prev));
|
|
74
|
+
}, [session, setMessages, mountedRef]);
|
|
64
75
|
/** Puts a stored conversation on screen and makes it the one this tab continues. */
|
|
65
76
|
const showStoredChat = (0, react_1.useCallback)((chat) => {
|
|
66
77
|
(0, chatSession_1.adoptStoredChat)(session, chat);
|
|
@@ -75,15 +86,18 @@ function useChatPersistence(params) {
|
|
|
75
86
|
return;
|
|
76
87
|
}
|
|
77
88
|
setMessages(chat.messages);
|
|
78
|
-
watchForeign(remainingMs, () =>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
watchForeign(remainingMs, () => settleForeign(chat.threadId));
|
|
90
|
+
}, [session, stopForeign, watchForeign, settleForeign, setMessages]);
|
|
91
|
+
/** Asks the tab streaming the answer on screen to stop it; settles it here if no tab does. */
|
|
92
|
+
const abortForeign = (0, react_1.useCallback)(() => {
|
|
93
|
+
const { current } = storeRef;
|
|
94
|
+
const threadId = session.storageThreadId;
|
|
95
|
+
const last = messagesRef.current[messagesRef.current.length - 1];
|
|
96
|
+
if (!canAbortAcrossTabs(current) || !session.foreignInFlight || !threadId || !last)
|
|
97
|
+
return;
|
|
98
|
+
current.requestAbort(threadId, last.id);
|
|
99
|
+
watchForeign(chatThreads_1.FOREIGN_ABORT_TIMEOUT_MS, () => settleForeign(threadId));
|
|
100
|
+
}, [session, storeRef, messagesRef, watchForeign, settleForeign]);
|
|
87
101
|
/** Writes the conversation as it is now; `settle` finishes an in-flight answer, `sync` skips awaiting. */
|
|
88
102
|
const persistNow = (0, react_1.useCallback)((mode) => {
|
|
89
103
|
const { current } = storeRef;
|
|
@@ -327,6 +341,15 @@ function useChatPersistence(params) {
|
|
|
327
341
|
settleAndPersist();
|
|
328
342
|
}, [detachTurn, settleAndPersist]);
|
|
329
343
|
(0, react_1.useEffect)(() => (0, backgroundTurns_1.subscribeBackgroundTurns)(refreshThreads), [refreshThreads]);
|
|
344
|
+
(0, react_1.useEffect)(() => {
|
|
345
|
+
if (!(store === null || store === void 0 ? void 0 : store.subscribeAbort))
|
|
346
|
+
return undefined;
|
|
347
|
+
return store.subscribeAbort(({ messageId }) => {
|
|
348
|
+
var _a;
|
|
349
|
+
if (((_a = turnRef.current) === null || _a === void 0 ? void 0 : _a.assistantId) === messageId)
|
|
350
|
+
abortTurn();
|
|
351
|
+
});
|
|
352
|
+
}, [store, turnRef, abortTurn]);
|
|
330
353
|
// Follow changes another tab made to the list or to the thread on screen.
|
|
331
354
|
const syncFromStore = (0, react_1.useCallback)(() => tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
332
355
|
var _a;
|
|
@@ -475,6 +498,8 @@ function useChatPersistence(params) {
|
|
|
475
498
|
threads,
|
|
476
499
|
activeThreadId,
|
|
477
500
|
foreignInFlight,
|
|
501
|
+
canAbortForeign: foreignInFlight && canAbortAcrossTabs(store),
|
|
502
|
+
abortForeign,
|
|
478
503
|
beginTurn,
|
|
479
504
|
screenOwner,
|
|
480
505
|
clearHistory,
|
|
@@ -44,6 +44,19 @@ function useAsaResults(options) {
|
|
|
44
44
|
(_a = turnRef.current) === null || _a === void 0 ? void 0 : _a.cancel();
|
|
45
45
|
turnRef.current = null;
|
|
46
46
|
}, []);
|
|
47
|
+
const abortOwnTurn = (0, react_1.useCallback)(() => {
|
|
48
|
+
var _a;
|
|
49
|
+
if (!session.isStreaming)
|
|
50
|
+
return;
|
|
51
|
+
const abortedId = (_a = turnRef.current) === null || _a === void 0 ? void 0 : _a.assistantId;
|
|
52
|
+
cancelStream();
|
|
53
|
+
session.isStreaming = false;
|
|
54
|
+
setIsStreaming(false);
|
|
55
|
+
if (abortedId)
|
|
56
|
+
setMessages((prev) => (0, chatThreads_1.settleCancelledReply)(prev, abortedId));
|
|
57
|
+
// With persistence on, the cancelled turn is stored as it stands.
|
|
58
|
+
session.dirty = true;
|
|
59
|
+
}, [session, cancelStream]);
|
|
47
60
|
const store = (0, useChatPersistence_1.default)({
|
|
48
61
|
store: persistence,
|
|
49
62
|
scope: persistenceScope,
|
|
@@ -56,8 +69,9 @@ function useAsaResults(options) {
|
|
|
56
69
|
setIsStreaming,
|
|
57
70
|
turnRef,
|
|
58
71
|
cancelStream,
|
|
72
|
+
abortTurn: abortOwnTurn,
|
|
59
73
|
});
|
|
60
|
-
const { beginTurn, screenOwner } = store;
|
|
74
|
+
const { beginTurn, screenOwner, abortForeign } = store;
|
|
61
75
|
(0, react_1.useEffect)(() => cancelStream, [cancelStream]);
|
|
62
76
|
const sendToCurrentThread = (0, react_1.useCallback)((text, source) => {
|
|
63
77
|
var _a, _b;
|
|
@@ -154,33 +168,20 @@ function useAsaResults(options) {
|
|
|
154
168
|
* No beacon is sent: there is no "aborted" ASA event, and reporting the load as
|
|
155
169
|
* finished would be false. An aborted turn therefore leaves an
|
|
156
170
|
* `assistant_result_load_started` with no matching finished event.
|
|
171
|
+
*
|
|
172
|
+
* An answer streaming in another tab is stopped by that tab, which stores the result.
|
|
157
173
|
*/
|
|
158
174
|
const abort = (0, react_1.useCallback)(() => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
session.isStreaming = false;
|
|
165
|
-
setIsStreaming(false);
|
|
166
|
-
if (abortedId) {
|
|
167
|
-
setMessages((prev) => prev.flatMap((msg) => {
|
|
168
|
-
var _a;
|
|
169
|
-
if (msg.id !== abortedId)
|
|
170
|
-
return [msg];
|
|
171
|
-
if (!msg.text && !((_a = msg.groups) === null || _a === void 0 ? void 0 : _a.length) && !msg.refinement)
|
|
172
|
-
return [];
|
|
173
|
-
return [Object.assign(Object.assign({}, msg), { status: 'done' })];
|
|
174
|
-
}));
|
|
175
|
-
}
|
|
176
|
-
// With persistence on, the cancelled turn is stored as it stands.
|
|
177
|
-
session.dirty = true;
|
|
178
|
-
}, [session, cancelStream]);
|
|
175
|
+
if (session.isStreaming)
|
|
176
|
+
abortOwnTurn();
|
|
177
|
+
else
|
|
178
|
+
abortForeign();
|
|
179
|
+
}, [session, abortOwnTurn, abortForeign]);
|
|
179
180
|
return {
|
|
180
181
|
messages,
|
|
181
182
|
sendMessage,
|
|
182
183
|
isStreaming: isStreaming || store.foreignInFlight,
|
|
183
|
-
canAbort: isStreaming,
|
|
184
|
+
canAbort: isStreaming || store.canAbortForeign,
|
|
184
185
|
abort,
|
|
185
186
|
clearHistory: store.clearHistory,
|
|
186
187
|
isHydrating: store.isHydrating,
|
package/lib/cjs/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ConstructorIOClient = exports.clearPersistedConversations = exports.normalizeItemToProduct = exports.useCioAsaContext = exports.useAsaTracking = exports.useAsaResults = exports.Button = exports.ResultsBlock = exports.Chat = exports.CioAsaProvider = exports.CioAsa = void 0;
|
|
6
|
+
exports.ConstructorIOClient = exports.renderMarkdown = exports.clearPersistedConversations = exports.normalizeItemToProduct = exports.useCioAsaContext = exports.useAsaTracking = exports.useAsaResults = exports.Button = exports.ResultsBlock = exports.Chat = exports.CioAsaProvider = exports.CioAsa = void 0;
|
|
7
7
|
// Components
|
|
8
8
|
var CioAsa_1 = require("./components/CioAsa/CioAsa");
|
|
9
9
|
Object.defineProperty(exports, "CioAsa", { enumerable: true, get: function () { return __importDefault(CioAsa_1).default; } });
|
|
@@ -27,6 +27,8 @@ var productNormalizer_1 = require("./utils/productNormalizer");
|
|
|
27
27
|
Object.defineProperty(exports, "normalizeItemToProduct", { enumerable: true, get: function () { return productNormalizer_1.normalizeItemToProduct; } });
|
|
28
28
|
var localStoragePersistence_1 = require("./utils/localStoragePersistence");
|
|
29
29
|
Object.defineProperty(exports, "clearPersistedConversations", { enumerable: true, get: function () { return localStoragePersistence_1.clearPersistedConversations; } });
|
|
30
|
+
var markdown_1 = require("./utils/markdown");
|
|
31
|
+
Object.defineProperty(exports, "renderMarkdown", { enumerable: true, get: function () { return markdown_1.renderMarkdown; } });
|
|
30
32
|
// Re-exported client value from the JS client (used to construct/type a cioClient)
|
|
31
33
|
var types_1 = require("./types");
|
|
32
34
|
Object.defineProperty(exports, "ConstructorIOClient", { enumerable: true, get: function () { return types_1.ConstructorIOClient; } });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.moveThreads = exports.foreignStreamRemainingMs = exports.isThreadStreaming = exports.findRekeyedThread = exports.saveThreadAndRetireStale = exports.mergeMessages = exports.compactMessages = exports.normalizeHydratedMessages = exports.isInFlight = exports.nextMessageCounter = exports.getThreadTitle = exports.isLocalThreadId = exports.getTabId = exports.createLocalThreadId = exports.randomId = exports.IN_FLIGHT_GRACE_MS = exports.DEFAULT_PERSISTENCE_TTL_MS = exports.DEFAULT_PERSISTENCE_KEY = exports.PERSISTED_CHAT_VERSION = void 0;
|
|
3
|
+
exports.moveThreads = exports.foreignStreamRemainingMs = exports.isThreadStreaming = exports.findRekeyedThread = exports.saveThreadAndRetireStale = exports.mergeMessages = exports.compactMessages = exports.settleCancelledReply = exports.normalizeHydratedMessages = exports.isInFlight = exports.nextMessageCounter = exports.getThreadTitle = exports.isLocalThreadId = exports.getTabId = exports.createLocalThreadId = exports.randomId = exports.FOREIGN_ABORT_TIMEOUT_MS = exports.IN_FLIGHT_GRACE_MS = exports.DEFAULT_PERSISTENCE_TTL_MS = exports.DEFAULT_PERSISTENCE_KEY = exports.PERSISTED_CHAT_VERSION = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
/** Stored record format; records with another version are ignored on read. */
|
|
6
6
|
exports.PERSISTED_CHAT_VERSION = 1;
|
|
@@ -10,6 +10,8 @@ exports.DEFAULT_PERSISTENCE_KEY = 'cio-asa:chat';
|
|
|
10
10
|
exports.DEFAULT_PERSISTENCE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
11
|
/** How long a stored answer written by another tab is still shown as "typing" before it is settled. */
|
|
12
12
|
exports.IN_FLIGHT_GRACE_MS = 60 * 1000;
|
|
13
|
+
/** How long to wait for the tab streaming an answer to store it stopped, before settling it here. */
|
|
14
|
+
exports.FOREIGN_ABORT_TIMEOUT_MS = 3 * 1000;
|
|
13
15
|
/** Marks client-side thread ids, used before the server has assigned one. Never sent to the agent. */
|
|
14
16
|
const LOCAL_THREAD_PREFIX = 'local-';
|
|
15
17
|
/** Thread titles are the first question, cut to this many characters. */
|
|
@@ -125,6 +127,18 @@ function normalizeHydratedMessages(messages) {
|
|
|
125
127
|
});
|
|
126
128
|
}
|
|
127
129
|
exports.normalizeHydratedMessages = normalizeHydratedMessages;
|
|
130
|
+
/** `messages` with the cancelled reply settled: `done` when it has content, dropped otherwise. */
|
|
131
|
+
function settleCancelledReply(messages, assistantId) {
|
|
132
|
+
return messages.flatMap((m) => {
|
|
133
|
+
var _a;
|
|
134
|
+
if (m.id !== assistantId)
|
|
135
|
+
return [m];
|
|
136
|
+
if (!m.text && !((_a = m.groups) === null || _a === void 0 ? void 0 : _a.length) && !m.refinement)
|
|
137
|
+
return [];
|
|
138
|
+
return [Object.assign(Object.assign({}, m), { status: 'done' })];
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
exports.settleCancelledReply = settleCancelledReply;
|
|
128
142
|
/** Messages as stored: raw items replaced by the cards `normalize` makes; a restored group keeps its cards. */
|
|
129
143
|
function compactMessages(messages, normalize) {
|
|
130
144
|
return messages.map((m) => {
|
|
@@ -75,22 +75,33 @@ const emptyStored = () => ({
|
|
|
75
75
|
threads: {},
|
|
76
76
|
deleted: {},
|
|
77
77
|
});
|
|
78
|
-
|
|
78
|
+
/** The record `raw` holds, or `null` when it is not one of this version. */
|
|
79
|
+
function readStored(raw) {
|
|
79
80
|
var _a;
|
|
80
|
-
if (!raw)
|
|
81
|
-
return emptyStored();
|
|
82
81
|
try {
|
|
83
82
|
const parsed = JSON.parse(raw);
|
|
84
83
|
if ((parsed === null || parsed === void 0 ? void 0 : parsed.version) !== chatThreads_1.PERSISTED_CHAT_VERSION || !parsed.threads)
|
|
85
|
-
return
|
|
84
|
+
return null;
|
|
86
85
|
const threads = Object.fromEntries(Object.entries(parsed.threads).filter(([, chat]) => (0, chatRecordValidation_1.default)(chat)));
|
|
87
86
|
const deleted = Object.fromEntries(Object.entries((_a = parsed.deleted) !== null && _a !== void 0 ? _a : {}).filter(([, at]) => typeof at === 'number'));
|
|
88
87
|
return { version: chatThreads_1.PERSISTED_CHAT_VERSION, threads, deleted };
|
|
89
88
|
}
|
|
90
89
|
catch (_b) {
|
|
91
|
-
return
|
|
90
|
+
return null;
|
|
92
91
|
}
|
|
93
92
|
}
|
|
93
|
+
function parseStored(raw) {
|
|
94
|
+
return (raw && readStored(raw)) || emptyStored();
|
|
95
|
+
}
|
|
96
|
+
function dropExpired(data, cutoff) {
|
|
97
|
+
var _a;
|
|
98
|
+
return {
|
|
99
|
+
version: chatThreads_1.PERSISTED_CHAT_VERSION,
|
|
100
|
+
threads: Object.fromEntries(Object.entries(data.threads).filter(([, chat]) => chat.updatedAt >= cutoff)),
|
|
101
|
+
deleted: Object.fromEntries(Object.entries((_a = data.deleted) !== null && _a !== void 0 ? _a : {}).filter(([, at]) => at >= cutoff)),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const entryCount = (data) => { var _a; return Object.keys(data.threads).length + Object.keys((_a = data.deleted) !== null && _a !== void 0 ? _a : {}).length; };
|
|
94
105
|
function readItem(storage, key) {
|
|
95
106
|
try {
|
|
96
107
|
return storage.getItem(key);
|
|
@@ -99,6 +110,44 @@ function readItem(storage, key) {
|
|
|
99
110
|
return null;
|
|
100
111
|
}
|
|
101
112
|
}
|
|
113
|
+
/** How often a write is redone when another tab changed the key between its read and its write. */
|
|
114
|
+
const MAX_COMMIT_ATTEMPTS = 3;
|
|
115
|
+
/** Drops expired threads and tombstones under every namespace of `key`, other shoppers' included. */
|
|
116
|
+
function sweepExpired(storage, key, ttlMs) {
|
|
117
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
118
|
+
const prefix = storageKeyFor(key);
|
|
119
|
+
let keys;
|
|
120
|
+
try {
|
|
121
|
+
keys = Array.from({ length: storage.length }, (_, i) => storage.key(i)).filter((k) => k === prefix || Boolean(k === null || k === void 0 ? void 0 : k.startsWith(`${prefix}:`)));
|
|
122
|
+
}
|
|
123
|
+
catch (_a) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
yield Promise.all(keys.map((k) => withStorageLock(k, () => {
|
|
127
|
+
for (let attempt = 0; attempt < MAX_COMMIT_ATTEMPTS; attempt += 1) {
|
|
128
|
+
const raw = readItem(storage, k);
|
|
129
|
+
const stored = raw === null ? null : readStored(raw);
|
|
130
|
+
if (!stored)
|
|
131
|
+
return;
|
|
132
|
+
const kept = dropExpired(stored, Date.now() - ttlMs);
|
|
133
|
+
if (entryCount(kept) === entryCount(stored))
|
|
134
|
+
return;
|
|
135
|
+
if (readItem(storage, k) === raw) {
|
|
136
|
+
try {
|
|
137
|
+
if (entryCount(kept) === 0)
|
|
138
|
+
storage.removeItem(k);
|
|
139
|
+
else
|
|
140
|
+
storage.setItem(k, JSON.stringify(kept));
|
|
141
|
+
}
|
|
142
|
+
catch (_a) {
|
|
143
|
+
/* storage unavailable */
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
102
151
|
/**
|
|
103
152
|
* Deletes every stored conversation of one shopper, or of the guest without `userId`; open tabs
|
|
104
153
|
* follow. Resolves once the deletion is recorded.
|
|
@@ -148,6 +197,23 @@ function clearPersistedConversations(options) {
|
|
|
148
197
|
});
|
|
149
198
|
}
|
|
150
199
|
exports.clearPersistedConversations = clearPersistedConversations;
|
|
200
|
+
/** The channel abort requests travel on between tabs, or `null` where none can be opened. */
|
|
201
|
+
function openAbortChannel(name, onRequest) {
|
|
202
|
+
if (typeof window === 'undefined' || typeof BroadcastChannel === 'undefined')
|
|
203
|
+
return null;
|
|
204
|
+
try {
|
|
205
|
+
const channel = new BroadcastChannel(name);
|
|
206
|
+
channel.onmessage = ({ data }) => {
|
|
207
|
+
if (typeof (data === null || data === void 0 ? void 0 : data.threadId) !== 'string' || typeof (data === null || data === void 0 ? void 0 : data.messageId) !== 'string')
|
|
208
|
+
return;
|
|
209
|
+
onRequest({ threadId: data.threadId, messageId: data.messageId });
|
|
210
|
+
};
|
|
211
|
+
return channel;
|
|
212
|
+
}
|
|
213
|
+
catch (_a) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
151
217
|
/** Conversation store on top of `localStorage`; one key per namespace, merged across tabs. */
|
|
152
218
|
function createLocalStoragePersistence(options = {}) {
|
|
153
219
|
const { key = chatThreads_1.DEFAULT_PERSISTENCE_KEY, namespace, ttlMs = chatThreads_1.DEFAULT_PERSISTENCE_TTL_MS, maxTurns = Infinity, maxThreads = Infinity, storageArea = 'local', storage: storageOption, } = options;
|
|
@@ -156,20 +222,24 @@ function createLocalStoragePersistence(options = {}) {
|
|
|
156
222
|
// One sync reads the same key several times; parse and validate the blob once per distinct value.
|
|
157
223
|
let lastParsed = null;
|
|
158
224
|
const parse = (raw) => {
|
|
159
|
-
var _a;
|
|
160
225
|
if (!raw)
|
|
161
226
|
return emptyStored();
|
|
162
227
|
if ((lastParsed === null || lastParsed === void 0 ? void 0 : lastParsed.raw) !== raw)
|
|
163
228
|
lastParsed = { raw, data: parseStored(raw) };
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
229
|
+
return dropExpired(lastParsed.data, Date.now() - ttlMs);
|
|
230
|
+
};
|
|
231
|
+
// Other shoppers' records expire only here: their own store may never run again on this device.
|
|
232
|
+
let swept = false;
|
|
233
|
+
const sweepOnce = () => {
|
|
234
|
+
if (swept)
|
|
235
|
+
return;
|
|
236
|
+
swept = true;
|
|
237
|
+
const local = resolveStorage(storageOption, 'local');
|
|
238
|
+
if (local)
|
|
239
|
+
sweepExpired(local, key, ttlMs).catch(() => { });
|
|
171
240
|
};
|
|
172
241
|
const read = () => {
|
|
242
|
+
sweepOnce();
|
|
173
243
|
const storage = resolveStorage(storageOption, storageArea);
|
|
174
244
|
return parse(storage ? readRaw(storage) : null);
|
|
175
245
|
};
|
|
@@ -191,7 +261,7 @@ function createLocalStoragePersistence(options = {}) {
|
|
|
191
261
|
/* ignore */
|
|
192
262
|
}
|
|
193
263
|
};
|
|
194
|
-
const isEmpty = (data) =>
|
|
264
|
+
const isEmpty = (data) => entryCount(data) === 0;
|
|
195
265
|
// Retries without tombstones, oldest first: the last thing to shed, see `write`.
|
|
196
266
|
const tryWriteShedding = (storage, data) => {
|
|
197
267
|
var _a, _b;
|
|
@@ -256,8 +326,8 @@ function createLocalStoragePersistence(options = {}) {
|
|
|
256
326
|
tryWriteShedding(storage, others);
|
|
257
327
|
};
|
|
258
328
|
// Redo the merge if another tab wrote between our read and this write.
|
|
259
|
-
const MAX_COMMIT_ATTEMPTS = 3;
|
|
260
329
|
const transact = (mutate, priorityThreadId) => {
|
|
330
|
+
sweepOnce();
|
|
261
331
|
const storage = resolveStorage(storageOption, storageArea);
|
|
262
332
|
if (!storage)
|
|
263
333
|
return;
|
|
@@ -305,12 +375,34 @@ function createLocalStoragePersistence(options = {}) {
|
|
|
305
375
|
}
|
|
306
376
|
return next;
|
|
307
377
|
};
|
|
378
|
+
const abortListeners = new Set();
|
|
379
|
+
// Only `localStorage` is shared between tabs; a guest's `sessionStorage` thread streams in this tab alone.
|
|
380
|
+
const channel = storageArea === 'local'
|
|
381
|
+
? openAbortChannel(`${storageKey}:abort`, (request) => abortListeners.forEach((listener) => listener(request)))
|
|
382
|
+
: null;
|
|
383
|
+
const abortMethods = channel
|
|
384
|
+
? {
|
|
385
|
+
requestAbort(threadId, messageId) {
|
|
386
|
+
try {
|
|
387
|
+
channel.postMessage({ threadId, messageId });
|
|
388
|
+
}
|
|
389
|
+
catch (_a) {
|
|
390
|
+
/* channel closed */
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
subscribeAbort(listener) {
|
|
394
|
+
abortListeners.add(listener);
|
|
395
|
+
return () => {
|
|
396
|
+
abortListeners.delete(listener);
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
}
|
|
400
|
+
: {};
|
|
308
401
|
const retire = (data, staleId) => {
|
|
309
402
|
const _a = data.threads, _b = staleId, retired = _a[_b], threads = tslib_1.__rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
|
|
310
403
|
return Object.assign(Object.assign({}, data), { threads, deleted: Object.assign(Object.assign({}, data.deleted), { [staleId]: Date.now() }) });
|
|
311
404
|
};
|
|
312
|
-
return {
|
|
313
|
-
listThreads() {
|
|
405
|
+
return Object.assign({ listThreads() {
|
|
314
406
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
315
407
|
const now = Date.now();
|
|
316
408
|
return sortedThreads(read()).map((chat) => ({
|
|
@@ -367,8 +459,7 @@ function createLocalStoragePersistence(options = {}) {
|
|
|
367
459
|
};
|
|
368
460
|
window.addEventListener('storage', onStorage);
|
|
369
461
|
return () => window.removeEventListener('storage', onStorage);
|
|
370
|
-
},
|
|
371
|
-
};
|
|
462
|
+
} }, abortMethods);
|
|
372
463
|
}
|
|
373
464
|
exports.createLocalStoragePersistence = createLocalStoragePersistence;
|
|
374
465
|
const sharedStores = new Map();
|