Show a tabular file as a picture, drawn the way the export writes it - #215
Merged
MikeAlhayek merged 2 commits intoSep 21, 2026
Merged
Conversation
A reader handed a spreadsheet had no way to see it without downloading it first. Each worksheet is now drawn as a figure in the answer: a header band, a lettered column strip, a numbered row gutter, and a caption saying plainly what was left out of the window. The picture is drawn from the same formatting the workbook is written with. Resolving that in two places is how a preview ends up showing a different header colour, or a raw 74612.15999999999 where the file shows $74,612.16, so the resolution both tools need now lives in TabularFormattingResolver and the export delegates to it. Column formats inherited from the uploaded file are applied to the drawing too, which is what makes a column that was currency in the upload read as currency in the preview. The value a workbook stores is a number and a format code, and the presenting is left to whatever opens it, so the codes are read here. Currency, accounting, plain number, percent and date are covered; a code outside that vocabulary falls back to the general presentation rather than being guessed at. That general presentation now rounds to the significant digits a spreadsheet shows instead of printing a double to full binary precision. Two faults found while building it: The figures were addressed before the rows that back them were committed, so every picture arrived at an address that did not resolve yet. The document is now committed before its address is handed out. The model would not write the markers that place the pictures, through three rounds of asking. The host now appends a marker for any servable image the answer did not name, which is the rule this codebase already applies to a generated file: it is surfaced even when the model does not cite it. A picture is bounded to a fraction of its drawn size so a stack of sheets does not push the conversation off screen, which leaves a spreadsheet grid at about half size. Clicking one enlarges it. The download sits over the picture's corner rather than on a line of its own, and saves a PNG converted from the picture the page has already loaded, so nothing is fetched twice and nothing is converted for a picture nobody saves.
…f its name The check reads back which element a figure is wrapped in, because a marker is often written mid-sentence and a <div> tears the paragraph apart. It located that element by the first mention of the class name in the file, which is the markup only for as long as no rule or selector is written above it. Adding a stylesheet for the figures moved the first mention into CSS and the check started reading a selector, so it failed on a file whose markup it had never looked at. It now searches for the opening tag itself, and checks every figure the file emits rather than only the one that happens to come first.
MikeAlhayek
approved these changes
Sep 21, 2026
This was referenced Sep 21, 2026
JackTelford
pushed a commit
to JackTelford/CrestApps.Core
that referenced
this pull request
Sep 22, 2026
The MVC chat sample moved its download onto the picture and gained click-to-enlarge in CrestApps#215, but it did both inline in its own view. The shared chat UI every other surface renders with -- the chat page, the widget and the chat-interaction page -- kept the old shape: a button on a row of its own beneath the picture, and a thumbnail that could not be opened. A figure is capped to a fraction of the viewport so a stack of them does not push the conversation off screen, which makes every one of them a thumbnail; in the widget, 380px wide, more so. So the two things a reader wants are to see it larger and to keep a copy, and the second was costing a band of vertical space under every figure to offer. The download is now drawn over the picture's lower corner and revealed by pointing at the figure, on a pale ground of its own because it sits on whatever the drawing puts in that corner. focus-within keeps it reachable by keyboard, and a touch screen -- which has no hover to reveal it with -- simply always shows it. The link is otherwise untouched: same address, same download attribute, so the existing data-URI interception still names the saved file the way it did. Enlarging is wired to medium-zoom on the same deferred pass as the broken-image handler, because a picture arrives after the markdown is handed back and a streamed answer rewrites it on every chunk. The library is the host's to load: without it the picture is still shown and still downloadable, and only the enlarging is missing, so a host that has not registered it loses nothing it had. The opened copy is matched on its own class, since the library appends it to the body rather than inside the figure -- the widget stylesheet repeats that one rule unscoped for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Previews a spreadsheet in the answer instead of making the reader download it first. Each worksheet is drawn as a figure — header band, lettered column strip, numbered row gutter, and a caption saying what was left out of the window.
The picture is drawn from the formatting the file is written with
Resolving formatting in two places is how a preview ends up showing a different header colour, or a raw
74612.15999999999where the file shows$74,612.16. The resolution both tools need now lives inTabularFormattingResolver, andExportTabularDataTooldelegates to it, so the two cannot drift. Column formats inherited from the uploaded workbook are applied to the drawing too, which is what makes a column that was currency in the upload read as currency in the preview.A workbook stores a number and a format code and leaves the presenting to whatever opens it, so the codes are read in
TabularPreviewValueFormat. Currency, accounting, plain number, percent and date are covered; anything outside that vocabulary falls back to the general presentation rather than being guessed at.The general presentation now rounds to the significant digits a spreadsheet shows, instead of printing a double to full binary precision —
74612.15999999999was wrong even with no format applied.Two faults found while building it
Read-your-own-write. Figures were addressed before the rows backing them were committed, so every picture arrived at an address that did not resolve yet. The document is now committed before its address is handed out.
The model would not write the markers that place the pictures, through three rounds of prompt hardening. The host now appends a marker for any servable image the answer did not name — the rule this codebase already applies to generated files, which are surfaced even when the model does not cite them.
Reading the picture
A figure is bounded to a fraction of its drawn size so a stack of sheets does not push the conversation off screen, which leaves a grid at roughly half size. Clicking one enlarges it (~45% to ~72%, undistorted).
The download sits over the picture's corner rather than on a line of its own — that band cost a row of vertical space per sheet. It saves a PNG, converted in the browser from the picture already loaded: no second fetch, and nothing converted for a picture nobody saves. Every failure path falls back to the SVG the server holds.
Verification
TabularPreviewValueFormatcovered by 15 cases against the format codes a real workbook carries:$74,612.16,$0.00,-$81,065.04,(1,234.50),12.3%,09-01-26— all pass..xlsxvalidated withOpenXmlValidator— 0 errors; bytes served match bytes on disk by SHA-256.The second commit fixes a check that located the figure wrapper by the first mention of its class name in the file. That is the markup only while no rule or selector is written above it, so adding a stylesheet for the figures made it read a CSS selector instead. It now searches for the opening tag and checks every figure a file emits.
Known gaps
ai-chat.jsandchat-interaction.jsrender the same figure markup for the other chat surfaces and still need the zoom and PNG-download treatment, which requires an asset rebuild.🤖 Generated with Claude Code