From 396bbf9d9b19f8955130da6310cbecb84577f00a Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 21 Apr 2026 19:25:46 +0800 Subject: [PATCH] fix: reduce workflow settings input lag --- .../agent-workflow-settings-client.tsx | 236 +++++++++++++++--- 1 file changed, 201 insertions(+), 35 deletions(-) diff --git a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx index 13d691c92..dd90ea7a3 100644 --- a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx +++ b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx @@ -1,7 +1,18 @@ "use client"; import Link from "next/link"; -import { useDeferredValue, useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent, type ReactNode } from "react"; +import { + startTransition, + useDeferredValue, + useEffect, + useMemo, + useRef, + useState, + type ChangeEvent, + type ComponentProps, + type DragEvent, + type ReactNode, +} from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { motion, AnimatePresence } from "framer-motion"; @@ -176,6 +187,150 @@ interface AgentWorkflowSettingsDraft { state: AgentWorkflowSettingsDraftState; } +type BufferedInputProps = Omit< + ComponentProps, + "defaultValue" | "onChange" | "value" +> & { + commitDelayMs?: number; + onValueCommit: (value: string) => void; + value: string; +}; + +type BufferedTextareaProps = Omit< + ComponentProps, + "defaultValue" | "onChange" | "value" +> & { + commitDelayMs?: number; + onValueCommit: (value: string) => void; + value: string; +}; + +function scheduleWorkflowIdleTask(callback: () => void, timeoutMs = 2000) { + if (typeof window === "undefined") { + return () => {}; + } + + if ("requestIdleCallback" in window && "cancelIdleCallback" in window) { + const idleId = window.requestIdleCallback(callback, { timeout: timeoutMs }); + return () => window.cancelIdleCallback(idleId); + } + + const timer = window.setTimeout(callback, timeoutMs); + return () => window.clearTimeout(timer); +} + +function BufferedInput({ + commitDelayMs = 450, + onBlur, + onValueCommit, + value, + ...props +}: BufferedInputProps) { + const [draftValue, setDraftValue] = useState(value); + const committedValueRef = useRef(value); + + useEffect(() => { + setDraftValue(value); + committedValueRef.current = value; + }, [value]); + + const commitValue = (nextValue: string, options?: { immediate?: boolean }) => { + if (nextValue === committedValueRef.current) { + return; + } + + committedValueRef.current = nextValue; + if (options?.immediate) { + onValueCommit(nextValue); + return; + } + + startTransition(() => { + onValueCommit(nextValue); + }); + }; + + useEffect(() => { + if (draftValue === committedValueRef.current) { + return; + } + + const timer = window.setTimeout(() => { + commitValue(draftValue); + }, commitDelayMs); + + return () => window.clearTimeout(timer); + }, [commitDelayMs, draftValue]); + + return ( + setDraftValue(event.target.value)} + onBlur={(event) => { + commitValue(draftValue, { immediate: true }); + onBlur?.(event); + }} + /> + ); +} + +function BufferedTextarea({ + commitDelayMs = 450, + onBlur, + onValueCommit, + value, + ...props +}: BufferedTextareaProps) { + const [draftValue, setDraftValue] = useState(value); + const committedValueRef = useRef(value); + + useEffect(() => { + setDraftValue(value); + committedValueRef.current = value; + }, [value]); + + const commitValue = (nextValue: string, options?: { immediate?: boolean }) => { + if (nextValue === committedValueRef.current) { + return; + } + + committedValueRef.current = nextValue; + if (options?.immediate) { + onValueCommit(nextValue); + return; + } + + startTransition(() => { + onValueCommit(nextValue); + }); + }; + + useEffect(() => { + if (draftValue === committedValueRef.current) { + return; + } + + const timer = window.setTimeout(() => { + commitValue(draftValue); + }, commitDelayMs); + + return () => window.clearTimeout(timer); + }, [commitDelayMs, draftValue]); + + return ( +