@smart-cloud/ai-kit-ui 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-kit-ui.css +52 -0
- package/dist/index.cjs +9 -9
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9 -9
- package/package.json +2 -2
- package/src/ai-chatbot/AiChatbot.tsx +436 -77
- package/src/ai-chatbot/attachmentStorage.ts +66 -0
- package/src/doc-search/DocSearch.tsx +196 -59
- package/src/i18n/ar.ts +10 -0
- package/src/i18n/de.ts +10 -0
- package/src/i18n/en.ts +10 -0
- package/src/i18n/es.ts +10 -0
- package/src/i18n/fr.ts +10 -0
- package/src/i18n/he.ts +10 -0
- package/src/i18n/hi.ts +10 -0
- package/src/i18n/hu.ts +10 -0
- package/src/i18n/id.ts +10 -0
- package/src/i18n/it.ts +10 -0
- package/src/i18n/ja.ts +10 -0
- package/src/i18n/ko.ts +10 -0
- package/src/i18n/nb.ts +10 -0
- package/src/i18n/nl.ts +10 -0
- package/src/i18n/pl.ts +10 -0
- package/src/i18n/pt.ts +10 -0
- package/src/i18n/ru.ts +10 -0
- package/src/i18n/sv.ts +10 -0
- package/src/i18n/th.ts +10 -0
- package/src/i18n/tr.ts +10 -0
- package/src/i18n/ua.ts +10 -0
- package/src/i18n/zh.ts +10 -0
- package/src/styles/ai-kit-ui.css +52 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ActionIcon,
|
|
3
3
|
Anchor,
|
|
4
|
+
Box,
|
|
4
5
|
Button,
|
|
5
6
|
Group,
|
|
6
7
|
Input,
|
|
@@ -13,12 +14,14 @@ import {
|
|
|
13
14
|
import {
|
|
14
15
|
IconMaximize,
|
|
15
16
|
IconMessage,
|
|
17
|
+
IconMicrophone,
|
|
16
18
|
IconMinimize,
|
|
17
19
|
IconPaperclip,
|
|
18
20
|
IconPencil,
|
|
19
21
|
IconPlayerStop,
|
|
20
22
|
IconSend,
|
|
21
23
|
IconTrash,
|
|
24
|
+
IconX,
|
|
22
25
|
} from "@tabler/icons-react";
|
|
23
26
|
|
|
24
27
|
import {
|
|
@@ -57,6 +60,8 @@ I18n.putVocabularies(translations);
|
|
|
57
60
|
|
|
58
61
|
const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;
|
|
59
62
|
|
|
63
|
+
const USE_AUDIO = false; // Set to true to enable audio recording feature (requires backend support for audio input)
|
|
64
|
+
|
|
60
65
|
// New: history storage support
|
|
61
66
|
const DEFAULT_PRESERVATION_TIME_DAYS = 1;
|
|
62
67
|
const DEFAULT_HISTORY_STORAGE: HistoryStorageMode = "localstorage";
|
|
@@ -133,6 +138,8 @@ type ChatMessageAttachment = {
|
|
|
133
138
|
blobId?: string;
|
|
134
139
|
objectUrl?: string | null;
|
|
135
140
|
blob?: Blob;
|
|
141
|
+
duration?: number; // For audio/video attachments
|
|
142
|
+
mediaType?: "image" | "audio"; // Distinguish media types
|
|
136
143
|
};
|
|
137
144
|
|
|
138
145
|
type ChatMessage = {
|
|
@@ -152,6 +159,13 @@ type ComposerImage = {
|
|
|
152
159
|
objectUrl: string;
|
|
153
160
|
};
|
|
154
161
|
|
|
162
|
+
type ComposerAudio = {
|
|
163
|
+
id: string;
|
|
164
|
+
blob: Blob;
|
|
165
|
+
objectUrl: string;
|
|
166
|
+
duration: number;
|
|
167
|
+
};
|
|
168
|
+
|
|
155
169
|
type PersistedAttachment = Omit<ChatMessageAttachment, "objectUrl" | "blob">;
|
|
156
170
|
|
|
157
171
|
type PersistedChatMessage = Omit<ChatMessage, "attachments"> & {
|
|
@@ -288,6 +302,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
288
302
|
onClose,
|
|
289
303
|
|
|
290
304
|
// AiChatbotProps
|
|
305
|
+
context,
|
|
291
306
|
placeholder,
|
|
292
307
|
maxImages,
|
|
293
308
|
maxImageBytes,
|
|
@@ -312,6 +327,16 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
312
327
|
|
|
313
328
|
const [question, setQuestion] = useState("");
|
|
314
329
|
const [composerImages, setComposerImages] = useState<ComposerImage[]>([]);
|
|
330
|
+
const [composerAudio, setComposerAudio] = useState<ComposerAudio | null>(
|
|
331
|
+
null,
|
|
332
|
+
);
|
|
333
|
+
const [recording, setRecording] = useState<boolean>(false);
|
|
334
|
+
const [audioLevel, setAudioLevel] = useState<number>(0);
|
|
335
|
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
|
336
|
+
const audioChunksRef = useRef<Blob[]>([]);
|
|
337
|
+
const audioContextRef = useRef<AudioContext | null>(null);
|
|
338
|
+
const analyserRef = useRef<AnalyserNode | null>(null);
|
|
339
|
+
const animationFrameRef = useRef<number | null>(null);
|
|
315
340
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
316
341
|
const [statusLineError, setStatusLineError] = useState<string | null>(null);
|
|
317
342
|
const [resetDialogOpen, setResetDialogOpen] = useState(false);
|
|
@@ -355,6 +380,125 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
355
380
|
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
356
381
|
}, [disposeComposerImageList]);
|
|
357
382
|
|
|
383
|
+
const clearComposerAudio = useCallback(() => {
|
|
384
|
+
if (composerAudioRef.current) {
|
|
385
|
+
revokeObjectUrlSafe(composerAudioRef.current.objectUrl);
|
|
386
|
+
}
|
|
387
|
+
setComposerAudio(null);
|
|
388
|
+
audioChunksRef.current = [];
|
|
389
|
+
setAudioLevel(0);
|
|
390
|
+
}, []);
|
|
391
|
+
|
|
392
|
+
const startRecording = useCallback(async () => {
|
|
393
|
+
try {
|
|
394
|
+
// Clear question input when starting audio recording
|
|
395
|
+
setQuestion("");
|
|
396
|
+
// Clear any existing audio
|
|
397
|
+
clearComposerAudio();
|
|
398
|
+
|
|
399
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
400
|
+
const mediaRecorder = new MediaRecorder(stream, {
|
|
401
|
+
mimeType: "audio/webm",
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Setup audio analysis for visual feedback
|
|
405
|
+
const audioContext = new AudioContext();
|
|
406
|
+
const source = audioContext.createMediaStreamSource(stream);
|
|
407
|
+
const analyser = audioContext.createAnalyser();
|
|
408
|
+
analyser.fftSize = 2048;
|
|
409
|
+
analyser.smoothingTimeConstant = 0.8;
|
|
410
|
+
source.connect(analyser);
|
|
411
|
+
|
|
412
|
+
audioContextRef.current = audioContext;
|
|
413
|
+
analyserRef.current = analyser;
|
|
414
|
+
|
|
415
|
+
// Monitor audio level
|
|
416
|
+
const dataArray = new Uint8Array(analyser.fftSize);
|
|
417
|
+
const updateLevel = () => {
|
|
418
|
+
if (!analyserRef.current) return;
|
|
419
|
+
analyserRef.current.getByteTimeDomainData(dataArray);
|
|
420
|
+
|
|
421
|
+
let sum = 0;
|
|
422
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
423
|
+
const normalized = (dataArray[i]! - 128) / 128;
|
|
424
|
+
sum += normalized * normalized;
|
|
425
|
+
}
|
|
426
|
+
const rms = Math.sqrt(sum / dataArray.length);
|
|
427
|
+
const level = Math.min(100, rms * 200);
|
|
428
|
+
setAudioLevel(level);
|
|
429
|
+
|
|
430
|
+
animationFrameRef.current = requestAnimationFrame(updateLevel);
|
|
431
|
+
};
|
|
432
|
+
updateLevel();
|
|
433
|
+
|
|
434
|
+
audioChunksRef.current = [];
|
|
435
|
+
const recordStartTime = Date.now();
|
|
436
|
+
|
|
437
|
+
mediaRecorder.ondataavailable = (event) => {
|
|
438
|
+
if (event.data.size > 0) {
|
|
439
|
+
audioChunksRef.current.push(event.data);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
mediaRecorder.onstop = () => {
|
|
444
|
+
const audioBlob = new Blob(audioChunksRef.current, {
|
|
445
|
+
type: "audio/webm",
|
|
446
|
+
});
|
|
447
|
+
const duration = (Date.now() - recordStartTime) / 1000; // duration in seconds
|
|
448
|
+
const objectUrl = createObjectUrl(audioBlob);
|
|
449
|
+
|
|
450
|
+
if (objectUrl) {
|
|
451
|
+
setComposerAudio({
|
|
452
|
+
id: createMessageId("composer-audio"),
|
|
453
|
+
blob: audioBlob,
|
|
454
|
+
objectUrl,
|
|
455
|
+
duration,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
460
|
+
|
|
461
|
+
// Cleanup audio analysis
|
|
462
|
+
if (animationFrameRef.current) {
|
|
463
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
464
|
+
animationFrameRef.current = null;
|
|
465
|
+
}
|
|
466
|
+
if (audioContextRef.current) {
|
|
467
|
+
audioContextRef.current.close();
|
|
468
|
+
audioContextRef.current = null;
|
|
469
|
+
}
|
|
470
|
+
analyserRef.current = null;
|
|
471
|
+
setAudioLevel(0);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
mediaRecorderRef.current = mediaRecorder;
|
|
475
|
+
mediaRecorder.start();
|
|
476
|
+
setRecording(true);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error("Failed to start recording:", error);
|
|
479
|
+
}
|
|
480
|
+
}, [clearComposerAudio]);
|
|
481
|
+
|
|
482
|
+
const stopRecording = useCallback(() => {
|
|
483
|
+
if (mediaRecorderRef.current && recording) {
|
|
484
|
+
mediaRecorderRef.current.stop();
|
|
485
|
+
setRecording(false);
|
|
486
|
+
}
|
|
487
|
+
}, [recording]);
|
|
488
|
+
|
|
489
|
+
// Cleanup on unmount
|
|
490
|
+
useEffect(() => {
|
|
491
|
+
return () => {
|
|
492
|
+
if (animationFrameRef.current) {
|
|
493
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
494
|
+
}
|
|
495
|
+
if (audioContextRef.current) {
|
|
496
|
+
audioContextRef.current.close();
|
|
497
|
+
}
|
|
498
|
+
clearComposerAudio();
|
|
499
|
+
};
|
|
500
|
+
}, [clearComposerAudio]);
|
|
501
|
+
|
|
358
502
|
// New: persist timestamp of last actually-sent user message
|
|
359
503
|
const [lastUserSentAt, setLastUserSentAt] = useState<number | null>(null);
|
|
360
504
|
|
|
@@ -363,6 +507,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
363
507
|
const messagesRef = useRef(messages);
|
|
364
508
|
const lastUserSentAtRef = useRef(lastUserSentAt);
|
|
365
509
|
const composerImagesRef = useRef(composerImages);
|
|
510
|
+
const composerAudioRef = useRef(composerAudio);
|
|
366
511
|
|
|
367
512
|
useEffect(() => {
|
|
368
513
|
questionRef.current = question;
|
|
@@ -370,6 +515,9 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
370
515
|
useEffect(() => {
|
|
371
516
|
composerImagesRef.current = composerImages;
|
|
372
517
|
}, [composerImages]);
|
|
518
|
+
useEffect(() => {
|
|
519
|
+
composerAudioRef.current = composerAudio;
|
|
520
|
+
}, [composerAudio]);
|
|
373
521
|
useEffect(() => {
|
|
374
522
|
messagesRef.current = messages;
|
|
375
523
|
}, [messages]);
|
|
@@ -406,8 +554,9 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
406
554
|
|
|
407
555
|
const canSend = useMemo(() => {
|
|
408
556
|
if (isChatBusy) return false;
|
|
409
|
-
|
|
410
|
-
|
|
557
|
+
// Can send if we have text OR audio
|
|
558
|
+
return question.trim().length > 0 || composerAudio !== null;
|
|
559
|
+
}, [question, isChatBusy, composerAudio]);
|
|
411
560
|
|
|
412
561
|
const openButtonLabel = useMemo(() => {
|
|
413
562
|
const raw = openButtonTitle ? openButtonTitle : labels.askMeLabel;
|
|
@@ -657,47 +806,90 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
657
806
|
}, [statusLineError]);
|
|
658
807
|
|
|
659
808
|
const buildUserAttachments = useCallback(
|
|
660
|
-
async (
|
|
661
|
-
|
|
662
|
-
|
|
809
|
+
async (
|
|
810
|
+
images: ComposerImage[],
|
|
811
|
+
audio?: ComposerAudio | null,
|
|
812
|
+
): Promise<ChatMessageAttachment[]> => {
|
|
663
813
|
const shouldPersist = historyStorage !== "nostorage";
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
814
|
+
const attachments: ChatMessageAttachment[] = [];
|
|
815
|
+
|
|
816
|
+
// Build image attachments
|
|
817
|
+
if (images.length > 0) {
|
|
818
|
+
const imageAttachments = await Promise.all(
|
|
819
|
+
images.map(async (img) => {
|
|
820
|
+
const attachmentId = createMessageId("attachment-image");
|
|
821
|
+
let blobId: string | undefined;
|
|
822
|
+
if (shouldPersist) {
|
|
823
|
+
try {
|
|
824
|
+
const persisted = await persistAttachmentBlob(
|
|
825
|
+
attachmentId,
|
|
826
|
+
img.file,
|
|
827
|
+
{
|
|
828
|
+
name: img.file.name,
|
|
829
|
+
type: img.file.type,
|
|
830
|
+
size: img.file.size,
|
|
831
|
+
},
|
|
832
|
+
);
|
|
833
|
+
blobId = persisted ?? undefined;
|
|
834
|
+
} catch (error) {
|
|
835
|
+
console.warn("[AiChatbot] Failed to persist image", error);
|
|
836
|
+
}
|
|
683
837
|
}
|
|
838
|
+
|
|
839
|
+
const objectUrl = createObjectUrl(img.file);
|
|
840
|
+
|
|
841
|
+
return {
|
|
842
|
+
id: attachmentId,
|
|
843
|
+
name: img.file.name,
|
|
844
|
+
type: img.file.type || "application/octet-stream",
|
|
845
|
+
size: img.file.size,
|
|
846
|
+
blobId,
|
|
847
|
+
objectUrl: objectUrl ?? undefined,
|
|
848
|
+
blob: img.file,
|
|
849
|
+
mediaType: "image" as const,
|
|
850
|
+
} satisfies ChatMessageAttachment;
|
|
851
|
+
}),
|
|
852
|
+
);
|
|
853
|
+
attachments.push(...imageAttachments.filter(Boolean));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Build audio attachment
|
|
857
|
+
if (audio) {
|
|
858
|
+
const attachmentId = createMessageId("attachment-audio");
|
|
859
|
+
let blobId: string | undefined;
|
|
860
|
+
if (shouldPersist) {
|
|
861
|
+
try {
|
|
862
|
+
const persisted = await persistAttachmentBlob(
|
|
863
|
+
attachmentId,
|
|
864
|
+
audio.blob,
|
|
865
|
+
{
|
|
866
|
+
name: `audio-${Date.now()}.webm`,
|
|
867
|
+
type: audio.blob.type,
|
|
868
|
+
size: audio.blob.size,
|
|
869
|
+
},
|
|
870
|
+
);
|
|
871
|
+
blobId = persisted ?? undefined;
|
|
872
|
+
} catch (error) {
|
|
873
|
+
console.warn("[AiChatbot] Failed to persist audio", error);
|
|
684
874
|
}
|
|
875
|
+
}
|
|
685
876
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
877
|
+
const objectUrl = createObjectUrl(audio.blob);
|
|
878
|
+
|
|
879
|
+
attachments.push({
|
|
880
|
+
id: attachmentId,
|
|
881
|
+
name: `audio-${Date.now()}.webm`,
|
|
882
|
+
type: audio.blob.type || "audio/webm",
|
|
883
|
+
size: audio.blob.size,
|
|
884
|
+
blobId,
|
|
885
|
+
objectUrl: objectUrl ?? undefined,
|
|
886
|
+
blob: audio.blob,
|
|
887
|
+
duration: audio.duration,
|
|
888
|
+
mediaType: "audio" as const,
|
|
889
|
+
} satisfies ChatMessageAttachment);
|
|
890
|
+
}
|
|
699
891
|
|
|
700
|
-
return
|
|
892
|
+
return attachments;
|
|
701
893
|
},
|
|
702
894
|
[historyStorage],
|
|
703
895
|
);
|
|
@@ -818,6 +1010,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
818
1010
|
const resetConversation = useCallback(() => {
|
|
819
1011
|
disposeMessagesAttachments(messagesRef.current);
|
|
820
1012
|
clearComposerImages();
|
|
1013
|
+
clearComposerAudio();
|
|
821
1014
|
setMessages([]);
|
|
822
1015
|
setStatusLineError(null);
|
|
823
1016
|
sessionRef.current = null;
|
|
@@ -835,7 +1028,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
835
1028
|
}
|
|
836
1029
|
|
|
837
1030
|
void clearAllAttachments();
|
|
838
|
-
}, [clearComposerImages, historyStorage]);
|
|
1031
|
+
}, [clearComposerImages, clearComposerAudio, historyStorage]);
|
|
839
1032
|
|
|
840
1033
|
const handleResetClick = useCallback(() => {
|
|
841
1034
|
// Open confirmation dialog
|
|
@@ -881,6 +1074,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
881
1074
|
{
|
|
882
1075
|
signal,
|
|
883
1076
|
onStatus,
|
|
1077
|
+
context,
|
|
884
1078
|
},
|
|
885
1079
|
);
|
|
886
1080
|
return null;
|
|
@@ -918,20 +1112,26 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
918
1112
|
|
|
919
1113
|
const ask = useCallback(async () => {
|
|
920
1114
|
const trimmed = questionRef.current.trim();
|
|
921
|
-
|
|
1115
|
+
const selectedAudio = composerAudioRef.current;
|
|
1116
|
+
|
|
1117
|
+
// Can send if we have text OR audio
|
|
1118
|
+
if ((!trimmed && !selectedAudio) || ai.busy) return;
|
|
922
1119
|
|
|
923
1120
|
cancelRequestedRef.current = false;
|
|
924
1121
|
setStatusLineError(null);
|
|
925
1122
|
setActiveOp("chat");
|
|
926
1123
|
const selectedImages = [...composerImagesRef.current];
|
|
927
|
-
const userAttachments = await buildUserAttachments(
|
|
1124
|
+
const userAttachments = await buildUserAttachments(
|
|
1125
|
+
selectedImages,
|
|
1126
|
+
selectedAudio,
|
|
1127
|
+
);
|
|
928
1128
|
|
|
929
1129
|
const userMessageId = createMessageId("user");
|
|
930
1130
|
const userMessageCreatedAt = Date.now();
|
|
931
1131
|
const userMessage: ChatMessage = {
|
|
932
1132
|
id: userMessageId,
|
|
933
1133
|
role: "user",
|
|
934
|
-
content: trimmed,
|
|
1134
|
+
content: trimmed || (selectedAudio ? "[Audio message]" : ""),
|
|
935
1135
|
createdAt: userMessageCreatedAt,
|
|
936
1136
|
clientStatus: "pending",
|
|
937
1137
|
attachments: userAttachments.length ? userAttachments : undefined,
|
|
@@ -940,6 +1140,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
940
1140
|
// optimistic UI
|
|
941
1141
|
setQuestion("");
|
|
942
1142
|
clearComposerImages();
|
|
1143
|
+
clearComposerAudio();
|
|
943
1144
|
setMessages((prev) => [...prev, userMessage]);
|
|
944
1145
|
|
|
945
1146
|
if (!opened) setOpened(true);
|
|
@@ -957,12 +1158,14 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
957
1158
|
const out = await sendChatMessage(
|
|
958
1159
|
{
|
|
959
1160
|
sessionId: activeSessionId,
|
|
960
|
-
message: trimmed,
|
|
1161
|
+
message: trimmed || undefined,
|
|
1162
|
+
audio: selectedAudio?.blob,
|
|
961
1163
|
images: selectedImages.map((img) => img.file),
|
|
962
1164
|
},
|
|
963
1165
|
{
|
|
964
1166
|
signal,
|
|
965
1167
|
onStatus,
|
|
1168
|
+
context,
|
|
966
1169
|
},
|
|
967
1170
|
);
|
|
968
1171
|
return out;
|
|
@@ -1044,6 +1247,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
1044
1247
|
ai,
|
|
1045
1248
|
buildUserAttachments,
|
|
1046
1249
|
clearComposerImages,
|
|
1250
|
+
clearComposerAudio,
|
|
1047
1251
|
opened,
|
|
1048
1252
|
scrollToBottom,
|
|
1049
1253
|
markLastPendingAs,
|
|
@@ -1563,40 +1767,110 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
1563
1767
|
</Stack>
|
|
1564
1768
|
|
|
1565
1769
|
{msg.attachments && msg.attachments.length > 0 && (
|
|
1566
|
-
<
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
}}
|
|
1580
|
-
onClick={() =>
|
|
1581
|
-
openAttachmentPreview(
|
|
1582
|
-
attachment.objectUrl,
|
|
1583
|
-
attachment.name,
|
|
1584
|
-
)
|
|
1585
|
-
}
|
|
1586
|
-
disabled={!attachment.objectUrl}
|
|
1587
|
-
title={attachment.name || I18n.get("View image")}
|
|
1588
|
-
aria-label={
|
|
1589
|
-
attachment.name || I18n.get("View image")
|
|
1590
|
-
}
|
|
1770
|
+
<Stack
|
|
1771
|
+
gap="xs"
|
|
1772
|
+
style={{ maxWidth: "min(400px, 100%)" }}
|
|
1773
|
+
>
|
|
1774
|
+
{/* Image attachments */}
|
|
1775
|
+
{msg.attachments.filter(
|
|
1776
|
+
(att) =>
|
|
1777
|
+
att.mediaType === "image" ||
|
|
1778
|
+
(!att.mediaType && att.type.startsWith("image/")),
|
|
1779
|
+
).length > 0 && (
|
|
1780
|
+
<Group
|
|
1781
|
+
className="ai-thumbs ai-message-thumbs"
|
|
1782
|
+
gap="xs"
|
|
1591
1783
|
>
|
|
1592
|
-
{
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1784
|
+
{msg.attachments
|
|
1785
|
+
.filter(
|
|
1786
|
+
(att) =>
|
|
1787
|
+
att.mediaType === "image" ||
|
|
1788
|
+
(!att.mediaType &&
|
|
1789
|
+
att.type.startsWith("image/")),
|
|
1790
|
+
)
|
|
1791
|
+
.map((attachment) => (
|
|
1792
|
+
<button
|
|
1793
|
+
key={attachment.id}
|
|
1794
|
+
type="button"
|
|
1795
|
+
className="thumb"
|
|
1796
|
+
style={{
|
|
1797
|
+
backgroundImage: attachment.objectUrl
|
|
1798
|
+
? `url(${attachment.objectUrl})`
|
|
1799
|
+
: undefined,
|
|
1800
|
+
backgroundSize: "cover",
|
|
1801
|
+
backgroundPosition: "center",
|
|
1802
|
+
backgroundRepeat: "no-repeat",
|
|
1803
|
+
}}
|
|
1804
|
+
onClick={() =>
|
|
1805
|
+
openAttachmentPreview(
|
|
1806
|
+
attachment.objectUrl,
|
|
1807
|
+
attachment.name,
|
|
1808
|
+
)
|
|
1809
|
+
}
|
|
1810
|
+
disabled={!attachment.objectUrl}
|
|
1811
|
+
title={
|
|
1812
|
+
attachment.name || I18n.get("View image")
|
|
1813
|
+
}
|
|
1814
|
+
aria-label={
|
|
1815
|
+
attachment.name || I18n.get("View image")
|
|
1816
|
+
}
|
|
1817
|
+
>
|
|
1818
|
+
{!attachment.objectUrl && (
|
|
1819
|
+
<Text size="xs" c="dimmed">
|
|
1820
|
+
{I18n.get("Image no longer available")}
|
|
1821
|
+
</Text>
|
|
1822
|
+
)}
|
|
1823
|
+
</button>
|
|
1824
|
+
))}
|
|
1825
|
+
</Group>
|
|
1826
|
+
)}
|
|
1827
|
+
|
|
1828
|
+
{/* Audio attachments */}
|
|
1829
|
+
{msg.attachments
|
|
1830
|
+
.filter(
|
|
1831
|
+
(att) =>
|
|
1832
|
+
att.mediaType === "audio" ||
|
|
1833
|
+
(!att.mediaType &&
|
|
1834
|
+
att.type.startsWith("audio/")),
|
|
1835
|
+
)
|
|
1836
|
+
.map((attachment) => (
|
|
1837
|
+
<Box
|
|
1838
|
+
key={attachment.id}
|
|
1839
|
+
p="sm"
|
|
1840
|
+
style={{
|
|
1841
|
+
backgroundColor:
|
|
1842
|
+
"var(--ai-kit-chat-surface-subtle)",
|
|
1843
|
+
borderRadius: "var(--ai-kit-radius-sm)",
|
|
1844
|
+
border:
|
|
1845
|
+
"1px solid var(--ai-kit-chat-border-color)",
|
|
1846
|
+
}}
|
|
1847
|
+
>
|
|
1848
|
+
{attachment.objectUrl ? (
|
|
1849
|
+
<Stack gap="xs">
|
|
1850
|
+
<audio
|
|
1851
|
+
className="ai-kit-audio-player"
|
|
1852
|
+
controls
|
|
1853
|
+
src={attachment.objectUrl}
|
|
1854
|
+
preload="metadata"
|
|
1855
|
+
/>
|
|
1856
|
+
{attachment.duration && (
|
|
1857
|
+
<Text
|
|
1858
|
+
size="xs"
|
|
1859
|
+
c="dimmed"
|
|
1860
|
+
style={{ textAlign: "right" }}
|
|
1861
|
+
>
|
|
1862
|
+
{Math.round(attachment.duration)}s
|
|
1863
|
+
</Text>
|
|
1864
|
+
)}
|
|
1865
|
+
</Stack>
|
|
1866
|
+
) : (
|
|
1867
|
+
<Text size="xs" c="dimmed">
|
|
1868
|
+
{I18n.get("Audio no longer available")}
|
|
1869
|
+
</Text>
|
|
1870
|
+
)}
|
|
1871
|
+
</Box>
|
|
1872
|
+
))}
|
|
1873
|
+
</Stack>
|
|
1600
1874
|
)}
|
|
1601
1875
|
|
|
1602
1876
|
{isLastCanceled && (
|
|
@@ -1759,12 +2033,61 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
1759
2033
|
value={question}
|
|
1760
2034
|
onChange={(e) => {
|
|
1761
2035
|
setQuestion(e.target.value);
|
|
2036
|
+
// Clear audio when typing
|
|
2037
|
+
if (composerAudio) {
|
|
2038
|
+
clearComposerAudio();
|
|
2039
|
+
}
|
|
1762
2040
|
}}
|
|
1763
2041
|
onKeyDown={handleQuestionKeyDown}
|
|
1764
2042
|
rows={3}
|
|
2043
|
+
disabled={recording || !!composerAudio}
|
|
1765
2044
|
/>
|
|
1766
2045
|
</Group>
|
|
1767
2046
|
|
|
2047
|
+
{/* Audio level indicator when recording */}
|
|
2048
|
+
{USE_AUDIO && recording && (
|
|
2049
|
+
<Stack gap="xs" mt="xs">
|
|
2050
|
+
<Text size="xs" c="dimmed">
|
|
2051
|
+
<em>{I18n.get("Recording...")}</em>
|
|
2052
|
+
</Text>
|
|
2053
|
+
<div
|
|
2054
|
+
style={{
|
|
2055
|
+
width: "100%",
|
|
2056
|
+
height: "4px",
|
|
2057
|
+
backgroundColor: "var(--mantine-color-gray-3)",
|
|
2058
|
+
borderRadius: "2px",
|
|
2059
|
+
overflow: "hidden",
|
|
2060
|
+
}}
|
|
2061
|
+
>
|
|
2062
|
+
<div
|
|
2063
|
+
style={{
|
|
2064
|
+
width: `${audioLevel}%`,
|
|
2065
|
+
height: "100%",
|
|
2066
|
+
backgroundColor: "var(--mantine-color-red-6)",
|
|
2067
|
+
transition: "width 0.1s ease",
|
|
2068
|
+
}}
|
|
2069
|
+
/>
|
|
2070
|
+
</div>
|
|
2071
|
+
</Stack>
|
|
2072
|
+
)}
|
|
2073
|
+
|
|
2074
|
+
{/* Audio playback when recorded */}
|
|
2075
|
+
{USE_AUDIO && composerAudio && !recording && (
|
|
2076
|
+
<Stack gap="xs" mt="xs">
|
|
2077
|
+
<Text size="xs" c="dimmed">
|
|
2078
|
+
<em>
|
|
2079
|
+
{I18n.get("Audio recorded")} (
|
|
2080
|
+
{Math.round(composerAudio.duration)}s)
|
|
2081
|
+
</em>
|
|
2082
|
+
</Text>
|
|
2083
|
+
<audio
|
|
2084
|
+
className="ai-kit-audio-player"
|
|
2085
|
+
controls
|
|
2086
|
+
src={composerAudio.objectUrl}
|
|
2087
|
+
/>
|
|
2088
|
+
</Stack>
|
|
2089
|
+
)}
|
|
2090
|
+
|
|
1768
2091
|
<Group className="ai-actions" justify="space-between" w="100%">
|
|
1769
2092
|
<Group justify="flex-start">
|
|
1770
2093
|
<Button
|
|
@@ -1779,6 +2102,40 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
1779
2102
|
</Group>
|
|
1780
2103
|
|
|
1781
2104
|
<Group justify="flex-end">
|
|
2105
|
+
{/* Microphone button */}
|
|
2106
|
+
{USE_AUDIO && (
|
|
2107
|
+
<>
|
|
2108
|
+
{composerAudio ? (
|
|
2109
|
+
<Button
|
|
2110
|
+
variant="outline"
|
|
2111
|
+
leftSection={<IconX size={18} />}
|
|
2112
|
+
onClick={clearComposerAudio}
|
|
2113
|
+
disabled={isChatBusy}
|
|
2114
|
+
title={I18n.get("Clear audio")}
|
|
2115
|
+
data-ai-kit-clear-audio-button
|
|
2116
|
+
>
|
|
2117
|
+
{I18n.get("Clear")}
|
|
2118
|
+
</Button>
|
|
2119
|
+
) : (
|
|
2120
|
+
<Button
|
|
2121
|
+
variant="outline"
|
|
2122
|
+
leftSection={<IconMicrophone size={18} />}
|
|
2123
|
+
onClick={recording ? stopRecording : startRecording}
|
|
2124
|
+
disabled={isChatBusy}
|
|
2125
|
+
title={
|
|
2126
|
+
recording
|
|
2127
|
+
? I18n.get("Stop recording")
|
|
2128
|
+
: I18n.get("Record audio")
|
|
2129
|
+
}
|
|
2130
|
+
color={recording ? "red" : undefined}
|
|
2131
|
+
data-ai-kit-microphone-button
|
|
2132
|
+
>
|
|
2133
|
+
{recording ? I18n.get("Stop") : I18n.get("Record")}
|
|
2134
|
+
</Button>
|
|
2135
|
+
)}
|
|
2136
|
+
</>
|
|
2137
|
+
)}
|
|
2138
|
+
|
|
1782
2139
|
{resolvedMaxImages > 0 && (
|
|
1783
2140
|
<>
|
|
1784
2141
|
<Button
|
|
@@ -1787,7 +2144,9 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
|
|
|
1787
2144
|
onClick={() => fileInputRef.current?.click()}
|
|
1788
2145
|
disabled={
|
|
1789
2146
|
composerImages.length >= resolvedMaxImages ||
|
|
1790
|
-
isChatBusy
|
|
2147
|
+
isChatBusy ||
|
|
2148
|
+
recording ||
|
|
2149
|
+
!!composerAudio
|
|
1791
2150
|
}
|
|
1792
2151
|
title={I18n.get(labels.addImageLabel)}
|
|
1793
2152
|
data-ai-kit-add-image-button
|