2025-05-01 14:10:46 +08:00

104 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 方法
// 从 props 更新内部状态 (如果外部数据变化)
// useEffect(() => {
// setCurrentData(data);
// }, [data]);
// 处理内部表单变化的通用回调 - 直接更新 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 react-flow__node-genericNode nopan selected selectable draggable bg-purple-50 dark:bg-gray-800 border border-purple-200 dark:border-gray-700 rounded-lg shadow-lg w-96 overflow-hidden">
<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">
<Label className="text-xs font-semibold text-gray-500 dark:text-gray-400"></Label>
<Handle type="target" position={Position.Left} id="input-text" isConnectable={isConnectable} className="w-3 h-3 !bg-blue-500 top-1/2" />
{/* 直接使用 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" />
</div>
</div>
);
};
LLMNodeComponent.displayName = 'LLMNode';
export const LLMNode = memo(LLMNodeComponent);