@personaai/react 0.7.3 → 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 +11 -25
- package/dist/index.cjs +72 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -258,6 +258,16 @@ interface UseChatOptions {
|
|
|
258
258
|
onError?: (error: Error) => void;
|
|
259
259
|
/** Hook for receiving every low-level AG-UI streaming event (tool calls, steps, subagents) */
|
|
260
260
|
onEvent?: (event: PersonaStreamingEvent) => void;
|
|
261
|
+
/**
|
|
262
|
+
* Pass the object returned by `useVoice()` (sharing the same `threadId`) to have `useChat`
|
|
263
|
+
* merge live voice turns into `messages` automatically — one bubble per utterance, deduped
|
|
264
|
+
* against thread history and against a voice turn that already persisted back into this
|
|
265
|
+
* thread, with the in-progress agent line updated in place while it's still being spoken.
|
|
266
|
+
* No manual sync effects needed in the host app; injected messages get a `voice-` prefixed id.
|
|
267
|
+
* Only applied while the passed hook's `state` is active (not `idle`/`ended`/`error`); the
|
|
268
|
+
* host app still owns starting/stopping the call itself (e.g. `voice.start()`/`voice.stop()`).
|
|
269
|
+
*/
|
|
270
|
+
voice?: UseVoiceResult;
|
|
261
271
|
}
|
|
262
272
|
interface SendMessageOverride {
|
|
263
273
|
agentId?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -258,6 +258,16 @@ interface UseChatOptions {
|
|
|
258
258
|
onError?: (error: Error) => void;
|
|
259
259
|
/** Hook for receiving every low-level AG-UI streaming event (tool calls, steps, subagents) */
|
|
260
260
|
onEvent?: (event: PersonaStreamingEvent) => void;
|
|
261
|
+
/**
|
|
262
|
+
* Pass the object returned by `useVoice()` (sharing the same `threadId`) to have `useChat`
|
|
263
|
+
* merge live voice turns into `messages` automatically — one bubble per utterance, deduped
|
|
264
|
+
* against thread history and against a voice turn that already persisted back into this
|
|
265
|
+
* thread, with the in-progress agent line updated in place while it's still being spoken.
|
|
266
|
+
* No manual sync effects needed in the host app; injected messages get a `voice-` prefixed id.
|
|
267
|
+
* Only applied while the passed hook's `state` is active (not `idle`/`ended`/`error`); the
|
|
268
|
+
* host app still owns starting/stopping the call itself (e.g. `voice.start()`/`voice.stop()`).
|
|
269
|
+
*/
|
|
270
|
+
voice?: UseVoiceResult;
|
|
261
271
|
}
|
|
262
272
|
interface SendMessageOverride {
|
|
263
273
|
agentId?: string;
|
package/dist/index.js
CHANGED
|
@@ -392,6 +392,9 @@ function useChat(options = {}) {
|
|
|
392
392
|
const [presentedFile, setPresentedFile] = useState(null);
|
|
393
393
|
const abortControllerRef = useRef(null);
|
|
394
394
|
const loadedThreadIdRef = useRef(void 0);
|
|
395
|
+
const voiceThreadRef = useRef(threadId);
|
|
396
|
+
const voicePrevLenRef = useRef(0);
|
|
397
|
+
const voiceStreamingIdRef = useRef(null);
|
|
395
398
|
const stop = useCallback(() => {
|
|
396
399
|
if (abortControllerRef.current) {
|
|
397
400
|
chatLogger.info("stop streaming", {});
|
|
@@ -497,6 +500,75 @@ function useChat(options = {}) {
|
|
|
497
500
|
chatLogger.debug("loadThreadMessages trigger", { threadId });
|
|
498
501
|
void loadThreadMessages(threadId);
|
|
499
502
|
}, [threadId, isStreaming, messages.length, loadThreadMessages, chatLogger]);
|
|
503
|
+
const voice = options.voice;
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
if (!voice) return;
|
|
506
|
+
if (voiceThreadRef.current !== threadId) {
|
|
507
|
+
voiceThreadRef.current = threadId;
|
|
508
|
+
voicePrevLenRef.current = voice.transcript.length;
|
|
509
|
+
voiceStreamingIdRef.current = null;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const isVoiceActive = voice.state !== "idle" && voice.state !== "ended" && voice.state !== "error";
|
|
513
|
+
const curLen = voice.transcript.length;
|
|
514
|
+
if (curLen < voicePrevLenRef.current) {
|
|
515
|
+
voicePrevLenRef.current = 0;
|
|
516
|
+
voiceStreamingIdRef.current = null;
|
|
517
|
+
}
|
|
518
|
+
if (!isVoiceActive) {
|
|
519
|
+
voicePrevLenRef.current = voice.transcript.length;
|
|
520
|
+
if (voiceStreamingIdRef.current) {
|
|
521
|
+
const doneId = voiceStreamingIdRef.current;
|
|
522
|
+
voiceStreamingIdRef.current = null;
|
|
523
|
+
setMessages(
|
|
524
|
+
(prev) => prev.map((m) => m.id === doneId ? { ...m, isStreaming: false } : m)
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (curLen > voicePrevLenRef.current) {
|
|
530
|
+
const newLines = voice.transcript.slice(voicePrevLenRef.current);
|
|
531
|
+
voicePrevLenRef.current = curLen;
|
|
532
|
+
for (const line of newLines) {
|
|
533
|
+
const text = (line.text || "").trim();
|
|
534
|
+
if (!text) continue;
|
|
535
|
+
const role = line.speaker === "user" ? "user" : "assistant";
|
|
536
|
+
const voiceId = `voice-${line.id}`;
|
|
537
|
+
setMessages((prev) => {
|
|
538
|
+
if (prev.some((m) => m.id === voiceId)) return prev;
|
|
539
|
+
if (prev.some(
|
|
540
|
+
(m) => !m.id.startsWith("voice-") && m.role === role && m.content.trim() === text
|
|
541
|
+
))
|
|
542
|
+
return prev;
|
|
543
|
+
const last = prev[prev.length - 1];
|
|
544
|
+
if (last?.id.startsWith("voice-") && last.role === "assistant" && role === "assistant") {
|
|
545
|
+
const merged = text.startsWith(last.content) ? text : `${last.content} ${text}`.trim();
|
|
546
|
+
return [...prev.slice(0, -1), { ...last, content: merged, isStreaming: false }];
|
|
547
|
+
}
|
|
548
|
+
return [...prev, { id: voiceId, role, content: text, createdAt: /* @__PURE__ */ new Date() }];
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const partialText = voice.partial?.speaker === "agent" ? voice.partial.text.trim() : "";
|
|
553
|
+
if (partialText) {
|
|
554
|
+
setMessages((prev) => {
|
|
555
|
+
const last = prev[prev.length - 1];
|
|
556
|
+
if (last?.id.startsWith("voice-") && last.role === "assistant") {
|
|
557
|
+
voiceStreamingIdRef.current = last.id;
|
|
558
|
+
return [...prev.slice(0, -1), { ...last, content: partialText, isStreaming: true }];
|
|
559
|
+
}
|
|
560
|
+
const id = voiceStreamingIdRef.current || `voice-partial-${Date.now()}`;
|
|
561
|
+
voiceStreamingIdRef.current = id;
|
|
562
|
+
return [...prev, { id, role: "assistant", content: partialText, isStreaming: true, createdAt: /* @__PURE__ */ new Date() }];
|
|
563
|
+
});
|
|
564
|
+
} else if (voice.state !== "speaking" && voiceStreamingIdRef.current) {
|
|
565
|
+
const doneId = voiceStreamingIdRef.current;
|
|
566
|
+
voiceStreamingIdRef.current = null;
|
|
567
|
+
setMessages(
|
|
568
|
+
(prev) => prev.map((m) => m.id === doneId ? { ...m, isStreaming: false } : m)
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}, [voice?.transcript, voice?.partial, voice?.state, threadId]);
|
|
500
572
|
const sendMessage = useCallback(
|
|
501
573
|
async (contentToSend, overrideOptions) => {
|
|
502
574
|
const prompt = (contentToSend ?? input).trim();
|