fix(frontend): preserve artifacts during streaming updates (#3791)

This commit is contained in:
Huixin615 2026-06-27 23:06:26 +08:00 committed by GitHub
parent b5cac5e721
commit 7c8a17c650
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 109 additions and 6 deletions

View file

@ -44,13 +44,20 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
useEffect(() => {
const threadArtifacts = Array.isArray(thread.values.artifacts)
? thread.values.artifacts
: undefined;
if (threadIdRef.current !== threadId) {
threadIdRef.current = threadId;
deselect();
setArtifacts([]);
}
// 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.
// if (
@ -64,9 +71,9 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
autoSelectFirstArtifact
) {
if (thread?.values?.artifacts?.length > 0) {
if (threadArtifacts && threadArtifacts.length > 0) {
setAutoSelectFirstArtifact(false);
selectArtifact(thread.values.artifacts[0]!);
selectArtifact(threadArtifacts[0]!);
}
}
}, [
@ -149,7 +156,7 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
<XIcon />
</Button>
</div>
{thread.values.artifacts?.length === 0 ? (
{artifacts.length === 0 ? (
<ConversationEmptyState
icon={<FilesIcon />}
title="No artifact selected"
@ -163,7 +170,7 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
<main className="min-h-0 grow">
<ArtifactFileList
className="max-w-(--container-width-sm) p-4 pt-12"
files={thread.values.artifacts ?? []}
files={artifacts}
threadId={threadId}
/>
</main>

View file

@ -5,7 +5,7 @@ import type { Todo } from "../todos";
export interface AgentThreadState extends Record<string, unknown> {
title: string;
messages: Message[];
artifacts: string[];
artifacts?: string[];
todos?: Todo[];
}

View 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");
});