Kanzo UI

Field notes

Photographs of handwritten sighting slips in, a spreadsheet out — with the columns declared once as a SHACL shape and read four ways, and a review step that puts the paper next to the number.

Open full size

A photograph of five of the Amber Hall's sighting slips becomes five rows of a ledger, and the ledger becomes the CSV the archivist already keeps. ShellHeader is one row — the same height metadata-form's is — carrying the title, the source switch (Demo run · Live model), the two tallies and Extract with the CSV download; ShellMain holds the editable table; a trailing ShellAside stands the slip the selected row was read from BESIDE the record it became, with The shape on the second tab, and the shape switcher sits in that panel's own header.

The tallies are the way in rather than a summary: hovering unread or violations lists every cell it counted with the reason beside it, and pressing one marks those cells where they are. It is metadata-form's validation badge, doing the same job on a table — and it is the ONLY place a finding is drawn unasked. A cell the reader has typed in shows its own state, because there the mark is feedback on what they just typed; everywhere else the badge is how you ask.

The slip stands beside the record rather than above it, which is the panel's own shape being used rather than fought: a slip is a fifth as wide as it is tall and the panel is twice as tall as it is wide, so stacked the paper took a band across the top, left its own column empty and pushed the values it is there to be checked against out of sight. Side by side, a value sits at the height of the line it came from.

What fits in a 414 px column is still a reference, not a document. Pressing it opens the sheet in a viewer, at a size where the ink is legible and where redrawing the box has room to happen — and the viewer keeps one frame across both, because a dialog that sizes to its content moves the box out from under the pointer that just asked to drag it.

It exists to answer a question the other showcases do not reach: what does a screen look like when the machine is usually right? Press Extract and the rows fill in live. Six of the thirty-five cells arrive empty, because six of them are not in the photograph — and that is the screen, not a defect in the demo.

One declaration, four readings

The seven columns are written once, as a SHACL node shape, and nothing below restates them:

sh:property [
  sh:path gs:bounty ;
  sh:name "Bounty" ;
  sh:description "The gold actually PAID, printed at the foot beside «Bounty». Not the standing rate the hall's board prints for that beast." ;
  sh:order 6 ;
  sh:datatype xsd:decimal ;
  sh:minCount 1 ;
  sh:message "The gold paid on this slip." ;
] ;

None of that vocabulary is invented for this showcase. sh:name, sh:description and sh:order are SHACL's non-validating property shape characteristics — in the Recommendation precisely so a tool can present a shape rather than only validate against it. The one term this shape declines is SHACL-UI's shui:editor: the control it would name is already implied by sh:datatype and sh:in.

rudof — a Rust SHACL/ShEx stack compiled to WebAssembly — parses that document and hands back its own IR, so the column list is the shape. The same document then decides:

  1. the tablesh:name is the header, sh:order the column order, sh:datatype whether the cell is an Input or a NativeSelect, and sh:in the select's options;
  2. what the model is asked for — the schema the live adapter sends is generated from the projected columns, and key is an enum of those columns, so every cell comes back as an explicit {key, value} pair. Nothing is positional: the model never returns "the third value", and a key outside the enum is rejected by structured outputs. What tells it which is which is sh:description — the hall prints a standing rate and a bounty paid side by side, and only that sentence says which one bounty means;
  3. the findingsvalidate() is the real SHACL validator running in wasm, not a lookalike;
  4. the filetoCsv reads sh:name for the header and sh:datatype to know a date from a decimal.

Add a column to the shape and it appears in all four without a line changing anywhere else.

The converse is the part worth saying out loud: the shape is a projection. The slip also carries the hall, the region, the seal, the warrant number and the hour the hunter came in — the shape names seven properties, so everything else is dropped and does not exist downstream. That is what the archivist wants, and it is also why adding a column later means re-reading the photographs.

Why that is a decision rather than a convenience: a column written twice is a column that will disagree with itself, and here the disagreement leaves the building as a spreadsheet.

@kanzo-tech/rudof-wasm is the whole engine — loadShapes hands back the shape's own IR, so the column list is the shape rather than a transcription of it, and validate is the real SHACL validator rather than a lookalike written in the showcase. The wasm is 2.7 MB unpacked and loads lazily on mount; the docs build is green with it, which was the question that decided the design and was checked before any of the screen was written.

The case that does not fit. rudof returns its own message and the shape's sh:message in one array with nothing marking which is which, and the order is not stable between runs. spoken() in rudof.ts picks the one that does not say "not satisfied" — the single place this design guesses, and it is a guess about the engine's wording, not about the shape.

What this does not touch: @kanzo-tech/ui still knows nothing about RDF. The admission rule at the top of its barrel bars it by name, and every line above lives in docs/showcases/. A showcase is where specificity is allowed to live, and none of this is a candidate for the library until a second call site asks for it.

What would reverse it: a consumer that needs something the shape cannot say, or a wasm payload the docs build will not carry.

Held by docs/showcases/field-notes/shape.ts, SLIP_SHAPE; docs/showcases/field-notes/csv.test.ts, "writes the header from sh:name, in sh:order".

Why a model and not an OCR

Three of the seven columns are not printed on the slip at all. Beast, Observer and Verdict are written in ink in the field — a form OCR reads the printed layer and returns nothing for them. Every low confidence in the demo run is on one of those three, or on the one slip the photograph cuts in half.

That is also why the row thumbnail shows the head of each slip rather than the whole of it: the printed number is what tells two slips apart at thumbnail size.

The review step is the screen

Extraction gets most cells right and some cells wrong, and two independent mechanisms say so:

  • the model's own note — why a cell is empty or unsure, written for whoever has to check it against the paper (Under the mug., The slip runs off the bottom of the frame.);
  • the shapesh:minCount turns a cell nobody could read into a violation, sh:pattern catches a mistyped slip number, sh:in catches a beast the bestiary does not know and a verdict outside the three.

They agree without knowing about each other, and the aside shows both beside the crop of the slip. An unsure value carries a dotted amber underline; an invalid one gets Input's own aria-invalid ring.

What it composes

Existing parts, arranged — nothing here is new:

  • ShellRoot / ShellHeader / ShellBody / ShellMain / ShellAside — the frame. Exactly one <main>; the slip panel is an <aside> landmark.
  • useAiStream — the streaming engine, and its second consumer. It is domain-free by construction: a run of vision events uses it unchanged, alongside Complete and Suggest.
  • FileUpload + FileUploadDropzone — the empty state. One photograph may hold several slips.
  • @kanzo-tech/ui/table — the editable ledger, and the one call site here whose ColumnDef[] is built at runtime: the columns come out of a shape the reader is editing in the next pane, so the table redefines itself as they type. What survives that is the split between the two — the definitions are a pure function of the projected columns, and everything that moves per keystroke (the value, its findings, its confidence, the edit handler) reaches the cell through TanStack's meta. Rebuilding the definitions instead would hand every column a new identity on every keystroke, and column visibility is keyed off exactly that. The control a cell gets is decided by its sh:datatype and sh:in, not by a switch written here.
  • DataList — the slip panel's record, every column with what was read into it. Shark's, and this was one of the two renderers that got it adopted.
  • ImageCropper — the box a reader draws when the model put one in the wrong place, and the only paper a row typed by hand ever gets. Shark's, and this is the renderer that adopted it.
  • DownloadTrigger — the CSV, ;-separated with a BOM and the decimal comma left as typed, because that is the file a European spreadsheet locale opens by double-click.
  • ToggleGroup vertical — the activity bar. Two panes, independent, so it is multiple.
  • SegmentGroup, ScrollArea, Badge, Status, Show — the chrome.

Running it against a real model

The showcase ships a demo run — a real extraction, transcribed and replayed — so the docs build is deterministic and needs no key. The same (shots, signal) => AsyncIterable<ExtractEvent> seam takes a live adapter: paste your own Anthropic key into Preferences and Live model stops being greyed out. The option is greyed out until there is a key, rather than letting the button fail on click.

It runs in the browser, and there is no route handler. A server hop would exist to hide a key, and on a public docs site there is no key of ours to hide — the one in play is the reader's, so the SDK's dangerouslyAllowBrowser is the honest arrangement and the key stays in their localStorage. Not in the theme's prefs, which these docs persist as a cookie: a secret there would travel to the server on every request, which is the hop this removed.

@kanzo-tech/ui never learns what SHACL is either — the admission rule at the top of the barrel bars RDF by name, so every line of this lives in showcases/.