@clikvn/agent-widget-embedded 0.0.11-dev → 0.0.12-dev

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 (52) hide show
  1. package/dist/components/Chat/MultimodalInput.d.ts.map +1 -1
  2. package/dist/index.html +52 -12
  3. package/dist/web.js +1 -1
  4. package/package.json +3 -3
  5. package/.eslintrc +0 -34
  6. package/.prettierrc +0 -8
  7. package/src/assets/common.css +0 -148
  8. package/src/assets/tailwindcss.css +0 -3
  9. package/src/commons/constants/index.ts +0 -1
  10. package/src/commons/constants/variables.ts +0 -25
  11. package/src/components/Agent/index.tsx +0 -14
  12. package/src/components/Chat/AudioPlayer.tsx +0 -44
  13. package/src/components/Chat/Chat.tsx +0 -91
  14. package/src/components/Chat/Icons.tsx +0 -1796
  15. package/src/components/Chat/Markdown.tsx +0 -335
  16. package/src/components/Chat/Message.tsx +0 -217
  17. package/src/components/Chat/MultimodalInput.tsx +0 -505
  18. package/src/components/Chat/Overview.tsx +0 -46
  19. package/src/components/Chat/PreviewAttachment.tsx +0 -46
  20. package/src/components/Chat/SuggestedActions.tsx +0 -99
  21. package/src/components/Chat/ui/Button.tsx +0 -55
  22. package/src/components/Chat/ui/Textarea.tsx +0 -23
  23. package/src/constants.ts +0 -1
  24. package/src/env.d.ts +0 -10
  25. package/src/features/AgentWidget/index.tsx +0 -63
  26. package/src/global.d.ts +0 -1
  27. package/src/hooks/useAudioRecording.ts +0 -50
  28. package/src/hooks/useChat.ts +0 -262
  29. package/src/hooks/useChatData.tsx +0 -68
  30. package/src/hooks/useConfiguration.tsx +0 -63
  31. package/src/hooks/useScrollToBottom.ts +0 -31
  32. package/src/index.ts +0 -1
  33. package/src/models/FlowiseClient.ts +0 -103
  34. package/src/models.ts +0 -1
  35. package/src/register.tsx +0 -85
  36. package/src/services/apis.ts +0 -12
  37. package/src/services/bot.service.ts +0 -15
  38. package/src/services/chat.service.ts +0 -199
  39. package/src/types/bot.type.ts +0 -10
  40. package/src/types/chat.type.ts +0 -11
  41. package/src/types/common.type.ts +0 -24
  42. package/src/types/flowise.type.ts +0 -108
  43. package/src/types/user.type.ts +0 -15
  44. package/src/types.ts +0 -0
  45. package/src/utils/audioRecording.ts +0 -371
  46. package/src/utils/commonUtils.ts +0 -47
  47. package/src/utils/functionUtils.ts +0 -17
  48. package/src/utils/requestUtils.ts +0 -113
  49. package/src/utils/streamUtils.ts +0 -18
  50. package/src/web.ts +0 -6
  51. package/src/window.ts +0 -43
  52. package/tsconfig.json +0 -24
@@ -1,505 +0,0 @@
1
- import { motion } from 'framer-motion';
2
- import {
3
- type ChangeEvent,
4
- FC,
5
- useCallback,
6
- useEffect,
7
- useRef,
8
- useState,
9
- } from 'react';
10
- import { useLocalStorage, useWindowSize } from 'usehooks-ts';
11
- import { BotType } from '../../types/bot.type';
12
- import { ChatMessageType, IFileUpload } from '../../types/flowise.type';
13
- import {
14
- cn,
15
- generateExtendedFileName,
16
- generateUUID,
17
- } from '../../utils/commonUtils';
18
- import {
19
- ArrowUpIcon,
20
- CheckCirclFillIcon,
21
- ClikCloseIcon,
22
- ClikMessageIcon,
23
- ClikMicrophoneIcon,
24
- ClikPlusIcon,
25
- ClikVolumeIcon,
26
- ClikWaveIcon,
27
- StopIcon,
28
- } from './Icons';
29
- import { PreviewAttachment } from './PreviewAttachment';
30
-
31
- import { createAttachments } from '../../services/chat.service';
32
- import { Button } from './ui/Button';
33
- import { Textarea } from './ui/Textarea';
34
- import { useAudioRecording } from '../../hooks/useAudioRecording';
35
- import { useConfiguration } from '../../hooks/useConfiguration';
36
- import { SuggestionType } from 'types/common.type';
37
- import SuggestedActions from './SuggestedActions';
38
- import { LAYOUT_MODE } from 'commons/constants';
39
-
40
- const DEFAULT_COLOR_ICON = '#595959';
41
- const ACTIVE_COLOR_ICON = '#0A82F7';
42
-
43
- type PropsType = {
44
- input: string;
45
- setInput: (value: string) => void;
46
- isLoading: boolean;
47
- stop: () => void;
48
- messages: ChatMessageType[];
49
- setMessages: (messages: ChatMessageType[]) => void;
50
- chatId: string;
51
- handleSubmit: (
52
- event?: { preventDefault?: () => void },
53
- files?: IFileUpload[]
54
- ) => void;
55
- className?: string;
56
- append?: (message: ChatMessageType) => Promise<void>;
57
- attachments?: IFileUpload[];
58
- setAttachments?: (func: (files: IFileUpload[]) => IFileUpload[]) => void;
59
- bot: BotType | null;
60
- apiHost: string;
61
- setEnableTTS: (value: boolean) => void;
62
- enableTTS: boolean;
63
- suggestedActions?: SuggestionType[];
64
- };
65
-
66
- export const MultimodalInput: FC<PropsType> = ({
67
- input,
68
- setInput,
69
- isLoading,
70
- stop,
71
- messages,
72
- setMessages,
73
- chatId,
74
- handleSubmit,
75
- className,
76
- append,
77
- attachments,
78
- setAttachments,
79
- bot,
80
- apiHost,
81
- setEnableTTS,
82
- enableTTS,
83
- suggestedActions,
84
- }) => {
85
- const { theme } = useConfiguration();
86
- const {
87
- isRecording,
88
- setIsRecording,
89
- onRecordingCancelled,
90
- onRecordingStopped,
91
- elapsedTime,
92
- isLoadingRecording,
93
- } = useAudioRecording();
94
- const textareaRef = useRef<HTMLTextAreaElement | null>(null);
95
- const { width } = useWindowSize();
96
-
97
- const defaultRows = theme?.suggestion?.defaultRows || 1;
98
- const expandedRows = theme?.suggestion?.expandedRows || 2;
99
- const suggestedActionLayoutMode =
100
- theme?.suggestion?.layoutMode || LAYOUT_MODE.SCROLL;
101
-
102
- const [suggestedActionRows, setSuggestedActionRows] =
103
- useState<number>(defaultRows); // only use for scroll mode SuggestedActions
104
-
105
- useEffect(() => {
106
- if (textareaRef.current) {
107
- adjustHeight();
108
- }
109
- }, []);
110
-
111
- const adjustHeight = () => {
112
- if (textareaRef.current) {
113
- (textareaRef.current as HTMLTextAreaElement).style.height = 'auto';
114
- (textareaRef.current as HTMLTextAreaElement).style.height =
115
- `${textareaRef.current?.scrollHeight + 2}px`;
116
- }
117
- };
118
-
119
- const [localStorageInput, setLocalStorageInput] = useLocalStorage(
120
- 'input',
121
- ''
122
- );
123
-
124
- useEffect(() => {
125
- if (textareaRef.current) {
126
- const domValue = textareaRef.current?.value;
127
- // Prefer DOM value over localStorage to handle hydration
128
- const finalValue = domValue || localStorageInput || '';
129
- setInput(finalValue);
130
- adjustHeight();
131
- }
132
- // Only run once after hydration
133
- // eslint-disable-next-line react-hooks/exhaustive-deps
134
- }, []);
135
-
136
- useEffect(() => {
137
- setLocalStorageInput(input);
138
- }, [input, setLocalStorageInput]);
139
-
140
- const handleInput = (event: ChangeEvent<HTMLTextAreaElement>) => {
141
- setInput(event.target.value);
142
- adjustHeight();
143
- if (suggestedActionRows !== defaultRows)
144
- setSuggestedActionRows(defaultRows);
145
- };
146
-
147
- const fileInputRef = useRef<HTMLInputElement | null>(null);
148
- const [uploadQueue, setUploadQueue] = useState<Array<string>>([]);
149
-
150
- const submitForm = useCallback(async () => {
151
- handleSubmit(undefined, attachments);
152
- setLocalStorageInput('');
153
- if (setAttachments) {
154
- setAttachments((_) => []);
155
- if (fileInputRef.current) {
156
- (fileInputRef.current as HTMLInputElement).value = '';
157
- }
158
- }
159
- if (width && width > 768) {
160
- textareaRef.current?.focus();
161
- }
162
- }, [handleSubmit, setLocalStorageInput, width, attachments, chatId, bot]);
163
-
164
- const uploadFile = useCallback(
165
- async (file: File) => {
166
- const formData = new FormData();
167
- formData.append('file', file, generateExtendedFileName(file.name));
168
- try {
169
- return await createAttachments({
170
- chatId,
171
- apiHost,
172
- body: formData,
173
- });
174
- } catch (error) {
175
- console.error('Failed to upload file, please try again!');
176
- }
177
- },
178
- [chatId]
179
- );
180
-
181
- const toAudioBase64 = (blob: Blob) => {
182
- return new Promise<IFileUpload>((resolve) => {
183
- let mimeType = '';
184
- const pos = blob.type.indexOf(';');
185
- if (pos === -1) {
186
- mimeType = blob.type;
187
- } else {
188
- mimeType = blob.type.substring(0, pos);
189
- }
190
-
191
- // read blob and add to previews
192
- const reader = new FileReader();
193
- reader.readAsDataURL(blob);
194
- reader.onloadend = () => {
195
- const base64data = reader.result as string;
196
- const upload: IFileUpload = {
197
- tempId: generateUUID(),
198
- data: base64data,
199
- type: 'audio',
200
- name: `audio_${Date.now()}.wav`,
201
- mime: mimeType,
202
- };
203
- resolve(upload);
204
- };
205
- });
206
- };
207
-
208
- const toBase64 = async (
209
- file: File,
210
- type = 'file'
211
- ): Promise<IFileUpload | undefined> => {
212
- return new Promise((resolve, reject) => {
213
- const reader = new FileReader();
214
- reader.readAsDataURL(file);
215
- reader.onload = () => {
216
- resolve({
217
- tempId: generateUUID(),
218
- data: reader.result as string,
219
- type,
220
- name: generateExtendedFileName(file.name),
221
- mime: file.type,
222
- });
223
- };
224
- reader.onerror = () => {
225
- reject();
226
- };
227
- });
228
- };
229
-
230
- const checkUploadFile = useCallback(
231
- async (file: File): Promise<IFileUpload | undefined> => {
232
- if (file.type.startsWith('image')) {
233
- return toBase64(file);
234
- } else {
235
- setUploadQueue([file.name]);
236
- const response: any = await uploadFile(file);
237
- if (response?.type == 'file:full') {
238
- const f = response.result[0];
239
- if (!f) {
240
- return;
241
- }
242
- return {
243
- tempId: generateUUID(),
244
- data: f.content,
245
- name: f.name,
246
- mime: f.mimeType,
247
- type: response?.type,
248
- };
249
- } else if (response?.type == 'file:rag') {
250
- const { addedDocs } = response.result;
251
- if (!addedDocs.length) {
252
- return;
253
- }
254
- return toBase64(file, response?.type);
255
- }
256
- }
257
- },
258
- [setUploadQueue, uploadFile]
259
- );
260
-
261
- const handleLogicShowFAQ = () => {
262
- setSuggestedActionRows(
263
- suggestedActionRows === defaultRows ? expandedRows : defaultRows
264
- );
265
- };
266
-
267
- const handleFileChange = useCallback(
268
- async (event: ChangeEvent<HTMLInputElement>) => {
269
- const files = Array.from(event.target.files || []);
270
- try {
271
- const uploadPromises = files.map((file) => checkUploadFile(file));
272
- const uploadedAttachments = await Promise.all(uploadPromises);
273
- const successfullyUploadedAttachments = uploadedAttachments.filter(
274
- (attachment) => attachment !== undefined
275
- ) as IFileUpload[];
276
- if (setAttachments) {
277
- setAttachments((currentAttachments: IFileUpload[]) => [
278
- ...currentAttachments,
279
- ...successfullyUploadedAttachments,
280
- ]);
281
- }
282
- } catch (error) {
283
- console.error('Error uploading files!', error);
284
- } finally {
285
- setUploadQueue([]);
286
- }
287
- },
288
- [setAttachments, checkUploadFile]
289
- );
290
-
291
- const handleSubmitRecording = useCallback(
292
- async (blob: Blob) => {
293
- try {
294
- const audioFile = await toAudioBase64(blob);
295
- handleSubmit(undefined, [audioFile]);
296
- setIsRecording(false);
297
- } catch (error) {
298
- console.error('Error uploading files!', error);
299
- }
300
- },
301
- [handleSubmit, setIsRecording]
302
- );
303
-
304
- const handleSend = useCallback(async () => {
305
- if (isRecording) {
306
- onRecordingStopped(handleSubmitRecording);
307
- } else {
308
- submitForm();
309
- }
310
- }, [submitForm, onRecordingStopped, handleSubmitRecording]);
311
-
312
- return (
313
- <div className="relative w-full flex flex-col gap-4">
314
- {!!suggestedActions?.length && (
315
- <SuggestedActions
316
- suggestedActions={suggestedActions}
317
- append={append}
318
- layoutMode={suggestedActionLayoutMode}
319
- suggestedActionRows={suggestedActionRows}
320
- />
321
- )}
322
-
323
- <input
324
- type="file"
325
- className="fixed -top-4 -left-4 size-0.5 opacity-0 pointer-events-none"
326
- ref={fileInputRef}
327
- multiple
328
- onChange={handleFileChange}
329
- tabIndex={-1}
330
- />
331
-
332
- {((attachments && attachments.length > 0) || uploadQueue.length > 0) && (
333
- <div className="flex flex-row gap-2 overflow-x-scroll items-end">
334
- {attachments?.map((attachment) => (
335
- <PreviewAttachment
336
- key={attachment.tempId}
337
- attachment={attachment}
338
- />
339
- ))}
340
-
341
- {uploadQueue.map((filename) => (
342
- <PreviewAttachment
343
- key={filename}
344
- attachment={{
345
- data: '',
346
- name: filename,
347
- mime: '',
348
- type: '',
349
- }}
350
- isUploading
351
- />
352
- ))}
353
- </div>
354
- )}
355
- <Textarea
356
- ref={textareaRef}
357
- placeholder={theme?.input?.placeholder || 'Send a message...'}
358
- value={input}
359
- onChange={handleInput}
360
- className={cn(
361
- 'min-h-[24px] max-h-[calc(75dvh)] overflow-hidden resize-none rounded-xl text-base bg-muted bg-[#ffffff]',
362
- className
363
- )}
364
- rows={3}
365
- autoFocus
366
- onKeyDown={(event) => {
367
- if (event.key === 'Enter' && !event.shiftKey) {
368
- event.preventDefault();
369
- if (isLoading) {
370
- console.error(
371
- 'Please wait for the model to finish its response!'
372
- );
373
- } else if (uploadQueue.length) {
374
- console.error('Please wait for file is uploading!');
375
- } else {
376
- handleSend();
377
- }
378
- }
379
- }}
380
- />
381
-
382
- {isLoading ? (
383
- <Button
384
- className="rounded-full p-1.5 h-fit absolute bottom-2 right-2 m-0.5 border dark:border-zinc-600 bg-[#000000D9]"
385
- onClick={(event) => {
386
- event.preventDefault();
387
- stop();
388
- setMessages(messages);
389
- }}
390
- >
391
- <StopIcon size={14} />
392
- </Button>
393
- ) : (
394
- <Button
395
- className="rounded-full p-1.5 h-fit absolute bottom-2 right-2 m-0.5 border dark:border-zinc-600 bg-[#000000D9]"
396
- onClick={(event) => {
397
- event.preventDefault();
398
- handleSend();
399
- }}
400
- disabled={
401
- !isRecording && (input.length === 0 || !!uploadQueue.length)
402
- } // zero input or uploading
403
- >
404
- <ArrowUpIcon size={14} />
405
- </Button>
406
- )}
407
-
408
- <div className="absolute left-2 right-2 bottom-2 flex items-center">
409
- <Button
410
- className="rounded-full p-1.5 h-fit m-0.5 dark:border-zinc-700"
411
- onClick={(event) => {
412
- event.preventDefault();
413
- fileInputRef.current?.click();
414
- }}
415
- variant="outline"
416
- disabled={isLoading || isRecording}
417
- >
418
- <ClikPlusIcon size={14} />
419
- </Button>
420
-
421
- <Button
422
- className={`rounded-full py-1 px-2 h-fit m-0.5 dark:border-zinc-700 text-[#595959] ${suggestedActionRows === expandedRows && 'border border-[#B9C6D6] bg-[#CBE1F3] text-[#0A82F7]'} `}
423
- onClick={(event) => {
424
- event.preventDefault();
425
- handleLogicShowFAQ();
426
- }}
427
- variant="outline"
428
- disabled={isLoading || isRecording}
429
- >
430
- <ClikMessageIcon
431
- color={
432
- suggestedActionRows === expandedRows
433
- ? ACTIVE_COLOR_ICON
434
- : DEFAULT_COLOR_ICON
435
- }
436
- />
437
- FAQ
438
- </Button>
439
-
440
- {isRecording ? (
441
- <>
442
- <div
443
- className="rounded-[100px] flex items-center bg-[#F3F3F3] gap-2 p-2"
444
- data-testid="input"
445
- >
446
- <Button
447
- className="rounded-full dark:border-zinc-700 p-0 h-6"
448
- variant="outline"
449
- onClick={(event) => {
450
- event.preventDefault();
451
- onRecordingCancelled();
452
- }}
453
- >
454
- <ClikCloseIcon className="!w-full !h-full" />
455
- </Button>
456
- <div className="flex items-center gap-2 ">
457
- <span>
458
- <ClikWaveIcon />
459
- </span>
460
- <span>{elapsedTime || '00:00'}</span>
461
- {isLoadingRecording && (
462
- <span className="ml-1.5">Sending...</span>
463
- )}
464
- </div>
465
- <Button
466
- className="rounded-full dark:border-zinc-700 p-0 h-6"
467
- variant="outline"
468
- onClick={(event) => {
469
- event.preventDefault();
470
- }}
471
- >
472
- <CheckCirclFillIcon className="!w-full !h-full" />
473
- </Button>
474
- </div>
475
- </>
476
- ) : (
477
- <div>
478
- <Button
479
- className={`rounded-full py-1 px-2 h-fit m-0.5 dark:border-zinc-700 text-[#595959] ${enableTTS ? 'text-white hover:bg-primary/90 bg-primary' : ''}`}
480
- onClick={(event) => {
481
- event.preventDefault();
482
- setEnableTTS(!enableTTS);
483
- }}
484
- variant="outline"
485
- disabled={isLoading}
486
- >
487
- <ClikVolumeIcon /> Speak
488
- </Button>
489
- <Button
490
- className="rounded-full py-1 px-2 gap-[4px] h-fit m-0.5 dark:border-zinc-700 text-[#595959]"
491
- onClick={(event) => {
492
- event.preventDefault();
493
- setIsRecording(true);
494
- }}
495
- variant="outline"
496
- disabled={isLoading}
497
- >
498
- <ClikMicrophoneIcon size={14} /> Talk
499
- </Button>
500
- </div>
501
- )}
502
- </div>
503
- </div>
504
- );
505
- };
@@ -1,46 +0,0 @@
1
- import { motion } from 'framer-motion';
2
- import { FC } from 'react';
3
- import { BotType } from '../../types/bot.type';
4
- import { useConfiguration } from '../../hooks/useConfiguration';
5
-
6
- type PropsType = {
7
- bot: BotType | null;
8
- };
9
- export const Overview: FC<PropsType> = ({ bot }: PropsType) => {
10
- const { theme } = useConfiguration();
11
- return (
12
- <motion.div
13
- key="overview"
14
- className="max-w-3xl m-auto md:mt-20"
15
- initial={{ opacity: 0, scale: 0.98 }}
16
- animate={{ opacity: 1, scale: 1 }}
17
- exit={{ opacity: 0, scale: 0.98 }}
18
- transition={{ delay: 0.5 }}
19
- >
20
- <div className="rounded-xl p-6 flex flex-col gap-2 leading-relaxed text-center max-w-xl">
21
- <p className="flex flex-row justify-center gap-4 items-center"></p>
22
- {!theme?.overview ? (
23
- <>
24
- {bot?.avatar && (
25
- <img
26
- src={bot?.avatar}
27
- alt={bot?.name ?? 'Avatar'}
28
- width={40}
29
- height={40}
30
- className="rounded-full m-auto"
31
- />
32
- )}
33
- <p className="font-semibold text-xl">{bot?.name}</p>
34
- </>
35
- ) : (
36
- <>
37
- <p className="font-semibold text-xl">{theme?.overview?.title}</p>
38
- <p>{theme?.overview?.description}</p>
39
- </>
40
- )}
41
- </div>
42
- </motion.div>
43
- );
44
- };
45
-
46
- export default Overview;
@@ -1,46 +0,0 @@
1
- import { IFileUpload } from '../../types/flowise.type';
2
- import { LoaderIcon } from './Icons';
3
- import React from 'react';
4
-
5
- export const PreviewAttachment = ({
6
- attachment,
7
- isUploading = false,
8
- }: {
9
- attachment: IFileUpload;
10
- isUploading?: boolean;
11
- }) => {
12
- const { name, data, mime, tempId } = attachment;
13
- return (
14
- <div className="flex flex-col gap-2">
15
- <div className="w-30 p-0 max-w-[400px] bg-muted rounded-md relative flex flex-col items-center justify-center">
16
- {data ? (
17
- mime.startsWith('audio') ? (
18
- <audio controls>
19
- <source src={data} type={mime} />
20
- </audio>
21
- ) : mime.startsWith('image') ? (
22
- <img
23
- key={tempId}
24
- src={data}
25
- alt={name ?? 'An image attachment'}
26
- className="rounded-md size-full object-contain"
27
- />
28
- ) : (
29
- <div className="w-full text-left truncate">{name}</div>
30
- )
31
- ) : (
32
- <div className="w-full text-left truncate">{name}</div>
33
- )}
34
-
35
- {isUploading && (
36
- <div className="animate-spin absolute text-zinc-500">
37
- <LoaderIcon />
38
- </div>
39
- )}
40
- </div>
41
- {/*<div className="text-xs text-zinc-500 max-w-[200px] truncate ">*/}
42
- {/* {name}*/}
43
- {/*</div>*/}
44
- </div>
45
- );
46
- };
@@ -1,99 +0,0 @@
1
- import React from 'react';
2
- import { motion } from 'framer-motion';
3
- import { SuggestionType } from 'types/common.type';
4
- import { ChatMessageType } from 'types/flowise.type';
5
- import { LAYOUT_MODE } from 'commons/constants';
6
-
7
- interface SuggestedActionsProps {
8
- suggestedActions?: SuggestionType[];
9
- append?: (message: ChatMessageType) => Promise<void>;
10
- layoutMode?: string;
11
- suggestedActionRows?: number;
12
- }
13
-
14
- const SuggestedActions: React.FC<SuggestedActionsProps> = ({
15
- suggestedActions,
16
- append,
17
- layoutMode = LAYOUT_MODE.SCROLL,
18
- suggestedActionRows = 1,
19
- }) => {
20
- if (!suggestedActions?.length) return null;
21
-
22
- const containerHeight = suggestedActionRows * 60;
23
-
24
- const animation = {
25
- initial: { opacity: 0, y: 20 },
26
- animate: { opacity: 1, y: 0 },
27
- exit: { opacity: 0, y: 20 },
28
- };
29
-
30
- return layoutMode === LAYOUT_MODE.GRID ? (
31
- // UI Grid (2 columns)
32
- <div className="grid sm:grid-cols-2 gap-2 w-full">
33
- {suggestedActions.map((suggestedAction, index) => (
34
- <motion.div
35
- key={`suggested-action-${suggestedAction.title}-${index}`}
36
- {...animation}
37
- transition={{ delay: 0.05 * index }}
38
- className={index > 1 ? 'hidden sm:block' : 'block'}
39
- >
40
- <ActionButton suggestedAction={suggestedAction} append={append} />
41
- </motion.div>
42
- ))}
43
- </div>
44
- ) : (
45
- // UI Scroll
46
- <div
47
- className="w-full overflow-x-auto scrollbar-hide"
48
- style={{ maxHeight: `${containerHeight}px` }}
49
- >
50
- <div
51
- style={{
52
- gap: '8px',
53
- display: 'grid',
54
- gridAutoFlow: 'column',
55
- gridTemplateRows: `repeat(${suggestedActionRows}, minmax(0, 1fr))`,
56
- }}
57
- >
58
- {suggestedActions.map((suggestedAction, index) => (
59
- <motion.div
60
- key={`suggested-action-${suggestedAction.title}-${index}`}
61
- {...animation}
62
- transition={{ delay: 0.05 * index }}
63
- >
64
- <ActionButton suggestedAction={suggestedAction} append={append} />
65
- </motion.div>
66
- ))}
67
- </div>
68
- </div>
69
- );
70
- };
71
-
72
- interface ActionButtonProps {
73
- suggestedAction: SuggestionType;
74
- append?: (message: ChatMessageType) => Promise<void>;
75
- }
76
-
77
- const ActionButton: React.FC<ActionButtonProps> = ({
78
- suggestedAction,
79
- append,
80
- }) => (
81
- <button
82
- onClick={(e) => {
83
- e.preventDefault();
84
- if (append) {
85
- append({ role: 'apiMessage', content: suggestedAction.action });
86
- }
87
- }}
88
- className="text-left border rounded-xl px-4 py-3.5 text-sm flex-1 gap-1 sm:flex-col w-full h-auto justify-start items-start"
89
- >
90
- <span className="font-medium overflow-hidden whitespace-nowrap text-ellipsis w-full group-hover:overflow-visible group-hover:whitespace-normal">
91
- {suggestedAction.title}
92
- </span>
93
- {!!suggestedAction.label && (
94
- <span className="text-muted-foreground">{suggestedAction.label}</span>
95
- )}
96
- </button>
97
- );
98
-
99
- export default SuggestedActions;