@sybilion/uilib 1.3.66 → 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/Chart/Chart.helpers.js +20 -1
- package/dist/esm/components/ui/Chart/components/BaseChartWrapper.js +2 -1
- package/dist/esm/components/ui/Chart/components/ChartTooltipContent.js +25 -22
- 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/Chart/Chart.helpers.d.ts +16 -0
- package/dist/esm/types/src/components/ui/Chart/components/ChartTooltipItem.d.ts +1 -8
- 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/Chart/Chart.helpers.ts +42 -0
- package/src/components/ui/Chart/components/BaseChartWrapper.tsx +2 -4
- package/src/components/ui/Chart/components/ChartTooltipContent.tsx +37 -32
- package/src/components/ui/Chart/components/ChartTooltipItem.tsx +1 -10
- 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
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
function normalizeTooltipPayload(payload) {
|
|
2
|
+
if (!payload?.length)
|
|
3
|
+
return undefined;
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
const result = [];
|
|
6
|
+
for (const item of payload) {
|
|
7
|
+
if (item.value === null ||
|
|
8
|
+
item.value === undefined ||
|
|
9
|
+
item.type === 'none') {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const key = String(item.dataKey ?? item.name ?? result.length);
|
|
13
|
+
if (seen.has(key))
|
|
14
|
+
continue;
|
|
15
|
+
seen.add(key);
|
|
16
|
+
result.push({ ...item });
|
|
17
|
+
}
|
|
18
|
+
return result.length ? result : undefined;
|
|
19
|
+
}
|
|
1
20
|
function getPayloadConfigFromPayload(config, payload, key) {
|
|
2
21
|
if (typeof payload !== 'object' || payload === null) {
|
|
3
22
|
return config[key];
|
|
@@ -6,4 +25,4 @@ function getPayloadConfigFromPayload(config, payload, key) {
|
|
|
6
25
|
return config[key] || undefined;
|
|
7
26
|
}
|
|
8
27
|
|
|
9
|
-
export { getPayloadConfigFromPayload };
|
|
28
|
+
export { getPayloadConfigFromPayload, normalizeTooltipPayload };
|
|
@@ -5,6 +5,7 @@ import { getForecastColor, ChartLines } from '../../ChartAreaInteractive/ChartLi
|
|
|
5
5
|
import { Skeleton } from '../../Skeleton/Skeleton.js';
|
|
6
6
|
import { chartRenderQueue } from '../../../../utils/chartRenderQueue.js';
|
|
7
7
|
import { Tooltip, LineChart, ComposedChart } from 'recharts';
|
|
8
|
+
import { normalizeTooltipPayload } from '../Chart.helpers.js';
|
|
8
9
|
import { resolveChartMargin, getPlotViewBox } from '../tools/chartPlotGeometry.js';
|
|
9
10
|
import { formatDate } from '../tools/formatters.js';
|
|
10
11
|
import S from './BaseChartWrapper.styl.js';
|
|
@@ -165,7 +166,7 @@ const BaseChartWrapperContent = forwardRef((props, ref) => {
|
|
|
165
166
|
const renderTooltipContent = (props) => {
|
|
166
167
|
// Filter payload to exclude items with null/undefined values
|
|
167
168
|
// This prevents showing stale data when hovering on dates without data points
|
|
168
|
-
const filteredPayload = props.payload
|
|
169
|
+
const filteredPayload = normalizeTooltipPayload(props.payload);
|
|
169
170
|
// If no valid payload items, render ChartTooltipContent with active=false and empty payload
|
|
170
171
|
// This allows ChartTooltipContent to clear its lastTooltipData state
|
|
171
172
|
if (!filteredPayload || filteredPayload.length === 0) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
2
|
import cn from 'classnames';
|
|
3
|
-
import { useState,
|
|
3
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
4
4
|
import { useChart } from '../Chart.context.js';
|
|
5
|
-
import { getPayloadConfigFromPayload } from '../Chart.helpers.js';
|
|
5
|
+
import { normalizeTooltipPayload, getPayloadConfigFromPayload } from '../Chart.helpers.js';
|
|
6
6
|
import S from '../Chart.styl.js';
|
|
7
7
|
import { ChartTooltipItem } from './ChartTooltipItem.js';
|
|
8
8
|
|
|
@@ -10,28 +10,31 @@ function ChartTooltipContent({ active, className, indicator = 'dot', hideLabel =
|
|
|
10
10
|
const { config } = useChart();
|
|
11
11
|
// Keep last tooltip data in state to maintain position when inactive
|
|
12
12
|
const [lastTooltipData, setLastTooltipData] = useState(null);
|
|
13
|
+
const normalizedPayload = useMemo(() => normalizeTooltipPayload(payload), [payload]);
|
|
13
14
|
// Update last tooltip data when active
|
|
14
15
|
useEffect(() => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
16
|
+
setLastTooltipData(prev => {
|
|
17
|
+
// Clear when label changed (prevents stale data)
|
|
18
|
+
let next = prev;
|
|
19
|
+
if (prev && label && prev.label !== label) {
|
|
20
|
+
next = null;
|
|
21
|
+
}
|
|
22
|
+
if (active && normalizedPayload?.length) {
|
|
23
|
+
return {
|
|
24
|
+
active: true,
|
|
25
|
+
payload: normalizedPayload,
|
|
26
|
+
label,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Keep frozen snapshot when inactive (position maintenance in BaseChartWrapper)
|
|
30
|
+
if (!active && next) {
|
|
31
|
+
return { ...next, active: false };
|
|
32
|
+
}
|
|
33
|
+
return next;
|
|
34
|
+
});
|
|
35
|
+
}, [active, normalizedPayload, label]);
|
|
36
|
+
const displayPayload = active && normalizedPayload?.length
|
|
37
|
+
? normalizedPayload
|
|
35
38
|
: lastTooltipData?.payload;
|
|
36
39
|
const displayLabel = active ? label : lastTooltipData?.label;
|
|
37
40
|
const tooltipLabel = useMemo(() => {
|
|
@@ -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,
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { ChartConfig } from './Chart.types';
|
|
2
|
+
/** Recharts may reuse/mutate tooltip payload arrays across hovers — copy + dedupe by series key. */
|
|
3
|
+
export type TooltipPayloadEntry = {
|
|
4
|
+
type?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
dataKey?: string;
|
|
7
|
+
value?: unknown;
|
|
8
|
+
payload?: unknown;
|
|
9
|
+
color?: string;
|
|
10
|
+
};
|
|
11
|
+
export type TooltipItem = TooltipPayloadEntry & {
|
|
12
|
+
type: string;
|
|
13
|
+
name: string;
|
|
14
|
+
value: number | [number, number];
|
|
15
|
+
color: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function normalizeTooltipPayload(payload: readonly TooltipPayloadEntry[] | undefined | null): TooltipPayloadEntry[] | undefined;
|
|
2
18
|
export declare function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string): {
|
|
3
19
|
label?: React.ReactNode;
|
|
4
20
|
icon?: React.ComponentType;
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
+
import { type TooltipItem } from '../Chart.helpers';
|
|
2
3
|
import { ChartConfig } from '../Chart.types';
|
|
3
|
-
type TooltipItem = {
|
|
4
|
-
type: string;
|
|
5
|
-
name: string;
|
|
6
|
-
value: number | [number, number];
|
|
7
|
-
payload: unknown;
|
|
8
|
-
color: string;
|
|
9
|
-
dataKey?: string;
|
|
10
|
-
};
|
|
11
4
|
type ChartTooltipItemProps = {
|
|
12
5
|
item: TooltipItem;
|
|
13
6
|
index: number;
|
|
@@ -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,5 +1,47 @@
|
|
|
1
1
|
import { ChartConfig } from './Chart.types';
|
|
2
2
|
|
|
3
|
+
/** Recharts may reuse/mutate tooltip payload arrays across hovers — copy + dedupe by series key. */
|
|
4
|
+
export type TooltipPayloadEntry = {
|
|
5
|
+
type?: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
dataKey?: string;
|
|
8
|
+
value?: unknown;
|
|
9
|
+
payload?: unknown;
|
|
10
|
+
color?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type TooltipItem = TooltipPayloadEntry & {
|
|
14
|
+
type: string;
|
|
15
|
+
name: string;
|
|
16
|
+
value: number | [number, number];
|
|
17
|
+
color: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function normalizeTooltipPayload(
|
|
21
|
+
payload: readonly TooltipPayloadEntry[] | undefined | null,
|
|
22
|
+
): TooltipPayloadEntry[] | undefined {
|
|
23
|
+
if (!payload?.length) return undefined;
|
|
24
|
+
|
|
25
|
+
const seen = new Set<string>();
|
|
26
|
+
const result: TooltipPayloadEntry[] = [];
|
|
27
|
+
|
|
28
|
+
for (const item of payload) {
|
|
29
|
+
if (
|
|
30
|
+
item.value === null ||
|
|
31
|
+
item.value === undefined ||
|
|
32
|
+
item.type === 'none'
|
|
33
|
+
) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const key = String(item.dataKey ?? item.name ?? result.length);
|
|
37
|
+
if (seen.has(key)) continue;
|
|
38
|
+
seen.add(key);
|
|
39
|
+
result.push({ ...item });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return result.length ? result : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
3
45
|
export function getPayloadConfigFromPayload(
|
|
4
46
|
config: ChartConfig,
|
|
5
47
|
payload: unknown,
|
|
@@ -21,6 +21,7 @@ import { chartRenderQueue } from '#uilib/utils/chartRenderQueue';
|
|
|
21
21
|
import { Tooltip as ChartTooltip, ComposedChart, LineChart } from 'recharts';
|
|
22
22
|
import { LegendPayload } from 'recharts/types/component/DefaultLegendContent';
|
|
23
23
|
|
|
24
|
+
import { normalizeTooltipPayload } from '../Chart.helpers';
|
|
24
25
|
import type { ChartConfig } from '../Chart.types';
|
|
25
26
|
import { getPlotViewBox, resolveChartMargin } from '../tools/chartPlotGeometry';
|
|
26
27
|
import { formatDate } from '../tools/formatters';
|
|
@@ -390,10 +391,7 @@ const BaseChartWrapperContent = forwardRef<
|
|
|
390
391
|
const renderTooltipContent = (props: any) => {
|
|
391
392
|
// Filter payload to exclude items with null/undefined values
|
|
392
393
|
// This prevents showing stale data when hovering on dates without data points
|
|
393
|
-
const filteredPayload = props.payload
|
|
394
|
-
(item: any) =>
|
|
395
|
-
item.value !== null && item.value !== undefined && item.type !== 'none',
|
|
396
|
-
);
|
|
394
|
+
const filteredPayload = normalizeTooltipPayload(props.payload);
|
|
397
395
|
|
|
398
396
|
// If no valid payload items, render ChartTooltipContent with active=false and empty payload
|
|
399
397
|
// This allows ChartTooltipContent to clear its lastTooltipData state
|
|
@@ -2,21 +2,17 @@ import cn from 'classnames';
|
|
|
2
2
|
import { useEffect, useMemo, useState } from 'react';
|
|
3
3
|
|
|
4
4
|
import { useChart } from '#uilib/components/ui/Chart/Chart.context';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type TooltipItem,
|
|
7
|
+
type TooltipPayloadEntry,
|
|
8
|
+
getPayloadConfigFromPayload,
|
|
9
|
+
normalizeTooltipPayload,
|
|
10
|
+
} from '#uilib/components/ui/Chart/Chart.helpers';
|
|
6
11
|
import { ChartTooltipContentProps } from '#uilib/components/ui/Chart/Chart.types';
|
|
7
12
|
|
|
8
13
|
import S from '../Chart.styl';
|
|
9
14
|
import { ChartTooltipItem } from './ChartTooltipItem';
|
|
10
15
|
|
|
11
|
-
type TooltipItem = {
|
|
12
|
-
type: string;
|
|
13
|
-
name: string;
|
|
14
|
-
value: number | [number, number];
|
|
15
|
-
payload: unknown;
|
|
16
|
-
color: string;
|
|
17
|
-
dataKey?: string;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
16
|
export function ChartTooltipContent({
|
|
21
17
|
active,
|
|
22
18
|
className,
|
|
@@ -41,32 +37,41 @@ export function ChartTooltipContent({
|
|
|
41
37
|
label: string | number | undefined;
|
|
42
38
|
} | null>(null);
|
|
43
39
|
|
|
40
|
+
const normalizedPayload = useMemo(
|
|
41
|
+
() => normalizeTooltipPayload(payload as TooltipPayloadEntry[] | undefined),
|
|
42
|
+
[payload],
|
|
43
|
+
);
|
|
44
|
+
|
|
44
45
|
// Update last tooltip data when active
|
|
45
46
|
useEffect(() => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
setLastTooltipData(prev => {
|
|
48
|
+
// Clear when label changed (prevents stale data)
|
|
49
|
+
let next = prev;
|
|
50
|
+
if (prev && label && prev.label !== label) {
|
|
51
|
+
next = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (active && normalizedPayload?.length) {
|
|
55
|
+
return {
|
|
56
|
+
active: true,
|
|
57
|
+
payload: normalizedPayload as TooltipItem[],
|
|
58
|
+
label,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Keep frozen snapshot when inactive (position maintenance in BaseChartWrapper)
|
|
63
|
+
if (!active && next) {
|
|
64
|
+
return { ...next, active: false };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return next;
|
|
68
|
+
});
|
|
69
|
+
}, [active, normalizedPayload, label]);
|
|
62
70
|
|
|
63
|
-
// Use last tooltip data if current is inactive, otherwise use current
|
|
64
|
-
// lastTooltipData is already cleared in useEffect if label changed, so safe to use here
|
|
65
|
-
const displayActive = active || (lastTooltipData?.active ?? false);
|
|
66
71
|
const displayPayload: TooltipItem[] | undefined =
|
|
67
|
-
active &&
|
|
68
|
-
? (
|
|
69
|
-
: lastTooltipData?.payload;
|
|
72
|
+
active && normalizedPayload?.length
|
|
73
|
+
? (normalizedPayload as TooltipItem[])
|
|
74
|
+
: (lastTooltipData?.payload as TooltipItem[] | undefined);
|
|
70
75
|
const displayLabel = active ? label : lastTooltipData?.label;
|
|
71
76
|
|
|
72
77
|
const tooltipLabel = useMemo(() => {
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import cn from 'classnames';
|
|
2
2
|
import { CSSProperties, ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
-
import { getPayloadConfigFromPayload } from '../Chart.helpers';
|
|
4
|
+
import { getPayloadConfigFromPayload, type TooltipItem } from '../Chart.helpers';
|
|
5
5
|
import S from '../Chart.styl';
|
|
6
6
|
import { ChartConfig } from '../Chart.types';
|
|
7
7
|
|
|
8
|
-
type TooltipItem = {
|
|
9
|
-
type: string;
|
|
10
|
-
name: string;
|
|
11
|
-
value: number | [number, number];
|
|
12
|
-
payload: unknown;
|
|
13
|
-
color: string;
|
|
14
|
-
dataKey?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
8
|
type ChartTooltipItemProps = {
|
|
18
9
|
item: TooltipItem;
|
|
19
10
|
index: number;
|
|
@@ -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,
|