| "use client"; |
|
|
| import { useEffect, useState } from "react"; |
|
|
| type StepInfo = { |
| label: string; |
| key: string; |
| description: string; |
| }; |
|
|
| const STEPS: StepInfo[] = [ |
| { label: "Uploaded", key: "UPLOADED", description: "Document received and stored securely" }, |
| { label: "Processing", key: "PROCESSING", description: "AI is extracting key information from your tender" }, |
| { label: "Preview", key: "PREVIEW", description: "Generating tender summary and identifying requirements" }, |
| { label: "Analysis Ready", key: "ANALYSIS_READY", description: "Compliance matrix, bid score, and proposals are ready" }, |
| ]; |
|
|
| const ENGAGING_TIPS = [ |
| "💡 Did you know? Tenders with complete compliance documentation have 40% higher win rates.", |
| "⏱️ Tip: While you wait, gather your company registration and tax compliance certificates.", |
| "🎯 Fun fact: Our AI has analyzed over 10,000 Kenyan government tenders.", |
| "📊 Did you know? Understanding evaluation criteria early helps tailor winning proposals.", |
| "✅ Tip: Check the 'Mandatory Documents' section first—it is often the #1 reason bids are disqualified.", |
| "🏆 Kenyan SMEs that use AI-powered tender analysis report 3x more contract wins.", |
| ]; |
|
|
| function getStepState(stepKey: string, currentStatus: string, failed: boolean) { |
| const order = STEPS.map((s) => s.key); |
| const currentIdx = order.indexOf(currentStatus); |
| const stepIdx = order.indexOf(stepKey); |
|
|
| if (failed && stepKey === currentStatus) return "error"; |
| if (stepIdx < currentIdx) return "completed"; |
| if (stepIdx === currentIdx) return "active"; |
| return "pending"; |
| } |
|
|
| function getCurrentStepIndex(status: string) { |
| return STEPS.findIndex((s) => s.key === status); |
| } |
|
|
| function formatTimeRemaining(seconds: number) { |
| if (seconds < 60) return `${seconds}s remaining`; |
| const mins = Math.ceil(seconds / 60); |
| return `~${mins} min${mins > 1 ? 's' : ''} remaining`; |
| } |
|
|
| export default function ProcessingStatus({ |
| status, |
| lastError, |
| onRetry, |
| }: { |
| status: string; |
| lastError?: string | null; |
| onRetry?: () => void; |
| }) { |
| const failed = status === "FAILED"; |
| const currentStepIdx = getCurrentStepIndex(failed ? "PROCESSING" : status); |
| const currentStep = STEPS[currentStepIdx]; |
|
|
| |
| const [progress, setProgress] = useState(0); |
| const [tipIndex, setTipIndex] = useState(0); |
| const [elapsedSeconds, setElapsedSeconds] = useState(0); |
|
|
| useEffect(() => { |
| if (status !== "PROCESSING" || failed) return; |
|
|
| |
| const targetProgress = ((currentStepIdx + 1) / STEPS.length) * 100; |
| const interval = setInterval(() => { |
| setProgress((prev) => { |
| if (prev >= targetProgress - 5) return prev; |
| return prev + Math.random() * 2; |
| }); |
| setElapsedSeconds((s) => s + 1); |
| }, 1000); |
|
|
| return () => clearInterval(interval); |
| }, [status, failed, currentStepIdx]); |
|
|
| |
| useEffect(() => { |
| if (status !== "PROCESSING" || failed) return; |
|
|
| const tipInterval = setInterval(() => { |
| setTipIndex((prev) => (prev + 1) % ENGAGING_TIPS.length); |
| }, 15000); |
|
|
| return () => clearInterval(tipInterval); |
| }, [status, failed]); |
|
|
| |
| useEffect(() => { |
| setProgress((currentStepIdx / STEPS.length) * 100); |
| setElapsedSeconds(0); |
| }, [status, currentStepIdx]); |
|
|
| |
| const estimatedTotalSeconds = 150; |
| const remainingSeconds = Math.max(0, estimatedTotalSeconds - elapsedSeconds); |
|
|
| return ( |
| <div> |
| {/* Progress Bar */} |
| {status === "PROCESSING" && !failed && ( |
| <div style={{ marginBottom: "1.5rem" }}> |
| <div |
| style={{ |
| display: "flex", |
| justifyContent: "space-between", |
| alignItems: "center", |
| marginBottom: "0.5rem", |
| }} |
| > |
| <span style={{ fontWeight: 600, color: "var(--brand)" }}> |
| <span className="spinner" style={{ marginRight: "0.5rem" }}>◌</span> |
| {currentStep?.label} |
| </span> |
| <span className="text-sm text-muted"> |
| {formatTimeRemaining(remainingSeconds)} |
| </span> |
| </div> |
| <div |
| style={{ |
| background: "var(--border-light)", |
| borderRadius: "var(--radius-full)", |
| height: "8px", |
| overflow: "hidden", |
| }} |
| > |
| <div |
| style={{ |
| height: "100%", |
| background: "var(--brand-gradient)", |
| borderRadius: "var(--radius-full)", |
| width: `${Math.min(100, progress)}%`, |
| transition: "width 0.5s ease-out", |
| boxShadow: "0 0 10px rgba(5,150,105,0.3)", |
| }} |
| /> |
| </div> |
| <p className="text-sm text-muted" style={{ marginTop: "0.5rem" }}> |
| {currentStep?.description} |
| </p> |
| </div> |
| )} |
| |
| {/* Step Indicators */} |
| <div className="processing-steps"> |
| {STEPS.map((step, idx) => { |
| const state = getStepState(step.key, failed ? "PROCESSING" : status, failed); |
| const isCurrent = idx === currentStepIdx; |
| return ( |
| <div key={step.key} className={`processing-step ${state}`}> |
| <div |
| className="step-dot" |
| style={{ |
| animation: isCurrent && !failed ? "pulse 1.5s infinite" : undefined, |
| }} |
| > |
| {state === "completed" ? "✓" : state === "error" ? "!" : isCurrent ? "●" : ""} |
| </div> |
| <div className="step-label">{step.label}</div> |
| </div> |
| ); |
| })} |
| </div> |
| |
| {/* Rotating Tips */} |
| {status === "PROCESSING" && !failed && ( |
| <div |
| style={{ |
| marginTop: "1.5rem", |
| padding: "1rem", |
| background: "linear-gradient(135deg, var(--brand-subtle) 0%, #f0fdfa 100%)", |
| borderRadius: "var(--radius-md)", |
| border: "1px solid var(--brand-muted)", |
| textAlign: "center", |
| }} |
| > |
| <p |
| className="text-sm" |
| style={{ |
| color: "var(--brand-text)", |
| margin: 0, |
| animation: "fadeIn 0.5s ease-in", |
| }} |
| > |
| {ENGAGING_TIPS[tipIndex]} |
| </p> |
| </div> |
| )} |
| |
| {/* Error State */} |
| {failed && lastError && ( |
| <div |
| style={{ |
| padding: "1rem", |
| background: "var(--danger-subtle)", |
| borderRadius: "var(--radius-md)", |
| marginTop: "1rem", |
| }} |
| > |
| <div style={{ fontWeight: 600, color: "var(--danger-text)", marginBottom: "0.3rem" }}> |
| Processing Failed |
| </div> |
| <div className="text-sm" style={{ color: "var(--danger-text)" }}> |
| {lastError} |
| </div> |
| {onRetry && ( |
| <button |
| className="btn btn-danger btn-sm" |
| style={{ marginTop: "0.75rem" }} |
| onClick={onRetry} |
| > |
| Retry Processing |
| </button> |
| )} |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|