Spaces:
Running
Running
| /** | |
| * Transcription Display Component | |
| * | |
| * Shows progressive transcription with: | |
| * - Yellow text for fixed sentences (completed, won't change) | |
| * - Cyan dim text for active transcription (in-progress) | |
| */ | |
| import { useEffect, useRef } from 'react'; | |
| export default function TranscriptionDisplay({ fixedText, activeText, timestamp, isRecording, autoScroll = true, onAutoScrollToggle }) { | |
| const containerRef = useRef(null); | |
| // Auto-scroll to bottom when new text appears (if enabled) | |
| useEffect(() => { | |
| if (autoScroll && containerRef.current) { | |
| containerRef.current.scrollTop = containerRef.current.scrollHeight; | |
| } | |
| }, [fixedText, activeText, autoScroll]); | |
| const formatTimestamp = (seconds) => { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = (seconds % 60).toFixed(1); | |
| return `${mins}:${secs.padStart(4, '0')}`; | |
| }; | |
| return ( | |
| <div className="w-full max-w-4xl mx-auto"> | |
| <div className="bg-gray-900 rounded-lg border border-gray-700 p-6 shadow-xl"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-700"> | |
| <h2 className="text-xl font-semibold text-gray-100"> | |
| Live Transcription | |
| </h2> | |
| <div className="flex items-center gap-4"> | |
| {isRecording && ( | |
| <div className="flex items-center gap-2"> | |
| <div className="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div> | |
| <span className="text-sm text-gray-300">Recording</span> | |
| </div> | |
| )} | |
| {timestamp > 0 && ( | |
| <span className="text-sm text-gray-400 font-mono"> | |
| {formatTimestamp(timestamp)} | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| {/* Transcription Text */} | |
| <div | |
| ref={containerRef} | |
| className="min-h-[200px] max-h-[400px] overflow-y-auto font-sans text-lg leading-relaxed" | |
| > | |
| {!fixedText && !activeText && !isRecording && ( | |
| <p className="text-gray-500 italic"> | |
| Click "Start Recording" to begin transcription... | |
| </p> | |
| )} | |
| {!fixedText && !activeText && isRecording && ( | |
| <p className="text-gray-500 italic animate-pulse"> | |
| Listening... | |
| </p> | |
| )} | |
| {/* Fixed text (yellow) - sentences that won't change */} | |
| {fixedText && ( | |
| <span className="text-yellow-400 font-medium"> | |
| {fixedText} | |
| </span> | |
| )} | |
| {/* Space between fixed and active */} | |
| {fixedText && activeText && ' '} | |
| {/* Active text (cyan dim) - current partial transcription */} | |
| {activeText && ( | |
| <span className="text-cyan-400 opacity-80"> | |
| {activeText} | |
| </span> | |
| )} | |
| </div> | |
| {/* Legend + Auto-scroll Toggle */} | |
| <div className="mt-4 pt-4 border-t border-gray-700 flex items-center justify-between"> | |
| <div className="flex gap-6 text-sm"> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-4 h-4 bg-yellow-400 rounded"></div> | |
| <span className="text-gray-300">Fixed sentences</span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-4 h-4 bg-cyan-400 opacity-80 rounded"></div> | |
| <span className="text-gray-300">Active transcription</span> | |
| </div> | |
| </div> | |
| {/* Auto-scroll Toggle */} | |
| {onAutoScrollToggle && ( | |
| <button | |
| onClick={onAutoScrollToggle} | |
| className={`px-3 py-1 rounded text-xs font-medium transition-all duration-200 ${ | |
| autoScroll | |
| ? 'bg-cyan-900/20 border border-cyan-700/50 text-cyan-400 hover:bg-cyan-900/30' | |
| : 'bg-gray-800 border border-gray-600 text-gray-400 hover:bg-gray-700' | |
| }`} | |
| title={autoScroll ? 'Disable auto-scroll to read from top' : 'Enable auto-scroll to follow live transcription'} | |
| > | |
| {autoScroll ? '🔒 Auto-scroll' : '🔓 Scroll locked'} | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| {/* Technical Details */} | |
| <div className="mt-4 text-xs text-gray-500 text-center"> | |
| <p> | |
| Smart progressive streaming: Growing window (0-15s) → Sentence-aware sliding (>15s) | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| } | |