| 'use client'; |
|
|
| import { motion } from 'framer-motion'; |
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; |
| import { Progress } from '@/components/ui/progress'; |
| import { Badge } from '@/components/ui/badge'; |
| import { CheckCircle, Circle, Loader2, FileText, BarChart3, Users, Mail } from 'lucide-react'; |
| import { Workflow } from '@/lib/types'; |
| import { cn } from '@/lib/utils'; |
|
|
| interface WorkflowProgressProps { |
| workflow: Workflow; |
| } |
|
|
| const WORKFLOW_STEPS = [ |
| { |
| key: 'document_analysis', |
| label: 'Patent Analysis', |
| description: 'Extracting key innovations and TRL assessment', |
| icon: FileText, |
| progressRange: [0, 30], |
| }, |
| { |
| key: 'market_analysis', |
| label: 'Market Research', |
| description: 'Identifying commercialization opportunities', |
| icon: BarChart3, |
| progressRange: [30, 60], |
| }, |
| { |
| key: 'matchmaking', |
| label: 'Partner Matching', |
| description: 'Finding relevant stakeholders with semantic search', |
| icon: Users, |
| progressRange: [60, 85], |
| }, |
| { |
| key: 'outreach', |
| label: 'Brief Generation', |
| description: 'Creating valorization brief document', |
| icon: Mail, |
| progressRange: [85, 100], |
| }, |
| ]; |
|
|
| export function WorkflowProgress({ workflow }: WorkflowProgressProps) { |
| |
| const currentStepIndex = workflow.current_step |
| ? WORKFLOW_STEPS.findIndex((step) => step.key === workflow.current_step) |
| : Math.floor(workflow.progress / 25); |
|
|
| const getStepStatus = (stepIndex: number) => { |
| if (workflow.status === 'failed') { |
| return stepIndex <= currentStepIndex ? 'failed' : 'pending'; |
| } |
| if (workflow.status === 'completed') { |
| return 'completed'; |
| } |
| if (stepIndex < currentStepIndex) { |
| return 'completed'; |
| } |
| if (stepIndex === currentStepIndex) { |
| return 'in-progress'; |
| } |
| return 'pending'; |
| }; |
|
|
| return ( |
| <div className="w-full max-w-3xl mx-auto space-y-6"> |
| {/* Overall Progress */} |
| <Card> |
| <CardHeader> |
| <div className="flex items-center justify-between"> |
| <CardTitle className="text-2xl"> |
| {workflow.status === 'completed' && '✅ Analysis Complete'} |
| {workflow.status === 'failed' && '❌ Analysis Failed'} |
| {workflow.status === 'running' && '⚡ Analyzing Patent...'} |
| {workflow.status === 'queued' && '⏳ Queued for Processing'} |
| </CardTitle> |
| <Badge |
| variant={ |
| workflow.status === 'completed' |
| ? 'default' |
| : workflow.status === 'failed' |
| ? 'destructive' |
| : 'secondary' |
| } |
| className="text-sm" |
| > |
| {workflow.status.toUpperCase()} |
| </Badge> |
| </div> |
| </CardHeader> |
| <CardContent> |
| <div className="space-y-2"> |
| <div className="flex justify-between text-sm"> |
| <span className="text-gray-600">Overall Progress</span> |
| <span className="font-medium">{workflow.progress}%</span> |
| </div> |
| <Progress value={workflow.progress} className="h-3" /> |
| </div> |
| </CardContent> |
| </Card> |
| |
| {/* Workflow Steps */} |
| <div className="space-y-4"> |
| {WORKFLOW_STEPS.map((step, index) => { |
| const status = getStepStatus(index); |
| const Icon = step.icon; |
| |
| return ( |
| <motion.div |
| key={step.key} |
| initial={{ opacity: 0, x: -20 }} |
| animate={{ opacity: 1, x: 0 }} |
| transition={{ delay: index * 0.1 }} |
| > |
| <Card |
| className={cn( |
| 'transition-all', |
| status === 'in-progress' && 'border-blue-500 bg-blue-50', |
| status === 'completed' && 'border-green-200 bg-green-50', |
| status === 'failed' && 'border-red-200 bg-red-50' |
| )} |
| > |
| <CardContent className="p-6"> |
| <div className="flex items-start space-x-4"> |
| {/* Status Icon */} |
| <div |
| className={cn( |
| 'flex h-12 w-12 shrink-0 items-center justify-center rounded-full', |
| status === 'completed' && 'bg-green-100', |
| status === 'in-progress' && 'bg-blue-100', |
| status === 'pending' && 'bg-gray-100', |
| status === 'failed' && 'bg-red-100' |
| )} |
| > |
| {status === 'completed' && ( |
| <CheckCircle className="h-6 w-6 text-green-600" /> |
| )} |
| {status === 'in-progress' && ( |
| <Loader2 className="h-6 w-6 text-blue-600 animate-spin" /> |
| )} |
| {status === 'pending' && ( |
| <Circle className="h-6 w-6 text-gray-400" /> |
| )} |
| {status === 'failed' && ( |
| <Circle className="h-6 w-6 text-red-600" /> |
| )} |
| </div> |
| |
| {/* Step Content */} |
| <div className="flex-1 min-w-0"> |
| <div className="flex items-center space-x-3 mb-1"> |
| <Icon |
| className={cn( |
| 'h-5 w-5', |
| status === 'completed' && 'text-green-600', |
| status === 'in-progress' && 'text-blue-600', |
| status === 'pending' && 'text-gray-400', |
| status === 'failed' && 'text-red-600' |
| )} |
| /> |
| <h3 |
| className={cn( |
| 'text-lg font-semibold', |
| status === 'completed' && 'text-green-900', |
| status === 'in-progress' && 'text-blue-900', |
| status === 'pending' && 'text-gray-500', |
| status === 'failed' && 'text-red-900' |
| )} |
| > |
| {step.label} |
| </h3> |
| <Badge |
| variant={ |
| status === 'completed' |
| ? 'default' |
| : status === 'in-progress' |
| ? 'secondary' |
| : 'outline' |
| } |
| className="text-xs" |
| > |
| {status === 'completed' && 'Done'} |
| {status === 'in-progress' && 'Processing...'} |
| {status === 'pending' && 'Pending'} |
| {status === 'failed' && 'Failed'} |
| </Badge> |
| </div> |
| <p |
| className={cn( |
| 'text-sm', |
| status === 'completed' && 'text-green-700', |
| status === 'in-progress' && 'text-blue-700', |
| status === 'pending' && 'text-gray-500', |
| status === 'failed' && 'text-red-700' |
| )} |
| > |
| {step.description} |
| </p> |
| |
| {/* Step Progress Bar (only for in-progress step) */} |
| {status === 'in-progress' && ( |
| <motion.div |
| initial={{ opacity: 0, y: -5 }} |
| animate={{ opacity: 1, y: 0 }} |
| className="mt-3" |
| > |
| <Progress |
| value={ |
| ((workflow.progress - step.progressRange[0]) / |
| (step.progressRange[1] - step.progressRange[0])) * |
| 100 |
| } |
| className="h-2" |
| /> |
| </motion.div> |
| )} |
| </div> |
| </div> |
| </CardContent> |
| </Card> |
| </motion.div> |
| ); |
| })} |
| </div> |
| |
| {/* Error Display */} |
| {workflow.error && ( |
| <motion.div |
| initial={{ opacity: 0, y: 10 }} |
| animate={{ opacity: 1, y: 0 }} |
| > |
| <Card className="border-red-200 bg-red-50"> |
| <CardContent className="p-6"> |
| <div className="flex items-start space-x-3"> |
| <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-red-100"> |
| <span className="text-xl">⚠️</span> |
| </div> |
| <div> |
| <h3 className="font-semibold text-red-900">Error Occurred</h3> |
| <p className="text-sm text-red-700 mt-1">{workflow.error}</p> |
| </div> |
| </div> |
| </CardContent> |
| </Card> |
| </motion.div> |
| )} |
| |
| {/* Completion Message */} |
| {workflow.status === 'completed' && ( |
| <motion.div |
| initial={{ opacity: 0, scale: 0.95 }} |
| animate={{ opacity: 1, scale: 1 }} |
| transition={{ duration: 0.5 }} |
| > |
| <Card className="border-green-200 bg-gradient-to-br from-green-50 to-emerald-50"> |
| <CardContent className="p-6"> |
| <div className="text-center space-y-2"> |
| <div className="flex justify-center"> |
| <div className="flex h-16 w-16 items-center justify-center rounded-full bg-green-100"> |
| <CheckCircle className="h-10 w-10 text-green-600" /> |
| </div> |
| </div> |
| <h3 className="text-xl font-bold text-green-900"> |
| Analysis Complete! |
| </h3> |
| <p className="text-green-700"> |
| Your patent analysis is ready. Redirecting to results... |
| </p> |
| </div> |
| </CardContent> |
| </Card> |
| </motion.div> |
| )} |
| </div> |
| ); |
| } |
|
|