|
|
|
@ -1,6 +1,6 @@ |
|
|
|
"use client"; |
|
|
|
|
|
|
|
import { useCallback } from "react"; |
|
|
|
import { useCallback, useState } from "react"; |
|
|
|
import { Handle, Position, NodeProps, Node } from "@xyflow/react"; |
|
|
|
import { BaseNode } from "./BaseNode"; |
|
|
|
import { useCommentNavigation } from "@/hooks/useCommentNavigation"; |
|
|
|
@ -57,6 +57,15 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod |
|
|
|
[id, updateNodeData] |
|
|
|
); |
|
|
|
|
|
|
|
const handleMaxTokensChange = useCallback( |
|
|
|
(e: React.ChangeEvent<HTMLInputElement>) => { |
|
|
|
updateNodeData(id, { maxTokens: parseInt(e.target.value, 10) }); |
|
|
|
}, |
|
|
|
[id, updateNodeData] |
|
|
|
); |
|
|
|
|
|
|
|
const [showParams, setShowParams] = useState(false); |
|
|
|
|
|
|
|
const regenerateNode = useWorkflowStore((state) => state.regenerateNode); |
|
|
|
const isRunning = useWorkflowStore((state) => state.isRunning); |
|
|
|
|
|
|
|
@ -68,6 +77,20 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod |
|
|
|
updateNodeData(id, { outputText: null, status: "idle", error: null }); |
|
|
|
}, [id, updateNodeData]); |
|
|
|
|
|
|
|
const [copied, setCopied] = useState(false); |
|
|
|
|
|
|
|
const handleCopyOutput = useCallback(async () => { |
|
|
|
if (nodeData.outputText) { |
|
|
|
try { |
|
|
|
await navigator.clipboard.writeText(nodeData.outputText); |
|
|
|
setCopied(true); |
|
|
|
setTimeout(() => setCopied(false), 1500); |
|
|
|
} catch (err) { |
|
|
|
console.error("Failed to copy text:", err); |
|
|
|
} |
|
|
|
} |
|
|
|
}, [nodeData.outputText]); |
|
|
|
|
|
|
|
const provider = nodeData.provider || "google"; |
|
|
|
const availableModels = MODELS[provider] || MODELS.google; |
|
|
|
const model = availableModels.some(m => m.value === nodeData.model) |
|
|
|
@ -145,6 +168,21 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod |
|
|
|
{nodeData.outputText} |
|
|
|
</p> |
|
|
|
<div className="absolute top-1 right-1 flex gap-1"> |
|
|
|
<button |
|
|
|
onClick={handleCopyOutput} |
|
|
|
className={`w-5 h-5 ${copied ? "bg-green-600/80" : "bg-neutral-900/80 hover:bg-neutral-700/80"} rounded flex items-center justify-center text-neutral-400 hover:text-white transition-colors`} |
|
|
|
title={copied ? "Copied!" : "Copy to clipboard"} |
|
|
|
> |
|
|
|
{copied ? ( |
|
|
|
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /> |
|
|
|
</svg> |
|
|
|
) : ( |
|
|
|
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> |
|
|
|
</svg> |
|
|
|
)} |
|
|
|
</button> |
|
|
|
<button |
|
|
|
onClick={handleRegenerate} |
|
|
|
disabled={isRunning} |
|
|
|
@ -201,18 +239,53 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod |
|
|
|
))} |
|
|
|
</select> |
|
|
|
|
|
|
|
{/* Temperature slider */} |
|
|
|
<div className="flex flex-col gap-0.5 shrink-0"> |
|
|
|
<label className="text-[9px] text-neutral-500">Temp: {nodeData.temperature.toFixed(1)}</label> |
|
|
|
<input |
|
|
|
type="range" |
|
|
|
min="0" |
|
|
|
max="2" |
|
|
|
step="0.1" |
|
|
|
value={nodeData.temperature} |
|
|
|
onChange={handleTemperatureChange} |
|
|
|
className="w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-neutral-400" |
|
|
|
/> |
|
|
|
{/* Collapsible parameters section */} |
|
|
|
<div className="shrink-0"> |
|
|
|
<button |
|
|
|
onClick={() => setShowParams(!showParams)} |
|
|
|
className="w-full flex items-center justify-between text-[9px] text-neutral-400 hover:text-neutral-300 py-1" |
|
|
|
> |
|
|
|
<span>Parameters</span> |
|
|
|
<svg |
|
|
|
className={`w-3 h-3 transition-transform ${showParams ? "rotate-180" : ""}`} |
|
|
|
fill="none" |
|
|
|
viewBox="0 0 24 24" |
|
|
|
stroke="currentColor" |
|
|
|
strokeWidth={2} |
|
|
|
> |
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" /> |
|
|
|
</svg> |
|
|
|
</button> |
|
|
|
{showParams && ( |
|
|
|
<div className="flex flex-col gap-2 pt-1 border-t border-neutral-700/50"> |
|
|
|
{/* Temperature slider */} |
|
|
|
<div className="flex flex-col gap-0.5"> |
|
|
|
<label className="text-[9px] text-neutral-500">Temperature: {nodeData.temperature.toFixed(1)}</label> |
|
|
|
<input |
|
|
|
type="range" |
|
|
|
min="0" |
|
|
|
max="2" |
|
|
|
step="0.1" |
|
|
|
value={nodeData.temperature} |
|
|
|
onChange={handleTemperatureChange} |
|
|
|
className="nodrag w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-neutral-400" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
{/* Max tokens slider */} |
|
|
|
<div className="flex flex-col gap-0.5"> |
|
|
|
<label className="text-[9px] text-neutral-500">Max Tokens: {nodeData.maxTokens.toLocaleString()}</label> |
|
|
|
<input |
|
|
|
type="range" |
|
|
|
min="256" |
|
|
|
max="16384" |
|
|
|
step="256" |
|
|
|
value={nodeData.maxTokens} |
|
|
|
onChange={handleMaxTokensChange} |
|
|
|
className="nodrag w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-neutral-400" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</BaseNode> |
|
|
|
|