From b9ba98e518b93450b123d4a877c396216d6da046 Mon Sep 17 00:00:00 2001 From: Julio M Cruz Date: Sat, 15 Aug 2026 12:32:05 -0400 Subject: [PATCH] feat(voice): add final-turn chat consent UI --- .../agents/[agentId]/AgentVoiceCallCard.tsx | 36 +++++++++ .../[agentId]/AgentVoiceCallController.tsx | 43 +++++++++- app/(app)/agents/[agentId]/page.tsx | 2 +- app/lib/agentVoice.ts | 1 + app/lib/perkosApi.ts | 22 ++++- tests/AgentVoiceCallCard.test.tsx | 14 ++++ tests/AgentVoiceCallController.test.tsx | 81 ++++++++++++++++++- 7 files changed, 189 insertions(+), 10 deletions(-) diff --git a/app/(app)/agents/[agentId]/AgentVoiceCallCard.tsx b/app/(app)/agents/[agentId]/AgentVoiceCallCard.tsx index 716a703..30f2a6f 100644 --- a/app/(app)/agents/[agentId]/AgentVoiceCallCard.tsx +++ b/app/(app)/agents/[agentId]/AgentVoiceCallCard.tsx @@ -3,6 +3,7 @@ import { Loader2, Mic, MicOff, PhoneOff } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Card, CardContent, @@ -28,6 +29,10 @@ export function AgentVoiceCallCard({ onEnd, error, remoteAudioStatus, + chatMirrorAvailable = false, + chatMirrorEnabled = false, + chatMirrorScope = "direct", + onChatMirrorEnabledChange, }: { agentName: string; capability?: AgentVoiceCapability | null; @@ -36,10 +41,16 @@ export function AgentVoiceCallCard({ onEnd?: () => void; error?: string | null; remoteAudioStatus?: string | null; + chatMirrorAvailable?: boolean; + chatMirrorEnabled?: boolean; + chatMirrorScope?: "direct" | "project"; + onChatMirrorEnabledChange?: (enabled: boolean) => void; }) { const state = callState ?? resolveAgentVoiceState(capability); const busy = BUSY_STATES.includes(state); const enabled = canStartAgentVoiceCall(state) && Boolean(onStart); + const mirrorLocked = busy || state === "in-call"; + const mirrorDestination = chatMirrorScope === "project" ? "project chat" : "direct chat"; return ( @@ -58,6 +69,31 @@ export function AgentVoiceCallCard({ ? `${agentName} has not reported a verified voice gateway and speech provider. Text availability does not enable voice.` : "Voice calls use an active project meeting and temporary microphone processing consent."}

+ {chatMirrorAvailable ? ( +
+
+ onChatMirrorEnabledChange?.(checked === true)} + /> +
+

+ Save final voice turns to {mirrorDestination} +

+

+ On by default. Turn this off for a private, non-transcribed call. No raw audio or interim text is saved. +

+
+
+

+ {chatMirrorEnabled + ? `Final user and agent text will be saved to ${mirrorDestination}.` + : "Private call: no final user or agent text will be saved."} +

+
+ ) : null} {state === "in-call" ? :