Compare commits
4 Commits
f0863914c2
...
c9e8b0c123
| Author | SHA1 | Date | |
|---|---|---|---|
| c9e8b0c123 | |||
| 847b3e96c8 | |||
| 37bdf4dbfd | |||
| b57b2114cd |
@ -2,7 +2,7 @@
|
||||
# Description: 聚合 v1 版本的所有 API 路由
|
||||
|
||||
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()
|
||||
|
||||
@ -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(sessions.router, prefix="/sessions", tags=["Sessions"])
|
||||
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
|
||||
from app.db.database import create_db_and_tables # Import table creation function
|
||||
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 ---
|
||||
@asynccontextmanager
|
||||
@ -17,6 +18,7 @@ async def lifespan(app: FastAPI):
|
||||
print("数据库表已检查/创建。")
|
||||
# You can add the default assistant creation here if needed,
|
||||
# but doing it in the service/model definition might be simpler for defaults.
|
||||
print(f"已注册组件: {list(base.component_registry.keys())}")
|
||||
yield
|
||||
# Shutdown actions
|
||||
print("应用程序关闭中...")
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
# Description: Pydantic 模型定义 API 数据结构
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from typing import Dict, Optional, List
|
||||
import uuid
|
||||
from datetime import datetime # Use datetime directly
|
||||
|
||||
@ -90,4 +90,43 @@ class MessageRead(MessageBase):
|
||||
created_at: datetime
|
||||
|
||||
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
|
||||
// Description: 工作流页面占位符
|
||||
// File: frontend/app/workflow/page.tsx (更新)
|
||||
// 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 (
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold mb-4 dark:text-gray-200">工作流编辑器</h1>
|
||||
<p className="text-gray-700 dark:text-gray-400">这里将集成 React Flow 来构建可视化工作流。</p>
|
||||
{/* 后续添加 React Flow 画布和节点面板 */}
|
||||
<div className="flex flex-col h-full bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
|
||||
<h1 className="text-xl font-semibold p-4 border-b dark:border-gray-700 text-gray-800 dark:text-gray-200">
|
||||
工作流编辑器
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 主页面组件,包含 Provider ---
|
||||
export default function WorkflowPage() {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<WorkflowEditor />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,33 +1,51 @@
|
||||
// File: frontend/lib/api.ts (更新)
|
||||
// Description: 添加调用助手和会话管理 API 的函数
|
||||
// File: frontend/lib/api.ts (Update)
|
||||
// Description: 添加运行工作流的 API 函数
|
||||
|
||||
import { Assistant, AssistantCreateData, AssistantUpdateData } from "@/types/assistant";
|
||||
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 ---
|
||||
export interface Session {
|
||||
id: string;
|
||||
title: string;
|
||||
assistant_id: string;
|
||||
created_at: string; // ISO date string
|
||||
updated_at?: string | null; // Add updated_at
|
||||
// Workflow Run types (match backend pydantic models)
|
||||
interface WorkflowNodeData {
|
||||
label?: string | null;
|
||||
text?: string | null;
|
||||
displayText?: string | null;
|
||||
model?: string | null;
|
||||
temperature?: number | null;
|
||||
systemPrompt?: string | null;
|
||||
// Add other node data fields as needed
|
||||
[key: string]: any; // Allow extra fields
|
||||
}
|
||||
|
||||
// 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
|
||||
interface WorkflowNode {
|
||||
id: string;
|
||||
type: string;
|
||||
position: { x: number; y: number };
|
||||
data: WorkflowNodeData;
|
||||
}
|
||||
|
||||
// 聊天响应类型
|
||||
export interface ChatApiResponse {
|
||||
reply: string;
|
||||
session_id?: string | null; // 后端返回的新 session id
|
||||
session_title?: string | null; // 后端返回的新 session title
|
||||
interface WorkflowEdge {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
sourceHandle?: string | null;
|
||||
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 ---
|
||||
@ -159,17 +177,61 @@ export const deleteSession = async (sessionId: string): Promise<void> => {
|
||||
|
||||
// --- 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 {
|
||||
const response = await apiClient.get<Message[]>(`/messages/session/${sessionId}`, {
|
||||
params: { limit, skip }
|
||||
});
|
||||
const response = await apiClient.post<WorkflowRunResult>('/workflow/run', payload);
|
||||
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'));
|
||||
// Return a failed result structure on API error
|
||||
return {
|
||||
success: false,
|
||||
message: handleApiError(error, 'runWorkflow'),
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
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-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.56.1",
|
||||
"reactflow": "^11.11.4",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"zod": "^3.24.3"
|
||||
|
||||
462
frontend/pnpm-lock.yaml
generated
462
frontend/pnpm-lock.yaml
generated
@ -53,6 +53,9 @@ importers:
|
||||
react-hook-form:
|
||||
specifier: ^7.56.1
|
||||
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:
|
||||
specifier: ^2.0.3
|
||||
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':
|
||||
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':
|
||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||
|
||||
@ -786,9 +825,105 @@ packages:
|
||||
'@tybys/wasm-util@0.9.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
||||
|
||||
'@types/geojson@7946.0.16':
|
||||
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
@ -1078,6 +1213,9 @@ packages:
|
||||
class-variance-authority@0.7.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
@ -1113,6 +1251,44 @@ packages:
|
||||
csstype@3.1.3:
|
||||
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:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
@ -1974,6 +2150,12 @@ packages:
|
||||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -2238,6 +2420,11 @@ packages:
|
||||
'@types/react':
|
||||
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:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -2270,6 +2457,21 @@ packages:
|
||||
zod@3.24.3:
|
||||
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:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
@ -2776,6 +2978,84 @@ snapshots:
|
||||
|
||||
'@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': {}
|
||||
|
||||
'@rushstack/eslint-patch@1.11.0': {}
|
||||
@ -2859,8 +3139,127 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
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/geojson@7946.0.16': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/json5@0.0.29': {}
|
||||
@ -3185,6 +3584,8 @@ snapshots:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
classcat@5.0.5: {}
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
@ -3221,6 +3622,42 @@ snapshots:
|
||||
|
||||
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: {}
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
@ -4195,6 +4632,20 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@ -4552,6 +5003,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@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:
|
||||
dependencies:
|
||||
is-bigint: 1.1.0
|
||||
@ -4602,3 +5057,10 @@ snapshots:
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
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