Copy to clipboard and final polish

The pipeline works. Now we make it pleasant to use. Users want to copy the cleaned transcript, see a loading state while the LLM runs, and record with a keyboard shortcut instead of clicking a button.

frontend/src/App.tsx
typescript
const copyToClipboard = (text: string) => {
  navigator.clipboard
    .writeText(text)
    .then(() => {
      setIsCopied(true);
      setTimeout(() => setIsCopied(false), 2000);
    })
    .catch((err: Error) => setError('Copy failed: ' + err.message));
};

navigator.clipboard.writeText returns a Promise. On success, flip a flag for two seconds so the UI can show a confirmation. On failure, surface the error because the user expected a copy to happen.

frontend/src/App.tsx
typescript
useEffect(() => {
  const handleKeyDown = (e: KeyboardEvent) => {
    if (isProcessing || e.repeat || isKeyDownRef.current) return;
    const target = e.target as HTMLElement;
    if (e.key.toLowerCase() === 'v' && !['INPUT', 'TEXTAREA'].includes(target.tagName)) {
      e.preventDefault();
      isKeyDownRef.current = true;
      if (!isRecording) void startRecording();
    }
  };
  const handleKeyUp = (e: KeyboardEvent) => {
    if (e.key.toLowerCase() === 'v') {
      isKeyDownRef.current = false;
      if (isRecording) stopRecording();
    }
  };
  window.addEventListener('keydown', handleKeyDown);
  window.addEventListener('keyup', handleKeyUp);
  return () => {
    window.removeEventListener('keydown', handleKeyDown);
    window.removeEventListener('keyup', handleKeyUp);
  };
}, [isRecording, isProcessing]);

Hold V to record, release to stop. The INPUT/TEXTAREA check stops the shortcut from firing when the user is typing into the prompt textarea. A ref tracks keydown state so browser key-repeat does not spam startRecording.

frontend/src/App.tsx
typescript
setRawText(transcribeData.text || '');
setIsProcessing(false);

if (useLLM && transcribeData.text) {
  setIsCleaningWithLLM(true);
  // ... cleanup fetch
  setIsCleaningWithLLM(false);
}

isCleaningWithLLM is a separate flag from isProcessing. Raw text shows immediately when transcription completes, and a subtle cleaning indicator appears next to the cleaned text box while the LLM runs.

writeText works without a prompt as long as the call is triggered by a user action like a click and the page is in a secure context. readText requires explicit permission because it exposes potentially sensitive data. Since we only write, the UX is smooth.

Quiz: Quiz

Loading practice…