Refactor pipeline and assistant page components for improved structure and performance

- Remove unused imports and classes from pipeline.py to streamline the codebase.
- Consolidate dynamic variable handling and workflow management in AssistantPage, enhancing clarity and maintainability.
- Update WorkflowEditor to utilize a more modular approach, improving the overall architecture and reducing complexity.
- Enhance the import structure across components for better organization and readability.
This commit is contained in:
Xin Wang
2026-07-14 12:59:41 +08:00
parent 2d6ff5b7aa
commit 6e8fc70c5a
21 changed files with 6122 additions and 5439 deletions

View File

@@ -0,0 +1,337 @@
"use client";
import type { Edge } from "@xyflow/react";
import {
Braces,
GitBranch,
MessageSquareText,
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,
onChange,
}: {
edge: Edge;
sourceType?: string;
onChange: (patch: 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",
);
const [rules, setRules] = useState<ExpressionRule[]>(
data.expression?.rules?.length
? data.expression.rules
: [{ variable: "", operator: "eq", value: "" }],
);
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>) => {
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="选择由 Agent 判断、动态变量表达式判断,或作为确定性默认路径"
>
<NodeSelect
label="判断方式"
value={mode}
options={[
...(sourceType === "agent" ? [{ value: "llm", label: "LLM 判断" }] : []),
{ value: "expression", label: "动态变量表达式" },
{ value: "always", label: "默认路径" },
]}
onChange={(value) => {
const nextMode =
(value as WorkflowEdgeData["mode"]) || "always";
setMode(nextMode);
publish({ nextMode });
}}
allowNone={false}
/>
<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="配置画布标签以及这条连接被命中的条件"
>
<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">
<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>
<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>
);
}