@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,401 @@
1
+ import {
2
+ createContext,
3
+ CSSProperties,
4
+ PropsWithChildren,
5
+ ReactNode,
6
+ useContext,
7
+ useMemo,
8
+ useCallback,
9
+ } from 'react';
10
+ import { deepMerge } from 'src/utils/deep-merge';
11
+ import {
12
+ useAsgardAppInitializationContext,
13
+ Annotations,
14
+ } from 'src/context/asgard-app-initialization-context';
15
+
16
+ export interface AsgardThemeContextValue {
17
+ chatbot: Pick<
18
+ CSSProperties,
19
+ | 'width'
20
+ | 'height'
21
+ | 'maxWidth'
22
+ | 'minWidth'
23
+ | 'maxHeight'
24
+ | 'minHeight'
25
+ | 'backgroundColor'
26
+ | 'borderColor'
27
+ | 'borderRadius'
28
+ > & {
29
+ contentMaxWidth?: CSSProperties['maxWidth'];
30
+ backgroundColor?: CSSProperties['backgroundColor'];
31
+ borderColor?: CSSProperties['borderColor'];
32
+ inactiveColor?: CSSProperties['color'];
33
+ primaryComponent?: {
34
+ mainColor?: CSSProperties['color'];
35
+ secondaryColor?: CSSProperties['color'];
36
+ };
37
+ style?: CSSProperties;
38
+ header?: Partial<{
39
+ style: CSSProperties;
40
+ title: {
41
+ style: CSSProperties;
42
+ };
43
+ actionButton?: {
44
+ style: CSSProperties;
45
+ };
46
+ }>;
47
+ body?: Partial<{
48
+ style: CSSProperties;
49
+ }>;
50
+ footer?: Partial<{
51
+ style: CSSProperties;
52
+ textArea: {
53
+ style: CSSProperties;
54
+ '::placeholder': CSSProperties;
55
+ };
56
+ submitButton: {
57
+ style: CSSProperties;
58
+ };
59
+ speechInputButton: {
60
+ style: CSSProperties;
61
+ };
62
+ }>;
63
+ };
64
+ botMessage: Pick<CSSProperties, 'color' | 'backgroundColor'>;
65
+ userMessage: Pick<CSSProperties, 'color' | 'backgroundColor'>;
66
+ template?: Partial<{
67
+ /**
68
+ * first level for common/shared properties.
69
+ * Check MessageTemplate type for more details (packages/core/src/types/sse-response.ts).
70
+ */
71
+ quickReplies?: Partial<{
72
+ style: CSSProperties;
73
+ button: {
74
+ style: CSSProperties;
75
+ };
76
+ }>;
77
+ time?: Partial<{
78
+ style: CSSProperties;
79
+ }>;
80
+ /**
81
+ * TBD: Fill the necessary properties based on the requirements.
82
+ */
83
+ TextMessageTemplate: Partial<{ style: CSSProperties }>;
84
+ /**
85
+ * TBD: Fill the necessary properties based on the requirements.
86
+ */
87
+ HintMessageTemplate: Partial<{ style: CSSProperties }>;
88
+ /**
89
+ * TBD: Fill the necessary properties based on the requirements.
90
+ */
91
+ ImageMessageTemplate: Partial<{ style: CSSProperties }>;
92
+ /**
93
+ * TBD: Fill the necessary properties based on the requirements.
94
+ */
95
+ VideoMessageTemplate: Partial<{ style: CSSProperties }>;
96
+ /**
97
+ * TBD: Fill the necessary properties based on the requirements.
98
+ */
99
+ AudioMessageTemplate: Partial<{ style: CSSProperties }>;
100
+ /**
101
+ * TBD: Fill the necessary properties based on the requirements.
102
+ */
103
+ LocationMessageTemplate: Partial<{ style: CSSProperties }>;
104
+ /**
105
+ * TBD: Fill the necessary properties based on the requirements.
106
+ */
107
+ ChartMessageTemplate: Partial<{ style: CSSProperties }>;
108
+ /**
109
+ * TBD: Fill the necessary properties based on the requirements.
110
+ */
111
+ ButtonMessageTemplate: Partial<{
112
+ style: CSSProperties;
113
+ button?: {
114
+ style: CSSProperties;
115
+ };
116
+ }>;
117
+ /**
118
+ * TBD: Fill the necessary properties based on the requirements.
119
+ */
120
+ CarouselMessageTemplate: Partial<{
121
+ style: CSSProperties;
122
+ card: {
123
+ style: CSSProperties;
124
+ button?: {
125
+ style: CSSProperties;
126
+ };
127
+ };
128
+ }>;
129
+ }>;
130
+ }
131
+
132
+ export const defaultAsgardThemeContextValue: AsgardThemeContextValue = {
133
+ chatbot: {
134
+ width: '375px',
135
+ height: '640px',
136
+ backgroundColor: 'var(--asg-color-bg)',
137
+ borderColor: 'var(--asg-color-border)',
138
+ borderRadius: 'var(--asg-radius-md)',
139
+ contentMaxWidth: '1200px',
140
+ style: {},
141
+ header: {
142
+ style: {},
143
+ title: {
144
+ style: {},
145
+ },
146
+ actionButton: {
147
+ style: {},
148
+ },
149
+ },
150
+ body: {
151
+ style: {},
152
+ },
153
+ footer: {
154
+ style: {},
155
+ textArea: {
156
+ style: {},
157
+ '::placeholder': {
158
+ color: 'var(--asg-color-text-placeholder)',
159
+ },
160
+ },
161
+ submitButton: {
162
+ style: {},
163
+ },
164
+ speechInputButton: {
165
+ style: {},
166
+ },
167
+ },
168
+ },
169
+ botMessage: {
170
+ color: 'var(--asg-color-text)',
171
+ backgroundColor: 'var(--asg-color-secondary)',
172
+ },
173
+ userMessage: {
174
+ color: 'var(--asg-color-text)',
175
+ backgroundColor: 'var(--asg-color-primary)',
176
+ },
177
+ template: {
178
+ quickReplies: {
179
+ style: {},
180
+ button: {
181
+ style: {},
182
+ },
183
+ },
184
+ time: {
185
+ style: {},
186
+ },
187
+ TextMessageTemplate: {
188
+ style: {},
189
+ },
190
+ HintMessageTemplate: {
191
+ style: {},
192
+ },
193
+ ImageMessageTemplate: {
194
+ style: {},
195
+ },
196
+ VideoMessageTemplate: {
197
+ style: {},
198
+ },
199
+ AudioMessageTemplate: {
200
+ style: {},
201
+ },
202
+ LocationMessageTemplate: {
203
+ style: {},
204
+ },
205
+ ChartMessageTemplate: {
206
+ style: {},
207
+ },
208
+ ButtonMessageTemplate: {
209
+ style: {},
210
+ button: {
211
+ style: {
212
+ border: '1px solid var(--asg-color-border)',
213
+ },
214
+ },
215
+ },
216
+ CarouselMessageTemplate: {
217
+ style: {},
218
+ card: {
219
+ style: {},
220
+ button: {
221
+ style: {
222
+ border: '1px solid var(--asg-color-border)',
223
+ },
224
+ },
225
+ },
226
+ },
227
+ },
228
+ };
229
+
230
+ export const AsgardThemeContext = createContext<AsgardThemeContextValue>(
231
+ defaultAsgardThemeContextValue
232
+ );
233
+
234
+ export function AsgardThemeContextProvider(
235
+ props: PropsWithChildren<{
236
+ theme?: Partial<AsgardThemeContextValue>;
237
+ }>
238
+ ): ReactNode {
239
+ const { children, theme = {} } = props;
240
+ const {
241
+ data: { annotations },
242
+ } = useAsgardAppInitializationContext();
243
+
244
+ const deepMergeTheme = useCallback(
245
+ function () {
246
+ /**
247
+ * Orders of theme (high to low):
248
+ * 1. Theme from props
249
+ * 2. Theme from annotations
250
+ * 3. Default theme
251
+ */
252
+
253
+ const themeFromAnnotations: Annotations['embedConfig']['theme'] =
254
+ annotations?.embedConfig?.theme ?? {
255
+ chatbot: {},
256
+ botMessage: {},
257
+ userMessage: {},
258
+ };
259
+
260
+ const tempTheme = deepMerge(defaultAsgardThemeContextValue, {
261
+ chatbot: {
262
+ backgroundColor: themeFromAnnotations.chatbot?.backgroundColor,
263
+ borderColor: themeFromAnnotations.chatbot?.borderColor,
264
+ header: {
265
+ style: {
266
+ borderBottomColor: themeFromAnnotations.chatbot?.borderColor,
267
+ },
268
+ title: {
269
+ style: {
270
+ color:
271
+ themeFromAnnotations.chatbot?.primaryComponent
272
+ ?.secondaryColor, // Title text color
273
+ },
274
+ },
275
+ actionButton: {
276
+ style: {
277
+ color: themeFromAnnotations.chatbot?.inactiveColor,
278
+ },
279
+ },
280
+ },
281
+ body: {
282
+ style: {
283
+ // Time/timestamp text color
284
+ color: themeFromAnnotations.chatbot?.inactiveColor,
285
+ },
286
+ },
287
+ footer: {
288
+ style: {
289
+ borderTopColor: themeFromAnnotations.chatbot?.borderColor,
290
+ },
291
+ textArea: {
292
+ style: {
293
+ color: themeFromAnnotations.chatbot?.inactiveColor,
294
+ backgroundColor: themeFromAnnotations.chatbot?.backgroundColor,
295
+ },
296
+ '::placeholder': {
297
+ color: themeFromAnnotations.chatbot?.inactiveColor,
298
+ },
299
+ },
300
+ submitButton: {
301
+ style: {
302
+ color:
303
+ themeFromAnnotations.chatbot?.primaryComponent
304
+ ?.secondaryColor,
305
+ },
306
+ },
307
+ speechInputButton: {
308
+ style: {
309
+ color:
310
+ themeFromAnnotations.chatbot?.primaryComponent
311
+ ?.secondaryColor,
312
+ },
313
+ },
314
+ },
315
+ },
316
+ botMessage: {
317
+ backgroundColor: themeFromAnnotations.botMessage?.backgroundColor, // #585858
318
+ color: themeFromAnnotations.botMessage?.color,
319
+ },
320
+ userMessage: {
321
+ backgroundColor: themeFromAnnotations.userMessage?.backgroundColor,
322
+ color: themeFromAnnotations.userMessage?.color,
323
+ },
324
+ template: {
325
+ quickReplies: {
326
+ button: {
327
+ style: {
328
+ color:
329
+ themeFromAnnotations.chatbot?.primaryComponent
330
+ ?.secondaryColor, // Button text (#FFFFFF)
331
+ borderColor: themeFromAnnotations.chatbot?.borderColor,
332
+ backgroundColor: themeFromAnnotations.botMessage
333
+ ?.backgroundColor
334
+ ? `${themeFromAnnotations.botMessage.backgroundColor}33`
335
+ : undefined,
336
+ },
337
+ },
338
+ },
339
+ time: {
340
+ style: {
341
+ color: themeFromAnnotations.chatbot?.inactiveColor,
342
+ },
343
+ },
344
+ TextMessageTemplate: {
345
+ style: {
346
+ // For unset messages
347
+ color:
348
+ themeFromAnnotations.chatbot?.primaryComponent?.secondaryColor,
349
+ },
350
+ },
351
+ ButtonMessageTemplate: {
352
+ button: {
353
+ style: {
354
+ borderColor: themeFromAnnotations.chatbot?.borderColor,
355
+ backgroundColor:
356
+ themeFromAnnotations.chatbot?.primaryComponent?.mainColor,
357
+ color:
358
+ themeFromAnnotations.chatbot?.primaryComponent
359
+ ?.secondaryColor,
360
+ },
361
+ },
362
+ },
363
+ CarouselMessageTemplate: {
364
+ card: {
365
+ style: {
366
+ backgroundColor:
367
+ themeFromAnnotations.botMessage
368
+ ?.carouselButtonBackgroundColor,
369
+ },
370
+ button: {
371
+ style: {
372
+ borderColor: themeFromAnnotations.chatbot?.borderColor,
373
+ backgroundColor:
374
+ themeFromAnnotations.chatbot?.primaryComponent?.mainColor,
375
+ color:
376
+ themeFromAnnotations.chatbot?.primaryComponent
377
+ ?.secondaryColor,
378
+ },
379
+ },
380
+ },
381
+ },
382
+ },
383
+ });
384
+
385
+ return deepMerge(tempTheme, theme);
386
+ },
387
+ [theme, annotations?.embedConfig?.theme]
388
+ );
389
+
390
+ const value = useMemo(() => deepMergeTheme(), [deepMergeTheme]);
391
+
392
+ return (
393
+ <AsgardThemeContext.Provider value={value}>
394
+ {children}
395
+ </AsgardThemeContext.Provider>
396
+ );
397
+ }
398
+
399
+ export function useAsgardThemeContext(): AsgardThemeContextValue {
400
+ return useContext(AsgardThemeContext);
401
+ }
@@ -0,0 +1,4 @@
1
+ export * from './asgard-service-context';
2
+ export * from './asgard-template-context';
3
+ export * from './asgard-theme-context';
4
+ export * from './asgard-app-initialization-context';
@@ -0,0 +1,11 @@
1
+ export * from './use-asgard-service-client';
2
+ export * from './use-channel';
3
+ export * from './use-debounce';
4
+ export * from './use-viewport-size';
5
+ export * from './use-is-on-screen-keyboard-open';
6
+ export * from './use-on-screen-keyboard-scroll-fix';
7
+ export * from './use-prevent-over-scrolling';
8
+ export * from './use-update-vh';
9
+ export * from './use-resize-observer';
10
+ export * from './use-deep-compare-memo';
11
+ export * from 'src/components/templates/text-template/use-react-markdown-renderer';
@@ -0,0 +1,68 @@
1
+ import { ClientConfig, AsgardServiceClient, EventType } from '@asgard-js/core';
2
+ import { useEffect, useRef } from 'react';
3
+
4
+ interface UseAsgardServiceClientProps {
5
+ config: ClientConfig;
6
+ }
7
+
8
+ export function useAsgardServiceClient(
9
+ props: UseAsgardServiceClientProps
10
+ ): AsgardServiceClient | null {
11
+ const { config } = props;
12
+
13
+ const { onRunInit, onProcess, onMessage, onToolCall, onRunDone, onRunError } =
14
+ config;
15
+
16
+ const clientRef = useRef<AsgardServiceClient | null>(null);
17
+
18
+ if (!clientRef.current) {
19
+ clientRef.current = new AsgardServiceClient(config);
20
+ }
21
+
22
+ useEffect(() => {
23
+ return (): void => {
24
+ if (clientRef.current) {
25
+ clientRef.current.close();
26
+ clientRef.current = null;
27
+ }
28
+ };
29
+ }, []);
30
+
31
+ useEffect(() => {
32
+ if (!clientRef.current || !onRunInit) return;
33
+
34
+ clientRef.current.on(EventType.INIT, onRunInit);
35
+ }, [onRunInit]);
36
+
37
+ useEffect(() => {
38
+ if (!clientRef.current || !onProcess) return;
39
+
40
+ clientRef.current.on(EventType.PROCESS, onProcess);
41
+ }, [onProcess]);
42
+
43
+ useEffect(() => {
44
+ if (!clientRef.current || !onMessage) return;
45
+
46
+ clientRef.current.on(EventType.MESSAGE, onMessage);
47
+ }, [onMessage]);
48
+
49
+ useEffect(() => {
50
+ if (!clientRef.current || !onToolCall) return;
51
+
52
+ clientRef.current.on(EventType.TOOL_CALL, onToolCall);
53
+ }, [onToolCall]);
54
+
55
+ useEffect(() => {
56
+ if (!clientRef.current || !onRunDone) return;
57
+
58
+ clientRef.current.on(EventType.DONE, onRunDone);
59
+ }, [onRunDone]);
60
+
61
+ useEffect(() => {
62
+ if (!clientRef.current || !onRunError) return;
63
+
64
+ clientRef.current.on(EventType.ERROR, onRunError);
65
+ }, [onRunError]);
66
+
67
+ return clientRef.current;
68
+ }
@@ -0,0 +1,154 @@
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' | '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' | 'payload'>) => void;
34
+ resetChannel?: (payload?: Pick<FetchSsePayload, 'text' | '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' | '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) => {
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' | 'payload'>) =>
122
+ channel?.sendMessage(payload),
123
+ [channel]
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
+ }
@@ -0,0 +1,18 @@
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
+ }
@@ -0,0 +1,19 @@
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
+ }