@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 +39 -0
- package/dist/index.cjs +45 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +45 -24
- package/dist/index.js.map +1 -1
- package/package.json +82 -82
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
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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(
|
|
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({
|
|
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.
|
|
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,
|