Files
ai-video-fullstack/frontend/src/components/workflow/panels/EdgeSettingsPanel.tsx
2026-08-03 15:17:01 +08:00

278 lines
9.4 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 type { Edge } from "@xyflow/react";
import {
Braces,
GitBranch,
Plus,
Trash2,
} from "lucide-react";
import { useState } from "react";
import { SectionCard } from "@/components/editor/section-card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { NodeSelect } from "./controls";
import type { ExpressionRule, WorkflowEdgeData } from "../specs";
export function EdgeSettingsPanel({
edge,
sourceType,
isOnlyOutgoing,
hasOtherDefaultPath,
onChange,
}: {
edge: Edge;
sourceType?: string;
isOnlyOutgoing: boolean;
hasOtherDefaultPath: boolean;
onChange: (patch: Partial<WorkflowEdgeData>) => void;
}) {
const data = (edge.data ?? { mode: "always", priority: 10 }) as WorkflowEdgeData;
const [mode, setMode] = useState(data.mode ?? "always");
const [condition, setCondition] = useState(data.condition ?? "");
const [combinator, setCombinator] = useState<"and" | "or">(
data.expression?.combinator ?? "and",
);
const [rules, setRules] = useState<ExpressionRule[]>(
data.expression?.rules?.length
? data.expression.rules
: [{ variable: "", operator: "eq", value: "" }],
);
const publish = ({
nextMode = mode,
nextCondition = condition,
nextCombinator = combinator,
nextRules = rules,
}: {
nextMode?: WorkflowEdgeData["mode"];
nextCondition?: string;
nextCombinator?: "and" | "or";
nextRules?: ExpressionRule[];
}) =>
onChange({
mode: nextMode,
condition: nextMode === "llm" ? nextCondition : undefined,
expression:
nextMode === "expression"
? { combinator: nextCombinator, rules: nextRules }
: undefined,
});
const setRule = (index: number, patch: Partial<ExpressionRule>) => {
const nextRules = rules.map((rule, ruleIndex) =>
ruleIndex === index ? { ...rule, ...patch } : rule,
);
setRules(nextRules);
publish({ nextRules });
};
const parseValue = (value: string): unknown => {
if (value === "true") return true;
if (value === "false") return false;
if (value !== "" && Number.isFinite(Number(value))) return Number(value);
return value;
};
return (
<div className="space-y-3">
<SectionCard
icon={<GitBranch size={15} />}
title="路由方式"
description="每个节点最多一条默认路径,也可以配置多条条件路径"
>
<NodeSelect
label="条件类型"
value={mode}
options={[
{
value: "always",
label: "默认路径",
disabled:
mode !== "always" &&
(hasOtherDefaultPath ||
(sourceType === "agent" && isOnlyOutgoing)),
},
{ value: "llm", label: "大模型判断" },
{ value: "expression", label: "表达式" },
]}
onChange={(value) => {
const nextMode =
(value as WorkflowEdgeData["mode"]) || "always";
setMode(nextMode);
publish({ nextMode });
}}
allowNone={false}
/>
{mode !== "always" && hasOtherDefaultPath && (
<span className="-mt-1 block text-xs text-muted-foreground">
使
</span>
)}
{sourceType === "agent" && isOnlyOutgoing && mode === "always" && (
<span className="-mt-1 block text-xs text-destructive">
Agent
</span>
)}
</SectionCard>
<SectionCard
icon={<Braces size={15} />}
title="触发条件"
description="配置这条连接被命中的条件"
>
{mode === "llm" && (
<label className="block">
<div className="mb-1.5 text-sm font-medium text-foreground">
<span className="text-destructive">*</span>
</div>
<Textarea
rows={4}
value={condition}
placeholder="例如:用户已经明确表示需要人工客服。"
onChange={(event) => {
const nextCondition = event.target.value;
setCondition(nextCondition);
publish({ nextCondition });
}}
className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
/>
{!condition.trim() && (
<span className="mt-1.5 block text-xs text-destructive">
LLM
</span>
)}
</label>
)}
{mode === "expression" && (
<div className="space-y-3">
<NodeSelect
label="规则组合"
value={combinator}
options={[
{ value: "and", label: "全部满足AND" },
{ value: "or", label: "任一满足OR" },
]}
onChange={(value) => {
const nextCombinator = value === "or" ? "or" : "and";
setCombinator(nextCombinator);
publish({ nextCombinator });
}}
allowNone={false}
/>
{rules.map((rule, index) => (
<div
key={index}
className="space-y-2 rounded-xl border border-hairline bg-canvas-soft p-3"
>
<Input
value={rule.variable}
placeholder="动态变量名"
onChange={(event) => setRule(index, { variable: event.target.value })}
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] gap-2">
<Select
value={rule.operator}
onValueChange={(value) =>
setRule(index, {
operator: value as ExpressionRule["operator"],
})
}
>
<SelectTrigger className="border-hairline-strong bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent>
{[
"eq",
"neq",
"gt",
"gte",
"lt",
"lte",
"contains",
"in",
"exists",
].map((operator) => (
<SelectItem key={operator} value={operator}>
{operator}
</SelectItem>
))}
</SelectContent>
</Select>
<Input
disabled={rule.operator === "exists"}
value={rule.value == null ? "" : String(rule.value)}
placeholder="比较值"
onChange={(event) =>
setRule(index, {
value: parseValue(event.target.value),
})
}
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
<Button
type="button"
size="icon"
variant="outline"
disabled={rules.length === 1}
aria-label={`删除第 ${index + 1} 条规则`}
onClick={() => {
const nextRules = rules.filter(
(_, ruleIndex) => ruleIndex !== index,
);
setRules(nextRules);
publish({ nextRules });
}}
>
<Trash2 size={14} />
</Button>
</div>
{!rule.variable.trim() && (
<span className="text-xs text-destructive">
</span>
)}
</div>
))}
<Button
type="button"
variant="outline"
className="w-full gap-2 border-hairline-strong"
onClick={() => {
const nextRules = [
...rules,
{ variable: "", operator: "eq", value: "" } as ExpressionRule,
];
setRules(nextRules);
publish({ nextRules });
}}
>
<Plus size={14} />
</Button>
</div>
)}
{mode === "always" && (
<p className="rounded-xl border border-hairline bg-canvas-soft px-3.5 py-3 text-sm leading-6 text-muted-foreground">
沿
</p>
)}
</SectionCard>
</div>
);
}