fix(ui): drop stale classifier prompt preview responses

This commit is contained in:
Tin Chi Lo 2026-08-28 11:27:29 -07:00
parent b4af020632
commit 27d63d57e4
2 changed files with 47 additions and 24 deletions

View file

@ -78,6 +78,32 @@ describe("CustomTierPromptEditor", () => {
);
});
it("ignores a stale response that resolves after a newer one", async () => {
let resolveFirst: (text: string) => void = () => {};
getAutoRouterCustomTierPromptCall
.mockImplementationOnce(
() =>
new Promise<string>((resolve) => {
resolveFirst = resolve;
}),
)
.mockResolvedValueOnce("assembled from the edited draft");
renderEditor();
fireEvent.click(screen.getByRole("button", { name: "Edit prompt" }));
await vi.waitFor(() => expect(getAutoRouterCustomTierPromptCall).toHaveBeenCalledTimes(1));
fireEvent.change(screen.getByLabelText("Classifier opening instructions"), { target: { value: "edited" } });
expect(await screen.findByLabelText("Assembled classifier prompt")).toHaveTextContent(
"assembled from the edited draft",
);
resolveFirst("assembled from the stale draft");
await new Promise((resolve) => setTimeout(resolve, 0));
expect(screen.getByLabelText("Assembled classifier prompt")).toHaveTextContent(
"assembled from the edited draft",
);
});
it("keeps the editor usable when the preview cannot be fetched", async () => {
getAutoRouterCustomTierPromptCall.mockRejectedValue(new Error("boom"));
renderEditor();

View file

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { getAutoRouterCustomTierPromptCall } from "@/components/networking";
import { Button } from "@/components/ui/button";
@ -33,30 +33,27 @@ const CustomTierPromptEditor: React.FC<CustomTierPromptEditorProps> = ({
>({ status: "loading" });
const isOverridden = Boolean(classificationPrompt?.trim());
// Debounced so the preview follows the draft without a request per keystroke. Nothing is saved
// from here, so a failed fetch leaves the preview empty rather than blocking the edit.
const refreshPreview = useCallback(async () => {
if (!accessToken) return;
try {
const text = await getAutoRouterCustomTierPromptCall(
accessToken,
contextWindowSize,
tierDefinitionsFromRows(tierRows),
draft,
);
setPreview({ status: "ready", text });
} catch {
// Distinct from loading: a role that may not call the preview, or a prompt the write gate
// would reject, otherwise leaves the panel claiming it is still fetching, forever.
setPreview({ status: "error" });
}
}, [accessToken, contextWindowSize, tierRows, draft]);
useEffect(() => {
if (!isOpen) return;
const timer = setTimeout(refreshPreview, 300);
return () => clearTimeout(timer);
}, [isOpen, refreshPreview]);
if (!isOpen || !accessToken) return;
let stale = false;
const timer = setTimeout(async () => {
try {
const text = await getAutoRouterCustomTierPromptCall(
accessToken,
contextWindowSize,
tierDefinitionsFromRows(tierRows),
draft,
);
if (!stale) setPreview({ status: "ready", text });
} catch {
if (!stale) setPreview({ status: "error" });
}
}, 300);
return () => {
stale = true;
clearTimeout(timer);
};
}, [isOpen, accessToken, contextWindowSize, tierRows, draft]);
const openEditor = () => {
setDraft(classificationPrompt ?? "");