@sybilion/uilib 1.3.79 → 1.3.81
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/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +5 -1
- package/dist/esm/components/ui/Chat/buildChatSendMessagePayload.js +3 -1
- package/dist/esm/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.js +3 -1
- package/dist/esm/contexts/chat-context.js +31 -24
- package/dist/esm/contexts/chatPersistence.js +67 -0
- package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +2 -0
- package/dist/esm/types/src/contexts/chatPersistence.d.ts +5 -0
- package/dist/esm/types/src/contexts/chatPersistence.test.d.ts +1 -0
- package/dist/esm/types/tests/mocks/homecodeUiMock.d.ts +6 -0
- package/package.json +1 -1
- package/src/components/ui/Chat/Chat.types.ts +2 -0
- package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +15 -6
- package/src/components/ui/Chat/buildChatSendMessagePayload.ts +2 -1
- package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.test.ts +16 -4
- package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.ts +2 -1
- package/src/contexts/chat-context.tsx +38 -22
- package/src/contexts/chatPersistence.test.ts +142 -0
- package/src/contexts/chatPersistence.ts +79 -0
|
@@ -10,6 +10,7 @@ import { DropZone } from '../../DropZone/DropZone.js';
|
|
|
10
10
|
import { PanelResizeHandle } from '../../Sidebar/Sidebar.js';
|
|
11
11
|
import SidebarStem from '../../Sidebar/Sidebar.styl.js';
|
|
12
12
|
import { Chat } from '../Chat.js';
|
|
13
|
+
import { MessageRole } from '../Chat.types.js';
|
|
13
14
|
import { filterToTextAttachments, isAttachmentsDropzoneEnabled, buildAcceptAttr } from '../chatAttachmentAccept.js';
|
|
14
15
|
import { extractChatAttachmentItems } from '../chatAttachmentExtract.js';
|
|
15
16
|
import S from './ChatChrome.styl.js';
|
|
@@ -21,6 +22,7 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
|
|
|
21
22
|
const [pendingAttachments, setPendingAttachments] = useState([]);
|
|
22
23
|
const [isExtractingAttachments, setIsExtractingAttachments] = useState(false);
|
|
23
24
|
const promptDisabled = isExtractingAttachments;
|
|
25
|
+
const hasInProgressSystemMessage = useMemo(() => messages.some(msg => msg.role === MessageRole.SYSTEM && msg.inProgress === true), [messages]);
|
|
24
26
|
const handleAttachmentFiles = useCallback((files) => {
|
|
25
27
|
if (promptDisabled || files.length === 0)
|
|
26
28
|
return;
|
|
@@ -85,7 +87,9 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
|
|
|
85
87
|
const label = displayLabelForBranchKeyFromMessages(key, messages) ??
|
|
86
88
|
humanizeBranchKey(key);
|
|
87
89
|
return (jsx("span", { className: S.branchBtnWrap, children: jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: isLoading, onClick: () => onQuickReply(key, label), children: [jsx(PaperPlaneRightIcon, {}), label] }) }, key));
|
|
88
|
-
}) })) : null, showInlinePresets && renderPresets('inline'), isLoading &&
|
|
90
|
+
}) })) : null, showInlinePresets && renderPresets('inline'), isLoading &&
|
|
91
|
+
!hasInProgressSystemMessage &&
|
|
92
|
+
(isLastMessageFromUser || loadingLabel) && (jsx(TextShimmer, { duration: 1, spread: 5, className: S.loader, children: loadingLabel ?? 'Thinking...' }))] }) })), isEmpty ? (jsx("div", { className: S.fixedPresets, children: renderPresets('fixed') })) : null, jsxs("div", { className: cn(S.footer, footerClassName), children: [isEmpty ? (jsx("div", { className: S.notice, children: "Forecast Assistant can make mistakes." })) : null, jsx(Chat.Prompt, { onSubmit: handlePromptSubmitWithAttachments, disabled: promptDisabled, attachments: pendingAttachments, onRemoveAttachment: handleRemoveAttachment, prefillMessage: promptPrefill ?? undefined, placeholder: promptPlaceholder, slashCommandItems: slashCommandItems, onSlashItemCommand: onSlashItemCommand, attachmentAccept: attachmentsDropzoneEnabled ? attachmentAccept : undefined, onAttachmentFiles: attachmentsDropzoneEnabled ? handleAttachmentFiles : undefined })] })] })] })] }));
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
export { ChatChrome };
|
|
@@ -68,7 +68,9 @@ function displayTextFromSendPayload(message) {
|
|
|
68
68
|
}
|
|
69
69
|
/** Optional loading shimmer label from a structured send payload. */
|
|
70
70
|
function loadingLabelFromSendPayload(message) {
|
|
71
|
-
|
|
71
|
+
if (typeof message === 'string')
|
|
72
|
+
return undefined;
|
|
73
|
+
return message.loadingLabel ?? message.systemProgressLabel;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
export { buildChatSendMessagePayload, displayTextFromSendPayload, loadingLabelFromSendPayload, normalizeUserTextFileAttachments };
|
package/dist/esm/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.js
CHANGED
|
@@ -97,12 +97,14 @@ function shiftNormalizedSeriesBackward(series, lagMonths) {
|
|
|
97
97
|
function applyDriversComparisonViewToPayload(payload, tab) {
|
|
98
98
|
if (!payload || tab === 'lagged')
|
|
99
99
|
return payload;
|
|
100
|
+
if (!payload.target?.normalized_series)
|
|
101
|
+
return payload;
|
|
100
102
|
return {
|
|
101
103
|
target: {
|
|
102
104
|
...payload.target,
|
|
103
105
|
normalized_series: { ...payload.target.normalized_series },
|
|
104
106
|
},
|
|
105
|
-
drivers: payload.drivers.map(driver => {
|
|
107
|
+
drivers: (payload.drivers ?? []).map(driver => {
|
|
106
108
|
const lagMonths = parseLagMonthsFromLabel(resolveDriverLagLabel(driver));
|
|
107
109
|
const series = driver.normalized_series ?? {};
|
|
108
110
|
const normalized_series = lagMonths != null && lagMonths > 0
|
|
@@ -4,6 +4,7 @@ import { MessageRole } from '../components/ui/Chat/Chat.types.js';
|
|
|
4
4
|
import { normalizeUserTextFileAttachments } from '../components/ui/Chat/buildChatSendMessagePayload.js';
|
|
5
5
|
import { stripJsonDashboardFences } from '../lib/dashboard-spec/stripJsonDashboardFences.js';
|
|
6
6
|
import { LS } from '@homecode/ui';
|
|
7
|
+
import { persistChatsToLS, safeLsSet } from './chatPersistence.js';
|
|
7
8
|
|
|
8
9
|
const CHATS_PREFIX = 'chats-';
|
|
9
10
|
const CHAT_SCOPE_IDS_REGISTRY_KEY = 'chat-scope-ids';
|
|
@@ -77,7 +78,7 @@ function addScopeIdToRegistry(scopeId) {
|
|
|
77
78
|
const raw = LS.get(CHAT_SCOPE_IDS_REGISTRY_KEY);
|
|
78
79
|
const registry = Array.isArray(raw) ? [...raw] : [];
|
|
79
80
|
if (!registry.includes(scopeId)) {
|
|
80
|
-
|
|
81
|
+
safeLsSet(CHAT_SCOPE_IDS_REGISTRY_KEY, [...registry, scopeId]);
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
/** Shallow-clone messages for seeding another session; drops in-progress rows. */
|
|
@@ -155,25 +156,23 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
155
156
|
setChats(prev => {
|
|
156
157
|
const currentChats = prev[scopeId] ?? [];
|
|
157
158
|
const updatedChats = [newChat, ...currentChats];
|
|
158
|
-
|
|
159
|
-
LS.set(chatsKey, updatedChats);
|
|
159
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
160
160
|
return {
|
|
161
161
|
...prev,
|
|
162
162
|
[scopeId]: updatedChats,
|
|
163
163
|
};
|
|
164
164
|
});
|
|
165
165
|
setCurrentChatIdState(prev => ({ ...prev, [scopeId]: sessionId }));
|
|
166
|
-
|
|
166
|
+
safeLsSet(getCurrentChatIdKey(scopeId), sessionId);
|
|
167
167
|
return sessionId;
|
|
168
168
|
}, [userSwitchKey]);
|
|
169
169
|
const setCurrentChatId = useCallback((currScopeId, sessionId) => {
|
|
170
170
|
if (!sessionId)
|
|
171
171
|
return;
|
|
172
172
|
setCurrentChatIdState(prev => ({ ...prev, [currScopeId]: sessionId }));
|
|
173
|
-
|
|
173
|
+
safeLsSet(getCurrentChatIdKey(currScopeId), sessionId);
|
|
174
174
|
}, []);
|
|
175
175
|
const deleteChat = useCallback((scopeId, sessionId) => {
|
|
176
|
-
const chatsKey = getChatsKey(scopeId);
|
|
177
176
|
const currentKey = getCurrentChatIdKey(scopeId);
|
|
178
177
|
setChats(prev => {
|
|
179
178
|
const scopeChats = prev[scopeId] ?? [];
|
|
@@ -182,7 +181,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
182
181
|
return prev;
|
|
183
182
|
}
|
|
184
183
|
const updatedChats = scopeChats.filter(c => c.session_id !== sessionId);
|
|
185
|
-
|
|
184
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
186
185
|
setCurrentChatIdState(prevCurr => {
|
|
187
186
|
if (prevCurr[scopeId] !== sessionId) {
|
|
188
187
|
return prevCurr;
|
|
@@ -191,7 +190,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
191
190
|
? scopeChats[deletedIndex - 1].session_id
|
|
192
191
|
: (updatedChats[0]?.session_id ?? null);
|
|
193
192
|
if (nextId) {
|
|
194
|
-
|
|
193
|
+
safeLsSet(currentKey, nextId);
|
|
195
194
|
}
|
|
196
195
|
else {
|
|
197
196
|
LS.remove(currentKey);
|
|
@@ -228,8 +227,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
228
227
|
}
|
|
229
228
|
return chat;
|
|
230
229
|
});
|
|
231
|
-
|
|
232
|
-
LS.set(chatsKey, updatedChats);
|
|
230
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
233
231
|
return { ...prev, [scopeId]: updatedChats };
|
|
234
232
|
});
|
|
235
233
|
return newMessage.id;
|
|
@@ -247,8 +245,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
247
245
|
messages: chat.messages.filter(m => m.id !== messageId),
|
|
248
246
|
};
|
|
249
247
|
});
|
|
250
|
-
|
|
251
|
-
LS.set(chatsKey, updatedChats);
|
|
248
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
252
249
|
return { ...prev, [scopeId]: updatedChats };
|
|
253
250
|
});
|
|
254
251
|
}, [userSwitchKey]);
|
|
@@ -286,8 +283,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
286
283
|
}),
|
|
287
284
|
};
|
|
288
285
|
});
|
|
289
|
-
|
|
290
|
-
LS.set(chatsKey, updatedChats);
|
|
286
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
291
287
|
return { ...prev, [scopeId]: updatedChats };
|
|
292
288
|
});
|
|
293
289
|
}, [userSwitchKey]);
|
|
@@ -306,8 +302,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
306
302
|
return chat;
|
|
307
303
|
return { ...chat, messages: cloned };
|
|
308
304
|
});
|
|
309
|
-
|
|
310
|
-
LS.set(chatsKey, updatedChats);
|
|
305
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
311
306
|
return { ...prev, [scopeId]: updatedChats };
|
|
312
307
|
});
|
|
313
308
|
}, [userSwitchKey]);
|
|
@@ -324,8 +319,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
324
319
|
meta: { ...chat.meta, ...patch },
|
|
325
320
|
};
|
|
326
321
|
});
|
|
327
|
-
|
|
328
|
-
LS.set(chatsKey, updatedChats);
|
|
322
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
329
323
|
return { ...prev, [scopeId]: updatedChats };
|
|
330
324
|
});
|
|
331
325
|
}, [userSwitchKey]);
|
|
@@ -343,33 +337,45 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
343
337
|
userTextFileAttachments: normalizeUserTextFileAttachments(message),
|
|
344
338
|
});
|
|
345
339
|
}
|
|
340
|
+
let progressMessageId;
|
|
341
|
+
if (typeof message !== 'string' && message.systemProgressLabel) {
|
|
342
|
+
progressMessageId = addMessage(scopeId, targetChatId, MessageRole.SYSTEM, message.systemProgressLabel, { inProgress: true });
|
|
343
|
+
}
|
|
346
344
|
const pendingChatSessionId = targetChatId;
|
|
347
345
|
beginOutboundPending(scopeId, pendingChatSessionId);
|
|
346
|
+
let effectiveSessionId = pendingChatSessionId;
|
|
348
347
|
try {
|
|
349
348
|
const data = await sendChatMessageFn(apiPayload, pendingChatSessionId);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
349
|
+
effectiveSessionId =
|
|
350
|
+
data.session_id && data.session_id !== pendingChatSessionId
|
|
351
|
+
? data.session_id
|
|
352
|
+
: pendingChatSessionId;
|
|
353
353
|
if (data.session_id && data.session_id !== pendingChatSessionId) {
|
|
354
354
|
setChats(prev => {
|
|
355
355
|
const scopeChats = prev[scopeId] ?? [];
|
|
356
356
|
const updatedChats = scopeChats.map(chat => chat.session_id === pendingChatSessionId
|
|
357
357
|
? { ...chat, session_id: data.session_id }
|
|
358
358
|
: chat);
|
|
359
|
-
|
|
360
|
-
LS.set(chatsKey, updatedChats);
|
|
359
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
361
360
|
return { ...prev, [scopeId]: updatedChats };
|
|
362
361
|
});
|
|
363
362
|
setCurrentChatId(scopeId, data.session_id);
|
|
364
363
|
}
|
|
364
|
+
if (progressMessageId) {
|
|
365
|
+
removeMessageById(scopeId, effectiveSessionId, progressMessageId);
|
|
366
|
+
progressMessageId = undefined;
|
|
367
|
+
}
|
|
365
368
|
addMessage(scopeId, effectiveSessionId, MessageRole.ASSISTANT, data.response);
|
|
366
369
|
return { response: data.response, sessionId: effectiveSessionId };
|
|
367
370
|
}
|
|
368
371
|
catch (error) {
|
|
372
|
+
if (progressMessageId) {
|
|
373
|
+
removeMessageById(scopeId, effectiveSessionId, progressMessageId);
|
|
374
|
+
}
|
|
369
375
|
const errorMessage = error instanceof Error
|
|
370
376
|
? error.message
|
|
371
377
|
: 'Sorry, I encountered an error processing your message. Please try again.';
|
|
372
|
-
addMessage(scopeId,
|
|
378
|
+
addMessage(scopeId, effectiveSessionId, MessageRole.ASSISTANT, errorMessage);
|
|
373
379
|
throw error;
|
|
374
380
|
}
|
|
375
381
|
finally {
|
|
@@ -377,6 +383,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
377
383
|
}
|
|
378
384
|
}, [
|
|
379
385
|
addMessage,
|
|
386
|
+
removeMessageById,
|
|
380
387
|
beginOutboundPending,
|
|
381
388
|
endOutboundPending,
|
|
382
389
|
getCurrentChatId,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { stripJsonDashboardFences } from '../lib/dashboard-spec/stripJsonDashboardFences.js';
|
|
2
|
+
import { LS } from '@homecode/ui';
|
|
3
|
+
|
|
4
|
+
/** Ephemeral scopes (e.g. /reports/new draft) — keep only the latest session in LS. */
|
|
5
|
+
const EPHEMERAL_SCOPE_MARKERS = ['__reports_new_draft__'];
|
|
6
|
+
function isEphemeralChatScope(scopeId) {
|
|
7
|
+
return EPHEMERAL_SCOPE_MARKERS.some(marker => scopeId.endsWith(marker));
|
|
8
|
+
}
|
|
9
|
+
function isQuotaExceededError(error) {
|
|
10
|
+
if (!(error instanceof DOMException))
|
|
11
|
+
return false;
|
|
12
|
+
return (error.name === 'QuotaExceededError' ||
|
|
13
|
+
error.code === 22 ||
|
|
14
|
+
error.code === 1014);
|
|
15
|
+
}
|
|
16
|
+
function stripAttachmentForPersistence(attachment) {
|
|
17
|
+
return {
|
|
18
|
+
displayName: attachment.displayName,
|
|
19
|
+
filename: attachment.filename,
|
|
20
|
+
content: '',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function stripMessageForPersistence(message) {
|
|
24
|
+
const text = stripJsonDashboardFences(message.text);
|
|
25
|
+
const next = { ...message, text };
|
|
26
|
+
if (message.inProgress) {
|
|
27
|
+
delete next.inProgress;
|
|
28
|
+
}
|
|
29
|
+
if (message.userTextFileAttachments?.length) {
|
|
30
|
+
next.userTextFileAttachments = message.userTextFileAttachments.map(stripAttachmentForPersistence);
|
|
31
|
+
}
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
34
|
+
function stripChatsForPersistence(chats) {
|
|
35
|
+
return chats.map(chat => ({
|
|
36
|
+
...chat,
|
|
37
|
+
messages: chat.messages
|
|
38
|
+
.filter(message => !message.inProgress)
|
|
39
|
+
.map(stripMessageForPersistence),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
function chatsForScopePersistence(scopeId, chats) {
|
|
43
|
+
const scopeChats = isEphemeralChatScope(scopeId) ? chats.slice(0, 1) : chats;
|
|
44
|
+
return stripChatsForPersistence(scopeChats);
|
|
45
|
+
}
|
|
46
|
+
function persistChatsToLS(scopeId, chats) {
|
|
47
|
+
const chatsKey = `chats-${scopeId}`;
|
|
48
|
+
const payload = chatsForScopePersistence(scopeId, chats);
|
|
49
|
+
try {
|
|
50
|
+
LS.set(chatsKey, payload);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (!isQuotaExceededError(error))
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function safeLsSet(key, value) {
|
|
58
|
+
try {
|
|
59
|
+
LS.set(key, value);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (!isQuotaExceededError(error))
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { persistChatsToLS, safeLsSet, stripChatsForPersistence, stripMessageForPersistence };
|
|
@@ -21,6 +21,8 @@ export type ChatSendMessagePayload = {
|
|
|
21
21
|
omitUserMessage?: boolean;
|
|
22
22
|
/** Shimmer label while waiting on the API; defaults to "Thinking...". */
|
|
23
23
|
loadingLabel?: string;
|
|
24
|
+
/** When set, show as a SYSTEM inProgress bubble instead of the footer shimmer. */
|
|
25
|
+
systemProgressLabel?: string;
|
|
24
26
|
};
|
|
25
27
|
export type ChatMetaValue = string | number | boolean | null;
|
|
26
28
|
export type ChatMeta = Record<string, ChatMetaValue>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Chat, Message } from '#uilib/components/ui/Chat/Chat.types';
|
|
2
|
+
export declare function stripMessageForPersistence(message: Message): Message;
|
|
3
|
+
export declare function stripChatsForPersistence(chats: Chat[]): Chat[];
|
|
4
|
+
export declare function persistChatsToLS(scopeId: string, chats: Chat[]): void;
|
|
5
|
+
export declare function safeLsSet(key: string, value: unknown): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -29,6 +29,8 @@ export type ChatSendMessagePayload = {
|
|
|
29
29
|
omitUserMessage?: boolean;
|
|
30
30
|
/** Shimmer label while waiting on the API; defaults to "Thinking...". */
|
|
31
31
|
loadingLabel?: string;
|
|
32
|
+
/** When set, show as a SYSTEM inProgress bubble instead of the footer shimmer. */
|
|
33
|
+
systemProgressLabel?: string;
|
|
32
34
|
};
|
|
33
35
|
|
|
34
36
|
export type ChatMetaValue = string | number | boolean | null;
|
|
@@ -14,7 +14,7 @@ import { DropZone } from '../../DropZone/DropZone';
|
|
|
14
14
|
import { PanelResizeHandle } from '../../Sidebar/Sidebar';
|
|
15
15
|
import SidebarStem from '../../Sidebar/Sidebar.styl';
|
|
16
16
|
import { Chat } from '../Chat';
|
|
17
|
-
import type
|
|
17
|
+
import { type ChatAttachmentDropItem, MessageRole } from '../Chat.types';
|
|
18
18
|
import {
|
|
19
19
|
buildAcceptAttr,
|
|
20
20
|
filterToTextAttachments,
|
|
@@ -76,6 +76,13 @@ export function ChatChrome({
|
|
|
76
76
|
>([]);
|
|
77
77
|
const [isExtractingAttachments, setIsExtractingAttachments] = useState(false);
|
|
78
78
|
const promptDisabled = isExtractingAttachments;
|
|
79
|
+
const hasInProgressSystemMessage = useMemo(
|
|
80
|
+
() =>
|
|
81
|
+
messages.some(
|
|
82
|
+
msg => msg.role === MessageRole.SYSTEM && msg.inProgress === true,
|
|
83
|
+
),
|
|
84
|
+
[messages],
|
|
85
|
+
);
|
|
79
86
|
|
|
80
87
|
const handleAttachmentFiles = useCallback(
|
|
81
88
|
(files: File[]) => {
|
|
@@ -263,11 +270,13 @@ export function ChatChrome({
|
|
|
263
270
|
|
|
264
271
|
{showInlinePresets && renderPresets('inline')}
|
|
265
272
|
|
|
266
|
-
{isLoading &&
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
273
|
+
{isLoading &&
|
|
274
|
+
!hasInProgressSystemMessage &&
|
|
275
|
+
(isLastMessageFromUser || loadingLabel) && (
|
|
276
|
+
<TextShimmer duration={1} spread={5} className={S.loader}>
|
|
277
|
+
{loadingLabel ?? 'Thinking...'}
|
|
278
|
+
</TextShimmer>
|
|
279
|
+
)}
|
|
271
280
|
</Scroll>
|
|
272
281
|
</div>
|
|
273
282
|
)}
|
|
@@ -85,5 +85,6 @@ export function displayTextFromSendPayload(
|
|
|
85
85
|
export function loadingLabelFromSendPayload(
|
|
86
86
|
message: string | ChatSendMessagePayload,
|
|
87
87
|
): string | undefined {
|
|
88
|
-
|
|
88
|
+
if (typeof message === 'string') return undefined;
|
|
89
|
+
return message.loadingLabel ?? message.systemProgressLabel;
|
|
89
90
|
}
|
package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.test.ts
CHANGED
|
@@ -127,7 +127,16 @@ describe('applyDriversComparisonViewToPayload', () => {
|
|
|
127
127
|
};
|
|
128
128
|
|
|
129
129
|
it('returns original payload for lagged tab', () => {
|
|
130
|
-
expect(applyDriversComparisonViewToPayload(payload, 'lagged')).toBe(
|
|
130
|
+
expect(applyDriversComparisonViewToPayload(payload, 'lagged')).toBe(
|
|
131
|
+
payload,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('returns original payload for overlapped tab when target series missing', () => {
|
|
136
|
+
const incomplete = { drivers: payload.drivers } as typeof payload;
|
|
137
|
+
expect(applyDriversComparisonViewToPayload(incomplete, 'overlapped')).toBe(
|
|
138
|
+
incomplete,
|
|
139
|
+
);
|
|
131
140
|
});
|
|
132
141
|
|
|
133
142
|
it('shifts driver series backward for overlapped tab without mutating source', () => {
|
|
@@ -253,6 +262,9 @@ describe('buildDriversComparisonChartData historical window floor', () => {
|
|
|
253
262
|
};
|
|
254
263
|
|
|
255
264
|
const datasetHistorical: ChartDataPoint[] = [
|
|
265
|
+
{ date: '2014-07-01', historical: 97 },
|
|
266
|
+
{ date: '2014-08-01', historical: 98 },
|
|
267
|
+
{ date: '2014-09-01', historical: 99 },
|
|
256
268
|
{ date: '2014-10-01', historical: 100 },
|
|
257
269
|
{ date: '2014-11-01', historical: 101 },
|
|
258
270
|
{ date: '2014-12-01', historical: 102 },
|
|
@@ -327,8 +339,8 @@ describe('buildDriversComparisonChartData historical window floor', () => {
|
|
|
327
339
|
|
|
328
340
|
expect(laggedChart[0]?.date).toBe('2014-10-01');
|
|
329
341
|
expect(overlappedChart[0]?.date).toBe('2014-07-01');
|
|
330
|
-
expect(
|
|
331
|
-
|
|
332
|
-
)
|
|
342
|
+
expect(overlappedChart[0]?.date!.localeCompare(laggedChart[0]?.date!)).toBe(
|
|
343
|
+
-1,
|
|
344
|
+
);
|
|
333
345
|
});
|
|
334
346
|
});
|
|
@@ -125,13 +125,14 @@ export function applyDriversComparisonViewToPayload(
|
|
|
125
125
|
tab: DriversComparisonViewTab,
|
|
126
126
|
): BacktestsComponentPayload | null {
|
|
127
127
|
if (!payload || tab === 'lagged') return payload;
|
|
128
|
+
if (!payload.target?.normalized_series) return payload;
|
|
128
129
|
|
|
129
130
|
return {
|
|
130
131
|
target: {
|
|
131
132
|
...payload.target,
|
|
132
133
|
normalized_series: { ...payload.target.normalized_series },
|
|
133
134
|
},
|
|
134
|
-
drivers: payload.drivers.map(driver => {
|
|
135
|
+
drivers: (payload.drivers ?? []).map(driver => {
|
|
135
136
|
const lagMonths = parseLagMonthsFromLabel(resolveDriverLagLabel(driver));
|
|
136
137
|
const series = driver.normalized_series ?? {};
|
|
137
138
|
const normalized_series =
|
|
@@ -21,6 +21,8 @@ import { stripJsonDashboardFences } from '#uilib/lib/dashboard-spec/stripJsonDas
|
|
|
21
21
|
import type { ChatResponse } from '#uilib/types/chat-api.types';
|
|
22
22
|
import { LS } from '@homecode/ui';
|
|
23
23
|
|
|
24
|
+
import { persistChatsToLS, safeLsSet } from './chatPersistence';
|
|
25
|
+
|
|
24
26
|
export type SendChatMessageFn = (
|
|
25
27
|
message: string,
|
|
26
28
|
targetChatId: string,
|
|
@@ -184,7 +186,7 @@ function addScopeIdToRegistry(scopeId: string) {
|
|
|
184
186
|
const raw = LS.get(CHAT_SCOPE_IDS_REGISTRY_KEY);
|
|
185
187
|
const registry = Array.isArray(raw) ? [...raw] : [];
|
|
186
188
|
if (!registry.includes(scopeId)) {
|
|
187
|
-
|
|
189
|
+
safeLsSet(CHAT_SCOPE_IDS_REGISTRY_KEY, [...registry, scopeId]);
|
|
188
190
|
}
|
|
189
191
|
}
|
|
190
192
|
|
|
@@ -300,8 +302,7 @@ export function ChatProvider({
|
|
|
300
302
|
const currentChats = prev[scopeId] ?? [];
|
|
301
303
|
const updatedChats = [newChat, ...currentChats];
|
|
302
304
|
|
|
303
|
-
|
|
304
|
-
LS.set(chatsKey, updatedChats);
|
|
305
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
305
306
|
|
|
306
307
|
return {
|
|
307
308
|
...prev,
|
|
@@ -309,7 +310,7 @@ export function ChatProvider({
|
|
|
309
310
|
};
|
|
310
311
|
});
|
|
311
312
|
setCurrentChatIdState(prev => ({ ...prev, [scopeId]: sessionId }));
|
|
312
|
-
|
|
313
|
+
safeLsSet(getCurrentChatIdKey(scopeId), sessionId);
|
|
313
314
|
return sessionId;
|
|
314
315
|
},
|
|
315
316
|
[userSwitchKey],
|
|
@@ -319,13 +320,12 @@ export function ChatProvider({
|
|
|
319
320
|
(currScopeId: string, sessionId: string) => {
|
|
320
321
|
if (!sessionId) return;
|
|
321
322
|
setCurrentChatIdState(prev => ({ ...prev, [currScopeId]: sessionId }));
|
|
322
|
-
|
|
323
|
+
safeLsSet(getCurrentChatIdKey(currScopeId), sessionId);
|
|
323
324
|
},
|
|
324
325
|
[],
|
|
325
326
|
);
|
|
326
327
|
|
|
327
328
|
const deleteChat = useCallback((scopeId: string, sessionId: string) => {
|
|
328
|
-
const chatsKey = getChatsKey(scopeId);
|
|
329
329
|
const currentKey = getCurrentChatIdKey(scopeId);
|
|
330
330
|
|
|
331
331
|
setChats(prev => {
|
|
@@ -337,7 +337,7 @@ export function ChatProvider({
|
|
|
337
337
|
return prev;
|
|
338
338
|
}
|
|
339
339
|
const updatedChats = scopeChats.filter(c => c.session_id !== sessionId);
|
|
340
|
-
|
|
340
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
341
341
|
|
|
342
342
|
setCurrentChatIdState(prevCurr => {
|
|
343
343
|
if (prevCurr[scopeId] !== sessionId) {
|
|
@@ -348,7 +348,7 @@ export function ChatProvider({
|
|
|
348
348
|
? scopeChats[deletedIndex - 1].session_id
|
|
349
349
|
: (updatedChats[0]?.session_id ?? null);
|
|
350
350
|
if (nextId) {
|
|
351
|
-
|
|
351
|
+
safeLsSet(currentKey, nextId);
|
|
352
352
|
} else {
|
|
353
353
|
LS.remove(currentKey);
|
|
354
354
|
}
|
|
@@ -395,8 +395,7 @@ export function ChatProvider({
|
|
|
395
395
|
return chat;
|
|
396
396
|
});
|
|
397
397
|
|
|
398
|
-
|
|
399
|
-
LS.set(chatsKey, updatedChats);
|
|
398
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
400
399
|
|
|
401
400
|
return { ...prev, [scopeId]: updatedChats };
|
|
402
401
|
});
|
|
@@ -417,8 +416,7 @@ export function ChatProvider({
|
|
|
417
416
|
messages: chat.messages.filter(m => m.id !== messageId),
|
|
418
417
|
};
|
|
419
418
|
});
|
|
420
|
-
|
|
421
|
-
LS.set(chatsKey, updatedChats);
|
|
419
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
422
420
|
return { ...prev, [scopeId]: updatedChats };
|
|
423
421
|
});
|
|
424
422
|
},
|
|
@@ -463,8 +461,7 @@ export function ChatProvider({
|
|
|
463
461
|
}),
|
|
464
462
|
};
|
|
465
463
|
});
|
|
466
|
-
|
|
467
|
-
LS.set(chatsKey, updatedChats);
|
|
464
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
468
465
|
return { ...prev, [scopeId]: updatedChats };
|
|
469
466
|
});
|
|
470
467
|
},
|
|
@@ -487,8 +484,7 @@ export function ChatProvider({
|
|
|
487
484
|
return { ...chat, messages: cloned };
|
|
488
485
|
});
|
|
489
486
|
|
|
490
|
-
|
|
491
|
-
LS.set(chatsKey, updatedChats);
|
|
487
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
492
488
|
|
|
493
489
|
return { ...prev, [scopeId]: updatedChats };
|
|
494
490
|
});
|
|
@@ -509,8 +505,7 @@ export function ChatProvider({
|
|
|
509
505
|
};
|
|
510
506
|
});
|
|
511
507
|
|
|
512
|
-
|
|
513
|
-
LS.set(chatsKey, updatedChats);
|
|
508
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
514
509
|
|
|
515
510
|
return { ...prev, [scopeId]: updatedChats };
|
|
516
511
|
});
|
|
@@ -546,12 +541,24 @@ export function ChatProvider({
|
|
|
546
541
|
);
|
|
547
542
|
}
|
|
548
543
|
|
|
544
|
+
let progressMessageId: string | undefined;
|
|
545
|
+
if (typeof message !== 'string' && message.systemProgressLabel) {
|
|
546
|
+
progressMessageId = addMessage(
|
|
547
|
+
scopeId,
|
|
548
|
+
targetChatId,
|
|
549
|
+
MessageRole.SYSTEM,
|
|
550
|
+
message.systemProgressLabel,
|
|
551
|
+
{ inProgress: true },
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
549
555
|
const pendingChatSessionId = targetChatId;
|
|
550
556
|
beginOutboundPending(scopeId, pendingChatSessionId);
|
|
557
|
+
let effectiveSessionId = pendingChatSessionId;
|
|
551
558
|
try {
|
|
552
559
|
const data = await sendChatMessageFn(apiPayload, pendingChatSessionId);
|
|
553
560
|
|
|
554
|
-
|
|
561
|
+
effectiveSessionId =
|
|
555
562
|
data.session_id && data.session_id !== pendingChatSessionId
|
|
556
563
|
? data.session_id
|
|
557
564
|
: pendingChatSessionId;
|
|
@@ -565,14 +572,18 @@ export function ChatProvider({
|
|
|
565
572
|
: chat,
|
|
566
573
|
);
|
|
567
574
|
|
|
568
|
-
|
|
569
|
-
LS.set(chatsKey, updatedChats);
|
|
575
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
570
576
|
|
|
571
577
|
return { ...prev, [scopeId]: updatedChats };
|
|
572
578
|
});
|
|
573
579
|
setCurrentChatId(scopeId, data.session_id);
|
|
574
580
|
}
|
|
575
581
|
|
|
582
|
+
if (progressMessageId) {
|
|
583
|
+
removeMessageById(scopeId, effectiveSessionId, progressMessageId);
|
|
584
|
+
progressMessageId = undefined;
|
|
585
|
+
}
|
|
586
|
+
|
|
576
587
|
addMessage(
|
|
577
588
|
scopeId,
|
|
578
589
|
effectiveSessionId,
|
|
@@ -582,6 +593,10 @@ export function ChatProvider({
|
|
|
582
593
|
|
|
583
594
|
return { response: data.response, sessionId: effectiveSessionId };
|
|
584
595
|
} catch (error) {
|
|
596
|
+
if (progressMessageId) {
|
|
597
|
+
removeMessageById(scopeId, effectiveSessionId, progressMessageId);
|
|
598
|
+
}
|
|
599
|
+
|
|
585
600
|
const errorMessage =
|
|
586
601
|
error instanceof Error
|
|
587
602
|
? error.message
|
|
@@ -589,7 +604,7 @@ export function ChatProvider({
|
|
|
589
604
|
|
|
590
605
|
addMessage(
|
|
591
606
|
scopeId,
|
|
592
|
-
|
|
607
|
+
effectiveSessionId,
|
|
593
608
|
MessageRole.ASSISTANT,
|
|
594
609
|
errorMessage,
|
|
595
610
|
);
|
|
@@ -600,6 +615,7 @@ export function ChatProvider({
|
|
|
600
615
|
},
|
|
601
616
|
[
|
|
602
617
|
addMessage,
|
|
618
|
+
removeMessageById,
|
|
603
619
|
beginOutboundPending,
|
|
604
620
|
endOutboundPending,
|
|
605
621
|
getCurrentChatId,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { MessageRole } from '#uilib/components/ui/Chat/Chat.types';
|
|
2
|
+
import type { Chat } from '#uilib/components/ui/Chat/Chat.types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
persistChatsToLS,
|
|
6
|
+
stripChatsForPersistence,
|
|
7
|
+
stripMessageForPersistence,
|
|
8
|
+
} from './chatPersistence';
|
|
9
|
+
|
|
10
|
+
describe('stripMessageForPersistence', () => {
|
|
11
|
+
it('removes json-dashboard fences from message text', () => {
|
|
12
|
+
const message = stripMessageForPersistence({
|
|
13
|
+
id: '1',
|
|
14
|
+
role: MessageRole.ASSISTANT,
|
|
15
|
+
text: 'Summary\n```json-dashboard\n{"tiles":[]}\n```',
|
|
16
|
+
timestamp: 1,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(message.text).toBe('Summary');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('drops attachment content but keeps file metadata', () => {
|
|
23
|
+
const message = stripMessageForPersistence({
|
|
24
|
+
id: '2',
|
|
25
|
+
role: MessageRole.USER,
|
|
26
|
+
text: 'see file',
|
|
27
|
+
timestamp: 2,
|
|
28
|
+
userTextFileAttachments: [
|
|
29
|
+
{
|
|
30
|
+
displayName: 'Report',
|
|
31
|
+
filename: 'report.pdf',
|
|
32
|
+
content: 'x'.repeat(5000),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(message.userTextFileAttachments).toEqual([
|
|
38
|
+
{ displayName: 'Report', filename: 'report.pdf', content: '' },
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('stripChatsForPersistence', () => {
|
|
44
|
+
it('filters in-progress messages', () => {
|
|
45
|
+
const chats: Chat[] = [
|
|
46
|
+
{
|
|
47
|
+
session_id: 's1',
|
|
48
|
+
name: '',
|
|
49
|
+
messages: [
|
|
50
|
+
{
|
|
51
|
+
id: 'a',
|
|
52
|
+
role: MessageRole.SYSTEM,
|
|
53
|
+
text: 'Working...',
|
|
54
|
+
timestamp: 1,
|
|
55
|
+
inProgress: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'b',
|
|
59
|
+
role: MessageRole.USER,
|
|
60
|
+
text: 'hello',
|
|
61
|
+
timestamp: 2,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
expect(stripChatsForPersistence(chats)[0].messages).toHaveLength(1);
|
|
68
|
+
expect(stripChatsForPersistence(chats)[0].messages[0].id).toBe('b');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('persistChatsToLS', () => {
|
|
73
|
+
const quotaError = new DOMException('quota', 'QuotaExceededError');
|
|
74
|
+
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
window.localStorage.clear();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('swallows QuotaExceededError without throwing', () => {
|
|
80
|
+
const setItem = jest
|
|
81
|
+
.spyOn(Storage.prototype, 'setItem')
|
|
82
|
+
.mockImplementation(() => {
|
|
83
|
+
throw quotaError;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(() =>
|
|
87
|
+
persistChatsToLS('94-__reports_new_draft__', [
|
|
88
|
+
{
|
|
89
|
+
session_id: 's1',
|
|
90
|
+
name: '',
|
|
91
|
+
messages: [
|
|
92
|
+
{
|
|
93
|
+
id: '1',
|
|
94
|
+
role: MessageRole.USER,
|
|
95
|
+
text: 'hello',
|
|
96
|
+
timestamp: 1,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
]),
|
|
101
|
+
).not.toThrow();
|
|
102
|
+
|
|
103
|
+
setItem.mockRestore();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('persists only the latest session for ephemeral draft scopes', () => {
|
|
107
|
+
persistChatsToLS('94-__reports_new_draft__', [
|
|
108
|
+
{
|
|
109
|
+
session_id: 'current',
|
|
110
|
+
name: '',
|
|
111
|
+
messages: [
|
|
112
|
+
{
|
|
113
|
+
id: '1',
|
|
114
|
+
role: MessageRole.USER,
|
|
115
|
+
text: 'current',
|
|
116
|
+
timestamp: 1,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
session_id: 'old',
|
|
122
|
+
name: '',
|
|
123
|
+
messages: [
|
|
124
|
+
{
|
|
125
|
+
id: '2',
|
|
126
|
+
role: MessageRole.USER,
|
|
127
|
+
text: 'old',
|
|
128
|
+
timestamp: 2,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
const stored = window.localStorage.getItem(
|
|
135
|
+
'chats-94-__reports_new_draft__',
|
|
136
|
+
);
|
|
137
|
+
expect(stored).not.toBeNull();
|
|
138
|
+
const parsed = JSON.parse(stored ?? '[]');
|
|
139
|
+
expect(parsed).toHaveLength(1);
|
|
140
|
+
expect(parsed[0].session_id).toBe('current');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Chat,
|
|
3
|
+
Message,
|
|
4
|
+
UserTextFileAttachment,
|
|
5
|
+
} from '#uilib/components/ui/Chat/Chat.types';
|
|
6
|
+
import { stripJsonDashboardFences } from '#uilib/lib/dashboard-spec/stripJsonDashboardFences';
|
|
7
|
+
import { LS } from '@homecode/ui';
|
|
8
|
+
|
|
9
|
+
/** Ephemeral scopes (e.g. /reports/new draft) — keep only the latest session in LS. */
|
|
10
|
+
const EPHEMERAL_SCOPE_MARKERS = ['__reports_new_draft__'] as const;
|
|
11
|
+
|
|
12
|
+
function isEphemeralChatScope(scopeId: string): boolean {
|
|
13
|
+
return EPHEMERAL_SCOPE_MARKERS.some(marker => scopeId.endsWith(marker));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isQuotaExceededError(error: unknown): boolean {
|
|
17
|
+
if (!(error instanceof DOMException)) return false;
|
|
18
|
+
return (
|
|
19
|
+
error.name === 'QuotaExceededError' ||
|
|
20
|
+
error.code === 22 ||
|
|
21
|
+
error.code === 1014
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function stripAttachmentForPersistence(
|
|
26
|
+
attachment: UserTextFileAttachment,
|
|
27
|
+
): UserTextFileAttachment {
|
|
28
|
+
return {
|
|
29
|
+
displayName: attachment.displayName,
|
|
30
|
+
filename: attachment.filename,
|
|
31
|
+
content: '',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function stripMessageForPersistence(message: Message): Message {
|
|
36
|
+
const text = stripJsonDashboardFences(message.text);
|
|
37
|
+
const next: Message = { ...message, text };
|
|
38
|
+
if (message.inProgress) {
|
|
39
|
+
delete next.inProgress;
|
|
40
|
+
}
|
|
41
|
+
if (message.userTextFileAttachments?.length) {
|
|
42
|
+
next.userTextFileAttachments = message.userTextFileAttachments.map(
|
|
43
|
+
stripAttachmentForPersistence,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return next;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function stripChatsForPersistence(chats: Chat[]): Chat[] {
|
|
50
|
+
return chats.map(chat => ({
|
|
51
|
+
...chat,
|
|
52
|
+
messages: chat.messages
|
|
53
|
+
.filter(message => !message.inProgress)
|
|
54
|
+
.map(stripMessageForPersistence),
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function chatsForScopePersistence(scopeId: string, chats: Chat[]): Chat[] {
|
|
59
|
+
const scopeChats = isEphemeralChatScope(scopeId) ? chats.slice(0, 1) : chats;
|
|
60
|
+
return stripChatsForPersistence(scopeChats);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function persistChatsToLS(scopeId: string, chats: Chat[]): void {
|
|
64
|
+
const chatsKey = `chats-${scopeId}`;
|
|
65
|
+
const payload = chatsForScopePersistence(scopeId, chats);
|
|
66
|
+
try {
|
|
67
|
+
LS.set(chatsKey, payload);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (!isQuotaExceededError(error)) throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function safeLsSet(key: string, value: unknown): void {
|
|
74
|
+
try {
|
|
75
|
+
LS.set(key, value);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (!isQuotaExceededError(error)) throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|