"use client"; import { AlertTriangle, Bot, CalendarClock, CheckCircle2, ClipboardCheck, ClipboardList, Clock3, FileText, Lightbulb, ListChecks, Loader2, MessageSquare, Route, Send, ShieldAlert, Wrench } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useEffect, useState } from "react"; import { cn } from "@/lib/utils"; import { showMapNotice } from "@/features/map/core/components/notice"; import { getRunningWorkflowDefinition, getRunningWorkflowState, getWorkflowStepStatus } from "@/features/workbench/data/running-workflows"; import type { ScheduledConditionAnalysisInsight, ScheduledConditionItem, ScheduledConditionKpi, ScheduledConditionRecord, ScheduledWorkOrderItem } from "@/features/workbench/types"; import { formatDuration, formatTime, formatTimeRange, getDetailAccentClassName, getKpiStatusClassName, getRiskLabel, getWorkOrderCurrentStage, getWorkOrderStagePillClassName, statusClassNames, statusLabels } from "./scheduled-condition-feed-utils"; const RUNNING_WORKFLOW_TICK_MS = 1_000; type IncidentOperation = { target: string; elementType: string; action: string; command: string; verification: string; }; type SubmittedIncidentWorkOrder = { code: string; submittedAt: number; notifiedTeams: string[]; operations: IncidentOperation[]; }; type ExecutionStepStatus = "done" | "current" | "pending"; type ExecutionStepMeta = { label: string; value: string; }; type ExecutionStep = { id: string; title: string; description: string; status: ExecutionStepStatus; meta?: ExecutionStepMeta[]; }; type ConditionDetailPanelProps = { condition: ScheduledConditionItem; onContinueConversation: () => void; }; export function StatusPill({ condition, className }: { condition: ScheduledConditionItem; className?: string; }) { const isWorkOrder = condition.kind === "work_order"; const currentStage = isWorkOrder ? getWorkOrderCurrentStage(condition) : null; return ( {isWorkOrder ? currentStage?.title ?? "工单" : statusLabels[condition.status]} ); } export function ConditionDetailPanel({ condition, onContinueConversation }: ConditionDetailPanelProps) { const detailKey = condition.kind === "work_order" ? `${condition.id}-${getWorkOrderCurrentStage(condition)?.id ?? condition.status}` : condition.id; let content: React.ReactNode; if (condition.kind === "work_order") { content = ; } else { content = ; } return ( {content} ); } function ConditionRecordDetailPanel({ condition, onContinueConversation }: { condition: ScheduledConditionRecord; onContinueConversation: () => void; }) { const insight = getConditionAnalysisInsight(condition); const [submittedWorkOrder, setSubmittedWorkOrder] = useState(null); const header = getConditionDetailHeader(condition); const bodyKey = `${condition.id}-${condition.status}-${condition.riskLevel}`; function handleSubmitIncidentWorkOrder() { const nextWorkOrder = createSubmittedIncidentWorkOrder(condition); setSubmittedWorkOrder(nextWorkOrder); showMapNotice({ id: `incident-work-order-${condition.id}`, tone: "success", title: "系统工单已提交", message: `${nextWorkOrder.code} 已通知 ${nextWorkOrder.notifiedTeams.join("、")}。` }); } return ( {condition.status === "running" ? ( ) : condition.status === "error" ? ( <> ) : condition.status === "warning" || condition.riskLevel === "attention" ? ( <> ) : ( <> )} ); } function WorkOrderDetailPanel({ condition }: { condition: ScheduledWorkOrderItem }) { const currentStage = getWorkOrderCurrentStage(condition); return ( ); } function RunningConditionBody({ condition }: { condition: ScheduledConditionRecord }) { const runningEvidence = (condition.evidence ?? []).slice(0, 2); const [nowMs, setNowMs] = useState(() => Date.now()); useEffect(() => { setNowMs(Date.now()); const interval = window.setInterval(() => { setNowMs(Date.now()); }, RUNNING_WORKFLOW_TICK_MS); return () => window.clearInterval(interval); }, [condition.id]); return ( <> ); } function getConditionDetailHeader(condition: ScheduledConditionRecord) { if (condition.status === "running") { return { icon: Loader2, iconClassName: "bg-blue-600 text-white", iconSpin: true }; } if (condition.status === "error") { return { icon: ShieldAlert, iconClassName: "bg-red-50 text-red-700", iconSpin: false }; } if (condition.status === "warning" || condition.riskLevel === "attention") { return { icon: AlertTriangle, iconClassName: "bg-orange-50 text-orange-700", iconSpin: false }; } return { icon: FileText, iconClassName: undefined, iconSpin: false }; } function DetailScroll({ children }: { children: React.ReactNode }) { return (
{children}
); } type DetailShellProps = { condition: ScheduledConditionItem; children: React.ReactNode; }; function DetailShell({ condition, children }: DetailShellProps) { const isWorkOrder = condition.kind === "work_order"; return (
); } type DetailHeaderProps = { condition: ScheduledConditionItem; icon: typeof FileText; iconClassName?: string; iconSpin?: boolean; children?: React.ReactNode; }; function DetailHeader({ condition, icon: Icon, iconClassName, iconSpin = false, children }: DetailHeaderProps) { return (
{formatTime(condition.scheduledAt)}

{condition.title}

{condition.detail ?

{condition.detail}

: null}
{children ?
{children}
: null}
); } type DetailTone = "violet" | "blue" | "red" | "orange"; const decisionSurfaceClassNames: Record = { red: "border-red-100 border-l-4 border-l-red-400", orange: "border-orange-100 border-l-4 border-l-orange-400", blue: "border-blue-100 border-l-4 border-l-blue-500", violet: "border-violet-100 border-l-4 border-l-violet-500" }; const detailDotClassNames: Record = { red: "bg-red-500", orange: "bg-orange-500", blue: "bg-blue-500", violet: "bg-violet-500" }; const taskExecutionBadgeClassNames: Record = { red: "border-red-100 bg-red-50 text-red-700", orange: "border-orange-100 bg-orange-50 text-orange-700", blue: "border-blue-100 bg-blue-50 text-blue-700", violet: "border-violet-100 bg-violet-50 text-violet-700" }; const taskExecutionCurrentStepClassNames: Record = { red: "border-red-100 bg-red-50/70", orange: "border-orange-100 bg-orange-50/70", blue: "border-blue-100 bg-blue-50/70", violet: "border-violet-100 bg-violet-50/70" }; function DetailDecisionBody({ children }: { children: React.ReactNode }) { return (
{children}
); } function DecisionSummaryBlock({ icon: Icon, title, tone, summary, notes }: { icon: typeof Bot; title: string; tone: DetailTone; summary: string; notes: string[]; }) { return (

{summary}

    {notes.map((note) => (
  • ))}
); } function SolutionOptionsList({ options }: { options: NonNullable; }) { return (
{options.map((option, index) => (
{index + 1}

{option.title}

适用
{option.scenario}
动作
{option.action}
代价
{option.tradeoff}
))}
); } function IncidentWorkOrderPanel({ condition, submittedWorkOrder, onSubmitWorkOrder }: { condition: ScheduledConditionItem; submittedWorkOrder: SubmittedIncidentWorkOrder | null; onSubmitWorkOrder: () => void; }) { const operations = submittedWorkOrder?.operations ?? createIncidentOperations(condition); const submitted = Boolean(submittedWorkOrder); const steps: ExecutionStep[] = [ { id: "analysis", title: "事件分析", status: "done", description: "汇总异常证据、影响分区和风险等级。" }, { id: "solution", title: "形成方案", status: "done", description: "生成阀门、泵站和现场复核操作清单。" }, { id: "confirm", title: "人工确认", status: submitted ? "done" : "current", description: "调度员确认影响范围和操作边界。" }, { id: "submit", title: "提交工单", status: submitted ? "done" : "pending", description: "写入系统工单并通知执行岗位。" } ]; return ( ); } function IncidentOperationList({ operations, submitted }: { operations: IncidentOperation[]; submitted: boolean; }) { return (
{operations.map((operation) => (
{submitted ? "已通知" : operation.elementType}
对象
{operation.target}
指令
{operation.command}
复核
{operation.verification}
))}
); } function EscalationCriteriaList({ items }: { items: string[] }) { return (
    {items.map((item, index) => (
  1. {index + 1} {item}
  2. ))}
); } function ConditionStatusReport({ condition, tone }: { condition: ScheduledConditionItem; tone: DetailTone; }) { if (condition.kind !== "condition" || condition.status === "running" || (!condition.report && !condition.kpis?.length)) { return null; } const report = condition.report; const kpis = condition.kpis ?? []; const statusTime = formatTimeRange(condition.scheduledAt, condition.durationMinutes); return (

{report?.title ?? condition.title}

{report?.conclusion ?

{report.conclusion}

: null}
{kpis.length > 0 ? (
{kpis.map((kpi) => ( ))}
) : null} {report?.sections?.length ? (
{report.sections.map((section) => ( ))}
) : null}
); } function ReportMetaTile({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); } function ReportSection({ title, items, tone }: { title: string; items: string[]; tone: DetailTone; }) { return (
{title}
    {items.map((item) => (
  • ))}
); } function KpiTile({ kpi }: { kpi: ScheduledConditionKpi }) { return (
{kpi.label}
{kpi.value} {kpi.unit ? {kpi.unit} : null}
{getRiskLabel(kpi.status)}

{kpi.description}

{kpi.threshold || kpi.baseline ? (
{kpi.threshold ?
阈值:{kpi.threshold}
: null} {kpi.baseline ?
基线:{kpi.baseline}
: null}
) : null}
); } function DecisionActionStrip({ icon: Icon, title, value, actionLabel, actionDisabled = false, actionTitle, onAction }: { icon: typeof Bot; title: string; value: string; actionLabel: string; actionDisabled?: boolean; actionTitle?: string; onAction?: () => void; }) { return (

{value}

); } function SectionEyebrow({ icon: Icon, label }: { icon?: typeof Bot; label: string; }) { return (
{Icon ?
); } function getDecisionSurfaceClassName(tone: DetailTone) { return decisionSurfaceClassNames[tone]; } function getDetailDotClassName(tone: DetailTone) { return detailDotClassNames[tone]; } function getTaskExecutionBadgeClassName(tone: DetailTone) { return taskExecutionBadgeClassNames[tone]; } function getTaskExecutionStepIconClassName(status: ExecutionStepStatus, tone: DetailTone) { if (status === "done") { return "bg-green-500 text-white"; } if (status === "current") { if (tone === "red") { return "bg-red-600 text-white"; } if (tone === "orange") { return "bg-orange-500 text-white"; } if (tone === "violet") { return "bg-violet-600 text-white"; } return "bg-blue-600 text-white"; } return "bg-slate-100 text-slate-400"; } function getTaskExecutionCurrentStepClassName(tone: DetailTone) { return taskExecutionCurrentStepClassNames[tone]; } function getTaskExecutionStepStatusClassName(status: ExecutionStepStatus, tone: DetailTone) { if (status === "done") { return "border-green-100 bg-green-50 text-green-700"; } if (status === "current") { return getTaskExecutionBadgeClassName(tone); } return "border-slate-200 bg-white text-slate-500"; } function getTaskExecutionStepStatusLabel(status: ExecutionStepStatus) { if (status === "done") { return "已完成"; } if (status === "current") { return "当前"; } return "待处理"; } function DetailMetricGrid({ condition }: { condition: ScheduledConditionItem }) { const durationLabel = condition.status === "running" ? "状态" : "耗时"; const durationValue = condition.status === "running" ? "实时执行" : formatDuration(condition.durationMinutes, condition.status); const modelValue = condition.kind === "work_order" ? "预约工单" : condition.modelName ?? "未记录"; const sessionValue = condition.kind === "work_order" ? formatTimeRange(condition.scheduledAt, condition.durationMinutes) : condition.sessionId ?? "未记录"; return (
); } function RunningWorkflowPanel({ condition, nowMs }: { condition: ScheduledConditionRecord; nowMs: number; }) { const workflow = getRunningWorkflowDefinition(condition); const workflowState = getRunningWorkflowState(condition, workflow, nowMs); const currentStepIndex = workflowState.currentStepIndex; const currentStep = workflow.steps[currentStepIndex] ?? workflow.steps[0]; const nextStep = workflow.steps[currentStepIndex + 1]; const steps: ExecutionStep[] = workflow.steps.map((step, index) => ({ id: step.id, title: step.title, description: step.detail, status: getWorkflowStepStatus(condition, index, currentStepIndex) })); return ( ); } function SectionTitle({ icon: Icon, title }: { icon: typeof Bot; title: string }) { return (
); } function DetailSectionCard({ children, className }: { children: React.ReactNode; className?: string; }) { return (
{children}
); } function TaskExecutionPanel({ icon: Icon, title, badge, summary, meta, steps, tone = "blue", children }: { icon: typeof Bot; title: string; badge: string; summary?: string; meta?: ExecutionStepMeta[]; steps: ExecutionStep[]; tone?: DetailTone; children?: React.ReactNode; }) { return (
{badge}
{summary ?

{summary}

: null} {meta?.length ? (
{meta.map((item) => ( ))}
) : null}
    {steps.map((step, index) => ( ))}
{children ?
{children}
: null}
); } function TaskExecutionStepRow({ step, tone, isLast }: { step: ExecutionStep; tone: DetailTone; isLast: boolean; }) { return (
  • {!isLast ? ( {step.title} {getTaskExecutionStepStatusLabel(step.status)} {step.meta?.length ? ( {step.meta.map((item) => ( {item.label} {item.value} ))} ) : null} {step.description}
  • ); } function WorkOrderLifecyclePanel({ condition }: { condition: ScheduledWorkOrderItem }) { const duration = formatDuration(condition.durationMinutes, condition.status); const currentStage = getWorkOrderCurrentStage(condition); const steps = condition.stages.map((stage) => ({ id: stage.id, title: stage.title, status: stage.status, description: stage.description, meta: [ { label: "责任人", value: stage.owner }, { label: "时间", value: stage.timeLabel } ] })); return ( ); } function WorkOrderReplyRequirements({ condition }: { condition: ScheduledWorkOrderItem }) { return (
    执行窗口 {formatTimeRange(condition.scheduledAt, condition.durationMinutes)} 复令时限 执行完成后 {condition.replyWindowMinutes} 分钟内
      {condition.replyRequirements.map((item, index) => (
    • {index + 1} {item}
    • ))}
    ); } function RunningEvidencePreview({ items }: { items: string[] }) { return (
      {items.length > 0 ? ( items.map((item, index) => (
    1. {index + 1} {item} 采集中
    2. )) ) : (
    3. 正在等待首批证据返回。
    4. )}
    ); } function EvidenceList({ items }: { items: string[] }) { return (
      {items.length > 0 ? ( items.map((item, index) => (
    1. {index + 1} {item}
    2. )) ) : (
    3. 暂无关键证据记录。
    4. )}
    ); } function createSubmittedIncidentWorkOrder(condition: ScheduledConditionItem): SubmittedIncidentWorkOrder { const operations = createIncidentOperations(condition); const submittedAt = Date.now(); const codeSeed = `${condition.id}-${submittedAt}`.replace(/[^a-zA-Z0-9]/g, "").slice(-8).toUpperCase(); return { code: `INC-WO-${codeSeed}`, submittedAt, notifiedTeams: ["调度执行岗", "现场抢修班组", "模型复核岗"], operations }; } function createIncidentOperations(condition: ScheduledConditionItem): IncidentOperation[] { const baseVerification = condition.status === "error" ? "执行后 5 分钟内复核压力、流量和影响用户反馈。" : "下一轮工况继续跟踪偏差是否回落。"; if (condition.title.includes("智能调度")) { return [ { target: "华山路沿线阀门组 VG-11", elementType: "阀门", action: "阀门开度调整", command: "按方案将边界阀门预置到 45% 开度,执行前确认上下游压力。", verification: "确认高区末端压力不低于 0.22MPa。" }, { target: "北辰二级泵站 P-02", elementType: "泵站", action: "泵站出水复核", command: "保持当前泵组组合,复核出水压力和频率波动。", verification: baseVerification } ]; } if (condition.title.includes("SCADA")) { return [ { target: "SCADA 异常测点组", elementType: "测点", action: "异常测点隔离", command: "调度计算暂不采用异常测点,使用相邻测点插补边界。", verification: "数据维护岗确认通信链路恢复后再解除隔离。" }, { target: "异常 DMA 现场巡检点", elementType: "巡检点", action: "现场复核", command: "通知巡检班组核对压力表、流量计和阀井状态。", verification: baseVerification } ]; } return [ { target: "疑似影响区上下游阀门", elementType: "阀门", action: "阀门边界确认", command: "核对阀门当前开度,暂不执行大范围关断,保留应急隔离方案。", verification: "确认影响区压力恢复趋势和用户侧反馈。" }, { target: "关联泵站与分区边界", elementType: "泵站/边界", action: "调度边界复核", command: "使用最新泵站出水、阀门开度和 SCADA 流量刷新模拟边界。", verification: baseVerification } ]; } function getConditionAnalysisInsight(condition: ScheduledConditionItem): ScheduledConditionAnalysisInsight { if (condition.kind === "condition" && condition.analysisInsight) { return condition.analysisInsight; } if (condition.status === "error") { return { summary: "Agent 判断这条工况未完整闭环,需要先确认异常来源,再决定是否进入调度处置。", notes: [ "优先核对遥测源新鲜度和关键测点完整率。", "结合历史基线复核压力/流量偏差是否持续。", "处置前应先确认影响范围,避免误触发联动动作。" ], solutionOptions: [ { id: "fallback-source", title: "数据源复核", scenario: "遥测源超时或数据完整率不足。", action: "重试关键测点并标记缺失源。", tradeoff: "判断更稳妥,但会延后处置。" }, { id: "fallback-model", title: "模型比对", scenario: "压力或流量偏差集中在局部分区。", action: "对比基线工况并圈定疑似影响范围。", tradeoff: "响应更快,但依赖模型边界条件质量。" }, { id: "fallback-manual", title: "人工复核", scenario: "异常持续或伴随用户侧反馈。", action: "交由调度员确认阀门、泵站和巡检动作。", tradeoff: "闭环完整,但会占用人工资源。" } ] }; } return { summary: "Agent 将这条工况标记为关注项,当前偏差不足以触发自动处置。", notes: [ "保留当前证据,下一轮继续比对同一测点趋势。", "重点观察偏差是否扩大,避免单点噪声误判。", "需要时可在 Agent 面板追问偏差来源。" ], escalationCriteria: [ "连续两轮工况仍高于关注阈值。", "关键测点完整率继续下降。", "同一分区出现用户投诉、低压或水质联动线索。" ] }; }