mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-24 00:45:57 +00:00
fix(frontend): wait for async chat submit before clearing (#2940)
* fix(frontend): wait for async chat submit before clearing * test(frontend): cover pending attachment uploads * fix(frontend): preserve sync submit semantics
This commit is contained in:
@@ -66,6 +66,7 @@ export default function AgentChatPage() {
|
|||||||
thread,
|
thread,
|
||||||
pendingUsageMessages,
|
pendingUsageMessages,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
isUploading,
|
||||||
isHistoryLoading,
|
isHistoryLoading,
|
||||||
hasMoreHistory,
|
hasMoreHistory,
|
||||||
loadMoreHistory,
|
loadMoreHistory,
|
||||||
@@ -106,7 +107,11 @@ export default function AgentChatPage() {
|
|||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(message: PromptInputMessage) => {
|
(message: PromptInputMessage) => {
|
||||||
void sendMessage(threadId, message, { agent_name });
|
const sendPromise = sendMessage(threadId, message, { agent_name });
|
||||||
|
if (message.files.length > 0) {
|
||||||
|
return sendPromise;
|
||||||
|
}
|
||||||
|
void sendPromise;
|
||||||
},
|
},
|
||||||
[sendMessage, threadId, agent_name],
|
[sendMessage, threadId, agent_name],
|
||||||
);
|
);
|
||||||
@@ -243,7 +248,10 @@ export default function AgentChatPage() {
|
|||||||
<AgentWelcome agent={agent} agentName={agent_name} />
|
<AgentWelcome agent={agent} agentName={agent_name} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
|
disabled={
|
||||||
|
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
|
||||||
|
isUploading
|
||||||
|
}
|
||||||
onContextChange={(context) => setSettings("context", context)}
|
onContextChange={(context) => setSettings("context", context)}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onStop={handleStop}
|
onStop={handleStop}
|
||||||
|
|||||||
@@ -109,7 +109,11 @@ export default function ChatPage() {
|
|||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(message: PromptInputMessage) => {
|
(message: PromptInputMessage) => {
|
||||||
void sendMessage(threadId, message);
|
const sendPromise = sendMessage(threadId, message);
|
||||||
|
if (message.files.length > 0) {
|
||||||
|
return sendPromise;
|
||||||
|
}
|
||||||
|
void sendPromise;
|
||||||
},
|
},
|
||||||
[sendMessage, threadId],
|
[sendMessage, threadId],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -499,6 +499,10 @@ export const PromptInput = ({
|
|||||||
// Keep a ref to files for cleanup on unmount (avoids stale closure)
|
// Keep a ref to files for cleanup on unmount (avoids stale closure)
|
||||||
const filesRef = useRef(files);
|
const filesRef = useRef(files);
|
||||||
filesRef.current = files;
|
filesRef.current = files;
|
||||||
|
const providerTextRef = useRef("");
|
||||||
|
if (usingProvider) {
|
||||||
|
providerTextRef.current = controller.textInput.value;
|
||||||
|
}
|
||||||
|
|
||||||
const openFileDialogLocal = useCallback(() => {
|
const openFileDialogLocal = useCallback(() => {
|
||||||
inputRef.current?.click();
|
inputRef.current?.click();
|
||||||
@@ -768,6 +772,24 @@ export const PromptInput = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert blob URLs to data URLs asynchronously
|
// Convert blob URLs to data URLs asynchronously
|
||||||
|
const submittedFileIds = files.map((file) => file.id);
|
||||||
|
const clearSubmittedState = () => {
|
||||||
|
const currentFileIds = new Set(filesRef.current.map((file) => file.id));
|
||||||
|
const submittedFileIdsStillPresent = submittedFileIds.filter((id) =>
|
||||||
|
currentFileIds.has(id),
|
||||||
|
);
|
||||||
|
if (submittedFileIdsStillPresent.length === filesRef.current.length) {
|
||||||
|
clear();
|
||||||
|
} else {
|
||||||
|
for (const id of submittedFileIdsStillPresent) {
|
||||||
|
remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (usingProvider && providerTextRef.current === text) {
|
||||||
|
controller.textInput.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
files.map(async ({ id, ...item }) => {
|
files.map(async ({ id, ...item }) => {
|
||||||
if (item.file instanceof File) {
|
if (item.file instanceof File) {
|
||||||
@@ -793,20 +815,14 @@ export const PromptInput = ({
|
|||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
result
|
result
|
||||||
.then(() => {
|
.then(() => {
|
||||||
clear();
|
clearSubmittedState();
|
||||||
if (usingProvider) {
|
|
||||||
controller.textInput.clear();
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Don't clear on error - user may want to retry
|
// Don't clear on error - user may want to retry
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Sync function completed without throwing, clear attachments
|
// Sync function completed without throwing, clear attachments
|
||||||
clear();
|
clearSubmittedState();
|
||||||
if (usingProvider) {
|
|
||||||
controller.textInput.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Don't clear on error - user may want to retry
|
// Don't clear on error - user may want to retry
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ export function InputBox({
|
|||||||
threadId,
|
threadId,
|
||||||
initialValue,
|
initialValue,
|
||||||
onContextChange,
|
onContextChange,
|
||||||
|
onFollowupsVisibilityChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onStop,
|
onStop,
|
||||||
...props
|
...props
|
||||||
@@ -142,7 +143,8 @@ export function InputBox({
|
|||||||
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
||||||
},
|
},
|
||||||
) => void;
|
) => void;
|
||||||
onSubmit?: (message: PromptInputMessage) => void;
|
onFollowupsVisibilityChange?: (visible: boolean) => void;
|
||||||
|
onSubmit?: (message: PromptInputMessage) => void | Promise<void>;
|
||||||
onStop?: () => void;
|
onStop?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -251,12 +253,12 @@ export function InputBox({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
async (message: PromptInputMessage) => {
|
(message: PromptInputMessage) => {
|
||||||
if (status === "streaming") {
|
if (status === "streaming") {
|
||||||
onStop?.();
|
onStop?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!message.text) {
|
if (!message.text.trim() && message.files.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setFollowups([]);
|
setFollowups([]);
|
||||||
@@ -274,11 +276,14 @@ export function InputBox({
|
|||||||
selectedModel?.supports_thinking ?? false,
|
selectedModel?.supports_thinking ?? false,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
setTimeout(() => onSubmit?.(message), 0);
|
return new Promise<void>((resolve, reject) => {
|
||||||
return;
|
setTimeout(() => {
|
||||||
|
Promise.resolve(onSubmit?.(message)).then(resolve).catch(reject);
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit?.(message);
|
return onSubmit?.(message);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
context,
|
context,
|
||||||
@@ -348,6 +353,14 @@ export function InputBox({
|
|||||||
!followupsHidden &&
|
!followupsHidden &&
|
||||||
(followupsLoading || followups.length > 0);
|
(followupsLoading || followups.length > 0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onFollowupsVisibilityChange?.(showFollowups);
|
||||||
|
}, [onFollowupsVisibilityChange, showFollowups]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => onFollowupsVisibilityChange?.(false);
|
||||||
|
}, [onFollowupsVisibilityChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesRef.current = thread.messages;
|
messagesRef.current = thread.messages;
|
||||||
}, [thread.messages]);
|
}, [thread.messages]);
|
||||||
|
|||||||
@@ -48,4 +48,66 @@ test.describe("Chat workspace", () => {
|
|||||||
timeout: 10_000,
|
timeout: 10_000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("keeps attachments visible while upload submit is pending", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
let releaseUpload!: () => void;
|
||||||
|
const uploadCanFinish = new Promise<void>((resolve) => {
|
||||||
|
releaseUpload = resolve;
|
||||||
|
});
|
||||||
|
let uploadStarted!: () => void;
|
||||||
|
const uploadStartedPromise = new Promise<void>((resolve) => {
|
||||||
|
uploadStarted = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/api/threads/*/uploads", async (route) => {
|
||||||
|
uploadStarted();
|
||||||
|
await uploadCanFinish;
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "Uploaded",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
filename: "report.docx",
|
||||||
|
size: 12,
|
||||||
|
path: "report.docx",
|
||||||
|
virtual_path: "/mnt/user-data/uploads/report.docx",
|
||||||
|
artifact_url: "/api/threads/test/uploads/report.docx",
|
||||||
|
extension: ".docx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/workspace/chats/new");
|
||||||
|
|
||||||
|
const textarea = page.getByPlaceholder(/how can i assist you/i);
|
||||||
|
await expect(textarea).toBeVisible({ timeout: 15_000 });
|
||||||
|
const promptForm = page.locator("form").filter({ has: textarea });
|
||||||
|
|
||||||
|
await page.getByLabel("Upload files").setInputFiles({
|
||||||
|
name: "report.docx",
|
||||||
|
mimeType:
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
buffer: Buffer.from("fake docx"),
|
||||||
|
});
|
||||||
|
await expect(promptForm.getByText("report.docx")).toBeVisible();
|
||||||
|
|
||||||
|
await textarea.fill("Summarize this document");
|
||||||
|
await textarea.press("Enter");
|
||||||
|
|
||||||
|
await uploadStartedPromise;
|
||||||
|
await expect(promptForm.getByText("report.docx")).toBeVisible();
|
||||||
|
|
||||||
|
releaseUpload();
|
||||||
|
await expect(page.getByText("Hello from DeerFlow!")).toBeVisible({
|
||||||
|
timeout: 10_000,
|
||||||
|
});
|
||||||
|
await expect(promptForm.getByText("report.docx")).toBeHidden();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user