{"version":3,"file":"CopilotChatAudioRecorder.mjs","names":[],"sources":["../../../src/components/chat/CopilotChatAudioRecorder.tsx"],"sourcesContent":["import {\n  useRef,\n  useEffect,\n  useImperativeHandle,\n  forwardRef,\n  useCallback,\n  useState,\n} from \"react\";\nimport { twMerge } from \"tailwind-merge\";\n\n/** Finite-state machine for every recorder implementation */\nexport type AudioRecorderState = \"idle\" | \"recording\" | \"processing\";\n\n/** Error subclass so callers can `instanceof`-guard recorder failures */\nexport class AudioRecorderError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"AudioRecorderError\";\n  }\n}\n\nexport interface AudioRecorderRef {\n  state: AudioRecorderState;\n  start: () => Promise<void>;\n  stop: () => Promise<Blob>;\n  dispose: () => void;\n}\n\nexport const CopilotChatAudioRecorder = forwardRef<\n  AudioRecorderRef,\n  React.HTMLAttributes<HTMLDivElement>\n>((props, ref) => {\n  const { className, ...divProps } = props;\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  // Recording state\n  const [recorderState, setRecorderState] =\n    useState<AudioRecorderState>(\"idle\");\n  const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n  const audioChunksRef = useRef<Blob[]>([]);\n  const streamRef = useRef<MediaStream | null>(null);\n  const analyserRef = useRef<AnalyserNode | null>(null);\n  const audioContextRef = useRef<AudioContext | null>(null);\n  const animationIdRef = useRef<number | null>(null);\n\n  // Amplitude history buffer for scrolling waveform\n  const amplitudeHistoryRef = useRef<number[]>([]);\n  const frameCountRef = useRef<number>(0);\n  const scrollOffsetRef = useRef<number>(0);\n  const smoothedAmplitudeRef = useRef<number>(0);\n  const fadeOpacityRef = useRef<number>(0);\n\n  // Clean up all resources\n  const cleanup = useCallback(() => {\n    if (animationIdRef.current) {\n      cancelAnimationFrame(animationIdRef.current);\n      animationIdRef.current = null;\n    }\n    if (\n      mediaRecorderRef.current &&\n      mediaRecorderRef.current.state !== \"inactive\"\n    ) {\n      try {\n        mediaRecorderRef.current.stop();\n      } catch {\n        // Ignore errors during cleanup\n      }\n    }\n    if (streamRef.current) {\n      streamRef.current.getTracks().forEach((track) => track.stop());\n      streamRef.current = null;\n    }\n    if (audioContextRef.current && audioContextRef.current.state !== \"closed\") {\n      audioContextRef.current.close().catch(() => {\n        // Ignore close errors\n      });\n      audioContextRef.current = null;\n    }\n    mediaRecorderRef.current = null;\n    analyserRef.current = null;\n    audioChunksRef.current = [];\n    amplitudeHistoryRef.current = [];\n    frameCountRef.current = 0;\n    scrollOffsetRef.current = 0;\n    smoothedAmplitudeRef.current = 0;\n    fadeOpacityRef.current = 0;\n  }, []);\n\n  // Start recording\n  const start = useCallback(async () => {\n    if (recorderState !== \"idle\") {\n      throw new AudioRecorderError(\"Recorder is already active\");\n    }\n\n    try {\n      // Request microphone access\n      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n      streamRef.current = stream;\n\n      // Set up audio context for visualization\n      const audioContext = new AudioContext();\n      audioContextRef.current = audioContext;\n      const source = audioContext.createMediaStreamSource(stream);\n      const analyser = audioContext.createAnalyser();\n      analyser.fftSize = 2048; // Higher resolution for time-domain waveform\n      source.connect(analyser);\n      analyserRef.current = analyser;\n\n      // Determine best MIME type for recording\n      const mimeType = MediaRecorder.isTypeSupported(\"audio/webm;codecs=opus\")\n        ? \"audio/webm;codecs=opus\"\n        : MediaRecorder.isTypeSupported(\"audio/webm\")\n          ? \"audio/webm\"\n          : MediaRecorder.isTypeSupported(\"audio/mp4\")\n            ? \"audio/mp4\"\n            : \"\";\n\n      const options: MediaRecorderOptions = mimeType ? { mimeType } : {};\n      const mediaRecorder = new MediaRecorder(stream, options);\n      mediaRecorderRef.current = mediaRecorder;\n      audioChunksRef.current = [];\n\n      mediaRecorder.ondataavailable = (event) => {\n        if (event.data.size > 0) {\n          audioChunksRef.current.push(event.data);\n        }\n      };\n\n      // Start recording with timeslice to collect data periodically\n      mediaRecorder.start(100);\n      setRecorderState(\"recording\");\n    } catch (error) {\n      cleanup();\n      if (error instanceof Error && error.name === \"NotAllowedError\") {\n        throw new AudioRecorderError(\"Microphone permission denied\");\n      }\n      if (error instanceof Error && error.name === \"NotFoundError\") {\n        throw new AudioRecorderError(\"No microphone found\");\n      }\n      throw new AudioRecorderError(\n        error instanceof Error ? error.message : \"Failed to start recording\",\n      );\n    }\n  }, [recorderState, cleanup]);\n\n  // Stop recording and return audio blob\n  const stop = useCallback((): Promise<Blob> => {\n    return new Promise((resolve, reject) => {\n      const mediaRecorder = mediaRecorderRef.current;\n      if (!mediaRecorder || recorderState !== \"recording\") {\n        reject(new AudioRecorderError(\"No active recording\"));\n        return;\n      }\n\n      setRecorderState(\"processing\");\n\n      mediaRecorder.onstop = () => {\n        const mimeType = mediaRecorder.mimeType || \"audio/webm\";\n        const audioBlob = new Blob(audioChunksRef.current, { type: mimeType });\n\n        // Clean up but keep the blob\n        cleanup();\n        setRecorderState(\"idle\");\n        resolve(audioBlob);\n      };\n\n      mediaRecorder.onerror = () => {\n        cleanup();\n        setRecorderState(\"idle\");\n        reject(new AudioRecorderError(\"Recording failed\"));\n      };\n\n      mediaRecorder.stop();\n    });\n  }, [recorderState, cleanup]);\n\n  // Calculate RMS amplitude from time-domain data\n  const calculateAmplitude = (dataArray: Uint8Array): number => {\n    let sum = 0;\n    for (let i = 0; i < dataArray.length; i++) {\n      // Normalize to -1 to 1 range (128 is center/silence)\n      const sample = (dataArray[i] ?? 128) / 128 - 1;\n      sum += sample * sample;\n    }\n    return Math.sqrt(sum / dataArray.length);\n  };\n\n  // Canvas rendering with animation\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    // Configuration\n    const barWidth = 2;\n    const barGap = 1;\n    const barSpacing = barWidth + barGap;\n    const scrollSpeed = 1 / 3; // Pixels per frame\n\n    const draw = () => {\n      const rect = canvas.getBoundingClientRect();\n      const dpr = window.devicePixelRatio || 1;\n\n      // Update canvas dimensions if container resized\n      if (\n        canvas.width !== rect.width * dpr ||\n        canvas.height !== rect.height * dpr\n      ) {\n        canvas.width = rect.width * dpr;\n        canvas.height = rect.height * dpr;\n        ctx.scale(dpr, dpr);\n      }\n\n      // Calculate how many bars fit in the canvas (plus extra for smooth scrolling)\n      const maxBars = Math.floor(rect.width / barSpacing) + 2;\n\n      // Get current amplitude if recording\n      if (analyserRef.current && recorderState === \"recording\") {\n        // Pre-fill history with zeros on first frame so line is visible immediately\n        if (amplitudeHistoryRef.current.length === 0) {\n          amplitudeHistoryRef.current = new Array(maxBars).fill(0);\n        }\n\n        // Fade in the waveform smoothly\n        if (fadeOpacityRef.current < 1) {\n          fadeOpacityRef.current = Math.min(1, fadeOpacityRef.current + 0.03);\n        }\n\n        // Smooth scrolling - increment offset every frame\n        scrollOffsetRef.current += scrollSpeed;\n\n        // Sample amplitude every frame for smoothing\n        const bufferLength = analyserRef.current.fftSize;\n        const dataArray = new Uint8Array(bufferLength);\n        analyserRef.current.getByteTimeDomainData(dataArray);\n        const rawAmplitude = calculateAmplitude(dataArray);\n\n        // Smoothing: gradual attack and decay\n        const attackSpeed = 0.12; // Smooth rise\n        const decaySpeed = 0.08; // Smooth fade out\n        const speed =\n          rawAmplitude > smoothedAmplitudeRef.current\n            ? attackSpeed\n            : decaySpeed;\n        smoothedAmplitudeRef.current +=\n          (rawAmplitude - smoothedAmplitudeRef.current) * speed;\n\n        // When offset reaches a full bar width, add a new sample and reset offset\n        if (scrollOffsetRef.current >= barSpacing) {\n          scrollOffsetRef.current -= barSpacing;\n          amplitudeHistoryRef.current.push(smoothedAmplitudeRef.current);\n\n          // Trim history to fit canvas\n          if (amplitudeHistoryRef.current.length > maxBars) {\n            amplitudeHistoryRef.current =\n              amplitudeHistoryRef.current.slice(-maxBars);\n          }\n        }\n      }\n\n      // Clear canvas\n      ctx.clearRect(0, 0, rect.width, rect.height);\n\n      // Get current foreground color\n      const computedStyle = getComputedStyle(canvas);\n      ctx.fillStyle = computedStyle.color;\n      ctx.globalAlpha = fadeOpacityRef.current;\n\n      const centerY = rect.height / 2;\n      const maxAmplitude = rect.height / 2 - 2; // Leave some padding\n\n      const history = amplitudeHistoryRef.current;\n\n      // Only draw when recording (history has data)\n      if (history.length > 0) {\n        const offset = scrollOffsetRef.current;\n        const edgeFadeWidth = 12; // Pixels to fade at each edge\n\n        for (let i = 0; i < history.length; i++) {\n          const amplitude = history[i] ?? 0;\n          // Scale amplitude (RMS is typically 0-0.5 for normal speech)\n          const scaledAmplitude = Math.min(amplitude * 4, 1);\n          const barHeight = Math.max(2, scaledAmplitude * maxAmplitude * 2);\n\n          // Position: right-aligned with smooth scroll offset\n          const x = rect.width - (history.length - i) * barSpacing - offset;\n          const y = centerY - barHeight / 2;\n\n          // Only draw if visible\n          if (x + barWidth > 0 && x < rect.width) {\n            // Calculate edge fade opacity\n            let edgeOpacity = 1;\n            if (x < edgeFadeWidth) {\n              // Fade out on left edge\n              edgeOpacity = Math.max(0, x / edgeFadeWidth);\n            } else if (x > rect.width - edgeFadeWidth) {\n              // Fade in on right edge\n              edgeOpacity = Math.max(0, (rect.width - x) / edgeFadeWidth);\n            }\n\n            ctx.globalAlpha = fadeOpacityRef.current * edgeOpacity;\n            ctx.fillRect(x, y, barWidth, barHeight);\n          }\n        }\n      }\n\n      animationIdRef.current = requestAnimationFrame(draw);\n    };\n\n    draw();\n\n    return () => {\n      if (animationIdRef.current) {\n        cancelAnimationFrame(animationIdRef.current);\n      }\n    };\n  }, [recorderState]);\n\n  // Cleanup on unmount\n  useEffect(() => {\n    return cleanup;\n  }, [cleanup]);\n\n  // Expose AudioRecorder API via ref\n  useImperativeHandle(\n    ref,\n    () => ({\n      get state() {\n        return recorderState;\n      },\n      start,\n      stop,\n      dispose: cleanup,\n    }),\n    [recorderState, start, stop, cleanup],\n  );\n\n  return (\n    <div\n      className={twMerge(\"cpk:w-full cpk:py-3 cpk:px-5\", className)}\n      {...divProps}\n    >\n      <canvas ref={canvasRef} className=\"cpk:block cpk:w-full cpk:h-[26px]\" />\n    </div>\n  );\n});\n\nCopilotChatAudioRecorder.displayName = \"CopilotChatAudioRecorder\";\n"],"mappings":"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"}