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,458 @@
"use client";
import { Flag, PhoneForwarded, Play, Tag } from "lucide-react";
import { useState } from "react";
import { SectionCard } from "@/components/editor/section-card";
import { DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { ActionNodePanel } from "./ActionNodePanel";
import { AgentNodePanel } from "./AgentNodePanel";
import { NodeSelect, ToolOptionPicker } from "./controls";
import type { RuntimeNodeSpec, WorkflowNodeData } from "../specs";
import type { ModelOption, WorkflowSettings } from "../types";
export function NodeSettingsPanel({
panel = false,
spec,
data,
toolOptions,
knowledgeOptions,
llmOptions,
asrOptions,
ttsOptions,
workflowSettings,
onChange,
}: {
panel?: boolean;
spec: RuntimeNodeSpec;
data: WorkflowNodeData;
toolOptions: ModelOption[];
knowledgeOptions: ModelOption[];
llmOptions: ModelOption[];
asrOptions: ModelOption[];
ttsOptions: ModelOption[];
workflowSettings: WorkflowSettings;
onChange: (patch: WorkflowNodeData) => void;
}) {
const [draft, setDraft] = useState<WorkflowNodeData>({ ...data });
const [argumentsJson, setArgumentsJson] = useState(
JSON.stringify(data.arguments ?? {}, null, 2),
);
const [assignmentsJson, setAssignmentsJson] = useState(
JSON.stringify(data.resultAssignments ?? {}, null, 2),
);
const [jsonError, setJsonError] = useState("");
const commit = (next: WorkflowNodeData) => {
setDraft(next);
onChange(next);
};
const set = (key: string, val: unknown) =>
commit({ ...draft, [key]: val });
const setPatch = (patch: Partial<WorkflowNodeData>) =>
commit({ ...draft, ...patch });
const commitActionJson = (
nextArgumentsJson: string,
nextAssignmentsJson: string,
) => {
try {
const args = JSON.parse(nextArgumentsJson) as Record<string, unknown>;
const assignments = JSON.parse(nextAssignmentsJson) as Record<string, string>;
if (Array.isArray(args) || Array.isArray(assignments)) throw new Error();
setJsonError("");
commit({ ...draft, arguments: args, resultAssignments: assignments });
} catch {
setJsonError("参数和结果映射必须是 JSON 对象");
}
};
if (panel) {
if (spec.type !== "agent") {
return (
<WorkflowNodePanelForm
spec={spec}
draft={draft}
set={set}
toolOptions={toolOptions}
argumentsJson={argumentsJson}
assignmentsJson={assignmentsJson}
jsonError={jsonError}
setArgumentsJson={setArgumentsJson}
setAssignmentsJson={setAssignmentsJson}
commitActionJson={commitActionJson}
/>
);
}
return (
<AgentNodePanel
draft={draft}
set={set}
setPatch={setPatch}
workflowSettings={workflowSettings}
toolOptions={toolOptions}
knowledgeOptions={knowledgeOptions}
llmOptions={llmOptions}
asrOptions={asrOptions}
ttsOptions={ttsOptions}
/>
);
}
return (
<>
{!panel && (
<DialogHeader>
<DialogTitle className="font-display text-ink">
{spec.displayName}
</DialogTitle>
</DialogHeader>
)}
<div className="flex flex-col gap-5 py-2">
{spec.fields.map((field) => {
const raw = draft[field.key];
if (field.type === "switch") {
return (
<label
key={field.key}
className="flex items-center justify-between gap-3"
>
<span className="text-sm font-medium text-foreground">
{field.label}
</span>
<Switch
checked={Boolean(raw)}
onCheckedChange={(checked) => set(field.key, checked)}
/>
</label>
);
}
return (
<div key={field.key} className="flex flex-col gap-2">
<label className="text-sm font-medium text-foreground">
{field.label}
{field.required && <span className="text-destructive"> *</span>}
</label>
{field.type === "textarea" ? (
<Textarea
rows={4}
value={(raw as string) ?? ""}
onChange={(e) => set(field.key, e.target.value)}
className="field-sizing-fixed min-h-32 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
) : (
<Input
value={(raw as string) ?? ""}
onChange={(e) => set(field.key, e.target.value)}
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
)}
</div>
);
})}
{spec.type === "agent" && (
<>
<NodeSelect
label="进入节点时"
value={(draft.entryMode as string) || "wait_user"}
options={[
{ value: "wait_user", label: "等待用户说话(默认)" },
{ value: "generate", label: "立即让 LLM 回复" },
{ value: "fixed_speech", label: "播放固定进入语" },
]}
onChange={(value) => set("entryMode", value || "wait_user")}
allowNone={false}
/>
{draft.entryMode === "fixed_speech" && (
<div className="block">
<div className="mb-2 text-sm font-medium text-foreground">
<span className="text-destructive">*</span>
</div>
<Textarea
rows={3}
value={draft.entrySpeech ?? ""}
onChange={(event) => set("entrySpeech", event.target.value)}
placeholder="例如:您好,请告诉我需要处理的问题。"
className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
<span className="mt-2 block text-xs text-muted-soft">
使 {"{{variable}}"} LLM
</span>
</div>
)}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-foreground"></label>
<ToolOptionPicker
options={toolOptions}
selectedIds={draft.toolIds ?? []}
onChange={(toolIds) => set("toolIds", toolIds)}
/>
</div>
<NodeSelect
label="知识库"
value={(draft.knowledgeBaseId as string) || ""}
options={knowledgeOptions}
onChange={(value) => set("knowledgeBaseId", value || "")}
/>
<NodeSelect
label="知识库模式"
value={(draft.knowledgeMode as string) || "disabled"}
options={[
{ value: "disabled", label: "关闭" },
{ value: "automatic", label: "每轮自动检索" },
{ value: "on_demand", label: "Agent 按需调用" },
]}
onChange={(value) => set("knowledgeMode", value || "disabled")}
allowNone={false}
/>
<NodeSelect
label="ASR 资源"
value={(draft.asrResourceId as string) || ""}
options={asrOptions}
onChange={(value) => set("asrResourceId", value || "")}
noneLabel="继承 Workflow 默认值"
/>
<NodeSelect
label="TTS 资源"
value={(draft.ttsResourceId as string) || ""}
options={ttsOptions}
onChange={(value) => set("ttsResourceId", value || "")}
noneLabel="继承 Workflow 默认值"
/>
</>
)}
{spec.type === "action" && (
<>
<NodeSelect
label="确定性执行工具"
value={(draft.toolId as string) || ""}
options={toolOptions}
onChange={(value) => set("toolId", value || "")}
noneLabel="请选择工具"
/>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-foreground"> JSON</label>
<Textarea
rows={5}
value={argumentsJson}
onChange={(event) => {
const value = event.target.value;
setArgumentsJson(value);
commitActionJson(value, assignmentsJson);
}}
className="field-sizing-fixed min-h-32 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
<span className="text-xs text-muted-soft">使 {"{{variable}}"} </span>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-foreground"> JSON</label>
<Textarea
rows={4}
value={assignmentsJson}
onChange={(event) => {
const value = event.target.value;
setAssignmentsJson(value);
commitActionJson(argumentsJson, value);
}}
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
<span className="text-xs text-muted-soft"> JSON Path</span>
</div>
{jsonError && <span className="text-xs text-destructive">{jsonError}</span>}
</>
)}
{spec.type === "handoff" && (
<NodeSelect
label="转交类型"
value={(draft.targetType as string) || "human"}
options={[
{ value: "ai", label: "其他 AI" },
{ value: "human", label: "人工" },
{ value: "queue", label: "队列" },
{ value: "phone", label: "电话" },
]}
onChange={(value) => set("targetType", value || "human")}
allowNone={false}
/>
)}
{spec.type === "end" && (
<NodeSelect
label="结束范围"
value={(draft.scope as string) || "session"}
options={[
{ value: "flow", label: "仅结束 AI 流程" },
{ value: "session", label: "结束音视频会话" },
]}
onChange={(value) => set("scope", value || "session")}
allowNone={false}
/>
)}
</div>
</>
);
}
function WorkflowNodePanelForm({
spec,
draft,
set,
toolOptions,
argumentsJson,
assignmentsJson,
jsonError,
setArgumentsJson,
setAssignmentsJson,
commitActionJson,
}: {
spec: RuntimeNodeSpec;
draft: WorkflowNodeData;
set: (key: string, val: unknown) => void;
toolOptions: ModelOption[];
argumentsJson: string;
assignmentsJson: string;
jsonError: string;
setArgumentsJson: (value: string) => void;
setAssignmentsJson: (value: string) => void;
commitActionJson: (argumentsValue: string, assignmentsValue: string) => void;
}) {
return (
<div className="space-y-3">
<SectionCard
icon={<Tag size={15} />}
title="节点信息"
description="节点在画布上显示的名称"
>
<label className="block">
<div className="mb-1.5 text-sm font-medium text-foreground">
</div>
<Input
value={draft.name ?? ""}
onChange={(event) => set("name", event.target.value)}
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
</label>
</SectionCard>
{spec.type === "start" && (
<SectionCard
icon={<Play size={15} />}
title="开场白"
description="会话建立后首先向用户播放的固定内容"
>
<label className="block">
<div className="mb-1.5 text-sm font-medium text-foreground">
</div>
<Textarea
rows={5}
value={draft.greeting ?? ""}
onChange={(event) => set("greeting", event.target.value)}
placeholder="例如:你好,我是 AI 视频助手,有什么可以帮你?"
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
/>
</label>
</SectionCard>
)}
{spec.type === "action" && (
<ActionNodePanel
draft={draft}
set={set}
toolOptions={toolOptions}
argumentsJson={argumentsJson}
assignmentsJson={assignmentsJson}
jsonError={jsonError}
setArgumentsJson={setArgumentsJson}
setAssignmentsJson={setAssignmentsJson}
commitActionJson={commitActionJson}
/>
)}
{spec.type === "handoff" && (
<SectionCard
icon={<PhoneForwarded size={15} />}
title="转交配置"
description="发送转交事件并继续执行工作流"
>
<NodeSelect
label="转交类型"
value={(draft.targetType as string) || "human"}
options={[
{ value: "ai", label: "其他 AI" },
{ value: "human", label: "人工" },
{ value: "queue", label: "队列" },
{ value: "phone", label: "电话" },
]}
onChange={(value) => set("targetType", value || "human")}
allowNone={false}
/>
<label className="block">
<div className="mb-1.5 text-sm font-medium text-foreground">
</div>
<Input
value={draft.target ?? ""}
onChange={(event) => set("target", event.target.value)}
placeholder="人工、队列、AI 或电话号码"
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
/>
</label>
<label className="block">
<div className="mb-1.5 text-sm font-medium text-foreground">
</div>
<Textarea
rows={4}
value={draft.message ?? ""}
onChange={(event) => set("message", event.target.value)}
placeholder="例如:好的,正在为你转接人工客服。"
className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
/>
</label>
</SectionCard>
)}
{spec.type === "end" && (
<SectionCard
icon={<Flag size={15} />}
title="结束配置"
description="结束工作流,并可在结束前播放一段固定回复"
>
<NodeSelect
label="结束范围"
value={(draft.scope as string) || "session"}
options={[
{ value: "flow", label: "仅结束 AI 流程" },
{ value: "session", label: "结束音视频会话" },
]}
onChange={(value) => set("scope", value || "session")}
allowNone={false}
/>
<label className="block">
<div className="mb-1.5 text-sm font-medium text-foreground">
</div>
<Textarea
rows={4}
value={draft.message ?? ""}
onChange={(event) => set("message", event.target.value)}
placeholder="例如:感谢你的来电,再见。"
className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
/>
</label>
</SectionCard>
)}
</div>
);
}
/** Agent 右栏:与 AssistantPage 共用紧凑 SectionCard。 */