@smart-cloud/ai-kit-ui 1.2.7 → 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 +265 -13
- 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
|
@@ -83,6 +83,9 @@ export async function persistAttachmentBlob(
|
|
|
83
83
|
blob: Blob,
|
|
84
84
|
meta: { name: string; type: string; size: number },
|
|
85
85
|
): Promise<string | null> {
|
|
86
|
+
// Enforce storage quota before adding new attachment
|
|
87
|
+
await enforceStorageQuota();
|
|
88
|
+
|
|
86
89
|
const record: AttachmentRecord = {
|
|
87
90
|
id,
|
|
88
91
|
blob,
|
|
@@ -170,3 +173,66 @@ export async function cleanupDanglingAttachments(
|
|
|
170
173
|
console.warn("[AiChatbot] Failed to cleanup attachments", error);
|
|
171
174
|
});
|
|
172
175
|
}
|
|
176
|
+
|
|
177
|
+
// Storage quota management
|
|
178
|
+
const MAX_STORAGE_BYTES = 50 * 1024 * 1024; // 50 MB limit
|
|
179
|
+
|
|
180
|
+
export async function getAllAttachments(): Promise<StoredAttachment[]> {
|
|
181
|
+
const db = await getDatabase();
|
|
182
|
+
if (!db) return [];
|
|
183
|
+
|
|
184
|
+
return new Promise<StoredAttachment[]>((resolve, reject) => {
|
|
185
|
+
try {
|
|
186
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
187
|
+
const request = tx.objectStore(STORE_NAME).getAll();
|
|
188
|
+
|
|
189
|
+
request.onsuccess = () => {
|
|
190
|
+
const records = (request.result || []) as AttachmentRecord[];
|
|
191
|
+
resolve(records);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
request.onerror = () => reject(request.error);
|
|
195
|
+
tx.onerror = () => reject(tx.error);
|
|
196
|
+
tx.onabort = () => reject(tx.error);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
reject(error);
|
|
199
|
+
}
|
|
200
|
+
}).catch((error) => {
|
|
201
|
+
console.warn("[AiChatbot] Failed to get all attachments", error);
|
|
202
|
+
return [];
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function enforceStorageQuota(): Promise<void> {
|
|
207
|
+
const attachments = await getAllAttachments();
|
|
208
|
+
if (attachments.length === 0) return;
|
|
209
|
+
|
|
210
|
+
// Sort by creation time (oldest first)
|
|
211
|
+
const sorted = attachments.sort((a, b) => a.createdAt - b.createdAt);
|
|
212
|
+
|
|
213
|
+
// Calculate total size
|
|
214
|
+
let totalSize = sorted.reduce((sum, att) => sum + att.size, 0);
|
|
215
|
+
|
|
216
|
+
// Delete oldest attachments until under quota
|
|
217
|
+
const toDelete: string[] = [];
|
|
218
|
+
let i = 0;
|
|
219
|
+
while (totalSize > MAX_STORAGE_BYTES && i < sorted.length) {
|
|
220
|
+
const attachment = sorted[i];
|
|
221
|
+
if (attachment) {
|
|
222
|
+
toDelete.push(attachment.id);
|
|
223
|
+
totalSize -= attachment.size;
|
|
224
|
+
}
|
|
225
|
+
i++;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Delete the attachments
|
|
229
|
+
for (const id of toDelete) {
|
|
230
|
+
await deleteAttachmentBlob(id);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (toDelete.length > 0) {
|
|
234
|
+
console.log(
|
|
235
|
+
`[AiChatbot] Deleted ${toDelete.length} old attachment(s) to enforce storage quota`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Loader,
|
|
8
8
|
Modal,
|
|
9
9
|
Paper,
|
|
10
|
+
Progress,
|
|
10
11
|
Stack,
|
|
11
12
|
Text,
|
|
12
13
|
TextInput,
|
|
@@ -26,6 +27,7 @@ import rehypeRaw from "rehype-raw";
|
|
|
26
27
|
import remarkGfm from "remark-gfm";
|
|
27
28
|
|
|
28
29
|
import { IconSearch } from "@tabler/icons-react";
|
|
30
|
+
import { IconMicrophone, IconMicrophoneOff } from "@tabler/icons-react";
|
|
29
31
|
|
|
30
32
|
import { AiFeatureBorder } from "../ai-feature/AiFeatureBorder";
|
|
31
33
|
import { translations } from "../i18n";
|
|
@@ -37,6 +39,8 @@ I18n.putVocabularies(translations);
|
|
|
37
39
|
|
|
38
40
|
type Props = DocSearchProps & AiKitShellInjectedProps;
|
|
39
41
|
|
|
42
|
+
const USE_AUDIO = false; // Set to true to enable audio recording feature (requires backend support for audio input)
|
|
43
|
+
|
|
40
44
|
function groupChunksByDoc(result: SearchResult | null) {
|
|
41
45
|
const docs = result?.citations?.docs ?? [];
|
|
42
46
|
const chunks = result?.citations?.chunks ?? [];
|
|
@@ -81,6 +85,22 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
81
85
|
} = props;
|
|
82
86
|
|
|
83
87
|
const [query, setQuery] = useState<string>("");
|
|
88
|
+
const [recording, setRecording] = useState<boolean>(false);
|
|
89
|
+
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
|
|
90
|
+
const [audioLevel, setAudioLevel] = useState<number>(0);
|
|
91
|
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
|
92
|
+
const audioChunksRef = useRef<Blob[]>([]);
|
|
93
|
+
const audioContextRef = useRef<AudioContext | null>(null);
|
|
94
|
+
const analyserRef = useRef<AnalyserNode | null>(null);
|
|
95
|
+
const animationFrameRef = useRef<number | null>(null);
|
|
96
|
+
|
|
97
|
+
// Audio cache: store uploaded audio to avoid re-uploading within session
|
|
98
|
+
const audioCacheRef = useRef<{
|
|
99
|
+
blob: Blob;
|
|
100
|
+
uploadTimestamp: number;
|
|
101
|
+
} | null>(null);
|
|
102
|
+
const AUDIO_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
|
103
|
+
|
|
84
104
|
const { busy, error, statusEvent, result, run, cancel, reset } =
|
|
85
105
|
useAiRun<SearchResult>();
|
|
86
106
|
|
|
@@ -127,25 +147,170 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
127
147
|
|
|
128
148
|
const canSearch = useMemo(() => {
|
|
129
149
|
if (busy) return false;
|
|
150
|
+
// Can search if we have text OR audio
|
|
130
151
|
const text = typeof inputText === "function" ? inputText() : inputText;
|
|
131
|
-
return Boolean(text && text.trim().length > 0);
|
|
132
|
-
}, [inputText, busy]);
|
|
152
|
+
return Boolean((text && text.trim().length > 0) || audioBlob);
|
|
153
|
+
}, [inputText, busy, audioBlob]);
|
|
154
|
+
|
|
155
|
+
const startRecording = useCallback(async () => {
|
|
156
|
+
try {
|
|
157
|
+
// Clear query input when starting audio recording
|
|
158
|
+
setQuery("");
|
|
159
|
+
|
|
160
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
161
|
+
const mediaRecorder = new MediaRecorder(stream, {
|
|
162
|
+
mimeType: "audio/webm",
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Setup audio analysis for visual feedback
|
|
166
|
+
const audioContext = new AudioContext();
|
|
167
|
+
const source = audioContext.createMediaStreamSource(stream);
|
|
168
|
+
const analyser = audioContext.createAnalyser();
|
|
169
|
+
analyser.fftSize = 2048; // Higher for smoother results
|
|
170
|
+
analyser.smoothingTimeConstant = 0.8; // Smoother transitions
|
|
171
|
+
source.connect(analyser);
|
|
172
|
+
|
|
173
|
+
audioContextRef.current = audioContext;
|
|
174
|
+
analyserRef.current = analyser;
|
|
175
|
+
|
|
176
|
+
// Monitor audio level using time domain data (more reliable)
|
|
177
|
+
const dataArray = new Uint8Array(analyser.fftSize);
|
|
178
|
+
const updateLevel = () => {
|
|
179
|
+
if (!analyserRef.current) return;
|
|
180
|
+
analyserRef.current.getByteTimeDomainData(dataArray);
|
|
181
|
+
|
|
182
|
+
// Calculate RMS (Root Mean Square) for accurate volume
|
|
183
|
+
let sum = 0;
|
|
184
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
185
|
+
const normalized = (dataArray[i]! - 128) / 128; // Normalize to -1 to 1
|
|
186
|
+
sum += normalized * normalized;
|
|
187
|
+
}
|
|
188
|
+
const rms = Math.sqrt(sum / dataArray.length);
|
|
189
|
+
|
|
190
|
+
// Convert to percentage (0-100) with scaling for better visibility
|
|
191
|
+
const level = Math.min(100, rms * 200);
|
|
192
|
+
setAudioLevel(level);
|
|
193
|
+
|
|
194
|
+
animationFrameRef.current = requestAnimationFrame(updateLevel);
|
|
195
|
+
};
|
|
196
|
+
updateLevel();
|
|
197
|
+
|
|
198
|
+
audioChunksRef.current = [];
|
|
199
|
+
|
|
200
|
+
mediaRecorder.ondataavailable = (event) => {
|
|
201
|
+
if (event.data.size > 0) {
|
|
202
|
+
audioChunksRef.current.push(event.data);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
mediaRecorder.onstop = () => {
|
|
207
|
+
const audioBlob = new Blob(audioChunksRef.current, {
|
|
208
|
+
type: "audio/webm",
|
|
209
|
+
});
|
|
210
|
+
setAudioBlob(audioBlob);
|
|
211
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
212
|
+
|
|
213
|
+
// Cleanup audio analysis
|
|
214
|
+
if (animationFrameRef.current) {
|
|
215
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
216
|
+
animationFrameRef.current = null;
|
|
217
|
+
}
|
|
218
|
+
if (audioContextRef.current) {
|
|
219
|
+
audioContextRef.current.close();
|
|
220
|
+
audioContextRef.current = null;
|
|
221
|
+
}
|
|
222
|
+
analyserRef.current = null;
|
|
223
|
+
setAudioLevel(0);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
mediaRecorderRef.current = mediaRecorder;
|
|
227
|
+
mediaRecorder.start();
|
|
228
|
+
setRecording(true);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error("Failed to start recording:", error);
|
|
231
|
+
}
|
|
232
|
+
}, []);
|
|
233
|
+
|
|
234
|
+
const stopRecording = useCallback(() => {
|
|
235
|
+
if (mediaRecorderRef.current && recording) {
|
|
236
|
+
mediaRecorderRef.current.stop();
|
|
237
|
+
setRecording(false);
|
|
238
|
+
}
|
|
239
|
+
}, [recording]);
|
|
240
|
+
|
|
241
|
+
const clearAudio = useCallback(() => {
|
|
242
|
+
setAudioBlob(null);
|
|
243
|
+
audioChunksRef.current = [];
|
|
244
|
+
setAudioLevel(0);
|
|
245
|
+
// Clear audio cache when user manually clears audio
|
|
246
|
+
audioCacheRef.current = null;
|
|
247
|
+
}, []);
|
|
248
|
+
|
|
249
|
+
// Cleanup on unmount
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
return () => {
|
|
252
|
+
if (animationFrameRef.current) {
|
|
253
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
254
|
+
}
|
|
255
|
+
if (audioContextRef.current) {
|
|
256
|
+
audioContextRef.current.close();
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}, []);
|
|
133
260
|
|
|
134
261
|
const onSearch = useCallback(async () => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
262
|
+
let q: string | undefined;
|
|
263
|
+
|
|
264
|
+
// Get text query if available (not recording audio)
|
|
265
|
+
if (!audioBlob) {
|
|
266
|
+
q =
|
|
267
|
+
typeof inputText === "function"
|
|
268
|
+
? (inputText as () => string)()
|
|
269
|
+
: inputText;
|
|
270
|
+
if (!q) return;
|
|
271
|
+
setQuery(q);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log("Starting search with query:", q, "and audio:", audioBlob);
|
|
275
|
+
|
|
276
|
+
// Check if we can reuse cached audio (same blob within TTL)
|
|
277
|
+
const now = Date.now();
|
|
278
|
+
const isSameAudio =
|
|
279
|
+
audioBlob &&
|
|
280
|
+
audioCacheRef.current?.blob === audioBlob &&
|
|
281
|
+
now - audioCacheRef.current.uploadTimestamp < AUDIO_CACHE_TTL;
|
|
282
|
+
|
|
283
|
+
if (audioBlob && !isSameAudio) {
|
|
284
|
+
// Update cache for new audio blob
|
|
285
|
+
audioCacheRef.current = {
|
|
286
|
+
blob: audioBlob,
|
|
287
|
+
uploadTimestamp: now,
|
|
288
|
+
};
|
|
289
|
+
console.log("Audio cache updated for new recording");
|
|
290
|
+
} else if (isSameAudio) {
|
|
291
|
+
console.log(
|
|
292
|
+
"Reusing cached audio (no re-upload needed within",
|
|
293
|
+
Math.round(
|
|
294
|
+
(AUDIO_CACHE_TTL - (now - audioCacheRef.current!.uploadTimestamp)) /
|
|
295
|
+
1000,
|
|
296
|
+
),
|
|
297
|
+
"seconds)",
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
141
301
|
reset();
|
|
142
302
|
await run(async ({ signal, onStatus }) => {
|
|
143
303
|
return await sendSearchMessage(
|
|
144
|
-
{
|
|
304
|
+
{
|
|
305
|
+
sessionId,
|
|
306
|
+
...(q && { query: q }),
|
|
307
|
+
...(audioBlob && { audio: audioBlob }), // Pass Blob directly
|
|
308
|
+
topK,
|
|
309
|
+
},
|
|
145
310
|
{ signal, onStatus, context },
|
|
146
311
|
);
|
|
147
312
|
});
|
|
148
|
-
}, [context, inputText, run, reset, topK, sessionId]);
|
|
313
|
+
}, [context, inputText, audioBlob, run, reset, topK, sessionId]);
|
|
149
314
|
|
|
150
315
|
const close = useCallback(async () => {
|
|
151
316
|
reset();
|
|
@@ -313,9 +478,19 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
313
478
|
<TextInput
|
|
314
479
|
style={{ flex: 1 }}
|
|
315
480
|
value={query}
|
|
316
|
-
onChange={(e) =>
|
|
317
|
-
|
|
318
|
-
|
|
481
|
+
onChange={(e) => {
|
|
482
|
+
setQuery(e.currentTarget.value);
|
|
483
|
+
// Clear audio when typing
|
|
484
|
+
if (audioBlob) {
|
|
485
|
+
clearAudio();
|
|
486
|
+
}
|
|
487
|
+
}}
|
|
488
|
+
placeholder={
|
|
489
|
+
audioBlob
|
|
490
|
+
? I18n.get("Audio recorded")
|
|
491
|
+
: I18n.get("Search the documentation…")
|
|
492
|
+
}
|
|
493
|
+
disabled={busy || recording || !!audioBlob}
|
|
319
494
|
onKeyDown={(e) => {
|
|
320
495
|
if (e.key === "Enter" && canSearch) {
|
|
321
496
|
e.preventDefault();
|
|
@@ -324,6 +499,48 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
324
499
|
}}
|
|
325
500
|
/>
|
|
326
501
|
|
|
502
|
+
{
|
|
503
|
+
/* Microphone button */ USE_AUDIO && (
|
|
504
|
+
<>
|
|
505
|
+
{audioBlob ? (
|
|
506
|
+
<Button
|
|
507
|
+
variant="outline"
|
|
508
|
+
size="sm"
|
|
509
|
+
color="red"
|
|
510
|
+
onClick={clearAudio}
|
|
511
|
+
disabled={busy}
|
|
512
|
+
title={I18n.get("Clear audio")}
|
|
513
|
+
>
|
|
514
|
+
<IconMicrophoneOff size={18} />
|
|
515
|
+
</Button>
|
|
516
|
+
) : (
|
|
517
|
+
<Button
|
|
518
|
+
variant={recording ? "filled" : "outline"}
|
|
519
|
+
size="sm"
|
|
520
|
+
color={recording ? "red" : "gray"}
|
|
521
|
+
onClick={recording ? stopRecording : startRecording}
|
|
522
|
+
disabled={busy}
|
|
523
|
+
title={
|
|
524
|
+
recording
|
|
525
|
+
? I18n.get("Stop recording")
|
|
526
|
+
: I18n.get("Record audio")
|
|
527
|
+
}
|
|
528
|
+
style={
|
|
529
|
+
recording
|
|
530
|
+
? {
|
|
531
|
+
transform: `scale(${1 + audioLevel / 300})`,
|
|
532
|
+
transition: "transform 0.1s ease-out",
|
|
533
|
+
}
|
|
534
|
+
: undefined
|
|
535
|
+
}
|
|
536
|
+
>
|
|
537
|
+
<IconMicrophone size={18} />
|
|
538
|
+
</Button>
|
|
539
|
+
)}
|
|
540
|
+
</>
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
|
|
327
544
|
<Button
|
|
328
545
|
variant="filled"
|
|
329
546
|
size="sm"
|
|
@@ -346,6 +563,41 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
346
563
|
) : null}
|
|
347
564
|
</Group>
|
|
348
565
|
|
|
566
|
+
{
|
|
567
|
+
/* Audio level indicator when recording */ USE_AUDIO && (
|
|
568
|
+
<>
|
|
569
|
+
{recording && (
|
|
570
|
+
<Stack gap="xs">
|
|
571
|
+
<Text size="xs" c="dimmed">
|
|
572
|
+
{I18n.get("Recording...")} 🎤
|
|
573
|
+
</Text>
|
|
574
|
+
<Progress
|
|
575
|
+
value={audioLevel}
|
|
576
|
+
size="sm"
|
|
577
|
+
color="red"
|
|
578
|
+
animated
|
|
579
|
+
striped
|
|
580
|
+
/>
|
|
581
|
+
</Stack>
|
|
582
|
+
)}
|
|
583
|
+
|
|
584
|
+
{/* Audio playback when recorded */}
|
|
585
|
+
{audioBlob && !recording && (
|
|
586
|
+
<Stack gap="xs">
|
|
587
|
+
<Text size="xs" c="dimmed">
|
|
588
|
+
{I18n.get("Recorded audio:")}
|
|
589
|
+
</Text>
|
|
590
|
+
<audio
|
|
591
|
+
controls
|
|
592
|
+
src={URL.createObjectURL(audioBlob)}
|
|
593
|
+
className="ai-kit-audio-player"
|
|
594
|
+
/>
|
|
595
|
+
</Stack>
|
|
596
|
+
)}
|
|
597
|
+
</>
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
|
|
349
601
|
{error ? (
|
|
350
602
|
<Alert color="red" title={I18n.get("Error")}>
|
|
351
603
|
{error}
|
package/src/i18n/ar.ts
CHANGED
|
@@ -20,6 +20,9 @@ export const arDict: Record<string, string> = {
|
|
|
20
20
|
"Ask me": "اسألني",
|
|
21
21
|
Assistant: "المساعد",
|
|
22
22
|
"Assistant is thinking…": "المساعد يفكر…",
|
|
23
|
+
"Audio message": "رسالة صوتية",
|
|
24
|
+
"Audio no longer available": "لم يعد الصوت متاحًا",
|
|
25
|
+
"Audio recorded": "تم تسجيل الصوت",
|
|
23
26
|
auto: "آلي",
|
|
24
27
|
"Auto-detect": "الكشف التلقائي",
|
|
25
28
|
Cancel: "يلغي",
|
|
@@ -28,6 +31,8 @@ export const arDict: Record<string, string> = {
|
|
|
28
31
|
casual: "غير رسمي",
|
|
29
32
|
"Checking capabilities…": "إمكانيات الفحص…",
|
|
30
33
|
Chinese: "الصينية",
|
|
34
|
+
Clear: "مسح",
|
|
35
|
+
"Clear audio": "مسح الصوت",
|
|
31
36
|
"Click again to confirm": "انقر مرة أخرى للتأكيد",
|
|
32
37
|
Close: "يغلق",
|
|
33
38
|
"Close chat": "إغلاق الدردشة",
|
|
@@ -72,6 +77,7 @@ export const arDict: Record<string, string> = {
|
|
|
72
77
|
HTML: "HTML",
|
|
73
78
|
Hungarian: "المجرية",
|
|
74
79
|
"I'm ready to assist you.": "أنا جاهز لمساعدتك.",
|
|
80
|
+
"Image no longer available": "لم تعد الصورة متاحة",
|
|
75
81
|
Indonesian: "إندونيسيا",
|
|
76
82
|
"Initializing on-device AI…": "جارٍ تهيئة الذكاء الاصطناعي على الجهاز…",
|
|
77
83
|
"Inlining images as base64": "تضمين الصور بصيغة base64",
|
|
@@ -123,6 +129,9 @@ export const arDict: Record<string, string> = {
|
|
|
123
129
|
"Ready.": "جاهز.",
|
|
124
130
|
"Received backend response.": "تم استلام رد من الخادم الخلفي.",
|
|
125
131
|
"Receiving response...": "جارٍ استلام الرد...",
|
|
132
|
+
Record: "تسجيل",
|
|
133
|
+
"Record audio": "تسجيل صوت",
|
|
134
|
+
"Recording...": "جارٍ التسجيل…",
|
|
126
135
|
Reference: "مرجع",
|
|
127
136
|
References: "المراجع",
|
|
128
137
|
Regenerate: "تجديد",
|
|
@@ -156,6 +165,7 @@ export const arDict: Record<string, string> = {
|
|
|
156
165
|
Sources: "مصادر",
|
|
157
166
|
Spanish: "الأسبانية",
|
|
158
167
|
Stop: "قف",
|
|
168
|
+
"Stop recording": "إيقاف التسجيل",
|
|
159
169
|
"Suggested change": "التغيير المقترح",
|
|
160
170
|
Summarize: "لخص",
|
|
161
171
|
"Summarize again": "أعد التلخيص",
|
package/src/i18n/de.ts
CHANGED
|
@@ -20,6 +20,9 @@ export const deDict: Record<string, string> = {
|
|
|
20
20
|
"Ask me": "Frag mich",
|
|
21
21
|
Assistant: "Assistent",
|
|
22
22
|
"Assistant is thinking…": "Der Assistent denkt nach…",
|
|
23
|
+
"Audio message": "Sprachnachricht",
|
|
24
|
+
"Audio no longer available": "Audio nicht mehr verfügbar",
|
|
25
|
+
"Audio recorded": "Audio aufgenommen",
|
|
23
26
|
auto: "automatisch",
|
|
24
27
|
"Auto-detect": "Automatisch erkennen",
|
|
25
28
|
Cancel: "Abbrechen",
|
|
@@ -28,6 +31,8 @@ export const deDict: Record<string, string> = {
|
|
|
28
31
|
casual: "locker",
|
|
29
32
|
"Checking capabilities…": "Fähigkeiten werden geprüft…",
|
|
30
33
|
Chinese: "Chinesisch",
|
|
34
|
+
Clear: "Löschen",
|
|
35
|
+
"Clear audio": "Audio löschen",
|
|
31
36
|
"Click again to confirm": "Zum Bestätigen erneut klicken",
|
|
32
37
|
Close: "Schließen",
|
|
33
38
|
"Close chat": "Chat schließen",
|
|
@@ -72,6 +77,7 @@ export const deDict: Record<string, string> = {
|
|
|
72
77
|
HTML: "HTML",
|
|
73
78
|
Hungarian: "Ungarisch",
|
|
74
79
|
"I'm ready to assist you.": "Ich bin bereit, dir zu helfen.",
|
|
80
|
+
"Image no longer available": "Bild nicht mehr verfügbar",
|
|
75
81
|
Indonesian: "Indonesisch",
|
|
76
82
|
"Initializing on-device AI…": "On-Device-KI wird initialisiert…",
|
|
77
83
|
"Inlining images as base64": "Bilder werden als Base64 eingebettet",
|
|
@@ -123,6 +129,9 @@ export const deDict: Record<string, string> = {
|
|
|
123
129
|
"Ready.": "Bereit.",
|
|
124
130
|
"Received backend response.": "Backend-Antwort erhalten.",
|
|
125
131
|
"Receiving response...": "Antwort wird empfangen...",
|
|
132
|
+
Record: "Aufnehmen",
|
|
133
|
+
"Record audio": "Audio aufnehmen",
|
|
134
|
+
"Recording...": "Aufnahme…",
|
|
126
135
|
Reference: "Referenz",
|
|
127
136
|
References: "Referenzen",
|
|
128
137
|
Regenerate: "Neu generieren",
|
|
@@ -156,6 +165,7 @@ export const deDict: Record<string, string> = {
|
|
|
156
165
|
Sources: "Quellen",
|
|
157
166
|
Spanish: "Spanisch",
|
|
158
167
|
Stop: "Stoppen",
|
|
168
|
+
"Stop recording": "Aufnahme stoppen",
|
|
159
169
|
"Suggested change": "Vorgeschlagene Änderung",
|
|
160
170
|
Summarize: "Zusammenfassen",
|
|
161
171
|
"Summarize again": "Erneut zusammenfassen",
|
package/src/i18n/en.ts
CHANGED
|
@@ -20,6 +20,9 @@ export const enDict: Record<string, string> = {
|
|
|
20
20
|
"Ask me": "Ask me",
|
|
21
21
|
Assistant: "Assistant",
|
|
22
22
|
"Assistant is thinking…": "Assistant is thinking…",
|
|
23
|
+
"Audio message": "Audio message",
|
|
24
|
+
"Audio no longer available": "Audio no longer available",
|
|
25
|
+
"Audio recorded": "Audio recorded",
|
|
23
26
|
auto: "auto",
|
|
24
27
|
"Auto-detect": "Auto-detect",
|
|
25
28
|
Cancel: "Cancel",
|
|
@@ -28,6 +31,8 @@ export const enDict: Record<string, string> = {
|
|
|
28
31
|
casual: "casual",
|
|
29
32
|
"Checking capabilities…": "Checking capabilities…",
|
|
30
33
|
Chinese: "Chinese",
|
|
34
|
+
Clear: "Clear",
|
|
35
|
+
"Clear audio": "Clear audio",
|
|
31
36
|
"Click again to confirm": "Click again to confirm",
|
|
32
37
|
Close: "Close",
|
|
33
38
|
"Close chat": "Close chat",
|
|
@@ -72,6 +77,7 @@ export const enDict: Record<string, string> = {
|
|
|
72
77
|
HTML: "HTML",
|
|
73
78
|
Hungarian: "Hungarian",
|
|
74
79
|
"I'm ready to assist you.": "I'm ready to assist you.",
|
|
80
|
+
"Image no longer available": "Image no longer available",
|
|
75
81
|
Indonesian: "Indonesian",
|
|
76
82
|
"Initializing on-device AI…": "Initializing on-device AI…",
|
|
77
83
|
"Inlining images as base64": "Inlining images as base64",
|
|
@@ -123,6 +129,9 @@ export const enDict: Record<string, string> = {
|
|
|
123
129
|
"Ready.": "Ready.",
|
|
124
130
|
"Received backend response.": "Received backend response.",
|
|
125
131
|
"Receiving response...": "Receiving response...",
|
|
132
|
+
Record: "Record",
|
|
133
|
+
"Record audio": "Record audio",
|
|
134
|
+
"Recording...": "Recording...",
|
|
126
135
|
Reference: "Reference",
|
|
127
136
|
References: "References",
|
|
128
137
|
Regenerate: "Regenerate",
|
|
@@ -156,6 +165,7 @@ export const enDict: Record<string, string> = {
|
|
|
156
165
|
Sources: "Sources",
|
|
157
166
|
Spanish: "Spanish",
|
|
158
167
|
Stop: "Stop",
|
|
168
|
+
"Stop recording": "Stop recording",
|
|
159
169
|
"Suggested change": "Suggested change",
|
|
160
170
|
Summarize: "Summarize",
|
|
161
171
|
"Summarize again": "Summarize again",
|
package/src/i18n/es.ts
CHANGED
|
@@ -20,6 +20,9 @@ export const esDict: Record<string, string> = {
|
|
|
20
20
|
"Ask me": "Pregúntame",
|
|
21
21
|
Assistant: "Asistente",
|
|
22
22
|
"Assistant is thinking…": "El asistente está pensando…",
|
|
23
|
+
"Audio message": "Mensaje de audio",
|
|
24
|
+
"Audio no longer available": "El audio ya no está disponible",
|
|
25
|
+
"Audio recorded": "Audio grabado",
|
|
23
26
|
auto: "Automático",
|
|
24
27
|
"Auto-detect": "Detección automática",
|
|
25
28
|
Cancel: "Cancelar",
|
|
@@ -28,6 +31,8 @@ export const esDict: Record<string, string> = {
|
|
|
28
31
|
casual: "casual",
|
|
29
32
|
"Checking capabilities…": "Comprobando capacidades…",
|
|
30
33
|
Chinese: "Chino",
|
|
34
|
+
Clear: "Borrar",
|
|
35
|
+
"Clear audio": "Borrar audio",
|
|
31
36
|
"Click again to confirm": "Haz clic de nuevo para confirmar",
|
|
32
37
|
Close: "Cerrar",
|
|
33
38
|
"Close chat": "Cerrar chat",
|
|
@@ -74,6 +79,7 @@ export const esDict: Record<string, string> = {
|
|
|
74
79
|
HTML: "HTML",
|
|
75
80
|
Hungarian: "húngaro",
|
|
76
81
|
"I'm ready to assist you.": "Estoy listo para ayudarte.",
|
|
82
|
+
"Image no longer available": "La imagen ya no está disponible",
|
|
77
83
|
Indonesian: "indonesio",
|
|
78
84
|
"Initializing on-device AI…": "Inicializando IA en el dispositivo…",
|
|
79
85
|
"Inlining images as base64": "Inserción de imágenes en base64",
|
|
@@ -125,6 +131,9 @@ export const esDict: Record<string, string> = {
|
|
|
125
131
|
"Ready.": "Listo.",
|
|
126
132
|
"Received backend response.": "Se recibió una respuesta del backend.",
|
|
127
133
|
"Receiving response...": "Recibiendo respuesta...",
|
|
134
|
+
Record: "Grabar",
|
|
135
|
+
"Record audio": "Grabar audio",
|
|
136
|
+
"Recording...": "Grabando…",
|
|
128
137
|
Reference: "Referencia",
|
|
129
138
|
References: "Referencias",
|
|
130
139
|
Regenerate: "Regenerado",
|
|
@@ -158,6 +167,7 @@ export const esDict: Record<string, string> = {
|
|
|
158
167
|
Sources: "Fuentes",
|
|
159
168
|
Spanish: "Español",
|
|
160
169
|
Stop: "Detener",
|
|
170
|
+
"Stop recording": "Detener grabación",
|
|
161
171
|
"Suggested change": "Cambio sugerido",
|
|
162
172
|
Summarize: "Resumir",
|
|
163
173
|
"Summarize again": "Resumir de nuevo",
|
package/src/i18n/fr.ts
CHANGED
|
@@ -20,6 +20,9 @@ export const frDict: Record<string, string> = {
|
|
|
20
20
|
"Ask me": "Demandez-moi",
|
|
21
21
|
Assistant: "Assistant",
|
|
22
22
|
"Assistant is thinking…": "L'assistant réfléchit…",
|
|
23
|
+
"Audio message": "Message audio",
|
|
24
|
+
"Audio no longer available": "L’audio n’est plus disponible",
|
|
25
|
+
"Audio recorded": "Audio enregistré",
|
|
23
26
|
auto: "Automatique",
|
|
24
27
|
"Auto-detect": "Détection automatique",
|
|
25
28
|
Cancel: "Annuler",
|
|
@@ -28,6 +31,8 @@ export const frDict: Record<string, string> = {
|
|
|
28
31
|
casual: "occasionnel",
|
|
29
32
|
"Checking capabilities…": "Vérification des capacités…",
|
|
30
33
|
Chinese: "Chinois",
|
|
34
|
+
Clear: "Effacer",
|
|
35
|
+
"Clear audio": "Effacer l’audio",
|
|
31
36
|
"Click again to confirm": "Cliquez à nouveau pour confirmer",
|
|
32
37
|
Close: "Fermer",
|
|
33
38
|
"Close chat": "Fermer le chat",
|
|
@@ -74,6 +79,7 @@ export const frDict: Record<string, string> = {
|
|
|
74
79
|
HTML: "HTML",
|
|
75
80
|
Hungarian: "hongrois",
|
|
76
81
|
"I'm ready to assist you.": "Je suis prêt à vous aider.",
|
|
82
|
+
"Image no longer available": "L’image n’est plus disponible",
|
|
77
83
|
Indonesian: "indonésien",
|
|
78
84
|
"Initializing on-device AI…": "Initialisation de l'IA embarquée…",
|
|
79
85
|
"Inlining images as base64": "Intégration des images en base64",
|
|
@@ -125,6 +131,9 @@ export const frDict: Record<string, string> = {
|
|
|
125
131
|
"Ready.": "Prêt.",
|
|
126
132
|
"Received backend response.": "Réponse du serveur reçue.",
|
|
127
133
|
"Receiving response...": "Réception de la réponse...",
|
|
134
|
+
Record: "Enregistrer",
|
|
135
|
+
"Record audio": "Enregistrer un audio",
|
|
136
|
+
"Recording...": "Enregistrement…",
|
|
128
137
|
Reference: "Référence",
|
|
129
138
|
References: "Références",
|
|
130
139
|
Regenerate: "Régénérer",
|
|
@@ -158,6 +167,7 @@ export const frDict: Record<string, string> = {
|
|
|
158
167
|
Sources: "Sources",
|
|
159
168
|
Spanish: "Espagnol",
|
|
160
169
|
Stop: "Arrêt",
|
|
170
|
+
"Stop recording": "Arrêter l’enregistrement",
|
|
161
171
|
"Suggested change": "Changement suggéré",
|
|
162
172
|
Summarize: "Résumer",
|
|
163
173
|
"Summarize again": "Résumons à nouveau",
|
package/src/i18n/he.ts
CHANGED
|
@@ -20,6 +20,9 @@ export const heDict: Record<string, string> = {
|
|
|
20
20
|
"Ask me": "שאל אותי",
|
|
21
21
|
Assistant: "עוזר",
|
|
22
22
|
"Assistant is thinking…": "העוזר חושב…",
|
|
23
|
+
"Audio message": "הודעת שמע",
|
|
24
|
+
"Audio no longer available": "האודיו אינו זמין עוד",
|
|
25
|
+
"Audio recorded": "האודיו הוקלט",
|
|
23
26
|
auto: "אוטומטי",
|
|
24
27
|
"Auto-detect": "זיהוי אוטומטי",
|
|
25
28
|
Cancel: "לְבַטֵל",
|
|
@@ -28,6 +31,8 @@ export const heDict: Record<string, string> = {
|
|
|
28
31
|
casual: "אַגָבִי",
|
|
29
32
|
"Checking capabilities…": "בודק יכולות…",
|
|
30
33
|
Chinese: "סִינִית",
|
|
34
|
+
Clear: "נקה",
|
|
35
|
+
"Clear audio": "נקה שמע",
|
|
31
36
|
"Click again to confirm": "לחץ שוב כדי לאשר",
|
|
32
37
|
Close: "לִסְגוֹר",
|
|
33
38
|
"Close chat": "סגור צ׳אט",
|
|
@@ -72,6 +77,7 @@ export const heDict: Record<string, string> = {
|
|
|
72
77
|
HTML: "HTML",
|
|
73
78
|
Hungarian: "הוּנגָרִי",
|
|
74
79
|
"I'm ready to assist you.": "אני מוכן לעזור לך.",
|
|
80
|
+
"Image no longer available": "התמונה אינה זמינה עוד",
|
|
75
81
|
Indonesian: "אינדונזית",
|
|
76
82
|
"Initializing on-device AI…": "מאתחל את הבינה המלאכותית במכשיר…",
|
|
77
83
|
"Inlining images as base64": "תמונות מוטבעות כ-base64",
|
|
@@ -123,6 +129,9 @@ export const heDict: Record<string, string> = {
|
|
|
123
129
|
"Ready.": "מוכן.",
|
|
124
130
|
"Received backend response.": "התקבלה תגובה מה-backend.",
|
|
125
131
|
"Receiving response...": "מתקבלת תגובה...",
|
|
132
|
+
Record: "הקלט",
|
|
133
|
+
"Record audio": "הקלט שמע",
|
|
134
|
+
"Recording...": "מקליט...",
|
|
126
135
|
Reference: "מקור",
|
|
127
136
|
References: "מקורות",
|
|
128
137
|
Regenerate: "לְהִתְחַדֵשׁ",
|
|
@@ -156,6 +165,7 @@ export const heDict: Record<string, string> = {
|
|
|
156
165
|
Sources: "מקורות",
|
|
157
166
|
Spanish: "סְפָרַדִית",
|
|
158
167
|
Stop: "לְהַפְסִיק",
|
|
168
|
+
"Stop recording": "עצור הקלטה",
|
|
159
169
|
"Suggested change": "שינוי מוצע",
|
|
160
170
|
Summarize: "לְסַכֵּם",
|
|
161
171
|
"Summarize again": "לסכם שוב",
|