@personaai/react 0.7.1 → 0.7.3
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 +45 -0
- package/dist/index.cjs +35 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +35 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,51 @@ function VoiceCall() {
|
|
|
94
94
|
Requires a browser with `AudioWorklet` support (every current evergreen browser). Needs an HTTPS
|
|
95
95
|
page in production — browsers refuse an insecure `ws://` connection from an `https:` page.
|
|
96
96
|
|
|
97
|
+
### How `transcript` is shaped
|
|
98
|
+
|
|
99
|
+
`transcript` is **live-only**: it starts empty on every `start()` and grows purely from the
|
|
100
|
+
voice session's events. The server never replays the thread's prior history into it — pass
|
|
101
|
+
`threadId` and the session *persists* new turns to that thread (so `useChat`/`useThreads` can read
|
|
102
|
+
them back on reload), but `transcript` itself always reflects just what was spoken in this call.
|
|
103
|
+
|
|
104
|
+
Gemini Live streams an agent answer as several incremental transcription fragments. The hook merges
|
|
105
|
+
them so one utterance is **one transcript line**, mirroring how the same turns read back from the
|
|
106
|
+
thread (consecutive assistant messages are coalesced on reload). The merge applies across distinct
|
|
107
|
+
spoken segments of one answer too (e.g. split by a mid-answer tool call) — a new agent line is only
|
|
108
|
+
started once a committed user line has intervened.
|
|
109
|
+
|
|
110
|
+
### Showing voice turns in a text `useChat` feed
|
|
111
|
+
|
|
112
|
+
If you drive a `useVoice({ agentId, threadId })` session alongside `useChat({ threadId })` and
|
|
113
|
+
inject live turns into the chat feed yourself, dedupe before injecting. Because the session
|
|
114
|
+
persists to the same thread, `useChat.messages` may already carry a turn once it refreshes or on
|
|
115
|
+
reload — re-injecting the live copy appends it a second time:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
const { transcript, ... } = useVoice({ agentId, threadId });
|
|
119
|
+
const { messages, appendMessage, ... } = useChat({ threadId });
|
|
120
|
+
|
|
121
|
+
// Turn a committed voice line into a chat message once, and only if the feed
|
|
122
|
+
// does not already hold this content (from an earlier live inject OR a reload
|
|
123
|
+
// that pulled the persisted thread).
|
|
124
|
+
const pushVoiceLine = (line: { id: string; speaker: "user" | "agent"; text: string }) => {
|
|
125
|
+
if (messages.some((m) => m.id === line.id)) return; // injected before
|
|
126
|
+
if (messages.some((m) =>
|
|
127
|
+
!String(m.id).startsWith("voice-") &&
|
|
128
|
+
m.role === line.speaker &&
|
|
129
|
+
String(m.content).trim() === line.text.trim(),
|
|
130
|
+
)) return; // already on the thread
|
|
131
|
+
appendMessage({
|
|
132
|
+
id: `voice-${line.id}`,
|
|
133
|
+
role: line.speaker,
|
|
134
|
+
content: line.text,
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Voice turns are intentionally **live-only** in `useVoice` — history is the thread's job, not the
|
|
140
|
+
hook's, so when you share a thread with `useChat` you own the dedup at the injection boundary.
|
|
141
|
+
|
|
97
142
|
## Devtools
|
|
98
143
|
|
|
99
144
|
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,6 +1347,14 @@ 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]);
|
|
@@ -1373,6 +1381,9 @@ function useVoice(options = {}) {
|
|
|
1373
1381
|
const playerNodeRef = (0, import_react5.useRef)(null);
|
|
1374
1382
|
const acceptedTurnSeqRef = (0, import_react5.useRef)(0);
|
|
1375
1383
|
const mountedRef = (0, import_react5.useRef)(true);
|
|
1384
|
+
const lastFinalTurnRef = (0, import_react5.useRef)(
|
|
1385
|
+
null
|
|
1386
|
+
);
|
|
1376
1387
|
(0, import_react5.useEffect)(() => {
|
|
1377
1388
|
mountedRef.current = true;
|
|
1378
1389
|
return () => {
|
|
@@ -1463,21 +1474,29 @@ 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 sameUserBurst = speaker === "user" && last && last.speaker === speaker && last.turnSeq === turnSeq;
|
|
1486
|
+
setTranscript((prev) => {
|
|
1487
|
+
const tail = prev[prev.length - 1];
|
|
1488
|
+
const agentContinues = speaker === "agent" && tail?.speaker === "agent";
|
|
1489
|
+
if (agentContinues || sameUserBurst && tail?.speaker === "user") {
|
|
1490
|
+
const merged = mergeTranscriptText(tail.text, text);
|
|
1491
|
+
if (merged === tail.text) return prev;
|
|
1492
|
+
return prev.map(
|
|
1493
|
+
(l, i) => i === prev.length - 1 ? { ...l, text: merged } : l
|
|
1494
|
+
);
|
|
1477
1495
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1496
|
+
return [...prev, { id: `${speaker}-${prev.length}`, speaker, text }];
|
|
1497
|
+
});
|
|
1498
|
+
lastFinalTurnRef.current = { speaker, turnSeq };
|
|
1499
|
+
}, []);
|
|
1481
1500
|
const handleCustomEvent = (0, import_react5.useCallback)(
|
|
1482
1501
|
(name, value) => {
|
|
1483
1502
|
switch (name) {
|
|
@@ -1510,9 +1529,7 @@ function useVoice(options = {}) {
|
|
|
1510
1529
|
}
|
|
1511
1530
|
break;
|
|
1512
1531
|
case "voice_transcript":
|
|
1513
|
-
handleTranscript(
|
|
1514
|
-
value
|
|
1515
|
-
);
|
|
1532
|
+
handleTranscript(value);
|
|
1516
1533
|
break;
|
|
1517
1534
|
case "voice_interrupted":
|
|
1518
1535
|
acceptedTurnSeqRef.current = value.turnSeq;
|
|
@@ -1909,7 +1926,7 @@ function useMcpConnections(options = {}) {
|
|
|
1909
1926
|
|
|
1910
1927
|
// src/index.ts
|
|
1911
1928
|
var import_logger2 = require("@personaai/logger");
|
|
1912
|
-
var VERSION = "0.7.
|
|
1929
|
+
var VERSION = "0.7.3";
|
|
1913
1930
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1914
1931
|
0 && (module.exports = {
|
|
1915
1932
|
PersonaProvider,
|