import { useMemo } from "react"; import { View, Text, Pressable, ActivityIndicator } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { X, ArrowUp, RefreshCcw, Check, Mic, Pencil } from "lucide-react-native"; import { VolumeMeter } from "./volume-meter"; import { FOOTER_HEIGHT } from "@/constants/layout"; import type { DictationStatus } from "@/hooks/use-dictation"; interface DictationControlsProps { volume: number; duration: number; transcript?: string; isRecording: boolean; isProcessing: boolean; status: DictationStatus; onStart: () => void; onCancel: () => void; onAccept: () => void; onAcceptAndSend: () => void; onRetry?: () => void; onDiscard?: () => void; disabled?: boolean; } function formatDuration(seconds: number): string { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; } export function DictationControls({ volume, duration, isRecording, isProcessing, status, onStart, onCancel, onAccept, onAcceptAndSend, onRetry, onDiscard, disabled = false, }: DictationControlsProps) { const { theme } = useUnistyles(); const isFailed = status === "failed"; const showActiveState = isRecording || isProcessing || isFailed; const actionsDisabled = isProcessing; const handleCancel = isFailed && onDiscard ? onDiscard : onCancel; const micButtonStyle = useMemo( () => [styles.micButton, disabled && styles.buttonDisabled], [disabled], ); const timerTextStyle = useMemo( () => [styles.timerText, { color: theme.colors.foreground }], [theme.colors.foreground], ); const cancelButtonStyle = useMemo( () => [ styles.actionButton, styles.actionButtonCancel, actionsDisabled && !isFailed ? styles.buttonDisabled : undefined, ], [actionsDisabled, isFailed], ); if (!showActiveState) { return ( ); } return ( {formatDuration(duration)} {actionsDisabled ? ( ) : null} {!actionsDisabled && isFailed ? ( ) : null} {!actionsDisabled && !isFailed ? ( <> ) : null} ); } /** * Full-width overlay variant for the agent input footer. * Uses blue background with white icons. */ export function DictationOverlay({ volume, duration, isRecording, isProcessing, status, errorText, onCancel, onAccept, onAcceptAndSend, onRetry, onDiscard, }: Omit & { errorText?: string }) { const { theme } = useUnistyles(); const isFailed = status === "failed"; const showActiveState = isRecording || isProcessing || isFailed; const actionsDisabled = isProcessing; const handleCancel = isFailed && onDiscard ? onDiscard : onCancel; const containerStyle = useMemo( () => [overlayStyles.container, { backgroundColor: theme.colors.accent }], [theme.colors.accent], ); const overlayCancelButtonStyle = useMemo( () => [ overlayStyles.cancelButton, actionsDisabled && !isFailed && overlayStyles.buttonDisabled, ], [actionsDisabled, isFailed], ); const overlayTimerTextStyle = useMemo( () => [overlayStyles.timerText, { color: theme.colors.accentForeground }], [theme.colors.accentForeground], ); const overlayTranscriptTextStyle = useMemo( () => [overlayStyles.transcriptText, { color: theme.colors.accentForeground, opacity: 0.95 }], [theme.colors.accentForeground], ); const overlayRetryButtonStyle = useMemo( () => [overlayStyles.actionButton, { backgroundColor: theme.colors.accentForeground }], [theme.colors.accentForeground], ); const overlayConfirmButtonStyle = overlayRetryButtonStyle; if (!showActiveState) { return null; } return ( {formatDuration(duration)} {isFailed ? ( {errorText ? `Dictation failed: ${errorText}` : "Dictation failed. Tap retry."} ) : null} {actionsDisabled ? ( ) : null} {!actionsDisabled && isFailed ? ( ) : null} {!actionsDisabled && !isFailed ? ( <> ) : null} ); } const BUTTON_SIZE = 32; const styles = StyleSheet.create((theme) => ({ micButton: { width: BUTTON_SIZE, height: BUTTON_SIZE, borderRadius: theme.borderRadius.full, alignItems: "center", justifyContent: "center", borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, backgroundColor: theme.colors.surface0, }, activeContainer: { flexDirection: "row", alignItems: "center", gap: theme.spacing[3], }, meterWrapper: { width: 80, alignItems: "center", justifyContent: "center", }, timerText: { fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.semibold, fontVariant: ["tabular-nums"], }, actionGroup: { flexDirection: "row", gap: theme.spacing[2], }, actionButton: { width: BUTTON_SIZE, height: BUTTON_SIZE, borderRadius: theme.borderRadius.full, alignItems: "center", justifyContent: "center", borderWidth: theme.borderWidth[1], }, actionButtonCancel: { borderColor: theme.colors.border, backgroundColor: theme.colors.surface0, }, actionButtonSecondary: { borderColor: theme.colors.border, backgroundColor: theme.colors.surface0, }, actionButtonConfirm: { borderColor: theme.colors.foreground, backgroundColor: theme.colors.foreground, }, buttonDisabled: { opacity: 0.4, }, loadingContainer: { width: BUTTON_SIZE, height: BUTTON_SIZE, alignItems: "center", justifyContent: "center", }, statusLabel: { fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.semibold, }, })); const OVERLAY_BUTTON_SIZE = 44; const OVERLAY_VERTICAL_PADDING = (FOOTER_HEIGHT - OVERLAY_BUTTON_SIZE) / 2; const overlayStyles = StyleSheet.create((theme) => ({ container: { flexDirection: "row", alignItems: "center", width: "100%", borderRadius: theme.borderRadius["2xl"], justifyContent: "space-between", paddingHorizontal: theme.spacing[4], paddingVertical: OVERLAY_VERTICAL_PADDING, height: FOOTER_HEIGHT, }, cancelButton: { width: OVERLAY_BUTTON_SIZE, height: OVERLAY_BUTTON_SIZE, borderRadius: theme.borderRadius.full, backgroundColor: "rgba(0, 0, 0, 0.15)", alignItems: "center", justifyContent: "center", }, centerContainer: { flex: 1, flexDirection: "column", alignItems: "center", justifyContent: "center", gap: theme.spacing[2], }, meterRow: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: theme.spacing[4], }, timerText: { fontSize: theme.fontSize.xl, fontWeight: theme.fontWeight.semibold, fontVariant: ["tabular-nums"], }, transcriptText: { fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, textAlign: "center", paddingHorizontal: theme.spacing[2], opacity: 0.9, }, actionButtonsContainer: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, actionButton: { width: OVERLAY_BUTTON_SIZE, height: OVERLAY_BUTTON_SIZE, borderRadius: theme.borderRadius.full, alignItems: "center", justifyContent: "center", }, buttonDisabled: { opacity: 0.5, }, loadingContainer: { width: OVERLAY_BUTTON_SIZE, height: OVERLAY_BUTTON_SIZE, alignItems: "center", justifyContent: "center", }, })); const ACTION_CONFIRM_STYLE = [styles.actionButton, styles.actionButtonConfirm]; const ACTION_SECONDARY_STYLE = [styles.actionButton, styles.actionButtonSecondary]; const OVERLAY_ACCEPT_BUTTON_BG = { backgroundColor: "rgba(255, 255, 255, 0.25)" }; const OVERLAY_ACCEPT_BUTTON_STYLE = [overlayStyles.actionButton, OVERLAY_ACCEPT_BUTTON_BG];