mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 19:06:01 +00:00
fix(frontend): show filename for presented artifacts missing from thread.values.artifacts (#3198)
Closes #3192 Root cause ---------- The artifact preview header is driven by a Radix Select whose <SelectValue> renders the label of the <SelectItem> matching the current value. The option list was built solely from `artifacts` in ArtifactsContext, which is only synced from `thread.values.artifacts` (chat-box.tsx). Artifacts surfaced via the message-layer `present_files` tool call are never written back to `thread.values.artifacts`, so when such a file is selected its filepath has no matching <SelectItem>. Radix then renders an empty trigger and the header filename appears blank, even though the preview body loads correctly. Fix --- Compute `artifactOptions` as a defensive union: if the currently selected filepath is missing from `artifacts`, prepend it so a matching <SelectItem> always exists. This keeps the header label in sync with the active file without changing context semantics or coupling the UI to a future auto-discovery mechanism (see existing TODO in chat-box.tsx). Tests ----- Add an e2e case that mocks a thread whose only artifact is delivered via `present_files` (thread.values.artifacts = []) and asserts both the header title and preview content render. All 21 e2e tests pass.
This commit is contained in:
parent
7c8a17c650
commit
3e2f1bbe49
@ -84,6 +84,13 @@ export function ArtifactFileDetail({
|
|||||||
}
|
}
|
||||||
return filepathFromProps;
|
return filepathFromProps;
|
||||||
}, [filepathFromProps, isWriteFile]);
|
}, [filepathFromProps, isWriteFile]);
|
||||||
|
const artifactOptions = useMemo(() => {
|
||||||
|
const list = artifacts ?? [];
|
||||||
|
if (list.includes(filepath)) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
return [filepath, ...list];
|
||||||
|
}, [artifacts, filepath]);
|
||||||
const isSkillFile = useMemo(() => {
|
const isSkillFile = useMemo(() => {
|
||||||
return filepath.endsWith(".skill");
|
return filepath.endsWith(".skill");
|
||||||
}, [filepath]);
|
}, [filepath]);
|
||||||
@ -178,9 +185,9 @@ export function ArtifactFileDetail({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="select-none">
|
<SelectContent className="select-none">
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{(artifacts ?? []).map((filepath) => (
|
{artifactOptions.map((option) => (
|
||||||
<SelectItem key={filepath} value={filepath}>
|
<SelectItem key={option} value={option}>
|
||||||
{getFileName(filepath)}
|
{getFileName(option)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
|
|||||||
@ -5,11 +5,13 @@ import { mockLangGraphAPI } from "./utils/mock-api";
|
|||||||
const ARTIFACT_PATH = "/artifact-fixtures/report.html";
|
const ARTIFACT_PATH = "/artifact-fixtures/report.html";
|
||||||
const MARKDOWN_ARTIFACT_PATH = "/artifact-fixtures/report.md";
|
const MARKDOWN_ARTIFACT_PATH = "/artifact-fixtures/report.md";
|
||||||
const JSON_ARTIFACT_PATH = "/artifact-fixtures/report.json";
|
const JSON_ARTIFACT_PATH = "/artifact-fixtures/report.json";
|
||||||
|
const PRESENTED_ARTIFACT_PATH = "/mnt/user-data/outputs/presented-report.md";
|
||||||
const IN_PROGRESS_THREAD_ID = "00000000-0000-0000-0000-000000003119";
|
const IN_PROGRESS_THREAD_ID = "00000000-0000-0000-0000-000000003119";
|
||||||
const COMPLETE_THREAD_ID = "00000000-0000-0000-0000-000000003120";
|
const COMPLETE_THREAD_ID = "00000000-0000-0000-0000-000000003120";
|
||||||
const MARKDOWN_THREAD_ID = "00000000-0000-0000-0000-000000003121";
|
const MARKDOWN_THREAD_ID = "00000000-0000-0000-0000-000000003121";
|
||||||
const MARKDOWN_ANCHOR_THREAD_ID = "00000000-0000-0000-0000-000000003123";
|
const MARKDOWN_ANCHOR_THREAD_ID = "00000000-0000-0000-0000-000000003123";
|
||||||
const JSON_THREAD_ID = "00000000-0000-0000-0000-000000003122";
|
const JSON_THREAD_ID = "00000000-0000-0000-0000-000000003122";
|
||||||
|
const PRESENTED_THREAD_ID = "00000000-0000-0000-0000-000000003123";
|
||||||
|
|
||||||
function writeFileMessages({
|
function writeFileMessages({
|
||||||
path = ARTIFACT_PATH,
|
path = ARTIFACT_PATH,
|
||||||
@ -57,6 +59,30 @@ function writeFileMessages({
|
|||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function presentFilesMessages() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: "human",
|
||||||
|
id: "msg-human-present-file",
|
||||||
|
content: [{ type: "text", text: "Create a markdown report" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "ai",
|
||||||
|
id: "msg-ai-present-file",
|
||||||
|
content: "The report has been written. Now let me present the file.",
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
id: "present-file-artifact",
|
||||||
|
name: "present_files",
|
||||||
|
args: {
|
||||||
|
filepaths: [PRESENTED_ARTIFACT_PATH],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
test.describe("Artifact preview stability", () => {
|
test.describe("Artifact preview stability", () => {
|
||||||
test("renders preview iframe for an in-progress write artifact", async ({
|
test("renders preview iframe for an in-progress write artifact", async ({
|
||||||
page,
|
page,
|
||||||
@ -235,4 +261,45 @@ test.describe("Artifact preview stability", () => {
|
|||||||
artifactsPanel.getByText('"中文字段": "测试内容"'),
|
artifactsPanel.getByText('"中文字段": "测试内容"'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("shows the title for a presented artifact missing from thread artifacts", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
mockLangGraphAPI(page, {
|
||||||
|
threads: [
|
||||||
|
{
|
||||||
|
thread_id: PRESENTED_THREAD_ID,
|
||||||
|
title: "Presented artifact title fallback",
|
||||||
|
messages: presentFilesMessages(),
|
||||||
|
artifacts: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await page.route(
|
||||||
|
`**/api/threads/${PRESENTED_THREAD_ID}/artifacts/mnt/user-data/outputs/presented-report.md`,
|
||||||
|
(route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "text/markdown",
|
||||||
|
body: "# Presented Report\n\nGenerated content",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto(`/workspace/chats/${PRESENTED_THREAD_ID}`);
|
||||||
|
|
||||||
|
// The file card in the message list shows the basename only.
|
||||||
|
await expect(page.getByText("presented-report.md")).toBeVisible({
|
||||||
|
timeout: 15_000,
|
||||||
|
});
|
||||||
|
await page.getByText("presented-report.md").first().click();
|
||||||
|
|
||||||
|
const artifactsPanel = page.locator("#artifacts");
|
||||||
|
|
||||||
|
// 1. Header title should fall back to the current filepath even though
|
||||||
|
// thread.values.artifacts is empty.
|
||||||
|
await expect(artifactsPanel.getByText("presented-report.md")).toBeVisible();
|
||||||
|
|
||||||
|
// 2. Preview content should still render correctly.
|
||||||
|
await expect(artifactsPanel.getByText("Presented Report")).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user