# Module: `src/components/DetectionClasses.tsx` > **Source**: `src/components/DetectionClasses.tsx` (99 lines) > **Topo batch**: B3 (depends on B2 leaves: `api/client`, `features/annotations/classColors`, `types/index`) ## Purpose A side-panel widget that lets the annotator pick a detection class (1–9 or click) and a photo mode (`Regular`, `Winter`, `Night`). Loads the class catalogue from the backend and falls back to a hardcoded list when the API returns empty / errors. Replaces the legacy WPF detection-class picker referenced in `_docs/legacy/wpf-era.md` §"What survived". ## Public interface ```ts interface Props { selectedClassNum: number onSelect: (classNum: number) => void photoMode: number onPhotoModeChange: (mode: number) => void } export default function DetectionClasses(props: Props): JSX.Element ``` Fully controlled — the parent owns `selectedClassNum` and `photoMode`. The current contract for `photoMode` values is integer offsets `0` (Regular), `20` (Winter), `40` (Night), matching the `photoMode` field of `DetectionClass` (`src/types/index.ts`) and the offsets baked into `FALLBACK_CLASSES` below. ## Internal logic - **Class catalogue load** (mount-only `useEffect`): - `api.get(endpoints.annotations.classes())` (= `/api/annotations/classes`, since AZ-486 / F7). - On a non-empty array → `setClasses(list)`. - On an empty array OR a thrown error → `setClasses(FALLBACK_CLASSES)`. - **`FALLBACK_CLASSES`** is a module-private 3 × |`FALLBACK_CLASS_NAMES`| matrix: - For each mode offset in `[0, 20, 40]`, build one class entry per name in `FALLBACK_CLASS_NAMES` (imported from `features/annotations/classColors.ts`). - Each entry: `{ id: i + modeOffset, name, shortName: name.slice(0, 3), color: getClassColor(i), maxSizeM: 10, photoMode: modeOffset }`. - This means: in offline / API-down mode, the ID range is `0–N-1` (Regular), `20–20+N-1` (Winter), `40–40+N-1` (Night). The backend's class IDs MUST follow the same convention or fallback↔backend handoff yields ID collisions. Confirm in Step 4 via the `annotations/` service contract. - **Numeric hotkeys 1–9** (effect keyed on `[classes, photoMode, onSelect]`): - `keydown` on `window`. `parseInt(e.key)` → if 1–9, picks `classes[(num - 1) + photoMode]`. - **Bug-shaped**: `photoMode` is `0 | 20 | 40` (an *offset*), not a row count. `classes[idx + 0]` is correct for Regular; for Winter/Night the index `idx + 20` / `idx + 40` is meaningful only when `classes` is in the contiguous `[0..N-1, 20..20+N-1, 40..40+N-1]` shape that `FALLBACK_CLASSES` produces. **If the backend returns its classes in a different order**, hotkey 1–9 will pick the wrong class. Verify backend ordering in Step 4 — this is the principal correctness risk in this module. Flag. - **Auto-select first class on mode change** (effect keyed on `[classes, photoMode, selectedClassNum, onSelect]`): - Filter `classes` to the active `photoMode`. - If `selectedClassNum` is not within that filtered set, call `onSelect(modeClasses[0].id)`. - This guarantees the parent always holds a class ID consistent with the selected photo mode. - **Render**: - Class list — only entries whose `photoMode` matches the active mode. - Photo-mode bar — three buttons (Sunny / Snowflake / Moon icons) for Regular / Winter / Night. ## Dependencies - **Internal**: - `../api` (barrel) — `api`, `endpoints`. (Since AZ-485 / F4 + AZ-486 / F7.) - `../features/annotations/classColors` — `getClassColor(i)`, `FALLBACK_CLASS_NAMES`. (Cross-component import preserved; flagged in Consumers below.) - `../types` — `DetectionClass` type. - **External**: `react`, `react-i18next`, `react-icons/md`, `react-icons/fa`. ## Consumers (intra-repo) From the §7a dependency graph: - `src/features/annotations/AnnotationsPage.tsx` - `src/features/dataset/DatasetPage.tsx` This is the **canonical example** of the cross-layer import flagged in `_docs/02_document/00_discovery.md` §8: a `components/` (shared) module importing from `features/annotations/`. Two clean fixes are in scope for Step 4 / Step 8: 1. Lift `classColors.ts` into `src/components/detection/` (or `src/shared/`) and update the two consumers. 2. Or: move `DetectionClasses` itself into `src/features/annotations/` since both consumers are in `features/`. ## Data models `DetectionClass` (from `src/types/index.ts`) — see `_docs/02_document/modules/src__types__index.md`. `FALLBACK_CLASSES` is module-private; see Internal logic above. ## Configuration Endpoint: `endpoints.annotations.classes()` → `/api/annotations/classes` (typed builder from `../api/endpoints`, since AZ-486 / F7). Photo-mode value set is `{0, 20, 40}` — hardcoded, mirrored by `FALLBACK_CLASSES`. If the backend grows a fourth mode (e.g. thermal, IR), every consumer of `photoMode` will need a coordinated change. Tailwind tokens: `bg-az-orange` (Regular), `bg-az-blue` (Winter), `bg-purple-600` (Night). Defined in `src/index.css`. ## External integrations - HTTP `GET endpoints.annotations.classes()` (= `/api/annotations/classes`) → `DetectionClass[]`. Backed by the `annotations/` service (`.NET`) per `nginx.conf`. ## Security - **Silent failure on class load**: `.catch(() => setClasses(FALLBACK_CLASSES))` swallows the error and the user sees the fallback. Acceptable for UX continuity, but the lack of any user-visible signal means a misconfigured `/api/annotations/classes` deploy could go unnoticed in prod. Flag for Step 6 `security_approach.md` / Step 8. - No input that flows back to the server here. - The `getClassColor(i)` palette is deterministic; no PII. ## Tests None. ## Notes / open questions - **`getPhotoModeSuffix` redundancy** (carry-over from B1): `classColors.ts` exposes `getPhotoModeSuffix()` that derives the same suffix the typed `DetectionClass.photoMode` field already encodes. Once `classColors.ts` is repositioned (see Consumers), `getPhotoModeSuffix` is a deletion candidate. Defer to Step 8. - **`FALLBACK_CLASS_NAMES.length`** is implicitly assumed to be ≤ 9 by the hotkey code (only 1–9 are bound). If the catalogue grows to 10+ entries, hotkeys can no longer cover the tail. Acceptable for now. - **Mode-button colours don't use `az-` tokens for Night** (`bg-purple-600`, `text-purple-400` instead of an `az-purple` token). Cosmetic inconsistency; flag for Step 4 against `_docs/ui_design/README.md` colour palette. - The class list is rendered with `1.`, `2.`, … prefixes derived from `i+1` — so the hotkey number always matches the visible label inside the active mode. Good. - Does not handle modifier keys (`Shift`, `Ctrl`) on numeric hotkeys; pressing `Shift+5` will trigger the `5` branch. Fine for now.