@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.
- package/dist/esm/components/ui/Chat/ChatSheet/chatPanelOpenSync.js +5 -1
- package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +41 -18
- package/dist/esm/components/ui/Tooltip/Tooltip.styl.js +1 -1
- package/dist/esm/contexts/chat-context.js +18 -0
- package/dist/esm/types/src/components/ui/Chat/ChatSheet/chatPanelOpenSync.d.ts +4 -0
- package/dist/esm/types/src/contexts/chat-context.d.ts +3 -1
- package/package.json +1 -1
- package/src/components/ui/Chat/ChatSheet/chatPanelOpenSync.test.ts +26 -0
- package/src/components/ui/Chat/ChatSheet/chatPanelOpenSync.ts +16 -0
- package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +46 -15
- package/src/components/ui/Tooltip/Tooltip.styl +4 -4
- package/src/contexts/chat-context.tsx +29 -0
|
@@ -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 {
|
|
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
|
|
48
|
+
// Ensure a renderable session when the panel is active (pick existing or create).
|
|
49
49
|
useEffect(() => {
|
|
50
|
-
if (panelActive
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
|
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 (
|
|
713
|
-
|
|
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,
|
|
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}.
|
|
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,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
|
|
201
|
+
// Ensure a renderable session when the panel is active (pick existing or create).
|
|
201
202
|
useEffect(() => {
|
|
202
|
-
if (panelActive
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
500
|
-
|
|
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
|
|
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 (
|
|
964
|
-
|
|
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,
|
|
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,
|