37 KiB
CLAUDE.md - YouTube Summarizer Frontend
This file provides guidance to Claude Code when working with the YouTube Summarizer frontend application.
🚨 CRITICAL: Server Status Checking Protocol
MANDATORY: Always check server status before testing or debugging:
# ALWAYS check server status FIRST
lsof -i :3002 | grep LISTEN # Check if frontend is running
lsof -i :8000 | grep LISTEN # Check if backend is running
# If servers are NOT running, restart them:
./scripts/restart-frontend.sh # Restart frontend
./scripts/restart-backend.sh # Restart backend
./scripts/restart-both.sh # Restart both
# After making code changes:
1. ALWAYS restart the appropriate server
2. VERIFY the server is running (lsof -i)
3. ONLY THEN proceed with testing
Key Rules:
- ✅ ALWAYS check server status before testing
- ✅ ALWAYS restart after code changes
- ✅ ALWAYS verify restart was successful
- ❌ NEVER assume servers are running
- ❌ NEVER test without confirming server status
🚨 CRITICAL: Documentation Preservation Rule
MANDATORY: NEVER remove critical documentation sections:
- ❌ NEVER remove existing critical sections from CLAUDE.md or AGENTS.md
- ❌ NEVER delete server checking protocols
- ❌ NEVER remove development standards sections
- ❌ NEVER delete troubleshooting guides
- ✅ ONLY remove sections when explicitly instructed by the user
- ✅ ALWAYS preserve and build upon existing documentation
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.