From 6cc1807c72976099e67ea48e6598468899b76bdf Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 18 Feb 2026 21:04:23 +1300 Subject: [PATCH] fix: prevent cursor jumping in ModelParameters text/number inputs Use local state during editing and sync to store on blur, preventing React Flow re-renders from resetting cursor position on every keystroke. Also add click-to-seek on GenerateAudioNode waveform. Co-Authored-By: Claude Opus 4.6 --- .../__tests__/ModelParameters.test.tsx | 6 +-- src/components/nodes/ModelParameters.tsx | 51 ++++++++++++++----- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/components/__tests__/ModelParameters.test.tsx b/src/components/__tests__/ModelParameters.test.tsx index 3010a35b..157ef48e 100644 --- a/src/components/__tests__/ModelParameters.test.tsx +++ b/src/components/__tests__/ModelParameters.test.tsx @@ -284,9 +284,9 @@ describe("ModelParameters", () => { expect(screen.getByRole("textbox")).toBeInTheDocument(); }); - fireEvent.change(screen.getByRole("textbox"), { - target: { value: "new value" }, - }); + const textbox = screen.getByRole("textbox"); + fireEvent.change(textbox, { target: { value: "new value" } }); + fireEvent.blur(textbox); expect(onParametersChange).toHaveBeenCalledWith({ prompt: "new value" }); }); diff --git a/src/components/nodes/ModelParameters.tsx b/src/components/nodes/ModelParameters.tsx index c7f08684..cdb2178f 100644 --- a/src/components/nodes/ModelParameters.tsx +++ b/src/components/nodes/ModelParameters.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import { ProviderType, ModelInputDef } from "@/types"; import { ModelParameter } from "@/lib/providers/types"; import { useProviderApiKeys } from "@/store/workflowStore"; @@ -250,13 +250,29 @@ interface ParameterInputProps { } /** - * Individual parameter input based on type + * Individual parameter input based on type. + * Text and number inputs use local state during editing to prevent + * cursor-jump issues caused by React Flow re-renders on store updates. */ function ParameterInput({ param, value, onChange }: ParameterInputProps) { const displayName = param.name .replace(/_/g, " ") .replace(/\b\w/g, (c) => c.toUpperCase()); + // Local state for text/number inputs to prevent cursor jumping + const [localValue, setLocalValue] = useState(() => { + if (value === undefined || value === null) return ""; + return String(value); + }); + const isFocusedRef = useRef(false); + + // Sync from store when not focused (external changes) + useEffect(() => { + if (!isFocusedRef.current) { + setLocalValue(value === undefined || value === null ? "" : String(value)); + } + }, [value]); + // Determine input type and render accordingly if (param.enum && param.enum.length > 0) { // Enum: render as select @@ -319,15 +335,13 @@ function ParameterInput({ param, value, onChange }: ParameterInputProps) { } if (param.type === "number" || param.type === "integer") { - // Number: render as number input with validation - const numValue = value !== undefined ? Number(value) : ""; const hasMin = param.minimum !== undefined; const hasMax = param.maximum !== undefined; // Validate current value against constraints let validationError: string | null = null; - if (value !== undefined && value !== "" && !isNaN(Number(value))) { - const num = Number(value); + if (localValue !== "" && !isNaN(Number(localValue))) { + const num = Number(localValue); if (hasMin && num < param.minimum!) { validationError = `Min: ${param.minimum}`; } else if (hasMax && num > param.maximum!) { @@ -352,16 +366,20 @@ function ParameterInput({ param, value, onChange }: ParameterInputProps) { { isFocusedRef.current = true; }} onChange={(e) => { - const val = e.target.value; - if (val === "") { + setLocalValue(e.target.value); + }} + onBlur={() => { + isFocusedRef.current = false; + if (localValue === "") { onChange(undefined); } else { - const num = param.type === "integer" ? parseInt(val, 10) : parseFloat(val); + const num = param.type === "integer" ? parseInt(localValue, 10) : parseFloat(localValue); onChange(isNaN(num) ? undefined : num); } }} @@ -384,7 +402,7 @@ function ParameterInput({ param, value, onChange }: ParameterInputProps) { return null; } - // Default: string input + // Default: string input — uses local state, syncs to store on blur return (