From c9e8b0c123ab6f3b9092cb331ce583fe745fa3a5 Mon Sep 17 00:00:00 2001 From: adrian Date: Fri, 2 May 2025 17:31:33 +0800 Subject: [PATCH] add run workflow --- backend/app/api/v1/api.py | 3 +- backend/app/api/v1/endpoints/workflow.py | 22 ++ backend/app/flow_components/base.py | 86 ++++++ backend/app/flow_components/chat_input.py | 27 ++ backend/app/flow_components/chat_output.py | 38 +++ backend/app/flow_components/llm_node.py | 77 +++++ backend/app/main.py | 2 + backend/app/models/pydantic_models.py | 43 ++- backend/app/services/workflow_service.py | 154 ++++++++++ .../app/workflow/components/ChatInputNode.tsx | 69 +++++ .../workflow/components/ChatOutputNode.tsx | 84 ++++++ frontend/app/workflow/components/LLMNode.tsx | 265 +++++++++++++----- .../workflow/components/WorkflowSidebar.tsx | 4 +- frontend/app/workflow/components/types.ts | 14 + frontend/app/workflow/page.tsx | 98 +++++-- frontend/lib/api.ts | 130 ++++++--- frontend/lib/types.ts | 49 ++++ 17 files changed, 1037 insertions(+), 128 deletions(-) create mode 100644 backend/app/flow_components/base.py create mode 100644 backend/app/flow_components/chat_input.py create mode 100644 backend/app/flow_components/chat_output.py create mode 100644 backend/app/flow_components/llm_node.py create mode 100644 frontend/app/workflow/components/ChatInputNode.tsx create mode 100644 frontend/app/workflow/components/ChatOutputNode.tsx create mode 100644 frontend/lib/types.ts diff --git a/backend/app/api/v1/api.py b/backend/app/api/v1/api.py index c5f4c57..7285c66 100644 --- a/backend/app/api/v1/api.py +++ b/backend/app/api/v1/api.py @@ -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 \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/workflow.py b/backend/app/api/v1/endpoints/workflow.py index e69de29..8da3858 100644 --- a/backend/app/api/v1/endpoints/workflow.py +++ b/backend/app/api/v1/endpoints/workflow.py @@ -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 \ No newline at end of file diff --git a/backend/app/flow_components/base.py b/backend/app/flow_components/base.py new file mode 100644 index 0000000..23ff274 --- /dev/null +++ b/backend/app/flow_components/base.py @@ -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 \ No newline at end of file diff --git a/backend/app/flow_components/chat_input.py b/backend/app/flow_components/chat_input.py new file mode 100644 index 0000000..744ef69 --- /dev/null +++ b/backend/app/flow_components/chat_input.py @@ -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 \ No newline at end of file diff --git a/backend/app/flow_components/chat_output.py b/backend/app/flow_components/chat_output.py new file mode 100644 index 0000000..d269f5f --- /dev/null +++ b/backend/app/flow_components/chat_output.py @@ -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' diff --git a/backend/app/flow_components/llm_node.py b/backend/app/flow_components/llm_node.py new file mode 100644 index 0000000..c28f0e0 --- /dev/null +++ b/backend/app/flow_components/llm_node.py @@ -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 \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index 45f25b4..7d86361 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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("应用程序关闭中...") diff --git a/backend/app/models/pydantic_models.py b/backend/app/models/pydantic_models.py index c5278c2..55c475b 100644 --- a/backend/app/models/pydantic_models.py +++ b/backend/app/models/pydantic_models.py @@ -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 \ No newline at end of file + 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 diff --git a/backend/app/services/workflow_service.py b/backend/app/services/workflow_service.py index e69de29..f2c8cf7 100644 --- a/backend/app/services/workflow_service.py +++ b/backend/app/services/workflow_service.py @@ -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="工作流执行完成,但未找到指定的输出节点。" + ) diff --git a/frontend/app/workflow/components/ChatInputNode.tsx b/frontend/app/workflow/components/ChatInputNode.tsx new file mode 100644 index 0000000..4c23107 --- /dev/null +++ b/frontend/app/workflow/components/ChatInputNode.tsx @@ -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) => { + const { setNodes } = useReactFlow(); + + const handleTextChange = useCallback((event: ChangeEvent) => { + const newText = event.target.value; + setNodes((nds: Node[]) => + nds.map((node) => { + if (node.id === id) { + return { ...node, data: { ...node.data, text: newText } }; + } + return node; + }) + ); + }, [id, setNodes]); + + return ( +
{/* 调整宽度 */} + {/* 节点头部 */} +
+
+ + Chat Input +
+

从 Playground 获取聊天输入。

+
+ + {/* 节点内容 */} +
+
+ +