import { useEffect, useMemo, useRef, useState } from 'react'; import { CheckCircle2, Loader2, Database, Brain, Sparkles, Rocket, LineChart } from 'lucide-react'; import { MLBiasAPI } from '../services/api'; import { useJobRunner } from '../hooks/JobRunnerProvider'; type Health = { status?: string; timestamp?: string; loaded_models?: string[]; dataset_loaded?: boolean; generation_results_available?: boolean; finetune_running?: boolean; steps?: Record; }; type StepKey = | 'Activate Task' | 'Load Dataset' | 'Load Model' | 'Generate and Score' | 'Counterfactual' | 'Sampling' | 'Plot and Output' | 'Finetune'; type StepState = 'todo' | 'doing' | 'done'; export default function PipelineProgress() { const { result, resp } = useJobRunner(); const [health, setHealth] = useState(null); const pollRef = useRef(null); useEffect(() => { const poll = async () => { try { const h = (await MLBiasAPI.checkHealth()) as Health; setHealth(prev => (JSON.stringify(prev) === JSON.stringify(h) ? prev : h)); } catch { } }; void poll(); pollRef.current = window.setInterval(poll, 1000); return () => { if (pollRef.current) window.clearInterval(pollRef.current); }; }, []); const [elapsed, setElapsed] = useState(0); const timerRef = useRef(null); useEffect(() => { const startedAt = Date.now(); timerRef.current = window.setInterval(() => { setElapsed(Math.floor((Date.now() - startedAt) / 1000)); }, 1000); return () => { if (timerRef.current) window.clearInterval(timerRef.current); }; }, []); const modelName = result?.config?.languageModel || ''; const wantFT = Boolean( result?.config?.enableFineTuning ?? (resp?.results as any)?.config_used?.enableFineTuning ); const backendSteps = useMemo(() => { const fromResp = ((resp?.results as any)?.steps || {}) as Record; const fromHealth = ((health?.steps || {}) as Record); const merged: Record = { ...fromResp }; Object.keys(fromHealth).forEach(k => { const v = (fromHealth as any)[k]; merged[k] = v === true || v === 'doing' || v === 'done'; }); return merged; }, [health?.steps, resp?.results]); const resultsAny = (resp?.results ?? {}) as any; const inferred = useMemo(() => { const hasData = Boolean(health?.dataset_loaded); const hasModel = Boolean(health?.loaded_models && health.loaded_models.length > 0); const genDone = Boolean( backendSteps['3_generate_and_eval'] || health?.generation_results_available || resultsAny.generation_done ); const r4Flag = Boolean( backendSteps['4_rank_sampling_original'] || resultsAny.rank_sampling_original_done || resultsAny.rank_sampling?.original_done ); const r5Flag = Boolean( backendSteps['5_rank_sampling_cf'] || resultsAny.rank_sampling_cf_done || resultsAny.rank_sampling?.cf_done ); const plotsFlag = Boolean( backendSteps['6_plots_and_metrics'] || resultsAny.plot_urls || resultsAny.plots_ready || (resultsAny.plots && (resultsAny.plots.original_sentiment || resultsAny.plots.counterfactual_sentiment)) ); const ftDoneFlag = Boolean( backendSteps['7_finetune'] === true || resultsAny.finetune_done || resultsAny.finetune?.completed || resultsAny.finetune?.saved_model_path ); const ftRunning = Boolean(resultsAny.finetune?.running || (health as any)?.finetune_running); const noStepSignals = Object.keys(backendSteps || {}).length === 0 && !resultsAny.rank_sampling_original_done && !resultsAny.rank_sampling_cf_done && !resultsAny.plots_ready && !resultsAny.finetune_done; const cfByTime = noStepSignals && genDone && elapsed > 30; const rsByTime = noStepSignals && genDone && elapsed > 45; const plotsByTime= noStepSignals && genDone && elapsed > 70; const cfDone = Boolean( backendSteps['3_5_counterfactual'] || resultsAny.counterfactual_done || resultsAny.counterfactual_results || r4Flag || r5Flag || plotsFlag || ftDoneFlag || cfByTime ); const r4 = r4Flag || rsByTime; const r5 = r5Flag || rsByTime; const plots = plotsFlag || plotsByTime; const ftDone = ftDoneFlag; return { hasData, hasModel, genDone, cfDone, r4, r5, plots, ftDone, ftRunning }; }, [backendSteps, health, resultsAny, elapsed]); const rawSteps = useMemo>(() => { const states: Record = { 'Activate Task': 'todo', 'Load Dataset': 'todo', 'Load Model': 'todo', 'Generate and Score': 'todo', 'Counterfactual': 'todo', 'Sampling': 'todo', 'Plot and Output': 'todo', 'Finetune': 'todo', }; if (result?.status === 'running') { states['Activate Task'] = 'doing'; } if (inferred.hasData) { states['Activate Task'] = 'done'; states['Load Dataset'] = 'done'; } if (inferred.hasModel) { states['Load Model'] = 'done'; } else if (inferred.hasData) { states['Load Model'] = 'doing'; } if (inferred.genDone) { states['Generate and Score'] = 'done'; } else if (inferred.hasModel) { states['Generate and Score'] = 'doing'; } if (inferred.cfDone) { states['Counterfactual'] = 'done'; } else if (states['Generate and Score'] === 'done') { states['Counterfactual'] = 'doing'; } const shouldStartSampling = inferred.r4 || inferred.r5 || states['Counterfactual'] === 'done' || (states['Generate and Score'] === 'done' && elapsed > 20); if (inferred.r4 && inferred.r5) { states['Sampling'] = 'done'; } else if (shouldStartSampling) { states['Sampling'] = 'doing'; } const shouldStartPlotting = inferred.plots || states['Sampling'] === 'done' || (states['Sampling'] === 'doing' && elapsed > 40); if (inferred.plots) { states['Plot and Output'] = 'done'; } else if (shouldStartPlotting) { states['Plot and Output'] = 'doing'; } if (wantFT) { if (inferred.ftDone) states['Finetune'] = 'done'; else if (inferred.ftRunning || states['Plot and Output'] === 'done') states['Finetune'] = 'doing'; else states['Finetune'] = 'todo'; } else { states['Finetune'] = 'todo'; } return states; }, [elapsed, inferred, wantFT, result?.status]); const STUCK_TIMEOUT = 30; // 秒 const [enteredAt, setEnteredAt] = useState>({} as any); const [forcedDone, setForcedDone] = useState>({} as any); useEffect(() => { const next: Record = { ...enteredAt } as any; (Object.keys(rawSteps) as StepKey[]).forEach((k) => { if (rawSteps[k] === 'doing' && !next[k]) next[k] = Date.now(); if (rawSteps[k] !== 'doing' && next[k]) delete next[k]; }); if (JSON.stringify(next) !== JSON.stringify(enteredAt)) setEnteredAt(next); }, [rawSteps]); useEffect(() => { const now = Date.now(); const k: StepKey = 'Counterfactual'; if (rawSteps[k] === 'doing' && enteredAt[k] && now - enteredAt[k] > STUCK_TIMEOUT * 1000) { if (!forcedDone[k]) setForcedDone(prev => ({ ...prev, [k]: true })); } }, [enteredAt, rawSteps, forcedDone]); const steps = useMemo(() => { const s = { ...rawSteps } as Record; (Object.keys(forcedDone) as StepKey[]).forEach((k) => { if (forcedDone[k]) s[k] = 'done'; }); return s; }, [rawSteps, forcedDone]); const ft = resultsAny?.finetune || {}; const downloadPath: string | undefined = ft.download_url || ft.model_url || ft.saved_model_path || resultsAny?.finetune_model_url; const downloadHref = downloadPath ? MLBiasAPI.resolvePath(downloadPath) : undefined; const baseSteps = [ { key: 'Activate Task', icon: Rocket }, { key: 'Load Dataset', icon: Database }, { key: 'Load Model', icon: Brain }, { key: 'Generate and Score', icon: Sparkles }, { key: 'Counterfactual', icon: Sparkles }, { key: 'Sampling', icon: LineChart }, { key: 'Plot and Output', icon: LineChart }, ] as const; const stepList = wantFT ? [...baseSteps, { key: 'Finetune', icon: Rocket } as const] : baseSteps; const completedCount = stepList.reduce( (acc, s) => acc + (steps[s.key as StepKey] === 'done' ? 1 : 0), 0 ); const doingCount = stepList.reduce( (acc, s) => acc + (steps[s.key as StepKey] === 'doing' ? 1 : 0), 0 ); const percent = Math.min( 100, Math.round(((completedCount + doingCount * 0.5) / stepList.length) * 100) ); const hasStuckStep = Object.values(steps).some((state) => state === 'doing') && elapsed > 60 && completedCount < stepList.length - 1; return (

Pipeline Running

Model: {modelName || '(未指定)'}

{hasStuckStep && (

⚠️ Some steps may run slowly and are automatically attempted to proceed.

)}
Executed {elapsed}s
    {stepList.map(({ key, icon: Icon }) => { const state = steps[key as StepKey]; const isDone = state === 'done'; const isDoing = state === 'doing'; const startTs = enteredAt[key as StepKey]; const isStuck = isDoing && startTs && (Date.now() - startTs) / 1000 > STUCK_TIMEOUT; return (
  1. {isDone ? ( ) : isDoing ? ( ) : ( )}
    {key}
    {isDone ? 'Finished' : isDoing ? (isStuck ? 'Running...' : 'Running…') : 'Waiting'}
  2. ); })}
{/* 微調完成 → 顯示下載模型 */} {wantFT && inferred.ftDone && downloadHref && (
Download Finetuned Model {ft?.saved_model_path && (

Model path: {String(ft.saved_model_path)}

)}
)}
); }