tenderhub-webai-verification / apps /web /src /components /ProcessingStatus.tsx
engresearch's picture
Upload folder using huggingface_hub
7f88bdf verified
"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];
// Progress animation (0-100% based on estimated time)
const [progress, setProgress] = useState(0);
const [tipIndex, setTipIndex] = useState(0);
const [elapsedSeconds, setElapsedSeconds] = useState(0);
useEffect(() => {
if (status !== "PROCESSING" || failed) return;
// Animate progress from current position toward next milestone
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]);
// Rotate tips every 15 seconds
useEffect(() => {
if (status !== "PROCESSING" || failed) return;
const tipInterval = setInterval(() => {
setTipIndex((prev) => (prev + 1) % ENGAGING_TIPS.length);
}, 15000);
return () => clearInterval(tipInterval);
}, [status, failed]);
// Reset when status changes
useEffect(() => {
setProgress((currentStepIdx / STEPS.length) * 100);
setElapsedSeconds(0);
}, [status, currentStepIdx]);
// Estimate remaining time (average 2.5 minutes total)
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>
);
}