Spaces:
Runtime error
Runtime error
| import { useState, useEffect } from "react"; | |
| interface StepData { | |
| id: string; | |
| status: "pending" | "active" | "completed" | "error"; | |
| detail?: string; | |
| timestamp?: string; | |
| } | |
| interface StatusResponse { | |
| current_step: string | null; | |
| steps: StepData[]; | |
| started_at: string | null; | |
| completed_at: string | null; | |
| } | |
| interface WorkflowStep { | |
| id: string; | |
| label: string; | |
| icon: JSX.Element; | |
| status: "pending" | "active" | "completed" | "error"; | |
| detail?: string; | |
| } | |
| interface WorkflowStatusProps { | |
| sessionId?: string; | |
| } | |
| const STEP_CONFIG: { id: string; label: string; icon: JSX.Element; defaultDetail: string }[] = [ | |
| { | |
| id: "fetch", | |
| label: "Fetch Responses", | |
| icon: ( | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /> | |
| </svg> | |
| ), | |
| defaultDetail: "Getting data from Google Sheets", | |
| }, | |
| { | |
| id: "normalize", | |
| label: "Parse Data", | |
| icon: ( | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /> | |
| </svg> | |
| ), | |
| defaultDetail: "Organizing questions and answers", | |
| }, | |
| { | |
| id: "grade", | |
| label: "Grade Answers", | |
| icon: ( | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| ), | |
| defaultDetail: "Comparing with correct answers", | |
| }, | |
| { | |
| id: "explain", | |
| label: "Generate Explanations", | |
| icon: ( | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /> | |
| </svg> | |
| ), | |
| defaultDetail: "Explaining wrong answers", | |
| }, | |
| { | |
| id: "group", | |
| label: "Create Peer Groups", | |
| icon: ( | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /> | |
| </svg> | |
| ), | |
| defaultDetail: "Matching helpers with learners", | |
| }, | |
| { | |
| id: "report", | |
| label: "Generate Report", | |
| icon: ( | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> | |
| </svg> | |
| ), | |
| defaultDetail: "Creating comprehensive report", | |
| }, | |
| { | |
| id: "email", | |
| label: "Send Email", | |
| icon: ( | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /> | |
| </svg> | |
| ), | |
| defaultDetail: "Delivering report to inbox", | |
| }, | |
| ]; | |
| export function WorkflowStatus({ sessionId = "default" }: WorkflowStatusProps) { | |
| const [steps, setSteps] = useState<WorkflowStep[]>( | |
| STEP_CONFIG.map(s => ({ ...s, status: "pending" as const, detail: s.defaultDetail })) | |
| ); | |
| const [isConnected, setIsConnected] = useState(false); | |
| const [isAnalyzing, setIsAnalyzing] = useState(false); | |
| useEffect(() => { | |
| let eventSource: EventSource | null = null; | |
| let retryCount = 0; | |
| const maxRetries = 3; | |
| const connect = () => { | |
| eventSource = new EventSource(`/api/status/${sessionId}/stream`); | |
| eventSource.onopen = () => { | |
| setIsConnected(true); | |
| retryCount = 0; | |
| }; | |
| eventSource.onmessage = (event) => { | |
| try { | |
| const data: StatusResponse = JSON.parse(event.data); | |
| // Check if there's any activity | |
| const hasActivity = data.steps && data.steps.length > 0; | |
| setIsAnalyzing(hasActivity && !data.completed_at); | |
| // Update steps based on server data | |
| setSteps(prevSteps => { | |
| return prevSteps.map(step => { | |
| const serverStep = data.steps?.find(s => s.id === step.id); | |
| if (serverStep) { | |
| return { | |
| ...step, | |
| status: serverStep.status as WorkflowStep["status"], | |
| detail: serverStep.detail || step.detail, | |
| }; | |
| } | |
| return step; | |
| }); | |
| }); | |
| } catch (e) { | |
| console.error("Error parsing status:", e); | |
| } | |
| }; | |
| eventSource.onerror = () => { | |
| setIsConnected(false); | |
| eventSource?.close(); | |
| // Retry connection | |
| if (retryCount < maxRetries) { | |
| retryCount++; | |
| setTimeout(connect, 2000 * retryCount); | |
| } | |
| }; | |
| }; | |
| connect(); | |
| return () => { | |
| eventSource?.close(); | |
| }; | |
| }, [sessionId]); | |
| const getStatusColor = (status: WorkflowStep["status"]) => { | |
| switch (status) { | |
| case "completed": return "bg-[var(--color-success)] text-white"; | |
| case "active": return "bg-[var(--color-accent)] text-white animate-pulse"; | |
| case "error": return "bg-red-500 text-white"; | |
| default: return "bg-[var(--color-border)] text-[var(--color-text-muted)]"; | |
| } | |
| }; | |
| const getLineColor = (status: WorkflowStep["status"]) => { | |
| switch (status) { | |
| case "completed": return "bg-[var(--color-success)]"; | |
| case "active": return "bg-[var(--color-accent)]"; | |
| default: return "bg-[var(--color-border)]"; | |
| } | |
| }; | |
| const activeStep = steps.find(s => s.status === "active"); | |
| const completedCount = steps.filter(s => s.status === "completed").length; | |
| return ( | |
| <div className="card p-4"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between mb-4"> | |
| <div className="flex items-center gap-2"> | |
| <div className={`w-2 h-2 rounded-full ${isAnalyzing ? 'bg-[var(--color-accent)] animate-pulse' : isConnected ? 'bg-[var(--color-success)]' : 'bg-[var(--color-border)]'}`} /> | |
| <h3 className="font-display text-sm font-semibold text-[var(--color-text)]"> | |
| {isAnalyzing ? "AI Agent Working..." : "Agent Status"} | |
| </h3> | |
| </div> | |
| {completedCount > 0 && ( | |
| <span className="text-xs text-[var(--color-text-muted)]"> | |
| {completedCount}/{steps.length} | |
| </span> | |
| )} | |
| </div> | |
| {/* Active step highlight */} | |
| {activeStep && ( | |
| <div className="mb-4 p-3 rounded-lg bg-[var(--color-accent)]/10 border border-[var(--color-accent)]/20"> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-5 h-5 rounded-full bg-[var(--color-accent)] flex items-center justify-center animate-pulse"> | |
| <div className="w-2 h-2 bg-white rounded-full" /> | |
| </div> | |
| <div> | |
| <div className="text-sm font-medium text-[var(--color-accent)]"> | |
| {activeStep.label} | |
| </div> | |
| <div className="text-xs text-[var(--color-text-muted)]"> | |
| {activeStep.detail} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Steps list */} | |
| <div className="space-y-1"> | |
| {steps.map((step, index) => ( | |
| <div key={step.id} className="relative"> | |
| {/* Connector line */} | |
| {index < steps.length - 1 && ( | |
| <div | |
| className={`absolute left-[11px] top-[24px] w-0.5 h-4 transition-colors duration-300 ${getLineColor(step.status)}`} | |
| /> | |
| )} | |
| <div className="flex items-center gap-3 py-1"> | |
| {/* Icon circle */} | |
| <div | |
| className={`flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center transition-all duration-300 ${getStatusColor(step.status)}`} | |
| > | |
| {step.status === "completed" ? ( | |
| <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /> | |
| </svg> | |
| ) : step.status === "active" ? ( | |
| <div className="w-2 h-2 bg-white rounded-full" /> | |
| ) : step.status === "error" ? ( | |
| <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| ) : ( | |
| <span className="text-[10px]">{index + 1}</span> | |
| )} | |
| </div> | |
| {/* Label */} | |
| <div className={`text-sm transition-colors ${ | |
| step.status === "active" | |
| ? "font-medium text-[var(--color-accent)]" | |
| : step.status === "completed" | |
| ? "text-[var(--color-success)]" | |
| : step.status === "error" | |
| ? "text-red-500" | |
| : "text-[var(--color-text-muted)]" | |
| }`}> | |
| {step.label} | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Connection status */} | |
| <div className="mt-4 pt-3 border-t border-[var(--color-border)]"> | |
| <div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)]"> | |
| <div className={`w-1.5 h-1.5 rounded-full ${isConnected ? 'bg-green-500' : 'bg-gray-400'}`} /> | |
| {isConnected ? "Live updates" : "Connecting..."} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |