@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.
@@ -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?.filter((item) => item.value !== null && item.value !== undefined && item.type !== 'none');
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, useEffect, useMemo } from 'react';
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
- // Clear lastTooltipData immediately if label changed (prevents stale data)
16
- // Only clear if label is explicitly provided and different (not undefined)
17
- if (lastTooltipData && label && lastTooltipData.label !== label) {
18
- setLastTooltipData(null);
19
- }
20
- if (active && payload?.length) {
21
- setLastTooltipData({
22
- active,
23
- payload: payload,
24
- label,
25
- });
26
- }
27
- // Don't clear lastTooltipData when mouse moves out - keep it for position maintenance
28
- // It will be cleared when label changes (above) or when new active data arrives
29
- }, [active, payload, label]);
30
- // Use last tooltip data if current is inactive, otherwise use current
31
- // lastTooltipData is already cleared in useEffect if label changed, so safe to use here
32
- active || (lastTooltipData?.active ?? false);
33
- const displayPayload = active && payload?.length
34
- ? payload
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 { 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,
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.66",
3
+ "version": "1.3.68",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -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?.filter(
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 { getPayloadConfigFromPayload } from '#uilib/components/ui/Chart/Chart.helpers';
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
- // Clear lastTooltipData immediately if label changed (prevents stale data)
47
- // Only clear if label is explicitly provided and different (not undefined)
48
- if (lastTooltipData && label && lastTooltipData.label !== label) {
49
- setLastTooltipData(null);
50
- }
51
-
52
- if (active && payload?.length) {
53
- setLastTooltipData({
54
- active,
55
- payload: payload as TooltipItem[],
56
- label,
57
- });
58
- }
59
- // Don't clear lastTooltipData when mouse moves out - keep it for position maintenance
60
- // It will be cleared when label changes (above) or when new active data arrives
61
- }, [active, payload, label]);
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 && payload?.length
68
- ? (payload as TooltipItem[])
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 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,