"use client"; import { useEffect, useState, type ReactNode } from "react"; import { Check, Circle, Loader2, X, XCircle } from "lucide-react"; import { ANALYZE_PIPELINE_STEPS, AI_TASK_PIPELINE_STEPS, SCAN_PIPELINE_STEPS, STYLE_8D_PIPELINE_STEPS, describeTaskStatus, formatTaskElapsed, formatTaskMetric, getActivePhaseHint, isPipelineTask, parseJobProgressDetail, } from "@/lib/jobs/progress"; import type { JobTaskStatus } from "@/lib/jobs/types"; import { cn } from "@/lib/utils"; const STATUS_ICON: Record = { pending: , running: , done: , failed: , cancelled: , }; interface JobProgressPanelProps { summary?: string | null; progressDetailRaw?: string | null; compact?: boolean; completed?: boolean; jobType?: "scan" | "analyze-topic" | "style-8d" | "ai-task"; } function PhaseStepper({ steps, currentPhase, compact, }: { steps: ReadonlyArray<{ phase: string; title: string; hint: string }>; currentPhase?: string; compact?: boolean; }) { const currentIdx = steps.findIndex((s) => s.phase === currentPhase); return (
    {steps.map((step, idx) => { const isPast = currentIdx >= 0 && idx < currentIdx; const isCurrent = step.phase === currentPhase; return (
  1. {isPast ? ( ) : isCurrent ? ( ) : ( )} {step.title}
  2. ); })}
); } export function JobProgressPanel({ summary, progressDetailRaw, compact = false, completed = false, jobType = "scan", }: JobProgressPanelProps) { const detail = parseJobProgressDetail(progressDetailRaw); const tasks = detail?.tasks ?? []; const phase = detail?.phase; const phaseHint = getActivePhaseHint(phase, jobType); const hasRunning = !completed && tasks.some((t) => t.status === "running"); const [, tick] = useState(0); useEffect(() => { if (!hasRunning) return; const timer = window.setInterval(() => tick((n) => n + 1), 1000); return () => window.clearInterval(timer); }, [hasRunning]); if (!summary && tasks.length === 0) return null; const pipelineTasks = tasks.filter((t) => isPipelineTask(t)); const keywordTasks = tasks.filter((t) => !isPipelineTask(t)); const keywordDone = keywordTasks.filter((t) => t.status === "done").length; const keywordRunning = keywordTasks.find((t) => t.status === "running"); const keywordTotal = keywordTasks.length; const collapseKeywords = keywordTotal > 3; const steps = jobType === "analyze-topic" ? ANALYZE_PIPELINE_STEPS : jobType === "style-8d" ? STYLE_8D_PIPELINE_STEPS : jobType === "ai-task" ? AI_TASK_PIPELINE_STEPS : SCAN_PIPELINE_STEPS; return (
{!completed && phase && (
{phaseHint && (

目前:{phaseHint}

)}
)} {(detail?.summary || summary) && (

{detail?.summary ?? summary}

)} {pipelineTasks.length > 0 && (
    {pipelineTasks.map((task) => (
  • {STATUS_ICON[task.status]}
    {task.label} {describeTaskStatus(task, phase)} {formatTaskMetric(task)}
    {task.status === "running" && task.step && (

    → {task.step} {task.stepDetail ? `(${task.stepDetail})` : ""} {formatTaskElapsed(task.startedAt)}

    )} {task.error && (

    {task.error}

    )}
  • ))}
)} {keywordTotal > 0 && (
{collapseKeywords ? (
{keywordRunning ? STATUS_ICON.running : keywordDone === keywordTotal ? STATUS_ICON.done : STATUS_ICON.pending}
關鍵字搜尋 {keywordRunning ? `Threads 搜尋中… · ${keywordDone}/${keywordTotal}` : keywordDone === keywordTotal ? `完成 · ${keywordTotal} 個詞` : `等待 · 0/${keywordTotal}`}
{keywordRunning && (

正在搜:{keywordRunning.label} {formatTaskElapsed(keywordRunning.startedAt)}

{keywordRunning.step && (

→ {keywordRunning.step} {keywordRunning.stepDetail ? `(${keywordRunning.stepDetail})` : ""}

)}
)}
) : (
    {keywordTasks.map((task) => (
  • {STATUS_ICON[task.status]}
    {task.label} {describeTaskStatus(task, phase)} {formatTaskMetric(task)}
    {task.status === "running" && task.step && (

    → {task.step} {task.stepDetail ? `(${task.stepDetail})` : ""} {formatTaskElapsed(task.startedAt)}

    )} {task.error && (

    {task.error}

    )}
  • ))}
)}
)}
); }