@personaai/react 0.7.0 → 0.7.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/README.md CHANGED
@@ -49,12 +49,51 @@ function Chat() {
49
49
  | Hook | Purpose |
50
50
  | --------------- | -------------------------------------------------------------------------- |
51
51
  | `useChat` | Streaming chat — messages, send, stop, reload, interrupts, workspace files |
52
+ | `useVoice` | Real-time voice calls (Gemini Live) — start/stop/mute, live transcript, tool calls |
52
53
  | `useThreads` | Thread CRUD — list, create, delete, rename, reset, archive |
53
54
  | `useFiles` | Upload management — list, upload, delete |
54
55
  | `useMemory` | Persistent memory — read, write, delete |
55
56
  | `useAgents` | Agent discovery — list available agents |
56
57
  | `useConnection` | Health check — backend connectivity status |
57
58
 
59
+ ## Voice
60
+
61
+ Real-time voice calls with an Agent, powered by Gemini Live. Requires your backend to be running
62
+ `@personaai/runtime@^0.7.0` (or a framework adapter on top of it) so `POST /voice/sessions` exists
63
+ to mint tickets — see that package's README for why the actual call bypasses your backend and
64
+ goes straight to Persona instead of relaying through it like `useChat` does.
65
+
66
+ ```tsx
67
+ import { useVoice } from "@personaai/react";
68
+
69
+ function VoiceCall() {
70
+ const { state, transcript, partial, toolCalls, start, stop, mute, isMuted } = useVoice();
71
+
72
+ return (
73
+ <div>
74
+ <p>State: {state}</p>
75
+ {state === "idle" ? (
76
+ <button onClick={start}>Start call</button>
77
+ ) : (
78
+ <>
79
+ <button onClick={() => mute(!isMuted)}>{isMuted ? "Unmute" : "Mute"}</button>
80
+ <button onClick={stop}>End call</button>
81
+ </>
82
+ )}
83
+ {transcript.map((line) => (
84
+ <p key={line.id}>
85
+ <b>{line.speaker}:</b> {line.text}
86
+ </p>
87
+ ))}
88
+ {partial && <p style={{ opacity: 0.6 }}>{partial.text}</p>}
89
+ </div>
90
+ );
91
+ }
92
+ ```
93
+
94
+ Requires a browser with `AudioWorklet` support (every current evergreen browser). Needs an HTTPS
95
+ page in production — browsers refuse an insecure `ws://` connection from an `https:` page.
96
+
58
97
  ## Devtools
59
98
 
60
99
  Floating panel for local debugging — inspect hooks, messages and threads with zero runtime changes. Dev-only, not bundled to production unless imported.
package/dist/index.cjs CHANGED
@@ -1347,10 +1347,19 @@ function playerWorkletUrl() {
1347
1347
  }
1348
1348
 
1349
1349
  // src/hooks/useVoice.ts
1350
+ function mergeTranscriptText(prev, next) {
1351
+ if (!prev) return next;
1352
+ if (!next) return prev;
1353
+ if (next === prev || prev.endsWith(next)) return prev;
1354
+ if (next.startsWith(prev)) return next;
1355
+ const needSpace = !/\s$/.test(prev) && !/^\s/.test(next);
1356
+ return prev + (needSpace ? " " : "") + next;
1357
+ }
1350
1358
  function useVoice(options = {}) {
1351
1359
  const { defaultAgentId, fetchWithAuth, logger } = usePersonaContext();
1352
1360
  const voiceLogger = (0, import_react5.useMemo)(() => logger.child("voice"), [logger]);
1353
1361
  const agentId = options.agentId || defaultAgentId;
1362
+ const threadId = options.threadId;
1354
1363
  const [state, setState] = (0, import_react5.useState)("idle");
1355
1364
  const [isMuted, setIsMuted] = (0, import_react5.useState)(false);
1356
1365
  const [transcript, setTranscript] = (0, import_react5.useState)(
@@ -1372,6 +1381,9 @@ function useVoice(options = {}) {
1372
1381
  const playerNodeRef = (0, import_react5.useRef)(null);
1373
1382
  const acceptedTurnSeqRef = (0, import_react5.useRef)(0);
1374
1383
  const mountedRef = (0, import_react5.useRef)(true);
1384
+ const lastFinalTurnRef = (0, import_react5.useRef)(
1385
+ null
1386
+ );
1375
1387
  (0, import_react5.useEffect)(() => {
1376
1388
  mountedRef.current = true;
1377
1389
  return () => {
@@ -1454,8 +1466,7 @@ function useVoice(options = {}) {
1454
1466
  (id, patch) => {
1455
1467
  setToolCalls((prev) => {
1456
1468
  const idx = prev.findIndex((t) => t.id === id);
1457
- if (idx === -1)
1458
- return [...prev, { id, status: "running", ...patch }];
1469
+ if (idx === -1) return [...prev, { id, status: "running", ...patch }];
1459
1470
  const next = [...prev];
1460
1471
  next[idx] = { ...next[idx], ...patch };
1461
1472
  return next;
@@ -1463,21 +1474,28 @@ function useVoice(options = {}) {
1463
1474
  },
1464
1475
  []
1465
1476
  );
1466
- const handleTranscript = (0, import_react5.useCallback)(
1467
- (value) => {
1468
- const { speaker, text, isFinal } = value;
1469
- if (isFinal) {
1470
- setPartial((prev) => prev?.speaker === speaker ? null : prev);
1471
- setTranscript((prev) => [
1472
- ...prev,
1473
- { id: `${speaker}-${prev.length}`, speaker, text }
1474
- ]);
1475
- } else {
1476
- setPartial({ id: "partial", speaker, text });
1477
+ const handleTranscript = (0, import_react5.useCallback)((value) => {
1478
+ const { speaker, text, isFinal, turnSeq } = value;
1479
+ if (!isFinal) {
1480
+ setPartial({ id: "partial", speaker, text });
1481
+ return;
1482
+ }
1483
+ setPartial((prev) => prev?.speaker === speaker ? null : prev);
1484
+ const last = lastFinalTurnRef.current;
1485
+ const sameUtterance = last && last.speaker === speaker && last.turnSeq === turnSeq;
1486
+ setTranscript((prev) => {
1487
+ const tail = prev[prev.length - 1];
1488
+ if (sameUtterance && tail?.speaker === speaker) {
1489
+ const merged = mergeTranscriptText(tail.text, text);
1490
+ if (merged === tail.text) return prev;
1491
+ return prev.map(
1492
+ (l, i) => i === prev.length - 1 ? { ...l, text: merged } : l
1493
+ );
1477
1494
  }
1478
- },
1479
- []
1480
- );
1495
+ return [...prev, { id: `${speaker}-${prev.length}`, speaker, text }];
1496
+ });
1497
+ lastFinalTurnRef.current = { speaker, turnSeq };
1498
+ }, []);
1481
1499
  const handleCustomEvent = (0, import_react5.useCallback)(
1482
1500
  (name, value) => {
1483
1501
  switch (name) {
@@ -1510,9 +1528,7 @@ function useVoice(options = {}) {
1510
1528
  }
1511
1529
  break;
1512
1530
  case "voice_transcript":
1513
- handleTranscript(
1514
- value
1515
- );
1531
+ handleTranscript(value);
1516
1532
  break;
1517
1533
  case "voice_interrupted":
1518
1534
  acceptedTurnSeqRef.current = value.turnSeq;
@@ -1585,7 +1601,9 @@ function useVoice(options = {}) {
1585
1601
  }
1586
1602
  case "RUN_ERROR":
1587
1603
  voiceLogger.warn("RUN_ERROR", { message: msg.message });
1588
- setError(new Error(msg.message || "The agent run failed."));
1604
+ setError(
1605
+ new Error(msg.message || "The agent run failed.")
1606
+ );
1589
1607
  setState("error");
1590
1608
  break;
1591
1609
  default:
@@ -1610,12 +1628,15 @@ function useVoice(options = {}) {
1610
1628
  setToolCalls([]);
1611
1629
  acceptedTurnSeqRef.current = 0;
1612
1630
  setState("connecting");
1613
- voiceLogger.debug("start() called", { agentId });
1631
+ voiceLogger.debug("start() called", { agentId, threadId });
1614
1632
  try {
1615
1633
  const res = await fetchWithAuth("/voice/sessions", {
1616
1634
  method: "POST",
1617
1635
  headers: { "Content-Type": "application/json" },
1618
- body: JSON.stringify({ agentId })
1636
+ body: JSON.stringify({
1637
+ agentId,
1638
+ ...threadId ? { threadId } : {}
1639
+ })
1619
1640
  });
1620
1641
  if (!res.ok) {
1621
1642
  throw new Error(`Failed to start voice session: ${res.statusText}`);
@@ -1650,7 +1671,7 @@ function useVoice(options = {}) {
1650
1671
  }
1651
1672
  teardownAudio();
1652
1673
  }
1653
- }, [agentId, fetchWithAuth, handleMessage, teardownAudio, voiceLogger]);
1674
+ }, [agentId, threadId, fetchWithAuth, handleMessage, teardownAudio, voiceLogger]);
1654
1675
  const mute = (0, import_react5.useCallback)((muted) => {
1655
1676
  setIsMuted(muted);
1656
1677
  streamRef.current?.getAudioTracks().forEach((track) => {
@@ -1904,7 +1925,7 @@ function useMcpConnections(options = {}) {
1904
1925
 
1905
1926
  // src/index.ts
1906
1927
  var import_logger2 = require("@personaai/logger");
1907
- var VERSION = "0.7.0";
1928
+ var VERSION = "0.7.2";
1908
1929
  // Annotate the CommonJS export names for ESM import in node:
1909
1930
  0 && (module.exports = {
1910
1931
  PersonaProvider,