@sybilion/uilib 1.3.67 → 1.3.68

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.
@@ -14,6 +14,10 @@ function shouldHealChatShellDesync(chatOpen, shellChatPanelOpen, layoutDismissed
14
14
  function shouldCloseStaleLocalChatOpen(shellChatPanelOpen, chatOpen, isOpen) {
15
15
  return !shellChatPanelOpen && !chatOpen && isOpen;
16
16
  }
17
+ /** Shell still reserves width but no instance is showing chat (e.g. route change dropped `?chat=`). */
18
+ function shouldCloseOrphanShellChat(shellChatPanelOpen, chatOpen, isOpen) {
19
+ return shellChatPanelOpen && !chatOpen && !isOpen;
20
+ }
17
21
  function shouldDismissChatAfterShellClosed(params) {
18
22
  const { shellChatPanelOpen, wasShellChatPanelOpen, chatOpen, isOpen, openedShellChat, } = params;
19
23
  if (shellChatPanelOpen || !wasShellChatPanelOpen || !chatOpen) {
@@ -25,4 +29,4 @@ function isChatPanelVisible(isOpen, shellChatPanelOpen) {
25
29
  return isOpen && shellChatPanelOpen;
26
30
  }
27
31
 
28
- export { isChatPanelVisible, shouldCloseStaleLocalChatOpen, shouldDismissChatAfterShellClosed, shouldHealChatShellDesync, shouldOpenChatFromUrl };
32
+ export { isChatPanelVisible, shouldCloseOrphanShellChat, shouldCloseStaleLocalChatOpen, shouldDismissChatAfterShellClosed, shouldHealChatShellDesync, shouldOpenChatFromUrl };
@@ -2,7 +2,7 @@ import { jsx } from 'react/jsx-runtime';
2
2
  import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
3
3
  import { MessageRole } from '../Chat.types.js';
4
4
  import { isGraphIntakeAssistantStepComplete, matchUserTextToQuickReply, isPresetScriptGraph, branchesFromPresetScriptGraph, parseScriptLine, textHasQuickReplyMarkers, branchKeysUsedFromChatHistory, branchKeysUsedByUserMessages, extractQuickReplyLabelKeyPairsFromText, entryBranchKeyBeforeLastAssistant } from '../ChatMessage/presetScript.js';
5
- import { buildChatSendMessagePayload, displayTextFromSendPayload } from '../buildChatSendMessagePayload.js';
5
+ import { displayTextFromSendPayload, buildChatSendMessagePayload } from '../buildChatSendMessagePayload.js';
6
6
  import { usedPresetIdsFromMessages } from '../chat-preset-utils.js';
7
7
  import { useChatsForScopeId, useChat, useChatOutboundPending, useSyncChatPanelBusy, isChatEmpty } from '../../../../contexts/chat-context.js';
8
8
  import { shellFitsSidebarsLayout } from '../../../../hooks/panelWidth.js';
@@ -13,7 +13,7 @@ import logger from '../../../../lib/logger.js';
13
13
  import { mergePresetFreeformDefaults } from '../../../../utils/chatPresetMerge.js';
14
14
  import { useSidebar, DISMISS_CHAT_FOR_LAYOUT_EVENT } from '../../Sidebar/Sidebar.js';
15
15
  import { Chat } from '../Chat.js';
16
- import { shouldOpenChatFromUrl, shouldHealChatShellDesync, shouldCloseStaleLocalChatOpen, shouldDismissChatAfterShellClosed, isChatPanelVisible } from './chatPanelOpenSync.js';
16
+ import { shouldOpenChatFromUrl, shouldHealChatShellDesync, shouldCloseStaleLocalChatOpen, shouldCloseOrphanShellChat, shouldDismissChatAfterShellClosed, isChatPanelVisible } from './chatPanelOpenSync.js';
17
17
 
18
18
  /** Fallback when `scopeId` prop omitted; apps should pass an explicit composite scope (e.g. `${userId}-dashboard`). */
19
19
  const NO_SCOPE_FALLBACK = '__uilib_chat_scope_unset__';
@@ -45,17 +45,20 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
45
45
  /** Nav-forced chat close; blocks URL sync from reopening chat in the same tick. */
46
46
  const layoutDismissedRef = useRef(false);
47
47
  const panelActive = embedAsPage || isOpen;
48
- // Ensure valid currentChatId when chat opens
48
+ // Ensure a renderable session when the panel is active (pick existing or create).
49
49
  useEffect(() => {
50
- if (panelActive && (!currentChatId || currentChatId === '')) {
51
- if (chats.length > 0) {
52
- // Select the first available chat
53
- setCurrentChatId(chats[0].session_id);
54
- }
55
- else {
56
- // Create a new chat if none exists
57
- newChat();
58
- }
50
+ if (!panelActive)
51
+ return;
52
+ const currentValid = currentChatId != null &&
53
+ currentChatId !== '' &&
54
+ chats.some(chat => chat.session_id === currentChatId);
55
+ if (currentValid)
56
+ return;
57
+ if (chats.length > 0) {
58
+ setCurrentChatId(chats[0].session_id);
59
+ }
60
+ else {
61
+ newChat();
59
62
  }
60
63
  }, [panelActive, currentChatId, chats, setCurrentChatId, newChat]);
61
64
  const [scriptByChatId, setScriptByChatId] = useState({});
@@ -294,8 +297,12 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
294
297
  endLocalDemoFlow(chatId);
295
298
  void (async () => {
296
299
  try {
297
- await sendMessage(displayLabel);
298
- onMessage?.(displayLabel);
300
+ let payload = displayLabel;
301
+ if (transformSendPayload) {
302
+ payload = await transformSendPayload(displayLabel, undefined, displayLabel);
303
+ }
304
+ const assistantResponse = await sendMessage(payload);
305
+ onMessage?.(displayTextFromSendPayload(payload), assistantResponse);
299
306
  }
300
307
  catch (error) {
301
308
  logger.error('Error sending chat message:', error);
@@ -312,6 +319,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
312
319
  sendMessage,
313
320
  onMessage,
314
321
  onScriptComplete,
322
+ transformSendPayload,
315
323
  ]);
316
324
  const handlePromptSubmit = useCallback(async (message, attachments) => {
317
325
  const chatId = currentChatId;
@@ -688,6 +696,16 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
688
696
  }
689
697
  setIsOpen(false);
690
698
  }, [embedAsPage, shellChatPanelOpen, chatOpen, isOpen]);
699
+ /** Shell width reserved but no chat UI (e.g. nav dropped `?chat=` while old page still had it). */
700
+ useEffect(() => {
701
+ if (embedAsPage) {
702
+ return;
703
+ }
704
+ if (!shouldCloseOrphanShellChat(shellChatPanelOpen, chatOpen, isOpen)) {
705
+ return;
706
+ }
707
+ setChatPanelOpen(false);
708
+ }, [embedAsPage, shellChatPanelOpen, chatOpen, isOpen, setChatPanelOpen]);
691
709
  /** Shell chat closed while `?chat=` still set (fallback if layout event was missed). */
692
710
  useEffect(() => {
693
711
  if (embedAsPage) {
@@ -706,15 +724,20 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
706
724
  }
707
725
  dismissChatForLayout();
708
726
  }, [embedAsPage, shellChatPanelOpen, chatOpen, dismissChatForLayout, isOpen]);
709
- /** Route change: release shell slot only if this instance opened it (not Strict Mode remount). */
727
+ /** Route change: release shell unless destination URL still has `?chat=` (read live search, not stale closure). */
710
728
  useEffect(() => {
711
729
  return () => {
712
- if (!embedAsPage && openedShellChatRef.current && !chatOpen) {
713
- openedShellChatRef.current = false;
730
+ if (embedAsPage || !openedShellChatRef.current) {
731
+ return;
732
+ }
733
+ openedShellChatRef.current = false;
734
+ const chatStillInUrl = typeof window !== 'undefined' &&
735
+ new URLSearchParams(window.location.search).has(CHAT_QUERY_PARAM);
736
+ if (!chatStillInUrl) {
714
737
  setChatPanelOpen(false);
715
738
  }
716
739
  };
717
- }, [embedAsPage, chatOpen, setChatPanelOpen]);
740
+ }, [embedAsPage, setChatPanelOpen]);
718
741
  useEffect(() => {
719
742
  if (embedAsPage) {
720
743
  return;
@@ -1,6 +1,6 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.Tooltip_tooltipContent__b3pS-{backdrop-filter:blur(10px);background-color:var(--popover);border:1px solid var(--border);border-radius:var(--p-3);box-shadow:0 10px 15px -3px rgba(0,0,0,.1);color:var(--popover-foreground);font-size:12px;min-width:100%;padding:6px 12px;text-wrap:balance;transform-origin:var(--radix-tooltip-content-transform-origin);width:-moz-fit-content;width:fit-content;word-break:break-word;z-index:50}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open],.Tooltip_tooltipContent__b3pS-[data-state=instant-open],.Tooltip_tooltipContent__b3pS-[data-state=open]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=closed]{animation:Tooltip_fade-out__UOBET .1s ease-in,Tooltip_zoom-out__fodOk .1s ease-in}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=bottom]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-top-2__8uuS- .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=left]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-right-2__Uu79F .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=right]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-left-2__23kHm .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=top],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=top],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=top]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-bottom-2__O-Aa8 .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=top]{animation:Tooltip_fade-out__UOBET .1s ease-in,Tooltip_zoom-out__fodOk .1s ease-in}.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=delayed-open],.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=instant-open],.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=open]{animation:Tooltip_fade-in__ZQqZv .15s ease-out}.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=closed]{animation:Tooltip_fade-out__UOBET .1s ease-in}.Tooltip_tooltipContentOverTrigger__VQAU3{box-sizing:border-box;height:auto}.Tooltip_tooltipArrow__87DVL{background-color:var(--popover);border-bottom:1px solid var(--border);border-left-width:1px;border-left:0 solid var(--border);border-radius:2px;border-right:1px solid var(--border);border-top-width:1px;border-top:0 solid var(--border);fill:var(--popover);height:10px;transform:translateY(calc(-50% + .5px)) rotate(45deg);width:10px;z-index:50}@keyframes Tooltip_fade-in__ZQqZv{0%{opacity:0}to{opacity:1}}@keyframes Tooltip_fade-out__UOBET{0%{opacity:1}to{opacity:0}}@keyframes Tooltip_zoom-in__SbWQb{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes Tooltip_zoom-out__fodOk{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes Tooltip_slide-in-from-top-2__8uuS-{0%{opacity:0;transform:translateY(-.5rem)}to{opacity:1;transform:translateY(0)}}@keyframes Tooltip_slide-in-from-right-2__Uu79F{0%{opacity:0;transform:translateX(.5rem)}to{opacity:1;transform:translateX(0)}}@keyframes Tooltip_slide-in-from-left-2__23kHm{0%{opacity:0;transform:translateX(-.5rem)}to{opacity:1;transform:translateX(0)}}@keyframes Tooltip_slide-in-from-bottom-2__O-Aa8{0%{opacity:0;transform:translateY(.5rem)}to{opacity:1;transform:translateY(0)}}";
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.Tooltip_tooltipContent__b3pS-{backdrop-filter:blur(10px);background-color:var(--popover);border:1px solid var(--border);border-radius:var(--p-3);box-shadow:0 10px 15px -3px rgba(0,0,0,.1);color:var(--popover-foreground);font-size:12px;min-width:100%;padding:6px 12px;text-wrap:balance;transform-origin:var(--radix-tooltip-content-transform-origin);width:-moz-fit-content;width:fit-content;word-break:break-word;z-index:50}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open],.Tooltip_tooltipContent__b3pS-[data-state=instant-open],.Tooltip_tooltipContent__b3pS-[data-state=open]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=closed]{animation:Tooltip_fade-out__UOBET .1s ease-in,Tooltip_zoom-out__fodOk .1s ease-in}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=bottom]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-top-2__8uuS- .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=left]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-right-2__Uu79F .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=right]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-left-2__23kHm .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=top],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=top],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=top]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-bottom-2__O-Aa8 .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=top]{animation:Tooltip_fade-out__UOBET .1s ease-in,Tooltip_zoom-out__fodOk .1s ease-in}.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3{box-sizing:border-box;height:auto;text-wrap:wrap}.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=delayed-open],.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=instant-open],.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=open]{animation:Tooltip_fade-in__ZQqZv .15s ease-out}.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=closed]{animation:Tooltip_fade-out__UOBET .1s ease-in}.Tooltip_tooltipArrow__87DVL{background-color:var(--popover);border-bottom:1px solid var(--border);border-left-width:1px;border-left:0 solid var(--border);border-radius:2px;border-right:1px solid var(--border);border-top-width:1px;border-top:0 solid var(--border);fill:var(--popover);height:10px;transform:translateY(calc(-50% + .5px)) rotate(45deg);width:10px;z-index:50}@keyframes Tooltip_fade-in__ZQqZv{0%{opacity:0}to{opacity:1}}@keyframes Tooltip_fade-out__UOBET{0%{opacity:1}to{opacity:0}}@keyframes Tooltip_zoom-in__SbWQb{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes Tooltip_zoom-out__fodOk{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes Tooltip_slide-in-from-top-2__8uuS-{0%{opacity:0;transform:translateY(-.5rem)}to{opacity:1;transform:translateY(0)}}@keyframes Tooltip_slide-in-from-right-2__Uu79F{0%{opacity:0;transform:translateX(.5rem)}to{opacity:1;transform:translateX(0)}}@keyframes Tooltip_slide-in-from-left-2__23kHm{0%{opacity:0;transform:translateX(-.5rem)}to{opacity:1;transform:translateX(0)}}@keyframes Tooltip_slide-in-from-bottom-2__O-Aa8{0%{opacity:0;transform:translateY(.5rem)}to{opacity:1;transform:translateY(0)}}";
4
4
  var S = {"tooltipContent":"Tooltip_tooltipContent__b3pS-","fade-in":"Tooltip_fade-in__ZQqZv","zoom-in":"Tooltip_zoom-in__SbWQb","fade-out":"Tooltip_fade-out__UOBET","zoom-out":"Tooltip_zoom-out__fodOk","slide-in-from-top-2":"Tooltip_slide-in-from-top-2__8uuS-","slide-in-from-right-2":"Tooltip_slide-in-from-right-2__Uu79F","slide-in-from-left-2":"Tooltip_slide-in-from-left-2__23kHm","slide-in-from-bottom-2":"Tooltip_slide-in-from-bottom-2__O-Aa8","tooltipContentOverTrigger":"Tooltip_tooltipContentOverTrigger__VQAU3","tooltipArrow":"Tooltip_tooltipArrow__87DVL"};
5
5
  styleInject(css_248z);
6
6
 
@@ -270,6 +270,23 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
270
270
  return { ...prev, [scopeId]: updatedChats };
271
271
  });
272
272
  }, [userSwitchKey]);
273
+ const setChatMessages = useCallback((scopeId, chatId, messages) => {
274
+ if (userSwitchKey === null)
275
+ return;
276
+ addScopeIdToRegistry(scopeId);
277
+ const cloned = messages.map(message => ({ ...message }));
278
+ setChats(prev => {
279
+ const scopeChats = prev[scopeId] ?? [];
280
+ const updatedChats = scopeChats.map(chat => {
281
+ if (chat.session_id !== chatId)
282
+ return chat;
283
+ return { ...chat, messages: cloned };
284
+ });
285
+ const chatsKey = getChatsKey(scopeId);
286
+ LS.set(chatsKey, updatedChats);
287
+ return { ...prev, [scopeId]: updatedChats };
288
+ });
289
+ }, [userSwitchKey]);
273
290
  const sendMessage = useCallback(async (scopeId, message, chatId) => {
274
291
  const targetChatId = chatId ?? getCurrentChatId(scopeId);
275
292
  if (targetChatId === null || targetChatId === '') {
@@ -347,6 +364,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
347
364
  addMessage,
348
365
  removeMessageById,
349
366
  updateMessageById,
367
+ setChatMessages,
350
368
  sendMessage,
351
369
  getChatsForScopeId,
352
370
  getCurrentChatId,
@@ -2,6 +2,10 @@
2
2
  export declare function shouldOpenChatFromUrl(chatOpen: boolean, layoutDismissed: boolean): boolean;
3
3
  export declare function shouldHealChatShellDesync(chatOpen: boolean, shellChatPanelOpen: boolean, layoutDismissed: boolean, isOpen: boolean): boolean;
4
4
  export declare function shouldCloseStaleLocalChatOpen(shellChatPanelOpen: boolean, chatOpen: boolean, isOpen: boolean): boolean;
5
+ /** Shell still reserves width but no instance is showing chat (e.g. route change dropped `?chat=`). */
6
+ export declare function shouldCloseOrphanShellChat(shellChatPanelOpen: boolean, chatOpen: boolean, isOpen: boolean): boolean;
7
+ /** App shell: collapse chat column when URL has no `?chat=` (no mounted ChatSheet required). */
8
+ export declare function shouldCollapseShellChatWithoutUrlParam(hasChatUrlParam: boolean): boolean;
5
9
  export declare function shouldDismissChatAfterShellClosed(params: {
6
10
  shellChatPanelOpen: boolean;
7
11
  wasShellChatPanelOpen: boolean;
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { type Chat, type ChatSendMessagePayload, MessageRole, type UserTextFileAttachment } from '#uilib/components/ui/Chat/Chat.types';
2
+ import { type Chat, type ChatSendMessagePayload, type Message, MessageRole, type UserTextFileAttachment } from '#uilib/components/ui/Chat/Chat.types';
3
3
  import type { ChatResponse } from '#uilib/types/chat-api.types';
4
4
  export type SendChatMessageFn = (message: string, targetChatId: string) => Promise<ChatResponse>;
5
5
  export type { ChatSendMessagePayload, UserTextFileAttachment, } from '#uilib/components/ui/Chat/Chat.types';
@@ -20,6 +20,8 @@ export interface ChatContextType {
20
20
  addMessage: (scopeId: string, chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string | undefined;
21
21
  removeMessageById: (scopeId: string, chatId: string, messageId: string) => void;
22
22
  updateMessageById: (scopeId: string, chatId: string, messageId: string, patch: UpdateChatMessagePatch) => void;
23
+ /** Replaces all messages on a session (e.g. seeding from another chat). */
24
+ setChatMessages: (scopeId: string, chatId: string, messages: Message[]) => void;
23
25
  sendMessage: (scopeId: string, message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
24
26
  getChatsForScopeId: (scopeId: string) => Chat[];
25
27
  getCurrentChatId: (scopeId: string) => string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.67",
3
+ "version": "1.3.68",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  isChatPanelVisible,
3
+ shouldCloseOrphanShellChat,
3
4
  shouldCloseStaleLocalChatOpen,
5
+ shouldCollapseShellChatWithoutUrlParam,
4
6
  shouldDismissChatAfterShellClosed,
5
7
  shouldHealChatShellDesync,
6
8
  shouldOpenChatFromUrl,
@@ -36,6 +38,30 @@ describe('shouldCloseStaleLocalChatOpen', () => {
36
38
  });
37
39
  });
38
40
 
41
+ describe('shouldCollapseShellChatWithoutUrlParam', () => {
42
+ it('collapses when chat param absent', () => {
43
+ expect(shouldCollapseShellChatWithoutUrlParam(false)).toBe(true);
44
+ });
45
+
46
+ it('keeps shell when chat param present', () => {
47
+ expect(shouldCollapseShellChatWithoutUrlParam(true)).toBe(false);
48
+ });
49
+ });
50
+
51
+ describe('shouldCloseOrphanShellChat', () => {
52
+ it('closes shell slot when URL and local UI are both closed', () => {
53
+ expect(shouldCloseOrphanShellChat(true, false, false)).toBe(true);
54
+ });
55
+
56
+ it('keeps shell while ?chat= requests panel', () => {
57
+ expect(shouldCloseOrphanShellChat(true, true, false)).toBe(false);
58
+ });
59
+
60
+ it('keeps shell while this instance still renders chat', () => {
61
+ expect(shouldCloseOrphanShellChat(true, false, true)).toBe(false);
62
+ });
63
+ });
64
+
39
65
  describe('shouldDismissChatAfterShellClosed', () => {
40
66
  it('dismisses when shell closes with ?chat= and this instance opened chat', () => {
41
67
  expect(
@@ -30,6 +30,22 @@ export function shouldCloseStaleLocalChatOpen(
30
30
  return !shellChatPanelOpen && !chatOpen && isOpen;
31
31
  }
32
32
 
33
+ /** Shell still reserves width but no instance is showing chat (e.g. route change dropped `?chat=`). */
34
+ export function shouldCloseOrphanShellChat(
35
+ shellChatPanelOpen: boolean,
36
+ chatOpen: boolean,
37
+ isOpen: boolean,
38
+ ): boolean {
39
+ return shellChatPanelOpen && !chatOpen && !isOpen;
40
+ }
41
+
42
+ /** App shell: collapse chat column when URL has no `?chat=` (no mounted ChatSheet required). */
43
+ export function shouldCollapseShellChatWithoutUrlParam(
44
+ hasChatUrlParam: boolean,
45
+ ): boolean {
46
+ return !hasChatUrlParam;
47
+ }
48
+
33
49
  export function shouldDismissChatAfterShellClosed(params: {
34
50
  shellChatPanelOpen: boolean;
35
51
  wasShellChatPanelOpen: boolean;
@@ -54,6 +54,7 @@ import type { ChatEmptyStateConfig } from '../ChatEmptyState/ChatEmptyState.type
54
54
  import type { ChatEmptyStateProps } from '../ChatEmptyState/ChatEmptyState.types';
55
55
  import {
56
56
  isChatPanelVisible,
57
+ shouldCloseOrphanShellChat,
57
58
  shouldCloseStaleLocalChatOpen,
58
59
  shouldDismissChatAfterShellClosed,
59
60
  shouldHealChatShellDesync,
@@ -197,16 +198,20 @@ export function useChatPanelChromeModel({
197
198
  const layoutDismissedRef = useRef(false);
198
199
  const panelActive = embedAsPage || isOpen;
199
200
 
200
- // Ensure valid currentChatId when chat opens
201
+ // Ensure a renderable session when the panel is active (pick existing or create).
201
202
  useEffect(() => {
202
- if (panelActive && (!currentChatId || currentChatId === '')) {
203
- if (chats.length > 0) {
204
- // Select the first available chat
205
- setCurrentChatId(chats[0].session_id);
206
- } else {
207
- // Create a new chat if none exists
208
- newChat();
209
- }
203
+ if (!panelActive) return;
204
+
205
+ const currentValid =
206
+ currentChatId != null &&
207
+ currentChatId !== '' &&
208
+ chats.some(chat => chat.session_id === currentChatId);
209
+ if (currentValid) return;
210
+
211
+ if (chats.length > 0) {
212
+ setCurrentChatId(chats[0].session_id);
213
+ } else {
214
+ newChat();
210
215
  }
211
216
  }, [panelActive, currentChatId, chats, setCurrentChatId, newChat]);
212
217
  const [scriptByChatId, setScriptByChatId] = useState<
@@ -496,8 +501,16 @@ export function useChatPanelChromeModel({
496
501
  endLocalDemoFlow(chatId);
497
502
  void (async () => {
498
503
  try {
499
- await sendMessage(displayLabel);
500
- onMessage?.(displayLabel);
504
+ let payload: string | ChatSendMessagePayload = displayLabel;
505
+ if (transformSendPayload) {
506
+ payload = await transformSendPayload(
507
+ displayLabel,
508
+ undefined,
509
+ displayLabel,
510
+ );
511
+ }
512
+ const assistantResponse = await sendMessage(payload);
513
+ onMessage?.(displayTextFromSendPayload(payload), assistantResponse);
501
514
  } catch (error) {
502
515
  logger.error('Error sending chat message:', error);
503
516
  }
@@ -514,6 +527,7 @@ export function useChatPanelChromeModel({
514
527
  sendMessage,
515
528
  onMessage,
516
529
  onScriptComplete,
530
+ transformSendPayload,
517
531
  ],
518
532
  );
519
533
 
@@ -936,6 +950,17 @@ export function useChatPanelChromeModel({
936
950
  setIsOpen(false);
937
951
  }, [embedAsPage, shellChatPanelOpen, chatOpen, isOpen]);
938
952
 
953
+ /** Shell width reserved but no chat UI (e.g. nav dropped `?chat=` while old page still had it). */
954
+ useEffect(() => {
955
+ if (embedAsPage) {
956
+ return;
957
+ }
958
+ if (!shouldCloseOrphanShellChat(shellChatPanelOpen, chatOpen, isOpen)) {
959
+ return;
960
+ }
961
+ setChatPanelOpen(false);
962
+ }, [embedAsPage, shellChatPanelOpen, chatOpen, isOpen, setChatPanelOpen]);
963
+
939
964
  /** Shell chat closed while `?chat=` still set (fallback if layout event was missed). */
940
965
  useEffect(() => {
941
966
  if (embedAsPage) {
@@ -957,15 +982,21 @@ export function useChatPanelChromeModel({
957
982
  dismissChatForLayout();
958
983
  }, [embedAsPage, shellChatPanelOpen, chatOpen, dismissChatForLayout, isOpen]);
959
984
 
960
- /** Route change: release shell slot only if this instance opened it (not Strict Mode remount). */
985
+ /** Route change: release shell unless destination URL still has `?chat=` (read live search, not stale closure). */
961
986
  useEffect(() => {
962
987
  return () => {
963
- if (!embedAsPage && openedShellChatRef.current && !chatOpen) {
964
- openedShellChatRef.current = false;
988
+ if (embedAsPage || !openedShellChatRef.current) {
989
+ return;
990
+ }
991
+ openedShellChatRef.current = false;
992
+ const chatStillInUrl =
993
+ typeof window !== 'undefined' &&
994
+ new URLSearchParams(window.location.search).has(CHAT_QUERY_PARAM);
995
+ if (!chatStillInUrl) {
965
996
  setChatPanelOpen(false);
966
997
  }
967
998
  };
968
- }, [embedAsPage, chatOpen, setChatPanelOpen]);
999
+ }, [embedAsPage, setChatPanelOpen]);
969
1000
 
970
1001
  useEffect(() => {
971
1002
  if (embedAsPage) {
@@ -59,6 +59,10 @@
59
59
  animation fade-out 0.1s ease-in, zoom-out 0.1s ease-in
60
60
 
61
61
  &.tooltipContentOverTrigger
62
+ box-sizing border-box
63
+ height auto
64
+ text-wrap initial
65
+
62
66
  &[data-state="open"],
63
67
  &[data-state="instant-open"],
64
68
  &[data-state="delayed-open"]
@@ -67,10 +71,6 @@
67
71
  &[data-state="closed"]
68
72
  animation fade-out 0.1s ease-in
69
73
 
70
- .tooltipContentOverTrigger
71
- box-sizing border-box
72
- height auto
73
-
74
74
  .tooltipArrow
75
75
  z-index 50
76
76
  width 10px
@@ -67,6 +67,12 @@ export interface ChatContextType {
67
67
  messageId: string,
68
68
  patch: UpdateChatMessagePatch,
69
69
  ) => void;
70
+ /** Replaces all messages on a session (e.g. seeding from another chat). */
71
+ setChatMessages: (
72
+ scopeId: string,
73
+ chatId: string,
74
+ messages: Message[],
75
+ ) => void;
70
76
  sendMessage: (
71
77
  scopeId: string,
72
78
  message: string | ChatSendMessagePayload,
@@ -425,6 +431,28 @@ export function ChatProvider({
425
431
  [userSwitchKey],
426
432
  );
427
433
 
434
+ const setChatMessages = useCallback(
435
+ (scopeId: string, chatId: string, messages: Message[]) => {
436
+ if (userSwitchKey === null) return;
437
+ addScopeIdToRegistry(scopeId);
438
+ const cloned = messages.map(message => ({ ...message }));
439
+
440
+ setChats(prev => {
441
+ const scopeChats = prev[scopeId] ?? [];
442
+ const updatedChats = scopeChats.map(chat => {
443
+ if (chat.session_id !== chatId) return chat;
444
+ return { ...chat, messages: cloned };
445
+ });
446
+
447
+ const chatsKey = getChatsKey(scopeId);
448
+ LS.set(chatsKey, updatedChats);
449
+
450
+ return { ...prev, [scopeId]: updatedChats };
451
+ });
452
+ },
453
+ [userSwitchKey],
454
+ );
455
+
428
456
  const sendMessage = useCallback(
429
457
  async (
430
458
  scopeId: string,
@@ -543,6 +571,7 @@ export function ChatProvider({
543
571
  addMessage,
544
572
  removeMessageById,
545
573
  updateMessageById,
574
+ setChatMessages,
546
575
  sendMessage,
547
576
  getChatsForScopeId,
548
577
  getCurrentChatId,