@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smart-cloud/ai-kit-ui",
3
- "version": "1.2.7",
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.2.7",
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
- const q =
136
- typeof inputText === "function"
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
- if (!q) return;
140
- setQuery(q);
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
- { sessionId, query: q, topK },
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) => setQuery(e.currentTarget.value)}
317
- placeholder={I18n.get("Search the documentation…")}
318
- disabled={busy}
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"