Repository navigation
feat(media): add generated images, sources and MediaViewer - #125
Conversation
GeneratedImages holds each tile at its final size under the dither grain while it generates, with the elapsed time beside the label. An image made while watched is revealed by the grain; one already there loads plainly; a failed one shows a message and a retry. The grain reads its colours from existing tokens and draws one still frame under reduced motion. Sources lists citations, and MediaViewer shows images on the native dialog with arrow keys between items and focus returned to the opener. Media addresses from agent data are checked before use.
dc9a67a to
fb3faf7
Compare
TheGreatAxios
left a comment
There was a problem hiding this comment.
Automated multi-agent review: independent review + second-agent validation; only confirmed items listed (the family itself is complete per task 11 and well covered).
- should-fix
src/lib/media-src.ts:21+src/ui/media-viewer.tsx:125—data:documents are filtered out before the viewer ever sees them, so they silently render as broken tiles with actions attached ("Open" does nothing), and there's no unloadable-tile state to communicate the failure. Either surface them or render an explicit unloadable state. - minor
src/ui/media-viewer.tsx:258-336—loaded/failedflags never reset whensrcchanges on the same open element (swap a URL in place and the old state can stick). - minor
src/ui/sources.tsx:70— the Link icon renders on citations that aren't links (candidates verified); gate by type. - test gap — the 7 e2e and 4 unit tests only cover generated images: the non-image viewer branches, sources rendering, and unloadable tiles have zero coverage.
- nits confirmed by git-grep —
list-style: noneon an<ol>in the sources list, prompt-less tile labels,src: ""sentinel usage, a duplicatedBAYER_4/recipe block, a re-implementedtoSafeHref, magic spacing multipliers, and a few speculative public exports with zero consumers at the series tip.
An image tile that could not be shown still offered Download, Copy and Open, handing the host an index to an image it cannot load and opening an empty viewer. Broken tiles now show only host-supplied action children, and viewer items for such images carry no src instead of an empty-string sentinel, so the viewer says there is no preview and offers no download. A tile whose address changed in place kept its failed or loaded flag, so the new address was never tried and skipped its reveal. The flags now reset when the source changes. Citations shown as plain text no longer carry the link icon, the sources list keeps list semantics without bullets, and an image without alt text is named after the prompt's opening words. Adds viewer tests for video, audio, PDF, text and unloadable items, and the first tests for Sources.
fb3faf7 to
9660d62
Compare
|
Thanks for the review. Here's what happened to each point. Fixed1. Images that can't load still offered actions that do nothing (should-fix)
2. Changing
3. The link icon showed on citations that aren't links (minor)
4. Only generated images were tested (test gap)
5. Nits
Not changed, and why
Commit: |
9660d62 to
40def38
Compare
40def38 to
bc0fa3e
Compare
bc0fa3e to
d86517e
Compare
TheGreatAxios
left a comment
There was a problem hiding this comment.
Review · Request changes
Adds generated-image tiles, source citations, and a media viewer dialog.
Findings
-
src/ui/media-viewer.tsx:89— Closing the dialog returns focus to the first opener, not the latest. The layout effect snapshotsopenerRef.currentonce ([openerRef]does not change) and the cleanup focuses that snapshot. A laterviewer.openupdates the ref (use-media-viewer.ts:66) while the dialog stays mounted, soopen(0, buttonA)thenopen(1, buttonB)then Close focusesbuttonA. ReadopenerRef.currentin the cleanup, or re-run the effect when the opener changes. -
src/ui/generated-images.tsx:423— A watched tile that has already revealed keeps the previous image fully visible when its address changes. The src-swap reset clearsloadedandfailedonly. It does not resetdevelopingor remount the<img>. After the grain finishes,developingis false, so the overlay at:493does not return, and thedevelopbranch forcesopacity-100(:453-454). The new request starts on the same node. Resetdeveloping, or key the image onsrc, so the old bitmap is not shown at full opacity whiledata-stateisloading. -
src/ui/media-viewer.tsx:334— The PDF<iframe>is in the tab order, and Escape is handled only on the dialog (onCancelat:142,onKeyDownat:151). A key delivered to the frame does not reach those handlers, so keyboard focus inside the PDF viewer cannot close the dialog.tabIndex={-1}keeps the frame out of the tab order; the Close button stays the keyboard exit.
d86517e to
c9738df
Compare
MediaViewer took the opener when it mounted, so if a host opened another item while the viewer was up, closing it focused the first opener. The cleanup now reads the latest opener and falls back to whatever had focus on open. A watched image that had already revealed kept the browser's old picture at full opacity after its source changed, under a loading state, until the new image arrived. The src swap now puts a watched tile back under the grain, which reveals the new image once loaded. Also document that Escape inside a PDF reaches the PDF viewer rather than the dialog, and that Tab leaves the PDF for Close.
c9738df to
1433e3d
Compare
|
Thanks for the review. Two points are fixed. The PDF frame is documented instead, for the reason below. 1. Closing the viewer focused the first opener, not the latest
2. A watched tile that had revealed showed the old picture at full opacity when its
3. Escape inside the PDF frame doesn't close the viewer
Commit: |
Without onOpen, a tile's picture took its alt from the image alone, so an image with no alt was marked decorative and screen readers skipped it, though the tile already had a name from the prompt's opening words. A tile that failed to load likewise read "Couldn't load this image". Both now use the tile's name, so a plain grid names every picture and every failure.
The opener named in viewer.open stayed in the hook after the viewer closed, and MediaViewer prefers it over what had focus on open. Once any tile had opened the viewer, a later opening through the controlled index sent focus back to that old tile instead of to the control that opened it. MediaViewer now clears the opener when it returns focus there, so an opening without open falls back to what had focus at the time.
MediaViewer read ref from its props, which only React 19 passes to a function component; under React 18, inside the peer range, a host's ref never reached the dialog. MediaViewer is now a forwardRef component that hands the ref to the <dialog>, and its props type leaves ref to forwardRef.
1433e3d to
bf91f4c
Compare
Round 3 fixesA fresh review pass over the stack turned up the points below; each is fixed in a new commit at the end of this branch, and the stack is rebased onto it.
|
bf91f4c to
a88212f
Compare
|
Triage: Fail Waiting on:
Stacked on #124: only this pull request's own changes were checked. Notes:
|
Superseded by the completed stacked review; blocking fixes were addressed and the rebased stack is being landed.
GeneratedImages holds each tile at its final size under the dither grain while it generates, with the elapsed time beside the label. An image made while watched is revealed by the grain; one already there loads plainly; a failed one shows a message and a retry. The grain reads its colours from existing tokens and draws one still frame under reduced motion. Sources lists citations, and MediaViewer shows images on the native dialog with arrow keys between items and focus returned to the opener. Media addresses from agent data are checked before use.
Stacked on #124: review only this commit. Part of #94.
Notes for review
viewer; focus returns to the element passed to viewer.open(index, opener). Backdrop is bg-background/90 (no literal scrim colour).Verification
bun run typecheckandbun run linton its own (the stack is checked commit by commit).bun run check,bun run build,bun run dep-guard,bun run contrast-testandbun run test:e2e(Playwright specs and the Storybook axe sweep in light and dark) pass.Closes #105