← Tasks
[UI-03] TranscriptOverlay — real-time scrolling transcript (retry 1) (retry 2)
completedcode_genP1
Description
## UI-03: TranscriptOverlay — real-time scrolling transcript
Section: UI Components | Milestone: M4 | Owner: AG Lu
### Failing Tests
- **transcript_overlay** (file_exists): File `c8-audio/components/TranscriptOverlay.tsx` must exist
- **transcript_data** (grep): Pattern `transcript|caption|subtitle` in `c8-audio/components/TranscriptOverlay.tsx` (need 1+ matches)
### Acceptance Criteria
- File `c8-audio/components/TranscriptOverlay.tsx` must exist
- File matching `c8-audio/components/TranscriptOverlay.tsx` must contain pattern `transcript|caption|subtitle`
### Codebase Conventions
- Trilingual comments: // EN: ... // VI: ... // ES: ...
- SPEC section refs in comments
- Zod at module level with .safeParse()
- (supabase as any) with eslint-disable
- NextResponse.json() responses
- Error format: { error: 'message' }
### Dependencies
- LK-02: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
1d ago
Completed
1d ago
Quality
Overall
99%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 300,
"estimated_usd": 0.040545,
"output_tokens": 2643
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 33399,
"response_text": "I'll create the TranscriptOverlay component for real-time scrolling transcript display.\n\n```tsx\n// c8-audio/components/TranscriptOverlay.tsx\n'use client';\n\nimport React, { useState, useEffect, useRef } from 'react';\nimport { z } from 'zod';\n\n// EN: Schema for transcript segment data\n// VI: Schema cho dữ liệu đoạn transcript\n// ES: Schema para datos de segmento de transcripción\nconst TranscriptSegmentSchema = z.object({\n id: z.string(),\n text: z.string(),\n startTime: z.number(),\n endTime: z.number(),\n speaker: z.string().optional(),\n confidence: z.number().min(0).max(1).optional()\n});\n\nconst TranscriptDataSchema = z.object({\n segments: z.array(TranscriptSegmentSchema),\n currentTime: z.number().min(0),\n isLive: z.boolean().optional().default(false)\n});\n\ntype TranscriptSegment = z.infer<typeof TranscriptSegmentSchema>;\ntype TranscriptData = z.infer<typeof TranscriptDataSchema>;\n\ninterface TranscriptOverlayProps {\n transcriptData: TranscriptData;\n isVisible: boolean;\n onToggleVisibility: () => void;\n className?: string;\n autoScroll?: boolean;\n showTimestamps?: boolean;\n showSpeakers?: boolean;\n}\n\n// EN: Real-time scrolling transcript overlay component - SPEC UI-03\n// VI: Component overlay transcript cuộn thời gian thực - SPEC UI-03 \n// ES: Componente de superposición de transcripción con desplazamiento en tiempo real - SPEC UI-03\nexport default function TranscriptOverlay({\n transcriptData,\n isVisible,\n onToggleVisibility,\n className = '',\n autoScroll = true,\n showTimestamps = false,\n showSpeakers = true\n}: TranscriptOverlayProps) {\n const [validatedData, setValidatedData] = useState<TranscriptData | null>(null);\n const [activeSegmentId, setActiveSegmentId] = useState<string | null>(null);\n const scrollContainerRef = useRef<HTMLDivElement>(null);\n const activeSegmentRef = useRef<HTMLDivElement>(null);\n\n // EN: Validate transcript data on prop changes\n // VI: Xác thực dữ liệu transcript khi props thay đổi\n // ES: Validar datos de transcripción cuando cambian las props\n useEffect(() => {\n const result = TranscriptDataSchema.safeParse(transcriptData);\n if (result.success) {\n setValidatedData(result.data);\n } else {\n console.error('Invalid transcript data:', result.error);\n setValidatedData(null);\n }\n }, [transcriptData]);\n\n // EN: Update active segment based on current time\n // VI: Cập nhật đoạn đang hoạt động dựa trên thời gian hiện tại\n // ES: Actualizar segmento activo basado en el tiempo actual\n useEffect(() => {\n if (!validatedData) return;\n\n const currentSegment = validatedData.segments.find(segment => \n validatedData.currentTime >= segment.startTime && \n validatedData.currentTime <= segment.endTime\n );\n\n setActiveSegmentId(currentSegment?.id || null);\n }, [validatedData]);\n\n // EN: Auto-scroll to active segment\n // VI: Tự động cuộn đến đoạn đang hoạt động\n // ES: Desplazamiento automático al segmento activo\n useEffect(() => {\n if (autoScroll && activeSegmentRef.current && scrollContainerRef.current) {\n activeSegmentRef.current.scrollIntoView({\n behavior: 'smooth',\n block: 'center'\n });\n }\n }, [activeSegmentId, autoScroll]);\n\n // EN: Format time for display\n // VI: Định dạng thời gian để hiển thị\n // ES: Formatear tiempo para mostrar\n const formatTime = (seconds: number): string => {\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n };\n\n // EN: Get confidence color class\n // VI: Lấy class màu độ tin cậy\n // ES: Obtener clase de color de confianza\n const getConfidenceColor = (confidence?: number): string => {\n if (!confidence) return '';\n if (confidence >= 0.8) return 'text-green-600';\n if (confidence >= 0.6) return 'text-yellow-600';\n return 'text-red-600';\n };\n\n if (!isVisible) return null;\n\n return (\n <div className={`transcript-overlay ${className}`}>\n {/* EN: Overlay header with controls */}\n {/* VI: Header overlay với các điều khiển */}\n {/* ES: Encabezado de superposición con controles */}\n <div className=\"transcript-header bg-black bg-opacity-75 text-white p-3 flex justify-between items-center\">\n <div className=\"flex items-center gap-4\">\n <h3 className=\"text-lg font-semibold\">\n {validatedData?.isLive ? 'Live Transcript' : 'Transcript'}\n </h3>\n {validatedData?.isLive && (\n <div className=\"flex items-center gap-2\">\n <div className=\"w-2 h-2 bg-red-500 rounded-full animate-pulse\"></div>\n <span className=\"text-sm\">LIVE</span>\n </div>\n )}\n </div>\n \n <div className=\"flex items-center gap-2\">\n <button\n onClick={onToggleVisibility}\n className=\"text-white hover:text-gray-300 text-xl\"\n aria-label=\"Close transcript\"\n >\n ×\n </button>\n </div>\n </div>\n\n {/* EN: Scrollable transcript content */}\n {/* VI: Nội dung transcript có thể cuộn */}\n {/* ES: Contenido de transcripción desplazable */}\n <div \n ref={scrollContainerRef}\n className=\"transcript-content bg-black bg-opacity-60 text-white p-4 max-h-96 overflow-y-auto\"\n >\n {!validatedData ? (\n <div className=\"text-center text-gray-400\">\n Invalid transcript data\n </div>\n ) : validatedData.segments.length === 0 ? (\n <div className=\"text-center text-gray-400\">\n {validatedData.isLive ? 'Waiting for transcript...' : 'No transcript available'}\n </div>\n ) : (\n <div className=\"space-y-2\">\n {validatedData.segments.map((segment) => (\n <div\n key={segment.id}\n ref={activeSegmentId === segment.id ? activeSegmentRef : null}\n className={`transcript-segment p-2 rounded transition-colors duration-200 ${\n activeSegmentId === segment.id \n ? 'bg-blue-600 bg-opacity-50' \n : 'hover:bg-white hover:bg-opacity-10'\n }`}\n >\n {/* EN: Segment metadata */}\n {/* VI: Metadata của đoạn */}\n {/* ES: Metadatos del segmento */}\n {(showTimestamps || showSpeakers) && (\n <div className=\"segment-meta flex items-center gap-3 text-xs text-gray-300 mb-1\">\n {showTimestamps && (\n <span className=\"timestamp\">\n {formatTime(segment.startTime)}\n </span>\n )}\n {showSpeakers && segment.speaker && (\n <span className=\"speaker font-medium\">\n {segment.speaker}:\n </span>\n )}\n {segment.confidence && (\n <span className={`confidence ${getConfidenceColor(segment.confidence)}`}>\n ({Math.round(segment.confidence * 100)}%)\n </span>\n )}\n </div>\n )}\n\n {/* EN: Segment text content */}\n {/* VI: Nội dung văn bản của đoạn */}\n {/* ES: Contenido de texto del segmento */}\n <div className=\"segment-text leading-relaxed\">\n {segment.text}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n\n {/* EN: Caption/subtitle display for accessibility */}\n {/* VI: Hiển thị caption/subtitle cho khả năng tiếp cận */}\n {/* ES: Visualización de subtítulos/caption para accesibilidad */}\n {validatedData && activeSegmentId && (\n <div className=\"caption-display sr-only\" aria-live=\"polite\">\n {validatedData.segments.find(s => s.id === activeSegmentId)?.text}\n </div>\n )}\n </div>\n );\n}\n\n// EN: Export types for external use\n// VI: Export các type để sử dụng bên ngoài\n// ES: Exportar tipos para uso externo\nexport type { TranscriptOverlayProps, TranscriptSegment, TranscriptData };\n```\n\nThis TranscriptOverlay component provides:\n\n1. **Real-time transcript display** with auto-scrolling to active segments\n2. **Live transcript support** with visual indicators \n3. **Zod validation** at module level with `.safeParse()`\n4. **Trilingual comments** following the convention\n5. **SPEC section references** in comments\n6. **Accessibility features** including screen reader support\n7. **Flexible display options** for timestamps, speakers, and confidence scores\n8. **Smooth scrolling** and visual highlighting of current segments\n9. **Pattern matching** for transcript/caption/subtitle keywords as required by tests\n\nThe component handles both live and pre-recorded transcript data, with proper validation and error handling."
}Task ID: ba6de73f-412d-42e4-81fe-b105268a4700