refactor(workflow): simplify edge configuration
This commit is contained in:
@@ -93,12 +93,16 @@ export function EditableTitle({
|
||||
placeholder = "未命名助手",
|
||||
editLabel = "助手名称",
|
||||
variant = "page",
|
||||
allowEmpty = false,
|
||||
maxLength,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
editLabel?: string;
|
||||
variant?: "page" | "panel";
|
||||
allowEmpty?: boolean;
|
||||
maxLength?: number;
|
||||
}) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState(value);
|
||||
@@ -118,7 +122,7 @@ export function EditableTitle({
|
||||
|
||||
function commit() {
|
||||
const next = draft.trim();
|
||||
if (next) {
|
||||
if (next || allowEmpty) {
|
||||
onChange(next);
|
||||
}
|
||||
setEditing(false);
|
||||
@@ -129,6 +133,7 @@ export function EditableTitle({
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={draft}
|
||||
maxLength={maxLength}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(event) => {
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
type NodeSpecMap,
|
||||
type RuntimeNodeSpec,
|
||||
type WorkflowGraph,
|
||||
type WorkflowEdgeData,
|
||||
type WorkflowNodeData,
|
||||
type WorkflowNodeType,
|
||||
} from "./specs";
|
||||
@@ -396,7 +397,7 @@ export function WorkflowCanvas({
|
||||
const updateEdgeData = useCallback(
|
||||
(
|
||||
id: string,
|
||||
patch: WorkflowGraph["edges"][number]["data"],
|
||||
patch: Partial<WorkflowEdgeData>,
|
||||
) => {
|
||||
setEdges((es) =>
|
||||
es.map((e) => {
|
||||
@@ -535,6 +536,7 @@ export function WorkflowCanvas({
|
||||
const editingNode = nodes.find((n) => n.id === editingNodeId);
|
||||
const editingSpec = editingNode ? specsByType[editingNode.type as string] : null;
|
||||
const editingEdge = edges.find((e) => e.id === editingEdgeId);
|
||||
const editingEdgeData = editingEdge?.data as WorkflowEdgeData | undefined;
|
||||
const addableSpecs = Object.values(specsByType).filter((s) => s.addable);
|
||||
const canAddSpec = useCallback(
|
||||
(spec: RuntimeNodeSpec) => {
|
||||
@@ -683,7 +685,27 @@ export function WorkflowCanvas({
|
||||
: settingsOpen
|
||||
? "工作流设置"
|
||||
: editingEdge
|
||||
? "编辑连接条件"
|
||||
? (
|
||||
<EditableTitle
|
||||
value={editingEdgeData?.label ?? ""}
|
||||
placeholder={
|
||||
editingEdgeData?.mode === "llm"
|
||||
? "大模型判断"
|
||||
: editingEdgeData?.mode === "expression"
|
||||
? "表达式"
|
||||
: "默认路径"
|
||||
}
|
||||
editLabel="连接名称"
|
||||
variant="panel"
|
||||
allowEmpty
|
||||
maxLength={64}
|
||||
onChange={(label) =>
|
||||
updateEdgeData(editingEdge.id, {
|
||||
label: label || undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
: editingNode && editingSpec
|
||||
? (
|
||||
<EditableTitle
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
import type { Edge } from "@xyflow/react";
|
||||
import {
|
||||
Braces,
|
||||
CircleCheck,
|
||||
CircleX,
|
||||
GitBranch,
|
||||
MessageSquareText,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
@@ -38,14 +35,11 @@ export function EdgeSettingsPanel({
|
||||
sourceType?: string;
|
||||
isOnlyOutgoing: boolean;
|
||||
hasOtherDefaultPath: boolean;
|
||||
onChange: (patch: WorkflowEdgeData) => void;
|
||||
onChange: (patch: Partial<WorkflowEdgeData>) => void;
|
||||
}) {
|
||||
const data = (edge.data ?? { mode: "always", priority: 10 }) as WorkflowEdgeData;
|
||||
const [mode, setMode] = useState(data.mode ?? "always");
|
||||
const [priority, setPriority] = useState(data.priority ?? 10);
|
||||
const [label, setLabel] = useState(data.label ?? "");
|
||||
const [condition, setCondition] = useState(data.condition ?? "");
|
||||
const [transitionSpeech, setTransitionSpeech] = useState(data.transitionSpeech ?? "");
|
||||
const [combinator, setCombinator] = useState<"and" | "or">(
|
||||
data.expression?.combinator ?? "and",
|
||||
);
|
||||
@@ -57,33 +51,22 @@ export function EdgeSettingsPanel({
|
||||
|
||||
const publish = ({
|
||||
nextMode = mode,
|
||||
nextPriority = priority,
|
||||
nextLabel = label,
|
||||
nextCondition = condition,
|
||||
nextTransitionSpeech = transitionSpeech,
|
||||
nextCombinator = combinator,
|
||||
nextRules = rules,
|
||||
}: {
|
||||
nextMode?: WorkflowEdgeData["mode"];
|
||||
nextPriority?: number;
|
||||
nextLabel?: string;
|
||||
nextCondition?: string;
|
||||
nextTransitionSpeech?: string;
|
||||
nextCombinator?: "and" | "or";
|
||||
nextRules?: ExpressionRule[];
|
||||
}) =>
|
||||
onChange({
|
||||
mode: nextMode,
|
||||
priority: nextPriority,
|
||||
label: nextLabel.trim() ? nextLabel : undefined,
|
||||
condition: nextMode === "llm" ? nextCondition : undefined,
|
||||
expression:
|
||||
nextMode === "expression"
|
||||
? { combinator: nextCombinator, rules: nextRules }
|
||||
: undefined,
|
||||
transitionSpeech: nextTransitionSpeech.trim()
|
||||
? nextTransitionSpeech
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const setRule = (index: number, patch: Partial<ExpressionRule>) => {
|
||||
@@ -94,24 +77,6 @@ export function EdgeSettingsPanel({
|
||||
publish({ nextRules });
|
||||
};
|
||||
|
||||
const applyActionResultPreset = (status: "ok" | "error") => {
|
||||
const nextRules: ExpressionRule[] = [
|
||||
{
|
||||
variable: "system__last_action_status",
|
||||
operator: "eq",
|
||||
value: status,
|
||||
},
|
||||
];
|
||||
setMode("expression");
|
||||
setCombinator("and");
|
||||
setRules(nextRules);
|
||||
publish({
|
||||
nextMode: "expression",
|
||||
nextCombinator: "and",
|
||||
nextRules,
|
||||
});
|
||||
};
|
||||
|
||||
const parseValue = (value: string): unknown => {
|
||||
if (value === "true") return true;
|
||||
if (value === "false") return false;
|
||||
@@ -159,82 +124,13 @@ export function EdgeSettingsPanel({
|
||||
Agent 不能只有默认路径;请改为条件路径,或删除连接以持续对话。
|
||||
</span>
|
||||
)}
|
||||
{sourceType === "action" && (
|
||||
<div className="rounded-xl border border-hairline bg-canvas-soft p-3">
|
||||
<div className="mb-2 text-xs text-muted-foreground">
|
||||
Action 常用结果条件
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="gap-1.5"
|
||||
onClick={() => applyActionResultPreset("ok")}
|
||||
>
|
||||
<CircleCheck size={14} />
|
||||
执行成功
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="gap-1.5"
|
||||
onClick={() => applyActionResultPreset("error")}
|
||||
>
|
||||
<CircleX size={14} />
|
||||
执行失败
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{mode !== "always" && (
|
||||
<label className="block">
|
||||
<div className="mb-1.5 text-sm font-medium text-foreground">
|
||||
优先级
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
value={priority}
|
||||
onChange={(event) => {
|
||||
const nextPriority = Number(event.target.value) || 0;
|
||||
setPriority(nextPriority);
|
||||
publish({ nextPriority });
|
||||
}}
|
||||
className="border-hairline-strong bg-background text-foreground"
|
||||
/>
|
||||
<span className="mt-1.5 block text-xs text-muted-foreground">
|
||||
条件路径按数字从小到大判断;默认路径不参与优先级。
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<Braces size={15} />}
|
||||
title="触发条件"
|
||||
description="配置画布标签以及这条连接被命中的条件"
|
||||
description="配置这条连接被命中的条件"
|
||||
>
|
||||
<label className="block">
|
||||
<div className="mb-1.5 text-sm font-medium text-foreground">
|
||||
条件标签
|
||||
</div>
|
||||
<Input
|
||||
value={label}
|
||||
maxLength={64}
|
||||
placeholder="例如:用户想转人工"
|
||||
onChange={(event) => {
|
||||
const nextLabel = event.target.value;
|
||||
setLabel(nextLabel);
|
||||
publish({ nextLabel });
|
||||
}}
|
||||
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
|
||||
/>
|
||||
<span className="mt-1.5 block text-xs text-muted-foreground">
|
||||
用于画布和日志中识别该路径,{label.length}/64
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{mode === "llm" && (
|
||||
<label className="block">
|
||||
<div className="mb-1.5 text-sm font-medium text-foreground">
|
||||
@@ -376,32 +272,6 @@ export function EdgeSettingsPanel({
|
||||
</p>
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<MessageSquareText size={15} />}
|
||||
title="过渡语"
|
||||
description="命中连接后、进入下一节点前播放的固定内容"
|
||||
>
|
||||
<label className="block">
|
||||
<div className="mb-1.5 text-sm font-medium text-foreground">
|
||||
固定过渡语(可选)
|
||||
</div>
|
||||
<Textarea
|
||||
rows={3}
|
||||
value={transitionSpeech}
|
||||
placeholder="例如:好的,正在为你转接。"
|
||||
onChange={(event) => {
|
||||
const nextTransitionSpeech = event.target.value;
|
||||
setTransitionSpeech(nextTransitionSpeech);
|
||||
publish({ nextTransitionSpeech });
|
||||
}}
|
||||
className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
|
||||
/>
|
||||
<span className="mt-1.5 block text-xs text-muted-foreground">
|
||||
会显示在调试与完整聊天记录中,同时使用当前 TTS 播放。
|
||||
</span>
|
||||
</label>
|
||||
</SectionCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,11 +121,11 @@ export type ExpressionRule = {
|
||||
|
||||
export type WorkflowEdgeData = {
|
||||
mode: EdgeMode;
|
||||
/** Internal route order, assigned automatically when an edge is created. */
|
||||
priority: number;
|
||||
condition?: string;
|
||||
expression?: { combinator: "and" | "or"; rules: ExpressionRule[] };
|
||||
label?: string;
|
||||
transitionSpeech?: string;
|
||||
};
|
||||
|
||||
export type FieldSpec = {
|
||||
|
||||
Reference in New Issue
Block a user