@sybilion/uilib 1.3.17 → 1.3.19
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/ChartAreaInteractive/ChartAreaInteractive.js +1 -1
- package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +2 -1
- package/dist/esm/components/ui/InteractionOverlay/InteractionOverlay.js +9 -6
- package/dist/esm/components/ui/InteractionOverlay/InteractionOverlay.styl.js +1 -1
- package/dist/esm/contexts/chat-context.js +35 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/types/src/components/ui/InteractionOverlay/InteractionOverlay.d.ts +3 -1
- package/dist/esm/types/src/contexts/chat-context.d.ts +8 -0
- package/package.json +1 -1
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +4 -1
- package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +2 -0
- package/src/components/ui/InteractionOverlay/InteractionOverlay.styl +19 -4
- package/src/components/ui/InteractionOverlay/InteractionOverlay.tsx +17 -5
- package/src/contexts/chat-context.tsx +42 -0
- package/src/docs/pages/ChartAreaInteractivePage.tsx +19 -1
|
@@ -111,7 +111,7 @@ function ChartAreaInteractive({ className, chartContainerClassName, legendClassN
|
|
|
111
111
|
return (jsx(BaseChartWrapper, { ...baseChartProps, chartClassName: cn(S.chartContainer, chartContainerClassName) }));
|
|
112
112
|
}
|
|
113
113
|
};
|
|
114
|
-
return (jsxs(InteractionOverlay, { className: cn(className, loading && S.loading), children: [(!disableTimeRangeSelector || headerActions) && (jsxs("div", { className: S.chartHeaderContainer, children: [!disableTimeRangeSelector && (jsx(TimeRangeControls, { timeRange: timeRange, onTimeRangeChange: onTimeRangeChange, loading: loading })), headerActions] })), jsx(TimeRangeBrushHost, { chartData: bridgedChartData, onTimeRangeChange: onTimeRangeChange, enabled: brushEnabled, layoutKey: chartRenderId ?? null, children: renderChart() })] }));
|
|
114
|
+
return (jsxs(InteractionOverlay, { className: cn(className, loading && S.loading), disabled: loading, children: [(!disableTimeRangeSelector || headerActions) && (jsxs("div", { className: S.chartHeaderContainer, children: [!disableTimeRangeSelector && (jsx(TimeRangeControls, { timeRange: timeRange, onTimeRangeChange: onTimeRangeChange, loading: loading })), headerActions] })), jsx(TimeRangeBrushHost, { chartData: bridgedChartData, onTimeRangeChange: onTimeRangeChange, enabled: brushEnabled, layoutKey: chartRenderId ?? null, children: renderChart() })] }));
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
export { ChartAreaInteractive, chartConfig };
|
|
@@ -4,7 +4,7 @@ import { MessageRole, GENERATING_DASHBOARD_SYSTEM_TEXT } from '../Chat.types.js'
|
|
|
4
4
|
import { isGraphIntakeAssistantStepComplete, matchUserTextToQuickReply, parseScriptLine, textHasQuickReplyMarkers, branchKeysUsedFromChatHistory, branchKeysUsedByUserMessages, extractQuickReplyLabelKeyPairsFromText, entryBranchKeyBeforeLastAssistant, isPresetScriptGraph, branchesFromPresetScriptGraph } from '../ChatMessage/presetScript.js';
|
|
5
5
|
import { buildChatSendMessagePayload, displayTextFromSendPayload } from '../buildChatSendMessagePayload.js';
|
|
6
6
|
import { usedPresetIdsFromMessages, formatChatTranscript } from '../chat-preset-utils.js';
|
|
7
|
-
import { useChatsForScopeId, useChat, useChatOutboundPending, isChatEmpty } from '../../../../contexts/chat-context.js';
|
|
7
|
+
import { useChatsForScopeId, useChat, useChatOutboundPending, useSyncChatPanelBusy, isChatEmpty } from '../../../../contexts/chat-context.js';
|
|
8
8
|
import useEvent from '../../../../hooks/useEvent.js';
|
|
9
9
|
import { useIsMobile } from '../../../../hooks/useIsMobile.js';
|
|
10
10
|
import { useQueryParams } from '../../../../hooks/useQueryParams.js';
|
|
@@ -31,6 +31,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
31
31
|
const chat = useChat(effectiveScopeId, currentChatId);
|
|
32
32
|
const isOutboundPending = useChatOutboundPending(effectiveScopeId, currentChatId);
|
|
33
33
|
const isLoading = isOutboundPending || localUiBusy;
|
|
34
|
+
useSyncChatPanelBusy(isLoading);
|
|
34
35
|
const { searchParams, addSearchParams, removeSearchParams, mutateSearchParams, } = useQueryParams();
|
|
35
36
|
const chatOpen = searchParams.has(CHAT_QUERY_PARAM);
|
|
36
37
|
const [isOpen, setIsOpen] = useState(false);
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
2
2
|
import cn from 'classnames';
|
|
3
3
|
import { useState, useCallback, useEffect } from 'react';
|
|
4
|
+
import { useIsChatLoading } from '../../../contexts/chat-context.js';
|
|
4
5
|
import { useIsTouchDevice } from '../../../hooks/useIsTouchDevice.js';
|
|
5
6
|
import S from './InteractionOverlay.styl.js';
|
|
6
7
|
|
|
7
|
-
function InteractionOverlay({ className, children, message = 'Touch to interact', }) {
|
|
8
|
+
function InteractionOverlay({ className, children, message = 'Touch to interact', disabled = false, }) {
|
|
8
9
|
const isTouchDevice = useIsTouchDevice();
|
|
10
|
+
const isChatLoading = useIsChatLoading();
|
|
11
|
+
const suppressOverlay = disabled || isChatLoading;
|
|
9
12
|
const [isVisible, setIsVisible] = useState(true);
|
|
10
13
|
const handleClick = useCallback(() => {
|
|
11
14
|
setIsVisible(false);
|
|
12
15
|
}, []);
|
|
13
16
|
useEffect(() => {
|
|
14
|
-
if (isTouchDevice) {
|
|
17
|
+
if (isTouchDevice && !suppressOverlay) {
|
|
15
18
|
setIsVisible(true);
|
|
16
19
|
}
|
|
17
|
-
}, [isTouchDevice]);
|
|
20
|
+
}, [isTouchDevice, suppressOverlay]);
|
|
18
21
|
useEffect(() => {
|
|
19
|
-
if (!isTouchDevice)
|
|
22
|
+
if (!isTouchDevice || suppressOverlay)
|
|
20
23
|
return;
|
|
21
24
|
const handleScroll = () => {
|
|
22
25
|
setIsVisible(true);
|
|
@@ -25,11 +28,11 @@ function InteractionOverlay({ className, children, message = 'Touch to interact'
|
|
|
25
28
|
return () => {
|
|
26
29
|
document.removeEventListener('scroll', handleScroll, { capture: true });
|
|
27
30
|
};
|
|
28
|
-
}, [isTouchDevice]);
|
|
31
|
+
}, [isTouchDevice, suppressOverlay]);
|
|
29
32
|
if (!isTouchDevice) {
|
|
30
33
|
return jsx(Fragment, { children: children });
|
|
31
34
|
}
|
|
32
|
-
return (jsxs("div", { className: cn(S.root, !isVisible && S.hidden, className), children: [children, jsx("button", { type: "button", className: S.overlay, onClick: handleClick, "aria-label": message, children: jsx("span", { className: S.message, children: message }) })] }));
|
|
35
|
+
return (jsxs("div", { className: cn(S.root, (!isVisible || suppressOverlay) && S.hidden, className), children: [children, jsx("button", { type: "button", className: S.overlay, onClick: handleClick, "aria-label": message, children: jsx("span", { className: S.message, children: message }) })] }));
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export { InteractionOverlay };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import styleInject from 'style-inject';
|
|
2
2
|
|
|
3
|
-
var css_248z = ".InteractionOverlay_root__9FV58{position:relative}.InteractionOverlay_overlay__Nwawr{background:transparent;border:none;box-shadow:inset 0 0 80px 40px var(--overlay-background-color);
|
|
3
|
+
var css_248z = ".InteractionOverlay_root__9FV58{position:relative}.InteractionOverlay_overlay__Nwawr{background:transparent;border:none;cursor:pointer;inset:0;margin:0;padding:0;position:absolute;touch-action:manipulation;z-index:4}.InteractionOverlay_overlay__Nwawr:before{background-color:color-mix(in srgb,var(--overlay-background-color) 70%,transparent);bottom:calc(var(--p-2)*-1);box-shadow:inset 0 0 80px 40px var(--overlay-background-color);content:\"\";left:calc(var(--p-2)*-1);pointer-events:none;position:absolute;right:calc(var(--p-2)*-1);top:calc(var(--p-2)*-1);transition:.2s ease-out;transition-property:box-shadow,background-color;z-index:-1}.InteractionOverlay_hidden__NRlgG .InteractionOverlay_overlay__Nwawr{pointer-events:none}.InteractionOverlay_hidden__NRlgG .InteractionOverlay_overlay__Nwawr:before{background-color:transparent;box-shadow:inset 0 0 0 0 var(--overlay-background-color)}.InteractionOverlay_message__oCuPR{backdrop-filter:blur(10px);background-color:color-mix(in srgb,var(--overlay-background-color) 80%,transparent);border:1px solid color-mix(in srgb,var(--foreground) 14%,transparent);border-radius:9999px;box-shadow:0 0 24px 10px var(--overlay-background-color),0 0 0 1px color-mix(in srgb,var(--overlay-background-color) 40%,transparent);color:var(--foreground);font-size:.875rem;font-weight:500;left:50%;line-height:1.25;padding:var(--p-3) var(--p-5);pointer-events:none;position:absolute;top:50%;transform:translate(-50%,-50%);transition:.2s ease-out;transition-property:transform,opacity;white-space:nowrap;z-index:1}.InteractionOverlay_hidden__NRlgG .InteractionOverlay_message__oCuPR{opacity:0;transform:translate(-50%,-50%) scale(.3)}:root{--overlay-background-color:var(--page-color)}";
|
|
4
4
|
var S = {"root":"InteractionOverlay_root__9FV58","overlay":"InteractionOverlay_overlay__Nwawr","hidden":"InteractionOverlay_hidden__NRlgG","message":"InteractionOverlay_message__oCuPR"};
|
|
5
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
@@ -92,6 +92,13 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
92
92
|
return loadChatsFromLS(userSwitchKey).currentChatId;
|
|
93
93
|
});
|
|
94
94
|
const [outboundPendingByKey, setOutboundPendingByKey] = useState({});
|
|
95
|
+
const [panelBusyCount, setPanelBusyCount] = useState(0);
|
|
96
|
+
const acquirePanelBusy = useCallback(() => {
|
|
97
|
+
setPanelBusyCount(count => count + 1);
|
|
98
|
+
}, []);
|
|
99
|
+
const releasePanelBusy = useCallback(() => {
|
|
100
|
+
setPanelBusyCount(count => Math.max(0, count - 1));
|
|
101
|
+
}, []);
|
|
95
102
|
const beginOutboundPending = useCallback((scopeId, chatSessionId) => {
|
|
96
103
|
const key = outboundPendingKey(scopeId, chatSessionId);
|
|
97
104
|
setOutboundPendingByKey(prev => ({
|
|
@@ -282,6 +289,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
282
289
|
setChats({});
|
|
283
290
|
setCurrentChatIdState({});
|
|
284
291
|
setOutboundPendingByKey({});
|
|
292
|
+
setPanelBusyCount(0);
|
|
285
293
|
return;
|
|
286
294
|
}
|
|
287
295
|
const loaded = loadChatsFromLS(userSwitchKey);
|
|
@@ -306,6 +314,9 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
306
314
|
getCurrentChatId,
|
|
307
315
|
deleteChat,
|
|
308
316
|
outboundPendingByKey,
|
|
317
|
+
panelBusyCount,
|
|
318
|
+
acquirePanelBusy,
|
|
319
|
+
releasePanelBusy,
|
|
309
320
|
}, children: children }));
|
|
310
321
|
}
|
|
311
322
|
const isChatEmpty = (chat) => chat?.messages.length === 0;
|
|
@@ -334,6 +345,29 @@ function useChatOutboundPending(scopeId, chatSessionId) {
|
|
|
334
345
|
return (outboundPendingByKey[key] ?? 0) > 0;
|
|
335
346
|
}, [scopeId, chatSessionId, outboundPendingByKey]);
|
|
336
347
|
}
|
|
348
|
+
/** True when chat is waiting on API or panel UI busy (safe outside ChatProvider). */
|
|
349
|
+
function useIsChatLoading() {
|
|
350
|
+
const context = useContext(ChatContext);
|
|
351
|
+
return useMemo(() => {
|
|
352
|
+
if (!context)
|
|
353
|
+
return false;
|
|
354
|
+
const outboundPending = Object.values(context.outboundPendingByKey).some(count => count > 0);
|
|
355
|
+
return outboundPending || context.panelBusyCount > 0;
|
|
356
|
+
}, [context?.outboundPendingByKey, context?.panelBusyCount]);
|
|
357
|
+
}
|
|
358
|
+
/** Syncs chat panel `isLoading` into context for touch-overlay suppression. */
|
|
359
|
+
function useSyncChatPanelBusy(isLoading) {
|
|
360
|
+
const acquirePanelBusy = useContext(ChatContext)?.acquirePanelBusy;
|
|
361
|
+
const releasePanelBusy = useContext(ChatContext)?.releasePanelBusy;
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
if (!isLoading || !acquirePanelBusy || !releasePanelBusy)
|
|
364
|
+
return;
|
|
365
|
+
acquirePanelBusy();
|
|
366
|
+
return () => {
|
|
367
|
+
releasePanelBusy();
|
|
368
|
+
};
|
|
369
|
+
}, [isLoading, acquirePanelBusy, releasePanelBusy]);
|
|
370
|
+
}
|
|
337
371
|
function useChatsForScopeId(scopeId) {
|
|
338
372
|
const { getChatsForScopeId, getCurrentChatId, setCurrentChatId, newChat, addMessage, removeMessageById, sendMessage, deleteChat, } = useChats();
|
|
339
373
|
const chats = getChatsForScopeId(scopeId);
|
|
@@ -363,4 +397,4 @@ function useCurrentChat(scopeId) {
|
|
|
363
397
|
return useChat(scopeId, chatId ?? undefined);
|
|
364
398
|
}
|
|
365
399
|
|
|
366
|
-
export { ChatContext, ChatProvider, isChatEmpty, outboundPendingKey, useChat, useChatOutboundPending, useChats, useChatsForDataset, useChatsForScopeId, useCurrentChat };
|
|
400
|
+
export { ChatContext, ChatProvider, isChatEmpty, outboundPendingKey, useChat, useChatOutboundPending, useChats, useChatsForDataset, useChatsForScopeId, useCurrentChat, useIsChatLoading, useSyncChatPanelBusy };
|
package/dist/esm/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme.js';
|
|
|
6
6
|
export { SybilionAuthProvider, createSybilionApiFetch, getSybilionApiOriginFromSdk, sybilionApiFetch, useSybilionApiFetch, useSybilionAuth } from './sybilion-auth/SybilionAuthProvider.js';
|
|
7
7
|
export { SYBILION_AUTH_LOGIN_PATH, normalizeApiBaseUrl } from './sybilion-auth/authPaths.js';
|
|
8
8
|
export { exchangeAuth0AccessTokenForSybilionJwt } from './sybilion-auth/exchangeSybilionToken.js';
|
|
9
|
-
export { ChatContext, ChatProvider, isChatEmpty, outboundPendingKey, useChat, useChatOutboundPending, useChats, useChatsForDataset, useChatsForScopeId, useCurrentChat } from './contexts/chat-context.js';
|
|
9
|
+
export { ChatContext, ChatProvider, isChatEmpty, outboundPendingKey, useChat, useChatOutboundPending, useChats, useChatsForDataset, useChatsForScopeId, useCurrentChat, useIsChatLoading, useSyncChatPanelBusy } from './contexts/chat-context.js';
|
|
10
10
|
export { AnalysesSelector } from './components/ui/AnalysesSelector/AnalysesSelector.js';
|
|
11
11
|
export { AnalysisLineIcon } from './components/ui/AnalysisLineIcon/AnalysisLineIcon.js';
|
|
12
12
|
export { AppHeaderHost, AppHeaderPortal } from './components/ui/AppHeader/AppHeader.js';
|
|
@@ -2,6 +2,8 @@ interface InteractionOverlayProps {
|
|
|
2
2
|
className?: string;
|
|
3
3
|
children?: React.ReactNode;
|
|
4
4
|
message?: string;
|
|
5
|
+
/** When true, hides the touch overlay (e.g. chart data loading). */
|
|
6
|
+
disabled?: boolean;
|
|
5
7
|
}
|
|
6
|
-
export declare function InteractionOverlay({ className, children, message, }: InteractionOverlayProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export declare function InteractionOverlay({ className, children, message, disabled, }: InteractionOverlayProps): import("react/jsx-runtime").JSX.Element;
|
|
7
9
|
export {};
|
|
@@ -21,6 +21,10 @@ export interface ChatContextType {
|
|
|
21
21
|
* `outboundPendingKey(scopeId, chatSessionId)` (session id at send start).
|
|
22
22
|
*/
|
|
23
23
|
outboundPendingByKey: Readonly<Record<string, number>>;
|
|
24
|
+
/** Ref-count of chat panels reporting UI busy (outbound wait, preset scripts, etc.). */
|
|
25
|
+
panelBusyCount: number;
|
|
26
|
+
acquirePanelBusy: () => void;
|
|
27
|
+
releasePanelBusy: () => void;
|
|
24
28
|
}
|
|
25
29
|
declare const ChatContext: import("react").Context<ChatContextType>;
|
|
26
30
|
/** Stable composite key; avoids collisions if `scopeId` contains `:`. */
|
|
@@ -36,6 +40,10 @@ export declare const isChatEmpty: (chat: Chat | null) => boolean;
|
|
|
36
40
|
export declare function useChats(): ChatContextType;
|
|
37
41
|
export declare function useChat(scopeId: string | undefined, chatId: string | undefined): Chat | null;
|
|
38
42
|
export declare function useChatOutboundPending(scopeId: string | undefined | null, chatSessionId: string | null | undefined): boolean;
|
|
43
|
+
/** True when chat is waiting on API or panel UI busy (safe outside ChatProvider). */
|
|
44
|
+
export declare function useIsChatLoading(): boolean;
|
|
45
|
+
/** Syncs chat panel `isLoading` into context for touch-overlay suppression. */
|
|
46
|
+
export declare function useSyncChatPanelBusy(isLoading: boolean): void;
|
|
39
47
|
export declare function useChatsForScopeId(scopeId: string): {
|
|
40
48
|
chats: Chat[];
|
|
41
49
|
currentChat: Chat;
|
package/package.json
CHANGED
|
@@ -231,7 +231,10 @@ export function ChartAreaInteractive({
|
|
|
231
231
|
};
|
|
232
232
|
|
|
233
233
|
return (
|
|
234
|
-
<InteractionOverlay
|
|
234
|
+
<InteractionOverlay
|
|
235
|
+
className={cn(className, loading && S.loading)}
|
|
236
|
+
disabled={loading}
|
|
237
|
+
>
|
|
235
238
|
{(!disableTimeRangeSelector || headerActions) && (
|
|
236
239
|
<div className={S.chartHeaderContainer}>
|
|
237
240
|
{!disableTimeRangeSelector && (
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
useChat,
|
|
33
33
|
useChatOutboundPending,
|
|
34
34
|
useChatsForScopeId,
|
|
35
|
+
useSyncChatPanelBusy,
|
|
35
36
|
} from '#uilib/contexts/chat-context';
|
|
36
37
|
import useEvent from '#uilib/hooks/useEvent';
|
|
37
38
|
import { useIsMobile } from '#uilib/hooks/useIsMobile';
|
|
@@ -144,6 +145,7 @@ export function useChatPanelChromeModel({
|
|
|
144
145
|
currentChatId,
|
|
145
146
|
);
|
|
146
147
|
const isLoading = isOutboundPending || localUiBusy;
|
|
148
|
+
useSyncChatPanelBusy(isLoading);
|
|
147
149
|
|
|
148
150
|
const {
|
|
149
151
|
searchParams,
|
|
@@ -11,13 +11,28 @@
|
|
|
11
11
|
cursor pointer
|
|
12
12
|
touch-action manipulation
|
|
13
13
|
background transparent
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
&::before
|
|
16
|
+
content ''
|
|
17
|
+
position absolute
|
|
18
|
+
top calc(-1 * var(--p-2))
|
|
19
|
+
right calc(-1 * var(--p-2))
|
|
20
|
+
bottom calc(-1 * var(--p-2))
|
|
21
|
+
left calc(-1 * var(--p-2))
|
|
22
|
+
background-color unquote('color-mix(in srgb, var(--overlay-background-color) 70%, transparent)')
|
|
23
|
+
box-shadow inset 0 0 80px 40px var(--overlay-background-color)
|
|
24
|
+
pointer-events none
|
|
25
|
+
z-index -1
|
|
26
|
+
transition 200ms ease-out
|
|
27
|
+
transition-property box-shadow, background-color
|
|
16
28
|
|
|
17
29
|
.hidden &
|
|
18
|
-
box-shadow inset 0 0 0 0 var(--overlay-background-color)
|
|
19
30
|
pointer-events none
|
|
20
31
|
|
|
32
|
+
&::before
|
|
33
|
+
box-shadow inset 0 0 0 0 var(--overlay-background-color)
|
|
34
|
+
background-color transparent
|
|
35
|
+
|
|
21
36
|
.message
|
|
22
37
|
position absolute
|
|
23
38
|
top 50%
|
|
@@ -33,7 +48,7 @@
|
|
|
33
48
|
padding var(--p-3) var(--p-5)
|
|
34
49
|
border-radius 9999px
|
|
35
50
|
border 1px solid unquote('color-mix(in srgb, var(--foreground) 14%, transparent)')
|
|
36
|
-
background-color unquote('color-mix(in srgb, var(--overlay-background-color)
|
|
51
|
+
background-color unquote('color-mix(in srgb, var(--overlay-background-color) 80%, transparent)')
|
|
37
52
|
backdrop-filter blur(10px)
|
|
38
53
|
box-shadow \
|
|
39
54
|
0 0 24px 10px var(--overlay-background-color), \
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import cn from 'classnames';
|
|
2
2
|
import { useCallback, useEffect, useState } from 'react';
|
|
3
3
|
|
|
4
|
+
import { useIsChatLoading } from '#uilib/contexts/chat-context';
|
|
4
5
|
import { useIsTouchDevice } from '#uilib/hooks/useIsTouchDevice';
|
|
5
6
|
|
|
6
7
|
import S from './InteractionOverlay.styl';
|
|
@@ -9,14 +10,19 @@ interface InteractionOverlayProps {
|
|
|
9
10
|
className?: string;
|
|
10
11
|
children?: React.ReactNode;
|
|
11
12
|
message?: string;
|
|
13
|
+
/** When true, hides the touch overlay (e.g. chart data loading). */
|
|
14
|
+
disabled?: boolean;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export function InteractionOverlay({
|
|
15
18
|
className,
|
|
16
19
|
children,
|
|
17
20
|
message = 'Touch to interact',
|
|
21
|
+
disabled = false,
|
|
18
22
|
}: InteractionOverlayProps) {
|
|
19
23
|
const isTouchDevice = useIsTouchDevice();
|
|
24
|
+
const isChatLoading = useIsChatLoading();
|
|
25
|
+
const suppressOverlay = disabled || isChatLoading;
|
|
20
26
|
const [isVisible, setIsVisible] = useState(true);
|
|
21
27
|
|
|
22
28
|
const handleClick = useCallback(() => {
|
|
@@ -24,13 +30,13 @@ export function InteractionOverlay({
|
|
|
24
30
|
}, []);
|
|
25
31
|
|
|
26
32
|
useEffect(() => {
|
|
27
|
-
if (isTouchDevice) {
|
|
33
|
+
if (isTouchDevice && !suppressOverlay) {
|
|
28
34
|
setIsVisible(true);
|
|
29
35
|
}
|
|
30
|
-
}, [isTouchDevice]);
|
|
36
|
+
}, [isTouchDevice, suppressOverlay]);
|
|
31
37
|
|
|
32
38
|
useEffect(() => {
|
|
33
|
-
if (!isTouchDevice) return;
|
|
39
|
+
if (!isTouchDevice || suppressOverlay) return;
|
|
34
40
|
|
|
35
41
|
const handleScroll = () => {
|
|
36
42
|
setIsVisible(true);
|
|
@@ -40,14 +46,20 @@ export function InteractionOverlay({
|
|
|
40
46
|
return () => {
|
|
41
47
|
document.removeEventListener('scroll', handleScroll, { capture: true });
|
|
42
48
|
};
|
|
43
|
-
}, [isTouchDevice]);
|
|
49
|
+
}, [isTouchDevice, suppressOverlay]);
|
|
44
50
|
|
|
45
51
|
if (!isTouchDevice) {
|
|
46
52
|
return <>{children}</>;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
return (
|
|
50
|
-
<div
|
|
56
|
+
<div
|
|
57
|
+
className={cn(
|
|
58
|
+
S.root,
|
|
59
|
+
(!isVisible || suppressOverlay) && S.hidden,
|
|
60
|
+
className,
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
51
63
|
{children}
|
|
52
64
|
<button
|
|
53
65
|
type="button"
|
|
@@ -66,6 +66,10 @@ export interface ChatContextType {
|
|
|
66
66
|
* `outboundPendingKey(scopeId, chatSessionId)` (session id at send start).
|
|
67
67
|
*/
|
|
68
68
|
outboundPendingByKey: Readonly<Record<string, number>>;
|
|
69
|
+
/** Ref-count of chat panels reporting UI busy (outbound wait, preset scripts, etc.). */
|
|
70
|
+
panelBusyCount: number;
|
|
71
|
+
acquirePanelBusy: () => void;
|
|
72
|
+
releasePanelBusy: () => void;
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
const ChatContext = createContext<ChatContextType | undefined>(undefined);
|
|
@@ -175,6 +179,15 @@ export function ChatProvider({
|
|
|
175
179
|
const [outboundPendingByKey, setOutboundPendingByKey] = useState<
|
|
176
180
|
Record<string, number>
|
|
177
181
|
>({});
|
|
182
|
+
const [panelBusyCount, setPanelBusyCount] = useState(0);
|
|
183
|
+
|
|
184
|
+
const acquirePanelBusy = useCallback(() => {
|
|
185
|
+
setPanelBusyCount(count => count + 1);
|
|
186
|
+
}, []);
|
|
187
|
+
|
|
188
|
+
const releasePanelBusy = useCallback(() => {
|
|
189
|
+
setPanelBusyCount(count => Math.max(0, count - 1));
|
|
190
|
+
}, []);
|
|
178
191
|
|
|
179
192
|
const beginOutboundPending = useCallback(
|
|
180
193
|
(scopeId: string, chatSessionId: string) => {
|
|
@@ -444,6 +457,7 @@ export function ChatProvider({
|
|
|
444
457
|
setChats({});
|
|
445
458
|
setCurrentChatIdState({});
|
|
446
459
|
setOutboundPendingByKey({});
|
|
460
|
+
setPanelBusyCount(0);
|
|
447
461
|
return;
|
|
448
462
|
}
|
|
449
463
|
|
|
@@ -475,6 +489,9 @@ export function ChatProvider({
|
|
|
475
489
|
getCurrentChatId,
|
|
476
490
|
deleteChat,
|
|
477
491
|
outboundPendingByKey,
|
|
492
|
+
panelBusyCount,
|
|
493
|
+
acquirePanelBusy,
|
|
494
|
+
releasePanelBusy,
|
|
478
495
|
}}
|
|
479
496
|
>
|
|
480
497
|
{children}
|
|
@@ -519,6 +536,31 @@ export function useChatOutboundPending(
|
|
|
519
536
|
}, [scopeId, chatSessionId, outboundPendingByKey]);
|
|
520
537
|
}
|
|
521
538
|
|
|
539
|
+
/** True when chat is waiting on API or panel UI busy (safe outside ChatProvider). */
|
|
540
|
+
export function useIsChatLoading(): boolean {
|
|
541
|
+
const context = useContext(ChatContext);
|
|
542
|
+
return useMemo(() => {
|
|
543
|
+
if (!context) return false;
|
|
544
|
+
const outboundPending = Object.values(context.outboundPendingByKey).some(
|
|
545
|
+
count => count > 0,
|
|
546
|
+
);
|
|
547
|
+
return outboundPending || context.panelBusyCount > 0;
|
|
548
|
+
}, [context?.outboundPendingByKey, context?.panelBusyCount]);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Syncs chat panel `isLoading` into context for touch-overlay suppression. */
|
|
552
|
+
export function useSyncChatPanelBusy(isLoading: boolean): void {
|
|
553
|
+
const acquirePanelBusy = useContext(ChatContext)?.acquirePanelBusy;
|
|
554
|
+
const releasePanelBusy = useContext(ChatContext)?.releasePanelBusy;
|
|
555
|
+
useEffect(() => {
|
|
556
|
+
if (!isLoading || !acquirePanelBusy || !releasePanelBusy) return;
|
|
557
|
+
acquirePanelBusy();
|
|
558
|
+
return () => {
|
|
559
|
+
releasePanelBusy();
|
|
560
|
+
};
|
|
561
|
+
}, [isLoading, acquirePanelBusy, releasePanelBusy]);
|
|
562
|
+
}
|
|
563
|
+
|
|
522
564
|
export function useChatsForScopeId(scopeId: string) {
|
|
523
565
|
const {
|
|
524
566
|
getChatsForScopeId,
|
|
@@ -6,7 +6,9 @@ import type {
|
|
|
6
6
|
OverlayMode,
|
|
7
7
|
} from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
8
8
|
import { ForecastItemData } from '#uilib/components/ui/ChartAreaInteractive/ChartLines';
|
|
9
|
+
import { Label } from '#uilib/components/ui/Label';
|
|
9
10
|
import { PageContentSection } from '#uilib/components/ui/Page';
|
|
11
|
+
import { Switch } from '#uilib/components/ui/Switch';
|
|
10
12
|
import { Tabs, TabsList, TabsTrigger } from '#uilib/components/ui/Tabs';
|
|
11
13
|
import { useTheme } from '#uilib/contexts/theme-context';
|
|
12
14
|
import type { ForecastData } from '#uilib/types/forecast-data';
|
|
@@ -102,6 +104,7 @@ export default function ChartAreaInteractivePage() {
|
|
|
102
104
|
string | null
|
|
103
105
|
>('0.9');
|
|
104
106
|
const [upperThreshold, setUpperThreshold] = useState(15);
|
|
107
|
+
const [loading, setLoading] = useState(false);
|
|
105
108
|
|
|
106
109
|
const toggleLegendSeries = useCallback((key: string) => {
|
|
107
110
|
setHidden(prev => {
|
|
@@ -155,6 +158,21 @@ export default function ChartAreaInteractivePage() {
|
|
|
155
158
|
<TabsTrigger value="thresholds">Thresholds</TabsTrigger>
|
|
156
159
|
</TabsList>
|
|
157
160
|
</Tabs>
|
|
161
|
+
<div
|
|
162
|
+
style={{
|
|
163
|
+
display: 'flex',
|
|
164
|
+
alignItems: 'center',
|
|
165
|
+
gap: 8,
|
|
166
|
+
marginBottom: 16,
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<Switch
|
|
170
|
+
id="chart-loading"
|
|
171
|
+
checked={loading}
|
|
172
|
+
onCheckedChange={setLoading}
|
|
173
|
+
/>
|
|
174
|
+
<Label htmlFor="chart-loading">Loading</Label>
|
|
175
|
+
</div>
|
|
158
176
|
<ChartAreaInteractive
|
|
159
177
|
timeRange={timeRange}
|
|
160
178
|
onTimeRangeChange={setTimeRange}
|
|
@@ -163,7 +181,7 @@ export default function ChartAreaInteractivePage() {
|
|
|
163
181
|
mode={mode}
|
|
164
182
|
chartData={chartData}
|
|
165
183
|
forecastData={DEMO_FORECAST_ITEMS}
|
|
166
|
-
loading={
|
|
184
|
+
loading={loading}
|
|
167
185
|
isDarkTheme={isDarkMode}
|
|
168
186
|
toggleLegendSeries={toggleLegendSeries}
|
|
169
187
|
ensureAnalysisSeriesVisible={ensureAnalysisSeriesVisible}
|