104 lines
6.5 KiB
TypeScript
104 lines
6.5 KiB
TypeScript
// 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); |