@clikvn/agent-widget-embedded 0.0.9-dev → 0.0.11-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 (45) hide show
  1. package/dist/commons/constants/variables.d.ts +4 -0
  2. package/dist/commons/constants/variables.d.ts.map +1 -1
  3. package/dist/components/Chat/Chat.d.ts.map +1 -1
  4. package/dist/components/Chat/Icons.d.ts +19 -1
  5. package/dist/components/Chat/Icons.d.ts.map +1 -1
  6. package/dist/components/Chat/Message.d.ts.map +1 -1
  7. package/dist/components/Chat/MultimodalInput.d.ts +3 -1
  8. package/dist/components/Chat/MultimodalInput.d.ts.map +1 -1
  9. package/dist/components/Chat/SuggestedActions.d.ts +12 -0
  10. package/dist/components/Chat/SuggestedActions.d.ts.map +1 -0
  11. package/dist/features/AgentWidget/index.d.ts +8 -2
  12. package/dist/features/AgentWidget/index.d.ts.map +1 -1
  13. package/dist/hooks/useChat.d.ts +2 -0
  14. package/dist/hooks/useChat.d.ts.map +1 -1
  15. package/dist/hooks/useConfiguration.d.ts +8 -1
  16. package/dist/hooks/useConfiguration.d.ts.map +1 -1
  17. package/dist/index.html +12 -51
  18. package/dist/services/apis.d.ts +1 -0
  19. package/dist/services/apis.d.ts.map +1 -1
  20. package/dist/services/chat.service.d.ts +7 -1
  21. package/dist/services/chat.service.d.ts.map +1 -1
  22. package/dist/types/common.type.d.ts +6 -0
  23. package/dist/types/common.type.d.ts.map +1 -1
  24. package/dist/web.js +1 -1
  25. package/package.json +2 -1
  26. package/src/commons/constants/variables.ts +5 -0
  27. package/src/components/Chat/Chat.tsx +2 -0
  28. package/src/components/Chat/Icons.tsx +867 -1
  29. package/src/components/Chat/Message.tsx +42 -16
  30. package/src/components/Chat/MultimodalInput.tsx +140 -107
  31. package/src/components/Chat/SuggestedActions.tsx +99 -0
  32. package/src/features/AgentWidget/index.tsx +8 -2
  33. package/src/hooks/useChat.ts +19 -2
  34. package/src/hooks/useConfiguration.tsx +8 -1
  35. package/src/services/apis.ts +2 -0
  36. package/src/services/chat.service.ts +36 -1
  37. package/src/types/common.type.ts +7 -0
  38. package/dist/components/Chat/AudioRecording.d.ts +0 -9
  39. package/dist/components/Chat/AudioRecording.d.ts.map +0 -1
  40. package/dist/hooks/useConnection.d.ts +0 -15
  41. package/dist/hooks/useConnection.d.ts.map +0 -1
  42. package/dist/services/user.service.d.ts +0 -3
  43. package/dist/services/user.service.d.ts.map +0 -1
  44. package/dist/types/agentType.d.ts +0 -11
  45. package/dist/types/agentType.d.ts.map +0 -1
@@ -11,6 +11,7 @@ import {
11
11
  } from '../../types/flowise.type';
12
12
  import { cn } from '../../utils/commonUtils';
13
13
  import AudioPlayer from './AudioPlayer';
14
+ import { useConfiguration } from 'hooks/useConfiguration';
14
15
 
15
16
  type PropsType = {
16
17
  chatId?: string;
@@ -35,6 +36,7 @@ export const PreviewMessage: FC<PropsType> = ({
35
36
  bot,
36
37
  enableTTS,
37
38
  }) => {
39
+ const { theme } = useConfiguration();
38
40
  const parseOutput = (outputData: any) => {
39
41
  try {
40
42
  return JSON.parse(outputData);
@@ -56,12 +58,21 @@ export const PreviewMessage: FC<PropsType> = ({
56
58
  return { customToolResults, toolResults };
57
59
  }, [message.usedTools]);
58
60
 
61
+ const role = useMemo(() => {
62
+ return getRole(message.role);
63
+ }, [message.role]);
64
+
65
+ const assistantAvatar =
66
+ role === 'user'
67
+ ? theme?.userMessage?.avatarSrc
68
+ : theme?.botMessage?.avatarSrc || bot?.avatar;
69
+
59
70
  return (
60
71
  <motion.div
61
72
  className="w-full mx-auto max-w-3xl px-4 group/message"
62
73
  initial={{ y: 5, opacity: 0 }}
63
74
  animate={{ y: 0, opacity: 1 }}
64
- data-role={getRole(message.role)}
75
+ data-role={role}
65
76
  >
66
77
  <div className="flex justify-end text-[14px]">
67
78
  {message?.fileUploads && !!message.fileUploads.length && (
@@ -70,15 +81,21 @@ export const PreviewMessage: FC<PropsType> = ({
70
81
  </div>
71
82
  <div
72
83
  className={cn(
73
- 'group-data-[role=user]/message:bg-primary group-data-[role=user]/message:text-primary-foreground flex gap-4 group-data-[role=user]/message:px-3 w-full group-data-[role=user]/message:w-fit group-data-[role=user]/message:ml-auto group-data-[role=user]/message:max-w-2xl group-data-[role=user]/message:py-2 rounded-xl'
84
+ `group-data-[role=user]/message:bg-primary group-data-[role=user]/message:text-primary-foreground flex gap-4 px-3 w-full group-data-[role=user]/message:w-fit group-data-[role=user]/message:ml-auto group-data-[role=user]/message:max-w-2xl py-2 rounded-xl `
74
85
  )}
86
+ style={{
87
+ backgroundColor:
88
+ role === 'user'
89
+ ? theme?.userMessage?.backgroundColor
90
+ : theme?.botMessage?.backgroundColor || undefined,
91
+ }}
75
92
  >
76
- {message.role === 'apiMessage' && (
93
+ {theme?.botMessage?.showAvatar && message.role === 'apiMessage' && (
77
94
  <div className="size-8 flex items-center rounded-full justify-center ring-1 shrink-0 ring-border">
78
- {bot?.avatar ? (
95
+ {assistantAvatar ? (
79
96
  <img
80
- src={bot.avatar}
81
- alt={bot.name ?? 'User Avatar'}
97
+ src={assistantAvatar}
98
+ alt={bot?.name ?? 'User Avatar'}
82
99
  width={24}
83
100
  height={24}
84
101
  className="rounded-full"
@@ -133,7 +150,15 @@ export const PreviewMessage: FC<PropsType> = ({
133
150
  {/* </div>*/}
134
151
  {/* )}*/}
135
152
  {message.content && (
136
- <div className="flex flex-col gap-4">
153
+ <div
154
+ className="flex flex-col gap-4"
155
+ style={{
156
+ color:
157
+ getRole(message.role) === 'user'
158
+ ? theme?.userMessage?.textColor
159
+ : theme?.botMessage?.textColor || undefined,
160
+ }}
161
+ >
137
162
  <Markdown usedTools={message?.usedTools}>
138
163
  {message.content as string}
139
164
  </Markdown>
@@ -151,6 +176,10 @@ export const PreviewMessage: FC<PropsType> = ({
151
176
  export const ThinkingMessage = ({ bot }: { bot: BotType | null }) => {
152
177
  const role = 'assistant';
153
178
 
179
+ const { theme } = useConfiguration();
180
+
181
+ const assistantAvatar = theme?.botMessage?.avatarSrc || bot?.avatar;
182
+
154
183
  return (
155
184
  <motion.div
156
185
  className="w-full mx-auto max-w-3xl px-4 group/message "
@@ -166,20 +195,17 @@ export const ThinkingMessage = ({ bot }: { bot: BotType | null }) => {
166
195
  }
167
196
  )}
168
197
  >
169
- <div className="size-8 flex items-center rounded-full justify-center ring-1 shrink-0 ring-border">
170
- {bot?.avatar ? (
198
+ {theme?.botMessage?.showAvatar && (
199
+ <div className="size-8 flex items-center rounded-full justify-center ring-1 shrink-0 ring-border">
171
200
  <img
172
- src={bot.avatar}
173
- alt={bot.name ?? 'User Avatar'}
201
+ src={assistantAvatar}
202
+ alt={bot?.name ?? 'User Avatar'}
174
203
  width={24}
175
204
  height={24}
176
205
  className="rounded-full"
177
206
  />
178
- ) : (
179
- <SparklesIcon size={14} />
180
- )}
181
- </div>
182
-
207
+ </div>
208
+ )}
183
209
  <div className="flex flex-col gap-2 w-full">
184
210
  <div className="flex flex-col gap-4 text-muted-foreground">
185
211
  Thinking...
@@ -1,3 +1,4 @@
1
+ import { motion } from 'framer-motion';
1
2
  import {
2
3
  type ChangeEvent,
3
4
  FC,
@@ -6,32 +7,38 @@ import {
6
7
  useRef,
7
8
  useState,
8
9
  } from 'react';
9
- import { motion } from 'framer-motion';
10
10
  import { useLocalStorage, useWindowSize } from 'usehooks-ts';
11
+ import { BotType } from '../../types/bot.type';
12
+ import { ChatMessageType, IFileUpload } from '../../types/flowise.type';
11
13
  import {
12
14
  cn,
13
15
  generateExtendedFileName,
14
16
  generateUUID,
15
17
  } from '../../utils/commonUtils';
16
- import { PreviewAttachment } from './PreviewAttachment';
17
18
  import {
18
19
  ArrowUpIcon,
19
- CircleDotIcon,
20
- MicrophoneIcon,
21
- PlusIcon,
20
+ CheckCirclFillIcon,
21
+ ClikCloseIcon,
22
+ ClikMessageIcon,
23
+ ClikMicrophoneIcon,
24
+ ClikPlusIcon,
25
+ ClikVolumeIcon,
26
+ ClikWaveIcon,
22
27
  StopIcon,
23
- TrashIcon,
24
- VolumeIcon,
25
28
  } from './Icons';
26
- import { ChatMessageType, IFileUpload } from '../../types/flowise.type';
27
- import { BotType } from '../../types/bot.type';
29
+ import { PreviewAttachment } from './PreviewAttachment';
28
30
 
29
31
  import { createAttachments } from '../../services/chat.service';
30
32
  import { Button } from './ui/Button';
31
33
  import { Textarea } from './ui/Textarea';
32
34
  import { useAudioRecording } from '../../hooks/useAudioRecording';
33
- import { useChatData } from '../../hooks/useChatData';
34
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';
35
42
 
36
43
  type PropsType = {
37
44
  input: string;
@@ -53,6 +60,7 @@ type PropsType = {
53
60
  apiHost: string;
54
61
  setEnableTTS: (value: boolean) => void;
55
62
  enableTTS: boolean;
63
+ suggestedActions?: SuggestionType[];
56
64
  };
57
65
 
58
66
  export const MultimodalInput: FC<PropsType> = ({
@@ -72,9 +80,9 @@ export const MultimodalInput: FC<PropsType> = ({
72
80
  apiHost,
73
81
  setEnableTTS,
74
82
  enableTTS,
83
+ suggestedActions,
75
84
  }) => {
76
85
  const { theme } = useConfiguration();
77
- const { suggestedActions = [] } = useChatData();
78
86
  const {
79
87
  isRecording,
80
88
  setIsRecording,
@@ -85,6 +93,15 @@ export const MultimodalInput: FC<PropsType> = ({
85
93
  } = useAudioRecording();
86
94
  const textareaRef = useRef<HTMLTextAreaElement | null>(null);
87
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
+
88
105
  useEffect(() => {
89
106
  if (textareaRef.current) {
90
107
  adjustHeight();
@@ -123,6 +140,8 @@ export const MultimodalInput: FC<PropsType> = ({
123
140
  const handleInput = (event: ChangeEvent<HTMLTextAreaElement>) => {
124
141
  setInput(event.target.value);
125
142
  adjustHeight();
143
+ if (suggestedActionRows !== defaultRows)
144
+ setSuggestedActionRows(defaultRows);
126
145
  };
127
146
 
128
147
  const fileInputRef = useRef<HTMLInputElement | null>(null);
@@ -239,6 +258,12 @@ export const MultimodalInput: FC<PropsType> = ({
239
258
  [setUploadQueue, uploadFile]
240
259
  );
241
260
 
261
+ const handleLogicShowFAQ = () => {
262
+ setSuggestedActionRows(
263
+ suggestedActionRows === defaultRows ? expandedRows : defaultRows
264
+ );
265
+ };
266
+
242
267
  const handleFileChange = useCallback(
243
268
  async (event: ChangeEvent<HTMLInputElement>) => {
244
269
  const files = Array.from(event.target.files || []);
@@ -286,37 +311,13 @@ export const MultimodalInput: FC<PropsType> = ({
286
311
 
287
312
  return (
288
313
  <div className="relative w-full flex flex-col gap-4">
289
- {messages.length === 0 && (
290
- <div className="grid sm:grid-cols-2 gap-2 w-full">
291
- {suggestedActions.map((suggestedAction, index) => (
292
- <motion.div
293
- initial={{ opacity: 0, y: 20 }}
294
- animate={{ opacity: 1, y: 0 }}
295
- exit={{ opacity: 0, y: 20 }}
296
- transition={{ delay: 0.05 * index }}
297
- key={`suggested-action-${suggestedAction.title}-${index}`}
298
- className={index > 1 ? 'hidden sm:block' : 'block'}
299
- >
300
- <Button
301
- variant="ghost"
302
- onClick={async () => {
303
- if (append) {
304
- append({
305
- role: 'apiMessage',
306
- content: suggestedAction.action,
307
- });
308
- }
309
- }}
310
- 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"
311
- >
312
- <span className="font-medium">{suggestedAction.title}</span>
313
- <span className="text-muted-foreground">
314
- {suggestedAction.label}
315
- </span>
316
- </Button>
317
- </motion.div>
318
- ))}
319
- </div>
314
+ {!!suggestedActions?.length && (
315
+ <SuggestedActions
316
+ suggestedActions={suggestedActions}
317
+ append={append}
318
+ layoutMode={suggestedActionLayoutMode}
319
+ suggestedActionRows={suggestedActionRows}
320
+ />
320
321
  )}
321
322
 
322
323
  <input
@@ -351,14 +352,13 @@ export const MultimodalInput: FC<PropsType> = ({
351
352
  ))}
352
353
  </div>
353
354
  )}
354
-
355
355
  <Textarea
356
356
  ref={textareaRef}
357
357
  placeholder={theme?.input?.placeholder || 'Send a message...'}
358
358
  value={input}
359
359
  onChange={handleInput}
360
360
  className={cn(
361
- 'min-h-[24px] max-h-[calc(75dvh)] overflow-hidden resize-none rounded-xl text-base bg-muted',
361
+ 'min-h-[24px] max-h-[calc(75dvh)] overflow-hidden resize-none rounded-xl text-base bg-muted bg-[#ffffff]',
362
362
  className
363
363
  )}
364
364
  rows={3}
@@ -381,7 +381,7 @@ export const MultimodalInput: FC<PropsType> = ({
381
381
 
382
382
  {isLoading ? (
383
383
  <Button
384
- className="rounded-full p-1.5 h-fit absolute bottom-2 right-2 m-0.5 border dark:border-zinc-600"
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
385
  onClick={(event) => {
386
386
  event.preventDefault();
387
387
  stop();
@@ -392,7 +392,7 @@ export const MultimodalInput: FC<PropsType> = ({
392
392
  </Button>
393
393
  ) : (
394
394
  <Button
395
- className="rounded-full p-1.5 h-fit absolute bottom-2 right-2 m-0.5 border dark:border-zinc-600"
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
396
  onClick={(event) => {
397
397
  event.preventDefault();
398
398
  handleSend();
@@ -404,69 +404,102 @@ export const MultimodalInput: FC<PropsType> = ({
404
404
  <ArrowUpIcon size={14} />
405
405
  </Button>
406
406
  )}
407
- {isRecording ? (
408
- <>
409
- <div
410
- className="rounded-full bg-background flex absolute p-1 bottom-2 right-[80px]"
411
- data-testid="input"
412
- >
413
- <div className="flex items-center gap-3">
414
- <span>
415
- <CircleDotIcon color="red" />
416
- </span>
417
- <span>{elapsedTime || '00:00'}</span>
418
- {isLoadingRecording && <span className="ml-1.5">Sending...</span>}
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>
419
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>
420
500
  </div>
421
- <Button
422
- className="rounded-full p-1.5 h-fit absolute bottom-2 right-11 m-0.5 dark:border-zinc-700"
423
- variant="outline"
424
- onClick={(event) => {
425
- event.preventDefault();
426
- onRecordingCancelled();
427
- }}
428
- >
429
- <TrashIcon size={14} color="red" />
430
- </Button>
431
- </>
432
- ) : (
433
- <>
434
- <Button
435
- className="rounded-full p-1.5 h-fit absolute bottom-2 right-11 m-0.5 dark:border-zinc-700"
436
- onClick={(event) => {
437
- event.preventDefault();
438
- setIsRecording(true);
439
- }}
440
- variant="outline"
441
- disabled={isLoading}
442
- >
443
- <MicrophoneIcon size={14} />
444
- </Button>
445
- <Button
446
- className={`rounded-full p-1.5 h-fit absolute bottom-2 right-[80px] m-0.5 dark:border-zinc-700 ${enableTTS ? 'text-white hover:bg-primary/90 bg-primary' : ''}`}
447
- onClick={(event) => {
448
- event.preventDefault();
449
- setEnableTTS(!enableTTS);
450
- }}
451
- variant="outline"
452
- disabled={isLoading}
453
- >
454
- <VolumeIcon />
455
- </Button>
456
- </>
457
- )}
458
-
459
- <Button
460
- className="rounded-full p-1.5 h-fit absolute bottom-2 left-2 m-0.5 dark:border-zinc-700"
461
- onClick={(event) => {
462
- event.preventDefault();
463
- fileInputRef.current?.click();
464
- }}
465
- variant="outline"
466
- disabled={isLoading || isRecording}
467
- >
468
- <PlusIcon size={14} />
469
- </Button>
501
+ )}
502
+ </div>
470
503
  </div>
471
504
  );
472
505
  };
@@ -0,0 +1,99 @@
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;
@@ -5,7 +5,7 @@ import Agent from '../../components/Agent';
5
5
  import { ChatDataProvider } from '../../hooks/useChatData';
6
6
  import styles from '../../assets/tailwindcss.css';
7
7
  import commonStyles from '../../assets/common.css';
8
- import { SuggestionType } from '../../types/common.type';
8
+ import { ConfigMessageType, SuggestionType } from '../../types/common.type';
9
9
 
10
10
  export type AgentWidgetType = {
11
11
  apiHost: string;
@@ -16,7 +16,13 @@ export type AgentWidgetType = {
16
16
  suggestedActions?: SuggestionType[];
17
17
  } & Record<string, unknown>;
18
18
  theme?: {
19
- avatar?: string;
19
+ botMessage?: ConfigMessageType;
20
+ userMessage?: ConfigMessageType;
21
+ suggestion?: {
22
+ defaultRows?: number; // only for scroll mode
23
+ expandedRows?: number; // only for scroll mode
24
+ layoutMode?: 'grid' | 'scroll';
25
+ };
20
26
  input?: {
21
27
  placeholder?: string;
22
28
  };
@@ -1,7 +1,7 @@
1
1
  import useSWR from 'swr';
2
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { StreamResponse } from '../models/FlowiseClient';
4
- import { predict } from '../services/chat.service';
4
+ import { getSuggestions, predict } from '../services/chat.service';
5
5
  import {
6
6
  ChatMessageMetadataType,
7
7
  ChatMessageType,
@@ -14,6 +14,7 @@ import { generateUUID } from '../utils/commonUtils';
14
14
  import { getBot } from '../services/bot.service';
15
15
  import { BotType } from '../types/bot.type';
16
16
  import { useConfiguration } from './useConfiguration';
17
+ import { SuggestionType } from 'types/common.type';
17
18
 
18
19
  type PropsType = {
19
20
  id?: string;
@@ -35,17 +36,20 @@ type ReturnType = {
35
36
  bot: BotType | null;
36
37
  enableTTS: boolean;
37
38
  setEnableTTS: (value: boolean) => void;
39
+ suggestions: SuggestionType[]
38
40
  };
39
41
 
40
42
  export const useChat = (props: PropsType): ReturnType => {
41
43
  const { id, initialMessages, initialInput, agentId } = props;
42
- const { apiHost, overrideConfig } = useConfiguration();
44
+ const { apiHost, overrideConfig, theme } = useConfiguration();
43
45
 
44
46
  const idKey = id ?? generateUUID();
45
47
  const chatIdRef = useRef<string>(idKey);
46
48
  const [chatId, setChatId] = useState(idKey);
47
49
  const [bot, setBot] = useState<BotType | null>(null);
48
50
  const [enableTTS, setEnableTTS] = useState(false);
51
+ const [suggestions, setSuggestions] = useState<SuggestionType[]>([]);
52
+
49
53
 
50
54
  const updateChatId = (uuid: string) => {
51
55
  chatIdRef.current = uuid;
@@ -62,6 +66,17 @@ export const useChat = (props: PropsType): ReturnType => {
62
66
  getBot(agentId || 'default', apiHost).then((res) => setBot(res));
63
67
  }, [agentId, apiHost]);
64
68
 
69
+ useEffect(() => {
70
+ if (bot?.id && chatId) {
71
+ updateSuggestions(`Agent name: ${bot.name}, Title: ${theme?.overview?.title}`)
72
+ }
73
+ }, [bot?.id, chatId])
74
+
75
+ const updateSuggestions = useCallback(async (question?: string) => {
76
+ const resSuggestions = await getSuggestions({ id: chatId, question, apiHost });
77
+ setSuggestions(resSuggestions);
78
+ }, [setSuggestions, chatId]);
79
+
65
80
  const processResponseStream = async (
66
81
  msgs: ChatMessageType[],
67
82
  req: PredictionData
@@ -94,6 +109,7 @@ export const useChat = (props: PropsType): ReturnType => {
94
109
  lastMsg.content = newMessage.metaData.question;
95
110
  }
96
111
  mutateMessages([...msgs, { ...newMessage }]);
112
+ updateSuggestions(`Agent name: ${bot?.name}, Title: ${theme?.overview?.title}`);
97
113
  } else if (chunk.event == 'audio') {
98
114
  newMessage.ttsUrl = chunk.data as string;
99
115
  mutateMessages([...msgs, { ...newMessage }]);
@@ -241,5 +257,6 @@ export const useChat = (props: PropsType): ReturnType => {
241
257
  bot,
242
258
  enableTTS,
243
259
  setEnableTTS,
260
+ suggestions
244
261
  };
245
262
  };
@@ -1,11 +1,18 @@
1
1
  import React, { createContext, useEffect, useState } from 'react';
2
+ import { ConfigMessageType } from 'types/common.type';
2
3
 
3
4
  type ConfigurationData = {
4
5
  apiHost: string;
5
6
  agentId: string;
6
7
  overrideConfig?: Record<string, any>;
7
8
  theme?: {
8
- avatar?: string;
9
+ suggestion?: {
10
+ defaultRows?: number; // only for scroll mode
11
+ expandedRows?: number; // only for scroll mode
12
+ layoutMode?: 'grid' | 'scroll';
13
+ };
14
+ botMessage?: ConfigMessageType;
15
+ userMessage?: ConfigMessageType;
9
16
  input?: {
10
17
  placeholder?: string;
11
18
  };