@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.
Files changed (41) hide show
  1. package/dist/esm/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.js +29 -21
  2. package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.js +2 -1
  3. package/dist/esm/components/ui/Chat/ChatSheet/ChatSheet.js +1 -0
  4. package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +36 -10
  5. package/dist/esm/components/ui/Sidebar/Sidebar.js +23 -7
  6. package/dist/esm/contexts/chat-context.js +180 -205
  7. package/dist/esm/contexts/chatPersistence.js +6 -18
  8. package/dist/esm/contexts/chatSessionStorage.js +245 -0
  9. package/dist/esm/lib/dashboard-spec/jsonDashboardFence.js +80 -0
  10. package/dist/esm/types/src/components/ui/Chat/ChatEmptyState/ChatEmptyState.types.d.ts +5 -1
  11. package/dist/esm/types/src/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.d.ts +1 -5
  12. package/dist/esm/types/src/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.test.d.ts +1 -0
  13. package/dist/esm/types/src/components/ui/Chat/ChatSheet/ChatSheet.d.ts +2 -0
  14. package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +8 -1
  15. package/dist/esm/types/src/components/ui/Chat/index.d.ts +1 -1
  16. package/dist/esm/types/src/components/ui/Sidebar/Sidebar.d.ts +2 -0
  17. package/dist/esm/types/src/contexts/chat-context.d.ts +6 -0
  18. package/dist/esm/types/src/contexts/chatPersistence.d.ts +4 -1
  19. package/dist/esm/types/src/contexts/chatSessionStorage.d.ts +32 -0
  20. package/dist/esm/types/src/contexts/chatSessionStorage.test.d.ts +1 -0
  21. package/dist/esm/types/src/lib/dashboard-spec/jsonDashboardFence.d.ts +9 -0
  22. package/dist/esm/types/src/lib/dashboard-spec/stripJsonDashboardFences.d.ts +1 -2
  23. package/dist/esm/types/src/lib/dashboard-spec/stripJsonDashboardFences.test.d.ts +1 -0
  24. package/package.json +1 -1
  25. package/src/components/ui/Chat/ChatEmptyState/ChatEmptyState.types.ts +9 -1
  26. package/src/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.test.tsx +25 -0
  27. package/src/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.tsx +35 -26
  28. package/src/components/ui/Chat/ChatMessage/ChatMessage.tsx +2 -1
  29. package/src/components/ui/Chat/ChatSheet/ChatSheet.tsx +3 -0
  30. package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +55 -10
  31. package/src/components/ui/Chat/index.ts +1 -0
  32. package/src/components/ui/Sidebar/Sidebar.tsx +27 -8
  33. package/src/contexts/chat-context.tsx +253 -220
  34. package/src/contexts/chatPersistence.test.ts +11 -0
  35. package/src/contexts/chatPersistence.ts +22 -6
  36. package/src/contexts/chatSessionStorage.test.ts +125 -0
  37. package/src/contexts/chatSessionStorage.ts +321 -0
  38. package/src/lib/dashboard-spec/jsonDashboardFence.ts +98 -0
  39. package/src/lib/dashboard-spec/stripJsonDashboardFences.test.ts +84 -0
  40. package/src/lib/dashboard-spec/stripJsonDashboardFences.ts +5 -6
  41. 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 };
@@ -29,6 +29,7 @@ function ChatSheet({ triggerLabel = 'Open Chat', triggerAriaLabel, actionsRef, r
29
29
  model.onOpenChange(true);
30
30
  },
31
31
  openNewChatWithPrefill: model.openNewChatWithPrefill,
32
+ openNewChatAndSubmit: model.openNewChatAndSubmit,
32
33
  };
33
34
  }
34
35
  if (inline) {
@@ -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
- const submitPreset = useCallback(async (preset) => {
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 = hasLinearScript ||
507
- scriptGraph ||
508
- Boolean(preset.answer?.trim()) ||
509
- Boolean(hasReplies);
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, { viewTransition: 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
- _setChatPanelOpen(open);
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]);