BlueFin / frontend /src /components /PipelineProgress.jsx
vedanshmadan21's picture
Deploy Intelli-Credit to HuggingFace Spaces
7f6dd09
import { CheckCircle, Loader, Circle, Zap } from "lucide-react";
const STAGES = [
"INIT", "GO_PDF", "GO_FRAUD", "AI_OCR", "AI_NER", "AI_RAG",
"AI_GRAPH", "AI_RESEARCH", "AI_SCORING", "AI_STRESS", "AI_CAM", "COMPLETE",
];
const STAGE_LABELS = {
INIT: "Init", GO_PDF: "PDF Parse", GO_FRAUD: "Fraud Check",
AI_OCR: "OCR", AI_NER: "NER", AI_RAG: "RAG",
AI_GRAPH: "Entity Graph", AI_RESEARCH: "Research",
AI_SCORING: "Scoring", AI_STRESS: "Stress Test",
AI_CAM: "CAM Gen", COMPLETE: "Complete",
};
const RADIUS = 60;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
function CircularProgress({ percent }) {
const offset = CIRCUMFERENCE * (1 - percent / 100);
return (
<svg viewBox="0 0 160 160" style={{ width: "160px", height: "160px" }}>
<circle cx="80" cy="80" r={RADIUS} fill="none" stroke="var(--bg-elevated)" strokeWidth="8" />
<circle
cx="80" cy="80" r={RADIUS} fill="none" stroke="var(--accent)" strokeWidth="8"
strokeLinecap="round" strokeDasharray={CIRCUMFERENCE} strokeDashoffset={offset}
transform="rotate(-90 80 80)" style={{ transition: "stroke-dashoffset 0.5s ease" }}
/>
<text x="80" y="76" textAnchor="middle" dominantBaseline="middle"
fill="var(--text-primary)" fontSize="24"
fontFamily="var(--font-heading)" fontWeight="bold"
>
{percent}%
</text>
</svg>
);
}
function StageIcon({ stage, currentStage, arrivedStages }) {
const currentIdx = STAGES.indexOf(currentStage);
const stageIdx = STAGES.indexOf(stage);
if (arrivedStages.has(stage) && stageIdx < currentIdx) {
return <CheckCircle size={16} style={{ color: "var(--success)", flexShrink: 0 }} />;
}
if (stage === currentStage) {
return <Loader size={16} className="animate-spin" style={{ color: "var(--accent)", flexShrink: 0 }} />;
}
return <Circle size={16} style={{ color: "var(--text-muted)", flexShrink: 0 }} />;
}
export default function PipelineProgress({ events, percent, currentStage }) {
const arrivedStages = new Set(events.map((e) => e.stage));
return (
<div className="page-enter" style={{ padding: "24px" }}>
<div className="container">
{/* Header */}
<div style={{ marginBottom: "32px" }}>
<div className="eyebrow" style={{ marginBottom: "8px" }}>Processing</div>
<h2 style={{ fontFamily: "var(--font-body)", fontWeight: 600 }}>
Running Analysis Pipeline
</h2>
</div>
{/* Two-column layout */}
<div className="flex gap-lg" style={{ marginBottom: "24px", flexWrap: "wrap" }}>
{/* Left — circular progress */}
<div
className="card flex flex-col items-center justify-center gap-md"
style={{ flex: "0 0 38%", minWidth: "260px" }}
>
<CircularProgress percent={percent} />
<p style={{ fontFamily: "var(--font-body)", fontSize: "14px", fontWeight: 600, letterSpacing: "0.04em" }}>
{currentStage}
</p>
<p style={{ fontFamily: "var(--font-body)", fontSize: "12px", color: "var(--text-muted)" }}>
~{Math.max(1, Math.ceil((100 - percent) / 10))} min remaining
</p>
</div>
{/* Right — pipeline log */}
<div className="card flex flex-col gap-sm" style={{ flex: 1, minWidth: "300px" }}>
<span className="label" style={{ marginBottom: "4px" }}>Pipeline Log</span>
<div style={{ maxHeight: "380px", overflowY: "auto" }} className="flex flex-col gap-sm">
{events.length === 0 && (
<p style={{ color: "var(--text-muted)", fontSize: "13px" }}>
Waiting for pipeline events...
</p>
)}
{events.map((event, idx) => {
if (event.type === "failover") {
return (
<div
key={idx}
className="flex items-start gap-sm"
style={{
background: "var(--warning-subtle)",
border: "1px solid rgba(234,179,8,0.3)",
borderRadius: "var(--radius-md)",
padding: "12px",
}}
>
<Zap size={14} style={{ color: "var(--warning)", flexShrink: 0, marginTop: "2px" }} />
<div>
<div className="flex items-center gap-sm" style={{ marginBottom: "4px" }}>
<span style={{ fontSize: "11px", fontWeight: 600, color: "var(--warning)" }}>{event.stage}</span>
</div>
<p style={{ color: "var(--warning)", fontSize: "13px" }}>
Databricks cold-start detected. Failed over to local DuckDB execution.
</p>
</div>
</div>
);
}
return (
<div key={idx} className="flex items-start gap-sm">
<div style={{ marginTop: "3px" }}>
<StageIcon stage={event.stage} currentStage={currentStage} arrivedStages={arrivedStages} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div className="flex items-center gap-sm" style={{ marginBottom: "2px" }}>
<span style={{ fontSize: "11px", fontWeight: 600, color: "var(--accent)" }}>{event.stage}</span>
{event.percent !== undefined && (
<span style={{ fontSize: "11px", color: "var(--text-muted)" }}>{event.percent}%</span>
)}
</div>
<p className="truncate" style={{ fontSize: "13px", color: "var(--text-secondary)" }}>
{event.message}
</p>
</div>
</div>
);
})}
</div>
</div>
</div>
{/* Stage stepper */}
<div className="card">
<span className="label" style={{ display: "block", marginBottom: "12px" }}>Pipeline Stages</span>
<div className="flex flex-wrap gap-xs">
{STAGES.map((stage, idx) => {
const isArrived = arrivedStages.has(stage);
const isCurrent = stage === currentStage;
let bg, color, borderColor;
if (isCurrent) {
bg = "var(--accent-subtle)"; color = "var(--accent)"; borderColor = "var(--accent)";
} else if (isArrived) {
bg = "var(--success-subtle)"; color = "var(--success)"; borderColor = "rgba(34,197,94,0.3)";
} else {
bg = "var(--bg-elevated)"; color = "var(--text-muted)"; borderColor = "var(--border)";
}
return (
<div key={stage} className="flex items-center gap-xs">
<span
style={{
fontFamily: "var(--font-body)", fontSize: "11px", fontWeight: 600,
padding: "4px 10px", borderRadius: "var(--radius-sm)",
background: bg, color, border: `1px solid ${borderColor}`,
transition: "all var(--transition-base)",
}}
>
{STAGE_LABELS[stage]}
</span>
{idx < STAGES.length - 1 && (
<span style={{ color: "var(--text-muted)", fontSize: "11px" }}></span>
)}
</div>
);
})}
</div>
</div>
</div>
</div>
);
}