fix: reduce workflow settings input lag
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-21 19:25:46 +08:00
parent 7a2169576d
commit 396bbf9d9b

View File

@ -1,7 +1,18 @@
"use client"; "use client";
import Link from "next/link"; 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 useSWR from "swr";
import { toast } from "sonner"; import { toast } from "sonner";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
@ -176,6 +187,150 @@ interface AgentWorkflowSettingsDraft {
state: AgentWorkflowSettingsDraftState; state: AgentWorkflowSettingsDraftState;
} }
type BufferedInputProps = Omit<
ComponentProps<typeof Input>,
"defaultValue" | "onChange" | "value"
> & {
commitDelayMs?: number;
onValueCommit: (value: string) => void;
value: string;
};
type BufferedTextareaProps = Omit<
ComponentProps<typeof Textarea>,
"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 (
<Input
{...props}
value={draftValue}
onChange={(event) => 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 (
<Textarea
{...props}
value={draftValue}
onChange={(event) => setDraftValue(event.target.value)}
onBlur={(event) => {
commitValue(draftValue, { immediate: true });
onBlur?.(event);
}}
/>
);
}
function stableStringifyWorkflowDraftValue(value: unknown): string { function stableStringifyWorkflowDraftValue(value: unknown): string {
if (value === null || typeof value !== "object") { if (value === null || typeof value !== "object") {
return JSON.stringify(value) ?? "null"; return JSON.stringify(value) ?? "null";
@ -1419,6 +1574,7 @@ export function AgentWorkflowSettingsClient({
Record<keyof AgentWorkflowSettingsPayload, string> | null Record<keyof AgentWorkflowSettingsPayload, string> | null
>(null); >(null);
const hasWarnedRemoteWorkflowUpdateRef = useRef(false); const hasWarnedRemoteWorkflowUpdateRef = useRef(false);
const draftCancelRef = useRef<(() => void) | null>(null);
useEffect(() => { useEffect(() => {
if (!data) return; if (!data) return;
@ -1633,26 +1789,36 @@ export function AgentWorkflowSettingsClient({
const draftTimer = window.setTimeout(() => { const draftTimer = window.setTimeout(() => {
const updatedAt = Date.now(); const updatedAt = Date.now();
try { const cancelIdleDraft = scheduleWorkflowIdleTask(() => {
writeAgentWorkflowSettingsDraft({ try {
version: 2, writeAgentWorkflowSettingsDraft({
updatedAt, version: 2,
baseSignature: updatedAt,
serverDraftBaseSignatureRef.current || baseSignature:
createAgentWorkflowSettingsStateSignature(autoDraftState), serverDraftBaseSignatureRef.current ||
baseSignatures: createAgentWorkflowSettingsStateSignature(autoDraftState),
serverDraftBaseSignaturesRef.current || baseSignatures:
createAgentWorkflowSettingsSectionSignatures(autoDraftState), serverDraftBaseSignaturesRef.current ||
state: autoDraftState, createAgentWorkflowSettingsSectionSignatures(autoDraftState),
}); state: autoDraftState,
setAutoDraftUpdatedAt(updatedAt); });
setAutoDraftStatus("saved"); startTransition(() => {
} catch { setAutoDraftUpdatedAt(updatedAt);
toast.error("本地自动保存失败,请及时手动保存配置"); setAutoDraftStatus("saved");
} });
}, 700); } catch {
toast.error("本地自动保存失败,请及时手动保存配置");
}
});
return () => window.clearTimeout(draftTimer); draftCancelRef.current = cancelIdleDraft;
}, 1200);
return () => {
window.clearTimeout(draftTimer);
draftCancelRef.current?.();
draftCancelRef.current = null;
};
}, [autoDraftState]); }, [autoDraftState]);
const sortedSteps = useMemo( const sortedSteps = useMemo(
@ -3603,12 +3769,12 @@ export function AgentWorkflowSettingsClient({
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-xs">按钮名称</Label> <Label className="text-xs">按钮名称</Label>
<Input <BufferedInput
value={option.buttonName} value={option.buttonName}
onChange={(e) => onValueCommit={(value) =>
updateOption(step.key, option.key, (currentOption) => ({ updateOption(step.key, option.key, (currentOption) => ({
...currentOption, ...currentOption,
buttonName: e.target.value, buttonName: value,
})) }))
} }
placeholder="选项名称" placeholder="选项名称"
@ -3617,12 +3783,12 @@ export function AgentWorkflowSettingsClient({
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-xs">说明文案</Label> <Label className="text-xs">说明文案</Label>
<Input <BufferedInput
value={option.description || ""} value={option.description || ""}
onChange={(e) => onValueCommit={(value) =>
updateOption(step.key, option.key, (currentOption) => ({ updateOption(step.key, option.key, (currentOption) => ({
...currentOption, ...currentOption,
description: e.target.value, description: value,
})) }))
} }
placeholder="简短描述" placeholder="简短描述"
@ -3637,13 +3803,13 @@ export function AgentWorkflowSettingsClient({
{AGENT_WORKFLOW_PROMPT_FRAGMENT_MAX_LENGTH} {AGENT_WORKFLOW_PROMPT_FRAGMENT_MAX_LENGTH}
</span> </span>
</div> </div>
<Textarea <BufferedTextarea
value={option.promptFragment || ""} value={option.promptFragment || ""}
maxLength={AGENT_WORKFLOW_PROMPT_FRAGMENT_MAX_LENGTH} maxLength={AGENT_WORKFLOW_PROMPT_FRAGMENT_MAX_LENGTH}
onChange={(e) => onValueCommit={(value) =>
updateOption(step.key, option.key, (currentOption) => ({ updateOption(step.key, option.key, (currentOption) => ({
...currentOption, ...currentOption,
promptFragment: e.target.value, promptFragment: value,
})) }))
} }
placeholder="注入的提示词片段" placeholder="注入的提示词片段"
@ -3973,12 +4139,12 @@ export function AgentWorkflowSettingsClient({
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Input <BufferedInput
value={step.title} value={step.title}
onChange={(e) => onValueCommit={(value) =>
updateStep(step.key, (currentStep) => ({ updateStep(step.key, (currentStep) => ({
...currentStep, ...currentStep,
title: e.target.value, title: value,
})) }))
} }
className="h-8 min-w-[180px] max-w-[260px] border-0 bg-transparent px-0 text-base font-medium shadow-none focus-visible:ring-0" className="h-8 min-w-[180px] max-w-[260px] border-0 bg-transparent px-0 text-base font-medium shadow-none focus-visible:ring-0"
@ -4077,12 +4243,12 @@ export function AgentWorkflowSettingsClient({
<CardContent className="border-t bg-muted/20 pt-4"> <CardContent className="border-t bg-muted/20 pt-4">
<div className="mb-4 space-y-2"> <div className="mb-4 space-y-2">
<Label className="text-xs">模块说明</Label> <Label className="text-xs">模块说明</Label>
<Textarea <BufferedTextarea
value={step.description || ""} value={step.description || ""}
onChange={(e) => onValueCommit={(value) =>
updateStep(step.key, (currentStep) => ({ updateStep(step.key, (currentStep) => ({
...currentStep, ...currentStep,
description: e.target.value, description: value,
})) }))
} }
rows={2} rows={2}