Update debounce logic for assistant renaming

This commit is contained in:
akhisud3195 2025-08-18 15:20:59 +05:30
parent 0a6794a467
commit b4b862c8e0
2 changed files with 64 additions and 37 deletions

View file

@ -8,6 +8,7 @@ interface TopBarProps {
localProjectName: string; localProjectName: string;
projectNameError: string | null; projectNameError: string | null;
onProjectNameChange: (value: string) => void; onProjectNameChange: (value: string) => void;
onProjectNameCommit: (value: string) => void;
publishing: boolean; publishing: boolean;
isLive: boolean; isLive: boolean;
showCopySuccess: boolean; showCopySuccess: boolean;
@ -29,6 +30,7 @@ export function TopBar({
localProjectName, localProjectName,
projectNameError, projectNameError,
onProjectNameChange, onProjectNameChange,
onProjectNameCommit,
publishing, publishing,
isLive, isLive,
showCopySuccess, showCopySuccess,
@ -58,6 +60,12 @@ export function TopBar({
type="text" type="text"
value={localProjectName} value={localProjectName}
onChange={(e) => onProjectNameChange(e.target.value)} onChange={(e) => onProjectNameChange(e.target.value)}
onBlur={() => onProjectNameCommit(localProjectName)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.currentTarget.blur();
}
}}
isInvalid={!!projectNameError} isInvalid={!!projectNameError}
errorMessage={projectNameError} errorMessage={projectNameError}
placeholder="Project name..." placeholder="Project name..."

View file

@ -25,6 +25,7 @@ import { Copilot } from "../copilot/app";
import { publishWorkflow } from "@/app/actions/project.actions"; import { publishWorkflow } from "@/app/actions/project.actions";
import { saveWorkflow } from "@/app/actions/project.actions"; import { saveWorkflow } from "@/app/actions/project.actions";
import { updateProjectName } from "@/app/actions/project.actions"; import { updateProjectName } from "@/app/actions/project.actions";
import { listProjects } from "@/app/actions/project.actions";
import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/icons"; import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/icons";
import { CopyIcon, ImportIcon, RadioIcon, RedoIcon, ServerIcon, Sparkles, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, XIcon, SettingsIcon, ChevronDownIcon, PhoneIcon, MessageCircleIcon, ZapIcon } from "lucide-react"; import { CopyIcon, ImportIcon, RadioIcon, RedoIcon, ServerIcon, Sparkles, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, XIcon, SettingsIcon, ChevronDownIcon, PhoneIcon, MessageCircleIcon, ZapIcon } from "lucide-react";
import { EntityList } from "./entity_list"; import { EntityList } from "./entity_list";
@ -887,8 +888,8 @@ export function WorkflowEditor({
// Project name state // Project name state
const [localProjectName, setLocalProjectName] = useState<string>(projectConfig.name || ''); const [localProjectName, setLocalProjectName] = useState<string>(projectConfig.name || '');
const [projectNameError, setProjectNameError] = useState<string | null>(null); const [projectNameError, setProjectNameError] = useState<string | null>(null);
const editingNameRef = useRef<string | null>(null); const [isEditingProjectName, setIsEditingProjectName] = useState<boolean>(false);
const projectNameDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); const [pendingProjectName, setPendingProjectName] = useState<string | null>(null);
// Load agent order from localStorage on mount // Load agent order from localStorage on mount
// useEffect(() => { // useEffect(() => {
@ -1182,12 +1183,23 @@ export function WorkflowEditor({
} }
}, [state.present.workflow, state.present.pendingChanges, processQueue, state]); }, [state.present.workflow, state.present.pendingChanges, processQueue, state]);
// Sync project name when projectConfig changes, but not while actively editing // Sync project name from server when not editing and no pending commit in-flight
useEffect(() => { useEffect(() => {
if (editingNameRef.current === null) { if (!isEditingProjectName && pendingProjectName === null) {
setLocalProjectName(projectConfig.name || ''); setLocalProjectName(projectConfig.name || '');
} }
}, [projectConfig.name]); }, [projectConfig.name, isEditingProjectName, pendingProjectName]);
// When a commit is pending, wait until server reflects it to clear the lock
useEffect(() => {
if (
pendingProjectName &&
(projectConfig.name || '').trim().toLowerCase() === pendingProjectName.trim().toLowerCase()
) {
setPendingProjectName(null);
setLocalProjectName(projectConfig.name || '');
}
}, [projectConfig.name, pendingProjectName]);
function handlePlaygroundClick() { function handlePlaygroundClick() {
setIsInitialState(false); setIsInitialState(false);
@ -1204,41 +1216,47 @@ export function WorkflowEditor({
const handleProjectNameChange = (value: string) => { const handleProjectNameChange = (value: string) => {
setLocalProjectName(value); setLocalProjectName(value);
editingNameRef.current = value; setIsEditingProjectName(true);
// Do not validate or save on every keystroke
if (projectNameDebounceRef.current) {
clearTimeout(projectNameDebounceRef.current);
}
projectNameDebounceRef.current = setTimeout(async () => {
const trimmed = value.trim();
if (!validateProjectName(trimmed)) {
editingNameRef.current = null;
return;
}
try {
if (trimmed !== (projectConfig.name || '')) {
await updateProjectName(projectId, trimmed);
onProjectConfigUpdated?.();
}
} catch (error) {
setProjectNameError("Failed to update project name");
console.error('Failed to update project name:', error);
} finally {
editingNameRef.current = null;
}
}, 500);
}; };
// Clear any pending debounce on unmount const handleProjectNameCommit = async (value: string) => {
useEffect(() => { const trimmed = value.trim();
return () => { // If unchanged, just clear editing state
if (projectNameDebounceRef.current) { if (trimmed === (projectConfig.name || '')) {
clearTimeout(projectNameDebounceRef.current); setProjectNameError(null);
setIsEditingProjectName(false);
return;
}
if (!validateProjectName(trimmed)) {
setIsEditingProjectName(false);
return;
}
try {
// Validate uniqueness against other projects (case-insensitive)
const projects = await listProjects();
const isDuplicate = projects.some(p => ((p as any).id ?? (p as any)._id) !== projectId && (p.name || '').trim().toLowerCase() === trimmed.toLowerCase());
if (isDuplicate) {
setProjectNameError("This name is already taken by another project");
return;
} }
}; // Lock local sync until server reflects the change
}, []); setPendingProjectName(trimmed);
await updateProjectName(projectId, trimmed);
onProjectConfigUpdated?.();
setProjectNameError(null);
} catch (error) {
setProjectNameError("Failed to update project name");
console.error('Failed to update project name:', error);
// Clear pending state so we resync from server
setPendingProjectName(null);
setLocalProjectName(projectConfig.name || '');
} finally {
setIsEditingProjectName(false);
}
};
return ( return (
<EntitySelectionContext.Provider value={{ <EntitySelectionContext.Provider value={{
@ -1252,6 +1270,7 @@ export function WorkflowEditor({
localProjectName={localProjectName} localProjectName={localProjectName}
projectNameError={projectNameError} projectNameError={projectNameError}
onProjectNameChange={handleProjectNameChange} onProjectNameChange={handleProjectNameChange}
onProjectNameCommit={handleProjectNameCommit}
publishing={state.present.publishing} publishing={state.present.publishing}
isLive={isLive} isLive={isLive}
showCopySuccess={showCopySuccess} showCopySuccess={showCopySuccess}