Files
deerflow-code/frontend-web/src/report-collaboration/components/ReportFlowNode.tsx
T
董宏凯andCursor db333a4c33 feat: 重构多轮RAG展示并完善报告协作、业务链审核与 agentfx 种子
抽出专用多轮RAG消息列表并升级会话模型;强化报告协作画布与检查器;业务链普通用户改为提交审核;扩展 agentfx 报告技能种子,并将含 admin 的用户名自动提权为管理员。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 17:34:16 +08:00

151 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { memo, type ReactNode } from "react";
import { Handle, Position, type NodeProps } from "@xyflow/react";
import {
AlertTriangle,
Bot,
Check,
CircleArrowRight,
CircleDashed,
FileCheck,
Flag,
LineChart,
Loader2,
PenLine,
RefreshCw,
Search,
ShieldCheck,
ShieldQuestion,
Users,
X,
} from "lucide-react";
import type { CollaborationNodeStatus } from "@/report-collaboration/api/types";
export type ReportFlowNodeKind = "start" | "end" | "agent";
/** 画布自定义节点数据(由 ReportFlowCanvas 组装)。 */
export interface ReportFlowNodeDatum {
nodeId: string;
label: string;
roleName?: string | null;
angle?: string | null;
status: CollaborationNodeStatus;
attempt: number;
selected: boolean;
kind: ReportFlowNodeKind;
tools: string[];
/** xyflow Node.data 需要 Record 兼容的索引签名。 */
[key: string]: unknown;
}
export type ReportFlowNodeType = "collaboration";
const STATUS_META: Record<
CollaborationNodeStatus,
{ text: string; color: string; icon: ReactNode }
> = {
planned: { text: "规划中", color: "var(--rc-text-secondary)", icon: <CircleDashed className="size-3.5" /> },
queued: { text: "排队中", color: "var(--rc-text-secondary)", icon: <CircleDashed className="size-3.5" /> },
running: { text: "运行中", color: "var(--rc-running)", icon: <Loader2 className="rc-spin size-3.5" /> },
validating: { text: "校验中", color: "var(--rc-running)", icon: <ShieldQuestion className="size-3.5" /> },
completed: { text: "已完成", color: "var(--rc-success)", icon: <Check className="size-3.5" /> },
failed: { text: "失败", color: "var(--rc-danger)", icon: <X className="size-3.5" /> },
awaiting_input: { text: "等待用户", color: "var(--rc-waiting)", icon: <AlertTriangle className="size-3.5" /> },
cancel_requested: { text: "取消中", color: "var(--rc-waiting)", icon: <X className="size-3.5" /> },
cancelled: { text: "已取消", color: "var(--rc-text-secondary)", icon: <X className="size-3.5" /> },
superseded: { text: "已被替代", color: "var(--rc-text-secondary)", icon: <RefreshCw className="size-3.5" /> },
reopened: { text: "已重开", color: "var(--rc-waiting)", icon: <RefreshCw className="size-3.5" /> },
interrupted: { text: "已中断", color: "var(--rc-waiting)", icon: <AlertTriangle className="size-3.5" /> },
};
export function roleIcon(roleName: string | null | undefined, kind: ReportFlowNodeKind = "agent"): ReactNode {
if (kind === "start") return <CircleArrowRight />;
if (kind === "end") return <Flag />;
switch (roleName) {
case "协调者":
return <Users />;
case "研究员":
return <Search />;
case "核验员":
return <ShieldCheck />;
case "分析员":
return <LineChart />;
case "撰写员":
return <PenLine />;
case "审稿员":
return <FileCheck />;
default:
return <Bot />;
}
}
function FlowPill({ children }: { children: ReactNode }) {
return <span className="rc-flow-pill rc-flow-pill-neutral">{children}</span>;
}
function FieldRow({ label, children }: { label: string; children: ReactNode }) {
return (
<>
<div className="rc-flow-node-field-label">{label}</div>
<div className="rc-flow-node-field-value">{children}</div>
</>
);
}
function CollaborationFlowNodeComponent({ data }: NodeProps) {
const node = data as unknown as ReportFlowNodeDatum;
const meta = STATUS_META[node.status] ?? STATUS_META.planned;
const statusText = node.attempt > 1 ? `${meta.text} · 第 ${node.attempt} 次` : meta.text;
return (
<div
className="rc-flow-node"
data-status={node.status}
data-selected={node.selected}
data-kind={node.kind}
data-role={node.roleName ?? ""}
tabIndex={0}
role="button"
aria-label={`节点 ${node.label},${statusText}`}
>
<Handle type="target" position={Position.Left} className="rc-flow-handle" isConnectable={false} />
<div className="rc-flow-node-header">
<span className="rc-flow-node-icon" aria-hidden>
{roleIcon(node.roleName, node.kind)}
</span>
<span className="rc-flow-node-title">{node.label}</span>
</div>
<div className="rc-flow-node-body">
{node.roleName ? (
<FieldRow label="角色">
<FlowPill>{node.roleName}</FlowPill>
</FieldRow>
) : null}
{node.angle ? (
<FieldRow label="角度">
<FlowPill>{node.angle}</FlowPill>
</FieldRow>
) : null}
{node.tools.length > 0 ? (
<FieldRow label="技能">
{node.tools.map((tool) => (
<FlowPill key={`tool-${tool}`}>{tool}</FlowPill>
))}
</FieldRow>
) : null}
<FieldRow label="状态">
<span className="rc-flow-node-status" style={{ color: meta.color }}>
{meta.icon}
<span>{statusText}</span>
</span>
</FieldRow>
</div>
<Handle type="source" position={Position.Right} className="rc-flow-handle" isConnectable={false} />
</div>
);
}
export const CollaborationFlowNode = memo(CollaborationFlowNodeComponent);