generating-progress.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. 'use client';
  2. import { useEffect, useState } from 'react';
  3. import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
  4. import { Loader2, CheckCircle2, XCircle, Circle } from 'lucide-react';
  5. import { useI18n } from '@/lib/hooks/use-i18n';
  6. interface GeneratingProgressProps {
  7. outlineReady: boolean; // Is outline generation complete?
  8. firstPageReady: boolean; // Is first page generated?
  9. statusMessage: string;
  10. error?: string | null;
  11. }
  12. // Status item component - declared outside main component
  13. function StatusItem({
  14. completed,
  15. inProgress,
  16. hasError,
  17. label,
  18. }: {
  19. completed: boolean;
  20. inProgress: boolean;
  21. hasError: boolean;
  22. label: string;
  23. }) {
  24. return (
  25. <div className="flex items-center gap-3 py-3">
  26. <div className="flex-shrink-0">
  27. {hasError ? (
  28. <XCircle className="size-6 text-destructive" />
  29. ) : completed ? (
  30. <CheckCircle2 className="size-6 text-green-500" />
  31. ) : inProgress ? (
  32. <Loader2 className="size-6 text-primary animate-spin" />
  33. ) : (
  34. <Circle className="size-6 text-muted-foreground" />
  35. )}
  36. </div>
  37. <span
  38. className={`text-base ${
  39. hasError
  40. ? 'text-destructive'
  41. : completed
  42. ? 'text-green-600 font-medium'
  43. : inProgress
  44. ? 'text-primary font-medium'
  45. : 'text-muted-foreground'
  46. }`}
  47. >
  48. {label}
  49. </span>
  50. </div>
  51. );
  52. }
  53. export function GeneratingProgress({
  54. outlineReady,
  55. firstPageReady,
  56. statusMessage,
  57. error,
  58. }: GeneratingProgressProps) {
  59. const { t } = useI18n();
  60. const [dots, setDots] = useState('');
  61. // Animated dots for loading state
  62. useEffect(() => {
  63. if (!error && !firstPageReady) {
  64. const interval = setInterval(() => {
  65. setDots((prev) => (prev.length >= 3 ? '' : prev + '.'));
  66. }, 500);
  67. return () => clearInterval(interval);
  68. }
  69. }, [error, firstPageReady]);
  70. return (
  71. <div className="space-y-6">
  72. <Card>
  73. <CardHeader>
  74. <CardTitle className="flex items-center gap-2">
  75. {error ? (
  76. <>
  77. <XCircle className="size-5 text-destructive" />
  78. {t('generation.generationFailed')}
  79. </>
  80. ) : firstPageReady ? (
  81. <>
  82. <CheckCircle2 className="size-5 text-green-500" />
  83. {t('generation.openingClassroom')}
  84. </>
  85. ) : (
  86. <>
  87. <Loader2 className="size-5 animate-spin" />
  88. {t('generation.generatingCourse')}
  89. {dots}
  90. </>
  91. )}
  92. </CardTitle>
  93. </CardHeader>
  94. <CardContent className="space-y-4">
  95. {/* Two milestone status items */}
  96. <div className="divide-y">
  97. <StatusItem
  98. completed={outlineReady}
  99. inProgress={!outlineReady && !error}
  100. hasError={!outlineReady && !!error}
  101. label={
  102. outlineReady ? t('generation.outlineReady') : t('generation.generatingOutlines')
  103. }
  104. />
  105. <StatusItem
  106. completed={firstPageReady}
  107. inProgress={outlineReady && !firstPageReady && !error}
  108. hasError={outlineReady && !firstPageReady && !!error}
  109. label={
  110. firstPageReady
  111. ? t('generation.firstPageReady')
  112. : t('generation.generatingFirstPage')
  113. }
  114. />
  115. </div>
  116. {/* Status message */}
  117. {statusMessage && !error && (
  118. <div className="pt-2 border-t">
  119. <p className="text-sm text-muted-foreground">{statusMessage}</p>
  120. </div>
  121. )}
  122. {/* Error message */}
  123. {error && (
  124. <div className="p-4 bg-destructive/10 border border-destructive/20 rounded-lg">
  125. <p className="text-sm text-destructive">{error}</p>
  126. </div>
  127. )}
  128. </CardContent>
  129. </Card>
  130. </div>
  131. );
  132. }