feat(review): remove species picker; species stays from Measure

Species is chosen on the merged Measure screen; the Review select was a
duplicate. Keep species_id on the slab from createSlab (Capture→draft→Woo)
and gate Create title from the hydrated value without offering edit.
This commit is contained in:
grokbot_ty
2026-09-26 08:47:40 -07:00
parent 89bc7d4ee9
commit 610b0883fe
6 changed files with 28 additions and 30 deletions
+1 -1
View File
@@ -118,7 +118,7 @@ export type Capture =
axis: Axis; // editable tape until Calculate; frozen after results
lengthEighths: Eighths;
thicknessEighths: Eighths;
/** Woo species leaf id; optional until Review. Persisted on draft + createSlab. */
/** Woo species leaf id; set on Measure. Persisted on draft + createSlab; Review does not edit. */
speciesId: number | null;
results: Measures | null;
/** Capture-order sibling results from the post-Calculate batch pass. */
+1 -1
View File
@@ -11,7 +11,7 @@ import type { PhotosReadyLineupItem } from '../capture/photosReadyLineup';
export interface CalibratedCapture {
lengthIn: number;
thicknessIn: number;
/** Woo species leaf from Measure; null if unset. Review can still change. */
/** Woo species leaf from Measure; null if unset. Review does not edit. */
speciesId: number | null;
sqft: number;
bdft: number;
+13 -25
View File
@@ -35,7 +35,6 @@
};
// --- Derived taxonomy option lists (resolve by slug, never hardcoded ids) ---
let speciesOpts: Opt[] = [];
let woodCatGroups: import('./MultiSelect.svelte').Group[] = [];
let edgeOpts: Opt[] = [];
let figureOpts: Opt[] = [];
@@ -47,10 +46,9 @@
function deriveOptions() {
if (!tax) return;
const cats: TaxCategory[] = tax.categories;
// Resolve the two top-level groupers BY SLUG (QUALITY-105 / METADATA-5 — never
// hardcode live Woo term ids): `species` → species options; `wcat` → wood
// categories. Both are parent_id:null groupers; their children are the leaves.
const speciesGrouper = cats.find((c) => c.parent_id === null && c.slug === 'species');
// Resolve the wood-category grouper BY SLUG (QUALITY-105 / METADATA-5 — never
// hardcode live Woo term ids). Species is chosen on Measure and is not editable
// on Review; only `wcat` leaves are listed here.
const wcatGrouper = cats.find((c) => c.parent_id === null && c.slug === 'wcat');
const childrenOf = (g: TaxCategory | undefined): TaxCategory[] =>
g ? cats.filter((c) => c.parent_id === g.id) : [];
@@ -67,7 +65,6 @@
return { id: t.id, name: t.name, ai: false, lowIn: Number.isFinite(lowIn) ? lowIn : undefined };
});
};
speciesOpts = childrenOf(speciesGrouper).map((c) => ({ id: c.id, name: c.name, ai: false }));
// Wood categories: parent/child grouped (METADATA-10 — parents group only, never
// assigned). Each direct child of `wcat` that has its own children becomes a parent
// group with those children as leaves; a child with no children is a leaf under the
@@ -94,12 +91,14 @@
}
// --- Editable model (mirrors the live slab) ---
// Species is set on Measure / createSlab — Review only reads it (no picker).
// Kept as a string so canCreateTitle can gate on a non-empty value from hydrate.
let speciesSel: string = '';
// #90: single-selects bind a STRING var to the native <select>, because
// <option value={id}> coerces the option value to a string. Binding a
// `number | ''` var directly to those string values is the classic
// number/string mismatch that leaves the selected option blank after AI
// prefill or reload. `Number(...)` at commit normalizes back.
let speciesSel: string = '';
let edgeSel: string = '';
let gradeSel: string = '';
let woodCatIds: number[] = [];
@@ -126,9 +125,10 @@
let reviewBusy = false;
let reviewError = '';
// #89: explicit "Create" actions. `createBusy` covers the deterministic
// title/short-desc generation; `canCreateTitle` is its gate (species + ≥1 wood
// category). `canCreateDesc` gates the AI long-description on title+short-desc
// being populated AND the content LLM being enabled.
// title/short-desc generation; `canCreateTitle` is its gate (species already
// on the slab from Measure + ≥1 wood category). `canCreateDesc` gates the AI
// long-description on title+short-desc being populated AND the content LLM
// being enabled.
let createBusy = false;
let createError = '';
$: canCreateTitle =
@@ -345,7 +345,6 @@
}
// AI-marked option views (legacy Call 1 badges; Call 1 disabled on this branch).
$: speciesOptsView = speciesOpts.map((o) => ({ ...o, ai: aiRecommendations.includes(o.id) }));
$: woodCatGroupsView = woodCatGroups.map((g) => ({
parent: g.parent,
leaves: g.leaves.map((o) => ({ ...o, ai: aiRecommendations.includes(o.id) })),
@@ -390,8 +389,9 @@
if (probe.title && title === '') title = probe.title;
if (probe.short_description && shortDescription === '') shortDescription = probe.short_description;
// species_id intentionally omitted — set at createSlab from Measure; Review
// must not clear or re-edit it.
const patch = {
species_id: speciesSel === '' ? null : Number(speciesSel),
wood_category_ids: woodCatIds,
edge_type_term_id: edgeSel === '' ? null : Number(edgeSel),
figure_term_ids: figureIds,
@@ -539,11 +539,9 @@
// option matches the bound value. Idempotent: only writes when they differ.
async function syncSelects() {
await tick();
if (speciesEl && speciesEl.value !== speciesSel) speciesEl.value = speciesSel;
if (edgeEl && edgeEl.value !== edgeSel) edgeEl.value = edgeSel;
if (gradeEl && gradeEl.value !== gradeSel) gradeEl.value = gradeSel;
}
let speciesEl: HTMLSelectElement | undefined;
let edgeEl: HTMLSelectElement | undefined;
let gradeEl: HTMLSelectElement | undefined;
/** Wood categories: toggle leaf + keep primary valid (REVIEW-15 / wireframe #90). */
@@ -652,17 +650,7 @@
{/if}
<!-- Call 1 OUT (overnight-whole-app): Suggest-with-AI removed. Call 2 generateContent stays. -->
<!-- Species (single, AI-assist) -->
<div class="field">
<label class="lbl" for="species">Species <span class="ai">AI assist</span></label>
<select id="species" class="sel" data-testid="species" bind:this={speciesEl} bind:value={speciesSel} on:change={commitReview}>
<option value="">— select —</option>
{#each speciesOpts as o (o.id)}
<option value={o.id} data-ai={aiRecommendations.includes(o.id) ? 'true' : ''}>{o.name}{aiRecommendations.includes(o.id) ? ' (AI)' : ''}</option>
{/each}
</select>
</div>
<!-- Species: chosen on Measure; no Review picker (value stays on the slab from createSlab). -->
<!-- Wood categories (multi + inline primary radio set, REVIEW-15 / #90 wireframe) -->
<div class="field" data-testid="wood-cat-field">