@asgard-js/react 0.0.29 → 0.0.32-canary.0

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 (138) hide show
  1. package/.babelrc +12 -0
  2. package/README.md +13 -13
  3. package/dist/components/chatbot/chatbot-header/chatbot-header.d.ts +1 -1
  4. package/dist/components/chatbot/chatbot-header/chatbot-header.d.ts.map +1 -1
  5. package/dist/components/chatbot/chatbot.d.ts +2 -1
  6. package/dist/components/chatbot/chatbot.d.ts.map +1 -1
  7. package/dist/components/templates/button-template/card.d.ts.map +1 -1
  8. package/dist/components/templates/text-template/{use-markdown-renderer.d.ts → use-react-markdown-renderer.d.ts} +3 -1
  9. package/dist/components/templates/text-template/use-react-markdown-renderer.d.ts.map +1 -0
  10. package/dist/context/asgard-template-context.d.ts +2 -0
  11. package/dist/context/asgard-template-context.d.ts.map +1 -1
  12. package/dist/hooks/index.d.ts +1 -1
  13. package/dist/hooks/index.d.ts.map +1 -1
  14. package/dist/hooks/use-channel.d.ts +1 -1
  15. package/dist/hooks/use-channel.d.ts.map +1 -1
  16. package/dist/index.js +74570 -54207
  17. package/dist/style.css +1 -1
  18. package/dist/utils/uri-validation.d.ts +20 -0
  19. package/dist/utils/uri-validation.d.ts.map +1 -0
  20. package/eslint.config.cjs +12 -0
  21. package/package.json +15 -9
  22. package/src/components/chatbot/chatbot-body/chatbot-body.module.scss +13 -0
  23. package/src/components/chatbot/chatbot-body/chatbot-body.tsx +45 -0
  24. package/src/components/chatbot/chatbot-body/conversation-message-renderer.tsx +55 -0
  25. package/src/components/chatbot/chatbot-body/index.ts +1 -0
  26. package/src/components/chatbot/chatbot-container/chatbot-container.module.scss +41 -0
  27. package/src/components/chatbot/chatbot-container/chatbot-container.tsx +49 -0
  28. package/src/components/chatbot/chatbot-container/chatbot-full-screen-container.tsx +54 -0
  29. package/src/components/chatbot/chatbot-footer/chatbot-footer.module.scss +67 -0
  30. package/src/components/chatbot/chatbot-footer/chatbot-footer.tsx +140 -0
  31. package/src/components/chatbot/chatbot-footer/index.ts +1 -0
  32. package/src/components/chatbot/chatbot-footer/speech-input-button.tsx +132 -0
  33. package/src/components/chatbot/chatbot-header/chatbot-header.module.scss +48 -0
  34. package/src/components/chatbot/chatbot-header/chatbot-header.tsx +98 -0
  35. package/src/components/chatbot/chatbot-header/index.ts +1 -0
  36. package/src/components/chatbot/chatbot.spec.tsx +8 -0
  37. package/src/components/chatbot/chatbot.tsx +118 -0
  38. package/src/components/chatbot/profile-icon.tsx +26 -0
  39. package/src/components/index.ts +2 -0
  40. package/src/components/templates/avatar/avatar.module.scss +6 -0
  41. package/src/components/templates/avatar/avatar.tsx +28 -0
  42. package/src/components/templates/avatar/index.ts +1 -0
  43. package/src/components/templates/button-template/button-template.module.scss +0 -0
  44. package/src/components/templates/button-template/button-template.tsx +45 -0
  45. package/src/components/templates/button-template/card.module.scss +58 -0
  46. package/src/components/templates/button-template/card.spec.tsx +213 -0
  47. package/src/components/templates/button-template/card.tsx +123 -0
  48. package/src/components/templates/button-template/index.ts +1 -0
  49. package/src/components/templates/carousel-template/carousel-template.module.scss +15 -0
  50. package/src/components/templates/carousel-template/carousel-template.tsx +48 -0
  51. package/src/components/templates/carousel-template/index.ts +1 -0
  52. package/src/components/templates/chart-template/chart-template.module.scss +52 -0
  53. package/src/components/templates/chart-template/chart-template.tsx +76 -0
  54. package/src/components/templates/chart-template/index.ts +1 -0
  55. package/src/components/templates/hint-template/hint-template.module.scss +39 -0
  56. package/src/components/templates/hint-template/hint-template.tsx +71 -0
  57. package/src/components/templates/hint-template/index.ts +1 -0
  58. package/src/components/templates/image-template/image-template.module.scss +67 -0
  59. package/src/components/templates/image-template/image-template.tsx +58 -0
  60. package/src/components/templates/image-template/index.ts +1 -0
  61. package/src/components/templates/index.ts +10 -0
  62. package/src/components/templates/quick-replies/index.ts +1 -0
  63. package/src/components/templates/quick-replies/quick-replies.module.scss +16 -0
  64. package/src/components/templates/quick-replies/quick-replies.tsx +44 -0
  65. package/src/components/templates/template-box/index.ts +2 -0
  66. package/src/components/templates/template-box/template-box-content.module.scss +13 -0
  67. package/src/components/templates/template-box/template-box-content.tsx +30 -0
  68. package/src/components/templates/template-box/template-box.module.scss +19 -0
  69. package/src/components/templates/template-box/template-box.tsx +48 -0
  70. package/src/components/templates/text-template/bot-typing-box.tsx +81 -0
  71. package/src/components/templates/text-template/bot-typing-placeholder.tsx +28 -0
  72. package/src/components/templates/text-template/index.ts +3 -0
  73. package/src/components/templates/text-template/text-template.module.scss +131 -0
  74. package/src/components/templates/text-template/text-template.tsx +90 -0
  75. package/src/components/templates/text-template/use-react-markdown-renderer.spec.tsx +758 -0
  76. package/src/components/templates/text-template/use-react-markdown-renderer.tsx +264 -0
  77. package/src/components/templates/time/index.ts +1 -0
  78. package/src/components/templates/time/time.module.scss +6 -0
  79. package/src/components/templates/time/time.tsx +34 -0
  80. package/src/context/asgard-app-initialization-context.tsx +154 -0
  81. package/src/context/asgard-service-context.tsx +139 -0
  82. package/src/context/asgard-template-context.tsx +83 -0
  83. package/src/context/asgard-theme-context.tsx +401 -0
  84. package/src/context/index.ts +4 -0
  85. package/src/hooks/index.ts +11 -0
  86. package/src/hooks/use-asgard-service-client.ts +68 -0
  87. package/src/hooks/use-channel.ts +154 -0
  88. package/src/hooks/use-debounce.ts +18 -0
  89. package/src/hooks/use-deep-compare-memo.ts +19 -0
  90. package/src/hooks/use-is-on-screen-keyboard-open.ts +43 -0
  91. package/src/hooks/use-on-screen-keyboard-scroll-fix.ts +15 -0
  92. package/src/hooks/use-prevent-over-scrolling.ts +77 -0
  93. package/src/hooks/use-resize-observer.tsx +27 -0
  94. package/src/hooks/use-update-vh.ts +30 -0
  95. package/src/hooks/use-viewport-size.ts +51 -0
  96. package/src/icons/add_a_photo.svg +3 -0
  97. package/src/icons/bot.svg +14 -0
  98. package/src/icons/close.svg +3 -0
  99. package/src/icons/distance.svg +3 -0
  100. package/src/icons/mic.svg +3 -0
  101. package/src/icons/photo_library.svg +3 -0
  102. package/src/icons/profile.svg +28 -0
  103. package/src/icons/refresh.svg +3 -0
  104. package/src/icons/send.svg +3 -0
  105. package/src/icons/stop.svg +22 -0
  106. package/src/icons/volume_up.svg +3 -0
  107. package/src/index.ts +4 -0
  108. package/src/models/bot-provider.ts +108 -0
  109. package/src/styles/_index.scss +1 -0
  110. package/src/styles/_styles.scss +11 -0
  111. package/src/styles/colors/_colors.scss +10 -0
  112. package/src/styles/colors/_index.scss +1 -0
  113. package/src/styles/colors/_variables.scss +72 -0
  114. package/src/styles/palette/_index.scss +1 -0
  115. package/src/styles/palette/_palette.scss +42 -0
  116. package/src/styles/palette/_variables.scss +40 -0
  117. package/src/styles/radius/_index.scss +1 -0
  118. package/src/styles/radius/_radius.scss +8 -0
  119. package/src/styles/radius/_variables.scss +12 -0
  120. package/src/styles/spacing/_index.scss +1 -0
  121. package/src/styles/spacing/_spacing.scss +8 -0
  122. package/src/styles/spacing/_variables.scss +13 -0
  123. package/src/styles/utils/_index.scss +1 -0
  124. package/src/styles/utils/_map.scss +22 -0
  125. package/src/test-setup.ts +1 -0
  126. package/src/utils/deep-merge.ts +23 -0
  127. package/src/utils/extractors.ts +20 -0
  128. package/src/utils/format-time.ts +8 -0
  129. package/src/utils/index.ts +1 -0
  130. package/src/utils/is.ts +72 -0
  131. package/src/utils/selectors.ts +7 -0
  132. package/src/utils/uri-validation.spec.ts +208 -0
  133. package/src/utils/uri-validation.ts +103 -0
  134. package/tsconfig.json +16 -0
  135. package/tsconfig.lib.json +62 -0
  136. package/tsconfig.spec.json +36 -0
  137. package/vite.config.ts +63 -0
  138. package/dist/components/templates/text-template/use-markdown-renderer.d.ts.map +0 -1
@@ -0,0 +1,264 @@
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 './text-template.module.scss';
16
+ import { useAsgardTemplateContext } from 'src/context/asgard-template-context';
17
+ import { safeWindowOpen } from 'src/utils/uri-validation';
18
+
19
+ interface MarkdownRenderResult {
20
+ htmlBlocks: ReactNode;
21
+ lastTypingText: string;
22
+ }
23
+
24
+ type Token = {
25
+ raw: string;
26
+ type: string;
27
+ };
28
+
29
+ // Maximum number of cached markdown blocks to prevent memory leaks
30
+ export const MAX_CACHE_SIZE = 100;
31
+
32
+ // Helper function to manage cache size with LRU eviction
33
+ export function manageCacheSize(cache: Map<string, ReactNode>): void {
34
+ if (cache.size >= MAX_CACHE_SIZE) {
35
+ // Remove the first (oldest) entry to make room for new ones
36
+ const firstKey = cache.keys().next().value;
37
+ if (firstKey) {
38
+ cache.delete(firstKey);
39
+ }
40
+ }
41
+ }
42
+
43
+ // Enhanced completion detection with math expression support
44
+ function isCompleteParagraph(raw: string): boolean {
45
+ // Basic completion logic - must end with proper punctuation or newlines
46
+ // OR contain complete markdown elements
47
+ const hasMarkdownElements =
48
+ /^(#{1,6}\s|>\s|[-*+]\s|\d+\.\s|---+|```|\|.*\|)/m.test(raw.trim());
49
+
50
+ // Check for complete table structure (header row + separator + at least one data row)
51
+ const hasCompleteTable = /\|.*\|\s*\n\s*\|[-:\s|]+\|\s*\n\s*\|.*\|/m.test(
52
+ raw.trim()
53
+ );
54
+
55
+ const basicCompletion =
56
+ raw.endsWith('\n\n') ||
57
+ raw.endsWith('\n') ||
58
+ raw.endsWith('.') ||
59
+ raw.endsWith('。') ||
60
+ raw.endsWith('!') ||
61
+ raw.endsWith('!') ||
62
+ raw.endsWith('?') ||
63
+ hasMarkdownElements || // Has complete markdown elements
64
+ hasCompleteTable; // Has complete table structure
65
+
66
+ // Math-specific completion detection
67
+ // Check for complete math patterns (properly closed with $..$ or $$..$$)
68
+ const completeInlineMath = /\$[^$\s][^$]*\$/.test(raw);
69
+ const completeBlockMath = /\$\$[^$]*\$\$/.test(raw);
70
+ const hasCompleteMath = completeInlineMath || completeBlockMath;
71
+
72
+ const mathCompletion =
73
+ !raw.includes('$') || // No math expressions
74
+ hasCompleteMath; // Has complete math and no incomplete math
75
+
76
+ // Complete if: (basic completion AND math completion) OR complete block math
77
+ // OR if it's just a single token without newlines (treat as complete)
78
+ const isSimpleToken = !raw.includes('\n\n') && raw.trim().length > 0;
79
+
80
+ return (
81
+ (basicCompletion && mathCompletion) || (isSimpleToken && mathCompletion)
82
+ );
83
+ }
84
+
85
+ // Custom table renderer to maintain current styling
86
+ const TableRenderer = ({ children, ...props }: any): ReactNode => (
87
+ <div className={classes.table_container}>
88
+ <table {...props}>{children}</table>
89
+ </div>
90
+ );
91
+
92
+ // Custom code renderer to maintain highlight.js classes exactly
93
+ const CodeRenderer = ({ children, className, ...props }: any): ReactNode => {
94
+ return (
95
+ <code className={`hljs ${className || ''}`} {...props}>
96
+ {children}
97
+ </code>
98
+ );
99
+ };
100
+
101
+ // Custom link renderer to integrate defaultLinkTarget prop
102
+ const LinkRenderer = ({ children, href, ...props }: any): ReactNode => {
103
+ const { defaultLinkTarget } = useAsgardTemplateContext();
104
+
105
+ const handleClick = useCallback(
106
+ (e: React.MouseEvent) => {
107
+ e.preventDefault();
108
+ if (href) {
109
+ safeWindowOpen(href, defaultLinkTarget || '_blank');
110
+ }
111
+ },
112
+ [href, defaultLinkTarget]
113
+ );
114
+
115
+ return (
116
+ <a href={href} onClick={handleClick} rel="noopener noreferrer" {...props}>
117
+ {children}
118
+ </a>
119
+ );
120
+ };
121
+
122
+ // Custom math renderers for inline and block math expressions
123
+ const InlineMathRenderer = ({ children, ...props }: any): ReactNode => (
124
+ <span className="math math-inline" {...props}>
125
+ {children}
126
+ </span>
127
+ );
128
+
129
+ const BlockMathRenderer = ({ children, ...props }: any): ReactNode => (
130
+ <div className="math math-display" {...props}>
131
+ {children}
132
+ </div>
133
+ );
134
+
135
+ // Component renderers that maintain current styling and behavior
136
+ const components = {
137
+ table: TableRenderer,
138
+ code: CodeRenderer,
139
+ a: LinkRenderer,
140
+ math: InlineMathRenderer, // Inline math: $expression$
141
+ div: ({ className, ...props }: any): ReactNode => {
142
+ // Block math: $$expression$$
143
+ // Check for KaTeX display math classes
144
+ if (
145
+ className?.includes('math-display') ||
146
+ className?.includes('katex-display')
147
+ ) {
148
+ return (
149
+ <BlockMathRenderer
150
+ className={`math math-display ${className || ''}`}
151
+ {...props}
152
+ />
153
+ );
154
+ }
155
+
156
+ return <div className={className} {...props} />;
157
+ },
158
+ };
159
+
160
+ export function useMarkdownRenderer(
161
+ markdownText: string,
162
+ delay = 100
163
+ ): MarkdownRenderResult {
164
+ const [blocks, setBlocks] = useState<ReactNode[]>([]);
165
+ const [typingText, setTypingText] = useState<string>('');
166
+
167
+ const cacheRef = useRef<Map<string, ReactNode>>(new Map());
168
+
169
+ const getRawText = useCallback((text: string): string => {
170
+ return text || '';
171
+ }, []);
172
+
173
+ // Mimic the exact token-based logic from current implementation
174
+ const parseToTokens = useCallback((text: string): Token[] => {
175
+ if (!text) return [];
176
+
177
+ // Simple tokenization - split by double newlines for paragraphs
178
+ // If there are no double newlines, treat the entire text as one token
179
+ const paragraphs = text.includes('\n\n') ? text.split(/\n\s*\n/) : [text];
180
+
181
+ return paragraphs.map((p) => ({
182
+ raw: p + (text.includes('\n\n') ? '\n\n' : ''),
183
+ type: 'paragraph',
184
+ }));
185
+ }, []);
186
+
187
+ useEffect(() => {
188
+ if (!markdownText) {
189
+ setBlocks([]);
190
+ setTypingText('');
191
+ cacheRef.current.clear();
192
+
193
+ return;
194
+ }
195
+
196
+ const handler = setTimeout(() => {
197
+ const tokens = parseToTokens(markdownText);
198
+ if (tokens.length === 0) {
199
+ setBlocks([]);
200
+ setTypingText('');
201
+
202
+ return;
203
+ }
204
+
205
+ // Find the last complete token
206
+ let lastCompleteIndex = -1;
207
+
208
+ for (let i = tokens.length - 1; i >= 0; i--) {
209
+ const raw = getRawText(tokens[i].raw);
210
+ if (isCompleteParagraph(raw)) {
211
+ lastCompleteIndex = i;
212
+
213
+ break;
214
+ }
215
+ }
216
+
217
+ const finishedTokens = tokens.slice(0, lastCompleteIndex + 1);
218
+ const unprocessedTokens = tokens.slice(lastCompleteIndex + 1);
219
+
220
+ const newBlocks: ReactNode[] = [];
221
+
222
+ for (const token of finishedTokens) {
223
+ const raw = getRawText(token.raw);
224
+ const blockInCache = cacheRef.current.get(raw);
225
+ if (blockInCache) {
226
+ newBlocks.push(blockInCache);
227
+ } else {
228
+ const reactElement = (
229
+ <ReactMarkdown
230
+ key={raw}
231
+ remarkPlugins={[remarkGfm, remarkMath]}
232
+ rehypePlugins={[rehypeHighlight, rehypeKatex]}
233
+ components={components}
234
+ >
235
+ {raw.trim()}
236
+ </ReactMarkdown>
237
+ );
238
+ // Manage cache size before adding new entry
239
+ manageCacheSize(cacheRef.current);
240
+ cacheRef.current.set(raw, reactElement);
241
+ newBlocks.push(reactElement);
242
+ }
243
+ }
244
+
245
+ const lastRaw = unprocessedTokens
246
+ .map((t) => getRawText(t.raw))
247
+ .join('\n')
248
+ .trim();
249
+ setBlocks(newBlocks);
250
+ setTypingText(lastRaw);
251
+ }, delay);
252
+
253
+ return (): void => clearTimeout(handler);
254
+ }, [markdownText, delay, getRawText, parseToTokens]);
255
+
256
+ const htmlBlocks = useMemo<ReactNode>(() => {
257
+ return <div className={classes.md_container}>{blocks}</div>;
258
+ }, [blocks]);
259
+
260
+ return {
261
+ htmlBlocks,
262
+ lastTypingText: typingText,
263
+ };
264
+ }
@@ -0,0 +1 @@
1
+ export * from './time';
@@ -0,0 +1,6 @@
1
+ .time {
2
+ display: flex;
3
+ align-items: flex-end;
4
+ font-size: 12px;
5
+ color: #8C8C8C;
6
+ }
@@ -0,0 +1,34 @@
1
+ import { ReactNode, useMemo } from 'react';
2
+ import { formatTime } from 'src/utils';
3
+ import styles from './time.module.scss';
4
+ import clsx from 'clsx';
5
+ import { useAsgardThemeContext } from 'src/context/asgard-theme-context';
6
+
7
+ interface TimeProps {
8
+ time?: Date;
9
+ className?: string;
10
+ }
11
+
12
+ export function Time(props: TimeProps): ReactNode {
13
+ const { time, className } = props;
14
+
15
+ const { template } = useAsgardThemeContext();
16
+
17
+ const timeStyle = useMemo(
18
+ () => ({
19
+ color: template?.time?.style?.color,
20
+ }),
21
+ [template?.time?.style?.color]
22
+ );
23
+
24
+ if (!time) return null;
25
+
26
+ return (
27
+ <div
28
+ className={clsx('asgard-time', styles.time, className)}
29
+ style={timeStyle}
30
+ >
31
+ {formatTime(time)}
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,154 @@
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useEffect,
5
+ useState,
6
+ PropsWithChildren,
7
+ ReactNode,
8
+ } from 'react';
9
+ import { ClientConfig } from '@asgard-js/core';
10
+ import { getBotProviderModels } from 'src/models/bot-provider';
11
+ import { useDeepCompareMemo } from 'src/hooks';
12
+ import { deepMerge } from 'src/utils/deep-merge';
13
+ import { extractRefs } from 'src/utils/extractors';
14
+
15
+ type AsyncInitializers = {
16
+ [key: string]: () => Promise<unknown>;
17
+ };
18
+
19
+ export interface Annotations {
20
+ embedConfig: {
21
+ avatar?: string;
22
+ botTypingPlaceholder?: string;
23
+ debugMode?: boolean;
24
+ fullScreen?: boolean;
25
+ inputPlaceholder?: string;
26
+ theme: {
27
+ chatbot: {
28
+ backgroundColor?: string;
29
+ borderColor?: string;
30
+ inactiveColor?: string;
31
+ primaryComponent?: {
32
+ mainColor?: string;
33
+ secondaryColor?: string;
34
+ };
35
+ };
36
+ botMessage: {
37
+ backgroundColor?: string;
38
+ carouselButtonBackgroundColor?: string;
39
+ color?: string;
40
+ };
41
+ userMessage: {
42
+ backgroundColor?: string;
43
+ color?: string;
44
+ };
45
+ };
46
+ title?: string;
47
+ };
48
+ }
49
+
50
+ export interface AsgardAppInitializationContextValue {
51
+ data: {
52
+ annotations?: Annotations;
53
+ };
54
+ loading: boolean;
55
+ error: Error | null;
56
+ }
57
+
58
+ export const AsgardAppInitializationContext =
59
+ createContext<AsgardAppInitializationContextValue>({
60
+ data: {},
61
+ loading: true,
62
+ error: null,
63
+ });
64
+
65
+ export interface AsgardAppInitializationContextProviderProps {
66
+ enabled: boolean;
67
+ config: ClientConfig;
68
+ asyncInitializers?: AsyncInitializers;
69
+ loadingComponent?: React.ReactNode;
70
+ }
71
+
72
+ export const AsgardAppInitializationContextProvider = (
73
+ props: PropsWithChildren<AsgardAppInitializationContextProviderProps>
74
+ ): ReactNode => {
75
+ const {
76
+ enabled,
77
+ asyncInitializers: asyncInitializersFromProp = {},
78
+ children,
79
+ loadingComponent = <div>Loading...</div>,
80
+ } = props;
81
+
82
+ const botProviderModels = useDeepCompareMemo(
83
+ () => getBotProviderModels(props.config),
84
+ [props.config]
85
+ );
86
+
87
+ const asyncInitializers = useDeepCompareMemo(
88
+ () =>
89
+ deepMerge(
90
+ { annotations: botProviderModels.getAnnotations },
91
+ asyncInitializersFromProp
92
+ ),
93
+ [...extractRefs(asyncInitializersFromProp), botProviderModels]
94
+ );
95
+
96
+ const [data, setData] = useState<AsgardAppInitializationContextValue['data']>(
97
+ {}
98
+ );
99
+ const [loading, setLoading] = useState(true);
100
+ const [error, setError] = useState<Error | null>(null);
101
+
102
+ useEffect(() => {
103
+ let isMounted = true;
104
+
105
+ if (!enabled) {
106
+ return;
107
+ }
108
+
109
+ setLoading(true);
110
+
111
+ Promise.all(
112
+ Object.entries(asyncInitializers).map(async ([key, fn]) => {
113
+ try {
114
+ const value = await fn();
115
+
116
+ return [key, value];
117
+ } catch {
118
+ return [key, undefined];
119
+ }
120
+ })
121
+ )
122
+ .then((results) => {
123
+ if (isMounted) setData(Object.fromEntries(results));
124
+ })
125
+ .catch((err) => {
126
+ if (isMounted) setError(err);
127
+ })
128
+ .finally(() => {
129
+ if (isMounted) setLoading(false);
130
+ });
131
+
132
+ return (): void => {
133
+ isMounted = false;
134
+ };
135
+ }, [asyncInitializers, enabled]);
136
+
137
+ if (!enabled) {
138
+ return children;
139
+ }
140
+
141
+ if (loading) {
142
+ return loadingComponent;
143
+ }
144
+
145
+ return (
146
+ <AsgardAppInitializationContext.Provider value={{ data, loading, error }}>
147
+ {children}
148
+ </AsgardAppInitializationContext.Provider>
149
+ );
150
+ };
151
+
152
+ export const useAsgardAppInitializationContext =
153
+ (): AsgardAppInitializationContextValue =>
154
+ useContext(AsgardAppInitializationContext);
@@ -0,0 +1,139 @@
1
+ import {
2
+ AsgardServiceClient,
3
+ ClientConfig,
4
+ ConversationMessage,
5
+ } from '@asgard-js/core';
6
+ import {
7
+ createContext,
8
+ ForwardedRef,
9
+ ReactNode,
10
+ RefObject,
11
+ useContext,
12
+ useImperativeHandle,
13
+ useMemo,
14
+ useRef,
15
+ } from 'react';
16
+ import {
17
+ useAsgardServiceClient,
18
+ useChannel,
19
+ UseChannelProps,
20
+ UseChannelReturn,
21
+ } from 'src/hooks';
22
+
23
+ export interface AsgardServiceContextValue {
24
+ avatar?: string;
25
+ client: AsgardServiceClient | null;
26
+ isOpen: boolean;
27
+ isResetting: boolean;
28
+ isConnecting: boolean;
29
+ messages: Map<string, ConversationMessage> | null;
30
+ messageBoxBottomRef: RefObject<HTMLDivElement>;
31
+ sendMessage?: UseChannelReturn['sendMessage'];
32
+ resetChannel?: UseChannelReturn['resetChannel'];
33
+ closeChannel?: UseChannelReturn['closeChannel'];
34
+ botTypingPlaceholder?: string;
35
+ }
36
+
37
+ export const AsgardServiceContext = createContext<AsgardServiceContextValue>({
38
+ avatar: undefined,
39
+ client: null,
40
+ isOpen: false,
41
+ isResetting: false,
42
+ isConnecting: false,
43
+ messages: null,
44
+ messageBoxBottomRef: { current: null },
45
+ botTypingPlaceholder: undefined,
46
+ });
47
+
48
+ export interface AsgardServiceContextProviderProps {
49
+ children: ReactNode;
50
+ parentRef?: ForwardedRef<
51
+ Partial<{ serviceContext?: AsgardServiceContextValue }>
52
+ >;
53
+ avatar?: string;
54
+ config: ClientConfig;
55
+ botTypingPlaceholder?: string;
56
+ customChannelId: string;
57
+ customMessageId?: string;
58
+ delayTime?: number;
59
+ initMessages?: ConversationMessage[];
60
+ onSseMessage?: UseChannelProps['onSseMessage'];
61
+ }
62
+
63
+ export function AsgardServiceContextProvider(
64
+ props: AsgardServiceContextProviderProps
65
+ ): ReactNode {
66
+ const {
67
+ avatar,
68
+ children,
69
+ parentRef,
70
+ config,
71
+ botTypingPlaceholder,
72
+ customChannelId,
73
+ initMessages,
74
+ onSseMessage,
75
+ } = props;
76
+
77
+ const messageBoxBottomRef = useRef<HTMLDivElement>(null);
78
+
79
+ const client = useAsgardServiceClient({ config });
80
+
81
+ const {
82
+ isOpen,
83
+ isResetting,
84
+ isConnecting,
85
+ conversation,
86
+ sendMessage,
87
+ resetChannel,
88
+ closeChannel,
89
+ } = useChannel({
90
+ client,
91
+ customChannelId,
92
+ initMessages,
93
+ onSseMessage,
94
+ });
95
+
96
+ const contextValue = useMemo(
97
+ () => ({
98
+ avatar,
99
+ client,
100
+ isOpen,
101
+ isResetting,
102
+ isConnecting,
103
+ messages: conversation?.messages ?? null,
104
+ sendMessage,
105
+ resetChannel,
106
+ closeChannel,
107
+ botTypingPlaceholder,
108
+ messageBoxBottomRef,
109
+ }),
110
+ [
111
+ avatar,
112
+ client,
113
+ isOpen,
114
+ isResetting,
115
+ isConnecting,
116
+ conversation?.messages,
117
+ sendMessage,
118
+ resetChannel,
119
+ closeChannel,
120
+ botTypingPlaceholder,
121
+ ]
122
+ );
123
+
124
+ useImperativeHandle(parentRef, () => {
125
+ return {
126
+ serviceContext: contextValue,
127
+ };
128
+ });
129
+
130
+ return (
131
+ <AsgardServiceContext.Provider value={contextValue}>
132
+ {children}
133
+ </AsgardServiceContext.Provider>
134
+ );
135
+ }
136
+
137
+ export function useAsgardContext(): AsgardServiceContextValue {
138
+ return useContext(AsgardServiceContext);
139
+ }
@@ -0,0 +1,83 @@
1
+ import {
2
+ createContext,
3
+ PropsWithChildren,
4
+ ReactNode,
5
+ useContext,
6
+ useMemo,
7
+ } from 'react';
8
+ import { ConversationErrorMessage, FetchSsePayload } from '@asgard-js/core';
9
+
10
+ export interface AsgardTemplateContextValue {
11
+ onErrorClick?: (message: ConversationErrorMessage) => void;
12
+ errorMessageRenderer?: (message: ConversationErrorMessage) => ReactNode;
13
+ onTemplateBtnClick?: (
14
+ payload: Record<string, unknown>,
15
+ {
16
+ sse,
17
+ }: {
18
+ sse: {
19
+ sendMessage: (
20
+ payload: Pick<FetchSsePayload, 'text' | 'payload'>
21
+ ) => void;
22
+ };
23
+ }
24
+ ) => void;
25
+ defaultLinkTarget?: '_blank' | '_self' | '_parent' | '_top';
26
+ }
27
+
28
+ export const AsgardTemplateContext = createContext<AsgardTemplateContextValue>({
29
+ onErrorClick: undefined,
30
+ errorMessageRenderer: undefined,
31
+ onTemplateBtnClick: undefined,
32
+ defaultLinkTarget: undefined,
33
+ });
34
+
35
+ interface AsgardTemplateContextProviderProps extends PropsWithChildren {
36
+ onErrorClick?: (message: ConversationErrorMessage) => void;
37
+ errorMessageRenderer?: (message: ConversationErrorMessage) => ReactNode;
38
+ onTemplateBtnClick?: (
39
+ payload: Record<string, unknown>,
40
+ {
41
+ sse,
42
+ }: {
43
+ sse: {
44
+ sendMessage: (
45
+ payload: Pick<FetchSsePayload, 'text' | 'payload'>
46
+ ) => void;
47
+ };
48
+ }
49
+ ) => void;
50
+ defaultLinkTarget?: '_blank' | '_self' | '_parent' | '_top';
51
+ }
52
+
53
+ export function AsgardTemplateContextProvider(
54
+ props: AsgardTemplateContextProviderProps
55
+ ): ReactNode {
56
+ const {
57
+ children,
58
+ onErrorClick,
59
+ errorMessageRenderer,
60
+ onTemplateBtnClick,
61
+ defaultLinkTarget,
62
+ } = props;
63
+
64
+ const contextValue = useMemo(
65
+ () => ({
66
+ onErrorClick,
67
+ errorMessageRenderer,
68
+ onTemplateBtnClick,
69
+ defaultLinkTarget,
70
+ }),
71
+ [errorMessageRenderer, onErrorClick, onTemplateBtnClick, defaultLinkTarget]
72
+ );
73
+
74
+ return (
75
+ <AsgardTemplateContext.Provider value={contextValue}>
76
+ {children}
77
+ </AsgardTemplateContext.Provider>
78
+ );
79
+ }
80
+
81
+ export function useAsgardTemplateContext(): AsgardTemplateContextValue {
82
+ return useContext(AsgardTemplateContext);
83
+ }