From 90b1925c189b6f6f74f3c9fcae023f500f3814d7 Mon Sep 17 00:00:00 2001 From: hrz Date: Wed, 8 Jul 2026 18:15:17 +0800 Subject: [PATCH] fix(web-ui): resolve relative markdown images in chat --- .../components/Markdown/Markdown.test.tsx | 25 ++++++++++++++++++- .../components/Markdown/Markdown.tsx | 6 ++--- .../flow_chat/components/FlowTextBlock.tsx | 4 +++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/web-ui/src/component-library/components/Markdown/Markdown.test.tsx b/src/web-ui/src/component-library/components/Markdown/Markdown.test.tsx index 1adb298a3f..52c58abfce 100644 --- a/src/web-ui/src/component-library/components/Markdown/Markdown.test.tsx +++ b/src/web-ui/src/component-library/components/Markdown/Markdown.test.tsx @@ -9,6 +9,7 @@ import { Markdown } from './Markdown'; const mocks = vi.hoisted(() => ({ getCurrentWorkspacePath: vi.fn(), revealInExplorer: vi.fn(), + readFileContent: vi.fn(), openExternal: vi.fn(), renderMath: vi.fn(), })); @@ -19,7 +20,7 @@ vi.mock('../../../infrastructure/api', () => ({ }, workspaceAPI: { revealInExplorer: (...args: unknown[]) => mocks.revealInExplorer(...args), - readFileContent: vi.fn(), + readFileContent: (...args: unknown[]) => mocks.readFileContent(...args), }, systemAPI: { openExternal: (...args: unknown[]) => mocks.openExternal(...args), @@ -98,9 +99,11 @@ describe('Markdown file links', () => { onFileViewRequest = vi.fn(); mocks.getCurrentWorkspacePath.mockReset(); mocks.revealInExplorer.mockReset(); + mocks.readFileContent.mockReset(); mocks.openExternal.mockReset(); mocks.renderMath.mockReset(); mocks.getCurrentWorkspacePath.mockResolvedValue(EXAMPLE_WORKSPACE); + mocks.readFileContent.mockResolvedValue('cmVsdS1wbmc='); }); afterEach(() => { @@ -228,4 +231,24 @@ describe('Markdown file links', () => { expect(container.querySelector('[data-testid="markdown-math-renderer"]')).not.toBeNull(); expect(mocks.renderMath).toHaveBeenCalledWith('Formula: $x + y$'); }); + + it('loads relative markdown images from the provided base path', async () => { + await act(async () => { + root.render( + , + ); + await Promise.resolve(); + await Promise.resolve(); + }); + + const image = container.querySelector('img[alt="ReLU 图像"]'); + expect(image).not.toBeNull(); + expect(mocks.readFileContent).toHaveBeenCalledWith(`${EXAMPLE_WORKSPACE}/relu.png`); + expect(image?.src).toBe('data:image/png;base64,cmVsdS1wbmc='); + expect(mocks.getCurrentWorkspacePath).not.toHaveBeenCalled(); + }); }); diff --git a/src/web-ui/src/component-library/components/Markdown/Markdown.tsx b/src/web-ui/src/component-library/components/Markdown/Markdown.tsx index 03ab510cfd..8533ba5602 100644 --- a/src/web-ui/src/component-library/components/Markdown/Markdown.tsx +++ b/src/web-ui/src/component-library/components/Markdown/Markdown.tsx @@ -811,7 +811,7 @@ export const Markdown = React.memo(({ ); useEffect(() => { - if (!needsWorkspacePathForLinks || currentWorkspacePath) { + if (!needsWorkspacePathForLinks || currentWorkspacePath || basePath) { return; } @@ -830,7 +830,7 @@ export const Markdown = React.memo(({ return () => { cancelled = true; }; - }, [currentWorkspacePath, needsWorkspacePathForLinks]); + }, [basePath, currentWorkspacePath, needsWorkspacePathForLinks]); const markdownFeatureProfile = useMemo(() => ({ contentLength: markdownContent.length, @@ -1319,7 +1319,7 @@ export const Markdown = React.memo(({ }, img({ node: _node, ...props }: any) { - return ; + return ; }, blockquote({ children }: any) { diff --git a/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx b/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx index e90318c0d8..0e1ce7f758 100644 --- a/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx +++ b/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx @@ -73,7 +73,10 @@ export const FlowTextBlock = React.memo(({ onTabOpen, onHttpLinkClick, onOpenVisualization, + activeSessionOverride, } = useFlowChatContext(); + const markdownBasePath = activeSessionOverride?.workspacePath + || activeSessionOverride?.config?.workspacePath; // Normalize content to a string. const content = typeof textItem.content === 'string' @@ -147,6 +150,7 @@ export const FlowTextBlock = React.memo(({ {textItem.isMarkdown ? (