|
|
|
|
| import { useEffect, useState } from 'react'; |
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; |
| import { Loader2, CheckCircle2, XCircle, Circle } from 'lucide-react'; |
| import { useI18n } from '@/lib/hooks/use-i18n'; |
|
|
| interface GeneratingProgressProps { |
| outlineReady: boolean; |
| firstPageReady: boolean; |
| statusMessage: string; |
| error?: string | null; |
| } |
|
|
| |
| function StatusItem({ |
| completed, |
| inProgress, |
| hasError, |
| label, |
| }: { |
| completed: boolean; |
| inProgress: boolean; |
| hasError: boolean; |
| label: string; |
| }) { |
| return ( |
| <div className="flex items-center gap-3 py-3"> |
| <div className="flex-shrink-0"> |
| {hasError ? ( |
| <XCircle className="size-6 text-destructive" /> |
| ) : completed ? ( |
| <CheckCircle2 className="size-6 text-green-500" /> |
| ) : inProgress ? ( |
| <Loader2 className="size-6 text-primary animate-spin" /> |
| ) : ( |
| <Circle className="size-6 text-muted-foreground" /> |
| )} |
| </div> |
| <span |
| className={`text-base ${ |
| hasError |
| ? 'text-destructive' |
| : completed |
| ? 'text-green-600 font-medium' |
| : inProgress |
| ? 'text-primary font-medium' |
| : 'text-muted-foreground' |
| }`} |
| > |
| {label} |
| </span> |
| </div> |
| ); |
| } |
|
|
| export function GeneratingProgress({ |
| outlineReady, |
| firstPageReady, |
| statusMessage, |
| error, |
| }: GeneratingProgressProps) { |
| const { t } = useI18n(); |
| const [dots, setDots] = useState(''); |
|
|
| |
| useEffect(() => { |
| if (!error && !firstPageReady) { |
| const interval = setInterval(() => { |
| setDots((prev) => (prev.length >= 3 ? '' : prev + '.')); |
| }, 500); |
| return () => clearInterval(interval); |
| } |
| }, [error, firstPageReady]); |
|
|
| return ( |
| <div className="space-y-6"> |
| <Card> |
| <CardHeader> |
| <CardTitle className="flex items-center gap-2"> |
| {error ? ( |
| <> |
| <XCircle className="size-5 text-destructive" /> |
| {t('generation.generationFailed')} |
| </> |
| ) : firstPageReady ? ( |
| <> |
| <CheckCircle2 className="size-5 text-green-500" /> |
| {t('generation.openingClassroom')} |
| </> |
| ) : ( |
| <> |
| <Loader2 className="size-5 animate-spin" /> |
| {t('generation.generatingCourse')} |
| {dots} |
| </> |
| )} |
| </CardTitle> |
| </CardHeader> |
| <CardContent className="space-y-4"> |
| {/* Two milestone status items */} |
| <div className="divide-y"> |
| <StatusItem |
| completed={outlineReady} |
| inProgress={!outlineReady && !error} |
| hasError={!outlineReady && !!error} |
| label={ |
| outlineReady ? t('generation.outlineReady') : t('generation.generatingOutlines') |
| } |
| /> |
| <StatusItem |
| completed={firstPageReady} |
| inProgress={outlineReady && !firstPageReady && !error} |
| hasError={outlineReady && !firstPageReady && !!error} |
| label={ |
| firstPageReady |
| ? t('generation.firstPageReady') |
| : t('generation.generatingFirstPage') |
| } |
| /> |
| </div> |
| |
| {/* Status message */} |
| {statusMessage && !error && ( |
| <div className="pt-2 border-t"> |
| <p className="text-sm text-muted-foreground">{statusMessage}</p> |
| </div> |
| )} |
| |
| {/* Error message */} |
| {error && ( |
| <div className="p-4 bg-destructive/10 border border-destructive/20 rounded-lg"> |
| <p className="text-sm text-destructive">{error}</p> |
| </div> |
| )} |
| </CardContent> |
| </Card> |
| </div> |
| ); |
| } |
|
|