fix(frontend): enable regenerate in custom agent chats (#3967)

This commit is contained in:
Huixin615 2026-07-07 07:10:48 +08:00 committed by GitHub
parent 47b0f604f4
commit 4915b5e4cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 127 additions and 1 deletions

View file

@ -91,6 +91,7 @@ export default function AgentChatPage() {
thread,
pendingUsageMessages,
sendMessage,
regenerateMessage,
isUploading,
isHistoryLoading,
hasMoreHistory,
@ -203,6 +204,11 @@ export default function AgentChatPage() {
const handleStop = useCallback(async () => {
await thread.stop();
}, [thread]);
const handleRegenerate = useCallback(
(messageId: string, supersededMessageIds: string[]) =>
regenerateMessage(threadId, messageId, supersededMessageIds),
[regenerateMessage, threadId],
);
const tokenUsageInlineMode = tokenUsageEnabled
? localSettings.tokenUsage.inlineMode
@ -293,6 +299,14 @@ export default function AgentChatPage() {
loadMoreHistory={loadMoreHistory}
isHistoryLoading={isHistoryLoading}
tokenUsageInlineMode={tokenUsageInlineMode}
canRegenerate={
!isNewThread &&
!isMock &&
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true" &&
!isUploading &&
!thread.isLoading
}
onRegenerateMessage={handleRegenerate}
onSubmitHumanInput={
isMock || env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"
? undefined

View file

@ -1,6 +1,10 @@
import { expect, test } from "@playwright/test";
import { mockLangGraphAPI } from "./utils/mock-api";
import {
handleRunStream,
mockLangGraphAPI,
MOCK_THREAD_ID,
} from "./utils/mock-api";
const MOCK_AGENTS = [
{
@ -43,4 +47,112 @@ test.describe("Agent chat", () => {
page.locator("header span", { hasText: "test-agent" }),
).toBeVisible({ timeout: 15_000 });
});
test("agent chat can regenerate its latest response", async ({ page }) => {
const humanMessage = {
type: "human",
id: "msg-human-agent",
content: [{ type: "text", text: "Original agent question" }],
};
const aiMessage = {
type: "ai",
id: "msg-ai-agent",
content: "Custom agent response",
};
mockLangGraphAPI(page, {
agents: MOCK_AGENTS,
threads: [
{
thread_id: MOCK_THREAD_ID,
title: "Agent conversation",
agent_name: "test-agent",
messages: [humanMessage, aiMessage],
},
],
});
let prepareMessageId: string | undefined;
let streamBody: Record<string, unknown> | undefined;
await page.route(
`**/api/threads/${MOCK_THREAD_ID}/runs/regenerate/prepare`,
(route) => {
prepareMessageId = (
route.request().postDataJSON() as { message_id?: string }
).message_id;
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
input: { messages: [humanMessage] },
checkpoint: {
checkpoint_id: "checkpoint-before-human",
checkpoint_ns: "",
checkpoint_map: null,
},
metadata: {
regenerate_from_message_id: aiMessage.id,
regenerate_from_run_id: `run-${MOCK_THREAD_ID}`,
regenerate_checkpoint_id: "checkpoint-before-human",
},
target_run_id: `run-${MOCK_THREAD_ID}`,
}),
});
},
);
await page.route(
`**/api/langgraph/threads/${MOCK_THREAD_ID}/runs/stream`,
(route) => {
streamBody = route.request().postDataJSON() as Record<string, unknown>;
return handleRunStream(route);
},
);
await page.goto(`/workspace/agents/test-agent/chats/${MOCK_THREAD_ID}`);
await expect(page.getByText(aiMessage.content)).toBeVisible({
timeout: 15_000,
});
await page.evaluate((selectedText) => {
const element = Array.from(document.querySelectorAll("p")).find(
(candidate) => candidate.textContent?.includes(selectedText),
);
const textNode = element?.firstChild;
if (!element || !textNode) {
throw new Error("Unable to find the custom agent response");
}
const range = document.createRange();
range.selectNodeContents(textNode);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
element.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
}, aiMessage.content);
await expect(
page.getByRole("button", { name: "Ask in side chat" }),
).toBeVisible();
await page.keyboard.press("Escape");
const assistantTurn = page.locator("[data-assistant-turn]").last();
await assistantTurn.hover();
await page.getByRole("button", { name: "Regenerate" }).click();
await expect.poll(() => prepareMessageId).toBe(aiMessage.id);
await expect.poll(() => streamBody).toBeDefined();
expect(streamBody).toMatchObject({
checkpoint: {
checkpoint_id: "checkpoint-before-human",
checkpoint_ns: "",
checkpoint_map: null,
},
metadata: {
regenerate_from_message_id: aiMessage.id,
regenerate_from_run_id: `run-${MOCK_THREAD_ID}`,
regenerate_checkpoint_id: "checkpoint-before-human",
},
context: {
agent_name: "test-agent",
thread_id: MOCK_THREAD_ID,
},
});
});
});