@smart-cloud/ai-kit-ui 1.2.7 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +9 -9
- package/dist/index.js +9 -9
- package/package.json +2 -2
- package/src/doc-search/DocSearch.tsx +126 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smart-cloud/ai-kit-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"@emotion/cache": "^11.14.0",
|
|
21
21
|
"@emotion/react": "^11.14.0",
|
|
22
22
|
"@mantine/colors-generator": "^8.3.15",
|
|
23
|
-
"@smart-cloud/ai-kit-core": "^1.
|
|
23
|
+
"@smart-cloud/ai-kit-core": "^1.3.0",
|
|
24
24
|
"@smart-cloud/wpsuite-core": "^2.2.6",
|
|
25
25
|
"@tabler/icons-react": "^3.36.1",
|
|
26
26
|
"chroma-js": "^3.2.0",
|
|
@@ -26,6 +26,7 @@ import rehypeRaw from "rehype-raw";
|
|
|
26
26
|
import remarkGfm from "remark-gfm";
|
|
27
27
|
|
|
28
28
|
import { IconSearch } from "@tabler/icons-react";
|
|
29
|
+
import { IconMicrophone, IconMicrophoneOff } from "@tabler/icons-react";
|
|
29
30
|
|
|
30
31
|
import { AiFeatureBorder } from "../ai-feature/AiFeatureBorder";
|
|
31
32
|
import { translations } from "../i18n";
|
|
@@ -81,6 +82,11 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
81
82
|
} = props;
|
|
82
83
|
|
|
83
84
|
const [query, setQuery] = useState<string>("");
|
|
85
|
+
const [recording, setRecording] = useState<boolean>(false);
|
|
86
|
+
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
|
|
87
|
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
|
88
|
+
const audioChunksRef = useRef<Blob[]>([]);
|
|
89
|
+
|
|
84
90
|
const { busy, error, statusEvent, result, run, cancel, reset } =
|
|
85
91
|
useAiRun<SearchResult>();
|
|
86
92
|
|
|
@@ -127,25 +133,95 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
127
133
|
|
|
128
134
|
const canSearch = useMemo(() => {
|
|
129
135
|
if (busy) return false;
|
|
136
|
+
// Can search if we have text OR audio
|
|
130
137
|
const text = typeof inputText === "function" ? inputText() : inputText;
|
|
131
|
-
return Boolean(text && text.trim().length > 0);
|
|
132
|
-
}, [inputText, busy]);
|
|
138
|
+
return Boolean((text && text.trim().length > 0) || audioBlob);
|
|
139
|
+
}, [inputText, busy, audioBlob]);
|
|
140
|
+
|
|
141
|
+
const startRecording = useCallback(async () => {
|
|
142
|
+
try {
|
|
143
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
144
|
+
const mediaRecorder = new MediaRecorder(stream, {
|
|
145
|
+
mimeType: "audio/webm",
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
audioChunksRef.current = [];
|
|
149
|
+
|
|
150
|
+
mediaRecorder.ondataavailable = (event) => {
|
|
151
|
+
if (event.data.size > 0) {
|
|
152
|
+
audioChunksRef.current.push(event.data);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
mediaRecorder.onstop = () => {
|
|
157
|
+
const audioBlob = new Blob(audioChunksRef.current, { type: "audio/webm" });
|
|
158
|
+
setAudioBlob(audioBlob);
|
|
159
|
+
stream.getTracks().forEach(track => track.stop());
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
mediaRecorderRef.current = mediaRecorder;
|
|
163
|
+
mediaRecorder.start();
|
|
164
|
+
setRecording(true);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error("Failed to start recording:", error);
|
|
167
|
+
}
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
const stopRecording = useCallback(() => {
|
|
171
|
+
if (mediaRecorderRef.current && recording) {
|
|
172
|
+
mediaRecorderRef.current.stop();
|
|
173
|
+
setRecording(false);
|
|
174
|
+
}
|
|
175
|
+
}, [recording]);
|
|
176
|
+
|
|
177
|
+
const clearAudio = useCallback(() => {
|
|
178
|
+
setAudioBlob(null);
|
|
179
|
+
audioChunksRef.current = [];
|
|
180
|
+
}, []);
|
|
133
181
|
|
|
134
182
|
const onSearch = useCallback(async () => {
|
|
135
|
-
|
|
136
|
-
|
|
183
|
+
let q: string | undefined;
|
|
184
|
+
let audio: { format: string; data: string } | undefined;
|
|
185
|
+
|
|
186
|
+
// Get text query if available
|
|
187
|
+
if (!audioBlob) {
|
|
188
|
+
q = typeof inputText === "function"
|
|
137
189
|
? (inputText as () => string)()
|
|
138
190
|
: inputText;
|
|
139
|
-
|
|
140
|
-
|
|
191
|
+
if (!q) return;
|
|
192
|
+
setQuery(q);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Convert audio blob to base64 if available
|
|
196
|
+
if (audioBlob) {
|
|
197
|
+
const reader = new FileReader();
|
|
198
|
+
await new Promise<void>((resolve, reject) => {
|
|
199
|
+
reader.onload = () => {
|
|
200
|
+
const base64 = (reader.result as string).split(",")[1];
|
|
201
|
+
audio = {
|
|
202
|
+
format: "audio/webm",
|
|
203
|
+
data: base64,
|
|
204
|
+
};
|
|
205
|
+
resolve();
|
|
206
|
+
};
|
|
207
|
+
reader.onerror = reject;
|
|
208
|
+
reader.readAsDataURL(audioBlob);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
141
212
|
reset();
|
|
142
213
|
await run(async ({ signal, onStatus }) => {
|
|
143
214
|
return await sendSearchMessage(
|
|
144
|
-
{
|
|
215
|
+
{
|
|
216
|
+
sessionId,
|
|
217
|
+
...(q && { query: q }),
|
|
218
|
+
audio,
|
|
219
|
+
topK
|
|
220
|
+
},
|
|
145
221
|
{ signal, onStatus, context },
|
|
146
222
|
);
|
|
147
223
|
});
|
|
148
|
-
}, [context, inputText, run, reset, topK, sessionId]);
|
|
224
|
+
}, [context, inputText, audioBlob, run, reset, topK, sessionId]);
|
|
149
225
|
|
|
150
226
|
const close = useCallback(async () => {
|
|
151
227
|
reset();
|
|
@@ -313,9 +389,19 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
313
389
|
<TextInput
|
|
314
390
|
style={{ flex: 1 }}
|
|
315
391
|
value={query}
|
|
316
|
-
onChange={(e) =>
|
|
317
|
-
|
|
318
|
-
|
|
392
|
+
onChange={(e) => {
|
|
393
|
+
setQuery(e.currentTarget.value);
|
|
394
|
+
// Clear audio when typing
|
|
395
|
+
if (audioBlob) {
|
|
396
|
+
clearAudio();
|
|
397
|
+
}
|
|
398
|
+
}}
|
|
399
|
+
placeholder={
|
|
400
|
+
audioBlob
|
|
401
|
+
? I18n.get("Audio recorded")
|
|
402
|
+
: I18n.get("Search the documentation…")
|
|
403
|
+
}
|
|
404
|
+
disabled={busy || recording || !!audioBlob}
|
|
319
405
|
onKeyDown={(e) => {
|
|
320
406
|
if (e.key === "Enter" && canSearch) {
|
|
321
407
|
e.preventDefault();
|
|
@@ -324,6 +410,35 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
324
410
|
}}
|
|
325
411
|
/>
|
|
326
412
|
|
|
413
|
+
{/* Microphone button */}
|
|
414
|
+
{audioBlob ? (
|
|
415
|
+
<Button
|
|
416
|
+
variant="outline"
|
|
417
|
+
size="sm"
|
|
418
|
+
color="red"
|
|
419
|
+
onClick={clearAudio}
|
|
420
|
+
disabled={busy}
|
|
421
|
+
title={I18n.get("Clear audio")}
|
|
422
|
+
>
|
|
423
|
+
<IconMicrophoneOff size={18} />
|
|
424
|
+
</Button>
|
|
425
|
+
) : (
|
|
426
|
+
<Button
|
|
427
|
+
variant={recording ? "filled" : "outline"}
|
|
428
|
+
size="sm"
|
|
429
|
+
color={recording ? "red" : "gray"}
|
|
430
|
+
onClick={recording ? stopRecording : startRecording}
|
|
431
|
+
disabled={busy}
|
|
432
|
+
title={
|
|
433
|
+
recording
|
|
434
|
+
? I18n.get("Stop recording")
|
|
435
|
+
: I18n.get("Record audio")
|
|
436
|
+
}
|
|
437
|
+
>
|
|
438
|
+
<IconMicrophone size={18} />
|
|
439
|
+
</Button>
|
|
440
|
+
)}
|
|
441
|
+
|
|
327
442
|
<Button
|
|
328
443
|
variant="filled"
|
|
329
444
|
size="sm"
|