@sybilion/uilib 1.3.87 → 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/ChatSheet.js +1 -0
- package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +36 -10
- package/dist/esm/components/ui/Sidebar/Sidebar.js +23 -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/ChatEmptyState/ChatEmptyState.types.d.ts +5 -1
- 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/ChatSheet.d.ts +2 -0
- package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +8 -1
- package/dist/esm/types/src/components/ui/Chat/index.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Sidebar/Sidebar.d.ts +2 -0
- 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/ChatEmptyState/ChatEmptyState.types.ts +9 -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/ChatSheet.tsx +3 -0
- package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +55 -10
- package/src/components/ui/Chat/index.ts +1 -0
- package/src/components/ui/Sidebar/Sidebar.tsx +27 -8
- 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();
|
|
@@ -40,6 +40,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
40
40
|
const [promptLinkPrefill, setPromptLinkPrefill] = useState(null);
|
|
41
41
|
/** Deduplicate Strict Mode double `useEffect` on the same mount. */
|
|
42
42
|
const promptParamHandledInEffectRef = useRef(false);
|
|
43
|
+
const pendingOpenChatAndSubmitRef = useRef(null);
|
|
43
44
|
const prevShellChatPanelOpenRef = useRef(shellChatPanelOpen);
|
|
44
45
|
/** This instance requested shell chat width (ignore other ChatSheet instances on the page). */
|
|
45
46
|
const openedShellChatRef = useRef(false);
|
|
@@ -200,6 +201,18 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
200
201
|
setPromptLinkPrefill(trimmed || null);
|
|
201
202
|
onOpenChange(true);
|
|
202
203
|
}, [startEmptyNewChat, onOpenChange]);
|
|
204
|
+
const openNewChatAndSubmit = useCallback((prompt) => {
|
|
205
|
+
const sessionId = startEmptyNewChat();
|
|
206
|
+
if (sessionId == null) {
|
|
207
|
+
logger.warn('Chat submit: sign in to use the assistant.');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const trimmed = prompt.trim();
|
|
211
|
+
if (!trimmed)
|
|
212
|
+
return;
|
|
213
|
+
pendingOpenChatAndSubmitRef.current = { sessionId, prompt: trimmed };
|
|
214
|
+
onOpenChange(true);
|
|
215
|
+
}, [startEmptyNewChat, onOpenChange]);
|
|
203
216
|
/**
|
|
204
217
|
* App link: `?prompt=…` — open panel, pre-fill composer, strip param (read once on mount
|
|
205
218
|
* from `location.search`). If the selected session already has messages, `newChat()` first.
|
|
@@ -337,7 +350,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
337
350
|
setOutboundLoadingLabel(loadingLabelFromSendPayload(payload));
|
|
338
351
|
try {
|
|
339
352
|
const { response: assistantResponse, sessionId } = await sendMessage(payload);
|
|
340
|
-
onMessage?.(displayTextFromSendPayload(payload), assistantResponse, sessionId);
|
|
353
|
+
await onMessage?.(displayTextFromSendPayload(payload), assistantResponse, sessionId);
|
|
341
354
|
}
|
|
342
355
|
finally {
|
|
343
356
|
setOutboundLoadingLabel(undefined);
|
|
@@ -475,7 +488,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
475
488
|
setOutboundLoadingLabel(loadingLabelFromSendPayload(payload));
|
|
476
489
|
try {
|
|
477
490
|
const { response: assistantResponse, sessionId } = await sendMessage(payload);
|
|
478
|
-
onMessage?.(displayTextFromSendPayload(payload), assistantResponse, sessionId);
|
|
491
|
+
await onMessage?.(displayTextFromSendPayload(payload), assistantResponse, sessionId);
|
|
479
492
|
}
|
|
480
493
|
finally {
|
|
481
494
|
setOutboundLoadingLabel(undefined);
|
|
@@ -498,20 +511,28 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
498
511
|
onScriptComplete,
|
|
499
512
|
transformSendPayload,
|
|
500
513
|
]);
|
|
501
|
-
|
|
514
|
+
useEffect(() => {
|
|
515
|
+
const pending = pendingOpenChatAndSubmitRef.current;
|
|
516
|
+
if (!pending || currentChatId !== pending.sessionId)
|
|
517
|
+
return;
|
|
518
|
+
pendingOpenChatAndSubmitRef.current = null;
|
|
519
|
+
void handlePromptSubmit(pending.prompt);
|
|
520
|
+
}, [currentChatId, handlePromptSubmit]);
|
|
521
|
+
const submitPreset = useCallback(async (preset, options) => {
|
|
502
522
|
const script = preset.script;
|
|
503
523
|
const scriptGraph = isPresetScriptGraph(script);
|
|
504
524
|
const hasLinearScript = Array.isArray(script) && script.length > 0;
|
|
505
525
|
const hasReplies = preset.replies && Object.keys(preset.replies).length > 0;
|
|
506
|
-
const isLocalDemo =
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
526
|
+
const isLocalDemo = !submitPresetsViaApi &&
|
|
527
|
+
(hasLinearScript ||
|
|
528
|
+
scriptGraph ||
|
|
529
|
+
Boolean(preset.answer?.trim()) ||
|
|
530
|
+
Boolean(hasReplies));
|
|
510
531
|
if (!isLocalDemo) {
|
|
511
532
|
if (!currentChatId)
|
|
512
533
|
return;
|
|
513
534
|
endLocalDemoFlow(currentChatId);
|
|
514
|
-
await handlePromptSubmit(preset.text);
|
|
535
|
+
await handlePromptSubmit(options?.message ?? preset.text);
|
|
515
536
|
return;
|
|
516
537
|
}
|
|
517
538
|
setLocalUiBusy(true);
|
|
@@ -597,6 +618,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
597
618
|
handlePromptSubmit,
|
|
598
619
|
addMessage,
|
|
599
620
|
presetsWithFreeform,
|
|
621
|
+
submitPresetsViaApi,
|
|
600
622
|
]);
|
|
601
623
|
const resolvedEmptyState = useMemo(() => {
|
|
602
624
|
if (!emptyState)
|
|
@@ -798,7 +820,10 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
798
820
|
chatWidthPx,
|
|
799
821
|
})) {
|
|
800
822
|
// Chat open uses `startViewTransition`; nested transition here aborts first → AbortError overlay.
|
|
801
|
-
setSidebarNavOpen(false, {
|
|
823
|
+
setSidebarNavOpen(false, {
|
|
824
|
+
viewTransition: false,
|
|
825
|
+
layoutAutoClose: true,
|
|
826
|
+
});
|
|
802
827
|
}
|
|
803
828
|
};
|
|
804
829
|
collapseNavIfNoSpace();
|
|
@@ -948,6 +973,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
948
973
|
toggleOpen,
|
|
949
974
|
newChat: startEmptyNewChat,
|
|
950
975
|
openNewChatWithPrefill,
|
|
976
|
+
openNewChatAndSubmit,
|
|
951
977
|
chatPanelContainer,
|
|
952
978
|
};
|
|
953
979
|
}
|
|
@@ -85,6 +85,8 @@ function SidebarProvider({ defaultOpen = getCookie('isSidebarOpen') ?? true, cla
|
|
|
85
85
|
const [chatPanelOpen, _setChatPanelOpen] = useState(false);
|
|
86
86
|
const sidebarWidthRef = useRef(sidebarWidthPx);
|
|
87
87
|
const chatWidthRef = useRef(chatWidthPx);
|
|
88
|
+
/** Left nav was auto-closed because chat opened on a narrow shell; reopen when chat closes. */
|
|
89
|
+
const sidebarAutoClosedForChatRef = useRef(false);
|
|
88
90
|
sidebarWidthRef.current = sidebarWidthPx;
|
|
89
91
|
chatWidthRef.current = chatWidthPx;
|
|
90
92
|
const mergeWrapperRef = useCallback((el) => {
|
|
@@ -226,10 +228,8 @@ function SidebarProvider({ defaultOpen = getCookie('isSidebarOpen') ?? true, cla
|
|
|
226
228
|
}
|
|
227
229
|
}
|
|
228
230
|
else if (openingChat && isOpen) {
|
|
231
|
+
sidebarAutoClosedForChatRef.current = true;
|
|
229
232
|
setIsOpen(false);
|
|
230
|
-
if (getCookiePreferences(userId)?.functional) {
|
|
231
|
-
setCookie('isSidebarOpen', 'false', 60 * 60 * 24 * 7);
|
|
232
|
-
}
|
|
233
233
|
}
|
|
234
234
|
}, [
|
|
235
235
|
isSidebarSheetLayout,
|
|
@@ -238,18 +238,34 @@ function SidebarProvider({ defaultOpen = getCookie('isSidebarOpen') ?? true, cla
|
|
|
238
238
|
chatPanelOpen,
|
|
239
239
|
sidebarWidthPx,
|
|
240
240
|
chatWidthPx,
|
|
241
|
-
userId,
|
|
242
241
|
]);
|
|
243
242
|
const setChatPanelOpen = useCallback((open) => {
|
|
244
243
|
if (open) {
|
|
245
244
|
closeOppositeSidebarIfNoSpace(false, true);
|
|
245
|
+
_setChatPanelOpen(true);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
_setChatPanelOpen(false);
|
|
249
|
+
if (sidebarAutoClosedForChatRef.current) {
|
|
250
|
+
sidebarAutoClosedForChatRef.current = false;
|
|
251
|
+
setIsOpen(true);
|
|
252
|
+
if (getCookiePreferences(userId)?.functional) {
|
|
253
|
+
setCookie('isSidebarOpen', 'true', 60 * 60 * 24 * 7);
|
|
254
|
+
}
|
|
246
255
|
}
|
|
247
|
-
|
|
248
|
-
}, [closeOppositeSidebarIfNoSpace]);
|
|
256
|
+
}, [closeOppositeSidebarIfNoSpace, userId]);
|
|
249
257
|
const setOpen = useCallback((value, options) => {
|
|
258
|
+
const layoutAutoClose = options?.layoutAutoClose === true;
|
|
250
259
|
if (value) {
|
|
260
|
+
sidebarAutoClosedForChatRef.current = false;
|
|
251
261
|
closeOppositeSidebarIfNoSpace(true, false);
|
|
252
262
|
}
|
|
263
|
+
else if (layoutAutoClose) {
|
|
264
|
+
sidebarAutoClosedForChatRef.current = true;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
sidebarAutoClosedForChatRef.current = false;
|
|
268
|
+
}
|
|
253
269
|
const useViewTransition = options?.viewTransition !== false &&
|
|
254
270
|
!isSidebarSheetLayout &&
|
|
255
271
|
'startViewTransition' in document &&
|
|
@@ -262,7 +278,7 @@ function SidebarProvider({ defaultOpen = getCookie('isSidebarOpen') ?? true, cla
|
|
|
262
278
|
else {
|
|
263
279
|
setIsOpen(value);
|
|
264
280
|
}
|
|
265
|
-
if (getCookiePreferences(userId)?.functional) {
|
|
281
|
+
if (!layoutAutoClose && getCookiePreferences(userId)?.functional) {
|
|
266
282
|
setCookie('isSidebarOpen', value.toString(), 60 * 60 * 24 * 7);
|
|
267
283
|
}
|
|
268
284
|
}, [isSidebarSheetLayout, userId, closeOppositeSidebarIfNoSpace]);
|