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";
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<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 {
if (value === null || typeof value !== "object") {
return JSON.stringify(value) ?? "null";
@ -1419,6 +1574,7 @@ export function AgentWorkflowSettingsClient({
Record<keyof AgentWorkflowSettingsPayload, string> | null
>(null);
const hasWarnedRemoteWorkflowUpdateRef = useRef(false);
const draftCancelRef = useRef<(() => void) | null>(null);
useEffect(() => {
if (!data) return;
@ -1633,26 +1789,36 @@ export function AgentWorkflowSettingsClient({
const draftTimer = window.setTimeout(() => {
const updatedAt = Date.now();
try {
writeAgentWorkflowSettingsDraft({
version: 2,
updatedAt,
baseSignature:
serverDraftBaseSignatureRef.current ||
createAgentWorkflowSettingsStateSignature(autoDraftState),
baseSignatures:
serverDraftBaseSignaturesRef.current ||
createAgentWorkflowSettingsSectionSignatures(autoDraftState),
state: autoDraftState,
});
setAutoDraftUpdatedAt(updatedAt);
setAutoDraftStatus("saved");
} catch {
toast.error("本地自动保存失败,请及时手动保存配置");
}
}, 700);
const cancelIdleDraft = scheduleWorkflowIdleTask(() => {
try {
writeAgentWorkflowSettingsDraft({
version: 2,
updatedAt,
baseSignature:
serverDraftBaseSignatureRef.current ||
createAgentWorkflowSettingsStateSignature(autoDraftState),
baseSignatures:
serverDraftBaseSignaturesRef.current ||
createAgentWorkflowSettingsSectionSignatures(autoDraftState),
state: autoDraftState,
});
startTransition(() => {
setAutoDraftUpdatedAt(updatedAt);
setAutoDraftStatus("saved");
});
} catch {
toast.error("本地自动保存失败,请及时手动保存配置");
}
});
return () => window.clearTimeout(draftTimer);
draftCancelRef.current = cancelIdleDraft;
}, 1200);
return () => {
window.clearTimeout(draftTimer);
draftCancelRef.current?.();
draftCancelRef.current = null;
};
}, [autoDraftState]);
const sortedSteps = useMemo(
@ -3603,12 +3769,12 @@ export function AgentWorkflowSettingsClient({
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-xs">按钮名称</Label>
<Input
<BufferedInput
value={option.buttonName}
onChange={(e) =>
onValueCommit={(value) =>
updateOption(step.key, option.key, (currentOption) => ({
...currentOption,
buttonName: e.target.value,
buttonName: value,
}))
}
placeholder="选项名称"
@ -3617,12 +3783,12 @@ export function AgentWorkflowSettingsClient({
</div>
<div className="space-y-1">
<Label className="text-xs">说明文案</Label>
<Input
<BufferedInput
value={option.description || ""}
onChange={(e) =>
onValueCommit={(value) =>
updateOption(step.key, option.key, (currentOption) => ({
...currentOption,
description: e.target.value,
description: value,
}))
}
placeholder="简短描述"
@ -3637,13 +3803,13 @@ export function AgentWorkflowSettingsClient({
{AGENT_WORKFLOW_PROMPT_FRAGMENT_MAX_LENGTH}
</span>
</div>
<Textarea
<BufferedTextarea
value={option.promptFragment || ""}
maxLength={AGENT_WORKFLOW_PROMPT_FRAGMENT_MAX_LENGTH}
onChange={(e) =>
onValueCommit={(value) =>
updateOption(step.key, option.key, (currentOption) => ({
...currentOption,
promptFragment: e.target.value,
promptFragment: value,
}))
}
placeholder="注入的提示词片段"
@ -3973,12 +4139,12 @@ export function AgentWorkflowSettingsClient({
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<Input
<BufferedInput
value={step.title}
onChange={(e) =>
onValueCommit={(value) =>
updateStep(step.key, (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"
@ -4077,12 +4243,12 @@ export function AgentWorkflowSettingsClient({
<CardContent className="border-t bg-muted/20 pt-4">
<div className="mb-4 space-y-2">
<Label className="text-xs">模块说明</Label>
<Textarea
<BufferedTextarea
value={step.description || ""}
onChange={(e) =>
onValueCommit={(value) =>
updateStep(step.key, (currentStep) => ({
...currentStep,
description: e.target.value,
description: value,
}))
}
rows={2}