@personaai/react 0.7.2 → 0.7.4
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 +31 -0
- package/dist/index.cjs +76 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +76 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,37 @@ 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
|
+
Share the same `threadId` between `useVoice` and `useChat`, and pass the voice hook's return
|
|
113
|
+
value into `useChat`'s `voice` option — that's the whole integration:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
const voice = useVoice({ agentId, threadId });
|
|
117
|
+
const { messages, ... } = useChat({ threadId, voice });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`useChat` merges live voice turns into `messages` for you: one bubble per utterance, deduped
|
|
121
|
+
against thread history and against a voice turn that already persisted back into the shared
|
|
122
|
+
thread, consecutive same-speaker fragments folded into the line they opened, and the
|
|
123
|
+
in-progress agent line updated in place while it's still being spoken (`isStreaming: true`, same
|
|
124
|
+
as a text response). Injected messages get a `voice-`-prefixed id. You still call `voice.start()`
|
|
125
|
+
/ `voice.stop()` yourself — `useChat` only owns the transcript-to-feed sync, not the call
|
|
126
|
+
lifecycle.
|
|
127
|
+
|
|
97
128
|
## Devtools
|
|
98
129
|
|
|
99
130
|
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
|
@@ -435,6 +435,9 @@ function useChat(options = {}) {
|
|
|
435
435
|
const [presentedFile, setPresentedFile] = (0, import_react2.useState)(null);
|
|
436
436
|
const abortControllerRef = (0, import_react2.useRef)(null);
|
|
437
437
|
const loadedThreadIdRef = (0, import_react2.useRef)(void 0);
|
|
438
|
+
const voiceThreadRef = (0, import_react2.useRef)(threadId);
|
|
439
|
+
const voicePrevLenRef = (0, import_react2.useRef)(0);
|
|
440
|
+
const voiceStreamingIdRef = (0, import_react2.useRef)(null);
|
|
438
441
|
const stop = (0, import_react2.useCallback)(() => {
|
|
439
442
|
if (abortControllerRef.current) {
|
|
440
443
|
chatLogger.info("stop streaming", {});
|
|
@@ -540,6 +543,75 @@ function useChat(options = {}) {
|
|
|
540
543
|
chatLogger.debug("loadThreadMessages trigger", { threadId });
|
|
541
544
|
void loadThreadMessages(threadId);
|
|
542
545
|
}, [threadId, isStreaming, messages.length, loadThreadMessages, chatLogger]);
|
|
546
|
+
const voice = options.voice;
|
|
547
|
+
(0, import_react2.useEffect)(() => {
|
|
548
|
+
if (!voice) return;
|
|
549
|
+
if (voiceThreadRef.current !== threadId) {
|
|
550
|
+
voiceThreadRef.current = threadId;
|
|
551
|
+
voicePrevLenRef.current = voice.transcript.length;
|
|
552
|
+
voiceStreamingIdRef.current = null;
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const isVoiceActive = voice.state !== "idle" && voice.state !== "ended" && voice.state !== "error";
|
|
556
|
+
const curLen = voice.transcript.length;
|
|
557
|
+
if (curLen < voicePrevLenRef.current) {
|
|
558
|
+
voicePrevLenRef.current = 0;
|
|
559
|
+
voiceStreamingIdRef.current = null;
|
|
560
|
+
}
|
|
561
|
+
if (!isVoiceActive) {
|
|
562
|
+
voicePrevLenRef.current = voice.transcript.length;
|
|
563
|
+
if (voiceStreamingIdRef.current) {
|
|
564
|
+
const doneId = voiceStreamingIdRef.current;
|
|
565
|
+
voiceStreamingIdRef.current = null;
|
|
566
|
+
setMessages(
|
|
567
|
+
(prev) => prev.map((m) => m.id === doneId ? { ...m, isStreaming: false } : m)
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (curLen > voicePrevLenRef.current) {
|
|
573
|
+
const newLines = voice.transcript.slice(voicePrevLenRef.current);
|
|
574
|
+
voicePrevLenRef.current = curLen;
|
|
575
|
+
for (const line of newLines) {
|
|
576
|
+
const text = (line.text || "").trim();
|
|
577
|
+
if (!text) continue;
|
|
578
|
+
const role = line.speaker === "user" ? "user" : "assistant";
|
|
579
|
+
const voiceId = `voice-${line.id}`;
|
|
580
|
+
setMessages((prev) => {
|
|
581
|
+
if (prev.some((m) => m.id === voiceId)) return prev;
|
|
582
|
+
if (prev.some(
|
|
583
|
+
(m) => !m.id.startsWith("voice-") && m.role === role && m.content.trim() === text
|
|
584
|
+
))
|
|
585
|
+
return prev;
|
|
586
|
+
const last = prev[prev.length - 1];
|
|
587
|
+
if (last?.id.startsWith("voice-") && last.role === "assistant" && role === "assistant") {
|
|
588
|
+
const merged = text.startsWith(last.content) ? text : `${last.content} ${text}`.trim();
|
|
589
|
+
return [...prev.slice(0, -1), { ...last, content: merged, isStreaming: false }];
|
|
590
|
+
}
|
|
591
|
+
return [...prev, { id: voiceId, role, content: text, createdAt: /* @__PURE__ */ new Date() }];
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const partialText = voice.partial?.speaker === "agent" ? voice.partial.text.trim() : "";
|
|
596
|
+
if (partialText) {
|
|
597
|
+
setMessages((prev) => {
|
|
598
|
+
const last = prev[prev.length - 1];
|
|
599
|
+
if (last?.id.startsWith("voice-") && last.role === "assistant") {
|
|
600
|
+
voiceStreamingIdRef.current = last.id;
|
|
601
|
+
return [...prev.slice(0, -1), { ...last, content: partialText, isStreaming: true }];
|
|
602
|
+
}
|
|
603
|
+
const id = voiceStreamingIdRef.current || `voice-partial-${Date.now()}`;
|
|
604
|
+
voiceStreamingIdRef.current = id;
|
|
605
|
+
return [...prev, { id, role: "assistant", content: partialText, isStreaming: true, createdAt: /* @__PURE__ */ new Date() }];
|
|
606
|
+
});
|
|
607
|
+
} else if (voice.state !== "speaking" && voiceStreamingIdRef.current) {
|
|
608
|
+
const doneId = voiceStreamingIdRef.current;
|
|
609
|
+
voiceStreamingIdRef.current = null;
|
|
610
|
+
setMessages(
|
|
611
|
+
(prev) => prev.map((m) => m.id === doneId ? { ...m, isStreaming: false } : m)
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}, [voice?.transcript, voice?.partial, voice?.state, threadId]);
|
|
543
615
|
const sendMessage = (0, import_react2.useCallback)(
|
|
544
616
|
async (contentToSend, overrideOptions) => {
|
|
545
617
|
const prompt = (contentToSend ?? input).trim();
|
|
@@ -1482,10 +1554,11 @@ function useVoice(options = {}) {
|
|
|
1482
1554
|
}
|
|
1483
1555
|
setPartial((prev) => prev?.speaker === speaker ? null : prev);
|
|
1484
1556
|
const last = lastFinalTurnRef.current;
|
|
1485
|
-
const
|
|
1557
|
+
const sameUserBurst = speaker === "user" && last && last.speaker === speaker && last.turnSeq === turnSeq;
|
|
1486
1558
|
setTranscript((prev) => {
|
|
1487
1559
|
const tail = prev[prev.length - 1];
|
|
1488
|
-
|
|
1560
|
+
const agentContinues = speaker === "agent" && tail?.speaker === "agent";
|
|
1561
|
+
if (agentContinues || sameUserBurst && tail?.speaker === "user") {
|
|
1489
1562
|
const merged = mergeTranscriptText(tail.text, text);
|
|
1490
1563
|
if (merged === tail.text) return prev;
|
|
1491
1564
|
return prev.map(
|
|
@@ -1925,7 +1998,7 @@ function useMcpConnections(options = {}) {
|
|
|
1925
1998
|
|
|
1926
1999
|
// src/index.ts
|
|
1927
2000
|
var import_logger2 = require("@personaai/logger");
|
|
1928
|
-
var VERSION = "0.7.
|
|
2001
|
+
var VERSION = "0.7.3";
|
|
1929
2002
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1930
2003
|
0 && (module.exports = {
|
|
1931
2004
|
PersonaProvider,
|