Skip to content

fix(web): render local-path media in both dashboard chains - #1562

Merged
Yunnglin merged 4 commits into
mainfrom
fix/markdown-media-src
Aug 10, 2026
Merged

Yunnglin merged 4 commits into
mainfrom
fix/markdown-media-src

Conversation

@Yunnglin

Copy link
Copy Markdown
Collaborator

What

Removes the legacy Gradio-era image convention from messages_to_markdown and makes the Web dashboard resolve server-side media paths in both rendering chains.

Why

messages_to_markdown emitted ![image](gradio_api/file=<abs path>), introduced by #1019 when the dashboard was Gradio-based. That convention is dead:

  • gradio appears in no requirements/*.txt and not in pyproject.toml
  • evalscope app is only a [DEPRECATED] Use "evalscope service" instead alias
  • nothing in Python code, evalscope/web/src or tests/ interprets gradio_api
  • the React dashboard serves server-side files via /api/v1/reports/media/file?path=

Consequence: every benchmark that puts a local path in ContentImage (miniwob, browsergym, general_vqa custom data, …) had broken images in the compare view, and Sample.pretty_print printed the unusable prefix as noise.

Dropping the prefix alone is not enough — MarkdownRenderer passed the markdown src straight to ImageLightbox, which renders <img src> verbatim. Only the structured Messages chain proxied media, via the module-private resolveMediaSrc in MediaBlocks.tsx.

Changes

  1. evalscope/api/messages/chat_message.py — emit ![image]({abs_path}) instead of the gradio_api/file= form.
  2. evalscope/web/src/utils/media.ts (new) — resolveMediaSrc extracted here and shared. It is not exported from MediaBlocks.tsx because MediaBlocks already imports MarkdownRenderer, so re-importing would create a cycle.
  3. MarkdownRenderer.tsx — the img renderer now resolves src through resolveMediaSrc, so the markdown chain proxies local paths exactly like the structured chain.
  4. evalscope/utils/data_utils.py — absolutise local media paths when serialising prediction messages (see below).

Extra fix: relative paths in the structured chain

While verifying, the single-report detail view turned out to be broken too — not by this change, but pre-existing on main. ContentImage values may be relative paths (the custom VQA data behind general_vqa uses custom_eval/multimodal/images/dog.jpg). A client cannot tell a relative path from a base64 payload, so resolveMediaSrc fell into the base64 branch and produced data:image/jpeg;base64,custom_eval/... → net::ERR_INVALID_URL.

Same prediction, before this PR:

Input    (markdown chain)   : ![image](gradio_api/file=/abs/.../dog.jpg)   → 404
Messages (structured chain) : 'custom_eval/multimodal/images/dog.jpg'      → ERR_INVALID_URL

Local media paths are now absolutised in _serialize_messages, mirroring what messages_to_markdown already did, so both chains expose media identically. Note this resolution is relative to the service process CWD — that constraint already applied to the markdown chain (messages_markdown is computed at read time), so it is not newly introduced.

Verification

Real run: general_vqa on the custom VQA sample data, DashScope qwen-vl-plus + qwen-vl-max, then a live dashboard.

API payload after the fix — both fields now carry an absolute path:

markdown Input img : /abs/.../custom_eval/multimodal/images/dog.jpg
structured image   : /abs/.../custom_eval/multimodal/images/dog.jpg

In the browser:

  • Compare view (markdown chain, the case this issue is about): image renders, src = /api/v1/reports/media/file?path=%2F...%2Fdog.jpg, 200 OK
  • Single-report chat view (structured chain, regression guard + relative-path fix): image renders, same proxy URL, no console errors

Test gate:

  • tests/api/test_messages_to_markdown.py (new, 4 cases) — no gradio_api, absolute path emitted, relative path absolutised, data URI passthrough, base64 max_length truncation
  • tests/report/test_prediction_media_paths.py (new, 3 cases) — relative path absolutised, absolute path preserved, data URI / base64 untouched
  • MarkdownRenderer.test.tsx (+2 cases) — local absolute path proxied; http and data: sources untouched
  • ChatView.test.tsx unchanged and still green (structured-chain guard)
  • make lint clean; pytest tests/cli/test_all.py::TestRun::test_ci_lite passes; npm run test 243/243; npm run lint and npm run build (tsc -b + vite) clean

Note for reviewers

evalscope/web/dist is not tracked, so the frontend part needs make web-build to take effect in a running dashboard.

Drop the legacy Gradio convention from `messages_to_markdown` and make the
Web dashboard resolve local media paths in Markdown as well.

`messages_to_markdown` emitted `![image](gradio_api/file=<abs path>)`, a
convention from the Gradio-era dashboard (#1019). `gradio` is no longer a
dependency, `evalscope app` is only a deprecated alias of `evalscope service`,
and nothing in the codebase interprets the prefix, so local-path images were
unusable in the compare view and printed as noise by `Sample.pretty_print`.

Dropping the prefix alone is not enough: `MarkdownRenderer` passed the image
`src` straight to `ImageLightbox`, while only the structured `Messages` chain
proxied media through `/api/v1/reports/media/file?path=`. `resolveMediaSrc` is
therefore extracted into `utils/media.ts` and shared by both chains.

The structured chain had a related gap: `ContentImage` values may be relative
paths (e.g. the custom VQA data used by `general_vqa`), which the client cannot
tell apart from a base64 payload, producing an invalid `data:` URI. Local media
paths are now absolutised when serialising prediction messages, matching what
`messages_to_markdown` already does, so both chains expose media identically.

Verified against a real `general_vqa` run (DashScope qwen-vl-plus / qwen-vl-max
on the custom VQA sample data): images now render in both the compare view and
the single-report chat view, requesting the media proxy endpoint.
Copilot AI lite review requested due to automatic review settings August 10, 2026 08:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes the legacy gradio_api/file= Markdown convention and makes the Web dashboard consistently proxy server-side media paths for both rendering chains (structured Messages blocks and Markdown-rendered content), including fixing relative-path handling by absolutizing media paths during message serialization.

Changes:

  • Update messages_to_markdown to emit local images as plain absolute paths (no gradio_api prefix).
  • Share a single resolveMediaSrc helper across structured and Markdown rendering so local paths are proxied uniformly.
  • Absolutize relative media paths when serializing prediction messages, with regression tests across Python and React.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
evalscope/api/messages/chat_message.py Drops gradio_api/file= and emits absolute local image paths in Markdown.
evalscope/utils/data_utils.py Absolutizes local media paths inside serialized structured ContentBlocks for frontend compatibility.
evalscope/web/src/utils/media.ts New shared resolveMediaSrc utility to proxy absolute paths and preserve http(s)/data URIs.
evalscope/web/src/components/single/chat/MediaBlocks.tsx Uses shared resolveMediaSrc for structured media blocks.
evalscope/web/src/components/common/MarkdownRenderer.tsx Proxies Markdown <img src> through resolveMediaSrc to match structured chain behavior.
evalscope/web/src/components/common/MarkdownRenderer.test.tsx Adds coverage for Markdown image source proxying and passthrough behavior.
tests/api/test_messages_to_markdown.py Adds regression tests for Markdown emission (no gradio_api, absolutization, passthrough, truncation).
tests/report/test_prediction_media_paths.py Adds tests ensuring structured-message media paths are absolutized/preserved appropriately.
Suppressed comments (1)

tests/api/test_messages_to_markdown.py:28

  • Test expectation should match the safer local-path Markdown emission (wrapping the destination in <...>), otherwise it will fail once local paths are escaped for Markdown parsing.
    assert f'![image]({image_path.resolve()})' in markdown

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread evalscope/api/messages/chat_message.py Outdated
Comment thread tests/api/test_messages_to_markdown.py Outdated
Local paths were emitted as bare markdown destinations, which breaks in two
ways found while addressing review feedback:

- A destination containing a space or an unbalanced parenthesis does not parse
  as an image at all, so the whole `![image](...)` degrades to literal text.
- A markdown parser percent-encodes the destination, so a path with non-ASCII
  characters (common in the OCR datasets) or spaces reached the frontend as
  `/tmp/%E5%9B%BE.jpg`. `encodeURIComponent` then escaped the `%` again and the
  media proxy received a literal `%E5%9B%BE` path and answered 404.

The destination is now wrapped in `<...>`, and the markdown image renderer
decodes the src before resolving it via `resolveMarkdownMediaSrc`. Remote URLs
and data: URIs are still passed through untouched so an intentionally escaped
remote URL is never rewritten.

Also simplify `_MEDIA_BLOCK_TYPES` (the previous mapping was an identity dict)
and tighten a test helper's return type.

Verified end to end on a path containing both CJK characters and spaces
(`.../图片 目录/狗 图片.jpg`): the compare view and the single-report chat view
both render the image, the src carries single-encoded UTF-8 with no `%25`, and
the media endpoint answers 200/304.
Follow-up from review: the resolver classified sources with a case-sensitive
prefix check, so `HTTPS://host/a.png` and `blob:` URIs fell through to the
base64 branch, and `//cdn/a.png` was proxied as if it were a local path. In the
markdown chain these all rendered fine before this PR, so it was a regression.

Scheme matching is now case-insensitive and covers `blob:`. Protocol-relative
URLs are only treated as remote in the markdown resolver: markdown is authored
text, whereas a structured `ContentBlock` carries a server-side media path where
a `//server/share/...` value should still go through the proxy.

Adds `utils/media.test.ts` covering both resolvers, including the
`decodeURI` fallback for a malformed escape sequence.
A blob URL is created client-side by URL.createObjectURL and is scoped to a
browser session, so it can never appear in a server-side prediction record.
Matching it only served a hypothetical case.
@Yunnglin
Yunnglin merged commit 970e951 into main Aug 10, 2026
4 checks passed
@Yunnglin
Yunnglin deleted the fix/markdown-media-src branch September 10, 2026 07:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants