@asgard-js/react 0.0.43-canary.9 → 0.0.44-canary.1
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/README.md +45 -1
- package/dist/components/chatbot/api-key-input/api-key-input.d.ts.map +1 -1
- package/dist/components/chatbot/chatbot.d.ts +2 -3
- package/dist/components/chatbot/chatbot.d.ts.map +1 -1
- package/dist/context/asgard-service-context.d.ts +1 -1
- package/dist/context/asgard-service-context.d.ts.map +1 -1
- package/dist/context/asgard-theme-context.d.ts +2 -0
- package/dist/context/asgard-theme-context.d.ts.map +1 -1
- package/dist/hooks/use-channel.d.ts +1 -1
- package/dist/hooks/use-channel.d.ts.map +1 -1
- package/dist/hooks/use-on-screen-keyboard-scroll-fix.d.ts +1 -1
- package/dist/hooks/use-on-screen-keyboard-scroll-fix.d.ts.map +1 -1
- package/dist/{style.css → index.css} +1 -1
- package/dist/index.js +21396 -21771
- package/package.json +3 -3
- package/.babelrc +0 -12
- package/eslint.config.cjs +0 -12
- package/src/components/.DS_Store +0 -0
- package/src/components/chatbot/api-key-input/api-key-input.module.scss +0 -184
- package/src/components/chatbot/api-key-input/api-key-input.tsx +0 -129
- package/src/components/chatbot/api-key-input/index.ts +0 -1
- package/src/components/chatbot/chatbot-body/chatbot-body.module.scss +0 -13
- package/src/components/chatbot/chatbot-body/chatbot-body.tsx +0 -45
- package/src/components/chatbot/chatbot-body/conversation-message-renderer.tsx +0 -55
- package/src/components/chatbot/chatbot-body/index.ts +0 -1
- package/src/components/chatbot/chatbot-container/chatbot-container.module.scss +0 -41
- package/src/components/chatbot/chatbot-container/chatbot-container.tsx +0 -49
- package/src/components/chatbot/chatbot-container/chatbot-full-screen-container.tsx +0 -54
- package/src/components/chatbot/chatbot-footer/chatbot-footer.module.scss +0 -67
- package/src/components/chatbot/chatbot-footer/chatbot-footer.tsx +0 -140
- package/src/components/chatbot/chatbot-footer/index.ts +0 -1
- package/src/components/chatbot/chatbot-footer/speech-input-button.tsx +0 -132
- package/src/components/chatbot/chatbot-header/chatbot-header.module.scss +0 -48
- package/src/components/chatbot/chatbot-header/chatbot-header.tsx +0 -98
- package/src/components/chatbot/chatbot-header/index.ts +0 -1
- package/src/components/chatbot/chatbot.spec.tsx +0 -8
- package/src/components/chatbot/chatbot.tsx +0 -233
- package/src/components/chatbot/profile-icon.tsx +0 -26
- package/src/components/index.ts +0 -2
- package/src/components/templates/avatar/avatar.module.scss +0 -6
- package/src/components/templates/avatar/avatar.tsx +0 -28
- package/src/components/templates/avatar/index.ts +0 -1
- package/src/components/templates/button-template/button-template.module.scss +0 -0
- package/src/components/templates/button-template/button-template.tsx +0 -45
- package/src/components/templates/button-template/card.module.scss +0 -58
- package/src/components/templates/button-template/card.spec.tsx +0 -213
- package/src/components/templates/button-template/card.tsx +0 -123
- package/src/components/templates/button-template/index.ts +0 -1
- package/src/components/templates/carousel-template/carousel-template.module.scss +0 -15
- package/src/components/templates/carousel-template/carousel-template.tsx +0 -49
- package/src/components/templates/carousel-template/index.ts +0 -1
- package/src/components/templates/chart-template/chart-template.module.scss +0 -52
- package/src/components/templates/chart-template/chart-template.tsx +0 -75
- package/src/components/templates/chart-template/index.ts +0 -1
- package/src/components/templates/hint-template/hint-template.module.scss +0 -43
- package/src/components/templates/hint-template/hint-template.tsx +0 -76
- package/src/components/templates/hint-template/index.ts +0 -1
- package/src/components/templates/image-template/image-template.module.scss +0 -67
- package/src/components/templates/image-template/image-template.tsx +0 -58
- package/src/components/templates/image-template/index.ts +0 -1
- package/src/components/templates/index.ts +0 -10
- package/src/components/templates/quick-replies/index.ts +0 -1
- package/src/components/templates/quick-replies/quick-replies.module.scss +0 -16
- package/src/components/templates/quick-replies/quick-replies.tsx +0 -47
- package/src/components/templates/template-box/index.ts +0 -2
- package/src/components/templates/template-box/template-box-content.module.scss +0 -13
- package/src/components/templates/template-box/template-box-content.tsx +0 -30
- package/src/components/templates/template-box/template-box.module.scss +0 -19
- package/src/components/templates/template-box/template-box.tsx +0 -48
- package/src/components/templates/text-template/bot-typing-box.tsx +0 -81
- package/src/components/templates/text-template/bot-typing-placeholder.tsx +0 -28
- package/src/components/templates/text-template/index.ts +0 -3
- package/src/components/templates/text-template/text-template.module.scss +0 -131
- package/src/components/templates/text-template/text-template.tsx +0 -94
- package/src/components/templates/text-template/use-react-markdown-renderer.spec.tsx +0 -758
- package/src/components/templates/time/index.ts +0 -1
- package/src/components/templates/time/time.module.scss +0 -6
- package/src/components/templates/time/time.tsx +0 -34
- package/src/context/asgard-app-initialization-context.tsx +0 -154
- package/src/context/asgard-service-context.tsx +0 -148
- package/src/context/asgard-template-context.tsx +0 -83
- package/src/context/asgard-theme-context.tsx +0 -546
- package/src/context/index.ts +0 -4
- package/src/hooks/index.ts +0 -11
- package/src/hooks/use-asgard-service-client.ts +0 -68
- package/src/hooks/use-channel.ts +0 -160
- package/src/hooks/use-debounce.ts +0 -18
- package/src/hooks/use-deep-compare-memo.ts +0 -19
- package/src/hooks/use-is-on-screen-keyboard-open.ts +0 -43
- package/src/hooks/use-on-screen-keyboard-scroll-fix.ts +0 -15
- package/src/hooks/use-prevent-over-scrolling.ts +0 -77
- package/src/hooks/use-react-markdown-renderer.tsx +0 -278
- package/src/hooks/use-resize-observer.tsx +0 -27
- package/src/hooks/use-update-vh.ts +0 -30
- package/src/hooks/use-viewport-size.ts +0 -51
- package/src/icons/add_a_photo.svg +0 -3
- package/src/icons/bot.svg +0 -14
- package/src/icons/close.svg +0 -3
- package/src/icons/distance.svg +0 -3
- package/src/icons/mic.svg +0 -3
- package/src/icons/photo_library.svg +0 -3
- package/src/icons/profile.svg +0 -28
- package/src/icons/refresh.svg +0 -3
- package/src/icons/send.svg +0 -3
- package/src/icons/stop.svg +0 -22
- package/src/icons/volume_up.svg +0 -3
- package/src/index.ts +0 -4
- package/src/models/bot-provider.ts +0 -108
- package/src/styles/_index.scss +0 -1
- package/src/styles/_styles.scss +0 -11
- package/src/styles/colors/_colors.scss +0 -10
- package/src/styles/colors/_index.scss +0 -1
- package/src/styles/colors/_variables.scss +0 -72
- package/src/styles/palette/_index.scss +0 -1
- package/src/styles/palette/_palette.scss +0 -42
- package/src/styles/palette/_variables.scss +0 -40
- package/src/styles/radius/_index.scss +0 -1
- package/src/styles/radius/_radius.scss +0 -8
- package/src/styles/radius/_variables.scss +0 -12
- package/src/styles/spacing/_index.scss +0 -1
- package/src/styles/spacing/_spacing.scss +0 -8
- package/src/styles/spacing/_variables.scss +0 -13
- package/src/styles/utils/_index.scss +0 -1
- package/src/styles/utils/_map.scss +0 -22
- package/src/test-setup.ts +0 -1
- package/src/utils/color-utils.ts +0 -52
- package/src/utils/deep-merge.ts +0 -26
- package/src/utils/extractors.ts +0 -20
- package/src/utils/format-time.ts +0 -8
- package/src/utils/index.ts +0 -1
- package/src/utils/is.ts +0 -72
- package/src/utils/selectors.ts +0 -7
- package/src/utils/uri-validation.spec.ts +0 -208
- package/src/utils/uri-validation.ts +0 -103
- package/tsconfig.json +0 -16
- package/tsconfig.lib.json +0 -63
- package/tsconfig.spec.json +0 -36
- package/tsconfig.tsbuildinfo +0 -1
- package/vite.config.ts +0 -63
package/src/hooks/use-channel.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AsgardServiceClient,
|
|
3
|
-
Channel,
|
|
4
|
-
ChannelStates,
|
|
5
|
-
Conversation,
|
|
6
|
-
ConversationMessage,
|
|
7
|
-
EventType,
|
|
8
|
-
FetchSsePayload,
|
|
9
|
-
SseResponse,
|
|
10
|
-
} from '@asgard-js/core';
|
|
11
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
12
|
-
|
|
13
|
-
export interface UseChannelProps {
|
|
14
|
-
defaultIsOpen?: boolean;
|
|
15
|
-
resetPayload?: Pick<FetchSsePayload, 'text'> & Partial<Pick<FetchSsePayload, 'payload'>>;
|
|
16
|
-
client: AsgardServiceClient | null;
|
|
17
|
-
customChannelId: string;
|
|
18
|
-
customMessageId?: string;
|
|
19
|
-
initMessages?: ConversationMessage[];
|
|
20
|
-
onSseMessage?: (
|
|
21
|
-
response: SseResponse<EventType>,
|
|
22
|
-
context: {
|
|
23
|
-
conversation: Conversation | null;
|
|
24
|
-
}
|
|
25
|
-
) => void;
|
|
26
|
-
onAuthError?: (error: { isAuthError: boolean; isBotProviderError: boolean; errorDetail?: any }) => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface UseChannelReturn {
|
|
30
|
-
isOpen: boolean;
|
|
31
|
-
isResetting: boolean;
|
|
32
|
-
isConnecting: boolean;
|
|
33
|
-
conversation: Conversation | null;
|
|
34
|
-
sendMessage?: (payload: Pick<FetchSsePayload, 'text'> & Partial<Pick<FetchSsePayload, 'payload'>>) => void;
|
|
35
|
-
resetChannel?: (payload?: Pick<FetchSsePayload, 'text'> & Partial<Pick<FetchSsePayload, 'payload'>>) => void;
|
|
36
|
-
closeChannel?: () => void;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function useChannel(props: UseChannelProps): UseChannelReturn {
|
|
40
|
-
const {
|
|
41
|
-
client,
|
|
42
|
-
defaultIsOpen,
|
|
43
|
-
resetPayload,
|
|
44
|
-
customChannelId,
|
|
45
|
-
customMessageId,
|
|
46
|
-
initMessages,
|
|
47
|
-
onSseMessage,
|
|
48
|
-
onAuthError,
|
|
49
|
-
} = props;
|
|
50
|
-
|
|
51
|
-
if (!client) {
|
|
52
|
-
throw new Error('Client instance is required');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!customChannelId) {
|
|
56
|
-
throw new Error('Custom channel id is required');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const [channel, setChannel] = useState<Channel | null>(null);
|
|
60
|
-
const [isOpen, setIsOpen] = useState(defaultIsOpen ?? true);
|
|
61
|
-
const [isResetting, setIsResetting] = useState(false);
|
|
62
|
-
const [isConnecting, setIsConnecting] = useState(false);
|
|
63
|
-
const [conversation, setConversation] = useState<Conversation | null>(null);
|
|
64
|
-
|
|
65
|
-
const resetChannel = useCallback(
|
|
66
|
-
async (payload?: Pick<FetchSsePayload, 'text'> & Partial<Pick<FetchSsePayload, 'payload'>>) => {
|
|
67
|
-
const conversation = new Conversation({
|
|
68
|
-
messages: new Map(
|
|
69
|
-
initMessages?.map((message) => [message.messageId, message])
|
|
70
|
-
),
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
setIsResetting(true);
|
|
74
|
-
setIsConnecting(true);
|
|
75
|
-
setConversation(conversation);
|
|
76
|
-
|
|
77
|
-
const channel = await Channel.reset(
|
|
78
|
-
{
|
|
79
|
-
client,
|
|
80
|
-
customChannelId,
|
|
81
|
-
customMessageId,
|
|
82
|
-
conversation,
|
|
83
|
-
statesObserver: (states: ChannelStates): void => {
|
|
84
|
-
setIsConnecting(states.isConnecting);
|
|
85
|
-
setConversation(states.conversation);
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
payload,
|
|
89
|
-
{
|
|
90
|
-
onSseCompleted() {
|
|
91
|
-
setIsResetting(false);
|
|
92
|
-
},
|
|
93
|
-
onSseError(error) {
|
|
94
|
-
setIsResetting(false);
|
|
95
|
-
// Handle authentication and bot provider errors
|
|
96
|
-
if (error && typeof error === 'object' && ('isAuthError' in error || 'isBotProviderError' in error)) {
|
|
97
|
-
onAuthError?.(error as any);
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
onSseMessage(response: SseResponse<EventType>) {
|
|
101
|
-
onSseMessage?.(response, {
|
|
102
|
-
conversation,
|
|
103
|
-
});
|
|
104
|
-
},
|
|
105
|
-
}
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
setIsOpen(true);
|
|
109
|
-
setChannel(channel);
|
|
110
|
-
},
|
|
111
|
-
[client, customChannelId, customMessageId, initMessages, onSseMessage, onAuthError]
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
const closeChannel = useCallback(() => {
|
|
115
|
-
setChannel((prevChannel: Channel | null) => {
|
|
116
|
-
prevChannel?.close();
|
|
117
|
-
|
|
118
|
-
return null;
|
|
119
|
-
});
|
|
120
|
-
setIsOpen(false);
|
|
121
|
-
setIsResetting(false);
|
|
122
|
-
setIsConnecting(false);
|
|
123
|
-
setConversation(null);
|
|
124
|
-
}, []);
|
|
125
|
-
|
|
126
|
-
const sendMessage = useCallback(
|
|
127
|
-
(payload: Pick<FetchSsePayload, 'text'> & Partial<Pick<FetchSsePayload, 'payload'>>) =>
|
|
128
|
-
channel?.sendMessage({ ...payload, customMessageId }),
|
|
129
|
-
[channel, customMessageId]
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
if (!channel && isOpen) resetChannel(resetPayload);
|
|
134
|
-
}, [channel, isOpen, resetChannel, resetPayload]);
|
|
135
|
-
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
return (): void => closeChannel();
|
|
138
|
-
}, [closeChannel]);
|
|
139
|
-
|
|
140
|
-
return useMemo(
|
|
141
|
-
() => ({
|
|
142
|
-
isOpen,
|
|
143
|
-
isResetting,
|
|
144
|
-
isConnecting,
|
|
145
|
-
conversation,
|
|
146
|
-
sendMessage,
|
|
147
|
-
resetChannel,
|
|
148
|
-
closeChannel,
|
|
149
|
-
}),
|
|
150
|
-
[
|
|
151
|
-
isOpen,
|
|
152
|
-
isResetting,
|
|
153
|
-
isConnecting,
|
|
154
|
-
conversation,
|
|
155
|
-
sendMessage,
|
|
156
|
-
resetChannel,
|
|
157
|
-
closeChannel,
|
|
158
|
-
]
|
|
159
|
-
);
|
|
160
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
export function useDebounce<ValueType>(
|
|
4
|
-
value: ValueType,
|
|
5
|
-
delay?: number
|
|
6
|
-
): ValueType {
|
|
7
|
-
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
8
|
-
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
const handler = window.setTimeout(() => {
|
|
11
|
-
setDebouncedValue(value);
|
|
12
|
-
}, delay ?? 300);
|
|
13
|
-
|
|
14
|
-
return (): void => clearTimeout(handler);
|
|
15
|
-
}, [value, delay]);
|
|
16
|
-
|
|
17
|
-
return debouncedValue;
|
|
18
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { useRef } from 'react';
|
|
2
|
-
import { isEqual } from '../utils/is';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* useDeepCompareMemo: React hook that only recomputes the value when deps deeply change.
|
|
6
|
-
* @param factory - function to create the value
|
|
7
|
-
* @param deps - dependency array (deep compared)
|
|
8
|
-
*/
|
|
9
|
-
export function useDeepCompareMemo<T>(factory: () => T, deps: unknown[]): T {
|
|
10
|
-
const valueRef = useRef<T>();
|
|
11
|
-
const depsRef = useRef<unknown[]>();
|
|
12
|
-
|
|
13
|
-
if (!depsRef.current || !isEqual(depsRef.current, deps)) {
|
|
14
|
-
depsRef.current = deps;
|
|
15
|
-
valueRef.current = factory();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return valueRef.current as T;
|
|
19
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
function isKeyboardInput(elem: HTMLElement): boolean {
|
|
4
|
-
return (
|
|
5
|
-
(['INPUT', 'TEXTAREA'].includes(elem.tagName) &&
|
|
6
|
-
!['button', 'submit', 'checkbox', 'file', 'image'].includes(
|
|
7
|
-
(elem as HTMLInputElement).type
|
|
8
|
-
)) ||
|
|
9
|
-
elem.hasAttribute('contenteditable')
|
|
10
|
-
);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function useIsOnScreenKeyboardOpen(): boolean {
|
|
14
|
-
const [isOpen, setOpen] = useState(false);
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
function handleFocusIn(e: FocusEvent): void {
|
|
18
|
-
if (!e.target) return;
|
|
19
|
-
|
|
20
|
-
const target = e.target as HTMLElement;
|
|
21
|
-
|
|
22
|
-
if (isKeyboardInput(target)) setOpen(true);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function handleFocusOut(e: FocusEvent): void {
|
|
26
|
-
if (!e.target) return;
|
|
27
|
-
|
|
28
|
-
const target = e.target as HTMLElement;
|
|
29
|
-
|
|
30
|
-
if (isKeyboardInput(target)) setOpen(false);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
document.addEventListener('focusin', handleFocusIn);
|
|
34
|
-
document.addEventListener('focusout', handleFocusOut);
|
|
35
|
-
|
|
36
|
-
return (): void => {
|
|
37
|
-
document.removeEventListener('focusin', handleFocusIn);
|
|
38
|
-
document.removeEventListener('focusout', handleFocusOut);
|
|
39
|
-
};
|
|
40
|
-
}, []);
|
|
41
|
-
|
|
42
|
-
return isOpen;
|
|
43
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
export function useOnScreenKeyboardScrollFix(): void {
|
|
4
|
-
useEffect(() => {
|
|
5
|
-
function handleScroll(): void {
|
|
6
|
-
window.scrollTo(0, 0);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
window.addEventListener('scroll', handleScroll);
|
|
10
|
-
|
|
11
|
-
return (): void => {
|
|
12
|
-
window.removeEventListener('scroll', handleScroll);
|
|
13
|
-
};
|
|
14
|
-
}, []);
|
|
15
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { RefObject, useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
function findNearestScrollContainer(
|
|
4
|
-
elem: HTMLElement
|
|
5
|
-
): HTMLElement | undefined {
|
|
6
|
-
if (elem.scrollHeight > elem.offsetHeight) {
|
|
7
|
-
return elem;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const parent = elem.parentElement;
|
|
11
|
-
if (!parent) {
|
|
12
|
-
return undefined;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return findNearestScrollContainer(parent);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function usePreventOverScrolling(ref: RefObject<HTMLDivElement>): void {
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
const elem = ref.current;
|
|
21
|
-
|
|
22
|
-
if (!elem) return;
|
|
23
|
-
|
|
24
|
-
let startTouch: Touch | undefined = undefined;
|
|
25
|
-
|
|
26
|
-
function handleTouchStart(e: TouchEvent): void {
|
|
27
|
-
if (e.touches.length !== 1) return;
|
|
28
|
-
|
|
29
|
-
startTouch = e.touches[0];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function handleTouchMove(e: TouchEvent): void {
|
|
33
|
-
if (e.touches.length !== 1 || !startTouch) return;
|
|
34
|
-
|
|
35
|
-
const deltaY = startTouch.pageY - e.targetTouches[0].pageY;
|
|
36
|
-
const deltaX = startTouch.pageX - e.targetTouches[0].pageX;
|
|
37
|
-
|
|
38
|
-
if (Math.abs(deltaX) > Math.abs(deltaY)) return;
|
|
39
|
-
|
|
40
|
-
const target = e.target as HTMLElement;
|
|
41
|
-
const nearestScrollContainer = findNearestScrollContainer(target);
|
|
42
|
-
|
|
43
|
-
if (!nearestScrollContainer) {
|
|
44
|
-
e.preventDefault();
|
|
45
|
-
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const isScrollingUp = deltaY < 0;
|
|
50
|
-
const isAtTop = nearestScrollContainer.scrollTop === 0;
|
|
51
|
-
if (isScrollingUp && isAtTop) {
|
|
52
|
-
e.preventDefault();
|
|
53
|
-
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const isAtBottom =
|
|
58
|
-
nearestScrollContainer.scrollTop ===
|
|
59
|
-
nearestScrollContainer.scrollHeight -
|
|
60
|
-
nearestScrollContainer.clientHeight;
|
|
61
|
-
|
|
62
|
-
if (!isScrollingUp && isAtBottom) {
|
|
63
|
-
e.preventDefault();
|
|
64
|
-
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
elem.addEventListener('touchstart', handleTouchStart);
|
|
70
|
-
elem.addEventListener('touchmove', handleTouchMove);
|
|
71
|
-
|
|
72
|
-
return (): void => {
|
|
73
|
-
elem.removeEventListener('touchstart', handleTouchStart);
|
|
74
|
-
elem.removeEventListener('touchmove', handleTouchMove);
|
|
75
|
-
};
|
|
76
|
-
}, [ref]);
|
|
77
|
-
}
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ReactNode,
|
|
3
|
-
useCallback,
|
|
4
|
-
useEffect,
|
|
5
|
-
useMemo,
|
|
6
|
-
useRef,
|
|
7
|
-
useState,
|
|
8
|
-
} from 'react';
|
|
9
|
-
import ReactMarkdown from 'react-markdown';
|
|
10
|
-
import remarkGfm from 'remark-gfm';
|
|
11
|
-
import remarkMath from 'remark-math';
|
|
12
|
-
import rehypeHighlight from 'rehype-highlight';
|
|
13
|
-
import rehypeKatex from 'rehype-katex';
|
|
14
|
-
import 'katex/dist/katex.min.css';
|
|
15
|
-
import classes from '../components/templates/text-template/text-template.module.scss';
|
|
16
|
-
import { useAsgardTemplateContext } from '../context/asgard-template-context';
|
|
17
|
-
import { useAsgardThemeContext } from '../context/asgard-theme-context';
|
|
18
|
-
import { safeWindowOpen } from '../utils/uri-validation';
|
|
19
|
-
|
|
20
|
-
interface MarkdownRenderResult {
|
|
21
|
-
htmlBlocks: ReactNode;
|
|
22
|
-
lastTypingText: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type Token = {
|
|
26
|
-
raw: string;
|
|
27
|
-
type: string;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Maximum number of cached markdown blocks to prevent memory leaks
|
|
31
|
-
export const MAX_CACHE_SIZE = 100;
|
|
32
|
-
|
|
33
|
-
// Helper function to manage cache size with LRU eviction
|
|
34
|
-
export function manageCacheSize(cache: Map<string, ReactNode>): void {
|
|
35
|
-
if (cache.size >= MAX_CACHE_SIZE) {
|
|
36
|
-
// Remove the first (oldest) entry to make room for new ones
|
|
37
|
-
const firstKey = cache.keys().next().value;
|
|
38
|
-
if (firstKey) {
|
|
39
|
-
cache.delete(firstKey);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Enhanced completion detection with math expression support
|
|
45
|
-
function isCompleteParagraph(raw: string): boolean {
|
|
46
|
-
// Basic completion logic - must end with proper punctuation or newlines
|
|
47
|
-
// OR contain complete markdown elements
|
|
48
|
-
const hasMarkdownElements =
|
|
49
|
-
/^(#{1,6}\s|>\s|[-*+]\s|\d+\.\s|---+|```|\|.*\|)/m.test(raw.trim());
|
|
50
|
-
|
|
51
|
-
// Check for complete table structure (header row + separator + at least one data row)
|
|
52
|
-
const hasCompleteTable = /\|.*\|\s*\n\s*\|[-:\s|]+\|\s*\n\s*\|.*\|/m.test(
|
|
53
|
-
raw.trim()
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
const basicCompletion =
|
|
57
|
-
raw.endsWith('\n\n') ||
|
|
58
|
-
raw.endsWith('\n') ||
|
|
59
|
-
raw.endsWith('.') ||
|
|
60
|
-
raw.endsWith('。') ||
|
|
61
|
-
raw.endsWith('!') ||
|
|
62
|
-
raw.endsWith('!') ||
|
|
63
|
-
raw.endsWith('?') ||
|
|
64
|
-
hasMarkdownElements || // Has complete markdown elements
|
|
65
|
-
hasCompleteTable; // Has complete table structure
|
|
66
|
-
|
|
67
|
-
// Math-specific completion detection
|
|
68
|
-
// Check for complete math patterns (properly closed with $..$ or $$..$$)
|
|
69
|
-
const completeInlineMath = /\$[^$\s][^$]*\$/.test(raw);
|
|
70
|
-
const completeBlockMath = /\$\$[^$]*\$\$/.test(raw);
|
|
71
|
-
const hasCompleteMath = completeInlineMath || completeBlockMath;
|
|
72
|
-
|
|
73
|
-
const mathCompletion =
|
|
74
|
-
!raw.includes('$') || // No math expressions
|
|
75
|
-
hasCompleteMath; // Has complete math and no incomplete math
|
|
76
|
-
|
|
77
|
-
// Complete if: (basic completion AND math completion) OR complete block math
|
|
78
|
-
// OR if it's just a single token without newlines (treat as complete)
|
|
79
|
-
const isSimpleToken = !raw.includes('\n\n') && raw.trim().length > 0;
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
(basicCompletion && mathCompletion) || (isSimpleToken && mathCompletion)
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Custom table renderer to maintain current styling
|
|
87
|
-
const TableRenderer = ({ children, ...props }: React.ComponentProps<'table'>): ReactNode => (
|
|
88
|
-
<div className={classes.table_container}>
|
|
89
|
-
<table {...props}>{children}</table>
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
// Custom code renderer to maintain highlight.js classes exactly
|
|
94
|
-
const CodeRenderer = ({ children, className, ...props }: React.ComponentProps<'code'>): ReactNode => {
|
|
95
|
-
return (
|
|
96
|
-
<code className={`hljs ${className || ''}`} {...props}>
|
|
97
|
-
{children}
|
|
98
|
-
</code>
|
|
99
|
-
);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// Custom link renderer to integrate defaultLinkTarget prop and theme colors
|
|
103
|
-
const LinkRenderer = ({ children, href, ...props }: React.ComponentProps<'a'>): ReactNode => {
|
|
104
|
-
const { defaultLinkTarget } = useAsgardTemplateContext();
|
|
105
|
-
const { botMessage } = useAsgardThemeContext();
|
|
106
|
-
|
|
107
|
-
const handleClick = useCallback(
|
|
108
|
-
(e: React.MouseEvent) => {
|
|
109
|
-
e.preventDefault();
|
|
110
|
-
if (href) {
|
|
111
|
-
safeWindowOpen(href, defaultLinkTarget || '_blank');
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
[href, defaultLinkTarget]
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<a
|
|
119
|
-
href={href}
|
|
120
|
-
onClick={handleClick}
|
|
121
|
-
rel="noopener noreferrer"
|
|
122
|
-
style={{
|
|
123
|
-
color: botMessage?.linkColor || '#0066cc',
|
|
124
|
-
textDecoration: 'underline',
|
|
125
|
-
...props.style
|
|
126
|
-
}}
|
|
127
|
-
{...props}
|
|
128
|
-
>
|
|
129
|
-
{children}
|
|
130
|
-
</a>
|
|
131
|
-
);
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// Custom math renderers for inline and block math expressions
|
|
135
|
-
const InlineMathRenderer = ({ children, ...props }: React.ComponentProps<'span'>): ReactNode => (
|
|
136
|
-
<span className="math math-inline" {...props}>
|
|
137
|
-
{children}
|
|
138
|
-
</span>
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const BlockMathRenderer = ({ children, ...props }: React.ComponentProps<'div'>): ReactNode => (
|
|
142
|
-
<div className="math math-display" {...props}>
|
|
143
|
-
{children}
|
|
144
|
-
</div>
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
// Component renderers that maintain current styling and behavior
|
|
148
|
-
const components = {
|
|
149
|
-
table: TableRenderer,
|
|
150
|
-
code: CodeRenderer,
|
|
151
|
-
a: LinkRenderer,
|
|
152
|
-
math: InlineMathRenderer, // Inline math: $expression$
|
|
153
|
-
div: ({ className, ...props }: React.ComponentProps<'div'>): ReactNode => {
|
|
154
|
-
// Block math: $$expression$$
|
|
155
|
-
// Check for KaTeX display math classes
|
|
156
|
-
if (
|
|
157
|
-
className?.includes('math-display') ||
|
|
158
|
-
className?.includes('katex-display')
|
|
159
|
-
) {
|
|
160
|
-
return (
|
|
161
|
-
<BlockMathRenderer
|
|
162
|
-
className={`math math-display ${className || ''}`}
|
|
163
|
-
{...props}
|
|
164
|
-
>
|
|
165
|
-
{props.children}
|
|
166
|
-
</BlockMathRenderer>
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return <div className={className} {...props} />;
|
|
171
|
-
},
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
export function useMarkdownRenderer(
|
|
175
|
-
markdownText: string,
|
|
176
|
-
delay = 100
|
|
177
|
-
): MarkdownRenderResult {
|
|
178
|
-
const [blocks, setBlocks] = useState<ReactNode[]>([]);
|
|
179
|
-
const [typingText, setTypingText] = useState<string>('');
|
|
180
|
-
|
|
181
|
-
const cacheRef = useRef<Map<string, ReactNode>>(new Map());
|
|
182
|
-
|
|
183
|
-
const getRawText = useCallback((text: string): string => {
|
|
184
|
-
return text || '';
|
|
185
|
-
}, []);
|
|
186
|
-
|
|
187
|
-
// Mimic the exact token-based logic from current implementation
|
|
188
|
-
const parseToTokens = useCallback((text: string): Token[] => {
|
|
189
|
-
if (!text) return [];
|
|
190
|
-
|
|
191
|
-
// Simple tokenization - split by double newlines for paragraphs
|
|
192
|
-
// If there are no double newlines, treat the entire text as one token
|
|
193
|
-
const paragraphs = text.includes('\n\n') ? text.split(/\n\s*\n/) : [text];
|
|
194
|
-
|
|
195
|
-
return paragraphs.map((p) => ({
|
|
196
|
-
raw: p + (text.includes('\n\n') ? '\n\n' : ''),
|
|
197
|
-
type: 'paragraph',
|
|
198
|
-
}));
|
|
199
|
-
}, []);
|
|
200
|
-
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
if (!markdownText) {
|
|
203
|
-
setBlocks([]);
|
|
204
|
-
setTypingText('');
|
|
205
|
-
cacheRef.current.clear();
|
|
206
|
-
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const handler = setTimeout(() => {
|
|
211
|
-
const tokens = parseToTokens(markdownText);
|
|
212
|
-
if (tokens.length === 0) {
|
|
213
|
-
setBlocks([]);
|
|
214
|
-
setTypingText('');
|
|
215
|
-
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Find the last complete token
|
|
220
|
-
let lastCompleteIndex = -1;
|
|
221
|
-
|
|
222
|
-
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
223
|
-
const raw = getRawText(tokens[i].raw);
|
|
224
|
-
if (isCompleteParagraph(raw)) {
|
|
225
|
-
lastCompleteIndex = i;
|
|
226
|
-
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const finishedTokens = tokens.slice(0, lastCompleteIndex + 1);
|
|
232
|
-
const unprocessedTokens = tokens.slice(lastCompleteIndex + 1);
|
|
233
|
-
|
|
234
|
-
const newBlocks: ReactNode[] = [];
|
|
235
|
-
|
|
236
|
-
for (const token of finishedTokens) {
|
|
237
|
-
const raw = getRawText(token.raw);
|
|
238
|
-
const blockInCache = cacheRef.current.get(raw);
|
|
239
|
-
if (blockInCache) {
|
|
240
|
-
newBlocks.push(blockInCache);
|
|
241
|
-
} else {
|
|
242
|
-
const reactElement = (
|
|
243
|
-
<ReactMarkdown
|
|
244
|
-
key={raw}
|
|
245
|
-
remarkPlugins={[remarkGfm, remarkMath]}
|
|
246
|
-
rehypePlugins={[rehypeHighlight, rehypeKatex]}
|
|
247
|
-
components={components}
|
|
248
|
-
>
|
|
249
|
-
{raw.trim()}
|
|
250
|
-
</ReactMarkdown>
|
|
251
|
-
);
|
|
252
|
-
// Manage cache size before adding new entry
|
|
253
|
-
manageCacheSize(cacheRef.current);
|
|
254
|
-
cacheRef.current.set(raw, reactElement);
|
|
255
|
-
newBlocks.push(reactElement);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const lastRaw = unprocessedTokens
|
|
260
|
-
.map((t) => getRawText(t.raw))
|
|
261
|
-
.join('\n')
|
|
262
|
-
.trim();
|
|
263
|
-
setBlocks(newBlocks);
|
|
264
|
-
setTypingText(lastRaw);
|
|
265
|
-
}, delay);
|
|
266
|
-
|
|
267
|
-
return (): void => clearTimeout(handler);
|
|
268
|
-
}, [markdownText, delay, getRawText, parseToTokens]);
|
|
269
|
-
|
|
270
|
-
const htmlBlocks = useMemo<ReactNode>(() => {
|
|
271
|
-
return <div className={classes.md_container}>{blocks}</div>;
|
|
272
|
-
}, [blocks]);
|
|
273
|
-
|
|
274
|
-
return {
|
|
275
|
-
htmlBlocks,
|
|
276
|
-
lastTypingText: typingText,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { RefObject, useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
interface UseResizeObserverProps {
|
|
4
|
-
ref: RefObject<HTMLDivElement>;
|
|
5
|
-
onResize: (width: number, height: number) => void;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function useResizeObserver(props: UseResizeObserverProps): void {
|
|
9
|
-
const { ref, onResize } = props;
|
|
10
|
-
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
const resizeObserver = new ResizeObserver((entries) => {
|
|
13
|
-
for (const entry of entries) {
|
|
14
|
-
const { width, height } = entry.contentRect;
|
|
15
|
-
onResize(width, height);
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
if (ref.current) {
|
|
20
|
-
resizeObserver.observe(ref.current);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return (): void => {
|
|
24
|
-
resizeObserver.disconnect();
|
|
25
|
-
};
|
|
26
|
-
}, [ref, onResize]);
|
|
27
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { RefObject, useCallback, useEffect, useLayoutEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
const useBrowserLayoutEffect =
|
|
4
|
-
typeof window !== 'undefined' ? useLayoutEffect : null;
|
|
5
|
-
|
|
6
|
-
export function useUpdateVh(ref: RefObject<HTMLDivElement>): void {
|
|
7
|
-
const updateVh = useCallback(() => {
|
|
8
|
-
const vh = window.innerHeight * 0.01;
|
|
9
|
-
if (ref.current) {
|
|
10
|
-
ref.current.style.setProperty('--vh', `${vh}px`);
|
|
11
|
-
}
|
|
12
|
-
}, [ref]);
|
|
13
|
-
|
|
14
|
-
useBrowserLayoutEffect?.(updateVh, [updateVh]);
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
function effectTwice(): void {
|
|
18
|
-
updateVh();
|
|
19
|
-
setTimeout(updateVh, 1000);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
updateVh();
|
|
23
|
-
|
|
24
|
-
window.addEventListener('resize', effectTwice);
|
|
25
|
-
|
|
26
|
-
return (): void => {
|
|
27
|
-
window.removeEventListener('resize', effectTwice);
|
|
28
|
-
};
|
|
29
|
-
}, [updateVh]);
|
|
30
|
-
}
|