@clikvn/agent-widget-embedded 0.0.3-dev → 0.0.5-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 (55) hide show
  1. package/dist/components/Chat/AudioPlayer.d.ts +6 -0
  2. package/dist/components/Chat/AudioPlayer.d.ts.map +1 -0
  3. package/dist/components/Chat/AudioRecording.d.ts +9 -0
  4. package/dist/components/Chat/AudioRecording.d.ts.map +1 -0
  5. package/dist/components/Chat/Chat.d.ts.map +1 -1
  6. package/dist/components/Chat/Icons.d.ts +9 -1
  7. package/dist/components/Chat/Icons.d.ts.map +1 -1
  8. package/dist/components/Chat/Markdown.d.ts.map +1 -1
  9. package/dist/components/Chat/Message.d.ts +1 -0
  10. package/dist/components/Chat/Message.d.ts.map +1 -1
  11. package/dist/components/Chat/MultimodalInput.d.ts +2 -0
  12. package/dist/components/Chat/MultimodalInput.d.ts.map +1 -1
  13. package/dist/components/Chat/PreviewAttachment.d.ts.map +1 -1
  14. package/dist/features/AgentWidget/index.d.ts +2 -0
  15. package/dist/features/AgentWidget/index.d.ts.map +1 -1
  16. package/dist/hooks/useAudioRecording.d.ts +11 -0
  17. package/dist/hooks/useAudioRecording.d.ts.map +1 -0
  18. package/dist/hooks/useChat.d.ts +2 -0
  19. package/dist/hooks/useChat.d.ts.map +1 -1
  20. package/dist/hooks/useChatData.d.ts +2 -0
  21. package/dist/hooks/useChatData.d.ts.map +1 -1
  22. package/dist/hooks/useConfiguration.d.ts +1 -6
  23. package/dist/hooks/useConfiguration.d.ts.map +1 -1
  24. package/dist/index.html +43 -0
  25. package/dist/register.d.ts +5 -3
  26. package/dist/register.d.ts.map +1 -1
  27. package/dist/types/common.type.d.ts +5 -0
  28. package/dist/types/common.type.d.ts.map +1 -1
  29. package/dist/types/flowise.type.d.ts +8 -0
  30. package/dist/types/flowise.type.d.ts.map +1 -1
  31. package/dist/utils/audioRecording.d.ts +21 -0
  32. package/dist/utils/audioRecording.d.ts.map +1 -0
  33. package/dist/web.d.ts +1 -12
  34. package/dist/web.d.ts.map +1 -1
  35. package/dist/web.js +1 -1
  36. package/dist/window.d.ts +3 -15
  37. package/dist/window.d.ts.map +1 -1
  38. package/package.json +2 -1
  39. package/src/components/Chat/AudioPlayer.tsx +43 -0
  40. package/src/components/Chat/Chat.tsx +5 -0
  41. package/src/components/Chat/Icons.tsx +50 -3
  42. package/src/components/Chat/Markdown.tsx +12 -1
  43. package/src/components/Chat/Message.tsx +6 -0
  44. package/src/components/Chat/MultimodalInput.tsx +132 -33
  45. package/src/components/Chat/PreviewAttachment.tsx +10 -5
  46. package/src/features/AgentWidget/index.tsx +5 -2
  47. package/src/hooks/useAudioRecording.ts +50 -0
  48. package/src/hooks/useChat.ts +23 -5
  49. package/src/hooks/useChatData.tsx +3 -0
  50. package/src/hooks/useConfiguration.tsx +1 -6
  51. package/src/register.tsx +5 -2
  52. package/src/types/common.type.ts +6 -0
  53. package/src/types/flowise.type.ts +9 -0
  54. package/src/utils/audioRecording.ts +371 -0
  55. package/src/window.ts +2 -15
package/dist/window.d.ts CHANGED
@@ -1,17 +1,5 @@
1
- import { EVENT_TYPE } from './models';
2
- type VoiceAgentWidget = {
3
- apiHost: string;
4
- agentId: string;
5
- overrideConfig?: {
6
- chatId?: string | undefined;
7
- overrideConfig?: Record<string, any>;
8
- } & Record<string, unknown>;
9
- theme?: {
10
- avatar?: string;
11
- } & Record<string, unknown>;
12
- listeners?: Record<EVENT_TYPE, (props: any) => void>;
13
- };
14
- export declare const initWidget: (props: VoiceAgentWidget & {
1
+ import { AgentWidgetType } from './register';
2
+ export declare const initWidget: (props: AgentWidgetType & {
15
3
  id?: string;
16
4
  }) => void;
17
5
  export declare const destroy: () => void;
@@ -20,7 +8,7 @@ type AgentWidget = {
20
8
  destroy: typeof destroy;
21
9
  };
22
10
  export declare const parseAgentVoice: () => {
23
- initWidget: (props: VoiceAgentWidget & {
11
+ initWidget: (props: AgentWidgetType & {
24
12
  id?: string;
25
13
  }) => void;
26
14
  destroy: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"window.d.ts","sourceRoot":"","sources":["../src/window.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAItC,KAAK,gBAAgB,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE;QACf,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC5B,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;KACtC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,KAAK,CAAC,EAAE;QACN,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC,CAAC;CACtD,CAAC;AACF,eAAO,MAAM,UAAU,UAAW,gBAAgB,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,SAYnE,CAAC;AAEF,eAAO,MAAM,OAAO,YAEnB,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,OAAO,EAAE,OAAO,OAAO,CAAC;CACzB,CAAC;AAQF,eAAO,MAAM,eAAe;wBA7BM,gBAAgB,GAAG;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE;;CAgClE,CAAC;AAEH,eAAO,MAAM,wBAAwB,UAAW,WAAW,SAG1D,CAAC"}
1
+ {"version":3,"file":"window.d.ts","sourceRoot":"","sources":["../src/window.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAyB,MAAM,YAAY,CAAC;AAIpE,eAAO,MAAM,UAAU,UAAW,eAAe,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,SAYlE,CAAC;AAEF,eAAO,MAAM,OAAO,YAEnB,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,OAAO,EAAE,OAAO,OAAO,CAAC;CACzB,CAAC;AAQF,eAAO,MAAM,eAAe;wBA7BM,eAAe,GAAG;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE;;CAgCjE,CAAC;AAEH,eAAO,MAAM,wBAAwB,UAAW,WAAW,SAG1D,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@clikvn/agent-widget-embedded",
3
3
  "description": "This is agent widget",
4
- "version": "0.0.3-dev",
4
+ "version": "0.0.5-dev",
5
5
  "author": "Clik JSC",
6
6
  "license": "ISC",
7
7
  "type": "module",
@@ -32,6 +32,7 @@
32
32
  "@types/react-dom": "^18.3.1",
33
33
  "class-variance-authority": "^0.7.1",
34
34
  "clsx": "^2.1.1",
35
+ "device-detector-js": "^3.0.3",
35
36
  "framer-motion": "^11.18.0",
36
37
  "react": "^18.3.1",
37
38
  "react-dom": "^18.3.1",
@@ -0,0 +1,43 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import { PlayIcon, StopIcon1 } from './Icons';
3
+
4
+ const AudioPlayer = ({
5
+ src,
6
+ autoplay = false,
7
+ }: {
8
+ src: string;
9
+ autoplay?: boolean;
10
+ }) => {
11
+ const audioRef = useRef<any>(null);
12
+ const [isPlaying, setIsPlaying] = useState(false);
13
+
14
+ const handlePlayPause = (e: any) => {
15
+ e.preventDefault();
16
+ if (isPlaying) {
17
+ audioRef.current.pause();
18
+ } else {
19
+ audioRef.current.play();
20
+ }
21
+ setIsPlaying(!isPlaying);
22
+ };
23
+
24
+ return (
25
+ <>
26
+ <button
27
+ className="rounded-full cursor-pointer h-fit w-[24px]"
28
+ onClick={handlePlayPause}
29
+ >
30
+ {isPlaying ? <StopIcon1 /> : <PlayIcon />}
31
+ </button>
32
+ <audio
33
+ ref={audioRef}
34
+ className="hidden"
35
+ src={src}
36
+ autoPlay={autoplay}
37
+ onEnded={() => setIsPlaying(false)}
38
+ ></audio>
39
+ </>
40
+ );
41
+ };
42
+
43
+ export default AudioPlayer;
@@ -25,6 +25,8 @@ export const Chat: FC<PropsType> = ({ id, agentId, initialMessages = [] }) => {
25
25
  chatId,
26
26
  append,
27
27
  bot,
28
+ enableTTS,
29
+ setEnableTTS,
28
30
  } = useChat({ id, initialMessages, agentId });
29
31
  const { apiHost } = useConfiguration();
30
32
  const [messagesContainerRef, messagesEndRef] =
@@ -47,6 +49,7 @@ export const Chat: FC<PropsType> = ({ id, agentId, initialMessages = [] }) => {
47
49
  chatId={id}
48
50
  message={message}
49
51
  isLoading={isLoading && (messages || []).length - 1 === index}
52
+ enableTTS={enableTTS}
50
53
  />
51
54
  ))}
52
55
 
@@ -76,6 +79,8 @@ export const Chat: FC<PropsType> = ({ id, agentId, initialMessages = [] }) => {
76
79
  setAttachments={setAttachments}
77
80
  bot={bot}
78
81
  apiHost={apiHost}
82
+ setEnableTTS={setEnableTTS}
83
+ enableTTS={enableTTS}
79
84
  />
80
85
  </form>
81
86
  </div>
@@ -477,14 +477,20 @@ export const MoreIcon = ({ size = 16 }: { size?: number }) => {
477
477
  );
478
478
  };
479
479
 
480
- export const TrashIcon = ({ size = 16 }: { size?: number }) => {
480
+ export const TrashIcon = ({
481
+ size = 16,
482
+ color,
483
+ }: {
484
+ size?: number;
485
+ color?: string;
486
+ }) => {
481
487
  return (
482
488
  <svg
483
489
  height={size}
484
490
  strokeLinejoin="round"
485
491
  viewBox="0 0 16 16"
486
492
  width={size}
487
- style={{ color: 'currentcolor' }}
493
+ style={{ color: color || 'currentcolor' }}
488
494
  >
489
495
  <path
490
496
  fillRule="evenodd"
@@ -859,9 +865,9 @@ export const CheckCirclFillIcon = ({ size = 16 }: { size?: number }) => {
859
865
  export const MicrophoneIcon = ({ size = 16 }: { size?: number }) => {
860
866
  return (
861
867
  <svg
862
- height={size}
863
868
  strokeLinejoin="round"
864
869
  viewBox="0 0 490.9 490.9"
870
+ height={size}
865
871
  width={size}
866
872
  style={{ color: 'currentcolor' }}
867
873
  >
@@ -881,3 +887,44 @@ export const MicrophoneIcon = ({ size = 16 }: { size?: number }) => {
881
887
  </svg>
882
888
  );
883
889
  };
890
+
891
+ export const CircleDotIcon = ({
892
+ size = 16,
893
+ color = 'red',
894
+ }: {
895
+ size?: number;
896
+ color?: string;
897
+ }) => (
898
+ <svg
899
+ xmlns="http://www.w3.org/2000/svg"
900
+ height={size}
901
+ width={size}
902
+ viewBox="0 0 24 24"
903
+ fill="none"
904
+ stroke={color}
905
+ stroke-width="2"
906
+ stroke-linecap="round"
907
+ stroke-linejoin="round"
908
+ >
909
+ <circle cx="12" cy="12" r="10" />
910
+ <circle cx="12" cy="12" r="1" />
911
+ </svg>
912
+ );
913
+
914
+ export const PlayIcon = () => (
915
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
916
+ <path d="M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zM188.3 147.1c7.6-4.2 16.8-4.1 24.3 .5l144 88c7.1 4.4 11.5 12.1 11.5 20.5s-4.4 16.1-11.5 20.5l-144 88c-7.4 4.5-16.7 4.7-24.3 .5s-12.3-12.2-12.3-20.9l0-176c0-8.7 4.7-16.7 12.3-20.9z" />
917
+ </svg>
918
+ );
919
+
920
+ export const StopIcon1 = () => (
921
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
922
+ <path d="M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm192-96l128 0c17.7 0 32 14.3 32 32l0 128c0 17.7-14.3 32-32 32l-128 0c-17.7 0-32-14.3-32-32l0-128c0-17.7 14.3-32 32-32z" />
923
+ </svg>
924
+ );
925
+
926
+ export const VolumeIcon = () => (
927
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
928
+ <path d="M533.6 32.5C598.5 85.2 640 165.8 640 256s-41.5 170.7-106.4 223.5c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C557.5 398.2 592 331.2 592 256s-34.5-142.2-88.7-186.3c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zM473.1 107c43.2 35.2 70.9 88.9 70.9 149s-27.7 113.8-70.9 149c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C475.3 341.3 496 301.1 496 256s-20.7-85.3-53.2-111.8c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zm-60.5 74.5C434.1 199.1 448 225.9 448 256s-13.9 56.9-35.4 74.5c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C393.1 284.4 400 271 400 256s-6.9-28.4-17.7-37.3c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zM301.1 34.8C312.6 40 320 51.4 320 64l0 384c0 12.6-7.4 24-18.9 29.2s-25 3.1-34.4-5.3L131.8 352 64 352c-35.3 0-64-28.7-64-64l0-64c0-35.3 28.7-64 64-64l67.8 0L266.7 40.1c9.4-8.4 22.9-10.4 34.4-5.3z" />
929
+ </svg>
930
+ );
@@ -243,7 +243,18 @@ const NonMemoizedMarkdown: FC<{
243
243
  content.indexOf('.jpeg') >= 0 ||
244
244
  content.indexOf('.png') >= 0 ||
245
245
  content.indexOf('.gif') >= 0);
246
- if (isImageUrl) {
246
+ const isAudioUrl =
247
+ content &&
248
+ (content.indexOf('.mp3') >= 0 ||
249
+ content.indexOf('.wav') >= 0 ||
250
+ content.indexOf('.ogg') >= 0);
251
+ if (isAudioUrl) {
252
+ return (
253
+ <audio controls>
254
+ <source src={content} />
255
+ </audio>
256
+ );
257
+ } else if (isImageUrl) {
247
258
  return (
248
259
  <a href={content} target="_blank" rel="noopener noreferrer">
249
260
  <img
@@ -10,12 +10,14 @@ import {
10
10
  ToolUsage,
11
11
  } from '../../types/flowise.type';
12
12
  import { cn } from '../../utils/commonUtils';
13
+ import AudioPlayer from './AudioPlayer';
13
14
 
14
15
  type PropsType = {
15
16
  chatId?: string;
16
17
  message: ChatMessageType;
17
18
  isLoading: boolean;
18
19
  bot: BotType | null;
20
+ enableTTS?: boolean;
19
21
  };
20
22
 
21
23
  const getRole = (role?: MessageRoleType) => {
@@ -31,6 +33,7 @@ export const PreviewMessage: FC<PropsType> = ({
31
33
  message,
32
34
  isLoading,
33
35
  bot,
36
+ enableTTS,
34
37
  }) => {
35
38
  const parseOutput = (outputData: any) => {
36
39
  try {
@@ -136,6 +139,9 @@ export const PreviewMessage: FC<PropsType> = ({
136
139
  </Markdown>
137
140
  </div>
138
141
  )}
142
+ {message?.ttsUrl && (
143
+ <AudioPlayer src={message.ttsUrl} autoplay={!!enableTTS} />
144
+ )}
139
145
  </div>
140
146
  </div>
141
147
  </motion.div>
@@ -14,36 +14,23 @@ import {
14
14
  generateUUID,
15
15
  } from '../../utils/commonUtils';
16
16
  import { PreviewAttachment } from './PreviewAttachment';
17
- import { ArrowUpIcon, PaperclipIcon, StopIcon } from './Icons';
17
+ import {
18
+ ArrowUpIcon,
19
+ CircleDotIcon,
20
+ MicrophoneIcon,
21
+ PlusIcon,
22
+ StopIcon,
23
+ TrashIcon,
24
+ VolumeIcon,
25
+ } from './Icons';
18
26
  import { ChatMessageType, IFileUpload } from '../../types/flowise.type';
19
27
  import { BotType } from '../../types/bot.type';
20
28
 
21
29
  import { createAttachments } from '../../services/chat.service';
22
30
  import { Button } from './ui/Button';
23
31
  import { Textarea } from './ui/Textarea';
24
-
25
- const suggestedActions = [
26
- {
27
- title: 'What is the weather',
28
- label: 'in Ha Noi?',
29
- action: 'What is the weather in Ha Noi?',
30
- },
31
- {
32
- title: 'Create a travel plan for an traveling',
33
- label: 'to Ha Noi',
34
- action: 'Create a travel plan for an traveling to Ha Noi',
35
- },
36
- {
37
- title: 'Top of tourist attractions',
38
- label: 'in Ha Noi',
39
- action: 'Top of tourist attractions in Ha Noi',
40
- },
41
- {
42
- title: 'List of museums',
43
- label: 'in Ha Noi',
44
- action: 'List of museums in Ha Noi',
45
- },
46
- ];
32
+ import { useAudioRecording } from '../../hooks/useAudioRecording';
33
+ import { useChatData } from '../../hooks/useChatData';
47
34
 
48
35
  type PropsType = {
49
36
  input: string;
@@ -63,6 +50,8 @@ type PropsType = {
63
50
  setAttachments?: (func: (files: IFileUpload[]) => IFileUpload[]) => void;
64
51
  bot: BotType | null;
65
52
  apiHost: string;
53
+ setEnableTTS: (value: boolean) => void;
54
+ enableTTS: boolean;
66
55
  };
67
56
 
68
57
  export const MultimodalInput: FC<PropsType> = ({
@@ -80,7 +69,18 @@ export const MultimodalInput: FC<PropsType> = ({
80
69
  setAttachments,
81
70
  bot,
82
71
  apiHost,
72
+ setEnableTTS,
73
+ enableTTS,
83
74
  }) => {
75
+ const { suggestedActions = [] } = useChatData();
76
+ const {
77
+ isRecording,
78
+ setIsRecording,
79
+ onRecordingCancelled,
80
+ onRecordingStopped,
81
+ elapsedTime,
82
+ isLoadingRecording,
83
+ } = useAudioRecording();
84
84
  const textareaRef = useRef<HTMLTextAreaElement | null>(null);
85
85
  const { width } = useWindowSize();
86
86
  useEffect(() => {
@@ -130,7 +130,7 @@ export const MultimodalInput: FC<PropsType> = ({
130
130
  handleSubmit(undefined, attachments);
131
131
  setLocalStorageInput('');
132
132
  if (setAttachments) {
133
- setAttachments((currentAttachments: IFileUpload[]) => []);
133
+ setAttachments((_) => []);
134
134
  if (fileInputRef.current) {
135
135
  (fileInputRef.current as HTMLInputElement).value = '';
136
136
  }
@@ -157,6 +157,33 @@ export const MultimodalInput: FC<PropsType> = ({
157
157
  [chatId]
158
158
  );
159
159
 
160
+ const toAudioBase64 = (blob: Blob) => {
161
+ return new Promise<IFileUpload>((resolve) => {
162
+ let mimeType = '';
163
+ const pos = blob.type.indexOf(';');
164
+ if (pos === -1) {
165
+ mimeType = blob.type;
166
+ } else {
167
+ mimeType = blob.type.substring(0, pos);
168
+ }
169
+
170
+ // read blob and add to previews
171
+ const reader = new FileReader();
172
+ reader.readAsDataURL(blob);
173
+ reader.onloadend = () => {
174
+ const base64data = reader.result as string;
175
+ const upload: IFileUpload = {
176
+ tempId: generateUUID(),
177
+ data: base64data,
178
+ type: 'audio',
179
+ name: `audio_${Date.now()}.wav`,
180
+ mime: mimeType,
181
+ };
182
+ resolve(upload);
183
+ };
184
+ });
185
+ };
186
+
160
187
  const toBase64 = async (
161
188
  file: File,
162
189
  type = 'file'
@@ -213,7 +240,6 @@ export const MultimodalInput: FC<PropsType> = ({
213
240
  const handleFileChange = useCallback(
214
241
  async (event: ChangeEvent<HTMLInputElement>) => {
215
242
  const files = Array.from(event.target.files || []);
216
-
217
243
  try {
218
244
  const uploadPromises = files.map((file) => checkUploadFile(file));
219
245
  const uploadedAttachments = await Promise.all(uploadPromises);
@@ -235,6 +261,27 @@ export const MultimodalInput: FC<PropsType> = ({
235
261
  [setAttachments, checkUploadFile]
236
262
  );
237
263
 
264
+ const handleSubmitRecording = useCallback(
265
+ async (blob: Blob) => {
266
+ try {
267
+ const audioFile = await toAudioBase64(blob);
268
+ handleSubmit(undefined, [audioFile]);
269
+ setIsRecording(false);
270
+ } catch (error) {
271
+ console.error('Error uploading files!', error);
272
+ }
273
+ },
274
+ [handleSubmit, setIsRecording]
275
+ );
276
+
277
+ const handleSend = useCallback(async () => {
278
+ if (isRecording) {
279
+ onRecordingStopped(handleSubmitRecording);
280
+ } else {
281
+ submitForm();
282
+ }
283
+ }, [submitForm, onRecordingStopped, handleSubmitRecording]);
284
+
238
285
  return (
239
286
  <div className="relative w-full flex flex-col gap-4">
240
287
  {messages.length === 0 && (
@@ -317,7 +364,6 @@ export const MultimodalInput: FC<PropsType> = ({
317
364
  onKeyDown={(event) => {
318
365
  if (event.key === 'Enter' && !event.shiftKey) {
319
366
  event.preventDefault();
320
-
321
367
  if (isLoading) {
322
368
  console.error(
323
369
  'Please wait for the model to finish its response!'
@@ -325,7 +371,7 @@ export const MultimodalInput: FC<PropsType> = ({
325
371
  } else if (uploadQueue.length) {
326
372
  console.error('Please wait for file is uploading!');
327
373
  } else {
328
- submitForm();
374
+ handleSend();
329
375
  }
330
376
  }
331
377
  }}
@@ -347,24 +393,77 @@ export const MultimodalInput: FC<PropsType> = ({
347
393
  className="rounded-full p-1.5 h-fit absolute bottom-2 right-2 m-0.5 border dark:border-zinc-600"
348
394
  onClick={(event) => {
349
395
  event.preventDefault();
350
- submitForm();
396
+ handleSend();
351
397
  }}
352
- disabled={input.length === 0 || !!uploadQueue.length}
398
+ disabled={
399
+ !isRecording && (input.length === 0 || !!uploadQueue.length)
400
+ } // zero input or uploading
353
401
  >
354
402
  <ArrowUpIcon size={14} />
355
403
  </Button>
356
404
  )}
405
+ {isRecording ? (
406
+ <>
407
+ <div
408
+ className="rounded-full bg-background flex absolute p-1 bottom-2 right-[80px]"
409
+ data-testid="input"
410
+ >
411
+ <div className="flex items-center gap-3">
412
+ <span>
413
+ <CircleDotIcon color="red" />
414
+ </span>
415
+ <span>{elapsedTime || '00:00'}</span>
416
+ {isLoadingRecording && <span className="ml-1.5">Sending...</span>}
417
+ </div>
418
+ </div>
419
+ <Button
420
+ className="rounded-full p-1.5 h-fit absolute bottom-2 right-11 m-0.5 dark:border-zinc-700"
421
+ variant="outline"
422
+ onClick={(event) => {
423
+ event.preventDefault();
424
+ onRecordingCancelled();
425
+ }}
426
+ >
427
+ <TrashIcon size={14} color="red" />
428
+ </Button>
429
+ </>
430
+ ) : (
431
+ <>
432
+ <Button
433
+ className="rounded-full p-1.5 h-fit absolute bottom-2 right-11 m-0.5 dark:border-zinc-700"
434
+ onClick={(event) => {
435
+ event.preventDefault();
436
+ setIsRecording(true);
437
+ }}
438
+ variant="outline"
439
+ disabled={isLoading}
440
+ >
441
+ <MicrophoneIcon size={14} />
442
+ </Button>
443
+ <Button
444
+ 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' : ''}`}
445
+ onClick={(event) => {
446
+ event.preventDefault();
447
+ setEnableTTS(!enableTTS);
448
+ }}
449
+ variant="outline"
450
+ disabled={isLoading}
451
+ >
452
+ <VolumeIcon />
453
+ </Button>
454
+ </>
455
+ )}
357
456
 
358
457
  <Button
359
- className="rounded-full p-1.5 h-fit absolute bottom-2 right-11 m-0.5 dark:border-zinc-700"
458
+ className="rounded-full p-1.5 h-fit absolute bottom-2 left-2 m-0.5 dark:border-zinc-700"
360
459
  onClick={(event) => {
361
460
  event.preventDefault();
362
461
  fileInputRef.current?.click();
363
462
  }}
364
463
  variant="outline"
365
- disabled={isLoading}
464
+ disabled={isLoading || isRecording}
366
465
  >
367
- <PaperclipIcon size={14} />
466
+ <PlusIcon size={14} />
368
467
  </Button>
369
468
  </div>
370
469
  );
@@ -1,5 +1,6 @@
1
1
  import { IFileUpload } from '../../types/flowise.type';
2
2
  import { LoaderIcon } from './Icons';
3
+ import React from 'react';
3
4
 
4
5
  export const PreviewAttachment = ({
5
6
  attachment,
@@ -11,9 +12,13 @@ export const PreviewAttachment = ({
11
12
  const { name, data, mime, tempId } = attachment;
12
13
  return (
13
14
  <div className="flex flex-col gap-2">
14
- <div className="w-30 p-2 max-w-[200px] bg-muted rounded-md relative flex flex-col items-center justify-center">
15
+ <div className="w-30 p-0 max-w-[400px] bg-muted rounded-md relative flex flex-col items-center justify-center">
15
16
  {data ? (
16
- mime.startsWith('image') ? (
17
+ mime.startsWith('audio') ? (
18
+ <audio controls>
19
+ <source src={data} type={mime} />
20
+ </audio>
21
+ ) : mime.startsWith('image') ? (
17
22
  <img
18
23
  key={tempId}
19
24
  src={data}
@@ -33,9 +38,9 @@ export const PreviewAttachment = ({
33
38
  </div>
34
39
  )}
35
40
  </div>
36
- <div className="text-xs text-zinc-500 max-w-[200px] truncate ">
37
- {name}
38
- </div>
41
+ {/*<div className="text-xs text-zinc-500 max-w-[200px] truncate ">*/}
42
+ {/* {name}*/}
43
+ {/*</div>*/}
39
44
  </div>
40
45
  );
41
46
  };
@@ -5,6 +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
9
 
9
10
  export type AgentWidgetType = {
10
11
  apiHost: string;
@@ -12,6 +13,7 @@ export type AgentWidgetType = {
12
13
  overrideConfig?: {
13
14
  chatId?: string | undefined;
14
15
  overrideConfig?: Record<string, unknown>;
16
+ suggestedActions?: SuggestionType[];
15
17
  } & Record<string, unknown>;
16
18
  theme?: {
17
19
  avatar?: string;
@@ -27,14 +29,15 @@ const AgentWidget: FC<AgentWidgetType> = (props: AgentWidgetType) => {
27
29
  config={{
28
30
  apiHost: props.apiHost,
29
31
  agentId: props.agentId,
30
- listeners: props.listeners,
31
- overrideConfig: props.overrideConfig,
32
+ overrideConfig: props.overrideConfig?.overrideConfig,
32
33
  theme: props.theme,
33
34
  }}
34
35
  >
35
36
  <ChatDataProvider
36
37
  data={{
37
38
  chatId: props.overrideConfig?.chatId,
39
+ suggestedActions: props.overrideConfig?.suggestedActions,
40
+ listeners: props.listeners,
38
41
  }}
39
42
  >
40
43
  <Agent />
@@ -0,0 +1,50 @@
1
+ import { useEffect, useState } from 'react';
2
+ import {
3
+ cancelAudioRecording,
4
+ startAudioRecording,
5
+ stopAudioRecording,
6
+ } from '../utils/audioRecording';
7
+
8
+ export const useAudioRecording = () => {
9
+ const [elapsedTime, setElapsedTime] = useState('00:00');
10
+ const [recordingNotSupported, setRecordingNotSupported] = useState(false);
11
+ const [isLoadingRecording, setIsLoadingRecording] = useState(false);
12
+ const [isRecording, setIsRecording] = useState(false);
13
+
14
+ useEffect(() => {
15
+ if (isRecording) {
16
+ onRecordingStarted();
17
+ }
18
+ }, [isRecording]);
19
+ const onRecordingStarted = () => {
20
+ setIsRecording(true);
21
+ setIsLoadingRecording(false);
22
+ startAudioRecording(
23
+ setIsRecording,
24
+ setRecordingNotSupported,
25
+ setElapsedTime
26
+ );
27
+ };
28
+
29
+ const onRecordingCancelled = () => {
30
+ if (!recordingNotSupported) cancelAudioRecording();
31
+ setIsRecording(false);
32
+ setRecordingNotSupported(false);
33
+ };
34
+
35
+ const onRecordingStopped = (onStop: null | ((blob: Blob) => void)) => {
36
+ setIsLoadingRecording(true);
37
+ stopAudioRecording(onStop);
38
+ };
39
+
40
+ return {
41
+ elapsedTime,
42
+ recordingNotSupported,
43
+ isLoadingRecording,
44
+ isRecording,
45
+ setIsRecording,
46
+ onRecordingCancelled,
47
+ onRecordingStopped,
48
+ onRecordingStarted,
49
+ };
50
+ };