@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/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();