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:
772
frontend/src/components/workflow/WorkflowCanvas.tsx
Normal file
772
frontend/src/components/workflow/WorkflowCanvas.tsx
Normal file
@@ -0,0 +1,772 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
addEdge,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
type Connection,
|
||||
Controls,
|
||||
type Edge,
|
||||
type Node,
|
||||
type NodeChange,
|
||||
type OnConnectEnd,
|
||||
Panel,
|
||||
ReactFlow,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { Braces, Plus, Settings2, X } from "lucide-react";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
import { edgeTypes } from "./ConditionEdge";
|
||||
import {
|
||||
ActiveNodeContext,
|
||||
EdgeActionContext,
|
||||
NodeActionContext,
|
||||
NodeSpecsContext,
|
||||
} from "./context";
|
||||
import { nodeTypes } from "./GenericNode";
|
||||
import { EdgeSettingsPanel } from "./panels/EdgeSettingsPanel";
|
||||
import { GlobalSettingsPanel } from "./panels/GlobalSettingsPanel";
|
||||
import { NodeSettingsPanel } from "./panels/NodeSettingsPanel";
|
||||
import {
|
||||
accentVar,
|
||||
defaultGraph,
|
||||
type NodeSpecMap,
|
||||
type RuntimeNodeSpec,
|
||||
type WorkflowGraph,
|
||||
type WorkflowNodeData,
|
||||
type WorkflowNodeType,
|
||||
} from "./specs";
|
||||
import type { WorkflowEditorProps } from "./types";
|
||||
|
||||
let nodeSeq = 0;
|
||||
|
||||
function defaultNodeData(spec: RuntimeNodeSpec): WorkflowNodeData {
|
||||
const data: WorkflowNodeData = {
|
||||
name: spec.displayName,
|
||||
...(spec.type === "agent"
|
||||
? {
|
||||
contextPolicy: "inherit",
|
||||
inheritGlobalConfig: true,
|
||||
entryMode: "wait_user",
|
||||
entrySpeech: "",
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
for (const field of spec.fields) {
|
||||
if (field.default !== undefined) data[field.key] = field.default;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function toFlow(graph: WorkflowGraph): { nodes: Node[]; edges: Edge[] } {
|
||||
return {
|
||||
nodes: graph.nodes.map((n) => ({
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
position: n.position,
|
||||
data: n.data,
|
||||
})),
|
||||
edges: graph.edges.map((e) => ({
|
||||
id: e.id,
|
||||
type: "condition",
|
||||
source: e.source,
|
||||
target: e.target,
|
||||
data: e.data ?? {},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function fromFlow(nodes: Node[], edges: Edge[]): WorkflowGraph {
|
||||
return {
|
||||
specVersion: 3,
|
||||
settings: {
|
||||
globalPrompt: "",
|
||||
defaultLlmResourceId: "",
|
||||
defaultAsrResourceId: "",
|
||||
defaultTtsResourceId: "",
|
||||
toolIds: [],
|
||||
knowledgeBaseId: "",
|
||||
knowledgeMode: "automatic",
|
||||
knowledgeTopN: 5,
|
||||
knowledgeScoreThreshold: 0,
|
||||
enableInterrupt: true,
|
||||
turnConfig: defaultGraph().settings.turnConfig,
|
||||
},
|
||||
nodes: nodes.map((n) => ({
|
||||
id: n.id,
|
||||
type: n.type as WorkflowNodeType,
|
||||
position: n.position,
|
||||
data: n.data as WorkflowNodeData,
|
||||
})),
|
||||
edges: edges.map((e) => ({
|
||||
id: e.id,
|
||||
source: e.source,
|
||||
target: e.target,
|
||||
data: (e.data ?? {
|
||||
mode: "always",
|
||||
priority: 10,
|
||||
}) as WorkflowGraph["edges"][number]["data"],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function WorkflowCanvas({
|
||||
value,
|
||||
onChange,
|
||||
settings,
|
||||
onSettingsChange,
|
||||
modelOptions,
|
||||
activeNodeId,
|
||||
onOpenDynamicVariables,
|
||||
editingNodeId,
|
||||
onEditingNodeIdChange,
|
||||
editingEdgeId,
|
||||
onEditingEdgeIdChange,
|
||||
settingsOpen,
|
||||
onSettingsOpenChange: setSettingsOpen,
|
||||
debugOpen,
|
||||
onDebugOpenChange,
|
||||
debugPanel,
|
||||
specsByType,
|
||||
toolOptions = [],
|
||||
knowledgeOptions = [],
|
||||
}: WorkflowEditorProps & { specsByType: NodeSpecMap }) {
|
||||
const initial = useMemo(() => toFlow(value ?? defaultGraph()), [value]);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initial.nodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initial.edges);
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [addSourceId, setAddSourceId] = useState<string | null>(null);
|
||||
const [addPosition, setAddPosition] = useState<{ x: number; y: number } | null>(null);
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
|
||||
// 回传画布状态给外部(助手 graph)。用 ref 避免把 onChange 放进依赖导致循环。
|
||||
const onChangeRef = useRef(onChange);
|
||||
useEffect(() => {
|
||||
onChangeRef.current = onChange;
|
||||
}, [onChange]);
|
||||
useEffect(() => {
|
||||
const graph = fromFlow(nodes, edges);
|
||||
graph.settings = {
|
||||
globalPrompt: settings.globalPrompt,
|
||||
defaultLlmResourceId: settings.llm ?? "",
|
||||
defaultAsrResourceId: settings.asr ?? "",
|
||||
defaultTtsResourceId: settings.tts ?? "",
|
||||
toolIds: settings.toolIds,
|
||||
knowledgeBaseId: settings.knowledgeBaseId,
|
||||
knowledgeMode: settings.knowledgeRetrievalConfig.mode,
|
||||
knowledgeTopN: settings.knowledgeRetrievalConfig.topN,
|
||||
knowledgeScoreThreshold:
|
||||
settings.knowledgeRetrievalConfig.scoreThreshold,
|
||||
enableInterrupt: settings.allowInterrupt,
|
||||
turnConfig: settings.turnConfig,
|
||||
};
|
||||
onChangeRef.current?.(graph);
|
||||
}, [
|
||||
nodes,
|
||||
edges,
|
||||
settings.globalPrompt,
|
||||
settings.llm,
|
||||
settings.asr,
|
||||
settings.tts,
|
||||
settings.toolIds,
|
||||
settings.knowledgeBaseId,
|
||||
settings.knowledgeRetrievalConfig,
|
||||
settings.allowInterrupt,
|
||||
settings.turnConfig,
|
||||
]);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(connection: Connection) => {
|
||||
const sourceType = nodes.find((node) => node.id === connection.source)?.type;
|
||||
const priority =
|
||||
edges.filter((edge) => edge.source === connection.source).length * 10 + 10;
|
||||
setEdges((eds) =>
|
||||
addEdge(
|
||||
{
|
||||
...connection,
|
||||
id: `e-${connection.source}-${connection.target}-${Date.now()}`,
|
||||
type: "condition",
|
||||
animated: true,
|
||||
data:
|
||||
sourceType === "agent"
|
||||
? {
|
||||
mode: "llm",
|
||||
priority,
|
||||
condition: "当前阶段任务已经完成",
|
||||
}
|
||||
: { mode: "always", priority },
|
||||
},
|
||||
eds,
|
||||
),
|
||||
);
|
||||
},
|
||||
[nodes, edges, setEdges],
|
||||
);
|
||||
|
||||
// 连线约束:不能连入开始节点(无入边句柄),不能自连。
|
||||
const isValidConnection = useCallback(
|
||||
(c: Connection | Edge) => {
|
||||
if (c.source === c.target) return false;
|
||||
const source = nodes.find((n) => n.id === c.source);
|
||||
const target = nodes.find((n) => n.id === c.target);
|
||||
if (!source || !target) return false;
|
||||
|
||||
const sourceSpec = specsByType[source.type as string];
|
||||
const targetSpec = specsByType[target.type as string];
|
||||
if (!sourceSpec?.hasSource || !targetSpec?.hasTarget) return false;
|
||||
if (edges.some((e) => e.source === c.source && e.target === c.target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sourceLimit = sourceSpec.constraints.maxOutgoing;
|
||||
if (
|
||||
sourceLimit !== undefined &&
|
||||
edges.filter((e) => e.source === c.source).length >= sourceLimit
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const targetLimit = targetSpec.constraints.maxIncoming;
|
||||
if (
|
||||
targetLimit !== undefined &&
|
||||
edges.filter((e) => e.target === c.target).length >= targetLimit
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[edges, nodes, specsByType],
|
||||
);
|
||||
|
||||
const addNode = useCallback(
|
||||
(spec: RuntimeNodeSpec) => {
|
||||
nodeSeq += 1;
|
||||
const id = `${spec.type}-${Date.now()}-${nodeSeq}`;
|
||||
const source = addSourceId
|
||||
? nodes.find((node) => node.id === addSourceId)
|
||||
: undefined;
|
||||
const position = addPosition
|
||||
? { x: addPosition.x - 125, y: addPosition.y }
|
||||
: screenToFlowPosition({
|
||||
x: window.innerWidth / 2,
|
||||
y: window.innerHeight / 2,
|
||||
});
|
||||
const data = defaultNodeData(spec);
|
||||
setNodes((ns) => [...ns, { id, type: spec.type, position, data }]);
|
||||
if (source) {
|
||||
setEdges((currentEdges) => {
|
||||
const priority =
|
||||
currentEdges.filter((edge) => edge.source === source.id).length * 10 + 10;
|
||||
return addEdge(
|
||||
{
|
||||
id: `e-${source.id}-${id}-${Date.now()}`,
|
||||
source: source.id,
|
||||
target: id,
|
||||
type: "condition",
|
||||
animated: true,
|
||||
data:
|
||||
source.type === "agent"
|
||||
? {
|
||||
mode: "llm",
|
||||
priority,
|
||||
condition: "当前阶段任务已经完成",
|
||||
}
|
||||
: { mode: "always", priority },
|
||||
},
|
||||
currentEdges,
|
||||
);
|
||||
});
|
||||
}
|
||||
setAddOpen(false);
|
||||
setAddSourceId(null);
|
||||
setAddPosition(null);
|
||||
setSettingsOpen(false);
|
||||
onDebugOpenChange(false);
|
||||
onEditingEdgeIdChange(null);
|
||||
onEditingNodeIdChange(id);
|
||||
},
|
||||
[
|
||||
addPosition,
|
||||
addSourceId,
|
||||
nodes,
|
||||
onDebugOpenChange,
|
||||
onEditingEdgeIdChange,
|
||||
onEditingNodeIdChange,
|
||||
screenToFlowPosition,
|
||||
setEdges,
|
||||
setNodes,
|
||||
setSettingsOpen,
|
||||
],
|
||||
);
|
||||
|
||||
const updateNodeData = useCallback(
|
||||
(id: string, patch: Partial<WorkflowNodeData>) => {
|
||||
setNodes((ns) =>
|
||||
ns.map((n) =>
|
||||
n.id === id ? { ...n, data: { ...n.data, ...patch } } : n,
|
||||
),
|
||||
);
|
||||
},
|
||||
[setNodes],
|
||||
);
|
||||
|
||||
const deleteNode = useCallback(
|
||||
(id: string) => {
|
||||
if (nodes.find((node) => node.id === id)?.type === "start") return;
|
||||
setNodes((ns) => ns.filter((n) => n.id !== id));
|
||||
setEdges((es) => es.filter((e) => e.source !== id && e.target !== id));
|
||||
if (editingNodeId === id) onEditingNodeIdChange(null);
|
||||
},
|
||||
[editingNodeId, nodes, onEditingNodeIdChange, setNodes, setEdges],
|
||||
);
|
||||
|
||||
const handleNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => {
|
||||
const startIds = new Set(
|
||||
nodes.filter((node) => node.type === "start").map((node) => node.id),
|
||||
);
|
||||
onNodesChange(
|
||||
changes.filter(
|
||||
(change) => change.type !== "remove" || !startIds.has(change.id),
|
||||
),
|
||||
);
|
||||
},
|
||||
[nodes, onNodesChange],
|
||||
);
|
||||
|
||||
const updateEdgeData = useCallback(
|
||||
(
|
||||
id: string,
|
||||
patch: WorkflowGraph["edges"][number]["data"],
|
||||
) => {
|
||||
setEdges((es) =>
|
||||
es.map((e) =>
|
||||
e.id === id ? { ...e, data: { ...(e.data ?? {}), ...patch } } : e,
|
||||
),
|
||||
);
|
||||
},
|
||||
[setEdges],
|
||||
);
|
||||
|
||||
const deleteEdge = useCallback(
|
||||
(id: string) => {
|
||||
setEdges((es) => es.filter((e) => e.id !== id));
|
||||
if (editingEdgeId === id) onEditingEdgeIdChange(null);
|
||||
},
|
||||
[editingEdgeId, onEditingEdgeIdChange, setEdges],
|
||||
);
|
||||
|
||||
const canCreateFromSource = useCallback(
|
||||
(id: string) => {
|
||||
const source = nodes.find((node) => node.id === id);
|
||||
if (!source) return false;
|
||||
const spec = specsByType[source.type as string];
|
||||
if (!spec?.hasSource) return false;
|
||||
const outgoingCount = edges.filter((edge) => edge.source === id).length;
|
||||
if (
|
||||
spec.constraints.maxOutgoing !== undefined &&
|
||||
outgoingCount >= spec.constraints.maxOutgoing
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return source.type === "agent" || outgoingCount === 0;
|
||||
},
|
||||
[edges, nodes, specsByType],
|
||||
);
|
||||
const onConnectEnd = useCallback<OnConnectEnd>(
|
||||
(event, connectionState) => {
|
||||
if (
|
||||
connectionState.isValid ||
|
||||
connectionState.toNode ||
|
||||
connectionState.fromHandle?.type !== "source" ||
|
||||
!connectionState.fromNode ||
|
||||
!canCreateFromSource(connectionState.fromNode.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const pointer = "changedTouches" in event
|
||||
? event.changedTouches[0]
|
||||
: event;
|
||||
if (!pointer) return;
|
||||
setAddSourceId(connectionState.fromNode.id);
|
||||
setAddPosition(
|
||||
screenToFlowPosition({ x: pointer.clientX, y: pointer.clientY }),
|
||||
);
|
||||
setAddOpen(true);
|
||||
},
|
||||
[canCreateFromSource, screenToFlowPosition],
|
||||
);
|
||||
const openSettings = useCallback(() => {
|
||||
onDebugOpenChange(false);
|
||||
onEditingNodeIdChange(null);
|
||||
onEditingEdgeIdChange(null);
|
||||
setSettingsOpen(true);
|
||||
}, [
|
||||
onDebugOpenChange,
|
||||
onEditingEdgeIdChange,
|
||||
onEditingNodeIdChange,
|
||||
setSettingsOpen,
|
||||
]);
|
||||
|
||||
const nodeActions = useMemo(
|
||||
() => ({
|
||||
edit: (id: string) => {
|
||||
setSettingsOpen(false);
|
||||
onDebugOpenChange(false);
|
||||
onEditingEdgeIdChange(null);
|
||||
onEditingNodeIdChange(id);
|
||||
},
|
||||
remove: deleteNode,
|
||||
}),
|
||||
[
|
||||
deleteNode,
|
||||
onDebugOpenChange,
|
||||
onEditingEdgeIdChange,
|
||||
onEditingNodeIdChange,
|
||||
setSettingsOpen,
|
||||
],
|
||||
);
|
||||
const edgeActions = useMemo(
|
||||
() => ({
|
||||
edit: (id: string) => {
|
||||
setSettingsOpen(false);
|
||||
onDebugOpenChange(false);
|
||||
onEditingNodeIdChange(null);
|
||||
onEditingEdgeIdChange(id);
|
||||
},
|
||||
remove: deleteEdge,
|
||||
}),
|
||||
[
|
||||
deleteEdge,
|
||||
onDebugOpenChange,
|
||||
onEditingEdgeIdChange,
|
||||
onEditingNodeIdChange,
|
||||
setSettingsOpen,
|
||||
],
|
||||
);
|
||||
|
||||
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 addableSpecs = Object.values(specsByType).filter((s) => s.addable);
|
||||
const canAddSpec = useCallback(
|
||||
(spec: RuntimeNodeSpec) => {
|
||||
const limit = spec.constraints.maxInstances;
|
||||
if (limit === undefined) return true;
|
||||
return nodes.filter((node) => node.type === spec.type).length < limit;
|
||||
},
|
||||
[nodes],
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeSpecsContext.Provider value={specsByType}>
|
||||
<ActiveNodeContext.Provider value={activeNodeId ?? null}>
|
||||
<NodeActionContext.Provider value={nodeActions}>
|
||||
<EdgeActionContext.Provider value={edgeActions}>
|
||||
<div className="relative h-full w-full min-h-[560px]">
|
||||
<section className="relative h-full w-full overflow-hidden rounded-2xl border border-hairline bg-canvas-soft shadow-sm">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -right-24 -top-24 z-0 h-80 w-80 rounded-full opacity-30 blur-3xl"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, var(--gradient-sky), transparent 68%)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -bottom-28 left-1/4 z-0 h-72 w-72 rounded-full opacity-25 blur-3xl"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, var(--gradient-lavender), transparent 68%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onConnectEnd={onConnectEnd}
|
||||
isValidConnection={isValidConnection}
|
||||
onPaneClick={() => {
|
||||
onEditingEdgeIdChange(null);
|
||||
}}
|
||||
fitView
|
||||
proOptions={{ hideAttribution: true }}
|
||||
defaultEdgeOptions={{ type: "condition", animated: true }}
|
||||
>
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
gap={22}
|
||||
size={1}
|
||||
color="var(--hairline-strong)"
|
||||
/>
|
||||
<Controls
|
||||
className="!rounded-xl !border !border-hairline !bg-card !shadow-sm [&_button]:!border-hairline [&_button]:!bg-card [&_button]:!text-foreground"
|
||||
/>
|
||||
<Panel position="top-left">
|
||||
<TooltipProvider>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="h-10 w-10 rounded-full shadow-sm"
|
||||
aria-label="添加节点"
|
||||
onClick={() => {
|
||||
setAddSourceId(null);
|
||||
setAddPosition(null);
|
||||
setAddOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={17} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">添加节点</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="h-10 w-10 rounded-full border-hairline-strong bg-card text-foreground shadow-sm hover:bg-surface-strong"
|
||||
aria-label="工作流设置"
|
||||
onClick={openSettings}
|
||||
>
|
||||
<Settings2 size={17} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">工作流设置</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="h-10 w-10 rounded-full border-hairline-strong bg-card text-foreground shadow-sm hover:bg-surface-strong"
|
||||
aria-label="动态变量"
|
||||
onClick={onOpenDynamicVariables}
|
||||
>
|
||||
<Braces size={17} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">动态变量</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
</section>
|
||||
|
||||
{/* 添加节点弹窗 */}
|
||||
<Dialog
|
||||
open={addOpen}
|
||||
onOpenChange={(open) => {
|
||||
setAddOpen(open);
|
||||
if (!open) {
|
||||
setAddSourceId(null);
|
||||
setAddPosition(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="gap-0 overflow-hidden border border-hairline bg-card p-0 shadow-2xl sm:max-w-[500px]">
|
||||
<DialogHeader className="relative overflow-hidden border-b border-hairline px-6 py-6 pr-16">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -right-14 -top-16 h-40 w-40 rounded-full opacity-40 blur-3xl"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, var(--gradient-sky), transparent 68%)",
|
||||
}}
|
||||
/>
|
||||
<div className="relative flex items-start gap-4">
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
|
||||
<Plus size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="caption-label text-muted-soft">
|
||||
节点目录
|
||||
</div>
|
||||
<DialogTitle className="font-display display-sm mt-1 text-ink">
|
||||
添加节点
|
||||
</DialogTitle>
|
||||
<DialogDescription className="mt-2 leading-6">
|
||||
{addSourceId
|
||||
? "选择节点类型,将在当前节点下方创建并自动连接。"
|
||||
: "选择节点类型并添加到画布中央,随后可编辑内容并建立连线。"}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="flex max-h-[440px] flex-col gap-3 overflow-y-auto bg-canvas-soft/70 p-4">
|
||||
{addableSpecs.length === 0 ? (
|
||||
<p className="rounded-2xl border border-dashed border-hairline-strong bg-card px-4 py-8 text-center text-sm text-muted-soft">
|
||||
暂无可添加的节点类型。
|
||||
</p>
|
||||
) : null}
|
||||
{addableSpecs.map((spec) => {
|
||||
const Icon = spec.icon;
|
||||
const canAdd = canAddSpec(spec);
|
||||
return (
|
||||
<button
|
||||
key={spec.type}
|
||||
type="button"
|
||||
disabled={!canAdd}
|
||||
className="group relative flex items-start gap-4 overflow-hidden rounded-2xl border border-hairline bg-card p-4 text-left shadow-sm transition-[border-color,box-shadow,transform] hover:-translate-y-0.5 hover:border-hairline-strong hover:shadow-md disabled:cursor-not-allowed disabled:opacity-55 disabled:hover:translate-y-0 disabled:hover:border-hairline disabled:hover:shadow-sm"
|
||||
onClick={() => addNode(spec)}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute left-5 right-5 top-0 h-px"
|
||||
style={{
|
||||
background: `linear-gradient(90deg, transparent, var(${accentVar(spec.accent)}), transparent)`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-foreground transition-transform group-hover:scale-105"
|
||||
style={{
|
||||
background: `color-mix(in srgb, var(${accentVar(spec.accent)}) 28%, var(--surface-strong))`,
|
||||
}}
|
||||
>
|
||||
<Icon size={17} />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
||||
<span>{spec.displayName}</span>
|
||||
{!canAdd && (
|
||||
<span className="rounded-full bg-surface-strong px-2 py-0.5 text-[10px] font-normal text-muted-foreground">
|
||||
已添加
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
{spec.description}
|
||||
</div>
|
||||
</div>
|
||||
<Plus
|
||||
size={15}
|
||||
className="mt-1 shrink-0 text-muted-soft transition-colors group-hover:text-foreground"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{(debugOpen ||
|
||||
settingsOpen ||
|
||||
(editingNode && editingSpec) ||
|
||||
editingEdge) && (
|
||||
<aside className="absolute inset-y-0 right-0 z-40 flex w-1/2 flex-col overflow-hidden rounded-r-2xl border-l border-hairline bg-card shadow-2xl">
|
||||
{debugOpen ? (
|
||||
debugPanel
|
||||
) : settingsOpen ||
|
||||
(editingNode && editingSpec) ||
|
||||
editingEdge ? (
|
||||
<>
|
||||
<div className="flex min-h-14 shrink-0 items-center gap-3 border-b border-hairline px-4 py-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
settingsOpen
|
||||
? "关闭工作流设置"
|
||||
: editingEdge
|
||||
? "关闭边编辑"
|
||||
: "关闭节点编辑"
|
||||
}
|
||||
title="关闭"
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-hairline-strong bg-card text-muted-foreground shadow-sm transition-colors hover:text-foreground"
|
||||
onClick={() => {
|
||||
if (settingsOpen) setSettingsOpen(false);
|
||||
else if (editingEdge) onEditingEdgeIdChange(null);
|
||||
else onEditingNodeIdChange(null);
|
||||
}}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
<h2 className="min-w-0 truncate text-sm font-medium text-foreground">
|
||||
{settingsOpen
|
||||
? "工作流设置"
|
||||
: editingEdge
|
||||
? "编辑连接条件"
|
||||
: `编辑${editingSpec?.displayName ?? "节点"}`}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="scrollbar-subtle min-h-0 flex-1 overflow-y-auto bg-canvas-soft px-4 pb-5 pt-4">
|
||||
{settingsOpen ? (
|
||||
<GlobalSettingsPanel
|
||||
settings={settings}
|
||||
onSettingsChange={onSettingsChange}
|
||||
modelOptions={modelOptions}
|
||||
toolOptions={toolOptions}
|
||||
knowledgeOptions={knowledgeOptions}
|
||||
/>
|
||||
) : editingNode && editingSpec ? (
|
||||
<NodeSettingsPanel
|
||||
key={editingNode.id}
|
||||
panel
|
||||
spec={editingSpec}
|
||||
data={editingNode.data as WorkflowNodeData}
|
||||
toolOptions={toolOptions}
|
||||
knowledgeOptions={knowledgeOptions}
|
||||
llmOptions={modelOptions.llm}
|
||||
asrOptions={modelOptions.asr}
|
||||
ttsOptions={modelOptions.tts}
|
||||
workflowSettings={settings}
|
||||
onChange={(patch) =>
|
||||
updateNodeData(editingNode.id, patch)
|
||||
}
|
||||
/>
|
||||
) : editingEdge ? (
|
||||
<EdgeSettingsPanel
|
||||
key={editingEdge.id}
|
||||
edge={editingEdge}
|
||||
sourceType={nodes.find((node) => node.id === editingEdge.source)?.type}
|
||||
onChange={(patch) =>
|
||||
updateEdgeData(editingEdge.id, patch)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
</EdgeActionContext.Provider>
|
||||
</NodeActionContext.Provider>
|
||||
</ActiveNodeContext.Provider>
|
||||
</NodeSpecsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user