youtube-summarizer/frontend/CLAUDE.md

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-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.