diff --git a/src/components/ConnectionDropMenu.tsx b/src/components/ConnectionDropMenu.tsx
index 00ad2c8e..7f460ef5 100644
--- a/src/components/ConnectionDropMenu.tsx
+++ b/src/components/ConnectionDropMenu.tsx
@@ -73,6 +73,15 @@ const IMAGE_TARGET_OPTIONS: MenuOption[] = [
];
const TEXT_TARGET_OPTIONS: MenuOption[] = [
+ {
+ type: "prompt",
+ label: "Prompt",
+ icon: (
+
+ ),
+ },
{
type: "nanoBanana",
label: "Generate Image",
diff --git a/src/components/ProjectSetupModal.tsx b/src/components/ProjectSetupModal.tsx
index 5cf6eaed..0812ccfa 100644
--- a/src/components/ProjectSetupModal.tsx
+++ b/src/components/ProjectSetupModal.tsx
@@ -778,6 +778,27 @@ export function ProjectSetupModal({
className="flex-1 h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-neutral-400"
/>
+
+ {/* Max Tokens slider */}
+
+
+ {
+ setLocalNodeDefaults(prev => ({
+ ...prev,
+ llm: { ...prev.llm, maxTokens: parseInt(e.target.value, 10) }
+ }));
+ }}
+ className="flex-1 h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-neutral-400"
+ />
+
diff --git a/src/components/__tests__/ConnectionDropMenu.test.tsx b/src/components/__tests__/ConnectionDropMenu.test.tsx
index c5851137..a2d2c556 100644
--- a/src/components/__tests__/ConnectionDropMenu.test.tsx
+++ b/src/components/__tests__/ConnectionDropMenu.test.tsx
@@ -226,14 +226,15 @@ describe("ConnectionDropMenu", () => {
it("should wrap around when navigating past last item", () => {
render();
- // Text options: nanoBanana, generateVideo, llmGenerate (3 items)
- // Navigate down 3 times to wrap to first
+ // Text target options: Prompt, nanoBanana, generateVideo, llmGenerate (4 items)
+ // Navigate down 4 times to wrap to first
+ fireEvent.keyDown(document, { key: "ArrowDown" });
fireEvent.keyDown(document, { key: "ArrowDown" });
fireEvent.keyDown(document, { key: "ArrowDown" });
fireEvent.keyDown(document, { key: "ArrowDown" });
- // Should be back on first item
- const firstButton = screen.getByText("Generate Image").closest("button");
+ // Should be back on first item (Prompt)
+ const firstButton = screen.getByText("Prompt").closest("button");
expect(firstButton).toHaveClass("bg-neutral-700");
});
});
diff --git a/src/components/__tests__/LLMGenerateNode.test.tsx b/src/components/__tests__/LLMGenerateNode.test.tsx
index 2f3f43bf..d32d4d09 100644
--- a/src/components/__tests__/LLMGenerateNode.test.tsx
+++ b/src/components/__tests__/LLMGenerateNode.test.tsx
@@ -194,14 +194,18 @@ describe("LLMGenerateNode", () => {
});
describe("Temperature Slider", () => {
- it("should render temperature slider with current value", () => {
+ it("should render temperature slider with current value after expanding parameters", () => {
render(
);
- expect(screen.getByText("Temp: 0.7")).toBeInTheDocument();
+ // Expand the Parameters section
+ const parametersButton = screen.getByText("Parameters");
+ fireEvent.click(parametersButton);
+
+ expect(screen.getByText("Temperature: 0.7")).toBeInTheDocument();
});
it("should call updateNodeData when temperature is changed", () => {
@@ -211,8 +215,14 @@ describe("LLMGenerateNode", () => {
);
- const slider = screen.getByRole("slider");
- fireEvent.change(slider, { target: { value: "1.5" } });
+ // Expand the Parameters section
+ const parametersButton = screen.getByText("Parameters");
+ fireEvent.click(parametersButton);
+
+ // Get the first slider (temperature) - there are two sliders: temperature and maxTokens
+ const sliders = screen.getAllByRole("slider");
+ const temperatureSlider = sliders[0];
+ fireEvent.change(temperatureSlider, { target: { value: "1.5" } });
expect(mockUpdateNodeData).toHaveBeenCalledWith("test-llm-1", {
temperature: 1.5,
diff --git a/src/components/__tests__/PromptNode.test.tsx b/src/components/__tests__/PromptNode.test.tsx
index a3b3702d..eb402d3a 100644
--- a/src/components/__tests__/PromptNode.test.tsx
+++ b/src/components/__tests__/PromptNode.test.tsx
@@ -17,6 +17,8 @@ vi.mock("@/store/workflowStore", () => ({
currentNodeId: null,
groups: {},
nodes: [],
+ edges: [],
+ getConnectedInputs: vi.fn(() => ({ images: [], videos: [], text: null, dynamicInputs: {} })),
getNodesWithComments: vi.fn(() => []),
markCommentViewed: vi.fn(),
setNavigationTarget: vi.fn(),
diff --git a/src/components/nodes/LLMGenerateNode.tsx b/src/components/nodes/LLMGenerateNode.tsx
index 3eb34267..c4e05614 100644
--- a/src/components/nodes/LLMGenerateNode.tsx
+++ b/src/components/nodes/LLMGenerateNode.tsx
@@ -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) => {
+ 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 {
+ 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
+