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:
458
frontend/src/components/workflow/panels/NodeSettingsPanel.tsx
Normal file
458
frontend/src/components/workflow/panels/NodeSettingsPanel.tsx
Normal 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。 */
|
||||
|
||||
Reference in New Issue
Block a user