@aginies/webuikit 0.2.0 → 0.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.js CHANGED
@@ -15,6 +15,7 @@ var trimSlash = (s) => s.replace(/\/+$/, "");
15
15
  var AginiesClient = class {
16
16
  baseUrl;
17
17
  token;
18
+ hosted;
18
19
  locale;
19
20
  fetchImpl;
20
21
  state = { status: "idle" };
@@ -23,9 +24,12 @@ var AginiesClient = class {
23
24
  session = null;
24
25
  constructor(config) {
25
26
  if (!config?.baseUrl) throw new AginiesError("baseUrl is required", 0, "MISSING_BASE_URL");
26
- if (!config?.token) throw new AginiesError("token is required", 0, "MISSING_TOKEN");
27
+ if (!config?.token && !config?.hosted) {
28
+ throw new AginiesError("token is required", 0, "MISSING_TOKEN");
29
+ }
27
30
  this.baseUrl = trimSlash(config.baseUrl);
28
- this.token = config.token;
31
+ this.token = config.token ?? null;
32
+ this.hosted = config.hosted === true;
29
33
  this.locale = config.locale ?? detectLocale();
30
34
  this.fetchImpl = config.fetch ?? ((...args) => fetch(...args));
31
35
  }
@@ -52,7 +56,7 @@ var AginiesClient = class {
52
56
  const res = await this.fetchImpl(`${this.baseUrl}/api/ui/activate`, {
53
57
  method: "POST",
54
58
  headers: { "Content-Type": "application/json" },
55
- body: JSON.stringify({ token: this.token })
59
+ body: JSON.stringify(this.hosted ? { hosted: true } : { token: this.token })
56
60
  });
57
61
  if (!res.ok) {
58
62
  const body2 = await safeJson(res);
@@ -127,6 +131,30 @@ var AginiesClient = class {
127
131
  this.assertActive();
128
132
  return this.request(path2, init2);
129
133
  }
134
+ /** Sends a recording to the platform's transcription model. */
135
+ async transcribe(identifier, audio, filename = "turn.webm", language) {
136
+ this.assertActive();
137
+ const form = new FormData();
138
+ form.append("file", audio, filename);
139
+ if (language) form.append("language", language);
140
+ const res = await this.request(`/api/chat/${encodeURIComponent(identifier)}/voice/transcribe`, {
141
+ method: "POST",
142
+ body: form
143
+ });
144
+ if (!res.ok) throw await voiceError(res);
145
+ return await res.json();
146
+ }
147
+ /** Audio for a reply from the platform's synthesis endpoint. */
148
+ async speak(identifier, text) {
149
+ this.assertActive();
150
+ const res = await this.request(`/api/chat/${encodeURIComponent(identifier)}/voice/speak`, {
151
+ method: "POST",
152
+ headers: { "Content-Type": "application/json" },
153
+ body: JSON.stringify({ text })
154
+ });
155
+ if (!res.ok) throw await voiceError(res);
156
+ return res.blob();
157
+ }
130
158
  /** Hosted-chat configuration. Resolves to an auth requirement instead of throwing on 401. */
131
159
  async getChat(identifier) {
132
160
  this.assertActive();
@@ -216,6 +244,15 @@ function decodeFrame(frame) {
216
244
  }
217
245
  return null;
218
246
  }
247
+ async function voiceError(res) {
248
+ const body = await safeJson(res);
249
+ if (res.status === 401 && body && typeof body.authRequired === "string") {
250
+ return new AginiesError("Authentication required", 401, "AUTH_REQUIRED");
251
+ }
252
+ if (res.status === 503)
253
+ return new AginiesError("Voice is not available", 503, "VOICE_UNAVAILABLE");
254
+ return new AginiesError(body?.error || `HTTP ${res.status}`, res.status);
255
+ }
219
256
  async function safeJson(res) {
220
257
  try {
221
258
  return await res.json();
@@ -514,6 +551,35 @@ var STRINGS = {
514
551
  },
515
552
  ssoButton: { tr: "Kurumsal giri\u015F", en: "Sign in" }
516
553
  },
554
+ voice: {
555
+ talk: { tr: "Konu\u015Fmak i\xE7in dokunun", en: "Tap to talk" },
556
+ stopTalking: { tr: "Bitirmek i\xE7in dokunun", en: "Tap when done" },
557
+ dictate: { tr: "Sesle yaz", en: "Dictate" },
558
+ handsFree: { tr: "Sesli sohbet", en: "Voice conversation" },
559
+ exit: { tr: "Sesli sohbetten \xE7\u0131k", en: "Leave voice conversation" },
560
+ listening: { tr: "Dinliyor", en: "Listening" },
561
+ transcribing: { tr: "Yaz\u0131ya d\xF6k\xFCl\xFCyor", en: "Transcribing" },
562
+ thinking: { tr: "Yan\u0131t haz\u0131rlan\u0131yor", en: "Working on it" },
563
+ speaking: { tr: "Konu\u015Fuyor", en: "Speaking" },
564
+ idle: { tr: "Haz\u0131r", en: "Ready" },
565
+ interrupt: { tr: "S\xF6z\xFCn\xFC kesmek i\xE7in dokunun", en: "Tap to interrupt" },
566
+ micDenied: {
567
+ tr: "Mikrofon izni verilmedi. Taray\u0131c\u0131 ayarlar\u0131ndan izin verin.",
568
+ en: "Microphone access was denied. Allow it in your browser settings."
569
+ },
570
+ unsupported: {
571
+ tr: "Bu taray\u0131c\u0131 ses kayd\u0131n\u0131 desteklemiyor.",
572
+ en: "This browser cannot record audio."
573
+ },
574
+ unavailable: {
575
+ tr: "Ses \xF6zelli\u011Fi bu sohbet i\xE7in a\xE7\u0131k de\u011Fil.",
576
+ en: "Voice is not enabled for this chat."
577
+ },
578
+ error: {
579
+ tr: "Ses i\u015Flenemedi. L\xFCtfen tekrar deneyin.",
580
+ en: "Voice could not be processed. Please try again."
581
+ }
582
+ },
517
583
  run: {
518
584
  submit: { tr: "\xC7al\u0131\u015Ft\u0131r", en: "Run" },
519
585
  cancel: { tr: "\u0130ptal", en: "Cancel" },
@@ -964,7 +1030,7 @@ function renderField(f, value, set, disabled) {
964
1030
  }
965
1031
 
966
1032
  // src/chat/chat-widget.tsx
967
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
1033
+ import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
968
1034
 
969
1035
  // src/chat/markdown.tsx
970
1036
  import { Fragment as Fragment3 } from "react";
@@ -1274,20 +1340,258 @@ function Pie({ labels, data }) {
1274
1340
  ] });
1275
1341
  }
1276
1342
 
1343
+ // src/chat/voice.ts
1344
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
1345
+ var MIME_CANDIDATES = [
1346
+ "audio/webm;codecs=opus",
1347
+ "audio/webm",
1348
+ "audio/mp4",
1349
+ "audio/ogg;codecs=opus"
1350
+ ];
1351
+ var MIN_CLIP_BYTES = 800;
1352
+ var SPEECH_RMS = 0.02;
1353
+ function isVoiceSupported() {
1354
+ return typeof window !== "undefined" && typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof MediaRecorder !== "undefined";
1355
+ }
1356
+ function useVoice({
1357
+ identifier,
1358
+ onTranscript,
1359
+ language,
1360
+ silenceMs = 1400,
1361
+ maxMs = 6e4
1362
+ }) {
1363
+ const { client, t } = useAginies();
1364
+ const supported = isVoiceSupported();
1365
+ const [state, setState] = useState3(supported ? "idle" : "unsupported");
1366
+ const [error, setError] = useState3(null);
1367
+ const [level, setLevel] = useState3(0);
1368
+ const recorderRef = useRef2(null);
1369
+ const streamRef = useRef2(null);
1370
+ const chunksRef = useRef2([]);
1371
+ const discardRef = useRef2(false);
1372
+ const timersRef = useRef2([]);
1373
+ const meterRef = useRef2(null);
1374
+ const audioRef = useRef2(null);
1375
+ const audioUrlRef = useRef2(null);
1376
+ const startingRef = useRef2(false);
1377
+ const onTranscriptRef = useRef2(onTranscript);
1378
+ onTranscriptRef.current = onTranscript;
1379
+ const clearTimers = useCallback2(() => {
1380
+ for (const id of timersRef.current) window.clearTimeout(id);
1381
+ timersRef.current = [];
1382
+ if (meterRef.current) {
1383
+ window.clearInterval(meterRef.current.interval);
1384
+ void meterRef.current.ctx.close().catch(() => {
1385
+ });
1386
+ meterRef.current = null;
1387
+ }
1388
+ setLevel(0);
1389
+ }, []);
1390
+ const releaseStream = useCallback2(() => {
1391
+ for (const track of streamRef.current?.getTracks() ?? []) track.stop();
1392
+ streamRef.current = null;
1393
+ }, []);
1394
+ const stopSpeaking = useCallback2(() => {
1395
+ const audio = audioRef.current;
1396
+ if (audio) {
1397
+ audio.pause();
1398
+ audio.src = "";
1399
+ audioRef.current = null;
1400
+ }
1401
+ if (audioUrlRef.current) {
1402
+ URL.revokeObjectURL(audioUrlRef.current);
1403
+ audioUrlRef.current = null;
1404
+ }
1405
+ setState((s) => s === "speaking" ? "idle" : s);
1406
+ }, []);
1407
+ const finish = useCallback2((recorder) => {
1408
+ recorder.stop();
1409
+ }, []);
1410
+ const stopListening = useCallback2(() => {
1411
+ const recorder = recorderRef.current;
1412
+ if (!recorder || recorder.state === "inactive") return;
1413
+ discardRef.current = false;
1414
+ finish(recorder);
1415
+ }, [finish]);
1416
+ const cancelListening = useCallback2(() => {
1417
+ const recorder = recorderRef.current;
1418
+ if (!recorder || recorder.state === "inactive") return;
1419
+ discardRef.current = true;
1420
+ finish(recorder);
1421
+ }, [finish]);
1422
+ const startMeter = useCallback2(
1423
+ (stream, onSilence) => {
1424
+ const Ctx = typeof AudioContext !== "undefined" ? AudioContext : window.webkitAudioContext;
1425
+ if (!Ctx) return;
1426
+ try {
1427
+ const ctx = new Ctx();
1428
+ const analyser = ctx.createAnalyser();
1429
+ analyser.fftSize = 2048;
1430
+ ctx.createMediaStreamSource(stream).connect(analyser);
1431
+ const data = new Uint8Array(analyser.fftSize);
1432
+ let heard = false;
1433
+ let lastVoice = Date.now();
1434
+ const interval = window.setInterval(() => {
1435
+ analyser.getByteTimeDomainData(data);
1436
+ let sum = 0;
1437
+ for (const v of data) {
1438
+ const n = (v - 128) / 128;
1439
+ sum += n * n;
1440
+ }
1441
+ const rms = Math.sqrt(sum / data.length);
1442
+ setLevel(Math.min(1, rms * 6));
1443
+ if (rms > SPEECH_RMS) {
1444
+ heard = true;
1445
+ lastVoice = Date.now();
1446
+ } else if (heard && Date.now() - lastVoice > silenceMs) {
1447
+ onSilence();
1448
+ }
1449
+ }, 100);
1450
+ meterRef.current = { ctx, interval };
1451
+ } catch {
1452
+ }
1453
+ },
1454
+ [silenceMs]
1455
+ );
1456
+ const startListening = useCallback2(async () => {
1457
+ if (!supported) {
1458
+ setState("unsupported");
1459
+ return;
1460
+ }
1461
+ if (startingRef.current) return;
1462
+ if (recorderRef.current && recorderRef.current.state !== "inactive") return;
1463
+ startingRef.current = true;
1464
+ stopSpeaking();
1465
+ setError(null);
1466
+ let stream;
1467
+ try {
1468
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1469
+ } catch {
1470
+ startingRef.current = false;
1471
+ setState("denied");
1472
+ setError(t("voice", "micDenied"));
1473
+ return;
1474
+ }
1475
+ startingRef.current = false;
1476
+ streamRef.current = stream;
1477
+ const mimeType = MIME_CANDIDATES.find((m) => MediaRecorder.isTypeSupported?.(m));
1478
+ const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : void 0);
1479
+ recorderRef.current = recorder;
1480
+ chunksRef.current = [];
1481
+ discardRef.current = false;
1482
+ recorder.ondataavailable = (e) => {
1483
+ if (e.data && e.data.size > 0) chunksRef.current.push(e.data);
1484
+ };
1485
+ recorder.onstop = () => {
1486
+ clearTimers();
1487
+ releaseStream();
1488
+ recorderRef.current = null;
1489
+ const type = recorder.mimeType || mimeType || "audio/webm";
1490
+ const clip = new Blob(chunksRef.current, { type });
1491
+ chunksRef.current = [];
1492
+ if (discardRef.current || clip.size < MIN_CLIP_BYTES) {
1493
+ setState("idle");
1494
+ return;
1495
+ }
1496
+ setState("transcribing");
1497
+ const ext = type.includes("mp4") ? "m4a" : type.includes("ogg") ? "ogg" : "webm";
1498
+ client.transcribe(identifier, clip, `turn.${ext}`, language).then((result) => {
1499
+ setState("idle");
1500
+ if (result.transcript) onTranscriptRef.current(result.transcript);
1501
+ }).catch((err) => {
1502
+ setState("idle");
1503
+ setError(
1504
+ err instanceof AginiesError && err.code === "VOICE_UNAVAILABLE" ? t("voice", "unavailable") : t("voice", "error")
1505
+ );
1506
+ });
1507
+ };
1508
+ recorder.start(250);
1509
+ setState("listening");
1510
+ startMeter(stream, () => stopListening());
1511
+ timersRef.current.push(window.setTimeout(() => stopListening(), maxMs));
1512
+ }, [
1513
+ supported,
1514
+ stopSpeaking,
1515
+ t,
1516
+ client,
1517
+ identifier,
1518
+ language,
1519
+ maxMs,
1520
+ stopListening,
1521
+ clearTimers,
1522
+ releaseStream,
1523
+ startMeter
1524
+ ]);
1525
+ const speak = useCallback2(
1526
+ async (text) => {
1527
+ stopSpeaking();
1528
+ setError(null);
1529
+ setState("speaking");
1530
+ try {
1531
+ const blob = await client.speak(identifier, text);
1532
+ const url = URL.createObjectURL(blob);
1533
+ audioUrlRef.current = url;
1534
+ const audio = new Audio(url);
1535
+ audioRef.current = audio;
1536
+ await new Promise((resolve) => {
1537
+ audio.onended = () => resolve();
1538
+ audio.onerror = () => resolve();
1539
+ audio.onpause = () => resolve();
1540
+ audio.play().catch(() => resolve());
1541
+ });
1542
+ } catch (err) {
1543
+ setError(
1544
+ err instanceof AginiesError && err.code === "VOICE_UNAVAILABLE" ? t("voice", "unavailable") : t("voice", "error")
1545
+ );
1546
+ } finally {
1547
+ if (audioUrlRef.current) URL.revokeObjectURL(audioUrlRef.current);
1548
+ audioUrlRef.current = null;
1549
+ audioRef.current = null;
1550
+ setState((s) => s === "speaking" ? "idle" : s);
1551
+ }
1552
+ },
1553
+ [client, identifier, stopSpeaking, t]
1554
+ );
1555
+ useEffect3(
1556
+ () => () => {
1557
+ discardRef.current = true;
1558
+ if (recorderRef.current && recorderRef.current.state !== "inactive")
1559
+ recorderRef.current.stop();
1560
+ clearTimers();
1561
+ releaseStream();
1562
+ const audio = audioRef.current;
1563
+ if (audio) audio.pause();
1564
+ if (audioUrlRef.current) URL.revokeObjectURL(audioUrlRef.current);
1565
+ },
1566
+ [clearTimers, releaseStream]
1567
+ );
1568
+ return {
1569
+ state,
1570
+ error,
1571
+ supported,
1572
+ level,
1573
+ startListening,
1574
+ stopListening,
1575
+ cancelListening,
1576
+ speak,
1577
+ stopSpeaking
1578
+ };
1579
+ }
1580
+
1277
1581
  // src/chat/chat-widget.tsx
1278
1582
  import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1279
1583
  var ATTACHMENT_LIMITS = { maxFiles: 5, maxBytes: 10 * 1024 * 1024 };
1280
1584
  function useChat(identifier, enabled = true) {
1281
1585
  const { client, t } = useAginies();
1282
- const [config, setConfig] = useState3(null);
1283
- const [authNeed, setAuthNeed] = useState3(null);
1284
- const [authTitle, setAuthTitle] = useState3();
1285
- const [loadError, setLoadError] = useState3(null);
1286
- const [messages, setMessages] = useState3([]);
1287
- const [busy, setBusy] = useState3(false);
1288
- const [conversationId] = useState3(() => randomId());
1289
- const abortRef = useRef2(null);
1290
- const load = useCallback2(async () => {
1586
+ const [config, setConfig] = useState4(null);
1587
+ const [authNeed, setAuthNeed] = useState4(null);
1588
+ const [authTitle, setAuthTitle] = useState4();
1589
+ const [loadError, setLoadError] = useState4(null);
1590
+ const [messages, setMessages] = useState4([]);
1591
+ const [busy, setBusy] = useState4(false);
1592
+ const [conversationId] = useState4(() => randomId());
1593
+ const abortRef = useRef3(null);
1594
+ const load = useCallback3(async () => {
1291
1595
  setLoadError(null);
1292
1596
  try {
1293
1597
  const res = await client.getChat(identifier);
@@ -1304,11 +1608,11 @@ function useChat(identifier, enabled = true) {
1304
1608
  );
1305
1609
  }
1306
1610
  }, [client, identifier, t]);
1307
- useEffect3(() => {
1611
+ useEffect4(() => {
1308
1612
  if (enabled) void load();
1309
1613
  }, [load, enabled]);
1310
1614
  const chatPath = `/api/chat/${encodeURIComponent(identifier)}`;
1311
- const authenticate = useCallback2(
1615
+ const authenticate = useCallback3(
1312
1616
  async ({ password }) => {
1313
1617
  const res = await client.fetchRaw(chatPath, {
1314
1618
  method: "POST",
@@ -1323,7 +1627,7 @@ function useChat(identifier, enabled = true) {
1323
1627
  },
1324
1628
  [client, chatPath, conversationId, load]
1325
1629
  );
1326
- const requestCode = useCallback2(
1630
+ const requestCode = useCallback3(
1327
1631
  async (email) => {
1328
1632
  const res = await client.fetchRaw(`${chatPath}/otp`, {
1329
1633
  method: "POST",
@@ -1335,7 +1639,7 @@ function useChat(identifier, enabled = true) {
1335
1639
  },
1336
1640
  [client, chatPath]
1337
1641
  );
1338
- const verifyCode = useCallback2(
1642
+ const verifyCode = useCallback3(
1339
1643
  async (email, otp) => {
1340
1644
  const res = await client.fetchRaw(`${chatPath}/otp`, {
1341
1645
  method: "PUT",
@@ -1350,7 +1654,7 @@ function useChat(identifier, enabled = true) {
1350
1654
  },
1351
1655
  [client, chatPath, load]
1352
1656
  );
1353
- const stop = useCallback2(() => {
1657
+ const stop = useCallback3(() => {
1354
1658
  abortRef.current?.abort();
1355
1659
  abortRef.current = null;
1356
1660
  setBusy(false);
@@ -1363,7 +1667,7 @@ function useChat(identifier, enabled = true) {
1363
1667
  ];
1364
1668
  });
1365
1669
  }, [t]);
1366
- const send = useCallback2(
1670
+ const send = useCallback3(
1367
1671
  async (text, files = []) => {
1368
1672
  const input = text.trim();
1369
1673
  if (!input && files.length === 0 || busy) return;
@@ -1455,15 +1759,36 @@ function ChatWidget({
1455
1759
  launcherLabel,
1456
1760
  defaultOpen = false,
1457
1761
  theme = "auto",
1762
+ voice = true,
1458
1763
  className
1459
1764
  }) {
1460
- const [open, setOpen] = useState3(defaultOpen || mode !== "bubble");
1765
+ const [open, setOpen] = useState4(defaultOpen || mode !== "bubble");
1461
1766
  const { t } = useAginies();
1462
1767
  const enabled = useModule("chat");
1463
1768
  const chat = useChat(identifier, enabled);
1769
+ const [voiceMode, setVoiceMode] = useState4(false);
1770
+ const [dictation, setDictation] = useState4(null);
1771
+ const voiceModeRef = useRef3(voiceMode);
1772
+ voiceModeRef.current = voiceMode;
1773
+ const sendRef = useRef3(chat.send);
1774
+ sendRef.current = chat.send;
1775
+ const voiceControls = useVoice({
1776
+ identifier,
1777
+ onTranscript: (text) => {
1778
+ if (voiceModeRef.current) void sendRef.current(text);
1779
+ else setDictation({ id: Date.now(), text });
1780
+ }
1781
+ });
1782
+ const capabilities = chat.config?.voice;
1783
+ const voiceAvailable = voice && voiceControls.supported && capabilities?.stt === true;
1464
1784
  const title = chat.config?.title ?? chat.authTitle ?? launcherLabel ?? "Aginies";
1465
1785
  const themeClass = theme === "auto" ? void 0 : theme === "dark" ? "dark" : "light";
1466
1786
  if (!enabled) return null;
1787
+ const leaveVoiceMode = () => {
1788
+ voiceControls.cancelListening();
1789
+ voiceControls.stopSpeaking();
1790
+ setVoiceMode(false);
1791
+ };
1467
1792
  const panel = /* @__PURE__ */ jsxs5(
1468
1793
  "section",
1469
1794
  {
@@ -1501,7 +1826,16 @@ function ChatWidget({
1501
1826
  onRequestCode: chat.requestCode,
1502
1827
  onVerifyCode: chat.verifyCode
1503
1828
  }
1504
- ) : !chat.config ? /* @__PURE__ */ jsx6("div", { className: "agi-chat__state", children: /* @__PURE__ */ jsx6(Spinner, { label: t("common", "loading") }) }) : /* @__PURE__ */ jsxs5(Fragment4, { children: [
1829
+ ) : !chat.config ? /* @__PURE__ */ jsx6("div", { className: "agi-chat__state", children: /* @__PURE__ */ jsx6(Spinner, { label: t("common", "loading") }) }) : voiceMode ? /* @__PURE__ */ jsx6(
1830
+ VoiceConversation,
1831
+ {
1832
+ messages: chat.messages,
1833
+ busy: chat.busy,
1834
+ voice: voiceControls,
1835
+ speakReplies: capabilities?.tts === true,
1836
+ onExit: leaveVoiceMode
1837
+ }
1838
+ ) : /* @__PURE__ */ jsxs5(Fragment4, { children: [
1505
1839
  /* @__PURE__ */ jsx6(
1506
1840
  MessageList,
1507
1841
  {
@@ -1510,7 +1844,22 @@ function ChatWidget({
1510
1844
  onAction: (action) => void chat.send(action)
1511
1845
  }
1512
1846
  ),
1513
- /* @__PURE__ */ jsx6(Composer, { busy: chat.busy, onSend: chat.send, onStop: chat.stop })
1847
+ /* @__PURE__ */ jsx6(
1848
+ Composer,
1849
+ {
1850
+ busy: chat.busy,
1851
+ onSend: chat.send,
1852
+ onStop: chat.stop,
1853
+ dictation,
1854
+ voice: voiceAvailable ? {
1855
+ state: voiceControls.state,
1856
+ error: voiceControls.error,
1857
+ start: voiceControls.startListening,
1858
+ stop: voiceControls.stopListening,
1859
+ onHandsFree: () => setVoiceMode(true)
1860
+ } : void 0
1861
+ }
1862
+ )
1514
1863
  ] }),
1515
1864
  /* @__PURE__ */ jsx6("footer", { className: "agi-chat__foot", children: /* @__PURE__ */ jsx6("a", { href: "https://www.aginies.com", target: "_blank", rel: "noreferrer", children: t("chat", "poweredBy") }) })
1516
1865
  ]
@@ -1565,53 +1914,57 @@ function MessageList({
1565
1914
  onAction
1566
1915
  }) {
1567
1916
  const { t } = useAginies();
1568
- const endRef = useRef2(null);
1569
- useEffect3(() => {
1917
+ const endRef = useRef3(null);
1918
+ useEffect4(() => {
1570
1919
  endRef.current?.scrollIntoView?.({ block: "end" });
1571
1920
  }, []);
1572
- return (
1573
- // biome-ignore lint/a11y/useSemanticElements: no native element conveys a live log
1574
- /* @__PURE__ */ jsxs5("div", { className: "agi-chat__messages", role: "log", "aria-live": "polite", children: [
1575
- /* @__PURE__ */ jsxs5("div", { className: "agi-msg agi-msg--assistant", children: [
1576
- /* @__PURE__ */ jsx6("span", { className: "agi-msg__who", children: t("chat", "assistant") }),
1577
- /* @__PURE__ */ jsx6("div", { className: "agi-msg__body", children: welcome })
1578
- ] }),
1579
- messages.map((m) => /* @__PURE__ */ jsxs5(
1580
- "div",
1581
- {
1582
- className: cx("agi-msg", `agi-msg--${m.role}`, m.error && "agi-msg--error"),
1583
- children: [
1584
- /* @__PURE__ */ jsx6("span", { className: "agi-msg__who", children: m.role === "user" ? t("chat", "you") : t("chat", "assistant") }),
1585
- /* @__PURE__ */ jsxs5("div", { className: "agi-msg__body", children: [
1586
- m.structured ? /* @__PURE__ */ jsx6(StructuredUI, { response: m.structured, onAction }) : m.content ? m.role === "assistant" && !m.error ? /* @__PURE__ */ jsx6(Markdown, { className: "agi-md", text: m.content }) : /* @__PURE__ */ jsx6("p", { children: m.content }) : m.streaming ? /* @__PURE__ */ jsxs5("span", { className: "agi-msg__thinking", children: [
1587
- /* @__PURE__ */ jsx6(Spinner, { label: t("chat", "thinking") }),
1588
- " ",
1589
- t("chat", "thinking")
1590
- ] }) : null,
1591
- m.attachments && m.attachments.length > 0 && /* @__PURE__ */ jsx6("ul", { className: "agi-msg__files", "aria-label": t("chat", "attachments"), children: m.attachments.map((file, n) => /* @__PURE__ */ jsxs5("li", { className: "agi-file", children: [
1592
- /* @__PURE__ */ jsx6(FileGlyph, {}),
1593
- /* @__PURE__ */ jsx6("span", { className: "agi-file__name", children: file.name }),
1594
- /* @__PURE__ */ jsx6("span", { className: "agi-file__size", children: formatBytes(file.size) })
1595
- ] }, n)) })
1596
- ] })
1597
- ]
1598
- },
1599
- m.id
1600
- )),
1601
- /* @__PURE__ */ jsx6("div", { ref: endRef })
1602
- ] })
1603
- );
1921
+ return /* @__PURE__ */ jsxs5("div", { className: "agi-chat__messages", role: "log", "aria-live": "polite", children: [
1922
+ /* @__PURE__ */ jsxs5("div", { className: "agi-msg agi-msg--assistant", children: [
1923
+ /* @__PURE__ */ jsx6("span", { className: "agi-msg__who", children: t("chat", "assistant") }),
1924
+ /* @__PURE__ */ jsx6("div", { className: "agi-msg__body", children: welcome })
1925
+ ] }),
1926
+ messages.map((m) => /* @__PURE__ */ jsxs5(
1927
+ "div",
1928
+ {
1929
+ className: cx("agi-msg", `agi-msg--${m.role}`, m.error && "agi-msg--error"),
1930
+ children: [
1931
+ /* @__PURE__ */ jsx6("span", { className: "agi-msg__who", children: m.role === "user" ? t("chat", "you") : t("chat", "assistant") }),
1932
+ /* @__PURE__ */ jsxs5("div", { className: "agi-msg__body", children: [
1933
+ m.structured ? /* @__PURE__ */ jsx6(StructuredUI, { response: m.structured, onAction }) : m.content ? m.role === "assistant" && !m.error ? /* @__PURE__ */ jsx6(Markdown, { className: "agi-md", text: m.content }) : /* @__PURE__ */ jsx6("p", { children: m.content }) : m.streaming ? /* @__PURE__ */ jsxs5("span", { className: "agi-msg__thinking", children: [
1934
+ /* @__PURE__ */ jsx6(Spinner, { label: t("chat", "thinking") }),
1935
+ " ",
1936
+ t("chat", "thinking")
1937
+ ] }) : null,
1938
+ m.attachments && m.attachments.length > 0 && /* @__PURE__ */ jsx6("ul", { className: "agi-msg__files", "aria-label": t("chat", "attachments"), children: m.attachments.map((file, n) => /* @__PURE__ */ jsxs5("li", { className: "agi-file", children: [
1939
+ /* @__PURE__ */ jsx6(FileGlyph, {}),
1940
+ /* @__PURE__ */ jsx6("span", { className: "agi-file__name", children: file.name }),
1941
+ /* @__PURE__ */ jsx6("span", { className: "agi-file__size", children: formatBytes(file.size) })
1942
+ ] }, n)) })
1943
+ ] })
1944
+ ]
1945
+ },
1946
+ m.id
1947
+ )),
1948
+ /* @__PURE__ */ jsx6("div", { ref: endRef })
1949
+ ] });
1604
1950
  }
1605
1951
  function Composer({
1606
1952
  busy,
1607
1953
  onSend,
1608
- onStop
1954
+ onStop,
1955
+ voice,
1956
+ dictation
1609
1957
  }) {
1610
1958
  const { t } = useAginies();
1611
- const [value, setValue] = useState3("");
1612
- const [files, setFiles] = useState3([]);
1613
- const [fileError, setFileError] = useState3(null);
1614
- const fileInput = useRef2(null);
1959
+ const [value, setValue] = useState4("");
1960
+ const [files, setFiles] = useState4([]);
1961
+ const [fileError, setFileError] = useState4(null);
1962
+ const fileInput = useRef3(null);
1963
+ useEffect4(() => {
1964
+ if (dictation) setValue((v) => v.trim() ? `${v.trimEnd()} ${dictation.text}` : dictation.text);
1965
+ }, [dictation]);
1966
+ const listening = voice?.state === "listening";
1967
+ const transcribing = voice?.state === "transcribing";
1615
1968
  const submit = (e) => {
1616
1969
  e.preventDefault();
1617
1970
  if (busy) return;
@@ -1657,6 +2010,7 @@ function Composer({
1657
2010
  ] }, n)),
1658
2011
  fileError && /* @__PURE__ */ jsx6("span", { className: "agi-chat__file-error", children: fileError })
1659
2012
  ] }),
2013
+ voice?.error && /* @__PURE__ */ jsx6("div", { className: "agi-chat__file-error", children: voice.error }),
1660
2014
  /* @__PURE__ */ jsxs5("div", { className: "agi-chat__row", children: [
1661
2015
  /* @__PURE__ */ jsx6(
1662
2016
  "input",
@@ -1700,10 +2054,156 @@ function Composer({
1700
2054
  autoComplete: "off"
1701
2055
  }
1702
2056
  ),
2057
+ voice && /* @__PURE__ */ jsxs5(Fragment4, { children: [
2058
+ /* @__PURE__ */ jsx6(
2059
+ Button,
2060
+ {
2061
+ variant: "ghost",
2062
+ type: "button",
2063
+ className: cx("agi-chat__mic", listening && "agi-chat__mic--on"),
2064
+ "aria-label": listening ? t("voice", "stopTalking") : t("voice", "dictate"),
2065
+ "aria-pressed": listening,
2066
+ title: listening ? t("voice", "stopTalking") : t("voice", "dictate"),
2067
+ disabled: busy || transcribing,
2068
+ onClick: () => listening ? voice.stop() : void voice.start(),
2069
+ children: transcribing ? /* @__PURE__ */ jsx6(Spinner, { label: t("voice", "transcribing") }) : /* @__PURE__ */ jsx6(MicGlyph, {})
2070
+ }
2071
+ ),
2072
+ /* @__PURE__ */ jsx6(
2073
+ Button,
2074
+ {
2075
+ variant: "ghost",
2076
+ type: "button",
2077
+ "aria-label": t("voice", "handsFree"),
2078
+ title: t("voice", "handsFree"),
2079
+ disabled: busy || listening || transcribing,
2080
+ onClick: voice.onHandsFree,
2081
+ children: /* @__PURE__ */ jsx6(HeadsetGlyph, {})
2082
+ }
2083
+ )
2084
+ ] }),
1703
2085
  busy ? /* @__PURE__ */ jsx6(Button, { variant: "outline", onClick: onStop, children: t("chat", "stop") }) : /* @__PURE__ */ jsx6(Button, { variant: "signal", type: "submit", disabled: !canSend, children: t("chat", "send") })
1704
2086
  ] })
1705
2087
  ] });
1706
2088
  }
2089
+ function VoiceConversation({
2090
+ messages,
2091
+ busy,
2092
+ voice,
2093
+ speakReplies,
2094
+ onExit
2095
+ }) {
2096
+ const { t } = useAginies();
2097
+ const spokenRef = useRef3(null);
2098
+ const mountedRef = useRef3(true);
2099
+ const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
2100
+ const lastUser = [...messages].reverse().find((m) => m.role === "user");
2101
+ useEffect4(() => {
2102
+ mountedRef.current = true;
2103
+ spokenRef.current = lastAssistant?.id ?? null;
2104
+ void voice.startListening();
2105
+ return () => {
2106
+ mountedRef.current = false;
2107
+ };
2108
+ }, []);
2109
+ const replyId = lastAssistant?.id;
2110
+ const replyStreaming = lastAssistant?.streaming === true;
2111
+ useEffect4(() => {
2112
+ if (!lastAssistant || replyStreaming || replyId === spokenRef.current) return;
2113
+ spokenRef.current = replyId ?? null;
2114
+ const run = async () => {
2115
+ if (speakReplies && lastAssistant.content && !lastAssistant.error) {
2116
+ await voice.speak(lastAssistant.content);
2117
+ }
2118
+ if (mountedRef.current) await voice.startListening();
2119
+ };
2120
+ void run();
2121
+ }, [replyId, replyStreaming]);
2122
+ const status = voice.state === "listening" ? t("voice", "listening") : voice.state === "transcribing" ? t("voice", "transcribing") : voice.state === "speaking" ? t("voice", "speaking") : busy ? t("voice", "thinking") : t("voice", "idle");
2123
+ const hint = voice.state === "listening" ? t("voice", "stopTalking") : voice.state === "speaking" ? t("voice", "interrupt") : voice.state === "transcribing" || busy ? "" : t("voice", "talk");
2124
+ const tap = () => {
2125
+ if (voice.state === "listening") voice.stopListening();
2126
+ else if (voice.state === "speaking") {
2127
+ voice.stopSpeaking();
2128
+ void voice.startListening();
2129
+ } else if (voice.state === "idle" && !busy) void voice.startListening();
2130
+ };
2131
+ return /* @__PURE__ */ jsxs5("section", { className: "agi-voice", "aria-label": t("voice", "handsFree"), children: [
2132
+ /* @__PURE__ */ jsx6(
2133
+ "button",
2134
+ {
2135
+ type: "button",
2136
+ className: "agi-voice__exit",
2137
+ onClick: onExit,
2138
+ "aria-label": t("voice", "exit"),
2139
+ children: "\xD7"
2140
+ }
2141
+ ),
2142
+ /* @__PURE__ */ jsx6("div", { className: "agi-voice__status", "aria-live": "polite", children: status }),
2143
+ lastUser?.content && /* @__PURE__ */ jsx6("p", { className: "agi-voice__you", children: lastUser.content }),
2144
+ /* @__PURE__ */ jsx6("div", { className: "agi-voice__reply", children: lastAssistant?.content ? /* @__PURE__ */ jsx6(Markdown, { className: "agi-md", text: lastAssistant.content }) : busy ? /* @__PURE__ */ jsx6(Spinner, { label: t("voice", "thinking") }) : null }),
2145
+ /* @__PURE__ */ jsx6(
2146
+ "button",
2147
+ {
2148
+ type: "button",
2149
+ className: cx(
2150
+ "agi-voice__orb",
2151
+ `agi-voice__orb--${voice.state}`,
2152
+ busy && "agi-voice__orb--busy"
2153
+ ),
2154
+ style: { ["--agi-level"]: voice.level },
2155
+ onClick: tap,
2156
+ "aria-label": hint || status,
2157
+ disabled: voice.state === "transcribing" || voice.state === "unsupported",
2158
+ children: /* @__PURE__ */ jsx6(MicGlyph, { size: 28 })
2159
+ }
2160
+ ),
2161
+ /* @__PURE__ */ jsx6("div", { className: "agi-voice__hint", children: hint }),
2162
+ voice.error && /* @__PURE__ */ jsx6("div", { className: "agi-voice__error", children: voice.error })
2163
+ ] });
2164
+ }
2165
+ function MicGlyph({ size = 16 }) {
2166
+ return /* @__PURE__ */ jsxs5(
2167
+ "svg",
2168
+ {
2169
+ width: size,
2170
+ height: size,
2171
+ viewBox: "0 0 24 24",
2172
+ fill: "none",
2173
+ stroke: "currentColor",
2174
+ strokeWidth: "1.8",
2175
+ strokeLinecap: "round",
2176
+ strokeLinejoin: "round",
2177
+ "aria-hidden": "true",
2178
+ children: [
2179
+ /* @__PURE__ */ jsx6("rect", { x: "9", y: "3", width: "6", height: "11", rx: "3" }),
2180
+ /* @__PURE__ */ jsx6("path", { d: "M5 11a7 7 0 0 0 14 0M12 18v3M9 21h6" })
2181
+ ]
2182
+ }
2183
+ );
2184
+ }
2185
+ function HeadsetGlyph() {
2186
+ return /* @__PURE__ */ jsxs5(
2187
+ "svg",
2188
+ {
2189
+ width: "16",
2190
+ height: "16",
2191
+ viewBox: "0 0 24 24",
2192
+ fill: "none",
2193
+ stroke: "currentColor",
2194
+ strokeWidth: "1.8",
2195
+ strokeLinecap: "round",
2196
+ strokeLinejoin: "round",
2197
+ "aria-hidden": "true",
2198
+ children: [
2199
+ /* @__PURE__ */ jsx6("path", { d: "M4 14v-2a8 8 0 0 1 16 0v2" }),
2200
+ /* @__PURE__ */ jsx6("rect", { x: "3", y: "13", width: "4", height: "6", rx: "1.5" }),
2201
+ /* @__PURE__ */ jsx6("rect", { x: "17", y: "13", width: "4", height: "6", rx: "1.5" }),
2202
+ /* @__PURE__ */ jsx6("path", { d: "M19 19v1a2 2 0 0 1-2 2h-3" })
2203
+ ]
2204
+ }
2205
+ );
2206
+ }
1707
2207
  function readFile(file) {
1708
2208
  return new Promise((resolve, reject) => {
1709
2209
  const reader = new FileReader();
@@ -1767,9 +2267,9 @@ function PasswordAuth({
1767
2267
  onPassword
1768
2268
  }) {
1769
2269
  const { t } = useAginies();
1770
- const [value, setValue] = useState3("");
1771
- const [error, setError] = useState3(null);
1772
- const [pending, setPending] = useState3(false);
2270
+ const [value, setValue] = useState4("");
2271
+ const [error, setError] = useState4(null);
2272
+ const [pending, setPending] = useState4(false);
1773
2273
  const submit = async (e) => {
1774
2274
  e.preventDefault();
1775
2275
  setPending(true);
@@ -1799,12 +2299,12 @@ function EmailAuth({
1799
2299
  onVerifyCode
1800
2300
  }) {
1801
2301
  const { t } = useAginies();
1802
- const [email, setEmail] = useState3("");
1803
- const [code, setCode] = useState3("");
1804
- const [step, setStep] = useState3("email");
1805
- const [error, setError] = useState3(null);
1806
- const [notice, setNotice] = useState3(null);
1807
- const [pending, setPending] = useState3(false);
2302
+ const [email, setEmail] = useState4("");
2303
+ const [code, setCode] = useState4("");
2304
+ const [step, setStep] = useState4("email");
2305
+ const [error, setError] = useState4(null);
2306
+ const [notice, setNotice] = useState4(null);
2307
+ const [pending, setPending] = useState4(false);
1808
2308
  const request = async () => {
1809
2309
  setPending(true);
1810
2310
  setError(null);
@@ -2015,7 +2515,7 @@ function CostBars({ items, format = usd, className, ...props }) {
2015
2515
  }
2016
2516
 
2017
2517
  // src/run/agent-runner.tsx
2018
- import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
2518
+ import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef4, useState as useState5 } from "react";
2019
2519
 
2020
2520
  // src/run/run-client.ts
2021
2521
  async function* runAgent(client, workflowId, options, signal) {
@@ -2155,19 +2655,19 @@ function decodeRunFrame(frame) {
2155
2655
  import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2156
2656
  function useAgentRun(workflowId, apiKey) {
2157
2657
  const { client, t } = useAginies();
2158
- const [status, setStatus] = useState4("idle");
2159
- const [text, setText] = useState4("");
2160
- const [steps, setSteps] = useState4([]);
2161
- const [output, setOutput] = useState4(null);
2162
- const [error, setError] = useState4(null);
2163
- const [executionId, setExecutionId] = useState4();
2164
- const abortRef = useRef3(null);
2165
- const cancel = useCallback3(() => {
2658
+ const [status, setStatus] = useState5("idle");
2659
+ const [text, setText] = useState5("");
2660
+ const [steps, setSteps] = useState5([]);
2661
+ const [output, setOutput] = useState5(null);
2662
+ const [error, setError] = useState5(null);
2663
+ const [executionId, setExecutionId] = useState5();
2664
+ const abortRef = useRef4(null);
2665
+ const cancel = useCallback4(() => {
2166
2666
  abortRef.current?.abort();
2167
2667
  abortRef.current = null;
2168
2668
  setStatus((s) => s === "running" ? "idle" : s);
2169
2669
  }, []);
2170
- const run = useCallback3(
2670
+ const run = useCallback4(
2171
2671
  async (input) => {
2172
2672
  abortRef.current?.abort();
2173
2673
  const controller = new AbortController();
@@ -2244,7 +2744,7 @@ function AgentRunner({
2244
2744
  const { t } = useAginies();
2245
2745
  const enabled = useModule("run");
2246
2746
  const agent = useAgentRun(workflowId, apiKey);
2247
- const [values, setValues] = useState4(
2747
+ const [values, setValues] = useState5(
2248
2748
  () => Object.fromEntries(
2249
2749
  fields.map((f) => [f.name, f.defaultValue ?? (f.type === "boolean" ? false : "")])
2250
2750
  )
@@ -2256,9 +2756,9 @@ function AgentRunner({
2256
2756
  const input = fields.length === 1 && fields[0]?.name === "input" && fields[0]?.type === "textarea" ? values.input : values;
2257
2757
  await agent.run(input);
2258
2758
  };
2259
- const onResultRef = useRef3(onResult);
2759
+ const onResultRef = useRef4(onResult);
2260
2760
  onResultRef.current = onResult;
2261
- useEffect4(() => {
2761
+ useEffect5(() => {
2262
2762
  if (agent.status === "done") onResultRef.current?.(agent.output);
2263
2763
  }, [agent.status, agent.output]);
2264
2764
  if (!enabled) return null;
@@ -2378,6 +2878,7 @@ export {
2378
2878
  getPausedExecution,
2379
2879
  init,
2380
2880
  initialValues,
2881
+ isVoiceSupported,
2381
2882
  listPausedExecutions,
2382
2883
  outputOf,
2383
2884
  parseFieldValue,
@@ -2391,6 +2892,7 @@ export {
2391
2892
  useAginies,
2392
2893
  useApproval,
2393
2894
  useChat,
2394
- useModule
2895
+ useModule,
2896
+ useVoice
2395
2897
  };
2396
2898
  //# sourceMappingURL=index.js.map