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