fix: reduce workflow settings input lag
This commit is contained in:
parent
7a2169576d
commit
396bbf9d9b
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user