Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .agents/skills/frontend/information-architecture/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ Navigation maps 1:1 to domain objects. This is the target structure; if implemen
└─ frames in the way: subsection INSIDE Schema; each row links
out to /projects/:id/batches/:batchId, once per holding batch
batches Batches (workflow) — omitted when the host wires no batch route
dataset Dataset — a section, not a buried route; three views as tabs
dataset Dataset — a section, not a buried route; four views as tabs
(component state, Overview by default): Overview (counts,
per class) · Assets (the trunk) · Releases (the timeline)
per class) · Assets (the trunk) · Pre-processing (the
project's recipes, applied at export) · Releases (the
timeline)
/projects/:id/ingest Ingest flow
/projects/:id/batches/:batchId Batch workspace (gallery)
/jobs/:jobId Annotator (full-bleed)
Expand Down Expand Up @@ -58,6 +60,7 @@ Rules:
- **A batch shows one job's frames at a time, and one job is the batch.** With exactly one job the gallery draws no accordion and no job-level bar: the job's controls — its door, Pre-label, and the assignee as an editable line — sit under the batch header, followed by that job's filter, order, strip and frames, and the batch bar is the page's one bar. From two jobs the gallery is an accordion with at most one panel open and every panel closable; the open panel's counts, timeline and frames are that job's, each header names its assignee, and the panel open on arrival is the first job with work left. A draft batch, having no jobs, keeps the flat grid. The rule is observability: a batch-wide grid beneath a per-job control puts two scopes for the same frames on one screen, and a one-row accordion is a choice with nothing to choose between. Nothing about it changes an address — the gallery route is unchanged and the open panel is not in the URL.
- **A correction batch is reached from the batch that needs correcting**, never from a "new batch" form: the gallery header and the Batches row both offer it on a `completed` batch, capability-gated on `create_correction`. The annotator's read-only banner and the gallery's bulk bar *link* to it rather than duplicating it — creating a batch is a curation act, curation lives on the batch view, and a second place batches are made is a second place the rules can drift.
- **Dataset is first-class.** It is the product's central object and must be reachable in ≤1 click from any project section. It is never gated behind, or discoverable only through, onboarding UI. Promotion success links onward to it; the gallery links to it once a batch is `completed`.
- **Pre-processing is a view of the Dataset, not a section and not a dialog-only control.** A recipe is a named project resource with no state, chosen at export beside the target model, so it lives where releases are exported from: the fourth view of the Dataset section, between Assets and Releases. Its views are component state like the section's other three — a view of the same resource, not a place — and `ProjectScreen` holds which one is open, because the editor on that view carries the page's filled control (*Save recipe*, or the invitation to write one) and the navigation column that steps back for it (`contentOwnsTheAction`) is drawn outside the section. The Export dialog's recipe control offers what this view made; it never makes one.
- **The frames blocking a narrowing are a subsection of Schema, not a screen.** They are a *view of* the draft on the editor above them, the same relation version history has to the schema. A row links to **every** batch holding its frame rather than to one: an annotation carries an `asset_id` and no batch, so there is no single annotator address to prefer. The section is omitted entirely when the host wires no batch route, on the rule the Batches section already follows. It shows a window of the frames and states the total as text rather than a "see all": the destination that control would need is a project-wide asset view, and there is none — the count is a property of the proposal, not the length of a list somebody can open.
- **"Schema history" is not a sibling section.** Version history lives inside the Schema section, below the editor and beside the `VersionNavigator` seam. The two overlap on purpose: the navigator is the *reader* (one version, with what it changed), the history is the *ledger* (every version at once). `?tab=versions` remains as a redirect; it does not appear in the navigation.
- **The 4-step checklist is onboarding, not navigation.** It retires itself twice over: when the journey is finished (`hasReleases` makes `done` derivable) and when somebody dismisses it. Dismissal is **per project** and persisted — finishing one project does not teach you the pipeline for the next. It gates nothing and is never the sole path to a screen.
Expand Down
1 change: 1 addition & 0 deletions docs/content/preprocessing.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ scaled to match, and the response is never cached: the spec is the request's own

| | |
| --- | --- |
| UI | The Dataset section's **Pre-processing** view: the project's recipes as a list, an editor of four steps (target model, resize, augmentation, preview), and *Save recipe*; the Export dialog's **Pre-processing recipe** control chooses one by name, `None` by default. See [ui.md](ui.md#the-dataset-its-releases-and-getting-the-data-out). |
| REST | `POST`/`GET /projects/{id}/preprocessing-recipes`, `GET`/`PUT`/`DELETE /projects/{id}/preprocessing-recipes/{name}`, `POST /projects/{id}/preprocessing-preview`; `recipe=` on `POST /releases/{id}/export` and `GET /releases/{id}/export-compatibility`. The job carries the recipe as a snapshot. |
| CLI | `visionset recipe create NAME -p P --spec FILE` or `--resize letterbox:640x640 --augment hflip,brightness_contrast --variants 2 --target yolo11`; `recipe list`, `show`, `update`, `delete`; `export --recipe NAME`. See [cli.md](cli.md#visionset-release-and-visionset-export). |
| MCP | `create_preprocessing_recipe`, `list_preprocessing_recipes`, `delete_preprocessing_recipe` (only with `--allow-destructive`); `recipe` on `export_release` and `check_export`. See [mcp.md](mcp.md#datasets-releases-and-export). |
Expand Down
76 changes: 68 additions & 8 deletions docs/content/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,74 @@ timeline offers no edit and no delete, because there is no `ReleaseService.delet
- only a project's own cascade removes one, and the manifest blob survives even
that.

**The section is three views behind one tab row** — Overview (the counts and the per-class
table), Assets (the trunk), Releases (the timeline) — because each answers a different
question and stacked in one column each pushed the next below the fold. The row is the
product's one tab shape (`Tabs`' `line` variant on a full-width hairline); Assets and
Releases carry their counts on the label. The view is component state, Overview by
default; `DatasetScreen` takes `tab`/`onTabChange` like every other screen, so a host
that wants the view in a URL can wire it without a router inside `ui-core`. Publish
release stays on the section header, which every view shares.
**The section is four views behind one tab row** — Overview (the counts and the per-class
table), Assets (the trunk), Pre-processing (the project's recipes), Releases (the timeline) —
because each answers a different question and stacked in one column each pushed the next
below the fold. The row is the product's one tab shape (`Tabs`' `line` variant on a
full-width hairline); Assets, Pre-processing and Releases carry their counts on the label.
The view is component state, Overview by default; `DatasetScreen` takes `tab`/`onTabChange`
like every other screen, and `ProjectScreen` is the host that holds it, because the
Pre-processing view's editor owns the page's filled control and the navigation column
that steps back for it (`contentOwnsTheAction`) is drawn outside the section. Publish
release stays on the section header, which every view shares, and is `secondary` on every
view.

**Pre-processing is a recipe, written once and chosen at export.** A recipe is a named
project resource — no state, no `allowed_actions`, every write unconditional
(`docs/content/preprocessing.md`) — so the view gates nothing. The left panel is the list,
one row per recipe with its one-line summary (`letterbox 640×640 · flip · 2 variants`) and
the target its hints came from as a `quiet` chip, a small `New`, and the line that says where
a recipe acts: *Applied at export. Choose a recipe in the Export dialog; exports without one
apply no transform.* With no recipe yet the panel is an invitation with one verb-first
action. The first recipe opens on arrival, so the editor is on screen whenever there is
something to edit.

The editor is **four steps, always visible, each settled or not** — not the ingest flow's
one-step-at-a-time stepper, because a recipe's choices have no order between them. The
marker at each step's head is the ingest flow's own (`patterns/StepMarker.tsx`), so a
decided step reads the same on both screens. *Target model* is the Export dialog's grouped
picker (`patterns/ExportTargetSelect.tsx`), and choosing one preselects the resize from the
target's hints — `recommended_strategy` on the strategy chips, marked *suggested*, and
`recommended_size` in the width and height — while the second line names the family, the
tasks, the format it writes, and the geometries it carries with the active schema's class
count. The hints are read off the wire; nothing in the browser computes a size. **Changing
the target rewrites the suggestion only for fields nobody has typed in**, so a size chosen on
purpose survives a change of model (`recipeDraft.ts`, `touched`). *Resize* leads with the
ambient line a trainer earns when `trainer_resizes` is set (*YOLO11 letterboxes to 640 on
its own. Pre-resizing shrinks the archive and speeds up loading.*), then Letterbox / Stretch
/ None, the size, the pad value for a letterbox, and the *Geometry exact* line.
*Augmentation* is three checkboxes — the amount beside brightness and contrast, the variants
per image beneath — and the line that says variants are written for the train fold only, so
an export with augmentation needs a release published with a split. Ticking the first
augmentation sets one variant and unticking the last clears it, which are the spec's own two
cross-field rules; every other bound of the request body is restated beside its field
(*Width is a whole number from 32 to 8192.*) so the reason a step is unsettled is next to
it and the footer carries the first one beside a shut *Save recipe*. Those are the body's
shape, not a kernel rule the client is mirroring: the server still answers 422 to a body the
form did not build.

*Preview* renders three sample assets through `POST /projects/{id}/preprocessing-preview`,
the export's own kernel path over a one-asset manifest: the first three train-fold members
of the newest release with a split — variants are the train fold's — or the project's first
three assets when no release has one. Three columns: the asset as it is, after the resize
step alone, and the first augmented variant; a stage the recipe does not have says so
(*No resize step*) rather than repeating the original. Each cell is one request keyed on
the spec it renders, and the spec settles for 400 ms before a cell asks, so typing `640`
does not pay for `6` and `64`. The cell is the member dialog's own picture-with-labels
mechanism (`patterns/StaticAnnotationOverlay.tsx`): the rendered image, and the response's
placed annotations drawn over it in the rendering's own pixel frame with the schema's class
colours — a box after a letterbox sits where the export writes it. A rendering without a
size shows the picture alone. A refused rendering — `UNSUPPORTED_MEDIA` for an asset that is
not a JPEG, PNG or WebP — is prose in the cell.

Save is a create for a new draft and a whole-replace `PUT` at the recipe's current name for
an open one; Discard puts the stored spec back; `PREPROCESSING_RECIPE_NAME_TAKEN` and every
other refusal render through the vocabulary. The Export dialog's second control,
**Pre-processing recipe**, lists the same recipes with the same one-line summary under
*None*, and sends the chosen name as `recipe=`; a project with none says so in a line rather
than offering a picker with one row. The two 409s an export can answer —
`AUGMENTATION_REQUIRES_SPLIT`, `PREPROCESSING_STEP_UNSUPPORTED_GEOMETRY` — are refusals, never
consents: prose with the remedy, and no checkbox.

**The trunk is a grid of pictures, looked at one at a time.** Every promoted asset is a
tile — the frame number over the picture and how many labels it carries underneath, both read
Expand Down
11 changes: 10 additions & 1 deletion docs/content/ui/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,16 @@ without knowing the URL scheme.
bottom. Every section's own header uses `secondary` actions — Ingest beside Annotate on
Overview and Batches, Publish on Dataset, Save version on Schema. On a sub-view (the
gallery, the ingest flow) the column draws no filled control at all: that page owns its
dominant action, and a second one beside it would be two answers to "what now?".
dominant action, and a second one beside it would be two answers to "what now?". A view
*inside* a section can own it too: while the Dataset's Pre-processing view is showing, its
editor's *Save recipe* — or, with no recipe yet, the invitation to write one — is the page's
one filled control, and the column's Annotate or Ingest steps back to `secondary` for as
long as that view is open, the way it does for the Overview's first-run invitation.
- **The Dataset's views are component state.** Overview, Assets, Pre-processing and Releases
are four lenses on one section, chosen with the product's one tab row; none is a path
segment or a query parameter, on the rule below that not everything selectable is a place.
The open view is held by the project screen rather than by the section, because the
navigation column has to know when the Pre-processing editor holds the filled control.
- **The project's identity is an eyebrow above the content, not part of the column.** One
line above a section's `h1`, at every width: the project's name and the active-version
chip, omitted when there is no schema (`patterns/ProjectEyebrow.tsx`). It is identity and
Expand Down
10 changes: 10 additions & 0 deletions frontend/ui-core/src/data/refusals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,16 @@ export const REFUSAL_PROSE: Record<string, string> = {
RELEASE_CONTENT_WOULD_VIOLATE_SCHEMA:
"The active schema no longer describes some annotations in this dataset.",

// Pre-processing recipes. The two 409s arrive on an export, where the
// remedy is a different release or a different recipe; the kernel's own
// sentences for the name pair carry a project id, which a person cannot use.
PREPROCESSING_RECIPE_NOT_FOUND: "That recipe is no longer on record.",
PREPROCESSING_RECIPE_NAME_TAKEN: "A recipe with that name already exists in this project.",
AUGMENTATION_REQUIRES_SPLIT:
"This recipe augments, and the release was published without a split — publish one with folds, or export with a recipe that only resizes.",
PREPROCESSING_STEP_UNSUPPORTED_GEOMETRY:
"A step in this recipe cannot move a shape this release holds — quarter turns cannot follow a polyline. Take the step out, or export without the recipe.",

// Inference connections.
INFERENCE_CONNECTION_NOT_FOUND: "That model connection is no longer on record.",
INFERENCE_CONNECTION_NOT_DOWNLOADABLE:
Expand Down
42 changes: 42 additions & 0 deletions frontend/ui-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,3 +451,45 @@ export {
} from "./patterns/ExportTargetSelect.js";
export { describeTargetDrops } from "./data/refusals.js";
export { StaticAnnotationOverlay, type StaticAnnotationOverlayProps } from "./patterns/StaticAnnotationOverlay.js";
export { DATASET_TABS, resolveDatasetTab, type DatasetTab } from "./screens/DatasetScreen.js";
export { PreprocessingTab, type PreprocessingTabProps } from "./screens/PreprocessingTab.js";
export {
AUGMENT_OPS,
applyTargetHints,
canonicalSpec,
describeRecipeSpec,
draftFromSpec,
draftToSpec,
EMPTY_DRAFT,
sameSpec,
touch,
type AugmentOp,
type RecipeDraft,
type RecipeSpec,
type ResizeChoice,
type SpecOutcome,
type SpecProblem,
} from "./screens/recipeDraft.js";
export {
recipeKeys,
useCreatePreprocessingRecipe,
useDeletePreprocessingRecipe,
usePreprocessingPreview,
usePreprocessingRecipes,
useReleaseAssignment,
useUpdatePreprocessingRecipe,
type PreprocessingPreview,
type PreprocessingRecipe,
type PreprocessingRecipePage,
type PreviewAnnotation,
type SplitAssignment,
} from "./screens/queries.js";
export { RecipeList, type RecipeListItem, type RecipeListProps } from "./patterns/RecipeList.js";
export {
RecipeEditor,
targetCarries,
targetSubtitle,
trainerResizesLine,
type RecipeEditorProps,
} from "./patterns/RecipeEditor.js";
export { StepMarker, type StepMarkerProps, type StepState } from "./patterns/StepMarker.js";
Loading
Loading