@aginies/webuikit 0.2.0 → 0.4.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 CHANGED
@@ -53,6 +53,7 @@ __export(src_exports, {
53
53
  getPausedExecution: () => getPausedExecution,
54
54
  init: () => init,
55
55
  initialValues: () => initialValues,
56
+ isVoiceSupported: () => isVoiceSupported,
56
57
  listPausedExecutions: () => listPausedExecutions,
57
58
  outputOf: () => outputOf,
58
59
  parseFieldValue: () => parseFieldValue,
@@ -66,7 +67,8 @@ __export(src_exports, {
66
67
  useAginies: () => useAginies,
67
68
  useApproval: () => useApproval,
68
69
  useChat: () => useChat,
69
- useModule: () => useModule
70
+ useModule: () => useModule,
71
+ useVoice: () => useVoice
70
72
  });
71
73
  module.exports = __toCommonJS(src_exports);
72
74
 
@@ -85,6 +87,8 @@ var trimSlash = (s) => s.replace(/\/+$/, "");
85
87
  var AginiesClient = class {
86
88
  baseUrl;
87
89
  token;
90
+ /** True when activated from the platform's own page. */
91
+ hosted;
88
92
  locale;
89
93
  fetchImpl;
90
94
  state = { status: "idle" };
@@ -93,9 +97,12 @@ var AginiesClient = class {
93
97
  session = null;
94
98
  constructor(config) {
95
99
  if (!config?.baseUrl) throw new AginiesError("baseUrl is required", 0, "MISSING_BASE_URL");
96
- if (!config?.token) throw new AginiesError("token is required", 0, "MISSING_TOKEN");
100
+ if (!config?.token && !config?.hosted) {
101
+ throw new AginiesError("token is required", 0, "MISSING_TOKEN");
102
+ }
97
103
  this.baseUrl = trimSlash(config.baseUrl);
98
- this.token = config.token;
104
+ this.token = config.token ?? null;
105
+ this.hosted = config.hosted === true;
99
106
  this.locale = config.locale ?? detectLocale();
100
107
  this.fetchImpl = config.fetch ?? ((...args) => fetch(...args));
101
108
  }
@@ -122,7 +129,7 @@ var AginiesClient = class {
122
129
  const res = await this.fetchImpl(`${this.baseUrl}/api/ui/activate`, {
123
130
  method: "POST",
124
131
  headers: { "Content-Type": "application/json" },
125
- body: JSON.stringify({ token: this.token })
132
+ body: JSON.stringify(this.hosted ? { hosted: true } : { token: this.token })
126
133
  });
127
134
  if (!res.ok) {
128
135
  const body2 = await safeJson(res);
@@ -197,6 +204,30 @@ var AginiesClient = class {
197
204
  this.assertActive();
198
205
  return this.request(path2, init2);
199
206
  }
207
+ /** Sends a recording to the platform's transcription model. */
208
+ async transcribe(identifier, audio, filename = "turn.webm", language) {
209
+ this.assertActive();
210
+ const form = new FormData();
211
+ form.append("file", audio, filename);
212
+ if (language) form.append("language", language);
213
+ const res = await this.request(`/api/chat/${encodeURIComponent(identifier)}/voice/transcribe`, {
214
+ method: "POST",
215
+ body: form
216
+ });
217
+ if (!res.ok) throw await voiceError(res);
218
+ return await res.json();
219
+ }
220
+ /** Audio for a reply from the platform's synthesis endpoint. */
221
+ async speak(identifier, text) {
222
+ this.assertActive();
223
+ const res = await this.request(`/api/chat/${encodeURIComponent(identifier)}/voice/speak`, {
224
+ method: "POST",
225
+ headers: { "Content-Type": "application/json" },
226
+ body: JSON.stringify({ text })
227
+ });
228
+ if (!res.ok) throw await voiceError(res);
229
+ return res.blob();
230
+ }
200
231
  /** Hosted-chat configuration. Resolves to an auth requirement instead of throwing on 401. */
201
232
  async getChat(identifier) {
202
233
  this.assertActive();
@@ -286,6 +317,15 @@ function decodeFrame(frame) {
286
317
  }
287
318
  return null;
288
319
  }
320
+ async function voiceError(res) {
321
+ const body = await safeJson(res);
322
+ if (res.status === 401 && body && typeof body.authRequired === "string") {
323
+ return new AginiesError("Authentication required", 401, "AUTH_REQUIRED");
324
+ }
325
+ if (res.status === 503)
326
+ return new AginiesError("Voice is not available", 503, "VOICE_UNAVAILABLE");
327
+ return new AginiesError(body?.error || `HTTP ${res.status}`, res.status);
328
+ }
289
329
  async function safeJson(res) {
290
330
  try {
291
331
  return await res.json();
@@ -582,7 +622,37 @@ var STRINGS = {
582
622
  tr: "Bu sohbet kurumsal kimlikle a\xE7\u0131l\u0131r.",
583
623
  en: "This chat opens with your organisation account."
584
624
  },
585
- ssoButton: { tr: "Kurumsal giri\u015F", en: "Sign in" }
625
+ ssoButton: { tr: "Kurumsal giri\u015F", en: "Sign in" },
626
+ ssoRetry: { tr: "Giri\u015F yapt\u0131m, yenile", en: "I have signed in, refresh" }
627
+ },
628
+ voice: {
629
+ talk: { tr: "Konu\u015Fmak i\xE7in dokunun", en: "Tap to talk" },
630
+ stopTalking: { tr: "Bitirmek i\xE7in dokunun", en: "Tap when done" },
631
+ dictate: { tr: "Sesle yaz", en: "Dictate" },
632
+ handsFree: { tr: "Sesli sohbet", en: "Voice conversation" },
633
+ exit: { tr: "Sesli sohbetten \xE7\u0131k", en: "Leave voice conversation" },
634
+ listening: { tr: "Dinliyor", en: "Listening" },
635
+ transcribing: { tr: "Yaz\u0131ya d\xF6k\xFCl\xFCyor", en: "Transcribing" },
636
+ thinking: { tr: "Yan\u0131t haz\u0131rlan\u0131yor", en: "Working on it" },
637
+ speaking: { tr: "Konu\u015Fuyor", en: "Speaking" },
638
+ idle: { tr: "Haz\u0131r", en: "Ready" },
639
+ interrupt: { tr: "S\xF6z\xFCn\xFC kesmek i\xE7in dokunun", en: "Tap to interrupt" },
640
+ micDenied: {
641
+ tr: "Mikrofon izni verilmedi. Taray\u0131c\u0131 ayarlar\u0131ndan izin verin.",
642
+ en: "Microphone access was denied. Allow it in your browser settings."
643
+ },
644
+ unsupported: {
645
+ tr: "Bu taray\u0131c\u0131 ses kayd\u0131n\u0131 desteklemiyor.",
646
+ en: "This browser cannot record audio."
647
+ },
648
+ unavailable: {
649
+ tr: "Ses \xF6zelli\u011Fi bu sohbet i\xE7in a\xE7\u0131k de\u011Fil.",
650
+ en: "Voice is not enabled for this chat."
651
+ },
652
+ error: {
653
+ tr: "Ses i\u015Flenemedi. L\xFCtfen tekrar deneyin.",
654
+ en: "Voice could not be processed. Please try again."
655
+ }
586
656
  },
587
657
  run: {
588
658
  submit: { tr: "\xC7al\u0131\u015Ft\u0131r", en: "Run" },
@@ -1034,7 +1104,7 @@ function renderField(f, value, set, disabled) {
1034
1104
  }
1035
1105
 
1036
1106
  // src/chat/chat-widget.tsx
1037
- var import_react5 = require("react");
1107
+ var import_react6 = require("react");
1038
1108
 
1039
1109
  // src/chat/markdown.tsx
1040
1110
  var import_react4 = require("react");
@@ -1344,20 +1414,258 @@ function Pie({ labels, data }) {
1344
1414
  ] });
1345
1415
  }
1346
1416
 
1417
+ // src/chat/voice.ts
1418
+ var import_react5 = require("react");
1419
+ var MIME_CANDIDATES = [
1420
+ "audio/webm;codecs=opus",
1421
+ "audio/webm",
1422
+ "audio/mp4",
1423
+ "audio/ogg;codecs=opus"
1424
+ ];
1425
+ var MIN_CLIP_BYTES = 800;
1426
+ var SPEECH_RMS = 0.02;
1427
+ function isVoiceSupported() {
1428
+ return typeof window !== "undefined" && typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof MediaRecorder !== "undefined";
1429
+ }
1430
+ function useVoice({
1431
+ identifier,
1432
+ onTranscript,
1433
+ language,
1434
+ silenceMs = 1400,
1435
+ maxMs = 6e4
1436
+ }) {
1437
+ const { client, t } = useAginies();
1438
+ const supported = isVoiceSupported();
1439
+ const [state, setState] = (0, import_react5.useState)(supported ? "idle" : "unsupported");
1440
+ const [error, setError] = (0, import_react5.useState)(null);
1441
+ const [level, setLevel] = (0, import_react5.useState)(0);
1442
+ const recorderRef = (0, import_react5.useRef)(null);
1443
+ const streamRef = (0, import_react5.useRef)(null);
1444
+ const chunksRef = (0, import_react5.useRef)([]);
1445
+ const discardRef = (0, import_react5.useRef)(false);
1446
+ const timersRef = (0, import_react5.useRef)([]);
1447
+ const meterRef = (0, import_react5.useRef)(null);
1448
+ const audioRef = (0, import_react5.useRef)(null);
1449
+ const audioUrlRef = (0, import_react5.useRef)(null);
1450
+ const startingRef = (0, import_react5.useRef)(false);
1451
+ const onTranscriptRef = (0, import_react5.useRef)(onTranscript);
1452
+ onTranscriptRef.current = onTranscript;
1453
+ const clearTimers = (0, import_react5.useCallback)(() => {
1454
+ for (const id of timersRef.current) window.clearTimeout(id);
1455
+ timersRef.current = [];
1456
+ if (meterRef.current) {
1457
+ window.clearInterval(meterRef.current.interval);
1458
+ void meterRef.current.ctx.close().catch(() => {
1459
+ });
1460
+ meterRef.current = null;
1461
+ }
1462
+ setLevel(0);
1463
+ }, []);
1464
+ const releaseStream = (0, import_react5.useCallback)(() => {
1465
+ for (const track of streamRef.current?.getTracks() ?? []) track.stop();
1466
+ streamRef.current = null;
1467
+ }, []);
1468
+ const stopSpeaking = (0, import_react5.useCallback)(() => {
1469
+ const audio = audioRef.current;
1470
+ if (audio) {
1471
+ audio.pause();
1472
+ audio.src = "";
1473
+ audioRef.current = null;
1474
+ }
1475
+ if (audioUrlRef.current) {
1476
+ URL.revokeObjectURL(audioUrlRef.current);
1477
+ audioUrlRef.current = null;
1478
+ }
1479
+ setState((s) => s === "speaking" ? "idle" : s);
1480
+ }, []);
1481
+ const finish = (0, import_react5.useCallback)((recorder) => {
1482
+ recorder.stop();
1483
+ }, []);
1484
+ const stopListening = (0, import_react5.useCallback)(() => {
1485
+ const recorder = recorderRef.current;
1486
+ if (!recorder || recorder.state === "inactive") return;
1487
+ discardRef.current = false;
1488
+ finish(recorder);
1489
+ }, [finish]);
1490
+ const cancelListening = (0, import_react5.useCallback)(() => {
1491
+ const recorder = recorderRef.current;
1492
+ if (!recorder || recorder.state === "inactive") return;
1493
+ discardRef.current = true;
1494
+ finish(recorder);
1495
+ }, [finish]);
1496
+ const startMeter = (0, import_react5.useCallback)(
1497
+ (stream, onSilence) => {
1498
+ const Ctx = typeof AudioContext !== "undefined" ? AudioContext : window.webkitAudioContext;
1499
+ if (!Ctx) return;
1500
+ try {
1501
+ const ctx = new Ctx();
1502
+ const analyser = ctx.createAnalyser();
1503
+ analyser.fftSize = 2048;
1504
+ ctx.createMediaStreamSource(stream).connect(analyser);
1505
+ const data = new Uint8Array(analyser.fftSize);
1506
+ let heard = false;
1507
+ let lastVoice = Date.now();
1508
+ const interval = window.setInterval(() => {
1509
+ analyser.getByteTimeDomainData(data);
1510
+ let sum = 0;
1511
+ for (const v of data) {
1512
+ const n = (v - 128) / 128;
1513
+ sum += n * n;
1514
+ }
1515
+ const rms = Math.sqrt(sum / data.length);
1516
+ setLevel(Math.min(1, rms * 6));
1517
+ if (rms > SPEECH_RMS) {
1518
+ heard = true;
1519
+ lastVoice = Date.now();
1520
+ } else if (heard && Date.now() - lastVoice > silenceMs) {
1521
+ onSilence();
1522
+ }
1523
+ }, 100);
1524
+ meterRef.current = { ctx, interval };
1525
+ } catch {
1526
+ }
1527
+ },
1528
+ [silenceMs]
1529
+ );
1530
+ const startListening = (0, import_react5.useCallback)(async () => {
1531
+ if (!supported) {
1532
+ setState("unsupported");
1533
+ return;
1534
+ }
1535
+ if (startingRef.current) return;
1536
+ if (recorderRef.current && recorderRef.current.state !== "inactive") return;
1537
+ startingRef.current = true;
1538
+ stopSpeaking();
1539
+ setError(null);
1540
+ let stream;
1541
+ try {
1542
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1543
+ } catch {
1544
+ startingRef.current = false;
1545
+ setState("denied");
1546
+ setError(t("voice", "micDenied"));
1547
+ return;
1548
+ }
1549
+ startingRef.current = false;
1550
+ streamRef.current = stream;
1551
+ const mimeType = MIME_CANDIDATES.find((m) => MediaRecorder.isTypeSupported?.(m));
1552
+ const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : void 0);
1553
+ recorderRef.current = recorder;
1554
+ chunksRef.current = [];
1555
+ discardRef.current = false;
1556
+ recorder.ondataavailable = (e) => {
1557
+ if (e.data && e.data.size > 0) chunksRef.current.push(e.data);
1558
+ };
1559
+ recorder.onstop = () => {
1560
+ clearTimers();
1561
+ releaseStream();
1562
+ recorderRef.current = null;
1563
+ const type = recorder.mimeType || mimeType || "audio/webm";
1564
+ const clip = new Blob(chunksRef.current, { type });
1565
+ chunksRef.current = [];
1566
+ if (discardRef.current || clip.size < MIN_CLIP_BYTES) {
1567
+ setState("idle");
1568
+ return;
1569
+ }
1570
+ setState("transcribing");
1571
+ const ext = type.includes("mp4") ? "m4a" : type.includes("ogg") ? "ogg" : "webm";
1572
+ client.transcribe(identifier, clip, `turn.${ext}`, language).then((result) => {
1573
+ setState("idle");
1574
+ if (result.transcript) onTranscriptRef.current(result.transcript);
1575
+ }).catch((err) => {
1576
+ setState("idle");
1577
+ setError(
1578
+ err instanceof AginiesError && err.code === "VOICE_UNAVAILABLE" ? t("voice", "unavailable") : t("voice", "error")
1579
+ );
1580
+ });
1581
+ };
1582
+ recorder.start(250);
1583
+ setState("listening");
1584
+ startMeter(stream, () => stopListening());
1585
+ timersRef.current.push(window.setTimeout(() => stopListening(), maxMs));
1586
+ }, [
1587
+ supported,
1588
+ stopSpeaking,
1589
+ t,
1590
+ client,
1591
+ identifier,
1592
+ language,
1593
+ maxMs,
1594
+ stopListening,
1595
+ clearTimers,
1596
+ releaseStream,
1597
+ startMeter
1598
+ ]);
1599
+ const speak = (0, import_react5.useCallback)(
1600
+ async (text) => {
1601
+ stopSpeaking();
1602
+ setError(null);
1603
+ setState("speaking");
1604
+ try {
1605
+ const blob = await client.speak(identifier, text);
1606
+ const url = URL.createObjectURL(blob);
1607
+ audioUrlRef.current = url;
1608
+ const audio = new Audio(url);
1609
+ audioRef.current = audio;
1610
+ await new Promise((resolve) => {
1611
+ audio.onended = () => resolve();
1612
+ audio.onerror = () => resolve();
1613
+ audio.onpause = () => resolve();
1614
+ audio.play().catch(() => resolve());
1615
+ });
1616
+ } catch (err) {
1617
+ setError(
1618
+ err instanceof AginiesError && err.code === "VOICE_UNAVAILABLE" ? t("voice", "unavailable") : t("voice", "error")
1619
+ );
1620
+ } finally {
1621
+ if (audioUrlRef.current) URL.revokeObjectURL(audioUrlRef.current);
1622
+ audioUrlRef.current = null;
1623
+ audioRef.current = null;
1624
+ setState((s) => s === "speaking" ? "idle" : s);
1625
+ }
1626
+ },
1627
+ [client, identifier, stopSpeaking, t]
1628
+ );
1629
+ (0, import_react5.useEffect)(
1630
+ () => () => {
1631
+ discardRef.current = true;
1632
+ if (recorderRef.current && recorderRef.current.state !== "inactive")
1633
+ recorderRef.current.stop();
1634
+ clearTimers();
1635
+ releaseStream();
1636
+ const audio = audioRef.current;
1637
+ if (audio) audio.pause();
1638
+ if (audioUrlRef.current) URL.revokeObjectURL(audioUrlRef.current);
1639
+ },
1640
+ [clearTimers, releaseStream]
1641
+ );
1642
+ return {
1643
+ state,
1644
+ error,
1645
+ supported,
1646
+ level,
1647
+ startListening,
1648
+ stopListening,
1649
+ cancelListening,
1650
+ speak,
1651
+ stopSpeaking
1652
+ };
1653
+ }
1654
+
1347
1655
  // src/chat/chat-widget.tsx
1348
1656
  var import_jsx_runtime6 = require("react/jsx-runtime");
1349
1657
  var ATTACHMENT_LIMITS = { maxFiles: 5, maxBytes: 10 * 1024 * 1024 };
1350
1658
  function useChat(identifier, enabled = true) {
1351
1659
  const { client, t } = useAginies();
1352
- const [config, setConfig] = (0, import_react5.useState)(null);
1353
- const [authNeed, setAuthNeed] = (0, import_react5.useState)(null);
1354
- const [authTitle, setAuthTitle] = (0, import_react5.useState)();
1355
- const [loadError, setLoadError] = (0, import_react5.useState)(null);
1356
- const [messages, setMessages] = (0, import_react5.useState)([]);
1357
- const [busy, setBusy] = (0, import_react5.useState)(false);
1358
- const [conversationId] = (0, import_react5.useState)(() => randomId());
1359
- const abortRef = (0, import_react5.useRef)(null);
1360
- const load = (0, import_react5.useCallback)(async () => {
1660
+ const [config, setConfig] = (0, import_react6.useState)(null);
1661
+ const [authNeed, setAuthNeed] = (0, import_react6.useState)(null);
1662
+ const [authTitle, setAuthTitle] = (0, import_react6.useState)();
1663
+ const [loadError, setLoadError] = (0, import_react6.useState)(null);
1664
+ const [messages, setMessages] = (0, import_react6.useState)([]);
1665
+ const [busy, setBusy] = (0, import_react6.useState)(false);
1666
+ const [conversationId] = (0, import_react6.useState)(() => randomId());
1667
+ const abortRef = (0, import_react6.useRef)(null);
1668
+ const load = (0, import_react6.useCallback)(async () => {
1361
1669
  setLoadError(null);
1362
1670
  try {
1363
1671
  const res = await client.getChat(identifier);
@@ -1374,11 +1682,11 @@ function useChat(identifier, enabled = true) {
1374
1682
  );
1375
1683
  }
1376
1684
  }, [client, identifier, t]);
1377
- (0, import_react5.useEffect)(() => {
1685
+ (0, import_react6.useEffect)(() => {
1378
1686
  if (enabled) void load();
1379
1687
  }, [load, enabled]);
1380
1688
  const chatPath = `/api/chat/${encodeURIComponent(identifier)}`;
1381
- const authenticate = (0, import_react5.useCallback)(
1689
+ const authenticate = (0, import_react6.useCallback)(
1382
1690
  async ({ password }) => {
1383
1691
  const res = await client.fetchRaw(chatPath, {
1384
1692
  method: "POST",
@@ -1393,7 +1701,27 @@ function useChat(identifier, enabled = true) {
1393
1701
  },
1394
1702
  [client, chatPath, conversationId, load]
1395
1703
  );
1396
- const requestCode = (0, import_react5.useCallback)(
1704
+ const signInWithSso = (0, import_react6.useCallback)(
1705
+ async (email) => {
1706
+ const res = await client.fetchRaw(chatPath, {
1707
+ method: "POST",
1708
+ headers: { "Content-Type": "application/json" },
1709
+ body: JSON.stringify({ email, checkSSOAccess: true })
1710
+ });
1711
+ if (!res.ok) return res.status === 401 || res.status === 403 ? "unauthorized" : "error";
1712
+ const hostedPage = `${client.baseUrl}/chat/${encodeURIComponent(identifier)}`;
1713
+ if (client.hosted) {
1714
+ window.location.assign(
1715
+ `${client.baseUrl}/sso?email=${encodeURIComponent(email)}&callbackUrl=${encodeURIComponent(window.location.pathname)}`
1716
+ );
1717
+ } else {
1718
+ window.open(hostedPage, "_blank", "noopener");
1719
+ }
1720
+ return "redirected";
1721
+ },
1722
+ [client, chatPath, identifier]
1723
+ );
1724
+ const requestCode = (0, import_react6.useCallback)(
1397
1725
  async (email) => {
1398
1726
  const res = await client.fetchRaw(`${chatPath}/otp`, {
1399
1727
  method: "POST",
@@ -1405,7 +1733,7 @@ function useChat(identifier, enabled = true) {
1405
1733
  },
1406
1734
  [client, chatPath]
1407
1735
  );
1408
- const verifyCode = (0, import_react5.useCallback)(
1736
+ const verifyCode = (0, import_react6.useCallback)(
1409
1737
  async (email, otp) => {
1410
1738
  const res = await client.fetchRaw(`${chatPath}/otp`, {
1411
1739
  method: "PUT",
@@ -1420,7 +1748,7 @@ function useChat(identifier, enabled = true) {
1420
1748
  },
1421
1749
  [client, chatPath, load]
1422
1750
  );
1423
- const stop = (0, import_react5.useCallback)(() => {
1751
+ const stop = (0, import_react6.useCallback)(() => {
1424
1752
  abortRef.current?.abort();
1425
1753
  abortRef.current = null;
1426
1754
  setBusy(false);
@@ -1433,7 +1761,7 @@ function useChat(identifier, enabled = true) {
1433
1761
  ];
1434
1762
  });
1435
1763
  }, [t]);
1436
- const send = (0, import_react5.useCallback)(
1764
+ const send = (0, import_react6.useCallback)(
1437
1765
  async (text, files = []) => {
1438
1766
  const input = text.trim();
1439
1767
  if (!input && files.length === 0 || busy) return;
@@ -1468,6 +1796,14 @@ function useChat(identifier, enabled = true) {
1468
1796
  if (text2 && !content) {
1469
1797
  content = text2;
1470
1798
  update({ content });
1799
+ } else if (!content) {
1800
+ const raw = extractFinalOutput(ev.data);
1801
+ if (raw) {
1802
+ content = `\`\`\`json
1803
+ ${raw}
1804
+ \`\`\``;
1805
+ update({ content });
1806
+ }
1471
1807
  }
1472
1808
  } else if (ev.type === "error") {
1473
1809
  update({ content: ev.message, error: true, streaming: false });
@@ -1498,6 +1834,7 @@ function useChat(identifier, enabled = true) {
1498
1834
  authenticate,
1499
1835
  requestCode,
1500
1836
  verifyCode,
1837
+ signInWithSso,
1501
1838
  reload: load
1502
1839
  };
1503
1840
  }
@@ -1514,6 +1851,14 @@ function extractFinalText(data) {
1514
1851
  }
1515
1852
  return null;
1516
1853
  }
1854
+ function extractFinalOutput(data) {
1855
+ if (!data || typeof data !== "object") return null;
1856
+ const d = data;
1857
+ const payload = d.output ?? d.error;
1858
+ if (payload === void 0 || payload === null) return null;
1859
+ const text = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
1860
+ return text.trim() ? text : null;
1861
+ }
1517
1862
  function randomId() {
1518
1863
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
1519
1864
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
@@ -1525,15 +1870,38 @@ function ChatWidget({
1525
1870
  launcherLabel,
1526
1871
  defaultOpen = false,
1527
1872
  theme = "auto",
1873
+ voice = true,
1528
1874
  className
1529
1875
  }) {
1530
- const [open, setOpen] = (0, import_react5.useState)(defaultOpen || mode !== "bubble");
1531
- const { t } = useAginies();
1876
+ const [open, setOpen] = (0, import_react6.useState)(defaultOpen || mode !== "bubble");
1877
+ const { t, client } = useAginies();
1532
1878
  const enabled = useModule("chat");
1533
1879
  const chat = useChat(identifier, enabled);
1880
+ const [voiceMode, setVoiceMode] = (0, import_react6.useState)(false);
1881
+ const [dictation, setDictation] = (0, import_react6.useState)(null);
1882
+ const voiceModeRef = (0, import_react6.useRef)(voiceMode);
1883
+ voiceModeRef.current = voiceMode;
1884
+ const sendRef = (0, import_react6.useRef)(chat.send);
1885
+ sendRef.current = chat.send;
1886
+ const voiceControls = useVoice({
1887
+ identifier,
1888
+ onTranscript: (text) => {
1889
+ if (voiceModeRef.current) void sendRef.current(text);
1890
+ else setDictation({ id: Date.now(), text });
1891
+ }
1892
+ });
1893
+ const capabilities = chat.config?.voice;
1894
+ const voiceAvailable = voice && voiceControls.supported && capabilities?.stt === true;
1895
+ const state = client.getState();
1896
+ const brand = state.status === "active" ? state.config.brand : { name: "Aginies", logoUrl: null };
1534
1897
  const title = chat.config?.title ?? chat.authTitle ?? launcherLabel ?? "Aginies";
1535
1898
  const themeClass = theme === "auto" ? void 0 : theme === "dark" ? "dark" : "light";
1536
1899
  if (!enabled) return null;
1900
+ const leaveVoiceMode = () => {
1901
+ voiceControls.cancelListening();
1902
+ voiceControls.stopSpeaking();
1903
+ setVoiceMode(false);
1904
+ };
1537
1905
  const panel = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1538
1906
  "section",
1539
1907
  {
@@ -1569,9 +1937,20 @@ function ChatWidget({
1569
1937
  need: chat.authNeed,
1570
1938
  onPassword: chat.authenticate,
1571
1939
  onRequestCode: chat.requestCode,
1572
- onVerifyCode: chat.verifyCode
1940
+ onVerifyCode: chat.verifyCode,
1941
+ onSso: chat.signInWithSso,
1942
+ onRetry: () => void chat.reload()
1943
+ }
1944
+ ) : !chat.config ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "agi-chat__state", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Spinner, { label: t("common", "loading") }) }) : voiceMode ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1945
+ VoiceConversation,
1946
+ {
1947
+ messages: chat.messages,
1948
+ busy: chat.busy,
1949
+ voice: voiceControls,
1950
+ speakReplies: capabilities?.tts === true,
1951
+ onExit: leaveVoiceMode
1573
1952
  }
1574
- ) : !chat.config ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "agi-chat__state", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Spinner, { label: t("common", "loading") }) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1953
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1575
1954
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1576
1955
  MessageList,
1577
1956
  {
@@ -1580,9 +1959,24 @@ function ChatWidget({
1580
1959
  onAction: (action) => void chat.send(action)
1581
1960
  }
1582
1961
  ),
1583
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Composer, { busy: chat.busy, onSend: chat.send, onStop: chat.stop })
1962
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1963
+ Composer,
1964
+ {
1965
+ busy: chat.busy,
1966
+ onSend: chat.send,
1967
+ onStop: chat.stop,
1968
+ dictation,
1969
+ voice: voiceAvailable ? {
1970
+ state: voiceControls.state,
1971
+ error: voiceControls.error,
1972
+ start: voiceControls.startListening,
1973
+ stop: voiceControls.stopListening,
1974
+ onHandsFree: () => setVoiceMode(true)
1975
+ } : void 0
1976
+ }
1977
+ )
1584
1978
  ] }),
1585
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("footer", { className: "agi-chat__foot", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("a", { href: "https://www.aginies.com", target: "_blank", rel: "noreferrer", children: t("chat", "poweredBy") }) })
1979
+ brand.poweredBy !== false && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("footer", { className: "agi-chat__foot", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("a", { href: brand.siteUrl ?? "https://www.aginies.com", target: "_blank", rel: "noreferrer", children: t("chat", "poweredBy") }) })
1586
1980
  ]
1587
1981
  }
1588
1982
  );
@@ -1635,53 +2029,57 @@ function MessageList({
1635
2029
  onAction
1636
2030
  }) {
1637
2031
  const { t } = useAginies();
1638
- const endRef = (0, import_react5.useRef)(null);
1639
- (0, import_react5.useEffect)(() => {
2032
+ const endRef = (0, import_react6.useRef)(null);
2033
+ (0, import_react6.useEffect)(() => {
1640
2034
  endRef.current?.scrollIntoView?.({ block: "end" });
1641
2035
  }, []);
1642
- return (
1643
- // biome-ignore lint/a11y/useSemanticElements: no native element conveys a live log
1644
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "agi-chat__messages", role: "log", "aria-live": "polite", children: [
1645
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "agi-msg agi-msg--assistant", children: [
1646
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "agi-msg__who", children: t("chat", "assistant") }),
1647
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "agi-msg__body", children: welcome })
1648
- ] }),
1649
- messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1650
- "div",
1651
- {
1652
- className: cx("agi-msg", `agi-msg--${m.role}`, m.error && "agi-msg--error"),
1653
- children: [
1654
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "agi-msg__who", children: m.role === "user" ? t("chat", "you") : t("chat", "assistant") }),
1655
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "agi-msg__body", children: [
1656
- m.structured ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StructuredUI, { response: m.structured, onAction }) : m.content ? m.role === "assistant" && !m.error ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Markdown, { className: "agi-md", text: m.content }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: m.content }) : m.streaming ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "agi-msg__thinking", children: [
1657
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Spinner, { label: t("chat", "thinking") }),
1658
- " ",
1659
- t("chat", "thinking")
1660
- ] }) : null,
1661
- m.attachments && m.attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "agi-msg__files", "aria-label": t("chat", "attachments"), children: m.attachments.map((file, n) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { className: "agi-file", children: [
1662
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FileGlyph, {}),
1663
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "agi-file__name", children: file.name }),
1664
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "agi-file__size", children: formatBytes(file.size) })
1665
- ] }, n)) })
1666
- ] })
1667
- ]
1668
- },
1669
- m.id
1670
- )),
1671
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { ref: endRef })
1672
- ] })
1673
- );
2036
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "agi-chat__messages", role: "log", "aria-live": "polite", children: [
2037
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "agi-msg agi-msg--assistant", children: [
2038
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "agi-msg__who", children: t("chat", "assistant") }),
2039
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "agi-msg__body", children: welcome })
2040
+ ] }),
2041
+ messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2042
+ "div",
2043
+ {
2044
+ className: cx("agi-msg", `agi-msg--${m.role}`, m.error && "agi-msg--error"),
2045
+ children: [
2046
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "agi-msg__who", children: m.role === "user" ? t("chat", "you") : t("chat", "assistant") }),
2047
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "agi-msg__body", children: [
2048
+ m.structured ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StructuredUI, { response: m.structured, onAction }) : m.content ? m.role === "assistant" && !m.error ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Markdown, { className: "agi-md", text: m.content }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: m.content }) : m.streaming ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "agi-msg__thinking", children: [
2049
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Spinner, { label: t("chat", "thinking") }),
2050
+ " ",
2051
+ t("chat", "thinking")
2052
+ ] }) : null,
2053
+ m.attachments && m.attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "agi-msg__files", "aria-label": t("chat", "attachments"), children: m.attachments.map((file, n) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { className: "agi-file", children: [
2054
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FileGlyph, {}),
2055
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "agi-file__name", children: file.name }),
2056
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "agi-file__size", children: formatBytes(file.size) })
2057
+ ] }, n)) })
2058
+ ] })
2059
+ ]
2060
+ },
2061
+ m.id
2062
+ )),
2063
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { ref: endRef })
2064
+ ] });
1674
2065
  }
1675
2066
  function Composer({
1676
2067
  busy,
1677
2068
  onSend,
1678
- onStop
2069
+ onStop,
2070
+ voice,
2071
+ dictation
1679
2072
  }) {
1680
2073
  const { t } = useAginies();
1681
- const [value, setValue] = (0, import_react5.useState)("");
1682
- const [files, setFiles] = (0, import_react5.useState)([]);
1683
- const [fileError, setFileError] = (0, import_react5.useState)(null);
1684
- const fileInput = (0, import_react5.useRef)(null);
2074
+ const [value, setValue] = (0, import_react6.useState)("");
2075
+ const [files, setFiles] = (0, import_react6.useState)([]);
2076
+ const [fileError, setFileError] = (0, import_react6.useState)(null);
2077
+ const fileInput = (0, import_react6.useRef)(null);
2078
+ (0, import_react6.useEffect)(() => {
2079
+ if (dictation) setValue((v) => v.trim() ? `${v.trimEnd()} ${dictation.text}` : dictation.text);
2080
+ }, [dictation]);
2081
+ const listening = voice?.state === "listening";
2082
+ const transcribing = voice?.state === "transcribing";
1685
2083
  const submit = (e) => {
1686
2084
  e.preventDefault();
1687
2085
  if (busy) return;
@@ -1727,6 +2125,7 @@ function Composer({
1727
2125
  ] }, n)),
1728
2126
  fileError && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "agi-chat__file-error", children: fileError })
1729
2127
  ] }),
2128
+ voice?.error && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "agi-chat__file-error", children: voice.error }),
1730
2129
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "agi-chat__row", children: [
1731
2130
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1732
2131
  "input",
@@ -1770,10 +2169,156 @@ function Composer({
1770
2169
  autoComplete: "off"
1771
2170
  }
1772
2171
  ),
2172
+ voice && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
2173
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2174
+ Button,
2175
+ {
2176
+ variant: "ghost",
2177
+ type: "button",
2178
+ className: cx("agi-chat__mic", listening && "agi-chat__mic--on"),
2179
+ "aria-label": listening ? t("voice", "stopTalking") : t("voice", "dictate"),
2180
+ "aria-pressed": listening,
2181
+ title: listening ? t("voice", "stopTalking") : t("voice", "dictate"),
2182
+ disabled: busy || transcribing,
2183
+ onClick: () => listening ? voice.stop() : void voice.start(),
2184
+ children: transcribing ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Spinner, { label: t("voice", "transcribing") }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicGlyph, {})
2185
+ }
2186
+ ),
2187
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2188
+ Button,
2189
+ {
2190
+ variant: "ghost",
2191
+ type: "button",
2192
+ "aria-label": t("voice", "handsFree"),
2193
+ title: t("voice", "handsFree"),
2194
+ disabled: busy || listening || transcribing,
2195
+ onClick: voice.onHandsFree,
2196
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HeadsetGlyph, {})
2197
+ }
2198
+ )
2199
+ ] }),
1773
2200
  busy ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Button, { variant: "outline", onClick: onStop, children: t("chat", "stop") }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Button, { variant: "signal", type: "submit", disabled: !canSend, children: t("chat", "send") })
1774
2201
  ] })
1775
2202
  ] });
1776
2203
  }
2204
+ function VoiceConversation({
2205
+ messages,
2206
+ busy,
2207
+ voice,
2208
+ speakReplies,
2209
+ onExit
2210
+ }) {
2211
+ const { t } = useAginies();
2212
+ const spokenRef = (0, import_react6.useRef)(null);
2213
+ const mountedRef = (0, import_react6.useRef)(true);
2214
+ const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
2215
+ const lastUser = [...messages].reverse().find((m) => m.role === "user");
2216
+ (0, import_react6.useEffect)(() => {
2217
+ mountedRef.current = true;
2218
+ spokenRef.current = lastAssistant?.id ?? null;
2219
+ void voice.startListening();
2220
+ return () => {
2221
+ mountedRef.current = false;
2222
+ };
2223
+ }, []);
2224
+ const replyId = lastAssistant?.id;
2225
+ const replyStreaming = lastAssistant?.streaming === true;
2226
+ (0, import_react6.useEffect)(() => {
2227
+ if (!lastAssistant || replyStreaming || replyId === spokenRef.current) return;
2228
+ spokenRef.current = replyId ?? null;
2229
+ const run = async () => {
2230
+ if (speakReplies && lastAssistant.content && !lastAssistant.error) {
2231
+ await voice.speak(lastAssistant.content);
2232
+ }
2233
+ if (mountedRef.current) await voice.startListening();
2234
+ };
2235
+ void run();
2236
+ }, [replyId, replyStreaming]);
2237
+ 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");
2238
+ const hint = voice.state === "listening" ? t("voice", "stopTalking") : voice.state === "speaking" ? t("voice", "interrupt") : voice.state === "transcribing" || busy ? "" : t("voice", "talk");
2239
+ const tap = () => {
2240
+ if (voice.state === "listening") voice.stopListening();
2241
+ else if (voice.state === "speaking") {
2242
+ voice.stopSpeaking();
2243
+ void voice.startListening();
2244
+ } else if (voice.state === "idle" && !busy) void voice.startListening();
2245
+ };
2246
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("section", { className: "agi-voice", "aria-label": t("voice", "handsFree"), children: [
2247
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2248
+ "button",
2249
+ {
2250
+ type: "button",
2251
+ className: "agi-voice__exit",
2252
+ onClick: onExit,
2253
+ "aria-label": t("voice", "exit"),
2254
+ children: "\xD7"
2255
+ }
2256
+ ),
2257
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "agi-voice__status", "aria-live": "polite", children: status }),
2258
+ lastUser?.content && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "agi-voice__you", children: lastUser.content }),
2259
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "agi-voice__reply", children: lastAssistant?.content ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Markdown, { className: "agi-md", text: lastAssistant.content }) : busy ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Spinner, { label: t("voice", "thinking") }) : null }),
2260
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2261
+ "button",
2262
+ {
2263
+ type: "button",
2264
+ className: cx(
2265
+ "agi-voice__orb",
2266
+ `agi-voice__orb--${voice.state}`,
2267
+ busy && "agi-voice__orb--busy"
2268
+ ),
2269
+ style: { ["--agi-level"]: voice.level },
2270
+ onClick: tap,
2271
+ "aria-label": hint || status,
2272
+ disabled: voice.state === "transcribing" || voice.state === "unsupported",
2273
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicGlyph, { size: 28 })
2274
+ }
2275
+ ),
2276
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "agi-voice__hint", children: hint }),
2277
+ voice.error && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "agi-voice__error", children: voice.error })
2278
+ ] });
2279
+ }
2280
+ function MicGlyph({ size = 16 }) {
2281
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2282
+ "svg",
2283
+ {
2284
+ width: size,
2285
+ height: size,
2286
+ viewBox: "0 0 24 24",
2287
+ fill: "none",
2288
+ stroke: "currentColor",
2289
+ strokeWidth: "1.8",
2290
+ strokeLinecap: "round",
2291
+ strokeLinejoin: "round",
2292
+ "aria-hidden": "true",
2293
+ children: [
2294
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("rect", { x: "9", y: "3", width: "6", height: "11", rx: "3" }),
2295
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M5 11a7 7 0 0 0 14 0M12 18v3M9 21h6" })
2296
+ ]
2297
+ }
2298
+ );
2299
+ }
2300
+ function HeadsetGlyph() {
2301
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2302
+ "svg",
2303
+ {
2304
+ width: "16",
2305
+ height: "16",
2306
+ viewBox: "0 0 24 24",
2307
+ fill: "none",
2308
+ stroke: "currentColor",
2309
+ strokeWidth: "1.8",
2310
+ strokeLinecap: "round",
2311
+ strokeLinejoin: "round",
2312
+ "aria-hidden": "true",
2313
+ children: [
2314
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M4 14v-2a8 8 0 0 1 16 0v2" }),
2315
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("rect", { x: "3", y: "13", width: "4", height: "6", rx: "1.5" }),
2316
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("rect", { x: "17", y: "13", width: "4", height: "6", rx: "1.5" }),
2317
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M19 19v1a2 2 0 0 1-2 2h-3" })
2318
+ ]
2319
+ }
2320
+ );
2321
+ }
1777
2322
  function readFile(file) {
1778
2323
  return new Promise((resolve, reject) => {
1779
2324
  const reader = new FileReader();
@@ -1811,35 +2356,62 @@ function ChatAuth({
1811
2356
  need,
1812
2357
  onPassword,
1813
2358
  onRequestCode,
1814
- onVerifyCode
2359
+ onVerifyCode,
2360
+ onSso,
2361
+ onRetry
1815
2362
  }) {
1816
- const { t, client } = useAginies();
1817
2363
  if (need === "sso") {
1818
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "agi-chat__state", children: [
1819
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { children: t("auth", "ssoTitle") }),
1820
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: t("auth", "ssoHint") }),
1821
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1822
- Button,
1823
- {
1824
- variant: "signal",
1825
- onClick: () => window.open(`${client.baseUrl}/chat/`, "_blank", "noopener"),
1826
- children: t("auth", "ssoButton")
1827
- }
1828
- )
1829
- ] });
2364
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SsoAuth, { onSso, onRetry });
1830
2365
  }
1831
2366
  if (need === "email") {
1832
2367
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(EmailAuth, { onRequestCode, onVerifyCode });
1833
2368
  }
1834
2369
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PasswordAuth, { onPassword });
1835
2370
  }
2371
+ function SsoAuth({
2372
+ onSso,
2373
+ onRetry
2374
+ }) {
2375
+ const { t } = useAginies();
2376
+ const [email, setEmail] = (0, import_react6.useState)("");
2377
+ const [error, setError] = (0, import_react6.useState)(null);
2378
+ const [pending, setPending] = (0, import_react6.useState)(false);
2379
+ const [redirected, setRedirected] = (0, import_react6.useState)(false);
2380
+ const submit = async (e) => {
2381
+ e.preventDefault();
2382
+ setPending(true);
2383
+ setError(null);
2384
+ const result = await onSso(email.trim());
2385
+ setPending(false);
2386
+ if (result === "redirected") setRedirected(true);
2387
+ else setError(t("auth", result === "unauthorized" ? "invalidEmail" : "codeError"));
2388
+ };
2389
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("form", { className: "agi-chat__state agi-chat__auth", onSubmit: submit, children: [
2390
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { children: t("auth", "ssoTitle") }),
2391
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: t("auth", "ssoHint") }),
2392
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Field, { label: t("auth", "email"), error: error ?? void 0, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2393
+ Input,
2394
+ {
2395
+ type: "email",
2396
+ value: email,
2397
+ onChange: (e) => setEmail(e.target.value),
2398
+ required: true,
2399
+ autoComplete: "email"
2400
+ }
2401
+ ) }),
2402
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "agi-chat__auth-actions", children: [
2403
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Button, { variant: "signal", type: "submit", disabled: pending || !email.trim(), children: t("auth", "ssoButton") }),
2404
+ redirected && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Button, { variant: "ghost", type: "button", onClick: onRetry, children: t("auth", "ssoRetry") })
2405
+ ] })
2406
+ ] });
2407
+ }
1836
2408
  function PasswordAuth({
1837
2409
  onPassword
1838
2410
  }) {
1839
2411
  const { t } = useAginies();
1840
- const [value, setValue] = (0, import_react5.useState)("");
1841
- const [error, setError] = (0, import_react5.useState)(null);
1842
- const [pending, setPending] = (0, import_react5.useState)(false);
2412
+ const [value, setValue] = (0, import_react6.useState)("");
2413
+ const [error, setError] = (0, import_react6.useState)(null);
2414
+ const [pending, setPending] = (0, import_react6.useState)(false);
1843
2415
  const submit = async (e) => {
1844
2416
  e.preventDefault();
1845
2417
  setPending(true);
@@ -1869,12 +2441,12 @@ function EmailAuth({
1869
2441
  onVerifyCode
1870
2442
  }) {
1871
2443
  const { t } = useAginies();
1872
- const [email, setEmail] = (0, import_react5.useState)("");
1873
- const [code, setCode] = (0, import_react5.useState)("");
1874
- const [step, setStep] = (0, import_react5.useState)("email");
1875
- const [error, setError] = (0, import_react5.useState)(null);
1876
- const [notice, setNotice] = (0, import_react5.useState)(null);
1877
- const [pending, setPending] = (0, import_react5.useState)(false);
2444
+ const [email, setEmail] = (0, import_react6.useState)("");
2445
+ const [code, setCode] = (0, import_react6.useState)("");
2446
+ const [step, setStep] = (0, import_react6.useState)("email");
2447
+ const [error, setError] = (0, import_react6.useState)(null);
2448
+ const [notice, setNotice] = (0, import_react6.useState)(null);
2449
+ const [pending, setPending] = (0, import_react6.useState)(false);
1878
2450
  const request = async () => {
1879
2451
  setPending(true);
1880
2452
  setError(null);
@@ -2085,7 +2657,7 @@ function CostBars({ items, format = usd, className, ...props }) {
2085
2657
  }
2086
2658
 
2087
2659
  // src/run/agent-runner.tsx
2088
- var import_react6 = require("react");
2660
+ var import_react7 = require("react");
2089
2661
 
2090
2662
  // src/run/run-client.ts
2091
2663
  async function* runAgent(client, workflowId, options, signal) {
@@ -2225,19 +2797,19 @@ function decodeRunFrame(frame) {
2225
2797
  var import_jsx_runtime8 = require("react/jsx-runtime");
2226
2798
  function useAgentRun(workflowId, apiKey) {
2227
2799
  const { client, t } = useAginies();
2228
- const [status, setStatus] = (0, import_react6.useState)("idle");
2229
- const [text, setText] = (0, import_react6.useState)("");
2230
- const [steps, setSteps] = (0, import_react6.useState)([]);
2231
- const [output, setOutput] = (0, import_react6.useState)(null);
2232
- const [error, setError] = (0, import_react6.useState)(null);
2233
- const [executionId, setExecutionId] = (0, import_react6.useState)();
2234
- const abortRef = (0, import_react6.useRef)(null);
2235
- const cancel = (0, import_react6.useCallback)(() => {
2800
+ const [status, setStatus] = (0, import_react7.useState)("idle");
2801
+ const [text, setText] = (0, import_react7.useState)("");
2802
+ const [steps, setSteps] = (0, import_react7.useState)([]);
2803
+ const [output, setOutput] = (0, import_react7.useState)(null);
2804
+ const [error, setError] = (0, import_react7.useState)(null);
2805
+ const [executionId, setExecutionId] = (0, import_react7.useState)();
2806
+ const abortRef = (0, import_react7.useRef)(null);
2807
+ const cancel = (0, import_react7.useCallback)(() => {
2236
2808
  abortRef.current?.abort();
2237
2809
  abortRef.current = null;
2238
2810
  setStatus((s) => s === "running" ? "idle" : s);
2239
2811
  }, []);
2240
- const run = (0, import_react6.useCallback)(
2812
+ const run = (0, import_react7.useCallback)(
2241
2813
  async (input) => {
2242
2814
  abortRef.current?.abort();
2243
2815
  const controller = new AbortController();
@@ -2314,7 +2886,7 @@ function AgentRunner({
2314
2886
  const { t } = useAginies();
2315
2887
  const enabled = useModule("run");
2316
2888
  const agent = useAgentRun(workflowId, apiKey);
2317
- const [values, setValues] = (0, import_react6.useState)(
2889
+ const [values, setValues] = (0, import_react7.useState)(
2318
2890
  () => Object.fromEntries(
2319
2891
  fields.map((f) => [f.name, f.defaultValue ?? (f.type === "boolean" ? false : "")])
2320
2892
  )
@@ -2326,9 +2898,9 @@ function AgentRunner({
2326
2898
  const input = fields.length === 1 && fields[0]?.name === "input" && fields[0]?.type === "textarea" ? values.input : values;
2327
2899
  await agent.run(input);
2328
2900
  };
2329
- const onResultRef = (0, import_react6.useRef)(onResult);
2901
+ const onResultRef = (0, import_react7.useRef)(onResult);
2330
2902
  onResultRef.current = onResult;
2331
- (0, import_react6.useEffect)(() => {
2903
+ (0, import_react7.useEffect)(() => {
2332
2904
  if (agent.status === "done") onResultRef.current?.(agent.output);
2333
2905
  }, [agent.status, agent.output]);
2334
2906
  if (!enabled) return null;
@@ -2449,6 +3021,7 @@ function formatOutput(output) {
2449
3021
  getPausedExecution,
2450
3022
  init,
2451
3023
  initialValues,
3024
+ isVoiceSupported,
2452
3025
  listPausedExecutions,
2453
3026
  outputOf,
2454
3027
  parseFieldValue,
@@ -2462,6 +3035,7 @@ function formatOutput(output) {
2462
3035
  useAginies,
2463
3036
  useApproval,
2464
3037
  useChat,
2465
- useModule
3038
+ useModule,
3039
+ useVoice
2466
3040
  });
2467
3041
  //# sourceMappingURL=index.cjs.map