- 新增完整的AI画布工具系统 - 可视化节点编辑器,支持拖拽连线 - 多种节点类型:文本输入、图片生成、视频生成等 - 智能连接验证和数据流转换 - 异步处理引擎,支持进度追踪和取消 - 批量处理系统,支持并发处理 - AI服务集成框架,支持多种AI API - 用户体验优化 - 智能弹框定位,防止被遮挡 - 节点删除功能(悬停删除按钮 + 键盘快捷键) - 通知系统和错误处理 - 快速开始模板 - 键盘快捷键支持 - 界面调整 - 暂时隐藏AI画布,保留代码 - 设置项目列表为首页 - 简化导航栏结构
249 lines
7.4 KiB
TypeScript
249 lines
7.4 KiB
TypeScript
import React, { useCallback, useMemo, useState, useEffect } from 'react';
|
|
import ReactFlow, {
|
|
Background,
|
|
Controls,
|
|
MiniMap,
|
|
useReactFlow,
|
|
ReactFlowProvider,
|
|
Connection,
|
|
Node
|
|
} from 'reactflow';
|
|
import 'reactflow/dist/style.css';
|
|
|
|
import { useCanvasStore } from '../../store/canvasStore';
|
|
import { NodeSelector } from './NodeSelector';
|
|
import { TextInputNode } from './nodes/TextInputNode';
|
|
import { PromptOptimizeNode } from './nodes/PromptOptimizeNode';
|
|
import { TextDisplayNode } from './nodes/TextDisplayNode';
|
|
import { ImageUploadNode } from './nodes/ImageUploadNode';
|
|
import { ImageGenerateNode } from './nodes/ImageGenerateNode';
|
|
import { ImageDisplayNode } from './nodes/ImageDisplayNode';
|
|
import { ImageEditNode } from './nodes/ImageEditNode';
|
|
import { VideoGenerateNode } from './nodes/VideoGenerateNode';
|
|
import { VideoPlayerNode } from './nodes/VideoPlayerNode';
|
|
import { FolderUploadNode } from './nodes/FolderUploadNode';
|
|
import { BatchResultGrid } from './nodes/BatchResultGrid';
|
|
import { NodeType } from '../../types/canvas';
|
|
import { ConnectionValidator, DataFlowConverter } from '../../utils/connectionValidator';
|
|
import { NotificationSystem, useGlobalNotifications, globalNotifications } from './NotificationSystem';
|
|
import { ProcessingIndicator, KeyboardShortcutsHint } from './ConnectionIndicator';
|
|
|
|
// 节点类型映射
|
|
const nodeTypes = {
|
|
textInput: TextInputNode,
|
|
promptOptimize: PromptOptimizeNode,
|
|
textDisplay: TextDisplayNode,
|
|
imageUpload: ImageUploadNode,
|
|
imageGenerate: ImageGenerateNode,
|
|
imageDisplay: ImageDisplayNode,
|
|
imageEdit: ImageEditNode,
|
|
videoGenerate: VideoGenerateNode,
|
|
videoPlayer: VideoPlayerNode,
|
|
folderUpload: FolderUploadNode,
|
|
batchGrid: BatchResultGrid,
|
|
};
|
|
|
|
const CanvasFlow: React.FC = () => {
|
|
const [showShortcuts, setShowShortcuts] = useState(false);
|
|
const { notifications, removeNotification } = useGlobalNotifications();
|
|
|
|
const {
|
|
nodes,
|
|
edges,
|
|
onNodesChange,
|
|
onEdgesChange,
|
|
onConnect: storeOnConnect,
|
|
showNodeSelector,
|
|
nodeSelectorPosition,
|
|
showNodeSelectorAt,
|
|
hideNodeSelector,
|
|
addNode,
|
|
updateNode,
|
|
deleteNode,
|
|
processingNodes
|
|
} = useCanvasStore();
|
|
|
|
const { screenToFlowPosition } = useReactFlow();
|
|
|
|
// 处理画布点击
|
|
const onPaneClick = useCallback((event: React.MouseEvent) => {
|
|
// 单击空白区域关闭节点选择器
|
|
if (event.detail === 1 && (event.target as HTMLElement).classList.contains('react-flow__pane')) {
|
|
hideNodeSelector();
|
|
}
|
|
|
|
// 双击空白区域显示节点选择器
|
|
if (event.detail === 2 && (event.target as HTMLElement).classList.contains('react-flow__pane')) {
|
|
const position = screenToFlowPosition({
|
|
x: event.clientX,
|
|
y: event.clientY,
|
|
});
|
|
|
|
showNodeSelectorAt(position);
|
|
}
|
|
}, [screenToFlowPosition, showNodeSelectorAt, hideNodeSelector]);
|
|
|
|
// 处理连线
|
|
const onConnect = useCallback((connection: Connection) => {
|
|
// 验证连接是否有效
|
|
const validation = ConnectionValidator.validateConnection(connection, nodes);
|
|
if (!validation.valid) {
|
|
globalNotifications.error('连接失败', validation.reason);
|
|
return;
|
|
}
|
|
|
|
// 添加连线到store
|
|
storeOnConnect(connection);
|
|
|
|
// 触发数据流处理
|
|
const sourceNode = nodes.find(n => n.id === connection.source);
|
|
const targetNode = nodes.find(n => n.id === connection.target);
|
|
|
|
if (sourceNode && targetNode && sourceNode.data?.result) {
|
|
// 使用数据转换器准备数据
|
|
const preparedData = DataFlowConverter.prepareDataForNode(
|
|
sourceNode.data.result,
|
|
sourceNode.type as NodeType,
|
|
targetNode.type as NodeType
|
|
);
|
|
|
|
// 更新目标节点数据
|
|
updateNode(connection.target!, {
|
|
data: {
|
|
...targetNode.data,
|
|
...preparedData
|
|
}
|
|
});
|
|
}
|
|
}, [storeOnConnect, nodes, updateNode]);
|
|
|
|
// 创建节点
|
|
const createNode = useCallback((nodeType: NodeType, position: { x: number; y: number }) => {
|
|
const newNode: Node = {
|
|
id: `${nodeType}-${Date.now()}`,
|
|
type: nodeType,
|
|
position,
|
|
data: {
|
|
label: nodeType
|
|
}
|
|
};
|
|
|
|
addNode(newNode);
|
|
globalNotifications.success('节点已创建', `已添加 ${nodeType} 节点`);
|
|
}, [addNode]);
|
|
|
|
// 键盘事件处理
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'F1') {
|
|
e.preventDefault();
|
|
setShowShortcuts(!showShortcuts);
|
|
}
|
|
if (e.key === 'Escape') {
|
|
hideNodeSelector();
|
|
}
|
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
// 删除选中的节点
|
|
const selectedNodes = nodes.filter(node => node.selected);
|
|
if (selectedNodes.length > 0) {
|
|
const nodeNames = selectedNodes.map(node => node.data?.title || node.type).join(', ');
|
|
if (confirm(`确定要删除选中的节点吗?\n${nodeNames}`)) {
|
|
selectedNodes.forEach(node => {
|
|
deleteNode(node.id);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
}, [showShortcuts, hideNodeSelector, nodes, deleteNode]);
|
|
|
|
// 自定义边样式
|
|
const edgeOptions = useMemo(() => ({
|
|
animated: true,
|
|
style: { strokeWidth: 2 }
|
|
}), []);
|
|
|
|
return (
|
|
<div className="w-full h-full relative" style={{ minHeight: '500px' }}>
|
|
<ReactFlow
|
|
nodes={nodes}
|
|
edges={edges}
|
|
onNodesChange={onNodesChange}
|
|
onEdgesChange={onEdgesChange}
|
|
onConnect={onConnect}
|
|
onPaneClick={onPaneClick}
|
|
nodeTypes={nodeTypes}
|
|
defaultEdgeOptions={edgeOptions}
|
|
fitView={nodes.length === 0}
|
|
fitViewOptions={{
|
|
padding: 0.1,
|
|
includeHiddenNodes: false,
|
|
minZoom: 0.1,
|
|
maxZoom: 2
|
|
}}
|
|
minZoom={0.1}
|
|
maxZoom={2}
|
|
defaultViewport={{ x: 0, y: 0, zoom: 1 }}
|
|
preventScrolling={false}
|
|
panOnDrag={true}
|
|
zoomOnScroll={true}
|
|
zoomOnPinch={true}
|
|
zoomOnDoubleClick={false}
|
|
className="bg-gray-50 w-full h-full"
|
|
>
|
|
<Background color="#e5e7eb" gap={20} />
|
|
<Controls />
|
|
<MiniMap
|
|
nodeColor="#3b82f6"
|
|
maskColor="rgba(0, 0, 0, 0.1)"
|
|
className="bg-white border border-gray-200 rounded-lg"
|
|
/>
|
|
</ReactFlow>
|
|
|
|
{/* 节点选择器 */}
|
|
{showNodeSelector && (
|
|
<NodeSelector
|
|
position={nodeSelectorPosition}
|
|
onSelect={createNode}
|
|
onClose={hideNodeSelector}
|
|
/>
|
|
)}
|
|
|
|
{/* 处理进度指示器 */}
|
|
{Array.from(processingNodes.entries()).map(([nodeId, info]) => (
|
|
<ProcessingIndicator
|
|
key={nodeId}
|
|
nodeId={nodeId}
|
|
progress={info.progress}
|
|
status={info.status}
|
|
/>
|
|
))}
|
|
|
|
{/* 通知系统 */}
|
|
<NotificationSystem
|
|
notifications={notifications}
|
|
onRemove={removeNotification}
|
|
/>
|
|
|
|
{/* 快捷键提示 */}
|
|
<KeyboardShortcutsHint visible={showShortcuts} />
|
|
|
|
{/* 帮助提示 */}
|
|
<button
|
|
onClick={() => setShowShortcuts(!showShortcuts)}
|
|
className="absolute bottom-4 right-4 bg-blue-600 text-white p-2 rounded-full shadow-lg hover:bg-blue-700 transition-colors"
|
|
title="显示快捷键 (F1)"
|
|
>
|
|
?
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const CanvasContainer: React.FC = () => {
|
|
return <CanvasFlow />;
|
|
};
|