import type { VoicePhase } from "../components/voice/VoiceStage"; import type { SpeechStatus } from "./tts"; /** After the user speaks, leave rotating “thinking” copy for at most this long. */ export const WORKING_PROMOTE_DELAY_MS = 1500; /** @deprecated use WORKING_PROMOTE_DELAY_MS */ export const WORKING_LISTEN_DELAY_MS = WORKING_PROMOTE_DELAY_MS; export const INVITE_MESSAGE_PREFIX = "invite:"; const ACTIVE_RUN_STATUSES = new Set(["running", "queued", "leased"]); export type UserTurnSpeechKind = "bot-reply" | "bot-ask" | "invite" | null; export function isActiveRunStatus(status: string | undefined): boolean { return status !== undefined && ACTIVE_RUN_STATUSES.has(status); } export function isUserTurnInvitationMessageId(messageId: string | undefined): boolean { return messageId?.startsWith(INVITE_MESSAGE_PREFIX) ?? false; } /** True when CallView should switch to a static working caption (mic stays closed). */ export function shouldPromoteToWorking(input: { phase: VoicePhase; speakerStatus: SpeechStatus; userTurnPending: boolean; }): boolean { return input.userTurnPending && input.phase === "thinking" && input.speakerStatus === "idle"; } /** @deprecated use shouldPromoteToWorking */ export const shouldPromoteToWorkingListen = shouldPromoteToWorking; /** Block auto-listen after bot TTS idle while the agent is still working quietly. */ export function shouldSuppressMicOnSpeechIdle(input: { workingCaption: string | null; userTurnPending: boolean; }): boolean { return input.workingCaption !== null || input.userTurnPending; } /** What CallView should do after TTS finishes — invite before mic when the user got no ask. */ export function nextActionAfterSpeechIdle(input: { speechKind: UserTurnSpeechKind; suppressMic: boolean; }): "listen" | "invite" | "none" { if (input.suppressMic) return "none"; if (input.speechKind === "invite") return "listen"; if (input.speechKind === "bot-ask") return "listen"; if (input.speechKind === "bot-reply") return "invite"; return "none"; } /** Opening invite when CallView mounts with nothing left to speak from history. */ export function shouldSpeakOpeningInvitation(input: { hasUnspokenBotMessage: boolean; runActive: boolean; userTurnPending: boolean; workingCaption: string | null; }): boolean { if (input.hasUnspokenBotMessage) return false; if (input.workingCaption) return false; if (input.userTurnPending) return false; if (input.runActive) return false; return true; } /** Subtitle while the agent works quietly after the user spoke. */ export function voiceThinkingSubtitle(input: { workingCaption: string | null; caption: string; thinkingLine: string | undefined; fallback: string; }): string { if (input.workingCaption) return input.workingCaption; if (input.caption.trim()) return input.caption; if (input.thinkingLine) return input.thinkingLine; return input.fallback; } /** Subtitle when the mic is genuinely open for the user's turn. */ export function voiceListeningSubtitle(input: { heard: string; idlePrompt: string }): string { if (input.heard.trim()) return input.heard; return input.idlePrompt; }