2025-05-02 17:31:33 +08:00

232 lines
7.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 方法
// 处理内部表单变化的通用回调 - 直接更新 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);