Compare commits
4 Commits
f0863914c2
...
c9e8b0c123
| Author | SHA1 | Date | |
|---|---|---|---|
| c9e8b0c123 | |||
| 847b3e96c8 | |||
| 37bdf4dbfd | |||
| b57b2114cd |
@ -2,7 +2,7 @@
|
|||||||
# Description: 聚合 v1 版本的所有 API 路由
|
# Description: 聚合 v1 版本的所有 API 路由
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from app.api.v1.endpoints import chat, assistants, sessions, messages # Import messages router
|
from app.api.v1.endpoints import chat, assistants, sessions, messages, workflow # Import messages router
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
||||||
@ -10,3 +10,4 @@ api_router.include_router(chat.router, prefix="/chat", tags=["Chat"])
|
|||||||
api_router.include_router(assistants.router, prefix="/assistants", tags=["Assistants"])
|
api_router.include_router(assistants.router, prefix="/assistants", tags=["Assistants"])
|
||||||
api_router.include_router(sessions.router, prefix="/sessions", tags=["Sessions"])
|
api_router.include_router(sessions.router, prefix="/sessions", tags=["Sessions"])
|
||||||
api_router.include_router(messages.router, prefix="/messages", tags=["Messages"]) # Add messages router
|
api_router.include_router(messages.router, prefix="/messages", tags=["Messages"]) # Add messages router
|
||||||
|
api_router.include_router(workflow.router, prefix="/workflow", tags=["Workflow"]) # Add messages router
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
# File: backend/app/api/v1/endpoints/workflow.py (No significant changes needed)
|
||||||
|
# Description: 工作流相关的 API 路由 (uses the refactored service)
|
||||||
|
# ... (保持不变, 确保调用 refactored WorkflowService) ...
|
||||||
|
from typing import Optional
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from app.models.pydantic_models import WorkflowRunRequest, WorkflowRunResponse
|
||||||
|
from app.services.workflow_service import WorkflowService # Import the refactored class
|
||||||
|
from app.db.database import get_db_session # Import if service needs DB
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
workflow_service = WorkflowService()
|
||||||
|
|
||||||
|
@router.post("/run", response_model=WorkflowRunResponse)
|
||||||
|
async def run_workflow(
|
||||||
|
request: WorkflowRunRequest,
|
||||||
|
db: Optional[AsyncSession] = Depends(get_db_session) # Make DB optional or required based on component needs
|
||||||
|
):
|
||||||
|
print(f"收到运行工作流请求: {len(request.nodes)} 个节点, {len(request.edges)} 条边")
|
||||||
|
result = await workflow_service.execute_workflow(request.nodes, request.edges, db)
|
||||||
|
# No need to raise HTTPException here if service returns success=False
|
||||||
|
return result
|
||||||
86
backend/app/flow_components/base.py
Normal file
86
backend/app/flow_components/base.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# File: backend/app/flow_components/base.py (New)
|
||||||
|
# Description: Base class for all workflow node components
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import List, Dict, Any, Optional, Type, ClassVar
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession # Import if components need DB access
|
||||||
|
|
||||||
|
# --- Input/Output Field Definitions ---
|
||||||
|
# These classes help define the expected inputs and outputs for handles/UI fields
|
||||||
|
class InputField(BaseModel):
|
||||||
|
name: str # Internal name/key, matches handleId or data key
|
||||||
|
display_name: str # User-friendly name for UI
|
||||||
|
info: Optional[str] = None # Tooltip/description
|
||||||
|
field_type: str # e.g., 'str', 'int', 'float', 'bool', 'dict', 'code', 'prompt', 'llm', 'message'
|
||||||
|
required: bool = True
|
||||||
|
value: Any = None # Default value for UI fields
|
||||||
|
is_handle: bool = False # True if this input comes from a Handle connection
|
||||||
|
# Add more metadata as needed (e.g., options for dropdowns, range for sliders)
|
||||||
|
options: Optional[List[str]] = None
|
||||||
|
range_spec: Optional[Dict[str, float]] = None # e.g., {'min': 0, 'max': 1, 'step': 0.1}
|
||||||
|
|
||||||
|
class OutputField(BaseModel):
|
||||||
|
name: str # Internal name/key, matches handleId
|
||||||
|
display_name: str
|
||||||
|
field_type: str # Data type of the output handle
|
||||||
|
info: Optional[str] = None
|
||||||
|
|
||||||
|
# --- Base Component Class ---
|
||||||
|
class BaseComponent(ABC, BaseModel):
|
||||||
|
# Class variables for metadata (can be overridden by subclasses)
|
||||||
|
display_name: ClassVar[str] = "Base Component"
|
||||||
|
description: ClassVar[str] = "A base component for workflow nodes."
|
||||||
|
icon: ClassVar[Optional[str]] = None # Icon name (e.g., from Lucide)
|
||||||
|
name: ClassVar[str] # Unique internal name/type identifier (matches frontend node type)
|
||||||
|
|
||||||
|
# Instance variable to hold node-specific data from frontend
|
||||||
|
node_data: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
# Class variables defining inputs and outputs
|
||||||
|
inputs: ClassVar[List[InputField]] = []
|
||||||
|
outputs: ClassVar[List[OutputField]] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
arbitrary_types_allowed = True # Allow complex types like AsyncSession if needed
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def run(
|
||||||
|
self,
|
||||||
|
inputs: Dict[str, Any], # Resolved inputs from UI data and connected nodes
|
||||||
|
db: Optional[AsyncSession] = None # Pass DB session if needed
|
||||||
|
) -> Dict[str, Any]: # Return dictionary of output values keyed by output name
|
||||||
|
"""Executes the component's logic."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def validate_inputs(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Validates if required inputs are present."""
|
||||||
|
resolved_inputs = {}
|
||||||
|
missing = []
|
||||||
|
for field in self.inputs:
|
||||||
|
# Combine node_data (UI config) and resolved inputs from connections
|
||||||
|
value = inputs.get(field.name, self.node_data.get(field.name, field.value))
|
||||||
|
|
||||||
|
if field.required and value is None:
|
||||||
|
missing.append(field.display_name)
|
||||||
|
# TODO: Add type validation based on field.field_type
|
||||||
|
resolved_inputs[field.name] = value
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"节点 '{self.display_name}' 缺少必需的输入: {', '.join(missing)}")
|
||||||
|
return resolved_inputs
|
||||||
|
|
||||||
|
|
||||||
|
# --- Component Registry ---
|
||||||
|
# Simple dictionary to map node type strings to component classes
|
||||||
|
component_registry: Dict[str, Type[BaseComponent]] = {}
|
||||||
|
|
||||||
|
def register_component(cls: Type[BaseComponent]):
|
||||||
|
"""Decorator to register component classes."""
|
||||||
|
if not hasattr(cls, 'name') or not cls.name:
|
||||||
|
raise ValueError(f"Component class {cls.__name__} must have a 'name' attribute.")
|
||||||
|
if cls.name in component_registry:
|
||||||
|
print(f"警告: 组件 '{cls.name}' 被重复注册。")
|
||||||
|
component_registry[cls.name] = cls
|
||||||
|
print(f"已注册组件: {cls.name}")
|
||||||
|
return cls
|
||||||
27
backend/app/flow_components/chat_input.py
Normal file
27
backend/app/flow_components/chat_input.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# File: backend/app/flow_components/chat_input.py (New)
|
||||||
|
# Description: Backend component for ChatInputNode
|
||||||
|
|
||||||
|
from .base import BaseComponent, InputField, OutputField, register_component
|
||||||
|
from typing import ClassVar, Dict, Any, Optional, List
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
@register_component
|
||||||
|
class ChatInputNodeComponent(BaseComponent):
|
||||||
|
name: ClassVar[str] = "chatInputNode" # Matches frontend type
|
||||||
|
display_name: ClassVar[str] = "Chat Input"
|
||||||
|
description: ClassVar[str] = "从 Playground 获取聊天输入。"
|
||||||
|
icon: ClassVar[str] = "MessageCircleQuestion"
|
||||||
|
|
||||||
|
inputs: ClassVar[List[InputField]] = [
|
||||||
|
InputField(name="text", display_name="Text", field_type="str", required=False, is_handle=False, info="用户输入的文本或默认文本。"),
|
||||||
|
]
|
||||||
|
outputs: ClassVar[List[OutputField]] = [
|
||||||
|
OutputField(name="message-output", display_name="Message", field_type="message", info="输出的聊天消息。") # Use 'message' type
|
||||||
|
]
|
||||||
|
|
||||||
|
async def run(self, inputs: Dict[str, Any], db: Optional[AsyncSession] = None) -> Dict[str, Any]:
|
||||||
|
# Inputs are already validated and resolved by the base class/executor
|
||||||
|
text_input = inputs.get("text", self.node_data.get("text", "")) # Get text from UI data or default
|
||||||
|
print(f"ChatInputNode ({self.node_data.get('id', 'N/A')}): 输出文本 '{text_input}'")
|
||||||
|
# Output format should match the defined output field name
|
||||||
|
return {"message-output": text_input} # Output the text
|
||||||
38
backend/app/flow_components/chat_output.py
Normal file
38
backend/app/flow_components/chat_output.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# File: backend/app/flow_components/chat_output.py (New)
|
||||||
|
# Description: Backend component for ChatOutputNode
|
||||||
|
|
||||||
|
from .base import BaseComponent, InputField, OutputField, register_component
|
||||||
|
from typing import ClassVar, Dict, Any, Optional, List
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
@register_component
|
||||||
|
class ChatOutputNodeComponent(BaseComponent):
|
||||||
|
name: ClassVar[str] = "chatOutputNode"
|
||||||
|
display_name: ClassVar[str] = "Chat Output"
|
||||||
|
description: ClassVar[str] = "在 Playground 显示聊天消息。"
|
||||||
|
icon: ClassVar[str] = "MessageCircleReply"
|
||||||
|
|
||||||
|
inputs: ClassVar[List[InputField]] = [
|
||||||
|
InputField(name="message-input", display_name="Message", field_type="message", required=True, is_handle=True, info="连接要显示的消息。"),
|
||||||
|
InputField(name="displayText", display_name="Text", field_type="str", required=False, is_handle=False, info="(可选)覆盖显示的文本。"),
|
||||||
|
]
|
||||||
|
outputs: ClassVar[List[OutputField]] = [
|
||||||
|
# This node typically doesn't output further, but could pass through
|
||||||
|
# OutputField(name="output", display_name="Output", field_type="message")
|
||||||
|
]
|
||||||
|
|
||||||
|
async def run(self, inputs: Dict[str, Any], db: Optional[AsyncSession] = None) -> Dict[str, Any]:
|
||||||
|
message_input = inputs.get("message-input")
|
||||||
|
display_override = inputs.get("displayText", self.node_data.get("displayText")) # Check UI data too
|
||||||
|
|
||||||
|
if message_input is None:
|
||||||
|
raise ValueError("ChatOutputNode 未收到输入消息。")
|
||||||
|
|
||||||
|
# Determine what to "output" (in this context, what the workflow considers the result)
|
||||||
|
final_text = display_override if display_override else str(message_input)
|
||||||
|
|
||||||
|
print(f"ChatOutputNode ({self.node_data.get('id', 'N/A')}): 最终显示 '{final_text[:50]}...'")
|
||||||
|
|
||||||
|
# Since this is often a terminal node for execution, return the processed input
|
||||||
|
# The executor will decide how to handle this final output
|
||||||
|
return {"final_output": final_text} # Use a consistent key like 'final_output'
|
||||||
77
backend/app/flow_components/llm_node.py
Normal file
77
backend/app/flow_components/llm_node.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# File: backend/app/flow_components/llm_node.py (New)
|
||||||
|
# Description: Backend component for LLMNode
|
||||||
|
|
||||||
|
from .base import BaseComponent, InputField, OutputField, register_component
|
||||||
|
from typing import ClassVar, Dict, Any, Optional, List
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from app.services.chat_service import ChatService # Assuming ChatService can be used or adapted
|
||||||
|
from app.core.config import OPENAI_API_KEY
|
||||||
|
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, BaseMessage
|
||||||
|
|
||||||
|
# Instantiate or get ChatService instance
|
||||||
|
# This might need better dependency injection in a real app
|
||||||
|
chat_service_instance = ChatService(default_api_key=OPENAI_API_KEY)
|
||||||
|
|
||||||
|
@register_component
|
||||||
|
class LLMNodeComponent(BaseComponent):
|
||||||
|
name: ClassVar[str] = "llmNode"
|
||||||
|
display_name: ClassVar[str] = "LLM 调用"
|
||||||
|
description: ClassVar[str] = "使用大语言模型生成文本。"
|
||||||
|
icon: ClassVar[str] = "BrainCircuit"
|
||||||
|
|
||||||
|
inputs: ClassVar[List[InputField]] = [
|
||||||
|
InputField(name="input-text", display_name="输入", field_type="message", required=True, is_handle=True, info="连接输入的文本或消息。"),
|
||||||
|
InputField(name="systemPrompt", display_name="系统提示", field_type="str", required=True, is_handle=False, info="定义助手的角色和行为。"),
|
||||||
|
InputField(name="model", display_name="模型名称", field_type="str", required=True, is_handle=False, info="要使用的 LLM 模型。"), # Add options if needed
|
||||||
|
InputField(name="temperature", display_name="温度", field_type="float", required=True, is_handle=False, value=0.7, range_spec={'min': 0, 'max': 1, 'step': 0.1}),
|
||||||
|
InputField(name="apiKey", display_name="API Key", field_type="secret", required=False, is_handle=False, info="(不安全)覆盖默认 API Key。"),
|
||||||
|
# Add other parameters like max_tokens, etc. as InputFields
|
||||||
|
]
|
||||||
|
outputs: ClassVar[List[OutputField]] = [
|
||||||
|
OutputField(name="output-message", display_name="Message", field_type="message", info="LLM 生成的消息。")
|
||||||
|
]
|
||||||
|
|
||||||
|
async def run(self, inputs: Dict[str, Any], db: Optional[AsyncSession] = None) -> Dict[str, Any]:
|
||||||
|
prompt_input = inputs.get("input-text")
|
||||||
|
system_prompt = inputs.get("systemPrompt")
|
||||||
|
model = inputs.get("model")
|
||||||
|
temperature = inputs.get("temperature")
|
||||||
|
# api_key = inputs.get("apiKey") # Handle API key securely if used
|
||||||
|
|
||||||
|
if not prompt_input or not system_prompt or not model or temperature is None:
|
||||||
|
raise ValueError("LLMNode 配置或输入不完整。")
|
||||||
|
|
||||||
|
print(f"LLMNode ({self.node_data.get('id', 'N/A')}): 运行模型 '{model}' (Temp: {temperature})")
|
||||||
|
print(f" System Prompt: {system_prompt[:50]}...")
|
||||||
|
print(f" Input Prompt: {prompt_input[:50]}...")
|
||||||
|
|
||||||
|
# --- Adapt ChatService or LangChain call ---
|
||||||
|
# This simplified call assumes a method that takes direct inputs
|
||||||
|
# In reality, you might build a small LangChain chain here
|
||||||
|
try:
|
||||||
|
# Construct messages for a more robust call
|
||||||
|
messages: List[BaseMessage] = []
|
||||||
|
if system_prompt:
|
||||||
|
messages.append(SystemMessage(content=system_prompt))
|
||||||
|
# Assume input-text provides the user message content
|
||||||
|
messages.append(HumanMessage(content=str(prompt_input))) # Ensure it's a string
|
||||||
|
|
||||||
|
# Simplified call - needs adaptation based on ChatService structure
|
||||||
|
# Maybe ChatService needs a method like:
|
||||||
|
# async def invoke_llm(self, messages: List[BaseMessage], model_name: str, temperature: float, ...) -> str:
|
||||||
|
# result = await chat_service_instance.invoke_llm(messages, model, temperature)
|
||||||
|
|
||||||
|
# --- Temporary Simulation ---
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
result = f"AI回复(模拟): 处理了 '{str(prompt_input)[:20]}...'"
|
||||||
|
# --- End Simulation ---
|
||||||
|
|
||||||
|
print(f"LLMNode Output: {result[:50]}...")
|
||||||
|
return {"output-message": result}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"LLMNode 执行失败: {e}")
|
||||||
|
raise # Re-raise the exception for the executor to handle
|
||||||
|
|
||||||
|
# Need asyncio for simulation
|
||||||
|
import asyncio
|
||||||
@ -7,6 +7,7 @@ from app.api.v1.api import api_router as api_router_v1
|
|||||||
import app.core.config # Ensure config is loaded
|
import app.core.config # Ensure config is loaded
|
||||||
from app.db.database import create_db_and_tables # Import table creation function
|
from app.db.database import create_db_and_tables # Import table creation function
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from app.flow_components import base, chat_input, llm_node, chat_output # Add other component modules here
|
||||||
|
|
||||||
# --- Lifespan context manager for startup/shutdown events ---
|
# --- Lifespan context manager for startup/shutdown events ---
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@ -17,6 +18,7 @@ async def lifespan(app: FastAPI):
|
|||||||
print("数据库表已检查/创建。")
|
print("数据库表已检查/创建。")
|
||||||
# You can add the default assistant creation here if needed,
|
# You can add the default assistant creation here if needed,
|
||||||
# but doing it in the service/model definition might be simpler for defaults.
|
# but doing it in the service/model definition might be simpler for defaults.
|
||||||
|
print(f"已注册组件: {list(base.component_registry.keys())}")
|
||||||
yield
|
yield
|
||||||
# Shutdown actions
|
# Shutdown actions
|
||||||
print("应用程序关闭中...")
|
print("应用程序关闭中...")
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
# Description: Pydantic 模型定义 API 数据结构
|
# Description: Pydantic 模型定义 API 数据结构
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import Optional, List
|
from typing import Dict, Optional, List
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime # Use datetime directly
|
from datetime import datetime # Use datetime directly
|
||||||
|
|
||||||
@ -91,3 +91,42 @@ class MessageRead(MessageBase):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
# --- Workflow Node/Edge Models (for API request/response) ---
|
||||||
|
# Mirrors React Flow structure loosely
|
||||||
|
class NodeData(BaseModel):
|
||||||
|
# Define common fields or use Dict[str, Any]
|
||||||
|
label: Optional[str] = None
|
||||||
|
text: Optional[str] = None # For ChatInput
|
||||||
|
displayText: Optional[str] = None # For ChatOutput
|
||||||
|
model: Optional[str] = None # For LLMNode
|
||||||
|
temperature: Optional[float] = None # For LLMNode
|
||||||
|
systemPrompt: Optional[str] = None # For LLMNode
|
||||||
|
# Add other potential data fields from your nodes
|
||||||
|
# Use Extra.allow for flexibility if needed:
|
||||||
|
# class Config:
|
||||||
|
# extra = 'allow'
|
||||||
|
|
||||||
|
class NodeModel(BaseModel):
|
||||||
|
id: str
|
||||||
|
type: str # e.g., 'chatInputNode', 'llmNode'
|
||||||
|
position: Dict[str, float] # { x: number, y: number }
|
||||||
|
data: NodeData # Use the specific data model
|
||||||
|
|
||||||
|
class EdgeModel(BaseModel):
|
||||||
|
id: str
|
||||||
|
source: str
|
||||||
|
target: str
|
||||||
|
sourceHandle: Optional[str] = None
|
||||||
|
targetHandle: Optional[str] = None
|
||||||
|
|
||||||
|
# --- Workflow Execution Models ---
|
||||||
|
class WorkflowRunRequest(BaseModel):
|
||||||
|
nodes: List[NodeModel]
|
||||||
|
edges: List[EdgeModel]
|
||||||
|
|
||||||
|
class WorkflowRunResponse(BaseModel):
|
||||||
|
success: bool
|
||||||
|
message: Optional[str] = None
|
||||||
|
output: Optional[str] = None # The final output text
|
||||||
|
output_node_id: Optional[str] = None # ID of the node that produced the output
|
||||||
|
|||||||
@ -0,0 +1,154 @@
|
|||||||
|
# File: backend/app/services/workflow_service.py (Refactor)
|
||||||
|
# Description: 服务层,使用组件化方式执行工作流
|
||||||
|
|
||||||
|
from typing import List, Dict, Optional, Any, Tuple
|
||||||
|
from app.models.pydantic_models import NodeModel, EdgeModel, WorkflowRunResponse
|
||||||
|
from app.flow_components.base import component_registry, BaseComponent # Import registry and base
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
import graphlib # For topological sort
|
||||||
|
|
||||||
|
class WorkflowExecutionError(Exception): pass
|
||||||
|
|
||||||
|
class WorkflowService:
|
||||||
|
"""执行工作流的服务 (组件化)"""
|
||||||
|
|
||||||
|
async def execute_workflow(
|
||||||
|
self,
|
||||||
|
nodes: List[NodeModel],
|
||||||
|
edges: List[EdgeModel],
|
||||||
|
db: Optional[AsyncSession] = None # Pass DB if components need it
|
||||||
|
) -> WorkflowRunResponse:
|
||||||
|
print("开始执行工作流 (组件化)...")
|
||||||
|
|
||||||
|
# 1. 构建依赖图 & 拓扑排序
|
||||||
|
try:
|
||||||
|
graph: Dict[str, set[str]] = {node.id: set() for node in nodes}
|
||||||
|
node_map: Dict[str, NodeModel] = {node.id: node for node in nodes}
|
||||||
|
handle_to_node_map: Dict[str, Dict[str, str]] = {node.id: {} for node in nodes} # {node_id: {handle_id: input_name}}
|
||||||
|
|
||||||
|
# Pre-populate handle map based on component definitions
|
||||||
|
for node in nodes:
|
||||||
|
if node.type in component_registry:
|
||||||
|
component_cls = component_registry[node.type]
|
||||||
|
for input_field in component_cls.inputs:
|
||||||
|
if input_field.is_handle:
|
||||||
|
handle_to_node_map[node.id][input_field.name] = input_field.name # Map handle ID to input name
|
||||||
|
|
||||||
|
|
||||||
|
for edge in edges:
|
||||||
|
if edge.source in graph and edge.target in graph:
|
||||||
|
graph[edge.source].add(edge.target) # source depends on target? No, target depends on source
|
||||||
|
# Let's reverse: target depends on source
|
||||||
|
# graph[edge.target].add(edge.source) # This seems wrong for graphlib
|
||||||
|
# graphlib expects {node: {dependencies}}
|
||||||
|
# So, target node depends on source node
|
||||||
|
graph[edge.target].add(edge.source)
|
||||||
|
|
||||||
|
|
||||||
|
sorter = graphlib.TopologicalSorter(graph)
|
||||||
|
execution_order = list(sorter.static_order())
|
||||||
|
print(f"执行顺序: {execution_order}")
|
||||||
|
|
||||||
|
except graphlib.CycleError as e:
|
||||||
|
print(f"工作流中存在循环: {e}")
|
||||||
|
return WorkflowRunResponse(success=False, message="工作流中检测到循环,无法执行。")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"构建执行图时出错: {e}")
|
||||||
|
return WorkflowRunResponse(success=False, message=f"构建执行图失败: {e}")
|
||||||
|
|
||||||
|
# 2. 执行节点
|
||||||
|
node_outputs: Dict[str, Dict[str, Any]] = {} # Store outputs {node_id: {output_handle_id: value}}
|
||||||
|
final_output_value: Any = None
|
||||||
|
final_output_node_id: Optional[str] = None
|
||||||
|
|
||||||
|
for node_id in execution_order:
|
||||||
|
node = node_map.get(node_id)
|
||||||
|
if not node:
|
||||||
|
print(f"错误: 找不到节点 {node_id}")
|
||||||
|
continue # Should not happen if graph is correct
|
||||||
|
|
||||||
|
component_cls = component_registry.get(node.type)
|
||||||
|
if not component_cls:
|
||||||
|
print(f"警告: 找不到节点类型 '{node.type}' 的后端组件,跳过节点 {node_id}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n--- 执行节点: {node_id} (类型: {node.type}) ---")
|
||||||
|
|
||||||
|
# a. 实例化组件,传入节点数据
|
||||||
|
component_instance = component_cls(node_data=node.data.model_dump())
|
||||||
|
|
||||||
|
# b. 收集输入值
|
||||||
|
inputs_for_run: Dict[str, Any] = {}
|
||||||
|
try:
|
||||||
|
# Gather inputs from connected parent nodes
|
||||||
|
for edge in edges:
|
||||||
|
if edge.target == node_id:
|
||||||
|
source_node_id = edge.source
|
||||||
|
source_handle_id = edge.sourceHandle
|
||||||
|
target_handle_id = edge.targetHandle
|
||||||
|
|
||||||
|
if source_node_id in node_outputs and source_handle_id in node_outputs[source_node_id]:
|
||||||
|
# Map target handle ID to the correct input name for the component's run method
|
||||||
|
input_name = handle_to_node_map.get(node_id, {}).get(target_handle_id)
|
||||||
|
if input_name:
|
||||||
|
input_value = node_outputs[source_node_id][source_handle_id]
|
||||||
|
inputs_for_run[input_name] = input_value
|
||||||
|
print(f" 输入 '{input_name}' 来自 {source_node_id}.{source_handle_id} = {str(input_value)[:50]}...")
|
||||||
|
else:
|
||||||
|
print(f"警告: 找不到节点 {node_id} 的目标 Handle '{target_handle_id}' 对应的输入字段名。")
|
||||||
|
else:
|
||||||
|
# This might happen if the source node hasn't run or didn't produce the expected output
|
||||||
|
print(f"警告: 找不到来自 {source_node_id}.{source_handle_id} 的输出,无法连接到 {node_id}.{target_handle_id}")
|
||||||
|
# Check if the input is required
|
||||||
|
target_input_field = next((f for f in component_instance.inputs if f.name == handle_to_node_map.get(node_id, {}).get(target_handle_id)), None)
|
||||||
|
if target_input_field and target_input_field.required:
|
||||||
|
raise WorkflowExecutionError(f"节点 '{component_instance.display_name}' ({node_id}) 的必需输入 '{target_input_field.display_name}' 未连接或上游节点未提供输出。")
|
||||||
|
|
||||||
|
|
||||||
|
# c. 验证并合并来自 UI 的输入 (node.data)
|
||||||
|
# validate_inputs should combine inputs_for_run and node_data
|
||||||
|
resolved_inputs = component_instance.validate_inputs(inputs_for_run)
|
||||||
|
print(f" 解析后的输入: {resolved_inputs}")
|
||||||
|
|
||||||
|
|
||||||
|
# d. 执行组件的 run 方法
|
||||||
|
outputs = await component_instance.run(resolved_inputs, db)
|
||||||
|
node_outputs[node_id] = outputs # Store outputs
|
||||||
|
print(f" 节点输出: {outputs}")
|
||||||
|
|
||||||
|
# e. 检查是否为最终输出 (来自 ChatOutputNode)
|
||||||
|
if node.type == 'chatOutputNode' and 'final_output' in outputs:
|
||||||
|
final_output_value = outputs['final_output']
|
||||||
|
final_output_node_id = node_id
|
||||||
|
|
||||||
|
except ValueError as e: # Catch validation errors
|
||||||
|
print(f"节点 {node_id} 输入验证失败: {e}")
|
||||||
|
return WorkflowRunResponse(success=False, message=f"节点 '{component_instance.display_name}' ({node_id}) 执行错误: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"节点 {node_id} 执行时发生错误: {e}")
|
||||||
|
# Log traceback e
|
||||||
|
return WorkflowRunResponse(success=False, message=f"节点 '{component_instance.display_name}' ({node_id}) 执行失败: {e}")
|
||||||
|
|
||||||
|
print("\n--- 工作流执行完成 ---")
|
||||||
|
if final_output_value is not None:
|
||||||
|
return WorkflowRunResponse(
|
||||||
|
success=True,
|
||||||
|
output=str(final_output_value), # Ensure output is string
|
||||||
|
output_node_id=final_output_node_id,
|
||||||
|
message="工作流执行成功"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Workflow finished but didn't produce output via a ChatOutputNode
|
||||||
|
print("警告: 工作流执行完毕,但未找到指定的 ChatOutputNode 或其未产生 'final_output'。")
|
||||||
|
# Find the last node's output as a fallback?
|
||||||
|
last_node_id = execution_order[-1] if execution_order else None
|
||||||
|
fallback_output = node_outputs.get(last_node_id, {})
|
||||||
|
output_key = next(iter(fallback_output)) if fallback_output else None
|
||||||
|
fallback_value = fallback_output.get(output_key) if output_key else "执行完成,但无明确输出。"
|
||||||
|
|
||||||
|
return WorkflowRunResponse(
|
||||||
|
success=True, # Or False depending on requirements
|
||||||
|
output=str(fallback_value),
|
||||||
|
output_node_id=last_node_id,
|
||||||
|
message="工作流执行完成,但未找到指定的输出节点。"
|
||||||
|
)
|
||||||
69
frontend/app/workflow/components/ChatInputNode.tsx
Normal file
69
frontend/app/workflow/components/ChatInputNode.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// File: frontend/app/workflow/components/ChatInputNode.tsx (新建)
|
||||||
|
// Description: 自定义 ChatInput 节点
|
||||||
|
|
||||||
|
import React, { memo, useCallback, ChangeEvent } from 'react';
|
||||||
|
import { Handle, Position, useReactFlow, Node } from 'reactflow';
|
||||||
|
import { MessageCircleQuestion } from 'lucide-react'; // 使用合适的图标
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import type { ChatInputNodeData, CustomNodeProps } from './types';
|
||||||
|
|
||||||
|
const ChatInputNodeComponent = ({ id, data, isConnectable }: CustomNodeProps<ChatInputNodeData>) => {
|
||||||
|
const { setNodes } = useReactFlow<ChatInputNodeData>();
|
||||||
|
|
||||||
|
const handleTextChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const newText = event.target.value;
|
||||||
|
setNodes((nds: Node<ChatInputNodeData>[]) =>
|
||||||
|
nds.map((node) => {
|
||||||
|
if (node.id === id) {
|
||||||
|
return { ...node, data: { ...node.data, text: newText } };
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [id, setNodes]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="react-flow__node-genericNode nopan bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-lg w-96"> {/* 调整宽度 */}
|
||||||
|
{/* 节点头部 */}
|
||||||
|
<div className="bg-gray-100 dark:bg-gray-700 p-3 border-b border-gray-300 dark:border-gray-600">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MessageCircleQuestion size={18} className="text-gray-700 dark:text-gray-300" />
|
||||||
|
<strong className="text-gray-800 dark:text-gray-200">Chat Input</strong>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">从 Playground 获取聊天输入。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 节点内容 */}
|
||||||
|
<div className="p-4 space-y-2">
|
||||||
|
<div className="nodrag">
|
||||||
|
<Label htmlFor={`chat-input-text-${id}`} className="text-xs font-semibold text-gray-500 dark:text-gray-400">Text</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`chat-input-text-${id}`}
|
||||||
|
name="text"
|
||||||
|
value={data.text || ''}
|
||||||
|
onChange={handleTextChange}
|
||||||
|
placeholder="输入聊天内容..."
|
||||||
|
className="mt-1 text-sm min-h-[80px] bg-white dark:bg-gray-700"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 输出 Handle */}
|
||||||
|
<div className="relative p-2 border-t border-gray-300 dark:border-gray-600">
|
||||||
|
<Label className="text-xs font-semibold text-gray-500 dark:text-gray-400 block text-right pr-7">Message</Label>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
id="message-output"
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className="!w-3 !h-3 !bg-purple-500 top-1/2" // 使用紫色 Handle
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ChatInputNodeComponent.displayName = 'ChatInputNode';
|
||||||
|
export const ChatInputNode = memo(ChatInputNodeComponent);
|
||||||
84
frontend/app/workflow/components/ChatOutputNode.tsx
Normal file
84
frontend/app/workflow/components/ChatOutputNode.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// File: frontend/app/workflow/components/ChatOutputNode.tsx (新建)
|
||||||
|
// Description: 自定义 ChatOutput 节点
|
||||||
|
|
||||||
|
import React, { memo, useCallback, ChangeEvent } from 'react';
|
||||||
|
import { Handle, Position, useReactFlow, Node } from 'reactflow';
|
||||||
|
import { MessageCircleReply } from 'lucide-react'; // 使用合适的图标
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input"; // 使用 Input 或 Textarea 根据需要
|
||||||
|
import type { ChatOutputNodeData, CustomNodeProps } from './types';
|
||||||
|
|
||||||
|
const ChatOutputNodeComponent = ({ id, data, isConnectable }: CustomNodeProps<ChatOutputNodeData>) => {
|
||||||
|
const { setNodes } = useReactFlow<ChatOutputNodeData>();
|
||||||
|
|
||||||
|
// 处理文本变化 (如果需要可编辑)
|
||||||
|
const handleTextChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newText = event.target.value;
|
||||||
|
setNodes((nds: Node<ChatOutputNodeData>[]) =>
|
||||||
|
nds.map((node) => {
|
||||||
|
if (node.id === id) {
|
||||||
|
return { ...node, data: { ...node.data, displayText: newText } };
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [id, setNodes]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="react-flow__node-genericNode bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-lg w-96">
|
||||||
|
{/* 输入 Handle */}
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
id="message-input"
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className="!w-3 !h-3 !bg-purple-500 top-1/2" // 使用紫色 Handle
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 节点头部 */}
|
||||||
|
<div className="bg-gray-100 dark:bg-gray-700 p-3 border-b border-gray-300 dark:border-gray-600">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MessageCircleReply size={18} className="text-gray-700 dark:text-gray-300" />
|
||||||
|
<strong className="text-gray-800 dark:text-gray-200">Chat Output</strong>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">在 Playground 显示聊天消息。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 节点内容 */}
|
||||||
|
<div className="p-4 space-y-2">
|
||||||
|
<div className="nodrag">
|
||||||
|
<Label htmlFor={`chat-output-text-${id}`} className="text-xs font-semibold text-gray-500 dark:text-gray-400">Text</Label>
|
||||||
|
{/* 根据截图,这里像是一个 Input,可能用于显示模板或结果 */}
|
||||||
|
<Input
|
||||||
|
id={`chat-output-text-${id}`}
|
||||||
|
name="displayText"
|
||||||
|
value={data.displayText || ''} // 显示传入的数据
|
||||||
|
onChange={handleTextChange} // 如果需要可编辑
|
||||||
|
placeholder="等待输入..."
|
||||||
|
className="mt-1 text-sm h-10 bg-white dark:bg-gray-700"
|
||||||
|
// readOnly // 如果只是显示,可以设为只读
|
||||||
|
/>
|
||||||
|
{/* 或者只是一个简单的文本显示区域 */}
|
||||||
|
{/* <p className="mt-1 text-sm p-2 border rounded bg-white dark:bg-gray-700 min-h-[40px]">
|
||||||
|
{data.displayText || '等待输入...'}
|
||||||
|
</p> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 输出 Handle (可选,根据是否需要继续传递) */}
|
||||||
|
{/* <div className="relative p-2 border-t border-gray-300 dark:border-gray-600">
|
||||||
|
<Label className="text-xs font-semibold text-gray-500 dark:text-gray-400 block text-right pr-7">Message</Label>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
id="message-passthrough"
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className="w-3 h-3 !bg-purple-500 top-1/2"
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ChatOutputNodeComponent.displayName = 'ChatOutputNode';
|
||||||
|
export const ChatOutputNode = memo(ChatOutputNodeComponent);
|
||||||
231
frontend/app/workflow/components/LLMNode.tsx
Normal file
231
frontend/app/workflow/components/LLMNode.tsx
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
// File: frontend/app/workflow/components/LLMNode.tsx
|
||||||
|
// Description: 自定义 LLM 节点组件
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
memo,
|
||||||
|
ChangeEvent,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
Handle,
|
||||||
|
Position,
|
||||||
|
useUpdateNodeInternals,
|
||||||
|
useReactFlow,
|
||||||
|
Node,
|
||||||
|
} from "reactflow";
|
||||||
|
import { BrainCircuit } from "lucide-react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
type LLMNodeData,
|
||||||
|
type CustomNodeProps,
|
||||||
|
availableModels,
|
||||||
|
} from "./types"; // 导入类型和模型列表
|
||||||
|
|
||||||
|
const LLMNodeComponent = ({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable,
|
||||||
|
}: CustomNodeProps<LLMNodeData>) => {
|
||||||
|
// const [currentData, setCurrentData] = useState<LLMNodeData>(data);
|
||||||
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
|
const { setNodes } = useReactFlow<LLMNodeData>(); // 获取 setNodes 方法
|
||||||
|
|
||||||
|
// 处理内部表单变化的通用回调 - 直接更新 React Flow 主状态
|
||||||
|
const handleDataChange = useCallback(
|
||||||
|
(key: keyof LLMNodeData, value: any) => {
|
||||||
|
// 使用 setNodes 更新特定节点的数据
|
||||||
|
setNodes(
|
||||||
|
(
|
||||||
|
nds: Node<LLMNodeData>[] // 显式指定类型
|
||||||
|
) =>
|
||||||
|
nds.map((node) => {
|
||||||
|
if (node.id === id) {
|
||||||
|
// 创建一个新的 data 对象
|
||||||
|
const updatedData = { ...node.data, [key]: value };
|
||||||
|
return { ...node, data: updatedData };
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
console.log(`(LLMNode) Node ${id} data updated:`, { [key]: value });
|
||||||
|
},
|
||||||
|
[id, setNodes]
|
||||||
|
); // 依赖 id 和 setNodes
|
||||||
|
|
||||||
|
const handleSliderChange = useCallback(
|
||||||
|
(value: number[]) => {
|
||||||
|
handleDataChange("temperature", value[0]);
|
||||||
|
},
|
||||||
|
[handleDataChange]
|
||||||
|
);
|
||||||
|
const handleSelectChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
handleDataChange("model", value);
|
||||||
|
},
|
||||||
|
[handleDataChange]
|
||||||
|
);
|
||||||
|
const handleTextChange = useCallback(
|
||||||
|
(event: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||||
|
const { name, value } = event.target;
|
||||||
|
handleDataChange(name as keyof LLMNodeData, value);
|
||||||
|
},
|
||||||
|
[handleDataChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 检查 inputConnected 状态是否需要更新 (如果外部更新了)
|
||||||
|
// 注意:这个逻辑依赖于父组件正确更新了 data.inputConnected
|
||||||
|
useEffect(() => {
|
||||||
|
// 可以在这里添加逻辑,如果 props.data.inputConnected 和 UI 显示不一致时触发更新
|
||||||
|
// 但通常连接状态由 onConnect/onEdgesChange 在父组件处理更佳
|
||||||
|
// updateNodeInternals(id); // 如果 Handle 显示依赖 inputConnected,可能需要调用
|
||||||
|
}, [data.inputConnected, id, updateNodeInternals]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// 调整宽度,例如 w-96 (24rem)
|
||||||
|
<div className="react-flow__node-genericNode nopan bg-purple-50 dark:bg-gray-800 border border-purple-200 dark:border-gray-700 rounded-lg shadow-lg w-96">
|
||||||
|
<div className="bg-purple-100 dark:bg-gray-700 p-3 border-b border-purple-200 dark:border-gray-600">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BrainCircuit
|
||||||
|
size={18}
|
||||||
|
className="text-purple-700 dark:text-purple-300"
|
||||||
|
/>
|
||||||
|
<strong className="text-purple-800 dark:text-purple-200">
|
||||||
|
LLM 调用
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-purple-600 dark:text-purple-400 mt-1">
|
||||||
|
使用大语言模型生成文本
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
<div className="relative nodrag">
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
id="input-text"
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className="!w-3 !h-3 !bg-blue-500 top-1/2 z-50 !-left-5"
|
||||||
|
/>
|
||||||
|
<Label className="text-xs font-semibold text-gray-500 dark:text-gray-400">
|
||||||
|
输入
|
||||||
|
</Label>
|
||||||
|
{/* 直接使用 props.data.inputConnected */}
|
||||||
|
{!data.inputConnected && (
|
||||||
|
<p className="text-xs text-gray-400 italic mt-1">连接文本或提示</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="nodrag">
|
||||||
|
<Label
|
||||||
|
htmlFor={`systemPrompt-${id}`}
|
||||||
|
className="text-xs font-semibold"
|
||||||
|
>
|
||||||
|
系统提示
|
||||||
|
</Label>
|
||||||
|
{/* 直接使用 props.data 和 onChange 回调 */}
|
||||||
|
<Textarea
|
||||||
|
id={`systemPrompt-${id}`}
|
||||||
|
name="systemPrompt"
|
||||||
|
value={data.systemPrompt}
|
||||||
|
onChange={handleTextChange}
|
||||||
|
placeholder="例如:你是一个乐于助人的助手。"
|
||||||
|
className="mt-1 text-xs min-h-[60px] bg-white dark:bg-gray-700"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="nodrag">
|
||||||
|
<Label htmlFor={`model-${id}`} className="text-xs font-semibold">
|
||||||
|
模型名称
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
name="model"
|
||||||
|
value={data.model}
|
||||||
|
onValueChange={handleSelectChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id={`model-${id}`}
|
||||||
|
className="mt-1 h-8 text-xs bg-white dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="选择模型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableModels.map((model) => (
|
||||||
|
<SelectItem
|
||||||
|
key={model.value}
|
||||||
|
value={model.value}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{model.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="nodrag">
|
||||||
|
<Label htmlFor={`apiKey-${id}`} className="text-xs font-semibold">
|
||||||
|
API Key (不安全)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`apiKey-${id}`}
|
||||||
|
name="apiKey"
|
||||||
|
type="password"
|
||||||
|
value={data.apiKey || ""}
|
||||||
|
onChange={handleTextChange}
|
||||||
|
placeholder="sk-..."
|
||||||
|
className="mt-1 h-8 text-xs bg-white dark:bg-gray-700"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-red-500 mt-1">
|
||||||
|
警告:不建议在此处输入密钥。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="nodrag">
|
||||||
|
<Label
|
||||||
|
htmlFor={`temperature-${id}`}
|
||||||
|
className="text-xs font-semibold"
|
||||||
|
>
|
||||||
|
温度: {data.temperature?.toFixed(1) ?? "N/A"}
|
||||||
|
</Label>
|
||||||
|
<Slider
|
||||||
|
id={`temperature-${id}`}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.1}
|
||||||
|
value={[data.temperature ?? 0.7]}
|
||||||
|
onValueChange={handleSliderChange}
|
||||||
|
className="mt-2"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
<span>精确</span>
|
||||||
|
<span>创造性</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative p-2 border-t border-purple-200 dark:border-gray-600">
|
||||||
|
<Label className="text-xs font-semibold text-gray-500 dark:text-gray-400 block text-right pr-6">
|
||||||
|
输出
|
||||||
|
</Label>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
id="output-message"
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className="!w-3 !h-3 !bg-green-500 top-1/2 z-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LLMNodeComponent.displayName = "LLMNode";
|
||||||
|
export const LLMNode = memo(LLMNodeComponent);
|
||||||
27
frontend/app/workflow/components/OutputNode.tsx
Normal file
27
frontend/app/workflow/components/OutputNode.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// File: frontend/app/workflow/components/OutputNode.tsx
|
||||||
|
// Description: 自定义结束节点组件
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { Handle, Position } from 'reactflow';
|
||||||
|
import { CheckCircle } from 'lucide-react';
|
||||||
|
import type { OutputNodeData, CustomNodeProps } from './types'; // 导入类型
|
||||||
|
|
||||||
|
const OutputNodeComponent = ({ data }: CustomNodeProps<OutputNodeData>) => {
|
||||||
|
return (
|
||||||
|
<div className="react-flow__node-default bg-blue-100 dark:bg-blue-900 border-blue-300 dark:border-blue-700 p-4 rounded-lg shadow-md w-40">
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
id="output-target"
|
||||||
|
className="w-3 h-3 !bg-blue-500"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<CheckCircle size={16} className="text-blue-700 dark:text-blue-300" />
|
||||||
|
<strong className="text-blue-800 dark:text-blue-200">{data.label || '结束流程'}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
OutputNodeComponent.displayName = 'OutputNode';
|
||||||
|
export const OutputNode = memo(OutputNodeComponent);
|
||||||
28
frontend/app/workflow/components/StartNode.tsx
Normal file
28
frontend/app/workflow/components/StartNode.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// File: frontend/app/workflow/components/StartNode.tsx
|
||||||
|
// Description: 自定义开始节点组件
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { Handle, Position } from 'reactflow';
|
||||||
|
import { Play } from 'lucide-react';
|
||||||
|
import type { StartNodeData, CustomNodeProps } from './types'; // 导入类型
|
||||||
|
|
||||||
|
const StartNodeComponent = ({ data }: CustomNodeProps<StartNodeData>) => {
|
||||||
|
return (
|
||||||
|
<div className="react-flow__node-default bg-green-100 dark:bg-green-900 border-green-300 dark:border-green-700 p-4 rounded-lg shadow-md w-40">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Play size={16} className="text-green-700 dark:text-green-300" />
|
||||||
|
<strong className="text-green-800 dark:text-green-200">{data.label || '开始流程'}</strong>
|
||||||
|
</div>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
id="start-source"
|
||||||
|
className="w-3 h-3 !bg-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
StartNodeComponent.displayName = 'StartNode';
|
||||||
|
// 使用 memo 优化
|
||||||
|
export const StartNode = memo(StartNodeComponent);
|
||||||
46
frontend/app/workflow/components/WorkflowSidebar.tsx
Normal file
46
frontend/app/workflow/components/WorkflowSidebar.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// File: frontend/app/workflow/components/WorkflowSidebar.tsx
|
||||||
|
// Description: 侧边栏组件,用于拖放节点
|
||||||
|
|
||||||
|
import React, { DragEvent } from 'react';
|
||||||
|
import { MessageSquareText, BrainCircuit, Database, LogOut, Play, CheckCircle, MessageCircleQuestion, MessageCircleReply } from 'lucide-react';
|
||||||
|
|
||||||
|
// 定义可拖拽的节点类型 (从 page.tsx 移动过来)
|
||||||
|
const nodeTypesForPalette = [
|
||||||
|
{ type: 'startNode', label: '开始流程', icon: Play, defaultData: { label: '开始' } },
|
||||||
|
{ type: 'inputNode', label: '文本输入', icon: MessageSquareText, defaultData: { text: '用户输入...' } },
|
||||||
|
{ type: 'llmNode', label: 'LLM 调用', icon: BrainCircuit, defaultData: { model: 'gpt-3.5-turbo', temperature: 0.7, systemPrompt: '你是一个乐于助人的 AI 助手。' } },
|
||||||
|
{ type: 'ragNode', label: 'RAG 查询', icon: Database, defaultData: { query: '...', knowledgeBase: 'default' } },
|
||||||
|
{ type: 'outputNode', label: '结束流程', icon: CheckCircle, defaultData: { label: '结束' } },
|
||||||
|
{ type: 'chatInputNode', label: 'Chat Input', icon: MessageCircleQuestion, defaultData: { text: '' } }, // 添加 ChatInput
|
||||||
|
{ type: 'chatOutputNode', label: 'Chat Output', icon: MessageCircleReply, defaultData: { displayText: '' } }, // 添加 ChatOutput
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
export const WorkflowSidebar = () => {
|
||||||
|
const onDragStart = (event: DragEvent, nodeType: string, defaultData: any) => {
|
||||||
|
const nodeInfo = JSON.stringify({ nodeType, defaultData });
|
||||||
|
event.dataTransfer.setData('application/reactflow', nodeInfo);
|
||||||
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
|
console.log(`Drag Start: ${nodeType}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="w-64 bg-white dark:bg-gray-800 p-4 border-r dark:border-gray-700 shadow-md overflow-y-auto flex-shrink-0"> {/* 添加 flex-shrink-0 */}
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-200">节点面板</h3>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4">将节点拖拽到右侧画布</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{nodeTypesForPalette.map((nodeInfo) => (
|
||||||
|
<div
|
||||||
|
key={nodeInfo.type}
|
||||||
|
className="p-3 border dark:border-gray-600 rounded-lg cursor-grab flex items-center gap-2 bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
onDragStart={(event) => onDragStart(event, nodeInfo.type, nodeInfo.defaultData)}
|
||||||
|
draggable
|
||||||
|
>
|
||||||
|
<nodeInfo.icon size={18} className="text-gray-600 dark:text-gray-300 flex-shrink-0" />
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-200">{nodeInfo.label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
};
|
||||||
48
frontend/app/workflow/components/types.ts
Normal file
48
frontend/app/workflow/components/types.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// File: frontend/app/workflow/components/types.ts
|
||||||
|
// Description: Shared types for workflow components
|
||||||
|
|
||||||
|
import { NodeProps } from 'reactflow';
|
||||||
|
|
||||||
|
// 定义 LLM 节点的数据结构
|
||||||
|
export interface LLMNodeData {
|
||||||
|
model: string;
|
||||||
|
temperature: number;
|
||||||
|
systemPrompt: string;
|
||||||
|
apiKey?: string;
|
||||||
|
inputConnected?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义其他节点可能需要的数据类型
|
||||||
|
export interface StartNodeData {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OutputNodeData {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 新增 Chat 节点类型 ---
|
||||||
|
export interface ChatInputNodeData {
|
||||||
|
text: string; // 存储输入的文本
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatOutputNodeData {
|
||||||
|
// ChatOutput 通常接收输入并显示,或者只是一个结束点
|
||||||
|
// 如果需要配置显示方式,可以在这里添加字段
|
||||||
|
// 如果它也像截图那样有个输入框,可能是用于显示模板或最终结果
|
||||||
|
displayText?: string; // 用于显示或配置的文本
|
||||||
|
inputConnected?: boolean; // 标记输入是否连接
|
||||||
|
}
|
||||||
|
// --- 结束新增 ---
|
||||||
|
|
||||||
|
// 可以将 NodeProps 包装一下,方便使用
|
||||||
|
export type CustomNodeProps<T> = NodeProps<T>;
|
||||||
|
|
||||||
|
// 可选的模型列表 (也可以放在这里共享)
|
||||||
|
export const availableModels = [
|
||||||
|
{ value: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" },
|
||||||
|
{ value: "gpt-4", label: "GPT-4" },
|
||||||
|
{ value: "gpt-4-turbo", label: "GPT-4 Turbo" },
|
||||||
|
{ value: "gemini-pro", label: "Gemini Pro" },
|
||||||
|
{ value: "deepseek-coder", label: "DeepSeek Coder" },
|
||||||
|
];
|
||||||
@ -1,12 +1,271 @@
|
|||||||
// File: frontend/app/workflow/page.tsx
|
// File: frontend/app/workflow/page.tsx (更新)
|
||||||
// Description: 工作流页面占位符
|
// Description: 工作流编辑器主页面,引入模块化组件
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useCallback, useRef, DragEvent, useMemo } from 'react';
|
||||||
|
import ReactFlow, {
|
||||||
|
ReactFlowProvider,
|
||||||
|
MiniMap,
|
||||||
|
Controls,
|
||||||
|
Background,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
addEdge,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
Connection,
|
||||||
|
Position,
|
||||||
|
MarkerType,
|
||||||
|
NodeChange,
|
||||||
|
EdgeChange,
|
||||||
|
applyNodeChanges,
|
||||||
|
applyEdgeChanges,
|
||||||
|
useReactFlow,
|
||||||
|
XYPosition,
|
||||||
|
BackgroundVariant,
|
||||||
|
Panel,
|
||||||
|
} from 'reactflow';
|
||||||
|
|
||||||
|
// 引入自定义节点和侧边栏组件
|
||||||
|
import { StartNode } from './components/StartNode';
|
||||||
|
import { OutputNode } from './components/OutputNode';
|
||||||
|
import { LLMNode } from './components/LLMNode';
|
||||||
|
import { WorkflowSidebar } from './components/WorkflowSidebar';
|
||||||
|
import { ChatInputNode } from './components/ChatInputNode'; // 引入新节点
|
||||||
|
import { ChatOutputNode } from './components/ChatOutputNode'; // 引入新节点
|
||||||
|
// 引入类型 (如果需要)
|
||||||
|
// import type { LLMNodeData } from './components/types';
|
||||||
|
// 引入 API 函数
|
||||||
|
import { runWorkflow } from '@/lib/api'; // Import runWorkflow
|
||||||
|
|
||||||
|
import 'reactflow/dist/style.css';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { LoaderIcon, PlayIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
// --- 初始节点和边数据 ---
|
||||||
|
const initialNodes: Node[] = [
|
||||||
|
];
|
||||||
|
const initialEdges: Edge[] = [];
|
||||||
|
|
||||||
|
// --- 工作流编辑器组件 ---
|
||||||
|
function WorkflowEditor() {
|
||||||
|
const reactFlowWrapper = useRef<HTMLDivElement>(null); // Ref 指向 React Flow 容器
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
|
const [isRunning, setIsRunning] = useState(false); // State for run button loading
|
||||||
|
|
||||||
|
// --- 注册自定义节点类型 ---
|
||||||
|
const nodeTypes = useMemo(() => ({
|
||||||
|
startNode: StartNode,
|
||||||
|
outputNode: OutputNode,
|
||||||
|
llmNode: LLMNode, // LLMNode 现在需要一种方式来调用 updateNodeData
|
||||||
|
chatInputNode: ChatInputNode, // 注册 ChatInputNode
|
||||||
|
chatOutputNode: ChatOutputNode, // 注册 ChatOutputNode
|
||||||
|
// inputNode: CustomInputNode,
|
||||||
|
// ragNode: RAGNode,
|
||||||
|
}), []); // 移除 updateNodeData 依赖,因为它不应该直接传递
|
||||||
|
|
||||||
|
const onConnect = useCallback(
|
||||||
|
(connection: Connection) => {
|
||||||
|
setEdges((eds) => addEdge({ ...connection, markerEnd: { type: MarkerType.ArrowClosed } }, eds));
|
||||||
|
if (connection.targetHandle === 'input-text' && connection.target) {
|
||||||
|
setNodes((nds) =>
|
||||||
|
nds.map((node) => {
|
||||||
|
if (node.id === connection.target) {
|
||||||
|
return { ...node, data: { ...node.data, inputConnected: true } };
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 更新 ChatOutputNode 的连接状态 (如果需要)
|
||||||
|
if (connection.targetHandle === 'message-input' && connection.target) {
|
||||||
|
setNodes((nds) => nds.map((node) => node.id === connection.target ? { ...node, data: { ...node.data, inputConnected: true } } : node));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setEdges, setNodes] // 保持 updateNodeData 依赖
|
||||||
|
);
|
||||||
|
|
||||||
|
const onEdgesChangeIntercepted = useCallback(
|
||||||
|
(changes: EdgeChange[]) => {
|
||||||
|
changes.forEach(change => {
|
||||||
|
if (change.type === 'remove') {
|
||||||
|
const edgeToRemove = edges.find(edge => edge.id === change.id);
|
||||||
|
if (edgeToRemove?.targetHandle === 'input-text' && edgeToRemove.target) {
|
||||||
|
setNodes((nds) =>
|
||||||
|
nds.map((node) => {
|
||||||
|
if (node.id === edgeToRemove.target) {
|
||||||
|
return { ...node, data: { ...node.data, inputConnected: false } };
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 更新 ChatOutputNode 的断开状态 (如果需要)
|
||||||
|
if (edgeToRemove?.targetHandle === 'message-input' && edgeToRemove.target) {
|
||||||
|
setNodes((nds) => nds.map((node) => node.id === edgeToRemove.target ? { ...node, data: { ...node.data, inputConnected: false } } : node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onEdgesChange(changes);
|
||||||
|
},
|
||||||
|
[edges, onEdgesChange, setNodes] // 保持 updateNodeData 依赖
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理画布上的拖拽悬停事件
|
||||||
|
const onDragOver = useCallback((event: DragEvent) => {
|
||||||
|
event.preventDefault(); // 必须阻止默认行为才能触发 onDrop
|
||||||
|
event.dataTransfer.dropEffect = 'move'; // 设置放置效果
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 处理画布上的放置事件
|
||||||
|
const onDrop = useCallback(
|
||||||
|
(event: DragEvent) => {
|
||||||
|
event.preventDefault(); // 阻止默认行为(如打开文件)
|
||||||
|
|
||||||
|
if (!reactFlowWrapper.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 dataTransfer 中获取节点信息
|
||||||
|
const nodeInfoString = event.dataTransfer.getData('application/reactflow');
|
||||||
|
if (!nodeInfoString) {
|
||||||
|
console.warn("No reactflow data found in dataTransfer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodeInfo;
|
||||||
|
try {
|
||||||
|
nodeInfo = JSON.parse(nodeInfoString);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse node info from dataTransfer", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { nodeType, defaultData } = nodeInfo;
|
||||||
|
if (!nodeType) {
|
||||||
|
console.warn("Node type not found in dataTransfer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取鼠标相对于 React Flow 画布的位置
|
||||||
|
// 需要计算鼠标位置相对于 reactFlowWrapper 的偏移量
|
||||||
|
const position = screenToFlowPosition({
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建新节点
|
||||||
|
const newNode: Node = {
|
||||||
|
id: `${nodeType}-${Date.now()}`, // 使用更唯一的 ID
|
||||||
|
type: nodeType, // 设置节点类型
|
||||||
|
position,
|
||||||
|
data: { label: defaultData?.label || `${nodeType} node`, ...defaultData },
|
||||||
|
// 根据节点类型设置 Handle 位置 (可选,也可以在自定义节点内部定义)
|
||||||
|
// sourcePosition: Position.Bottom,
|
||||||
|
// targetPosition: Position.Top,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Node dropped:', newNode);
|
||||||
|
// 将新节点添加到状态中
|
||||||
|
setNodes((nds) => nds.concat(newNode));
|
||||||
|
},
|
||||||
|
[screenToFlowPosition, setNodes] // 依赖 project 方法和 setNodes
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- 处理工作流运行 ---
|
||||||
|
const handleRunWorkflow = useCallback(async () => {
|
||||||
|
setIsRunning(true);
|
||||||
|
toast.info("正在执行工作流...");
|
||||||
|
console.log("Running workflow with nodes:", nodes);
|
||||||
|
console.log("Running workflow with edges:", edges);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await runWorkflow(nodes, edges); // Call the API function
|
||||||
|
|
||||||
|
if (result.success && result.output !== undefined && result.output_node_id) {
|
||||||
|
toast.success(result.message || "工作流执行成功!");
|
||||||
|
console.log("Workflow output:", result.output);
|
||||||
|
|
||||||
|
// 更新 ChatOutputNode 的数据以显示结果
|
||||||
|
setNodes((nds) =>
|
||||||
|
nds.map((node) => {
|
||||||
|
if (node.id === result.output_node_id && node.type === 'chatOutputNode') {
|
||||||
|
return { ...node, data: { ...node.data, displayText: result.output } };
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.error(result.message || "工作流执行失败。");
|
||||||
|
console.error("Workflow execution failed:", result.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// This catch block might not be necessary if runWorkflow handles errors
|
||||||
|
toast.error("执行工作流时发生网络错误。");
|
||||||
|
console.error("API call error:", error);
|
||||||
|
} finally {
|
||||||
|
setIsRunning(false);
|
||||||
|
}
|
||||||
|
}, [nodes, edges, setNodes]); // Depend on nodes and edges
|
||||||
|
|
||||||
export default function WorkflowPage() {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col h-full bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
|
||||||
<h1 className="text-2xl font-semibold mb-4 dark:text-gray-200">工作流编辑器</h1>
|
<h1 className="text-xl font-semibold p-4 border-b dark:border-gray-700 text-gray-800 dark:text-gray-200">
|
||||||
<p className="text-gray-700 dark:text-gray-400">这里将集成 React Flow 来构建可视化工作流。</p>
|
工作流编辑器
|
||||||
{/* 后续添加 React Flow 画布和节点面板 */}
|
</h1>
|
||||||
|
{/* 主容器使用 Flexbox */}
|
||||||
|
<div className="flex flex-1 overflow-hidden">
|
||||||
|
{/* 侧边栏 */}
|
||||||
|
<WorkflowSidebar />
|
||||||
|
{/* React Flow 画布容器 */}
|
||||||
|
<div className="flex-1 h-full" ref={reactFlowWrapper}> {/* 添加 ref */}
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChangeIntercepted}
|
||||||
|
onConnect={onConnect}
|
||||||
|
onDragOver={onDragOver} // 添加 onDragOver
|
||||||
|
onDrop={onDrop} // 添加 onDrop
|
||||||
|
nodeTypes={nodeTypes} // <--- 传递自定义节点类型
|
||||||
|
fitView
|
||||||
|
className="bg-gray-50 dark:bg-gray-900"
|
||||||
|
>
|
||||||
|
{/* 使用 Panel 添加运行按钮到右上角 */}
|
||||||
|
<Panel position="top-right" className="p-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleRunWorkflow}
|
||||||
|
disabled={isRunning}
|
||||||
|
size="sm"
|
||||||
|
className="bg-green-600 hover:bg-green-700 text-white"
|
||||||
|
>
|
||||||
|
{isRunning ? (
|
||||||
|
<LoaderIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<PlayIcon className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
运行
|
||||||
|
</Button>
|
||||||
|
</Panel>
|
||||||
|
<Controls />
|
||||||
|
<MiniMap nodeStrokeWidth={3} zoomable pannable />
|
||||||
|
<Background gap={16} color="#ccc" variant={BackgroundVariant.Dots} />
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 主页面组件,包含 Provider ---
|
||||||
|
export default function WorkflowPage() {
|
||||||
|
return (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<WorkflowEditor />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,33 +1,51 @@
|
|||||||
// File: frontend/lib/api.ts (更新)
|
// File: frontend/lib/api.ts (Update)
|
||||||
// Description: 添加调用助手和会话管理 API 的函数
|
// Description: 添加运行工作流的 API 函数
|
||||||
|
|
||||||
import { Assistant, AssistantCreateData, AssistantUpdateData } from "@/types/assistant";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import type { Node, Edge } from "reactflow"; // Import React Flow types
|
||||||
|
import type {
|
||||||
|
Assistant,
|
||||||
|
Session,
|
||||||
|
Message,
|
||||||
|
AssistantCreateData,
|
||||||
|
AssistantUpdateData,
|
||||||
|
ChatApiResponse,
|
||||||
|
} from "./types"; // Assuming types are defined
|
||||||
|
|
||||||
// --- Types ---
|
// --- Types ---
|
||||||
export interface Session {
|
// Workflow Run types (match backend pydantic models)
|
||||||
id: string;
|
interface WorkflowNodeData {
|
||||||
title: string;
|
label?: string | null;
|
||||||
assistant_id: string;
|
text?: string | null;
|
||||||
created_at: string; // ISO date string
|
displayText?: string | null;
|
||||||
updated_at?: string | null; // Add updated_at
|
model?: string | null;
|
||||||
|
temperature?: number | null;
|
||||||
|
systemPrompt?: string | null;
|
||||||
|
// Add other node data fields as needed
|
||||||
|
[key: string]: any; // Allow extra fields
|
||||||
}
|
}
|
||||||
|
interface WorkflowNode {
|
||||||
// Message type from backend
|
id: string;
|
||||||
export interface Message {
|
type: string;
|
||||||
id: string;
|
position: { x: number; y: number };
|
||||||
session_id: string;
|
data: WorkflowNodeData;
|
||||||
sender: 'user' | 'ai'; // Or extend with 'system' if needed
|
|
||||||
text: string;
|
|
||||||
order: number;
|
|
||||||
created_at: string; // ISO date string
|
|
||||||
}
|
}
|
||||||
|
interface WorkflowEdge {
|
||||||
// 聊天响应类型
|
id: string;
|
||||||
export interface ChatApiResponse {
|
source: string;
|
||||||
reply: string;
|
target: string;
|
||||||
session_id?: string | null; // 后端返回的新 session id
|
sourceHandle?: string | null;
|
||||||
session_title?: string | null; // 后端返回的新 session title
|
targetHandle?: string | null;
|
||||||
|
}
|
||||||
|
interface WorkflowRunPayload {
|
||||||
|
nodes: WorkflowNode[];
|
||||||
|
edges: WorkflowEdge[];
|
||||||
|
}
|
||||||
|
export interface WorkflowRunResult {
|
||||||
|
success: boolean;
|
||||||
|
message?: string | null;
|
||||||
|
output?: string | null;
|
||||||
|
output_node_id?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- API Client Setup ---
|
// --- API Client Setup ---
|
||||||
@ -159,17 +177,61 @@ export const deleteSession = async (sessionId: string): Promise<void> => {
|
|||||||
|
|
||||||
// --- Message API (New) ---
|
// --- Message API (New) ---
|
||||||
/** 获取指定会话的消息列表 */
|
/** 获取指定会话的消息列表 */
|
||||||
export const getMessagesBySession = async (sessionId: string, limit: number = 100, skip: number = 0): Promise<Message[]> => {
|
export const getMessagesBySession = async (
|
||||||
|
sessionId: string,
|
||||||
|
limit: number = 100,
|
||||||
|
skip: number = 0
|
||||||
|
): Promise<Message[]> => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<Message[]>(
|
||||||
|
`/messages/session/${sessionId}`,
|
||||||
|
{
|
||||||
|
params: { limit, skip },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
// Handle 404 specifically if needed (session exists but no messages)
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return []; // Return empty list if session not found or no messages
|
||||||
|
}
|
||||||
|
throw new Error(handleApiError(error, "getMessagesBySession"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Workflow API (New) ---
|
||||||
|
/**
|
||||||
|
* 发送工作流定义到后端执行
|
||||||
|
* @param nodes - React Flow 节点数组
|
||||||
|
* @param edges - React Flow 边数组
|
||||||
|
* @returns 工作流执行结果
|
||||||
|
*/
|
||||||
|
export const runWorkflow = async (nodes: Node[], edges: Edge[]): Promise<WorkflowRunResult> => {
|
||||||
|
// Map React Flow nodes/edges to the structure expected by the backend API
|
||||||
|
const payload: WorkflowRunPayload = {
|
||||||
|
nodes: nodes.map(n => ({
|
||||||
|
id: n.id,
|
||||||
|
type: n.type || 'default', // Ensure type is present
|
||||||
|
position: n.position,
|
||||||
|
data: n.data as WorkflowNodeData, // Assume data matches for now
|
||||||
|
})),
|
||||||
|
edges: edges.map(e => ({
|
||||||
|
id: e.id,
|
||||||
|
source: e.source,
|
||||||
|
target: e.target,
|
||||||
|
sourceHandle: e.sourceHandle,
|
||||||
|
targetHandle: e.targetHandle,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get<Message[]>(`/messages/session/${sessionId}`, {
|
const response = await apiClient.post<WorkflowRunResult>('/workflow/run', payload);
|
||||||
params: { limit, skip }
|
|
||||||
});
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle 404 specifically if needed (session exists but no messages)
|
// Return a failed result structure on API error
|
||||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
return {
|
||||||
return []; // Return empty list if session not found or no messages
|
success: false,
|
||||||
}
|
message: handleApiError(error, 'runWorkflow'),
|
||||||
throw new Error(handleApiError(error, 'getMessagesBySession'));
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
49
frontend/lib/types.ts
Normal file
49
frontend/lib/types.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// --- Types (从后端模型同步或手动定义) ---
|
||||||
|
// 这些类型应该与后端 pydantic_models.py 中的 Read 模型匹配
|
||||||
|
export interface Assistant {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
avatar?: string | null;
|
||||||
|
system_prompt: string;
|
||||||
|
model: string;
|
||||||
|
temperature: number;
|
||||||
|
}
|
||||||
|
// 创建助手时发送的数据类型
|
||||||
|
export interface AssistantCreateData {
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
avatar?: string | null;
|
||||||
|
system_prompt: string;
|
||||||
|
model: string;
|
||||||
|
temperature: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新助手时发送的数据类型 (所有字段可选)
|
||||||
|
export type AssistantUpdateData = Partial<AssistantCreateData>;
|
||||||
|
|
||||||
|
// --- Types ---
|
||||||
|
export interface Session {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
assistant_id: string;
|
||||||
|
created_at: string; // ISO date string
|
||||||
|
updated_at?: string | null; // Add updated_at
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message type from backend
|
||||||
|
export interface Message {
|
||||||
|
id: string;
|
||||||
|
session_id: string;
|
||||||
|
sender: "user" | "ai"; // Or extend with 'system' if needed
|
||||||
|
text: string;
|
||||||
|
order: number;
|
||||||
|
created_at: string; // ISO date string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 聊天响应类型
|
||||||
|
export interface ChatApiResponse {
|
||||||
|
reply: string;
|
||||||
|
session_id?: string | null; // 后端返回的新 session id
|
||||||
|
session_title?: string | null; // 后端返回的新 session title
|
||||||
|
}
|
||||||
@ -24,6 +24,7 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.56.1",
|
"react-hook-form": "^7.56.1",
|
||||||
|
"reactflow": "^11.11.4",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"zod": "^3.24.3"
|
"zod": "^3.24.3"
|
||||||
|
|||||||
462
frontend/pnpm-lock.yaml
generated
462
frontend/pnpm-lock.yaml
generated
@ -53,6 +53,9 @@ importers:
|
|||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.56.1
|
specifier: ^7.56.1
|
||||||
version: 7.56.1(react@19.1.0)
|
version: 7.56.1(react@19.1.0)
|
||||||
|
reactflow:
|
||||||
|
specifier: ^11.11.4
|
||||||
|
version: 11.11.4(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^2.0.3
|
specifier: ^2.0.3
|
||||||
version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@ -680,6 +683,42 @@ packages:
|
|||||||
'@radix-ui/rect@1.1.1':
|
'@radix-ui/rect@1.1.1':
|
||||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||||
|
|
||||||
|
'@reactflow/background@11.3.14':
|
||||||
|
resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
|
||||||
|
'@reactflow/controls@11.2.14':
|
||||||
|
resolution: {integrity: sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
|
||||||
|
'@reactflow/core@11.11.4':
|
||||||
|
resolution: {integrity: sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
|
||||||
|
'@reactflow/minimap@11.7.14':
|
||||||
|
resolution: {integrity: sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
|
||||||
|
'@reactflow/node-resizer@2.2.14':
|
||||||
|
resolution: {integrity: sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
|
||||||
|
'@reactflow/node-toolbar@1.3.14':
|
||||||
|
resolution: {integrity: sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
|
||||||
'@rtsao/scc@1.1.0':
|
'@rtsao/scc@1.1.0':
|
||||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||||
|
|
||||||
@ -786,9 +825,105 @@ packages:
|
|||||||
'@tybys/wasm-util@0.9.0':
|
'@tybys/wasm-util@0.9.0':
|
||||||
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
|
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
|
||||||
|
|
||||||
|
'@types/d3-array@3.2.1':
|
||||||
|
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
|
||||||
|
|
||||||
|
'@types/d3-axis@3.0.6':
|
||||||
|
resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==}
|
||||||
|
|
||||||
|
'@types/d3-brush@3.0.6':
|
||||||
|
resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==}
|
||||||
|
|
||||||
|
'@types/d3-chord@3.0.6':
|
||||||
|
resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==}
|
||||||
|
|
||||||
|
'@types/d3-color@3.1.3':
|
||||||
|
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
|
||||||
|
|
||||||
|
'@types/d3-contour@3.0.6':
|
||||||
|
resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==}
|
||||||
|
|
||||||
|
'@types/d3-delaunay@6.0.4':
|
||||||
|
resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
|
||||||
|
|
||||||
|
'@types/d3-dispatch@3.0.6':
|
||||||
|
resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==}
|
||||||
|
|
||||||
|
'@types/d3-drag@3.0.7':
|
||||||
|
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
|
||||||
|
|
||||||
|
'@types/d3-dsv@3.0.7':
|
||||||
|
resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
|
||||||
|
|
||||||
|
'@types/d3-ease@3.0.2':
|
||||||
|
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
|
||||||
|
|
||||||
|
'@types/d3-fetch@3.0.7':
|
||||||
|
resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
|
||||||
|
|
||||||
|
'@types/d3-force@3.0.10':
|
||||||
|
resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
|
||||||
|
|
||||||
|
'@types/d3-format@3.0.4':
|
||||||
|
resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
|
||||||
|
|
||||||
|
'@types/d3-geo@3.1.0':
|
||||||
|
resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
|
||||||
|
|
||||||
|
'@types/d3-hierarchy@3.1.7':
|
||||||
|
resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
|
||||||
|
|
||||||
|
'@types/d3-interpolate@3.0.4':
|
||||||
|
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
|
||||||
|
|
||||||
|
'@types/d3-path@3.1.1':
|
||||||
|
resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
|
||||||
|
|
||||||
|
'@types/d3-polygon@3.0.2':
|
||||||
|
resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==}
|
||||||
|
|
||||||
|
'@types/d3-quadtree@3.0.6':
|
||||||
|
resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
|
||||||
|
|
||||||
|
'@types/d3-random@3.0.3':
|
||||||
|
resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
|
||||||
|
|
||||||
|
'@types/d3-scale-chromatic@3.1.0':
|
||||||
|
resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
|
||||||
|
|
||||||
|
'@types/d3-scale@4.0.9':
|
||||||
|
resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
|
||||||
|
|
||||||
|
'@types/d3-selection@3.0.11':
|
||||||
|
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
|
||||||
|
|
||||||
|
'@types/d3-shape@3.1.7':
|
||||||
|
resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==}
|
||||||
|
|
||||||
|
'@types/d3-time-format@4.0.3':
|
||||||
|
resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==}
|
||||||
|
|
||||||
|
'@types/d3-time@3.0.4':
|
||||||
|
resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
|
||||||
|
|
||||||
|
'@types/d3-timer@3.0.2':
|
||||||
|
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||||
|
|
||||||
|
'@types/d3-transition@3.0.9':
|
||||||
|
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
|
||||||
|
|
||||||
|
'@types/d3-zoom@3.0.8':
|
||||||
|
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
|
||||||
|
|
||||||
|
'@types/d3@7.4.3':
|
||||||
|
resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
|
||||||
|
|
||||||
'@types/estree@1.0.7':
|
'@types/estree@1.0.7':
|
||||||
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
||||||
|
|
||||||
|
'@types/geojson@7946.0.16':
|
||||||
|
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
@ -1078,6 +1213,9 @@ packages:
|
|||||||
class-variance-authority@0.7.1:
|
class-variance-authority@0.7.1:
|
||||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||||
|
|
||||||
|
classcat@5.0.5:
|
||||||
|
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
|
||||||
|
|
||||||
client-only@0.0.1:
|
client-only@0.0.1:
|
||||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||||
|
|
||||||
@ -1113,6 +1251,44 @@ packages:
|
|||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
|
d3-color@3.1.0:
|
||||||
|
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-dispatch@3.0.1:
|
||||||
|
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-drag@3.0.0:
|
||||||
|
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-ease@3.0.1:
|
||||||
|
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-interpolate@3.0.1:
|
||||||
|
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-selection@3.0.0:
|
||||||
|
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-timer@3.0.1:
|
||||||
|
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-transition@3.0.1:
|
||||||
|
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
peerDependencies:
|
||||||
|
d3-selection: 2 - 3
|
||||||
|
|
||||||
|
d3-zoom@3.0.0:
|
||||||
|
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
damerau-levenshtein@1.0.8:
|
damerau-levenshtein@1.0.8:
|
||||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||||
|
|
||||||
@ -1974,6 +2150,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
reactflow@11.11.4:
|
||||||
|
resolution: {integrity: sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17'
|
||||||
|
react-dom: '>=17'
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.10:
|
reflect.getprototypeof@1.0.10:
|
||||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -2238,6 +2420,11 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
use-sync-external-store@1.5.0:
|
||||||
|
resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
which-boxed-primitive@1.1.1:
|
which-boxed-primitive@1.1.1:
|
||||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -2270,6 +2457,21 @@ packages:
|
|||||||
zod@3.24.3:
|
zod@3.24.3:
|
||||||
resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
|
resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
|
||||||
|
|
||||||
|
zustand@4.5.6:
|
||||||
|
resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==}
|
||||||
|
engines: {node: '>=12.7.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=16.8'
|
||||||
|
immer: '>=9.0.6'
|
||||||
|
react: '>=16.8'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
@ -2776,6 +2978,84 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/rect@1.1.1': {}
|
'@radix-ui/rect@1.1.1': {}
|
||||||
|
|
||||||
|
'@reactflow/background@11.3.14(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@reactflow/core': 11.11.4(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
classcat: 5.0.5
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
zustand: 4.5.6(@types/react@19.1.2)(react@19.1.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- immer
|
||||||
|
|
||||||
|
'@reactflow/controls@11.2.14(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@reactflow/core': 11.11.4(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
classcat: 5.0.5
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
zustand: 4.5.6(@types/react@19.1.2)(react@19.1.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- immer
|
||||||
|
|
||||||
|
'@reactflow/core@11.11.4(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3': 7.4.3
|
||||||
|
'@types/d3-drag': 3.0.7
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
'@types/d3-zoom': 3.0.8
|
||||||
|
classcat: 5.0.5
|
||||||
|
d3-drag: 3.0.0
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-zoom: 3.0.0
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
zustand: 4.5.6(@types/react@19.1.2)(react@19.1.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- immer
|
||||||
|
|
||||||
|
'@reactflow/minimap@11.7.14(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@reactflow/core': 11.11.4(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
'@types/d3-zoom': 3.0.8
|
||||||
|
classcat: 5.0.5
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-zoom: 3.0.0
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
zustand: 4.5.6(@types/react@19.1.2)(react@19.1.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- immer
|
||||||
|
|
||||||
|
'@reactflow/node-resizer@2.2.14(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@reactflow/core': 11.11.4(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
classcat: 5.0.5
|
||||||
|
d3-drag: 3.0.0
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
zustand: 4.5.6(@types/react@19.1.2)(react@19.1.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- immer
|
||||||
|
|
||||||
|
'@reactflow/node-toolbar@1.3.14(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@reactflow/core': 11.11.4(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
classcat: 5.0.5
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
zustand: 4.5.6(@types/react@19.1.2)(react@19.1.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- immer
|
||||||
|
|
||||||
'@rtsao/scc@1.1.0': {}
|
'@rtsao/scc@1.1.0': {}
|
||||||
|
|
||||||
'@rushstack/eslint-patch@1.11.0': {}
|
'@rushstack/eslint-patch@1.11.0': {}
|
||||||
@ -2859,8 +3139,127 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/d3-array@3.2.1': {}
|
||||||
|
|
||||||
|
'@types/d3-axis@3.0.6':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
|
||||||
|
'@types/d3-brush@3.0.6':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
|
||||||
|
'@types/d3-chord@3.0.6': {}
|
||||||
|
|
||||||
|
'@types/d3-color@3.1.3': {}
|
||||||
|
|
||||||
|
'@types/d3-contour@3.0.6':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-array': 3.2.1
|
||||||
|
'@types/geojson': 7946.0.16
|
||||||
|
|
||||||
|
'@types/d3-delaunay@6.0.4': {}
|
||||||
|
|
||||||
|
'@types/d3-dispatch@3.0.6': {}
|
||||||
|
|
||||||
|
'@types/d3-drag@3.0.7':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
|
||||||
|
'@types/d3-dsv@3.0.7': {}
|
||||||
|
|
||||||
|
'@types/d3-ease@3.0.2': {}
|
||||||
|
|
||||||
|
'@types/d3-fetch@3.0.7':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-dsv': 3.0.7
|
||||||
|
|
||||||
|
'@types/d3-force@3.0.10': {}
|
||||||
|
|
||||||
|
'@types/d3-format@3.0.4': {}
|
||||||
|
|
||||||
|
'@types/d3-geo@3.1.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/geojson': 7946.0.16
|
||||||
|
|
||||||
|
'@types/d3-hierarchy@3.1.7': {}
|
||||||
|
|
||||||
|
'@types/d3-interpolate@3.0.4':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-color': 3.1.3
|
||||||
|
|
||||||
|
'@types/d3-path@3.1.1': {}
|
||||||
|
|
||||||
|
'@types/d3-polygon@3.0.2': {}
|
||||||
|
|
||||||
|
'@types/d3-quadtree@3.0.6': {}
|
||||||
|
|
||||||
|
'@types/d3-random@3.0.3': {}
|
||||||
|
|
||||||
|
'@types/d3-scale-chromatic@3.1.0': {}
|
||||||
|
|
||||||
|
'@types/d3-scale@4.0.9':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-time': 3.0.4
|
||||||
|
|
||||||
|
'@types/d3-selection@3.0.11': {}
|
||||||
|
|
||||||
|
'@types/d3-shape@3.1.7':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-path': 3.1.1
|
||||||
|
|
||||||
|
'@types/d3-time-format@4.0.3': {}
|
||||||
|
|
||||||
|
'@types/d3-time@3.0.4': {}
|
||||||
|
|
||||||
|
'@types/d3-timer@3.0.2': {}
|
||||||
|
|
||||||
|
'@types/d3-transition@3.0.9':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
|
||||||
|
'@types/d3-zoom@3.0.8':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-interpolate': 3.0.4
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
|
||||||
|
'@types/d3@7.4.3':
|
||||||
|
dependencies:
|
||||||
|
'@types/d3-array': 3.2.1
|
||||||
|
'@types/d3-axis': 3.0.6
|
||||||
|
'@types/d3-brush': 3.0.6
|
||||||
|
'@types/d3-chord': 3.0.6
|
||||||
|
'@types/d3-color': 3.1.3
|
||||||
|
'@types/d3-contour': 3.0.6
|
||||||
|
'@types/d3-delaunay': 6.0.4
|
||||||
|
'@types/d3-dispatch': 3.0.6
|
||||||
|
'@types/d3-drag': 3.0.7
|
||||||
|
'@types/d3-dsv': 3.0.7
|
||||||
|
'@types/d3-ease': 3.0.2
|
||||||
|
'@types/d3-fetch': 3.0.7
|
||||||
|
'@types/d3-force': 3.0.10
|
||||||
|
'@types/d3-format': 3.0.4
|
||||||
|
'@types/d3-geo': 3.1.0
|
||||||
|
'@types/d3-hierarchy': 3.1.7
|
||||||
|
'@types/d3-interpolate': 3.0.4
|
||||||
|
'@types/d3-path': 3.1.1
|
||||||
|
'@types/d3-polygon': 3.0.2
|
||||||
|
'@types/d3-quadtree': 3.0.6
|
||||||
|
'@types/d3-random': 3.0.3
|
||||||
|
'@types/d3-scale': 4.0.9
|
||||||
|
'@types/d3-scale-chromatic': 3.1.0
|
||||||
|
'@types/d3-selection': 3.0.11
|
||||||
|
'@types/d3-shape': 3.1.7
|
||||||
|
'@types/d3-time': 3.0.4
|
||||||
|
'@types/d3-time-format': 4.0.3
|
||||||
|
'@types/d3-timer': 3.0.2
|
||||||
|
'@types/d3-transition': 3.0.9
|
||||||
|
'@types/d3-zoom': 3.0.8
|
||||||
|
|
||||||
'@types/estree@1.0.7': {}
|
'@types/estree@1.0.7': {}
|
||||||
|
|
||||||
|
'@types/geojson@7946.0.16': {}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
'@types/json5@0.0.29': {}
|
'@types/json5@0.0.29': {}
|
||||||
@ -3185,6 +3584,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|
||||||
|
classcat@5.0.5: {}
|
||||||
|
|
||||||
client-only@0.0.1: {}
|
client-only@0.0.1: {}
|
||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
@ -3221,6 +3622,42 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
|
d3-color@3.1.0: {}
|
||||||
|
|
||||||
|
d3-dispatch@3.0.1: {}
|
||||||
|
|
||||||
|
d3-drag@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
|
||||||
|
d3-ease@3.0.1: {}
|
||||||
|
|
||||||
|
d3-interpolate@3.0.1:
|
||||||
|
dependencies:
|
||||||
|
d3-color: 3.1.0
|
||||||
|
|
||||||
|
d3-selection@3.0.0: {}
|
||||||
|
|
||||||
|
d3-timer@3.0.1: {}
|
||||||
|
|
||||||
|
d3-transition@3.0.1(d3-selection@3.0.0):
|
||||||
|
dependencies:
|
||||||
|
d3-color: 3.1.0
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-ease: 3.0.1
|
||||||
|
d3-interpolate: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-timer: 3.0.1
|
||||||
|
|
||||||
|
d3-zoom@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-drag: 3.0.0
|
||||||
|
d3-interpolate: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||||
|
|
||||||
damerau-levenshtein@1.0.8: {}
|
damerau-levenshtein@1.0.8: {}
|
||||||
|
|
||||||
data-view-buffer@1.0.2:
|
data-view-buffer@1.0.2:
|
||||||
@ -4195,6 +4632,20 @@ snapshots:
|
|||||||
|
|
||||||
react@19.1.0: {}
|
react@19.1.0: {}
|
||||||
|
|
||||||
|
reactflow@11.11.4(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
'@reactflow/background': 11.3.14(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@reactflow/controls': 11.2.14(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@reactflow/core': 11.11.4(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@reactflow/minimap': 11.7.14(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@reactflow/node-resizer': 2.2.14(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@reactflow/node-toolbar': 1.3.14(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- immer
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.10:
|
reflect.getprototypeof@1.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.8
|
call-bind: 1.0.8
|
||||||
@ -4552,6 +5003,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
|
|
||||||
|
use-sync-external-store@1.5.0(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
which-boxed-primitive@1.1.1:
|
which-boxed-primitive@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-bigint: 1.1.0
|
is-bigint: 1.1.0
|
||||||
@ -4602,3 +5057,10 @@ snapshots:
|
|||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
zod@3.24.3: {}
|
zod@3.24.3: {}
|
||||||
|
|
||||||
|
zustand@4.5.6(@types/react@19.1.2)(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
use-sync-external-store: 1.5.0(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.2
|
||||||
|
react: 19.1.0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user