mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 19:06:01 +00:00
fix(frontend): preserve artifacts during streaming updates (#3791)
This commit is contained in:
parent
b5cac5e721
commit
7c8a17c650
@ -44,13 +44,20 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
|||||||
|
|
||||||
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
|
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const threadArtifacts = Array.isArray(thread.values.artifacts)
|
||||||
|
? thread.values.artifacts
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (threadIdRef.current !== threadId) {
|
if (threadIdRef.current !== threadId) {
|
||||||
threadIdRef.current = threadId;
|
threadIdRef.current = threadId;
|
||||||
deselect();
|
deselect();
|
||||||
|
setArtifacts([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update artifacts from the current thread
|
// Update artifacts from the current thread
|
||||||
setArtifacts(thread.values.artifacts);
|
if (threadArtifacts) {
|
||||||
|
setArtifacts(threadArtifacts);
|
||||||
|
}
|
||||||
|
|
||||||
// DO NOT automatically deselect the artifact when switching threads, because the artifacts auto discovering is not work now.
|
// DO NOT automatically deselect the artifact when switching threads, because the artifacts auto discovering is not work now.
|
||||||
// if (
|
// if (
|
||||||
@ -64,9 +71,9 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
|||||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
|
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
|
||||||
autoSelectFirstArtifact
|
autoSelectFirstArtifact
|
||||||
) {
|
) {
|
||||||
if (thread?.values?.artifacts?.length > 0) {
|
if (threadArtifacts && threadArtifacts.length > 0) {
|
||||||
setAutoSelectFirstArtifact(false);
|
setAutoSelectFirstArtifact(false);
|
||||||
selectArtifact(thread.values.artifacts[0]!);
|
selectArtifact(threadArtifacts[0]!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@ -149,7 +156,7 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
|||||||
<XIcon />
|
<XIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{thread.values.artifacts?.length === 0 ? (
|
{artifacts.length === 0 ? (
|
||||||
<ConversationEmptyState
|
<ConversationEmptyState
|
||||||
icon={<FilesIcon />}
|
icon={<FilesIcon />}
|
||||||
title="No artifact selected"
|
title="No artifact selected"
|
||||||
@ -163,7 +170,7 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
|||||||
<main className="min-h-0 grow">
|
<main className="min-h-0 grow">
|
||||||
<ArtifactFileList
|
<ArtifactFileList
|
||||||
className="max-w-(--container-width-sm) p-4 pt-12"
|
className="max-w-(--container-width-sm) p-4 pt-12"
|
||||||
files={thread.values.artifacts ?? []}
|
files={artifacts}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type { Todo } from "../todos";
|
|||||||
export interface AgentThreadState extends Record<string, unknown> {
|
export interface AgentThreadState extends Record<string, unknown> {
|
||||||
title: string;
|
title: string;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
artifacts: string[];
|
artifacts?: string[];
|
||||||
todos?: Todo[];
|
todos?: Todo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
96
frontend/tests/e2e/artifact-stream-state.spec.ts
Normal file
96
frontend/tests/e2e/artifact-stream-state.spec.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { expect, test, type Route } from "@playwright/test";
|
||||||
|
|
||||||
|
import { mockLangGraphAPI } from "./utils/mock-api";
|
||||||
|
|
||||||
|
const THREAD_ID = "00000000-0000-0000-0000-000000003788";
|
||||||
|
const RUN_ID = "00000000-0000-0000-0000-000000003789";
|
||||||
|
const ARTIFACT_PATH = "/artifact-fixtures/report.md";
|
||||||
|
const THREAD_MESSAGES = [
|
||||||
|
{
|
||||||
|
type: "human",
|
||||||
|
id: "msg-human-artifact",
|
||||||
|
content: [{ type: "text", text: "Create a markdown report" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "ai",
|
||||||
|
id: "msg-ai-artifact",
|
||||||
|
content: "Created a markdown report.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function streamWithoutArtifacts(route: Route) {
|
||||||
|
const events = [
|
||||||
|
{
|
||||||
|
event: "metadata",
|
||||||
|
data: { run_id: RUN_ID, thread_id: THREAD_ID },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: "values",
|
||||||
|
data: {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
type: "human",
|
||||||
|
id: "msg-human-artifact-follow-up",
|
||||||
|
content: [{ type: "text", text: "Continue" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "ai",
|
||||||
|
id: "msg-ai-artifact-follow-up",
|
||||||
|
content: "Updated response while the artifact list is omitted.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "text/event-stream",
|
||||||
|
body: events
|
||||||
|
.map((event) => {
|
||||||
|
return `event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`;
|
||||||
|
})
|
||||||
|
.join(""),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("keeps artifact trigger after stream values omit artifacts", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
mockLangGraphAPI(page, {
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
thread_id: THREAD_ID,
|
||||||
|
title: "Artifact stream state",
|
||||||
|
artifacts: [ARTIFACT_PATH],
|
||||||
|
messages: THREAD_MESSAGES,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/api/langgraph/threads/*/runs/stream", (route) => {
|
||||||
|
return streamWithoutArtifacts(route);
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/workspace/chats/${THREAD_ID}`);
|
||||||
|
|
||||||
|
const artifactTrigger = page.getByRole("button", { name: /artifacts/i });
|
||||||
|
await expect(artifactTrigger).toBeVisible({ timeout: 15_000 });
|
||||||
|
|
||||||
|
const textarea = page.getByPlaceholder(/how can i assist you/i);
|
||||||
|
await textarea.fill("Continue");
|
||||||
|
await textarea.press("Enter");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByText("Updated response while the artifact list is omitted."),
|
||||||
|
).toBeVisible({ timeout: 10_000 });
|
||||||
|
await expect(artifactTrigger).toBeVisible();
|
||||||
|
|
||||||
|
await artifactTrigger.click();
|
||||||
|
|
||||||
|
const artifactsPanel = page.locator("#artifacts");
|
||||||
|
await expect(artifactsPanel.getByText("report.md")).toBeVisible();
|
||||||
|
await artifactsPanel.getByText("report.md").click();
|
||||||
|
|
||||||
|
await expect(artifactsPanel.getByRole("combobox")).toContainText("report.md");
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user