@asgard-js/react 0.0.31 → 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.
- package/.babelrc +12 -0
- package/README.md +10 -12
- package/dist/components/templates/button-template/card.d.ts.map +1 -1
- package/dist/context/asgard-template-context.d.ts.map +1 -1
- package/dist/index.js +46355 -9679
- package/eslint.config.cjs +12 -0
- package/package.json +9 -7
- package/src/components/chatbot/chatbot-body/chatbot-body.module.scss +13 -0
- package/src/components/chatbot/chatbot-body/chatbot-body.tsx +45 -0
- package/src/components/chatbot/chatbot-body/conversation-message-renderer.tsx +55 -0
- package/src/components/chatbot/chatbot-body/index.ts +1 -0
- package/src/components/chatbot/chatbot-container/chatbot-container.module.scss +41 -0
- package/src/components/chatbot/chatbot-container/chatbot-container.tsx +49 -0
- package/src/components/chatbot/chatbot-container/chatbot-full-screen-container.tsx +54 -0
- package/src/components/chatbot/chatbot-footer/chatbot-footer.module.scss +67 -0
- package/src/components/chatbot/chatbot-footer/chatbot-footer.tsx +140 -0
- package/src/components/chatbot/chatbot-footer/index.ts +1 -0
- package/src/components/chatbot/chatbot-footer/speech-input-button.tsx +132 -0
- package/src/components/chatbot/chatbot-header/chatbot-header.module.scss +48 -0
- package/src/components/chatbot/chatbot-header/chatbot-header.tsx +98 -0
- package/src/components/chatbot/chatbot-header/index.ts +1 -0
- package/src/components/chatbot/chatbot.spec.tsx +8 -0
- package/src/components/chatbot/chatbot.tsx +118 -0
- package/src/components/chatbot/profile-icon.tsx +26 -0
- package/src/components/index.ts +2 -0
- package/src/components/templates/avatar/avatar.module.scss +6 -0
- package/src/components/templates/avatar/avatar.tsx +28 -0
- package/src/components/templates/avatar/index.ts +1 -0
- package/src/components/templates/button-template/button-template.module.scss +0 -0
- package/src/components/templates/button-template/button-template.tsx +45 -0
- package/src/components/templates/button-template/card.module.scss +58 -0
- package/src/components/templates/button-template/card.spec.tsx +213 -0
- package/src/components/templates/button-template/card.tsx +123 -0
- package/src/components/templates/button-template/index.ts +1 -0
- package/src/components/templates/carousel-template/carousel-template.module.scss +15 -0
- package/src/components/templates/carousel-template/carousel-template.tsx +48 -0
- package/src/components/templates/carousel-template/index.ts +1 -0
- package/src/components/templates/chart-template/chart-template.module.scss +52 -0
- package/src/components/templates/chart-template/chart-template.tsx +76 -0
- package/src/components/templates/chart-template/index.ts +1 -0
- package/src/components/templates/hint-template/hint-template.module.scss +39 -0
- package/src/components/templates/hint-template/hint-template.tsx +71 -0
- package/src/components/templates/hint-template/index.ts +1 -0
- package/src/components/templates/image-template/image-template.module.scss +67 -0
- package/src/components/templates/image-template/image-template.tsx +58 -0
- package/src/components/templates/image-template/index.ts +1 -0
- package/src/components/templates/index.ts +10 -0
- package/src/components/templates/quick-replies/index.ts +1 -0
- package/src/components/templates/quick-replies/quick-replies.module.scss +16 -0
- package/src/components/templates/quick-replies/quick-replies.tsx +44 -0
- package/src/components/templates/template-box/index.ts +2 -0
- package/src/components/templates/template-box/template-box-content.module.scss +13 -0
- package/src/components/templates/template-box/template-box-content.tsx +30 -0
- package/src/components/templates/template-box/template-box.module.scss +19 -0
- package/src/components/templates/template-box/template-box.tsx +48 -0
- package/src/components/templates/text-template/bot-typing-box.tsx +81 -0
- package/src/components/templates/text-template/bot-typing-placeholder.tsx +28 -0
- package/src/components/templates/text-template/index.ts +3 -0
- package/src/components/templates/text-template/text-template.module.scss +131 -0
- package/src/components/templates/text-template/text-template.tsx +90 -0
- package/src/components/templates/text-template/use-react-markdown-renderer.spec.tsx +758 -0
- package/src/components/templates/text-template/use-react-markdown-renderer.tsx +264 -0
- package/src/components/templates/time/index.ts +1 -0
- package/src/components/templates/time/time.module.scss +6 -0
- package/src/components/templates/time/time.tsx +34 -0
- package/src/context/asgard-app-initialization-context.tsx +154 -0
- package/src/context/asgard-service-context.tsx +139 -0
- package/src/context/asgard-template-context.tsx +83 -0
- package/src/context/asgard-theme-context.tsx +401 -0
- package/src/context/index.ts +4 -0
- package/src/hooks/index.ts +11 -0
- package/src/hooks/use-asgard-service-client.ts +68 -0
- package/src/hooks/use-channel.ts +154 -0
- package/src/hooks/use-debounce.ts +18 -0
- package/src/hooks/use-deep-compare-memo.ts +19 -0
- package/src/hooks/use-is-on-screen-keyboard-open.ts +43 -0
- package/src/hooks/use-on-screen-keyboard-scroll-fix.ts +15 -0
- package/src/hooks/use-prevent-over-scrolling.ts +77 -0
- package/src/hooks/use-resize-observer.tsx +27 -0
- package/src/hooks/use-update-vh.ts +30 -0
- package/src/hooks/use-viewport-size.ts +51 -0
- package/src/icons/add_a_photo.svg +3 -0
- package/src/icons/bot.svg +14 -0
- package/src/icons/close.svg +3 -0
- package/src/icons/distance.svg +3 -0
- package/src/icons/mic.svg +3 -0
- package/src/icons/photo_library.svg +3 -0
- package/src/icons/profile.svg +28 -0
- package/src/icons/refresh.svg +3 -0
- package/src/icons/send.svg +3 -0
- package/src/icons/stop.svg +22 -0
- package/src/icons/volume_up.svg +3 -0
- package/src/index.ts +4 -0
- package/src/models/bot-provider.ts +108 -0
- package/src/styles/_index.scss +1 -0
- package/src/styles/_styles.scss +11 -0
- package/src/styles/colors/_colors.scss +10 -0
- package/src/styles/colors/_index.scss +1 -0
- package/src/styles/colors/_variables.scss +72 -0
- package/src/styles/palette/_index.scss +1 -0
- package/src/styles/palette/_palette.scss +42 -0
- package/src/styles/palette/_variables.scss +40 -0
- package/src/styles/radius/_index.scss +1 -0
- package/src/styles/radius/_radius.scss +8 -0
- package/src/styles/radius/_variables.scss +12 -0
- package/src/styles/spacing/_index.scss +1 -0
- package/src/styles/spacing/_spacing.scss +8 -0
- package/src/styles/spacing/_variables.scss +13 -0
- package/src/styles/utils/_index.scss +1 -0
- package/src/styles/utils/_map.scss +22 -0
- package/src/test-setup.ts +1 -0
- package/src/utils/deep-merge.ts +23 -0
- package/src/utils/extractors.ts +20 -0
- package/src/utils/format-time.ts +8 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/is.ts +72 -0
- package/src/utils/selectors.ts +7 -0
- package/src/utils/uri-validation.spec.ts +208 -0
- package/src/utils/uri-validation.ts +103 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +62 -0
- package/tsconfig.spec.json +36 -0
- package/vite.config.ts +63 -0
|
@@ -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,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
|
+
}
|