@asgard-js/react 0.0.40 → 0.0.41-canary.2

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 (137) hide show
  1. package/dist/components/chatbot/chatbot-body/conversation-message-renderer.d.ts.map +1 -1
  2. package/dist/components/chatbot/chatbot-footer/chatbot-footer.d.ts.map +1 -1
  3. package/dist/components/templates/index.d.ts +1 -0
  4. package/dist/components/templates/index.d.ts.map +1 -1
  5. package/dist/components/templates/user-image-template/index.d.ts +2 -0
  6. package/dist/components/templates/user-image-template/index.d.ts.map +1 -0
  7. package/dist/components/templates/user-image-template/user-image-template.d.ts +12 -0
  8. package/dist/components/templates/user-image-template/user-image-template.d.ts.map +1 -0
  9. package/dist/context/asgard-service-context.d.ts +1 -0
  10. package/dist/context/asgard-service-context.d.ts.map +1 -1
  11. package/dist/hooks/use-channel.d.ts +1 -1
  12. package/dist/hooks/use-channel.d.ts.map +1 -1
  13. package/dist/index.js +17672 -18151
  14. package/dist/style.css +1 -1
  15. package/dist/utils/file-validation.d.ts +12 -0
  16. package/dist/utils/file-validation.d.ts.map +1 -0
  17. package/package.json +2 -2
  18. package/.babelrc +0 -12
  19. package/eslint.config.cjs +0 -12
  20. package/src/components/chatbot/chatbot-body/chatbot-body.module.scss +0 -13
  21. package/src/components/chatbot/chatbot-body/chatbot-body.tsx +0 -45
  22. package/src/components/chatbot/chatbot-body/conversation-message-renderer.tsx +0 -55
  23. package/src/components/chatbot/chatbot-body/index.ts +0 -1
  24. package/src/components/chatbot/chatbot-container/chatbot-container.module.scss +0 -41
  25. package/src/components/chatbot/chatbot-container/chatbot-container.tsx +0 -49
  26. package/src/components/chatbot/chatbot-container/chatbot-full-screen-container.tsx +0 -54
  27. package/src/components/chatbot/chatbot-footer/chatbot-footer.module.scss +0 -67
  28. package/src/components/chatbot/chatbot-footer/chatbot-footer.tsx +0 -140
  29. package/src/components/chatbot/chatbot-footer/index.ts +0 -1
  30. package/src/components/chatbot/chatbot-footer/speech-input-button.tsx +0 -132
  31. package/src/components/chatbot/chatbot-header/chatbot-header.module.scss +0 -48
  32. package/src/components/chatbot/chatbot-header/chatbot-header.tsx +0 -98
  33. package/src/components/chatbot/chatbot-header/index.ts +0 -1
  34. package/src/components/chatbot/chatbot.spec.tsx +0 -8
  35. package/src/components/chatbot/chatbot.tsx +0 -121
  36. package/src/components/chatbot/profile-icon.tsx +0 -26
  37. package/src/components/index.ts +0 -2
  38. package/src/components/templates/avatar/avatar.module.scss +0 -6
  39. package/src/components/templates/avatar/avatar.tsx +0 -28
  40. package/src/components/templates/avatar/index.ts +0 -1
  41. package/src/components/templates/button-template/button-template.module.scss +0 -0
  42. package/src/components/templates/button-template/button-template.tsx +0 -45
  43. package/src/components/templates/button-template/card.module.scss +0 -58
  44. package/src/components/templates/button-template/card.spec.tsx +0 -213
  45. package/src/components/templates/button-template/card.tsx +0 -123
  46. package/src/components/templates/button-template/index.ts +0 -1
  47. package/src/components/templates/carousel-template/carousel-template.module.scss +0 -15
  48. package/src/components/templates/carousel-template/carousel-template.tsx +0 -49
  49. package/src/components/templates/carousel-template/index.ts +0 -1
  50. package/src/components/templates/chart-template/chart-template.module.scss +0 -52
  51. package/src/components/templates/chart-template/chart-template.tsx +0 -75
  52. package/src/components/templates/chart-template/index.ts +0 -1
  53. package/src/components/templates/hint-template/hint-template.module.scss +0 -43
  54. package/src/components/templates/hint-template/hint-template.tsx +0 -76
  55. package/src/components/templates/hint-template/index.ts +0 -1
  56. package/src/components/templates/image-template/image-template.module.scss +0 -67
  57. package/src/components/templates/image-template/image-template.tsx +0 -58
  58. package/src/components/templates/image-template/index.ts +0 -1
  59. package/src/components/templates/index.ts +0 -10
  60. package/src/components/templates/quick-replies/index.ts +0 -1
  61. package/src/components/templates/quick-replies/quick-replies.module.scss +0 -16
  62. package/src/components/templates/quick-replies/quick-replies.tsx +0 -47
  63. package/src/components/templates/template-box/index.ts +0 -2
  64. package/src/components/templates/template-box/template-box-content.module.scss +0 -13
  65. package/src/components/templates/template-box/template-box-content.tsx +0 -30
  66. package/src/components/templates/template-box/template-box.module.scss +0 -19
  67. package/src/components/templates/template-box/template-box.tsx +0 -48
  68. package/src/components/templates/text-template/bot-typing-box.tsx +0 -81
  69. package/src/components/templates/text-template/bot-typing-placeholder.tsx +0 -28
  70. package/src/components/templates/text-template/index.ts +0 -3
  71. package/src/components/templates/text-template/text-template.module.scss +0 -131
  72. package/src/components/templates/text-template/text-template.tsx +0 -94
  73. package/src/components/templates/text-template/use-react-markdown-renderer.spec.tsx +0 -758
  74. package/src/components/templates/time/index.ts +0 -1
  75. package/src/components/templates/time/time.module.scss +0 -6
  76. package/src/components/templates/time/time.tsx +0 -34
  77. package/src/context/asgard-app-initialization-context.tsx +0 -154
  78. package/src/context/asgard-service-context.tsx +0 -145
  79. package/src/context/asgard-template-context.tsx +0 -83
  80. package/src/context/asgard-theme-context.tsx +0 -546
  81. package/src/context/index.ts +0 -4
  82. package/src/hooks/index.ts +0 -11
  83. package/src/hooks/use-asgard-service-client.ts +0 -68
  84. package/src/hooks/use-channel.ts +0 -154
  85. package/src/hooks/use-debounce.ts +0 -18
  86. package/src/hooks/use-deep-compare-memo.ts +0 -19
  87. package/src/hooks/use-is-on-screen-keyboard-open.ts +0 -43
  88. package/src/hooks/use-on-screen-keyboard-scroll-fix.ts +0 -15
  89. package/src/hooks/use-prevent-over-scrolling.ts +0 -77
  90. package/src/hooks/use-react-markdown-renderer.tsx +0 -278
  91. package/src/hooks/use-resize-observer.tsx +0 -27
  92. package/src/hooks/use-update-vh.ts +0 -30
  93. package/src/hooks/use-viewport-size.ts +0 -51
  94. package/src/icons/add_a_photo.svg +0 -3
  95. package/src/icons/bot.svg +0 -14
  96. package/src/icons/close.svg +0 -3
  97. package/src/icons/distance.svg +0 -3
  98. package/src/icons/mic.svg +0 -3
  99. package/src/icons/photo_library.svg +0 -3
  100. package/src/icons/profile.svg +0 -28
  101. package/src/icons/refresh.svg +0 -3
  102. package/src/icons/send.svg +0 -3
  103. package/src/icons/stop.svg +0 -22
  104. package/src/icons/volume_up.svg +0 -3
  105. package/src/index.ts +0 -4
  106. package/src/models/bot-provider.ts +0 -108
  107. package/src/styles/_index.scss +0 -1
  108. package/src/styles/_styles.scss +0 -11
  109. package/src/styles/colors/_colors.scss +0 -10
  110. package/src/styles/colors/_index.scss +0 -1
  111. package/src/styles/colors/_variables.scss +0 -72
  112. package/src/styles/palette/_index.scss +0 -1
  113. package/src/styles/palette/_palette.scss +0 -42
  114. package/src/styles/palette/_variables.scss +0 -40
  115. package/src/styles/radius/_index.scss +0 -1
  116. package/src/styles/radius/_radius.scss +0 -8
  117. package/src/styles/radius/_variables.scss +0 -12
  118. package/src/styles/spacing/_index.scss +0 -1
  119. package/src/styles/spacing/_spacing.scss +0 -8
  120. package/src/styles/spacing/_variables.scss +0 -13
  121. package/src/styles/utils/_index.scss +0 -1
  122. package/src/styles/utils/_map.scss +0 -22
  123. package/src/test-setup.ts +0 -1
  124. package/src/utils/color-utils.ts +0 -52
  125. package/src/utils/deep-merge.ts +0 -26
  126. package/src/utils/extractors.ts +0 -20
  127. package/src/utils/format-time.ts +0 -8
  128. package/src/utils/index.ts +0 -1
  129. package/src/utils/is.ts +0 -72
  130. package/src/utils/selectors.ts +0 -7
  131. package/src/utils/uri-validation.spec.ts +0 -208
  132. package/src/utils/uri-validation.ts +0 -103
  133. package/tsconfig.json +0 -16
  134. package/tsconfig.lib.json +0 -63
  135. package/tsconfig.spec.json +0 -36
  136. package/tsconfig.tsbuildinfo +0 -1
  137. package/vite.config.ts +0 -63
@@ -1,154 +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
- }
27
-
28
- export interface UseChannelReturn {
29
- isOpen: boolean;
30
- isResetting: boolean;
31
- isConnecting: boolean;
32
- conversation: Conversation | null;
33
- sendMessage?: (payload: Pick<FetchSsePayload, 'text'> & Partial<Pick<FetchSsePayload, 'payload'>>) => void;
34
- resetChannel?: (payload?: Pick<FetchSsePayload, 'text'> & Partial<Pick<FetchSsePayload, 'payload'>>) => void;
35
- closeChannel?: () => void;
36
- }
37
-
38
- export function useChannel(props: UseChannelProps): UseChannelReturn {
39
- const {
40
- client,
41
- defaultIsOpen,
42
- resetPayload,
43
- customChannelId,
44
- customMessageId,
45
- initMessages,
46
- onSseMessage,
47
- } = props;
48
-
49
- if (!client) {
50
- throw new Error('Client instance is required');
51
- }
52
-
53
- if (!customChannelId) {
54
- throw new Error('Custom channel id is required');
55
- }
56
-
57
- const [channel, setChannel] = useState<Channel | null>(null);
58
- const [isOpen, setIsOpen] = useState(defaultIsOpen ?? true);
59
- const [isResetting, setIsResetting] = useState(false);
60
- const [isConnecting, setIsConnecting] = useState(false);
61
- const [conversation, setConversation] = useState<Conversation | null>(null);
62
-
63
- const resetChannel = useCallback(
64
- async (payload?: Pick<FetchSsePayload, 'text'> & Partial<Pick<FetchSsePayload, 'payload'>>) => {
65
- const conversation = new Conversation({
66
- messages: new Map(
67
- initMessages?.map((message) => [message.messageId, message])
68
- ),
69
- });
70
-
71
- setIsResetting(true);
72
- setIsConnecting(true);
73
- setConversation(conversation);
74
-
75
- const channel = await Channel.reset(
76
- {
77
- client,
78
- customChannelId,
79
- customMessageId,
80
- conversation,
81
- statesObserver: (states: ChannelStates): void => {
82
- setIsConnecting(states.isConnecting);
83
- setConversation(states.conversation);
84
- },
85
- },
86
- payload,
87
- {
88
- onSseCompleted() {
89
- setIsResetting(false);
90
- },
91
- onSseError() {
92
- setIsResetting(false);
93
- },
94
- onSseMessage(response: SseResponse<EventType>) {
95
- onSseMessage?.(response, {
96
- conversation,
97
- });
98
- },
99
- }
100
- );
101
-
102
- setIsOpen(true);
103
- setChannel(channel);
104
- },
105
- [client, customChannelId, customMessageId, initMessages, onSseMessage]
106
- );
107
-
108
- const closeChannel = useCallback(() => {
109
- setChannel((prevChannel: Channel | null) => {
110
- prevChannel?.close();
111
-
112
- return null;
113
- });
114
- setIsOpen(false);
115
- setIsResetting(false);
116
- setIsConnecting(false);
117
- setConversation(null);
118
- }, []);
119
-
120
- const sendMessage = useCallback(
121
- (payload: Pick<FetchSsePayload, 'text'> & Partial<Pick<FetchSsePayload, 'payload'>>) =>
122
- channel?.sendMessage({ ...payload, customMessageId }),
123
- [channel, customMessageId]
124
- );
125
-
126
- useEffect(() => {
127
- if (!channel && isOpen) resetChannel(resetPayload);
128
- }, [channel, isOpen, resetChannel, resetPayload]);
129
-
130
- useEffect(() => {
131
- return (): void => closeChannel();
132
- }, [closeChannel]);
133
-
134
- return useMemo(
135
- () => ({
136
- isOpen,
137
- isResetting,
138
- isConnecting,
139
- conversation,
140
- sendMessage,
141
- resetChannel,
142
- closeChannel,
143
- }),
144
- [
145
- isOpen,
146
- isResetting,
147
- isConnecting,
148
- conversation,
149
- sendMessage,
150
- resetChannel,
151
- closeChannel,
152
- ]
153
- );
154
- }
@@ -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
- }