@sybilion/uilib 1.3.88 → 1.3.89
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/ChatMessage/AgentMessageContent.helpers.js +29 -21
- package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.js +2 -1
- package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +9 -7
- package/dist/esm/contexts/chat-context.js +180 -205
- package/dist/esm/contexts/chatPersistence.js +6 -18
- package/dist/esm/contexts/chatSessionStorage.js +245 -0
- package/dist/esm/lib/dashboard-spec/jsonDashboardFence.js +80 -0
- package/dist/esm/types/src/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.d.ts +1 -5
- package/dist/esm/types/src/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.test.d.ts +1 -0
- package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +6 -1
- package/dist/esm/types/src/contexts/chat-context.d.ts +6 -0
- package/dist/esm/types/src/contexts/chatPersistence.d.ts +4 -1
- package/dist/esm/types/src/contexts/chatSessionStorage.d.ts +32 -0
- package/dist/esm/types/src/contexts/chatSessionStorage.test.d.ts +1 -0
- package/dist/esm/types/src/lib/dashboard-spec/jsonDashboardFence.d.ts +9 -0
- package/dist/esm/types/src/lib/dashboard-spec/stripJsonDashboardFences.d.ts +1 -2
- package/dist/esm/types/src/lib/dashboard-spec/stripJsonDashboardFences.test.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.test.tsx +25 -0
- package/src/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.tsx +35 -26
- package/src/components/ui/Chat/ChatMessage/ChatMessage.tsx +2 -1
- package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +14 -6
- package/src/contexts/chat-context.tsx +253 -220
- package/src/contexts/chatPersistence.test.ts +11 -0
- package/src/contexts/chatPersistence.ts +22 -6
- package/src/contexts/chatSessionStorage.test.ts +125 -0
- package/src/contexts/chatSessionStorage.ts +321 -0
- package/src/lib/dashboard-spec/jsonDashboardFence.ts +98 -0
- package/src/lib/dashboard-spec/stripJsonDashboardFences.test.ts +84 -0
- package/src/lib/dashboard-spec/stripJsonDashboardFences.ts +5 -6
- package/dist/esm/lib/dashboard-spec/stripJsonDashboardFences.js +0 -7
|
@@ -4,27 +4,6 @@ import logger from '../../../../lib/logger.js';
|
|
|
4
4
|
import S from './ChatMessage.styl.js';
|
|
5
5
|
import S$1 from '../../InteractiveContent/InteractiveContent.styl.js';
|
|
6
6
|
|
|
7
|
-
const injectHeaders = (content) => {
|
|
8
|
-
// Match #, ##, ###, or #### headers at start of line
|
|
9
|
-
const regex = /(^|\n)(#{1,4})\s+(.+?)(?=\n|$)/m;
|
|
10
|
-
const matches = content.match(regex);
|
|
11
|
-
if (!matches)
|
|
12
|
-
return null;
|
|
13
|
-
const level = matches[2].length;
|
|
14
|
-
const headerText = matches[3].replace(/^\*+|\*+$/g, '');
|
|
15
|
-
const Tag = level === 1 ? 'h1' : level === 2 ? 'h2' : level === 3 ? 'h3' : 'h4';
|
|
16
|
-
// Calculate the actual match position and length
|
|
17
|
-
// matches[0] includes the leading \n if present, but we want to replace from the # position
|
|
18
|
-
const hasLeadingNewline = matches[1] === '\n';
|
|
19
|
-
const startIndex = matches.index + (hasLeadingNewline ? 1 : 0);
|
|
20
|
-
// Length is: # markers + space + header text (excluding leading newline)
|
|
21
|
-
const length = matches[2].length + 1 + matches[3].length;
|
|
22
|
-
return {
|
|
23
|
-
elem: jsx(Tag, { children: headerText }),
|
|
24
|
-
index: startIndex,
|
|
25
|
-
length: length,
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
7
|
/** Match sits inside list/table HTML (those blocks use dangerouslySetInnerHTML elsewhere). */
|
|
29
8
|
const isInsideHtmlListOrTable = (content, matchStartIndex) => {
|
|
30
9
|
const before = content.substring(0, matchStartIndex);
|
|
@@ -402,6 +381,35 @@ const injectAnchor = (content) => {
|
|
|
402
381
|
length: matches[0].length,
|
|
403
382
|
};
|
|
404
383
|
};
|
|
384
|
+
const headerTextInjectors = [
|
|
385
|
+
injectAnchor,
|
|
386
|
+
injectMarkdownLink,
|
|
387
|
+
injectHTMLTags,
|
|
388
|
+
injectBold,
|
|
389
|
+
injectItalic,
|
|
390
|
+
injectAutolinkUrl,
|
|
391
|
+
];
|
|
392
|
+
const injectHeaders = (content) => {
|
|
393
|
+
// Match #, ##, ###, or #### headers at start of line
|
|
394
|
+
const regex = /(^|\n)(#{1,4})\s+(.+?)(?=\n|$)/m;
|
|
395
|
+
const matches = content.match(regex);
|
|
396
|
+
if (!matches)
|
|
397
|
+
return null;
|
|
398
|
+
const level = matches[2].length;
|
|
399
|
+
const headerText = matches[3].replace(/^\*+|\*+$/g, '');
|
|
400
|
+
const Tag = level === 1 ? 'h1' : level === 2 ? 'h2' : level === 3 ? 'h3' : 'h4';
|
|
401
|
+
// Calculate the actual match position and length
|
|
402
|
+
// matches[0] includes the leading \n if present, but we want to replace from the # position
|
|
403
|
+
const hasLeadingNewline = matches[1] === '\n';
|
|
404
|
+
const startIndex = matches.index + (hasLeadingNewline ? 1 : 0);
|
|
405
|
+
// Length is: # markers + space + header text (excluding leading newline)
|
|
406
|
+
const length = matches[2].length + 1 + matches[3].length;
|
|
407
|
+
return {
|
|
408
|
+
elem: jsx(Tag, { children: runFormattingPipeline(headerText, headerTextInjectors) }),
|
|
409
|
+
index: startIndex,
|
|
410
|
+
length: length,
|
|
411
|
+
};
|
|
412
|
+
};
|
|
405
413
|
const applyFormatting = (text) => runFormattingPipeline(text, [
|
|
406
414
|
injectHeaders,
|
|
407
415
|
injectAnchor,
|
|
@@ -4,6 +4,7 @@ import { InteractiveContent } from '../../InteractiveContent/InteractiveContent.
|
|
|
4
4
|
import 'lucide-react';
|
|
5
5
|
import '../../InteractiveContent/InteractiveContent.styl.js';
|
|
6
6
|
import { TextShimmer } from '../../TextShimmer/TextShimmer.js';
|
|
7
|
+
import { stripJsonDashboardFences } from '../../../../lib/dashboard-spec/jsonDashboardFence.js';
|
|
7
8
|
import { MessageRole } from '../Chat.types.js';
|
|
8
9
|
import { userTextFileAttachmentsFromMessage } from '../userTextFileAttachments.js';
|
|
9
10
|
import { AgentMessageContent } from './AgentMessageContent.js';
|
|
@@ -16,7 +17,7 @@ function ChatMessage({ role, text, inProgress, userTextFileAttachments, onQuickR
|
|
|
16
17
|
});
|
|
17
18
|
const isAssistant = role === MessageRole.ASSISTANT;
|
|
18
19
|
const isSystem = role === MessageRole.SYSTEM;
|
|
19
|
-
return (jsx("div", { className: cn(S.root, S[`role-${role}`], className), children: isSystem ? (jsx("div", { className: cn(S.text, textClassName), children: inProgress ? (jsx(TextShimmer, { as: "span", children: text })) : renderSystemMessage && message ? (renderSystemMessage(message)) : (text) })) : isAssistant ? (jsx(AgentMessageContent, { text: text, textClassName: textClassName, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: quickReplyDisabled, quickReplyHidden: quickReplyHidden, isLastMessage: isLastMessage, scriptContinue: scriptContinue, onScriptContinue: onScriptContinue, renderMessageChart: renderMessageChart })) : (jsxs("div", { className: S.userColumn, children: [jsx("div", { className: cn(S.text, textClassName && S.textCustom, textClassName), children: jsx(InteractiveContent, { text: text }) }), fileAttachments.map(attachment => (jsx(UserTextFileAttachmentBubble, { attachment: attachment }, `${attachment.displayName}:${attachment.filename}`)))] })) }));
|
|
20
|
+
return (jsx("div", { className: cn(S.root, S[`role-${role}`], className), children: isSystem ? (jsx("div", { className: cn(S.text, textClassName), children: inProgress ? (jsx(TextShimmer, { as: "span", children: text })) : renderSystemMessage && message ? (renderSystemMessage(message)) : (text) })) : isAssistant ? (jsx(AgentMessageContent, { text: stripJsonDashboardFences(text), textClassName: textClassName, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: quickReplyDisabled, quickReplyHidden: quickReplyHidden, isLastMessage: isLastMessage, scriptContinue: scriptContinue, onScriptContinue: onScriptContinue, renderMessageChart: renderMessageChart })) : (jsxs("div", { className: S.userColumn, children: [jsx("div", { className: cn(S.text, textClassName && S.textCustom, textClassName), children: jsx(InteractiveContent, { text: text }) }), fileAttachments.map(attachment => (jsx(UserTextFileAttachmentBubble, { attachment: attachment }, `${attachment.displayName}:${attachment.filename}`)))] })) }));
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export { ChatMessage };
|
|
@@ -22,7 +22,7 @@ const CHAT_NEW_SHORTCUT_KEY = 'o';
|
|
|
22
22
|
const CHAT_QUERY_PARAM = 'chat';
|
|
23
23
|
const CHAT_OPEN_VALUE = 'open';
|
|
24
24
|
const PROMPT_QUERY_PARAM = 'prompt';
|
|
25
|
-
function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, renderSystemMessage, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, copyHistoryOnNewChat = false, transformSendPayload, }) {
|
|
25
|
+
function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, renderSystemMessage, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, copyHistoryOnNewChat = false, transformSendPayload, submitPresetsViaApi = false, }) {
|
|
26
26
|
const effectiveScopeId = scopeId ?? NO_SCOPE_FALLBACK;
|
|
27
27
|
const isMobile = useIsMobile();
|
|
28
28
|
const { chatPanelContainer, isOpen: sidebarNavOpen, setOpen: setSidebarNavOpen, sidebarWidthPx, chatWidthPx, setChatWidthPx, getShellWidth, chatPanelOpen: shellChatPanelOpen, setChatPanelOpen, } = useSidebar();
|
|
@@ -350,7 +350,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
350
350
|
setOutboundLoadingLabel(loadingLabelFromSendPayload(payload));
|
|
351
351
|
try {
|
|
352
352
|
const { response: assistantResponse, sessionId } = await sendMessage(payload);
|
|
353
|
-
onMessage?.(displayTextFromSendPayload(payload), assistantResponse, sessionId);
|
|
353
|
+
await onMessage?.(displayTextFromSendPayload(payload), assistantResponse, sessionId);
|
|
354
354
|
}
|
|
355
355
|
finally {
|
|
356
356
|
setOutboundLoadingLabel(undefined);
|
|
@@ -488,7 +488,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
488
488
|
setOutboundLoadingLabel(loadingLabelFromSendPayload(payload));
|
|
489
489
|
try {
|
|
490
490
|
const { response: assistantResponse, sessionId } = await sendMessage(payload);
|
|
491
|
-
onMessage?.(displayTextFromSendPayload(payload), assistantResponse, sessionId);
|
|
491
|
+
await onMessage?.(displayTextFromSendPayload(payload), assistantResponse, sessionId);
|
|
492
492
|
}
|
|
493
493
|
finally {
|
|
494
494
|
setOutboundLoadingLabel(undefined);
|
|
@@ -523,10 +523,11 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
523
523
|
const scriptGraph = isPresetScriptGraph(script);
|
|
524
524
|
const hasLinearScript = Array.isArray(script) && script.length > 0;
|
|
525
525
|
const hasReplies = preset.replies && Object.keys(preset.replies).length > 0;
|
|
526
|
-
const isLocalDemo =
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
526
|
+
const isLocalDemo = !submitPresetsViaApi &&
|
|
527
|
+
(hasLinearScript ||
|
|
528
|
+
scriptGraph ||
|
|
529
|
+
Boolean(preset.answer?.trim()) ||
|
|
530
|
+
Boolean(hasReplies));
|
|
530
531
|
if (!isLocalDemo) {
|
|
531
532
|
if (!currentChatId)
|
|
532
533
|
return;
|
|
@@ -617,6 +618,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
617
618
|
handlePromptSubmit,
|
|
618
619
|
addMessage,
|
|
619
620
|
presetsWithFreeform,
|
|
621
|
+
submitPresetsViaApi,
|
|
620
622
|
]);
|
|
621
623
|
const resolvedEmptyState = useMemo(() => {
|
|
622
624
|
if (!emptyState)
|
|
@@ -2,84 +2,42 @@ import { jsx } from 'react/jsx-runtime';
|
|
|
2
2
|
import { createContext, useState, useCallback, useEffect, useContext, useMemo } from 'react';
|
|
3
3
|
import { MessageRole } from '../components/ui/Chat/Chat.types.js';
|
|
4
4
|
import { normalizeUserTextFileAttachments } from '../components/ui/Chat/buildChatSendMessagePayload.js';
|
|
5
|
-
import { stripJsonDashboardFences } from '../lib/dashboard-spec/
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { normalizeJsonDashboardChatText, stripJsonDashboardFences } from '../lib/dashboard-spec/jsonDashboardFence.js';
|
|
6
|
+
import { isEphemeralChatScope } from './chatPersistence.js';
|
|
7
|
+
import { loadChatsFromSessionStorage, loadScopeIndex, persistScopeIndex, linkSessionToScope, loadSession, persistSession, addScopeIdToRegistry, removeSessionFromScope, countSessionReferences, deleteSessionBlob, renameSessionId } from './chatSessionStorage.js';
|
|
8
8
|
|
|
9
|
-
const CHATS_PREFIX = 'chats-';
|
|
10
|
-
const CHAT_SCOPE_IDS_REGISTRY_KEY = 'chat-scope-ids';
|
|
11
9
|
const ChatContext = createContext(undefined);
|
|
12
10
|
/** Stable composite key; avoids collisions if `scopeId` contains `:`. */
|
|
13
11
|
function outboundPendingKey(scopeId, chatSessionId) {
|
|
14
12
|
return `${scopeId}\0${chatSessionId}`;
|
|
15
13
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const out = new Set();
|
|
25
|
-
for (const id of all) {
|
|
26
|
-
if (id.startsWith(prefix))
|
|
27
|
-
out.add(id);
|
|
28
|
-
}
|
|
29
|
-
return out;
|
|
30
|
-
}
|
|
31
|
-
function discoverScopeIdsFromLS() {
|
|
32
|
-
const scopeIds = new Set();
|
|
33
|
-
const registryRaw = LS.get(CHAT_SCOPE_IDS_REGISTRY_KEY);
|
|
34
|
-
const registry = Array.isArray(registryRaw) ? registryRaw : [];
|
|
35
|
-
registry.forEach((id) => {
|
|
36
|
-
if (typeof id === 'string' && id)
|
|
37
|
-
scopeIds.add(id);
|
|
38
|
-
});
|
|
39
|
-
try {
|
|
40
|
-
for (let i = 0; i < window.localStorage.length; i++) {
|
|
41
|
-
const key = window.localStorage.key(i);
|
|
42
|
-
if (key?.startsWith(CHATS_PREFIX)) {
|
|
43
|
-
const scopeId = key.slice(CHATS_PREFIX.length);
|
|
44
|
-
if (scopeId)
|
|
45
|
-
scopeIds.add(scopeId);
|
|
46
|
-
}
|
|
14
|
+
function applySessionUpdate(prev, sessionId, updater) {
|
|
15
|
+
let updatedChat = null;
|
|
16
|
+
const next = {};
|
|
17
|
+
for (const [scopeId, scopeChats] of Object.entries(prev)) {
|
|
18
|
+
const idx = scopeChats.findIndex(chat => chat.session_id === sessionId);
|
|
19
|
+
if (idx === -1) {
|
|
20
|
+
next[scopeId] = scopeChats;
|
|
21
|
+
continue;
|
|
47
22
|
}
|
|
23
|
+
const chat = updater(scopeChats[idx]);
|
|
24
|
+
updatedChat = chat;
|
|
25
|
+
const scopeNext = [...scopeChats];
|
|
26
|
+
scopeNext[idx] = chat;
|
|
27
|
+
next[scopeId] = scopeNext;
|
|
48
28
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return scopeIds;
|
|
53
|
-
}
|
|
54
|
-
function loadChatsFromLS(userId) {
|
|
55
|
-
if (typeof window === 'undefined' || userId === null) {
|
|
56
|
-
return { chats: {}, currentChatId: {} };
|
|
57
|
-
}
|
|
58
|
-
const chats = {};
|
|
59
|
-
const currentChatId = {};
|
|
60
|
-
const scopeIds = scopeIdsForUser(userId, discoverScopeIdsFromLS());
|
|
61
|
-
for (const scopeId of scopeIds) {
|
|
62
|
-
const chatsKey = getChatsKey(scopeId);
|
|
63
|
-
const storedRaw = LS.get(chatsKey);
|
|
64
|
-
const stored = Array.isArray(storedRaw) ? storedRaw : undefined;
|
|
65
|
-
if (stored?.length) {
|
|
66
|
-
chats[scopeId] = stored;
|
|
67
|
-
const ck = getCurrentChatIdKey(scopeId);
|
|
68
|
-
const savedCurrentIdRaw = LS.get(ck);
|
|
69
|
-
const savedCurrentId = typeof savedCurrentIdRaw === 'string' && savedCurrentIdRaw !== ''
|
|
70
|
-
? savedCurrentIdRaw
|
|
71
|
-
: undefined;
|
|
72
|
-
currentChatId[scopeId] = savedCurrentId ?? stored[0]?.session_id ?? null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return { chats, currentChatId };
|
|
29
|
+
if (updatedChat)
|
|
30
|
+
persistSession(updatedChat);
|
|
31
|
+
return next;
|
|
76
32
|
}
|
|
77
|
-
function
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
33
|
+
function remapSessionIdInMemory(prev, oldSessionId, newSessionId) {
|
|
34
|
+
const next = {};
|
|
35
|
+
for (const [scopeId, scopeChats] of Object.entries(prev)) {
|
|
36
|
+
next[scopeId] = scopeChats.map(chat => chat.session_id === oldSessionId
|
|
37
|
+
? { ...chat, session_id: newSessionId }
|
|
38
|
+
: chat);
|
|
82
39
|
}
|
|
40
|
+
return next;
|
|
83
41
|
}
|
|
84
42
|
/** Shallow-clone messages for seeding another session; drops in-progress rows. */
|
|
85
43
|
function cloneMessagesForNewSession(messages) {
|
|
@@ -87,6 +45,7 @@ function cloneMessagesForNewSession(messages) {
|
|
|
87
45
|
.filter(message => !message.inProgress)
|
|
88
46
|
.map(message => ({
|
|
89
47
|
...message,
|
|
48
|
+
text: stripJsonDashboardFences(message.text),
|
|
90
49
|
...(message.meta ? { meta: { ...message.meta } } : {}),
|
|
91
50
|
...(message.userTextFileAttachments
|
|
92
51
|
? {
|
|
@@ -97,14 +56,14 @@ function cloneMessagesForNewSession(messages) {
|
|
|
97
56
|
}
|
|
98
57
|
function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessageFn, }) {
|
|
99
58
|
const [chats, setChats] = useState(() => {
|
|
100
|
-
if (userSwitchKey === null)
|
|
59
|
+
if (userSwitchKey === null || typeof window === 'undefined')
|
|
101
60
|
return {};
|
|
102
|
-
return
|
|
61
|
+
return loadChatsFromSessionStorage(userSwitchKey).chats;
|
|
103
62
|
});
|
|
104
63
|
const [currentChatId, setCurrentChatIdState] = useState(() => {
|
|
105
|
-
if (userSwitchKey === null)
|
|
64
|
+
if (userSwitchKey === null || typeof window === 'undefined')
|
|
106
65
|
return {};
|
|
107
|
-
return
|
|
66
|
+
return loadChatsFromSessionStorage(userSwitchKey).currentChatId;
|
|
108
67
|
});
|
|
109
68
|
const [outboundPendingByKey, setOutboundPendingByKey] = useState({});
|
|
110
69
|
const [panelBusyCount, setPanelBusyCount] = useState(0);
|
|
@@ -140,6 +99,54 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
140
99
|
return null;
|
|
141
100
|
return v;
|
|
142
101
|
}, [currentChatId]);
|
|
102
|
+
const setCurrentChatId = useCallback((currScopeId, sessionId) => {
|
|
103
|
+
if (!sessionId)
|
|
104
|
+
return;
|
|
105
|
+
setCurrentChatIdState(prev => ({ ...prev, [currScopeId]: sessionId }));
|
|
106
|
+
const index = loadScopeIndex(currScopeId);
|
|
107
|
+
if (index) {
|
|
108
|
+
persistScopeIndex(currScopeId, {
|
|
109
|
+
...index,
|
|
110
|
+
currentSessionId: sessionId,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}, []);
|
|
114
|
+
const linkSessionToScope$1 = useCallback((scopeId, sessionId, options) => {
|
|
115
|
+
if (userSwitchKey === null)
|
|
116
|
+
return;
|
|
117
|
+
linkSessionToScope(scopeId, sessionId, options);
|
|
118
|
+
setChats(prev => {
|
|
119
|
+
const sessionFromLs = loadSession(sessionId);
|
|
120
|
+
let sessionFromMemory = null;
|
|
121
|
+
for (const scopeChats of Object.values(prev)) {
|
|
122
|
+
const found = scopeChats.find(chat => chat.session_id === sessionId);
|
|
123
|
+
if (found &&
|
|
124
|
+
(!sessionFromMemory ||
|
|
125
|
+
found.messages.length > sessionFromMemory.messages.length)) {
|
|
126
|
+
sessionFromMemory = found;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const session = sessionFromMemory &&
|
|
130
|
+
(!sessionFromLs ||
|
|
131
|
+
sessionFromMemory.messages.length >= sessionFromLs.messages.length)
|
|
132
|
+
? sessionFromMemory
|
|
133
|
+
: sessionFromLs;
|
|
134
|
+
if (!session)
|
|
135
|
+
return prev;
|
|
136
|
+
if (session === sessionFromMemory) {
|
|
137
|
+
persistSession(sessionFromMemory);
|
|
138
|
+
}
|
|
139
|
+
const scopeChats = prev[scopeId] ?? [];
|
|
140
|
+
const exists = scopeChats.some(chat => chat.session_id === sessionId);
|
|
141
|
+
const updatedChats = exists
|
|
142
|
+
? scopeChats.map(chat => chat.session_id === sessionId ? session : chat)
|
|
143
|
+
: [session, ...scopeChats];
|
|
144
|
+
return { ...prev, [scopeId]: updatedChats };
|
|
145
|
+
});
|
|
146
|
+
if (options?.makeCurrent !== false) {
|
|
147
|
+
setCurrentChatIdState(prev => ({ ...prev, [scopeId]: sessionId }));
|
|
148
|
+
}
|
|
149
|
+
}, [userSwitchKey]);
|
|
143
150
|
const newChat = useCallback((scopeId, options) => {
|
|
144
151
|
if (userSwitchKey === null)
|
|
145
152
|
return undefined;
|
|
@@ -148,63 +155,65 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
148
155
|
const seededMessages = options?.seedMessages && options.seedMessages.length > 0
|
|
149
156
|
? cloneMessagesForNewSession(options.seedMessages)
|
|
150
157
|
: [];
|
|
151
|
-
const
|
|
158
|
+
const created = {
|
|
152
159
|
session_id: sessionId,
|
|
153
160
|
name: '',
|
|
154
161
|
messages: seededMessages,
|
|
155
162
|
};
|
|
163
|
+
persistSession(created);
|
|
164
|
+
linkSessionToScope(scopeId, sessionId, { makeCurrent: true });
|
|
156
165
|
setChats(prev => {
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
const scopeChats = prev[scopeId] ?? [];
|
|
167
|
+
const isEphemeral = isEphemeralChatScope(scopeId);
|
|
168
|
+
if (isEphemeral) {
|
|
169
|
+
for (const old of scopeChats) {
|
|
170
|
+
if (old.session_id !== sessionId) {
|
|
171
|
+
removeSessionFromScope(scopeId, old.session_id);
|
|
172
|
+
if (countSessionReferences(userSwitchKey, old.session_id) === 0) {
|
|
173
|
+
deleteSessionBlob(old.session_id);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const updatedChats = isEphemeral ? [created] : [created, ...scopeChats];
|
|
179
|
+
return { ...prev, [scopeId]: updatedChats };
|
|
164
180
|
});
|
|
165
181
|
setCurrentChatIdState(prev => ({ ...prev, [scopeId]: sessionId }));
|
|
166
|
-
safeLsSet(getCurrentChatIdKey(scopeId), sessionId);
|
|
167
182
|
return sessionId;
|
|
168
183
|
}, [userSwitchKey]);
|
|
169
|
-
const setCurrentChatId = useCallback((currScopeId, sessionId) => {
|
|
170
|
-
if (!sessionId)
|
|
171
|
-
return;
|
|
172
|
-
setCurrentChatIdState(prev => ({ ...prev, [currScopeId]: sessionId }));
|
|
173
|
-
safeLsSet(getCurrentChatIdKey(currScopeId), sessionId);
|
|
174
|
-
}, []);
|
|
175
184
|
const deleteChat = useCallback((scopeId, sessionId) => {
|
|
176
|
-
const
|
|
185
|
+
const scopeChats = chats[scopeId] ?? [];
|
|
186
|
+
const deletedIndex = scopeChats.findIndex(chat => chat.session_id === sessionId);
|
|
187
|
+
if (deletedIndex === -1)
|
|
188
|
+
return;
|
|
189
|
+
removeSessionFromScope(scopeId, sessionId);
|
|
190
|
+
if (userSwitchKey != null &&
|
|
191
|
+
countSessionReferences(userSwitchKey, sessionId) === 0) {
|
|
192
|
+
deleteSessionBlob(sessionId);
|
|
193
|
+
}
|
|
177
194
|
setChats(prev => {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
const updatedChats = scopeChats.filter(c => c.session_id !== sessionId);
|
|
184
|
-
persistChatsToLS(scopeId, updatedChats);
|
|
185
|
-
setCurrentChatIdState(prevCurr => {
|
|
186
|
-
if (prevCurr[scopeId] !== sessionId) {
|
|
187
|
-
return prevCurr;
|
|
188
|
-
}
|
|
189
|
-
const nextId = deletedIndex > 0
|
|
190
|
-
? scopeChats[deletedIndex - 1].session_id
|
|
191
|
-
: (updatedChats[0]?.session_id ?? null);
|
|
192
|
-
if (nextId) {
|
|
193
|
-
safeLsSet(currentKey, nextId);
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
LS.remove(currentKey);
|
|
197
|
-
}
|
|
198
|
-
return { ...prevCurr, [scopeId]: nextId };
|
|
199
|
-
});
|
|
200
|
-
return { ...prev, [scopeId]: updatedChats };
|
|
195
|
+
const currentScopeChats = prev[scopeId] ?? [];
|
|
196
|
+
return {
|
|
197
|
+
...prev,
|
|
198
|
+
[scopeId]: currentScopeChats.filter(chat => chat.session_id !== sessionId),
|
|
199
|
+
};
|
|
201
200
|
});
|
|
202
|
-
|
|
201
|
+
setCurrentChatIdState(prevCurr => {
|
|
202
|
+
if (prevCurr[scopeId] !== sessionId)
|
|
203
|
+
return prevCurr;
|
|
204
|
+
const index = loadScopeIndex(scopeId);
|
|
205
|
+
const nextId = index?.currentSessionId ??
|
|
206
|
+
(deletedIndex > 0 ? scopeChats[deletedIndex - 1].session_id : null);
|
|
207
|
+
return { ...prevCurr, [scopeId]: nextId };
|
|
208
|
+
});
|
|
209
|
+
}, [chats, userSwitchKey]);
|
|
203
210
|
const addMessage = useCallback((scopeId, chatId, role, text, options) => {
|
|
204
211
|
if (userSwitchKey === null)
|
|
205
212
|
return undefined;
|
|
206
213
|
addScopeIdToRegistry(scopeId);
|
|
207
|
-
const storedText =
|
|
214
|
+
const storedText = role === MessageRole.ASSISTANT
|
|
215
|
+
? normalizeJsonDashboardChatText(text)
|
|
216
|
+
: stripJsonDashboardFences(text);
|
|
208
217
|
const attachments = role === MessageRole.USER
|
|
209
218
|
? options?.userTextFileAttachments
|
|
210
219
|
: undefined;
|
|
@@ -219,73 +228,52 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
219
228
|
...(options?.inProgress ? { inProgress: true } : {}),
|
|
220
229
|
...(options?.meta ? { meta: { ...options.meta } } : {}),
|
|
221
230
|
};
|
|
222
|
-
setChats(prev => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return { ...chat, messages: [...chat.messages, newMessage] };
|
|
227
|
-
}
|
|
228
|
-
return chat;
|
|
229
|
-
});
|
|
230
|
-
persistChatsToLS(scopeId, updatedChats);
|
|
231
|
-
return { ...prev, [scopeId]: updatedChats };
|
|
232
|
-
});
|
|
231
|
+
setChats(prev => applySessionUpdate(prev, chatId, chat => ({
|
|
232
|
+
...chat,
|
|
233
|
+
messages: [...chat.messages, newMessage],
|
|
234
|
+
})));
|
|
233
235
|
return newMessage.id;
|
|
234
236
|
}, [userSwitchKey]);
|
|
235
237
|
const removeMessageById = useCallback((scopeId, chatId, messageId) => {
|
|
236
238
|
if (userSwitchKey === null)
|
|
237
239
|
return;
|
|
238
|
-
setChats(prev => {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return chat;
|
|
243
|
-
return {
|
|
244
|
-
...chat,
|
|
245
|
-
messages: chat.messages.filter(m => m.id !== messageId),
|
|
246
|
-
};
|
|
247
|
-
});
|
|
248
|
-
persistChatsToLS(scopeId, updatedChats);
|
|
249
|
-
return { ...prev, [scopeId]: updatedChats };
|
|
250
|
-
});
|
|
240
|
+
setChats(prev => applySessionUpdate(prev, chatId, chat => ({
|
|
241
|
+
...chat,
|
|
242
|
+
messages: chat.messages.filter(message => message.id !== messageId),
|
|
243
|
+
})));
|
|
251
244
|
}, [userSwitchKey]);
|
|
252
245
|
const updateMessageById = useCallback((scopeId, chatId, messageId, patch) => {
|
|
253
246
|
if (userSwitchKey === null)
|
|
254
247
|
return;
|
|
255
|
-
setChats(prev => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
};
|
|
285
|
-
});
|
|
286
|
-
persistChatsToLS(scopeId, updatedChats);
|
|
287
|
-
return { ...prev, [scopeId]: updatedChats };
|
|
288
|
-
});
|
|
248
|
+
setChats(prev => applySessionUpdate(prev, chatId, chat => ({
|
|
249
|
+
...chat,
|
|
250
|
+
messages: chat.messages.map(message => {
|
|
251
|
+
if (message.id !== messageId)
|
|
252
|
+
return message;
|
|
253
|
+
const next = { ...message };
|
|
254
|
+
if (patch.role != null) {
|
|
255
|
+
next.role = patch.role;
|
|
256
|
+
}
|
|
257
|
+
if (patch.text != null) {
|
|
258
|
+
const targetRole = patch.role ?? message.role;
|
|
259
|
+
next.text =
|
|
260
|
+
targetRole === MessageRole.ASSISTANT
|
|
261
|
+
? normalizeJsonDashboardChatText(patch.text)
|
|
262
|
+
: stripJsonDashboardFences(patch.text);
|
|
263
|
+
}
|
|
264
|
+
if (patch.inProgress === true) {
|
|
265
|
+
next.inProgress = true;
|
|
266
|
+
}
|
|
267
|
+
else if (patch.inProgress === false ||
|
|
268
|
+
(patch.role != null && patch.role !== MessageRole.SYSTEM)) {
|
|
269
|
+
delete next.inProgress;
|
|
270
|
+
}
|
|
271
|
+
if (patch.meta != null) {
|
|
272
|
+
next.meta = { ...next.meta, ...patch.meta };
|
|
273
|
+
}
|
|
274
|
+
return next;
|
|
275
|
+
}),
|
|
276
|
+
})));
|
|
289
277
|
}, [userSwitchKey]);
|
|
290
278
|
const setChatMessages = useCallback((scopeId, chatId, messages) => {
|
|
291
279
|
if (userSwitchKey === null)
|
|
@@ -293,35 +281,23 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
293
281
|
addScopeIdToRegistry(scopeId);
|
|
294
282
|
const cloned = messages.map(message => ({
|
|
295
283
|
...message,
|
|
284
|
+
text: message.role === MessageRole.ASSISTANT
|
|
285
|
+
? message.text
|
|
286
|
+
: stripJsonDashboardFences(message.text),
|
|
296
287
|
...(message.meta ? { meta: { ...message.meta } } : {}),
|
|
297
288
|
}));
|
|
298
|
-
setChats(prev => {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
return chat;
|
|
303
|
-
return { ...chat, messages: cloned };
|
|
304
|
-
});
|
|
305
|
-
persistChatsToLS(scopeId, updatedChats);
|
|
306
|
-
return { ...prev, [scopeId]: updatedChats };
|
|
307
|
-
});
|
|
289
|
+
setChats(prev => applySessionUpdate(prev, chatId, chat => ({
|
|
290
|
+
...chat,
|
|
291
|
+
messages: cloned,
|
|
292
|
+
})));
|
|
308
293
|
}, [userSwitchKey]);
|
|
309
294
|
const updateChatMeta = useCallback((scopeId, chatId, patch) => {
|
|
310
295
|
if (userSwitchKey === null)
|
|
311
296
|
return;
|
|
312
|
-
setChats(prev => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return chat;
|
|
317
|
-
return {
|
|
318
|
-
...chat,
|
|
319
|
-
meta: { ...chat.meta, ...patch },
|
|
320
|
-
};
|
|
321
|
-
});
|
|
322
|
-
persistChatsToLS(scopeId, updatedChats);
|
|
323
|
-
return { ...prev, [scopeId]: updatedChats };
|
|
324
|
-
});
|
|
297
|
+
setChats(prev => applySessionUpdate(prev, chatId, chat => ({
|
|
298
|
+
...chat,
|
|
299
|
+
meta: { ...chat.meta, ...patch },
|
|
300
|
+
})));
|
|
325
301
|
}, [userSwitchKey]);
|
|
326
302
|
const sendMessage = useCallback(async (scopeId, message, chatId) => {
|
|
327
303
|
const targetChatId = chatId ?? getCurrentChatId(scopeId);
|
|
@@ -350,15 +326,11 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
350
326
|
data.session_id && data.session_id !== pendingChatSessionId
|
|
351
327
|
? data.session_id
|
|
352
328
|
: pendingChatSessionId;
|
|
353
|
-
if (data.session_id &&
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
: chat);
|
|
359
|
-
persistChatsToLS(scopeId, updatedChats);
|
|
360
|
-
return { ...prev, [scopeId]: updatedChats };
|
|
361
|
-
});
|
|
329
|
+
if (data.session_id &&
|
|
330
|
+
data.session_id !== pendingChatSessionId &&
|
|
331
|
+
userSwitchKey != null) {
|
|
332
|
+
renameSessionId(userSwitchKey, pendingChatSessionId, data.session_id);
|
|
333
|
+
setChats(prev => remapSessionIdInMemory(prev, pendingChatSessionId, data.session_id));
|
|
362
334
|
setCurrentChatId(scopeId, data.session_id);
|
|
363
335
|
}
|
|
364
336
|
if (progressMessageId) {
|
|
@@ -389,6 +361,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
389
361
|
getCurrentChatId,
|
|
390
362
|
sendChatMessageFn,
|
|
391
363
|
setCurrentChatId,
|
|
364
|
+
userSwitchKey,
|
|
392
365
|
]);
|
|
393
366
|
useEffect(() => {
|
|
394
367
|
if (userSwitchKey === null) {
|
|
@@ -398,7 +371,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
398
371
|
setPanelBusyCount(0);
|
|
399
372
|
return;
|
|
400
373
|
}
|
|
401
|
-
const loaded =
|
|
374
|
+
const loaded = loadChatsFromSessionStorage(userSwitchKey);
|
|
402
375
|
const updatedCurrentChatId = { ...loaded.currentChatId };
|
|
403
376
|
for (const [scopeId, scopeChats] of Object.entries(loaded.chats)) {
|
|
404
377
|
if (scopeChats.length > 0 &&
|
|
@@ -412,6 +385,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
412
385
|
}, [userSwitchKey]);
|
|
413
386
|
return (jsx(ChatContext.Provider, { value: {
|
|
414
387
|
newChat,
|
|
388
|
+
linkSessionToScope: linkSessionToScope$1,
|
|
415
389
|
setCurrentChatId,
|
|
416
390
|
addMessage,
|
|
417
391
|
removeMessageById,
|
|
@@ -478,7 +452,7 @@ function useSyncChatPanelBusy(isLoading) {
|
|
|
478
452
|
}, [isLoading, acquirePanelBusy, releasePanelBusy]);
|
|
479
453
|
}
|
|
480
454
|
function useChatsForScopeId(scopeId) {
|
|
481
|
-
const { getChatsForScopeId, getCurrentChatId, setCurrentChatId, newChat, addMessage, removeMessageById, updateMessageById, updateChatMeta, setChatMessages, sendMessage, deleteChat, } = useChats();
|
|
455
|
+
const { getChatsForScopeId, getCurrentChatId, setCurrentChatId, newChat, linkSessionToScope, addMessage, removeMessageById, updateMessageById, updateChatMeta, setChatMessages, sendMessage, deleteChat, } = useChats();
|
|
482
456
|
const chats = getChatsForScopeId(scopeId);
|
|
483
457
|
const currentChatId = getCurrentChatId(scopeId);
|
|
484
458
|
const currentChat = useChat(scopeId, currentChatId ?? undefined);
|
|
@@ -489,6 +463,7 @@ function useChatsForScopeId(scopeId) {
|
|
|
489
463
|
currentChatId,
|
|
490
464
|
isOutboundPending,
|
|
491
465
|
setCurrentChatId: (targetId) => setCurrentChatId(scopeId, targetId),
|
|
466
|
+
linkSessionToScope: (sessionId, options) => linkSessionToScope(scopeId, sessionId, options),
|
|
492
467
|
newChat: (options) => newChat(scopeId, options),
|
|
493
468
|
addMessage: (chatId, role, text, options) => addMessage(scopeId, chatId, role, text, options),
|
|
494
469
|
removeMessageById: (chatId, messageId) => removeMessageById(scopeId, chatId, messageId),
|