youtube-summarizer/frontend/CLAUDE.md

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-comparison showcasing 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: /dashboardSummarizePage.tsx (requires verified user)
  • History: /historySummaryHistoryPage.tsx (requires verified user)
  • Batch: /batchBatchProcessingPage.tsx (requires verified user)
  • Login/Auth: /login, /register, /verify-email etc.

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:

  1. Fixed tsconfig.app.json - Removed overly strict verbatimModuleSyntax setting that caused module resolution failures
  2. Corrected Path Aliases - Synchronized Vite and TypeScript path mapping for @/* imports
  3. Added Node.js Types - Included @types/node for proper import.meta.env support
  4. 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:

  1. Import/Export Resolution - SummaryCard.tsx importing Summary from summaryAPI.ts

    • Issue: Module not providing export despite correct syntax
    • Solution: Simplified summaryAPI.ts to remove circular dependencies
  2. Vite vs TypeScript Alignment - Path aliases not resolving

    • Issue: @/services/summaryAPI not found despite existing file
    • Solution: Ensure vite.config.ts aliases match tsconfig.json paths
  3. Development vs Production - Dev server working but build failing

    • Issue: Vite bypasses TypeScript errors in development
    • Solution: Regular npm run build checks 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.