@constructor-io/constructorio-ui-asa 1.7.2 → 1.8.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 +21 -17
- package/dist/constructorio-ui-asa-bundled.js +17 -17
- package/lib/cjs/components/Chat/AiMessage.js +2 -3
- package/lib/cjs/components/Chat/Chat.js +38 -10
- package/lib/cjs/components/CioAsa/CioAsa.js +6 -2
- package/lib/cjs/components/CioAsaProvider/CioAsaProvider.js +44 -3
- package/lib/cjs/components/ResultsBlock/ResultsGroup.js +5 -5
- package/lib/cjs/hooks/agentStream.js +113 -0
- package/lib/cjs/hooks/asaStreamHandlers.js +2 -1
- package/lib/cjs/hooks/persistence/backgroundTurns.js +121 -0
- package/lib/cjs/hooks/persistence/useChatPersistence.js +485 -0
- package/lib/cjs/hooks/persistence/useForeignStream.js +34 -0
- package/lib/cjs/hooks/persistence/useThreadList.js +45 -0
- package/lib/cjs/hooks/persistence/useWriteQueue.js +17 -0
- package/lib/cjs/hooks/useAsaResults.js +93 -160
- package/lib/cjs/hooks/useAsaTracking.js +3 -3
- package/lib/cjs/hooks/useIsMounted.js +15 -0
- package/lib/cjs/hooks/useLatest.js +10 -0
- package/lib/cjs/hooks/useViewportTracking.js +4 -3
- package/lib/cjs/index.js +3 -1
- package/lib/cjs/utils/chatRecordValidation.js +54 -0
- package/lib/cjs/utils/chatSession.js +98 -0
- package/lib/cjs/utils/chatThreads.js +248 -0
- package/lib/cjs/utils/clientOptions.js +11 -0
- package/lib/cjs/utils/localStoragePersistence.js +391 -0
- package/lib/cjs/utils/translate.js +1 -0
- package/lib/cjs/version.js +1 -1
- package/lib/mjs/components/Chat/AiMessage.js +2 -3
- package/lib/mjs/components/Chat/Chat.js +36 -10
- package/lib/mjs/components/CioAsa/CioAsa.js +6 -2
- package/lib/mjs/components/CioAsaProvider/CioAsaProvider.js +44 -4
- package/lib/mjs/components/ResultsBlock/ResultsGroup.js +2 -1
- package/lib/mjs/hooks/agentStream.js +106 -0
- package/lib/mjs/hooks/asaStreamHandlers.js +1 -1
- package/lib/mjs/hooks/persistence/backgroundTurns.js +120 -0
- package/lib/mjs/hooks/persistence/useChatPersistence.js +474 -0
- package/lib/mjs/hooks/persistence/useForeignStream.js +31 -0
- package/lib/mjs/hooks/persistence/useThreadList.js +41 -0
- package/lib/mjs/hooks/persistence/useWriteQueue.js +14 -0
- package/lib/mjs/hooks/useAsaResults.js +87 -157
- package/lib/mjs/hooks/useAsaTracking.js +2 -1
- package/lib/mjs/hooks/useIsMounted.js +12 -0
- package/lib/mjs/hooks/useLatest.js +7 -0
- package/lib/mjs/hooks/useViewportTracking.js +3 -3
- package/lib/mjs/index.js +1 -0
- package/lib/mjs/utils/chatRecordValidation.js +51 -0
- package/lib/mjs/utils/chatSession.js +86 -0
- package/lib/mjs/utils/chatThreads.js +219 -0
- package/lib/mjs/utils/clientOptions.js +7 -0
- package/lib/mjs/utils/localStoragePersistence.js +373 -0
- package/lib/mjs/utils/translate.js +1 -0
- package/lib/mjs/version.js +1 -1
- package/lib/types/components/Chat/Chat.d.ts +8 -2
- package/lib/types/components/CioAsa/CioAsa.d.ts +1 -1
- package/lib/types/hooks/agentStream.d.ts +48 -0
- package/lib/types/hooks/asaStreamHandlers.d.ts +1 -0
- package/lib/types/hooks/persistence/backgroundTurns.d.ts +43 -0
- package/lib/types/hooks/persistence/useChatPersistence.d.ts +34 -0
- package/lib/types/hooks/persistence/useForeignStream.d.ts +9 -0
- package/lib/types/hooks/persistence/useThreadList.d.ts +9 -0
- package/lib/types/hooks/persistence/useWriteQueue.d.ts +5 -0
- package/lib/types/hooks/useAsaTracking.d.ts +2 -0
- package/lib/types/hooks/useIsMounted.d.ts +3 -0
- package/lib/types/hooks/useLatest.d.ts +3 -0
- package/lib/types/index.d.ts +2 -1
- package/lib/types/types.d.ts +134 -2
- package/lib/types/utils/chatRecordValidation.d.ts +2 -0
- package/lib/types/utils/chatSession.d.ts +45 -0
- package/lib/types/utils/chatThreads.d.ts +63 -0
- package/lib/types/utils/clientOptions.d.ts +5 -0
- package/lib/types/utils/localStoragePersistence.d.ts +26 -0
- package/lib/types/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
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;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
/** Stored record format; records with another version are ignored on read. */
|
|
6
|
+
exports.PERSISTED_CHAT_VERSION = 1;
|
|
7
|
+
/** Prefix of the `localStorage` key; the api key, domain and user id are appended to it. */
|
|
8
|
+
exports.DEFAULT_PERSISTENCE_KEY = 'cio-asa:chat';
|
|
9
|
+
/** One week, matching how long the agent itself remembers a thread. */
|
|
10
|
+
exports.DEFAULT_PERSISTENCE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
|
+
/** How long a stored answer written by another tab is still shown as "typing" before it is settled. */
|
|
12
|
+
exports.IN_FLIGHT_GRACE_MS = 60 * 1000;
|
|
13
|
+
/** Marks client-side thread ids, used before the server has assigned one. Never sent to the agent. */
|
|
14
|
+
const LOCAL_THREAD_PREFIX = 'local-';
|
|
15
|
+
/** Thread titles are the first question, cut to this many characters. */
|
|
16
|
+
const TITLE_MAX_LENGTH = 80;
|
|
17
|
+
let fallbackCounter = 0;
|
|
18
|
+
/** Random token for the ids that must not collide between tabs: threads, tabs and messages. */
|
|
19
|
+
function randomId() {
|
|
20
|
+
if (typeof crypto !== 'undefined') {
|
|
21
|
+
if (typeof crypto.randomUUID === 'function')
|
|
22
|
+
return crypto.randomUUID();
|
|
23
|
+
if (typeof crypto.getRandomValues === 'function') {
|
|
24
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
25
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
fallbackCounter += 1;
|
|
29
|
+
return `${Date.now().toString(36)}-${fallbackCounter.toString(36)}`;
|
|
30
|
+
}
|
|
31
|
+
exports.randomId = randomId;
|
|
32
|
+
/** Client-side thread id for a conversation the server has not named yet. */
|
|
33
|
+
function createLocalThreadId() {
|
|
34
|
+
return `${LOCAL_THREAD_PREFIX}${randomId()}`;
|
|
35
|
+
}
|
|
36
|
+
exports.createLocalThreadId = createLocalThreadId;
|
|
37
|
+
const TAB_ID_KEY = 'cio-asa:tab';
|
|
38
|
+
let tabId;
|
|
39
|
+
/** Whether this page load is a reload, as opposed to a new, duplicated or restored tab. */
|
|
40
|
+
function isReload() {
|
|
41
|
+
try {
|
|
42
|
+
const [entry] = performance.getEntriesByType('navigation');
|
|
43
|
+
return (entry === null || entry === void 0 ? void 0 : entry.type) === 'reload';
|
|
44
|
+
}
|
|
45
|
+
catch (_a) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Per-tab id: survives a reload, differs between tabs, `undefined` on the server. A duplicated tab
|
|
51
|
+
* copies `sessionStorage`, so the id is read back on a reload only, and is kept there only while
|
|
52
|
+
* the page is hidden.
|
|
53
|
+
*/
|
|
54
|
+
function getTabId() {
|
|
55
|
+
if (tabId)
|
|
56
|
+
return tabId;
|
|
57
|
+
if (typeof window === 'undefined')
|
|
58
|
+
return undefined;
|
|
59
|
+
const id = randomId();
|
|
60
|
+
tabId = id;
|
|
61
|
+
try {
|
|
62
|
+
const { sessionStorage } = window;
|
|
63
|
+
tabId = (isReload() && sessionStorage.getItem(TAB_ID_KEY)) || id;
|
|
64
|
+
sessionStorage.removeItem(TAB_ID_KEY);
|
|
65
|
+
const claimed = tabId;
|
|
66
|
+
window.addEventListener('pagehide', () => {
|
|
67
|
+
try {
|
|
68
|
+
sessionStorage.setItem(TAB_ID_KEY, claimed);
|
|
69
|
+
}
|
|
70
|
+
catch (_a) {
|
|
71
|
+
/* ignore */
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
window.addEventListener('pageshow', (event) => {
|
|
75
|
+
if (!event.persisted)
|
|
76
|
+
return;
|
|
77
|
+
try {
|
|
78
|
+
sessionStorage.removeItem(TAB_ID_KEY);
|
|
79
|
+
}
|
|
80
|
+
catch (_a) {
|
|
81
|
+
/* ignore */
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (_a) {
|
|
86
|
+
/* sessionStorage blocked: the id lasts for this page only */
|
|
87
|
+
}
|
|
88
|
+
return tabId;
|
|
89
|
+
}
|
|
90
|
+
exports.getTabId = getTabId;
|
|
91
|
+
/** Whether `threadId` was made by `createLocalThreadId` rather than by the server. */
|
|
92
|
+
function isLocalThreadId(threadId) {
|
|
93
|
+
return typeof threadId === 'string' && threadId.startsWith(LOCAL_THREAD_PREFIX);
|
|
94
|
+
}
|
|
95
|
+
exports.isLocalThreadId = isLocalThreadId;
|
|
96
|
+
/** Title for a thread list: the first user message, truncated with an ellipsis. */
|
|
97
|
+
function getThreadTitle(messages) {
|
|
98
|
+
var _a, _b;
|
|
99
|
+
const first = (_b = (_a = messages.find((m) => m.role === 'user')) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : '';
|
|
100
|
+
return first.length > TITLE_MAX_LENGTH ? `${first.slice(0, TITLE_MAX_LENGTH - 1)}…` : first;
|
|
101
|
+
}
|
|
102
|
+
exports.getThreadTitle = getThreadTitle;
|
|
103
|
+
/** Counter to continue message ids from after a restore, so new ids never collide with restored ones. */
|
|
104
|
+
function nextMessageCounter(messages) {
|
|
105
|
+
return messages.reduce((max, m) => {
|
|
106
|
+
const match = /^msg-(\d+)-/.exec(m.id);
|
|
107
|
+
return match ? Math.max(max, Number(match[1])) : max;
|
|
108
|
+
}, messages.length);
|
|
109
|
+
}
|
|
110
|
+
exports.nextMessageCounter = nextMessageCounter;
|
|
111
|
+
/** Whether the latest answer in `messages` has not finished streaming. */
|
|
112
|
+
function isInFlight(messages) {
|
|
113
|
+
const last = messages[messages.length - 1];
|
|
114
|
+
return (last === null || last === void 0 ? void 0 : last.status) === 'loading' || (last === null || last === void 0 ? void 0 : last.status) === 'streaming';
|
|
115
|
+
}
|
|
116
|
+
exports.isInFlight = isInFlight;
|
|
117
|
+
/** Settles answers stored mid-stream: `done` when they have content, an interrupted `error` otherwise. */
|
|
118
|
+
function normalizeHydratedMessages(messages) {
|
|
119
|
+
return messages.map((m) => {
|
|
120
|
+
var _a;
|
|
121
|
+
if (m.status !== 'loading' && m.status !== 'streaming')
|
|
122
|
+
return m;
|
|
123
|
+
const hasContent = Boolean(m.text) || Boolean((_a = m.groups) === null || _a === void 0 ? void 0 : _a.length) || Boolean(m.refinement);
|
|
124
|
+
return hasContent ? Object.assign(Object.assign({}, m), { status: 'done' }) : Object.assign(Object.assign({}, m), { status: 'error', interrupted: true });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
exports.normalizeHydratedMessages = normalizeHydratedMessages;
|
|
128
|
+
/** Messages as stored: raw items replaced by the cards `normalize` makes; a restored group keeps its cards. */
|
|
129
|
+
function compactMessages(messages, normalize) {
|
|
130
|
+
return messages.map((m) => {
|
|
131
|
+
var _a;
|
|
132
|
+
return ((_a = m.groups) === null || _a === void 0 ? void 0 : _a.length)
|
|
133
|
+
? Object.assign(Object.assign({}, m), { groups: m.groups.map((g) => {
|
|
134
|
+
var _a;
|
|
135
|
+
return (Object.assign(Object.assign({}, g), { searchResults: [], products: (_a = g.products) !== null && _a !== void 0 ? _a : g.searchResults.map(normalize) }));
|
|
136
|
+
}) }) : m;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
exports.compactMessages = compactMessages;
|
|
140
|
+
const isUnsettled = (m) => m.status === 'loading' || m.status === 'streaming';
|
|
141
|
+
/**
|
|
142
|
+
* Both lists in stored order; a shared message takes the incoming version unless `preferStored`.
|
|
143
|
+
* A settled incoming version still replaces a stored one mid-stream: it is the later one by definition.
|
|
144
|
+
*/
|
|
145
|
+
function mergeMessages(stored, incoming, preferStored = false) {
|
|
146
|
+
const incomingById = new Map(incoming.map((m) => [m.id, m]));
|
|
147
|
+
const storedIds = new Set(stored.map((m) => m.id));
|
|
148
|
+
return [
|
|
149
|
+
...stored.map((m) => {
|
|
150
|
+
const update = incomingById.get(m.id);
|
|
151
|
+
if (!update)
|
|
152
|
+
return m;
|
|
153
|
+
if (!preferStored)
|
|
154
|
+
return update;
|
|
155
|
+
return isUnsettled(m) && !isUnsettled(update) ? update : m;
|
|
156
|
+
}),
|
|
157
|
+
...incoming.filter((m) => !storedIds.has(m.id)),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
exports.mergeMessages = mergeMessages;
|
|
161
|
+
/**
|
|
162
|
+
* Saves `snapshot`, then deletes `staleIds` once the new record is read back. Stores may fail
|
|
163
|
+
* without throwing, so both steps are checked by reading: `saved` is whether the snapshot landed,
|
|
164
|
+
* `leftover` the stale ids still stored.
|
|
165
|
+
*/
|
|
166
|
+
function saveThreadAndRetireStale(store, snapshot, staleIds) {
|
|
167
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
168
|
+
try {
|
|
169
|
+
yield store.saveThread(snapshot);
|
|
170
|
+
}
|
|
171
|
+
catch (_a) {
|
|
172
|
+
return { saved: false, leftover: staleIds };
|
|
173
|
+
}
|
|
174
|
+
const persisted = yield store.getThread(snapshot.threadId).catch(() => null);
|
|
175
|
+
if (!persisted || persisted.updatedAt < snapshot.updatedAt) {
|
|
176
|
+
return { saved: false, leftover: staleIds };
|
|
177
|
+
}
|
|
178
|
+
const retired = yield Promise.all(staleIds.map((id) => tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
179
|
+
yield store.deleteThread(id).catch(() => { });
|
|
180
|
+
return (yield store.getThread(id).catch(() => null)) === null;
|
|
181
|
+
})));
|
|
182
|
+
return { saved: true, leftover: staleIds.filter((_, i) => !retired[i]) };
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
exports.saveThreadAndRetireStale = saveThreadAndRetireStale;
|
|
186
|
+
/** Finds the server-keyed record another tab re-keyed a local thread into, by its first message. */
|
|
187
|
+
function findRekeyedThread(store, firstMessageId) {
|
|
188
|
+
var _a;
|
|
189
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
190
|
+
if (!firstMessageId)
|
|
191
|
+
return null;
|
|
192
|
+
const list = yield store.listThreads();
|
|
193
|
+
const candidates = list.filter((t) => !isLocalThreadId(t.threadId));
|
|
194
|
+
const loaded = yield Promise.all(candidates.map((t) => store.getThread(t.threadId)));
|
|
195
|
+
return (_a = loaded.find((c) => { var _a; return ((_a = c === null || c === void 0 ? void 0 : c.messages[0]) === null || _a === void 0 ? void 0 : _a.id) === firstMessageId; })) !== null && _a !== void 0 ? _a : null;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
exports.findRekeyedThread = findRekeyedThread;
|
|
199
|
+
/** Whether a stored thread still reads as streaming: this tab's own always, another tab's only within the grace. */
|
|
200
|
+
function isThreadStreaming(chat, now = Date.now()) {
|
|
201
|
+
if (!isInFlight(chat.messages))
|
|
202
|
+
return false;
|
|
203
|
+
if (chat.owner !== undefined && chat.owner === getTabId())
|
|
204
|
+
return true;
|
|
205
|
+
return now - chat.updatedAt < exports.IN_FLIGHT_GRACE_MS;
|
|
206
|
+
}
|
|
207
|
+
exports.isThreadStreaming = isThreadStreaming;
|
|
208
|
+
/** Ms left to show a stored answer as streaming in another tab; `0` when it should be settled. */
|
|
209
|
+
function foreignStreamRemainingMs(chat, now = Date.now()) {
|
|
210
|
+
if (!isInFlight(chat.messages))
|
|
211
|
+
return 0;
|
|
212
|
+
if (chat.owner !== undefined && chat.owner === getTabId())
|
|
213
|
+
return 0;
|
|
214
|
+
return Math.max(0, exports.IN_FLIGHT_GRACE_MS - (now - chat.updatedAt));
|
|
215
|
+
}
|
|
216
|
+
exports.foreignStreamRemainingMs = foreignStreamRemainingMs;
|
|
217
|
+
/**
|
|
218
|
+
* Moves every thread of `from` into `to`; copies of the chat on screen are not copied, its owner
|
|
219
|
+
* saves it. A source record is deleted only once `to` holds it after every copy was made, so one
|
|
220
|
+
* copy evicting another, or a save that did not land, leaves the source in place.
|
|
221
|
+
*/
|
|
222
|
+
function moveThreads(from, to, onScreen = {}) {
|
|
223
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
224
|
+
const { threadIds = [], firstMessageId } = onScreen;
|
|
225
|
+
const summaries = yield from.listThreads();
|
|
226
|
+
const chats = (yield Promise.all(summaries.map((t) => from.getThread(t.threadId)))).filter((chat) => chat !== null);
|
|
227
|
+
const isShown = (chat) => {
|
|
228
|
+
var _a;
|
|
229
|
+
return threadIds.includes(chat.threadId) ||
|
|
230
|
+
(firstMessageId !== undefined && ((_a = chat.messages[0]) === null || _a === void 0 ? void 0 : _a.id) === firstMessageId);
|
|
231
|
+
};
|
|
232
|
+
const toCopy = chats.filter((chat) => !isShown(chat));
|
|
233
|
+
yield Promise.all(toCopy.map((chat) => to.saveThread(chat)));
|
|
234
|
+
const targetList = yield to.listThreads();
|
|
235
|
+
const target = new Map((yield Promise.all(targetList.map((t) => to.getThread(t.threadId))))
|
|
236
|
+
.filter((chat) => chat !== null)
|
|
237
|
+
.map((chat) => [chat.threadId, chat]));
|
|
238
|
+
const shownSaved = Array.from(target.values()).some(isShown);
|
|
239
|
+
const moved = chats.filter((chat) => {
|
|
240
|
+
if (isShown(chat))
|
|
241
|
+
return shownSaved;
|
|
242
|
+
const copy = target.get(chat.threadId);
|
|
243
|
+
return copy !== undefined && copy.updatedAt >= chat.updatedAt;
|
|
244
|
+
});
|
|
245
|
+
yield Promise.all(moved.map((chat) => from.deleteThread(chat.threadId)));
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
exports.moveThreads = moveThreads;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readClientOptions = void 0;
|
|
4
|
+
/** `options` is private in the client's typings; read it here only, so a client bump fails a test, not storage keys. */
|
|
5
|
+
function readClientOptions(client) {
|
|
6
|
+
const { options } = (client !== null && client !== void 0 ? client : {});
|
|
7
|
+
if (!options || typeof options !== 'object')
|
|
8
|
+
return undefined;
|
|
9
|
+
return options;
|
|
10
|
+
}
|
|
11
|
+
exports.readClientOptions = readClientOptions;
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sharedLocalStoragePersistence = exports.createLocalStoragePersistence = exports.clearPersistedConversations = exports.persistenceNamespace = exports.storageAreaFor = exports.shopperId = exports.isGuest = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const chatThreads_1 = require("./chatThreads");
|
|
6
|
+
const chatRecordValidation_1 = tslib_1.__importDefault(require("./chatRecordValidation"));
|
|
7
|
+
/** Keeps the last `maxTurns` user/assistant pairs, always starting on a user message. */
|
|
8
|
+
function trimToTurns(messages, maxTurns) {
|
|
9
|
+
if (maxTurns <= 0)
|
|
10
|
+
return [];
|
|
11
|
+
let trimmed = Number.isFinite(maxTurns) ? messages.slice(-maxTurns * 2) : messages;
|
|
12
|
+
while (trimmed.length && trimmed[0].role !== 'user')
|
|
13
|
+
trimmed = trimmed.slice(1);
|
|
14
|
+
return trimmed;
|
|
15
|
+
}
|
|
16
|
+
// Web Locks where available; elsewhere the cycle runs synchronously.
|
|
17
|
+
function withStorageLock(name, fn) {
|
|
18
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
19
|
+
const locks = typeof navigator !== 'undefined' ? navigator.locks : undefined;
|
|
20
|
+
if (!(locks === null || locks === void 0 ? void 0 : locks.request)) {
|
|
21
|
+
fn();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
let ran = false;
|
|
25
|
+
try {
|
|
26
|
+
yield locks.request(name, () => tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
ran = true;
|
|
28
|
+
fn();
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
catch (_a) {
|
|
32
|
+
if (!ran)
|
|
33
|
+
fn();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** The storage to use, or `null` when none is available (server, or access blocked). */
|
|
38
|
+
function resolveStorage(storage, area) {
|
|
39
|
+
if (storage)
|
|
40
|
+
return storage;
|
|
41
|
+
try {
|
|
42
|
+
if (typeof window === 'undefined')
|
|
43
|
+
return null;
|
|
44
|
+
return area === 'session' ? window.sessionStorage : window.localStorage;
|
|
45
|
+
}
|
|
46
|
+
catch (_a) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** No user id, or an empty one: the conversation belongs to a guest. */
|
|
51
|
+
const isGuest = (userId) => userId == null || userId === '';
|
|
52
|
+
exports.isGuest = isGuest;
|
|
53
|
+
/** The id the client and storage use: `undefined` for a guest (no id, `null` or empty). */
|
|
54
|
+
const shopperId = (userId) => (0, exports.isGuest)(userId) ? undefined : String(userId);
|
|
55
|
+
exports.shopperId = shopperId;
|
|
56
|
+
/** Guests live in `sessionStorage`, so their history ends with the tab; shoppers keep theirs in `localStorage`. */
|
|
57
|
+
function storageAreaFor(userId) {
|
|
58
|
+
return (0, exports.isGuest)(userId) ? 'session' : 'local';
|
|
59
|
+
}
|
|
60
|
+
exports.storageAreaFor = storageAreaFor;
|
|
61
|
+
/** Namespace the provider stores under: api key, domain and, for a signed-in shopper, the user id. */
|
|
62
|
+
function persistenceNamespace(parts) {
|
|
63
|
+
var _a;
|
|
64
|
+
return [parts.apiKey, (_a = parts.domain) !== null && _a !== void 0 ? _a : 'default', (0, exports.shopperId)(parts.userId)]
|
|
65
|
+
.filter((part) => part !== undefined)
|
|
66
|
+
.map(encodeURIComponent)
|
|
67
|
+
.join(':');
|
|
68
|
+
}
|
|
69
|
+
exports.persistenceNamespace = persistenceNamespace;
|
|
70
|
+
function storageKeyFor(key, namespace) {
|
|
71
|
+
return [key, `v${chatThreads_1.PERSISTED_CHAT_VERSION}`, namespace].filter(Boolean).join(':');
|
|
72
|
+
}
|
|
73
|
+
const emptyStored = () => ({
|
|
74
|
+
version: chatThreads_1.PERSISTED_CHAT_VERSION,
|
|
75
|
+
threads: {},
|
|
76
|
+
deleted: {},
|
|
77
|
+
});
|
|
78
|
+
function parseStored(raw) {
|
|
79
|
+
var _a;
|
|
80
|
+
if (!raw)
|
|
81
|
+
return emptyStored();
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(raw);
|
|
84
|
+
if ((parsed === null || parsed === void 0 ? void 0 : parsed.version) !== chatThreads_1.PERSISTED_CHAT_VERSION || !parsed.threads)
|
|
85
|
+
return emptyStored();
|
|
86
|
+
const threads = Object.fromEntries(Object.entries(parsed.threads).filter(([, chat]) => (0, chatRecordValidation_1.default)(chat)));
|
|
87
|
+
const deleted = Object.fromEntries(Object.entries((_a = parsed.deleted) !== null && _a !== void 0 ? _a : {}).filter(([, at]) => typeof at === 'number'));
|
|
88
|
+
return { version: chatThreads_1.PERSISTED_CHAT_VERSION, threads, deleted };
|
|
89
|
+
}
|
|
90
|
+
catch (_b) {
|
|
91
|
+
return emptyStored();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function readItem(storage, key) {
|
|
95
|
+
try {
|
|
96
|
+
return storage.getItem(key);
|
|
97
|
+
}
|
|
98
|
+
catch (_a) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Deletes every stored conversation of one shopper, or of the guest without `userId`; open tabs
|
|
104
|
+
* follow. Resolves once the deletion is recorded.
|
|
105
|
+
*/
|
|
106
|
+
function clearPersistedConversations(options) {
|
|
107
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
108
|
+
const { apiKey, domain = 'chatbot', userId, storage: storageOption } = options;
|
|
109
|
+
const storage = resolveStorage(storageOption, storageAreaFor(userId));
|
|
110
|
+
if (!storage)
|
|
111
|
+
return;
|
|
112
|
+
const key = storageKeyFor(chatThreads_1.DEFAULT_PERSISTENCE_KEY, persistenceNamespace({ apiKey, domain, userId }));
|
|
113
|
+
let cleared = false;
|
|
114
|
+
// Under the store's lock, and as tombstones rather than a bare removal: a save still queued in
|
|
115
|
+
// some tab, holding a copy of a thread, must find it deleted instead of writing it back.
|
|
116
|
+
yield withStorageLock(key, () => {
|
|
117
|
+
var _a;
|
|
118
|
+
try {
|
|
119
|
+
const raw = readItem(storage, key);
|
|
120
|
+
if (raw === null) {
|
|
121
|
+
cleared = true;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const current = parseStored(raw);
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
const deleted = Object.keys(current.threads).reduce((all, threadId) => (Object.assign(Object.assign({}, all), { [threadId]: now })), (_a = current.deleted) !== null && _a !== void 0 ? _a : {});
|
|
127
|
+
if (Object.keys(deleted).length === 0)
|
|
128
|
+
storage.removeItem(key);
|
|
129
|
+
else
|
|
130
|
+
storage.setItem(key, JSON.stringify(Object.assign(Object.assign({}, emptyStored()), { deleted })));
|
|
131
|
+
cleared = true;
|
|
132
|
+
}
|
|
133
|
+
catch (_b) {
|
|
134
|
+
/* storage unavailable */
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
if (!cleared)
|
|
138
|
+
return;
|
|
139
|
+
// Other tabs get a native storage event; this tab does not, so dispatch one for a mounted chat.
|
|
140
|
+
if (typeof window === 'undefined' || typeof StorageEvent === 'undefined')
|
|
141
|
+
return;
|
|
142
|
+
try {
|
|
143
|
+
window.dispatchEvent(new StorageEvent('storage', { key, storageArea: storage }));
|
|
144
|
+
}
|
|
145
|
+
catch (_a) {
|
|
146
|
+
/* a non-Storage `storage` option cannot be attached to the event */
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
exports.clearPersistedConversations = clearPersistedConversations;
|
|
151
|
+
/** Conversation store on top of `localStorage`; one key per namespace, merged across tabs. */
|
|
152
|
+
function createLocalStoragePersistence(options = {}) {
|
|
153
|
+
const { key = chatThreads_1.DEFAULT_PERSISTENCE_KEY, namespace, ttlMs = chatThreads_1.DEFAULT_PERSISTENCE_TTL_MS, maxTurns = Infinity, maxThreads = Infinity, storageArea = 'local', storage: storageOption, } = options;
|
|
154
|
+
const storageKey = storageKeyFor(key, namespace);
|
|
155
|
+
const readRaw = (storage) => readItem(storage, storageKey);
|
|
156
|
+
// One sync reads the same key several times; parse and validate the blob once per distinct value.
|
|
157
|
+
let lastParsed = null;
|
|
158
|
+
const parse = (raw) => {
|
|
159
|
+
var _a;
|
|
160
|
+
if (!raw)
|
|
161
|
+
return emptyStored();
|
|
162
|
+
if ((lastParsed === null || lastParsed === void 0 ? void 0 : lastParsed.raw) !== raw)
|
|
163
|
+
lastParsed = { raw, data: parseStored(raw) };
|
|
164
|
+
const cutoff = Date.now() - ttlMs;
|
|
165
|
+
const { data } = lastParsed;
|
|
166
|
+
return {
|
|
167
|
+
version: chatThreads_1.PERSISTED_CHAT_VERSION,
|
|
168
|
+
threads: Object.fromEntries(Object.entries(data.threads).filter(([, chat]) => chat.updatedAt >= cutoff)),
|
|
169
|
+
deleted: Object.fromEntries(Object.entries((_a = data.deleted) !== null && _a !== void 0 ? _a : {}).filter(([, at]) => at >= cutoff)),
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
const read = () => {
|
|
173
|
+
const storage = resolveStorage(storageOption, storageArea);
|
|
174
|
+
return parse(storage ? readRaw(storage) : null);
|
|
175
|
+
};
|
|
176
|
+
const sortedThreads = (data) => Object.values(data.threads).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
177
|
+
const tryWrite = (storage, data) => {
|
|
178
|
+
try {
|
|
179
|
+
storage.setItem(storageKey, JSON.stringify(data));
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
catch (_a) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const remove = (storage) => {
|
|
187
|
+
try {
|
|
188
|
+
storage.removeItem(storageKey);
|
|
189
|
+
}
|
|
190
|
+
catch (_a) {
|
|
191
|
+
/* ignore */
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const isEmpty = (data) => { var _a; return Object.keys(data.threads).length === 0 && Object.keys((_a = data.deleted) !== null && _a !== void 0 ? _a : {}).length === 0; };
|
|
195
|
+
// Retries without tombstones, oldest first: the last thing to shed, see `write`.
|
|
196
|
+
const tryWriteShedding = (storage, data) => {
|
|
197
|
+
var _a, _b;
|
|
198
|
+
if (tryWrite(storage, data))
|
|
199
|
+
return true;
|
|
200
|
+
const oldestFirst = Object.entries((_a = data.deleted) !== null && _a !== void 0 ? _a : {})
|
|
201
|
+
.sort((a, b) => a[1] - b[1])
|
|
202
|
+
.map(([id]) => id);
|
|
203
|
+
let next = data;
|
|
204
|
+
for (let i = 0; i < oldestFirst.length; i += 1) {
|
|
205
|
+
const _c = (_b = next.deleted) !== null && _b !== void 0 ? _b : {}, _d = oldestFirst[i], shed = _c[_d], deleted = tslib_1.__rest(_c, [typeof _d === "symbol" ? _d : _d + ""]);
|
|
206
|
+
next = Object.assign(Object.assign({}, next), { deleted });
|
|
207
|
+
if (tryWrite(storage, next))
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
};
|
|
212
|
+
// A tombstone is the only thing stopping another tab from writing a deleted thread back, while an
|
|
213
|
+
// evicted thread is written back by the tab holding it. So when storage is full, shed other
|
|
214
|
+
// threads oldest first, then the saved thread's oldest turns, and only then tombstones.
|
|
215
|
+
const write = (storage, data, priorityThreadId) => {
|
|
216
|
+
if (isEmpty(data)) {
|
|
217
|
+
remove(storage);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (tryWrite(storage, data))
|
|
221
|
+
return;
|
|
222
|
+
const priority = priorityThreadId ? data.threads[priorityThreadId] : undefined;
|
|
223
|
+
const oldestFirst = sortedThreads(data)
|
|
224
|
+
.filter((chat) => chat.threadId !== (priority === null || priority === void 0 ? void 0 : priority.threadId))
|
|
225
|
+
.reverse();
|
|
226
|
+
let next = data;
|
|
227
|
+
for (let i = 0; i < oldestFirst.length; i += 1) {
|
|
228
|
+
const _a = next.threads, _b = oldestFirst[i].threadId, evicted = _a[_b], threads = tslib_1.__rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
|
|
229
|
+
next = Object.assign(Object.assign({}, next), { threads });
|
|
230
|
+
if (tryWrite(storage, next))
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (!priority) {
|
|
234
|
+
// Not even the tombstones fit without the threads; keep the threads rather than wipe them.
|
|
235
|
+
if (Object.keys(data.threads).length === 0)
|
|
236
|
+
remove(storage);
|
|
237
|
+
else
|
|
238
|
+
tryWriteShedding(storage, data);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
let chat = priority;
|
|
242
|
+
while (chat.messages.length > 2) {
|
|
243
|
+
chat = Object.assign(Object.assign({}, chat), { messages: trimToTurns(chat.messages.slice(2), maxTurns) });
|
|
244
|
+
next = Object.assign(Object.assign({}, next), { threads: Object.assign(Object.assign({}, next.threads), { [chat.threadId]: chat }) });
|
|
245
|
+
if (tryWrite(storage, next))
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (tryWriteShedding(storage, next))
|
|
249
|
+
return;
|
|
250
|
+
// Nothing fits: drop the stored copy too, so no in-flight record is left behind.
|
|
251
|
+
const _c = data.threads, _d = priority.threadId, dropped = _c[_d], rest = tslib_1.__rest(_c, [typeof _d === "symbol" ? _d : _d + ""]);
|
|
252
|
+
const others = Object.assign(Object.assign({}, data), { threads: rest });
|
|
253
|
+
if (isEmpty(others))
|
|
254
|
+
remove(storage);
|
|
255
|
+
else
|
|
256
|
+
tryWriteShedding(storage, others);
|
|
257
|
+
};
|
|
258
|
+
// Redo the merge if another tab wrote between our read and this write.
|
|
259
|
+
const MAX_COMMIT_ATTEMPTS = 3;
|
|
260
|
+
const transact = (mutate, priorityThreadId) => {
|
|
261
|
+
const storage = resolveStorage(storageOption, storageArea);
|
|
262
|
+
if (!storage)
|
|
263
|
+
return;
|
|
264
|
+
for (let attempt = 0; attempt < MAX_COMMIT_ATTEMPTS; attempt += 1) {
|
|
265
|
+
const raw = readRaw(storage);
|
|
266
|
+
const next = mutate(parse(raw));
|
|
267
|
+
if (!next)
|
|
268
|
+
return;
|
|
269
|
+
if (readRaw(storage) === raw) {
|
|
270
|
+
write(storage, next, priorityThreadId);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// The key kept changing under us: skip rather than overwrite another tab's turns. Without
|
|
275
|
+
// Web Locks this check is best effort; a write between the compare and the set can still win.
|
|
276
|
+
};
|
|
277
|
+
/** The record to store once `chat` is merged in, or `null` to leave storage alone. */
|
|
278
|
+
const mergeThread = (current, chat) => {
|
|
279
|
+
var _a, _b, _c, _d;
|
|
280
|
+
const now = Date.now();
|
|
281
|
+
const createdAt = (_a = chat.createdAt) !== null && _a !== void 0 ? _a : now;
|
|
282
|
+
const deletedAt = (_b = current.deleted) === null || _b === void 0 ? void 0 : _b[chat.threadId];
|
|
283
|
+
if (deletedAt !== undefined && deletedAt >= createdAt)
|
|
284
|
+
return null;
|
|
285
|
+
const stored = current.threads[chat.threadId];
|
|
286
|
+
const incomingAt = (_c = chat.updatedAt) !== null && _c !== void 0 ? _c : now;
|
|
287
|
+
// An older snapshot may add turns but must not regress ones already settled.
|
|
288
|
+
const stale = Boolean(stored) && incomingAt < stored.updatedAt;
|
|
289
|
+
// Merging keeps turns other tabs added. The tab that wrote the stored record is instead
|
|
290
|
+
// writing a newer full view of it, and may have dropped a message (an aborted reply), so
|
|
291
|
+
// merging its own older copy back would resurrect what it just removed.
|
|
292
|
+
const ownsStored = (stored === null || stored === void 0 ? void 0 : stored.owner) !== undefined && chat.owner !== undefined && stored.owner === chat.owner;
|
|
293
|
+
const replace = !stored || (ownsStored && !stale);
|
|
294
|
+
const messages = replace ? chat.messages : (0, chatThreads_1.mergeMessages)(stored.messages, chat.messages, stale);
|
|
295
|
+
const base = stale ? stored : chat;
|
|
296
|
+
// A record holding turns its owner has not seen is nobody's full view, so the next save merges too.
|
|
297
|
+
const absorbed = !replace && messages.length > base.messages.length;
|
|
298
|
+
const merged = Object.assign(Object.assign({}, base), { owner: absorbed ? undefined : base.owner, version: chatThreads_1.PERSISTED_CHAT_VERSION, threadId: chat.threadId, messages: trimToTurns(messages, maxTurns), createdAt, updatedAt: Math.max(incomingAt, (_d = stored === null || stored === void 0 ? void 0 : stored.updatedAt) !== null && _d !== void 0 ? _d : 0) });
|
|
299
|
+
let next = Object.assign(Object.assign({}, current), { threads: Object.assign(Object.assign({}, current.threads), { [chat.threadId]: merged }) });
|
|
300
|
+
if (Number.isFinite(maxThreads)) {
|
|
301
|
+
const kept = sortedThreads(next).slice(0, Math.max(0, maxThreads));
|
|
302
|
+
next = Object.assign(Object.assign({}, next), { threads: Object.fromEntries(kept.map((t) => [t.threadId, t])) });
|
|
303
|
+
if (!next.threads[chat.threadId])
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return next;
|
|
307
|
+
};
|
|
308
|
+
const retire = (data, staleId) => {
|
|
309
|
+
const _a = data.threads, _b = staleId, retired = _a[_b], threads = tslib_1.__rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
|
|
310
|
+
return Object.assign(Object.assign({}, data), { threads, deleted: Object.assign(Object.assign({}, data.deleted), { [staleId]: Date.now() }) });
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
listThreads() {
|
|
314
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
return sortedThreads(read()).map((chat) => ({
|
|
317
|
+
threadId: chat.threadId,
|
|
318
|
+
title: (0, chatThreads_1.getThreadTitle)(chat.messages),
|
|
319
|
+
createdAt: chat.createdAt,
|
|
320
|
+
updatedAt: chat.updatedAt,
|
|
321
|
+
inFlight: (0, chatThreads_1.isThreadStreaming)(chat, now),
|
|
322
|
+
}));
|
|
323
|
+
});
|
|
324
|
+
},
|
|
325
|
+
getThread(threadId) {
|
|
326
|
+
var _a;
|
|
327
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
328
|
+
return (_a = read().threads[threadId]) !== null && _a !== void 0 ? _a : null;
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
isThreadDeleted(threadId) {
|
|
332
|
+
var _a;
|
|
333
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
334
|
+
// Deletes leave tombstones, so a missing key means the store was cleared from outside.
|
|
335
|
+
const storage = resolveStorage(storageOption, storageArea);
|
|
336
|
+
if (!storage || readRaw(storage) === null)
|
|
337
|
+
return true;
|
|
338
|
+
return ((_a = read().deleted) === null || _a === void 0 ? void 0 : _a[threadId]) !== undefined;
|
|
339
|
+
});
|
|
340
|
+
},
|
|
341
|
+
saveThread(chat) {
|
|
342
|
+
return withStorageLock(storageKey, () => transact((current) => mergeThread(current, chat), chat.threadId));
|
|
343
|
+
},
|
|
344
|
+
saveThreadSync(chat, staleIds = []) {
|
|
345
|
+
// No Web Lock: its callback is a task, and tasks do not run while the document unloads.
|
|
346
|
+
transact((current) => {
|
|
347
|
+
const merged = mergeThread(current, chat);
|
|
348
|
+
if (!merged)
|
|
349
|
+
return null;
|
|
350
|
+
return staleIds
|
|
351
|
+
.filter((id) => id !== chat.threadId)
|
|
352
|
+
.reduce((next, id) => retire(next, id), merged);
|
|
353
|
+
}, chat.threadId);
|
|
354
|
+
},
|
|
355
|
+
deleteThread(threadId) {
|
|
356
|
+
return withStorageLock(storageKey, () => transact((current) => retire(current, threadId)));
|
|
357
|
+
},
|
|
358
|
+
subscribe(listener) {
|
|
359
|
+
if (typeof window === 'undefined')
|
|
360
|
+
return () => { };
|
|
361
|
+
const onStorage = (event) => {
|
|
362
|
+
const area = resolveStorage(storageOption, storageArea);
|
|
363
|
+
if (area && event.storageArea && event.storageArea !== area)
|
|
364
|
+
return;
|
|
365
|
+
if (event.key === null || event.key === storageKey)
|
|
366
|
+
listener();
|
|
367
|
+
};
|
|
368
|
+
window.addEventListener('storage', onStorage);
|
|
369
|
+
return () => window.removeEventListener('storage', onStorage);
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
exports.createLocalStoragePersistence = createLocalStoragePersistence;
|
|
374
|
+
const sharedStores = new Map();
|
|
375
|
+
/**
|
|
376
|
+
* One store per namespace and area for the page, so a chat mounted again finds the answers still
|
|
377
|
+
* streaming into it. Not shared on the server, where the map would outlive the request.
|
|
378
|
+
*/
|
|
379
|
+
function sharedLocalStoragePersistence(namespace, storageArea) {
|
|
380
|
+
if (typeof window === 'undefined')
|
|
381
|
+
return createLocalStoragePersistence({ namespace, storageArea });
|
|
382
|
+
const id = `${storageArea}:${namespace}`;
|
|
383
|
+
const existing = sharedStores.get(id);
|
|
384
|
+
if (existing)
|
|
385
|
+
return existing;
|
|
386
|
+
const store = createLocalStoragePersistence({ namespace, storageArea });
|
|
387
|
+
sharedStores.set(id, store);
|
|
388
|
+
return store;
|
|
389
|
+
}
|
|
390
|
+
exports.sharedLocalStoragePersistence = sharedLocalStoragePersistence;
|
|
391
|
+
exports.default = createLocalStoragePersistence;
|
|
@@ -21,6 +21,7 @@ const defaultTranslations = {
|
|
|
21
21
|
'CioAsa.results.saleBadge': 'Sale',
|
|
22
22
|
'CioAsa.refinement.ariaLabel': 'Refine your results',
|
|
23
23
|
'CioAsa.error.message': "I can't assist you with that request.",
|
|
24
|
+
'CioAsa.error.interrupted': 'The answer was interrupted. Please send your question again.',
|
|
24
25
|
};
|
|
25
26
|
function translate(key, translations) {
|
|
26
27
|
var _a;
|
package/lib/cjs/version.js
CHANGED
|
@@ -8,9 +8,8 @@ export default function AiMessage({ message, componentOverrides, translations })
|
|
|
8
8
|
const hasGroups = !!message.groups?.length;
|
|
9
9
|
// On error, fall back to the (translatable) error message when the agent
|
|
10
10
|
// returned no partial text of its own.
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
: message.text || '';
|
|
11
|
+
const errorKey = message.interrupted ? 'CioAsa.error.interrupted' : 'CioAsa.error.message';
|
|
12
|
+
const text = isError ? message.text || translate(errorKey, translations) : message.text || '';
|
|
14
13
|
const hasText = text.length > 0;
|
|
15
14
|
const loaderRenderProps = { translations };
|
|
16
15
|
const textRenderProps = { text };
|