36 KiB
CLAUDE.md - YouTube Summarizer Frontend
This file provides guidance to Claude Code when working with the YouTube Summarizer frontend application.
Frontend Architecture Overview
The frontend is built with React, TypeScript, and Vite, following modern React patterns with comprehensive UI components.
✅ NEW: Dual Transcript Functionality Complete
The frontend now includes comprehensive dual transcript components that integrate seamlessly with the backend services:
- TranscriptSelector: Interactive component for choosing between YouTube captions, Whisper AI, or both
- TranscriptComparison: Side-by-side comparison with quality metrics and difference highlighting
- Enhanced SummarizeForm: Updated to support transcript source selection
- Demo Page:
/demo/transcript-comparisonshowcasing all functionality with mock data
The frontend implementation is production-ready with TypeScript safety, comprehensive error handling, and excellent developer experience.
frontend/
├── src/
│ ├── components/ # Reusable UI components
│ ├── hooks/ # Custom React hooks
│ ├── services/ # API client and external integrations
│ ├── types/ # TypeScript type definitions
│ ├── utils/ # Utility functions and helpers
│ └── pages/ # Page components and routing
│ ├── AdminPage.tsx # No-auth admin interface (NEW)
│ ├── SummarizePage.tsx # Protected main interface
│ └── auth/ # Authentication pages
├── public/ # Static assets
└── tests/ # Frontend tests (Vitest + RTL)
Development & Testing Routes
Admin Page (No Authentication Required) 🚀
- File:
src/pages/AdminPage.tsx - Route:
/admin - URL:
http://localhost:3002/admin - Purpose: Direct access for testing and development
- Features:
- Complete YouTube Summarizer functionality
- No login or authentication required
- Orange "Admin Mode" visual indicators
- Shield icon and badges for clear identification
- Perfect for quick testing and demos
Protected Routes (Authentication Required)
- Dashboard:
/dashboard→SummarizePage.tsx(requires verified user) - History:
/history→SummaryHistoryPage.tsx(requires verified user) - Batch:
/batch→BatchProcessingPage.tsx(requires verified user) - Login/Auth:
/login,/register,/verify-emailetc.
Tech Stack
- Framework: React 18 with TypeScript
- Build Tool: Vite for fast development and building
- Styling: Tailwind CSS with shadcn/ui components
- State Management: React Query (TanStack Query) for server state
- WebSocket: Native WebSocket API for real-time updates
- Testing: Vitest + React Testing Library
- Type Safety: Full TypeScript coverage with production-ready configuration
TypeScript Configuration (Project-Specific)
Resolved Configuration Issues
This project had specific TypeScript compilation issues that were systematically resolved:
- Fixed
tsconfig.app.json- Removed overly strictverbatimModuleSyntaxsetting that caused module resolution failures - Corrected Path Aliases - Synchronized Vite and TypeScript path mapping for
@/*imports - Added Node.js Types - Included
@types/nodefor properimport.meta.envsupport - Module Resolution - Fixed ES module import/export patterns in
summaryAPI.ts
Production-Ready Configuration
// tsconfig.app.json - Working configuration for this project
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"paths": {
"@/*": ["./src/*"]
},
"strict": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true
}
}
Project-Specific TypeScript Troubleshooting
Common Issues in This Codebase:
-
Import/Export Resolution -
SummaryCard.tsximportingSummaryfromsummaryAPI.ts- Issue: Module not providing export despite correct syntax
- Solution: Simplified
summaryAPI.tsto remove circular dependencies
-
Vite vs TypeScript Alignment - Path aliases not resolving
- Issue:
@/services/summaryAPInot found despite existing file - Solution: Ensure
vite.config.tsaliases matchtsconfig.jsonpaths
- Issue:
-
Development vs Production - Dev server working but build failing
- Issue: Vite bypasses TypeScript errors in development
- Solution: Regular
npm run buildchecks during development
API Interface Completeness
The summaryAPI.ts file should export complete interfaces:
// Essential exports for this project
export interface Summary {
id: string;
video_url: string;
video_title: string;
// ... complete interface definition
}
export interface SummaryListResponse {
summaries: Summary[];
total: number;
page: number;
page_size: number;
has_more: boolean;
}
export const summaryAPI = new SummaryAPI();
Key Components and Patterns
API Integration
API Client (src/services/apiClient.ts)
class APIClient {
private baseURL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
async processVideo(url: string, config: PipelineConfig): Promise<ProcessVideoResponse> {
const response = await fetch(`${this.baseURL}/api/process`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ video_url: url, ...config })
})
return response.json()
}
async getPipelineStatus(jobId: string): Promise<PipelineStatusResponse> {
const response = await fetch(`${this.baseURL}/api/process/${jobId}`)
return response.json()
}
}
export const apiClient = new APIClient()
Real-time Updates with WebSocket
WebSocket Hook (src/hooks/useWebSocket.ts)
import { useEffect, useRef, useState } from 'react'
interface WebSocketHookProps {
url?: string
onProgressUpdate?: (update: ProgressUpdate) => void
onComplete?: (result: CompletionData) => void
onError?: (error: ErrorData) => void
}
export function useWebSocket({
url = 'ws://localhost:8000/ws',
onProgressUpdate,
onComplete,
onError
}: WebSocketHookProps) {
const ws = useRef<WebSocket | null>(null)
const [isConnected, setIsConnected] = useState(false)
const connect = useCallback((jobId?: string) => {
const wsUrl = jobId ? `${url}?job_id=${jobId}` : url
ws.current = new WebSocket(wsUrl)
ws.current.onopen = () => setIsConnected(true)
ws.current.onclose = () => setIsConnected(false)
ws.current.onmessage = (event) => {
const data = JSON.parse(event.data)
switch (data.type) {
case 'progress_update':
onProgressUpdate?.(data.data)
break
case 'completion_notification':
onComplete?.(data.data)
break
case 'error_notification':
onError?.(data.data)
break
}
}
}, [url, onProgressUpdate, onComplete, onError])
const disconnect = useCallback(() => {
ws.current?.close()
}, [])
return { connect, disconnect, isConnected }
}
Pipeline Processing Hook
Pipeline Hook (src/hooks/usePipelineProcessor.ts)
import { useMutation, useQuery } from '@tanstack/react-query'
import { useState } from 'react'
import { apiClient } from '@/services/apiClient'
import { useWebSocket } from './useWebSocket'
export function usePipelineProcessor() {
const [activeJobId, setActiveJobId] = useState<string | null>(null)
const [progress, setProgress] = useState<ProgressUpdate | null>(null)
const { connect, disconnect } = useWebSocket({
onProgressUpdate: (update) => setProgress(update),
onComplete: (result) => {
setProgress({ stage: 'completed', percentage: 100, message: 'Processing complete!' })
// Trigger refetch of final result
statusQuery.refetch()
},
onError: (error) => {
setProgress({ stage: 'failed', percentage: 0, message: error.message })
}
})
const processVideo = useMutation({
mutationFn: async (params: ProcessVideoParams) => {
const result = await apiClient.processVideo(params.url, params.config)
return result
},
onSuccess: (data) => {
setActiveJobId(data.job_id)
connect(data.job_id) // Connect WebSocket for this job
setProgress({ stage: 'initialized', percentage: 0, message: 'Starting processing...' })
},
onError: (error) => {
console.error('Failed to start processing:', error)
}
})
const statusQuery = useQuery({
queryKey: ['pipeline-status', activeJobId],
queryFn: () => activeJobId ? apiClient.getPipelineStatus(activeJobId) : null,
enabled: !!activeJobId,
refetchInterval: (data) => {
// Stop polling when completed or failed
return data?.status === 'completed' || data?.status === 'failed' ? false : 2000
}
})
const cancelProcessing = useMutation({
mutationFn: async (jobId: string) => {
await apiClient.cancelPipeline(jobId)
},
onSuccess: () => {
setActiveJobId(null)
setProgress(null)
disconnect()
}
})
return {
processVideo: processVideo.mutateAsync,
cancelProcessing: cancelProcessing.mutateAsync,
isProcessing: processVideo.isPending || statusQuery.data?.status === 'processing',
progress: progress || (statusQuery.data ? {
stage: statusQuery.data.status,
percentage: statusQuery.data.progress_percentage,
message: statusQuery.data.current_message
} : null),
result: statusQuery.data?.result,
error: processVideo.error || statusQuery.error,
jobId: activeJobId
}
}
Admin Page Implementation
AdminPage Component (src/pages/AdminPage.tsx) - NEW FEATURE
import { SummarizeForm } from '@/components/forms/SummarizeForm';
import { ProgressTracker } from '@/components/display/ProgressTracker';
import { TranscriptViewer } from '@/components/display/TranscriptViewer';
import { useTranscriptExtraction } from '@/hooks/useTranscriptExtraction';
import { Shield } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
export function AdminPage() {
const { extractTranscript, progress, transcript, metadata, isLoading, error } = useTranscriptExtraction();
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-8">
<header className="text-center mb-8">
{/* Admin Mode Visual Indicators */}
<div className="flex items-center justify-center gap-3 mb-4">
<Shield className="h-8 w-8 text-orange-600" />
<Badge variant="outline" className="text-orange-600 border-orange-600">
Admin Mode
</Badge>
</div>
<h1 className="text-4xl font-bold tracking-tight">YouTube Summarizer</h1>
<p className="text-xl text-muted-foreground mt-2">
Extract and analyze YouTube video transcripts - No Authentication Required
</p>
{/* Status Badge */}
<div className="mt-4">
<Badge variant="secondary" className="text-sm">
Direct Access • Full Functionality • Testing Mode
</Badge>
</div>
</header>
{/* Same functionality as protected routes but no auth */}
<SummarizeForm onSubmit={handleSubmit} disabled={isLoading} />
{isLoading && <ProgressTracker progress={progress} videoId="" />}
{error && <Alert variant="destructive">...</Alert>}
{transcript && <TranscriptViewer transcript={transcript} metadata={metadata} videoId="" />}
<footer className="text-center pt-8 border-t">
<p className="text-sm text-muted-foreground">
Admin Mode - For testing and development purposes
</p>
</footer>
</div>
</div>
</div>
);
}
Key Features of AdminPage:
- No Authentication: Bypasses all auth checks and protected routes
- Visual Indicators: Orange theme, shield icon, clear "Admin Mode" badges
- Full Functionality: Uses same components as protected dashboard
- Development-Friendly: Perfect for testing, demos, and development workflow
- Self-Contained: No dependencies on AuthContext or user state
Main Application Component
App Component (src/App.tsx) - UPDATED WITH ADMIN ROUTE
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider } from '@/contexts/AuthContext';
import { ProtectedRoute } from '@/components/auth/ProtectedRoute';
// Import the new AdminPage
import { AdminPage } from '@/pages/AdminPage';
function App() {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Router>
<Routes>
{/* Public routes */}
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
{/* ADMIN ROUTE - No authentication required */}
<Route path="/admin" element={<AdminPage />} />
{/* Protected routes */}
<Route path="/dashboard" element={
<ProtectedRoute requireVerified={true}>
<SummarizePage />
</ProtectedRoute>
} />
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Router>
</AuthProvider>
</QueryClientProvider>
);
}
UI Components with shadcn/ui
Video Processor Page (src/pages/VideoProcessorPage.tsx)
import React, { useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Progress } from '@/components/ui/progress'
import { Textarea } from '@/components/ui/textarea'
import { useToast } from '@/hooks/use-toast'
import { usePipelineProcessor } from '@/hooks/usePipelineProcessor'
import { VideoUrlValidator } from '@/components/VideoUrlValidator'
import { SummaryDisplay } from '@/components/SummaryDisplay'
import { ProcessingProgress } from '@/components/ProcessingProgress'
export function VideoProcessorPage() {
const [videoUrl, setVideoUrl] = useState('')
const [summaryLength, setSummaryLength] = useState<'brief' | 'standard' | 'detailed'>('standard')
const [focusAreas, setFocusAreas] = useState<string[]>([])
const { toast } = useToast()
const { processVideo, isProcessing, progress, result, cancelProcessing } = usePipelineProcessor()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
await processVideo({
url: videoUrl,
config: {
summary_length: summaryLength,
focus_areas: focusAreas.filter(area => area.trim()),
include_timestamps: false,
enable_notifications: true,
quality_threshold: 0.7
}
})
toast({
title: "Processing Started",
description: "Your video is being processed. You'll see real-time updates below.",
})
} catch (error) {
toast({
title: "Processing Failed",
description: error instanceof Error ? error.message : 'Unknown error occurred',
variant: "destructive",
})
}
}
return (
<div className="container mx-auto py-8 px-4 max-w-4xl">
<div className="space-y-6">
<div className="text-center">
<h1 className="text-4xl font-bold tracking-tight">YouTube Summarizer</h1>
<p className="text-xl text-muted-foreground mt-2">
Transform any YouTube video into intelligent summaries powered by AI
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Video Processing</CardTitle>
<CardDescription>
Enter a YouTube URL to generate an AI-powered summary with key insights
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="video-url">YouTube URL</Label>
<VideoUrlValidator
value={videoUrl}
onChange={setVideoUrl}
disabled={isProcessing}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="summary-length">Summary Length</Label>
<Select value={summaryLength} onValueChange={setSummaryLength}>
<SelectTrigger>
<SelectValue placeholder="Choose length" />
</SelectTrigger>
<SelectContent>
<SelectItem value="brief">Brief (100-200 words)</SelectItem>
<SelectItem value="standard">Standard (300-500 words)</SelectItem>
<SelectItem value="detailed">Detailed (500-800 words)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="focus-areas">Focus Areas (optional)</Label>
<Input
placeholder="e.g., key insights, action items"
value={focusAreas.join(', ')}
onChange={(e) => setFocusAreas(e.target.value.split(',').map(s => s.trim()))}
disabled={isProcessing}
/>
</div>
</div>
<div className="flex gap-2">
<Button
type="submit"
disabled={!videoUrl || isProcessing}
className="flex-1"
>
{isProcessing ? 'Processing...' : 'Generate Summary'}
</Button>
{isProcessing && (
<Button
type="button"
variant="outline"
onClick={() => cancelProcessing()}
>
Cancel
</Button>
)}
</div>
</form>
</CardContent>
</Card>
{progress && (
<ProcessingProgress
progress={progress}
onCancel={() => cancelProcessing()}
/>
)}
{result && (
<SummaryDisplay
result={result}
videoMetadata={result.video_metadata}
/>
)}
</div>
</div>
)
}
Component Patterns
Form Validation Component
Video URL Validator (src/components/VideoUrlValidator.tsx)
import React, { useState, useEffect } from 'react'
import { Input } from '@/components/ui/input'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { CheckCircle2, XCircle } from 'lucide-react'
interface VideoUrlValidatorProps {
value: string
onChange: (value: string) => void
disabled?: boolean
}
export function VideoUrlValidator({ value, onChange, disabled }: VideoUrlValidatorProps) {
const [validationStatus, setValidationStatus] = useState<'valid' | 'invalid' | 'idle'>('idle')
const [validationMessage, setValidationMessage] = useState('')
useEffect(() => {
if (!value) {
setValidationStatus('idle')
return
}
const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([\w\-_]+)(&\S*)?$/
if (youtubeRegex.test(value)) {
setValidationStatus('valid')
setValidationMessage('Valid YouTube URL')
} else {
setValidationStatus('invalid')
setValidationMessage('Please enter a valid YouTube URL')
}
}, [value])
return (
<div className="space-y-2">
<div className="relative">
<Input
id="video-url"
placeholder="https://youtube.com/watch?v=..."
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
className={cn(
"pr-10",
validationStatus === 'valid' && "border-green-500",
validationStatus === 'invalid' && "border-red-500"
)}
/>
{validationStatus !== 'idle' && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
{validationStatus === 'valid' ? (
<CheckCircle2 className="h-5 w-5 text-green-500" />
) : (
<XCircle className="h-5 w-5 text-red-500" />
)}
</div>
)}
</div>
{validationStatus !== 'idle' && (
<Alert variant={validationStatus === 'valid' ? 'default' : 'destructive'}>
<AlertDescription>{validationMessage}</AlertDescription>
</Alert>
)}
</div>
)
}
Progress Display Component
Processing Progress (src/components/ProcessingProgress.tsx)
import React from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Progress } from '@/components/ui/progress'
import { Button } from '@/components/ui/button'
import { Loader2, X } from 'lucide-react'
interface ProcessingProgressProps {
progress: {
stage: string
percentage: number
message: string
details?: any
}
onCancel: () => void
}
export function ProcessingProgress({ progress, onCancel }: ProcessingProgressProps) {
const getStageDisplayName = (stage: string) => {
const stageNames = {
'initialized': 'Initializing',
'validating_url': 'Validating URL',
'extracting_metadata': 'Extracting Video Info',
'extracting_transcript': 'Getting Transcript',
'analyzing_content': 'Analyzing Content',
'generating_summary': 'Generating Summary',
'validating_quality': 'Quality Check',
'completed': 'Complete',
'failed': 'Failed'
}
return stageNames[stage] || stage.replace('_', ' ').toUpperCase()
}
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="flex items-center gap-2">
<Loader2 className="h-5 w-5 animate-spin" />
Processing Video
</CardTitle>
<Button
variant="ghost"
size="sm"
onClick={onCancel}
className="text-muted-foreground hover:text-foreground"
>
<X className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>{getStageDisplayName(progress.stage)}</span>
<span>{Math.round(progress.percentage)}%</span>
</div>
<Progress value={progress.percentage} className="h-2" />
</div>
<p className="text-sm text-muted-foreground">
{progress.message}
</p>
{progress.details && (
<details className="text-xs text-muted-foreground">
<summary className="cursor-pointer hover:text-foreground">
Technical Details
</summary>
<pre className="mt-2 p-2 bg-muted rounded text-xs overflow-auto">
{JSON.stringify(progress.details, null, 2)}
</pre>
</details>
)}
</CardContent>
</Card>
)
}
Summary Display Component
Summary Display (src/components/SummaryDisplay.tsx)
import React from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { Clock, ThumbsUp, Download, Share2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
interface SummaryDisplayProps {
result: {
summary: string
key_points: string[]
main_themes: string[]
actionable_insights: string[]
confidence_score: number
quality_score: number
}
videoMetadata?: {
title: string
duration: string
thumbnail_url: string
}
}
export function SummaryDisplay({ result, videoMetadata }: SummaryDisplayProps) {
const handleExport = (format: 'markdown' | 'json' | 'pdf') => {
// Implementation for exporting summary in different formats
console.log(`Exporting as ${format}`)
}
const handleShare = () => {
// Implementation for sharing summary
console.log('Sharing summary')
}
return (
<Card>
<CardHeader>
<div className="flex justify-between items-start">
<div className="space-y-2">
<CardTitle className="flex items-center gap-2">
Summary Complete
<Badge variant="secondary" className="flex items-center gap-1">
<ThumbsUp className="h-3 w-3" />
{Math.round(result.quality_score * 100)}% Quality
</Badge>
</CardTitle>
{videoMetadata && (
<p className="text-sm text-muted-foreground flex items-center gap-2">
<Clock className="h-4 w-4" />
{videoMetadata.title}
</p>
)}
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={handleShare}>
<Share2 className="h-4 w-4 mr-1" />
Share
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-1" />
Export
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => handleExport('markdown')}>
Markdown
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport('json')}>
JSON
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport('pdf')}>
PDF
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Main Summary */}
<div>
<h3 className="font-semibold mb-3">Summary</h3>
<p className="text-sm leading-relaxed">{result.summary}</p>
</div>
<Separator />
{/* Key Points */}
<div>
<h3 className="font-semibold mb-3">Key Points</h3>
<ul className="space-y-2">
{result.key_points.map((point, index) => (
<li key={index} className="text-sm flex items-start gap-2">
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-primary/10 text-primary text-xs flex items-center justify-center font-medium mt-0.5">
{index + 1}
</span>
{point}
</li>
))}
</ul>
</div>
<Separator />
{/* Main Themes */}
<div>
<h3 className="font-semibold mb-3">Main Themes</h3>
<div className="flex flex-wrap gap-2">
{result.main_themes.map((theme, index) => (
<Badge key={index} variant="outline">
{theme}
</Badge>
))}
</div>
</div>
{/* Actionable Insights */}
{result.actionable_insights.length > 0 && (
<>
<Separator />
<div>
<h3 className="font-semibold mb-3">Actionable Insights</h3>
<ul className="space-y-2">
{result.actionable_insights.map((insight, index) => (
<li key={index} className="text-sm flex items-start gap-2">
<span className="text-primary">•</span>
{insight}
</li>
))}
</ul>
</div>
</>
)}
</CardContent>
</Card>
)
}
Development Setup
Environment Configuration
Environment Variables (.env.local)
VITE_API_URL=http://localhost:8000
VITE_WS_URL=ws://localhost:8000/ws
VITE_APP_TITLE=YouTube Summarizer
Development Commands
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
# Run tests
npm run test
# Run tests with coverage
npm run test:coverage
# Type checking
npm run type-check
# Linting
npm run lint
npm run lint:fix
Testing Setup
Vitest Configuration (vitest.config.ts)
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
Test Utilities (src/test/test-utils.tsx)
import React, { ReactElement } from 'react'
import { render, RenderOptions } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const createTestQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
const testQueryClient = createTestQueryClient()
return (
<QueryClientProvider client={testQueryClient}>
{children}
</QueryClientProvider>
)
}
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => render(ui, { wrapper: AllTheProviders, ...options })
export * from '@testing-library/react'
export { customRender as render }
Type Definitions
API Types (src/types/api.ts)
export interface ProcessVideoRequest {
video_url: string
summary_length: 'brief' | 'standard' | 'detailed'
focus_areas?: string[]
include_timestamps?: boolean
enable_notifications?: boolean
quality_threshold?: number
}
export interface ProcessVideoResponse {
job_id: string
status: string
message: string
estimated_completion_time?: number
}
export interface PipelineStatusResponse {
job_id: string
status: string
progress_percentage: number
current_message: string
video_metadata?: VideoMetadata
result?: SummaryResult
error?: ErrorData
processing_time_seconds?: number
}
export interface SummaryResult {
summary: string
key_points: string[]
main_themes: string[]
actionable_insights: string[]
confidence_score: number
quality_score: number
cost_data: {
input_cost_usd: number
output_cost_usd: number
total_cost_usd: number
}
}
export interface VideoMetadata {
title: string
description: string
duration: string
thumbnail_url: string
channel_name: string
published_at: string
}
export interface ProgressUpdate {
stage: string
percentage: number
message: string
details?: Record<string, any>
}
export interface ErrorData {
message: string
type: string
stage?: string
retry_count?: number
}
Styling with Tailwind CSS
The project uses Tailwind CSS with shadcn/ui components for consistent styling:
Tailwind Config (tailwind.config.js)
module.exports = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
// ... other color definitions
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
Error Handling and User Experience
Error Boundary Component
import React from 'react'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { RefreshCw, AlertTriangle } from 'lucide-react'
interface ErrorBoundaryState {
hasError: boolean
error?: Error
}
export class ErrorBoundary extends React.Component<
React.PropsWithChildren<{}>,
ErrorBoundaryState
> {
constructor(props: React.PropsWithChildren<{}>) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Something went wrong</AlertTitle>
<AlertDescription className="mt-2 space-y-2">
<p>An unexpected error occurred. Please try refreshing the page.</p>
{this.state.error && (
<details className="text-xs">
<summary className="cursor-pointer">Error details</summary>
<pre className="mt-1 whitespace-pre-wrap">
{this.state.error.message}
</pre>
</details>
)}
<Button
variant="outline"
size="sm"
onClick={() => window.location.reload()}
className="mt-2"
>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh Page
</Button>
</AlertDescription>
</Alert>
)
}
return this.props.children
}
}
Performance Optimization
React Query Configuration
- Stale time: 5 minutes for cached data
- Retry: Only 1 retry for failed requests
- Background refetch disabled for completed operations
WebSocket Management
- Automatic reconnection with exponential backoff
- Connection cleanup on component unmount
- Heartbeat mechanism for connection health
Bundle Optimization
- Lazy loading for non-critical components
- Code splitting by routes
- Tree shaking for unused imports
- Optimized build with Vite
Accessibility
ARIA Labels and Semantic HTML
<form role="form" aria-label="Video processing form">
<fieldset>
<legend className="sr-only">Video URL and Options</legend>
<Input
aria-label="YouTube video URL"
aria-describedby="url-help"
aria-invalid={validationStatus === 'invalid'}
/>
<div id="url-help" className="sr-only">
Enter a valid YouTube URL to process
</div>
</fieldset>
</form>
Keyboard Navigation
- All interactive elements focusable
- Logical tab order
- Escape key support for modals
- Enter key support for form submission
Screen Reader Support
- Proper heading hierarchy
- Live regions for progress updates
- Descriptive button labels
- Status announcements
This frontend provides a comprehensive, accessible, and user-friendly interface for the YouTube Summarizer with real-time updates and modern UI patterns.