@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.
Files changed (139) hide show
  1. package/README.md +45 -1
  2. package/dist/components/chatbot/api-key-input/api-key-input.d.ts.map +1 -1
  3. package/dist/components/chatbot/chatbot.d.ts +2 -3
  4. package/dist/components/chatbot/chatbot.d.ts.map +1 -1
  5. package/dist/context/asgard-service-context.d.ts +1 -1
  6. package/dist/context/asgard-service-context.d.ts.map +1 -1
  7. package/dist/context/asgard-theme-context.d.ts +2 -0
  8. package/dist/context/asgard-theme-context.d.ts.map +1 -1
  9. package/dist/hooks/use-channel.d.ts +1 -1
  10. package/dist/hooks/use-channel.d.ts.map +1 -1
  11. package/dist/hooks/use-on-screen-keyboard-scroll-fix.d.ts +1 -1
  12. package/dist/hooks/use-on-screen-keyboard-scroll-fix.d.ts.map +1 -1
  13. package/dist/{style.css → index.css} +1 -1
  14. package/dist/index.js +21396 -21771
  15. package/package.json +3 -3
  16. package/.babelrc +0 -12
  17. package/eslint.config.cjs +0 -12
  18. package/src/components/.DS_Store +0 -0
  19. package/src/components/chatbot/api-key-input/api-key-input.module.scss +0 -184
  20. package/src/components/chatbot/api-key-input/api-key-input.tsx +0 -129
  21. package/src/components/chatbot/api-key-input/index.ts +0 -1
  22. package/src/components/chatbot/chatbot-body/chatbot-body.module.scss +0 -13
  23. package/src/components/chatbot/chatbot-body/chatbot-body.tsx +0 -45
  24. package/src/components/chatbot/chatbot-body/conversation-message-renderer.tsx +0 -55
  25. package/src/components/chatbot/chatbot-body/index.ts +0 -1
  26. package/src/components/chatbot/chatbot-container/chatbot-container.module.scss +0 -41
  27. package/src/components/chatbot/chatbot-container/chatbot-container.tsx +0 -49
  28. package/src/components/chatbot/chatbot-container/chatbot-full-screen-container.tsx +0 -54
  29. package/src/components/chatbot/chatbot-footer/chatbot-footer.module.scss +0 -67
  30. package/src/components/chatbot/chatbot-footer/chatbot-footer.tsx +0 -140
  31. package/src/components/chatbot/chatbot-footer/index.ts +0 -1
  32. package/src/components/chatbot/chatbot-footer/speech-input-button.tsx +0 -132
  33. package/src/components/chatbot/chatbot-header/chatbot-header.module.scss +0 -48
  34. package/src/components/chatbot/chatbot-header/chatbot-header.tsx +0 -98
  35. package/src/components/chatbot/chatbot-header/index.ts +0 -1
  36. package/src/components/chatbot/chatbot.spec.tsx +0 -8
  37. package/src/components/chatbot/chatbot.tsx +0 -233
  38. package/src/components/chatbot/profile-icon.tsx +0 -26
  39. package/src/components/index.ts +0 -2
  40. package/src/components/templates/avatar/avatar.module.scss +0 -6
  41. package/src/components/templates/avatar/avatar.tsx +0 -28
  42. package/src/components/templates/avatar/index.ts +0 -1
  43. package/src/components/templates/button-template/button-template.module.scss +0 -0
  44. package/src/components/templates/button-template/button-template.tsx +0 -45
  45. package/src/components/templates/button-template/card.module.scss +0 -58
  46. package/src/components/templates/button-template/card.spec.tsx +0 -213
  47. package/src/components/templates/button-template/card.tsx +0 -123
  48. package/src/components/templates/button-template/index.ts +0 -1
  49. package/src/components/templates/carousel-template/carousel-template.module.scss +0 -15
  50. package/src/components/templates/carousel-template/carousel-template.tsx +0 -49
  51. package/src/components/templates/carousel-template/index.ts +0 -1
  52. package/src/components/templates/chart-template/chart-template.module.scss +0 -52
  53. package/src/components/templates/chart-template/chart-template.tsx +0 -75
  54. package/src/components/templates/chart-template/index.ts +0 -1
  55. package/src/components/templates/hint-template/hint-template.module.scss +0 -43
  56. package/src/components/templates/hint-template/hint-template.tsx +0 -76
  57. package/src/components/templates/hint-template/index.ts +0 -1
  58. package/src/components/templates/image-template/image-template.module.scss +0 -67
  59. package/src/components/templates/image-template/image-template.tsx +0 -58
  60. package/src/components/templates/image-template/index.ts +0 -1
  61. package/src/components/templates/index.ts +0 -10
  62. package/src/components/templates/quick-replies/index.ts +0 -1
  63. package/src/components/templates/quick-replies/quick-replies.module.scss +0 -16
  64. package/src/components/templates/quick-replies/quick-replies.tsx +0 -47
  65. package/src/components/templates/template-box/index.ts +0 -2
  66. package/src/components/templates/template-box/template-box-content.module.scss +0 -13
  67. package/src/components/templates/template-box/template-box-content.tsx +0 -30
  68. package/src/components/templates/template-box/template-box.module.scss +0 -19
  69. package/src/components/templates/template-box/template-box.tsx +0 -48
  70. package/src/components/templates/text-template/bot-typing-box.tsx +0 -81
  71. package/src/components/templates/text-template/bot-typing-placeholder.tsx +0 -28
  72. package/src/components/templates/text-template/index.ts +0 -3
  73. package/src/components/templates/text-template/text-template.module.scss +0 -131
  74. package/src/components/templates/text-template/text-template.tsx +0 -94
  75. package/src/components/templates/text-template/use-react-markdown-renderer.spec.tsx +0 -758
  76. package/src/components/templates/time/index.ts +0 -1
  77. package/src/components/templates/time/time.module.scss +0 -6
  78. package/src/components/templates/time/time.tsx +0 -34
  79. package/src/context/asgard-app-initialization-context.tsx +0 -154
  80. package/src/context/asgard-service-context.tsx +0 -148
  81. package/src/context/asgard-template-context.tsx +0 -83
  82. package/src/context/asgard-theme-context.tsx +0 -546
  83. package/src/context/index.ts +0 -4
  84. package/src/hooks/index.ts +0 -11
  85. package/src/hooks/use-asgard-service-client.ts +0 -68
  86. package/src/hooks/use-channel.ts +0 -160
  87. package/src/hooks/use-debounce.ts +0 -18
  88. package/src/hooks/use-deep-compare-memo.ts +0 -19
  89. package/src/hooks/use-is-on-screen-keyboard-open.ts +0 -43
  90. package/src/hooks/use-on-screen-keyboard-scroll-fix.ts +0 -15
  91. package/src/hooks/use-prevent-over-scrolling.ts +0 -77
  92. package/src/hooks/use-react-markdown-renderer.tsx +0 -278
  93. package/src/hooks/use-resize-observer.tsx +0 -27
  94. package/src/hooks/use-update-vh.ts +0 -30
  95. package/src/hooks/use-viewport-size.ts +0 -51
  96. package/src/icons/add_a_photo.svg +0 -3
  97. package/src/icons/bot.svg +0 -14
  98. package/src/icons/close.svg +0 -3
  99. package/src/icons/distance.svg +0 -3
  100. package/src/icons/mic.svg +0 -3
  101. package/src/icons/photo_library.svg +0 -3
  102. package/src/icons/profile.svg +0 -28
  103. package/src/icons/refresh.svg +0 -3
  104. package/src/icons/send.svg +0 -3
  105. package/src/icons/stop.svg +0 -22
  106. package/src/icons/volume_up.svg +0 -3
  107. package/src/index.ts +0 -4
  108. package/src/models/bot-provider.ts +0 -108
  109. package/src/styles/_index.scss +0 -1
  110. package/src/styles/_styles.scss +0 -11
  111. package/src/styles/colors/_colors.scss +0 -10
  112. package/src/styles/colors/_index.scss +0 -1
  113. package/src/styles/colors/_variables.scss +0 -72
  114. package/src/styles/palette/_index.scss +0 -1
  115. package/src/styles/palette/_palette.scss +0 -42
  116. package/src/styles/palette/_variables.scss +0 -40
  117. package/src/styles/radius/_index.scss +0 -1
  118. package/src/styles/radius/_radius.scss +0 -8
  119. package/src/styles/radius/_variables.scss +0 -12
  120. package/src/styles/spacing/_index.scss +0 -1
  121. package/src/styles/spacing/_spacing.scss +0 -8
  122. package/src/styles/spacing/_variables.scss +0 -13
  123. package/src/styles/utils/_index.scss +0 -1
  124. package/src/styles/utils/_map.scss +0 -22
  125. package/src/test-setup.ts +0 -1
  126. package/src/utils/color-utils.ts +0 -52
  127. package/src/utils/deep-merge.ts +0 -26
  128. package/src/utils/extractors.ts +0 -20
  129. package/src/utils/format-time.ts +0 -8
  130. package/src/utils/index.ts +0 -1
  131. package/src/utils/is.ts +0 -72
  132. package/src/utils/selectors.ts +0 -7
  133. package/src/utils/uri-validation.spec.ts +0 -208
  134. package/src/utils/uri-validation.ts +0 -103
  135. package/tsconfig.json +0 -16
  136. package/tsconfig.lib.json +0 -63
  137. package/tsconfig.spec.json +0 -36
  138. package/tsconfig.tsbuildinfo +0 -1
  139. package/vite.config.ts +0 -63
@@ -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
- }