# S01 — Import & Convert UI (View 1) — Research

**Date:** 2026-03-26

## Summary

This slice builds the first user-facing screen of the Kerf App: an Import & Convert view where users upload a raster image (PNG, JPG, etc.), select a vectorization preset, tune parameters via sliders, see a live SVG preview, and click "Use This" to advance to the Design Canvas. The engine API is fully built and validated (M001, 196 tests) — this slice is a pure frontend build that consumes it.

The work is greenfield — no `app/` directory, no `package.json`, no frontend tooling exists. The slice must scaffold a React + Vite + TypeScript project, establish the app shell with view routing, build the file upload flow, integrate with the Engine API's `multipart/form-data` endpoints, implement debounced re-tracing on parameter changes, and render SVG previews. A secondary prerequisite is adding CORS middleware to the engine so browser requests succeed.

The approach is straightforward: Vite + React + TypeScript scaffold, Zustand or simple React state for view-level state, raw `fetch` + `FormData` for engine API calls (no need for a heavy HTTP client), and inline SVG rendering via `dangerouslySetInnerHTML` for the preview. The primary risks are the debounced preview update loop (must cancel in-flight requests on param change) and correctly mapping UI slider values to the engine's nested parameter structure.

## Recommendation

Scaffold `app/` as a Vite + React + TypeScript project. Keep dependencies minimal — no UI framework needed for V1 (plain CSS or CSS modules). Use a simple view router (React Router or even a state-based conditional render since there are only 3 views). Build the engine API client as a thin typed wrapper around `fetch`. For the live preview loop, use an `AbortController`-based pattern: each parameter change triggers a debounced (300ms) re-trace call that aborts any in-flight request first. Add CORS to the engine's `main.py` as a prerequisite task.

## Implementation Landscape

### Key Files

**Engine (existing — read-only context for this slice):**
- `engine/main.py` — FastAPI app; needs `CORSMiddleware` added (only change to engine code)
- `engine/api/routes.py` — Defines `POST /engine/trace` (multipart: file, mode, output_format, preset, params), `POST /engine/simplify`, `GET /engine/presets`
- `engine/presets/*.json` — 5 preset configs (sign, patch, stencil, detailed, custom); each has `preprocessing`, `vectorization`, and `postprocessing` sections

**App (to be created):**
- `app/package.json` — Vite + React + TypeScript deps
- `app/vite.config.ts` — Dev server config with proxy to engine API (avoids CORS in dev, but CORS still needed for production)
- `app/src/main.tsx` — React root mount
- `app/src/App.tsx` — Top-level view router (View 1 → View 2 → View 3)
- `app/src/views/ImportConvert.tsx` — Main View 1 component
- `app/src/components/FileUpload.tsx` — Drag-and-drop file upload zone
- `app/src/components/PresetSelector.tsx` — Visual preset cards (icon + label + description)
- `app/src/components/ParameterSliders.tsx` — Slider controls mapped to engine params
- `app/src/components/SvgPreview.tsx` — Live SVG render panel
- `app/src/components/OutputInfoBar.tsx` — Stats bar (path count, node count, warnings) with color coding
- `app/src/api/engine.ts` — Typed API client: `traceImage()`, `getPresets()`, `simplifyVector()`
- `app/src/types/engine.ts` — TypeScript types for API request/response shapes, preset configs

### Build Order

1. **CORS on engine** — add `CORSMiddleware` to `engine/main.py`. Without this, no browser-based API call works. Quick, unblocks everything. Verify: `curl -H "Origin: http://localhost:5173" -I http://localhost:8000/engine/health` returns CORS headers.

2. **Vite + React scaffold** — `npm create vite@latest app -- --template react-ts`, install deps, configure dev proxy. Verify: `npm run dev` shows a React page at `localhost:5173`.

3. **API client + types** — Build `engine.ts` with typed `traceImage()` and `getPresets()` functions. Uses `FormData` for trace (the engine expects `multipart/form-data`), returns parsed JSON with typed `TraceResponse` interface. Verify: import and call `getPresets()` from a useEffect, log to console.

4. **View shell + routing** — App.tsx with view state (import → canvas → export). Only View 1 active for this slice; View 2/3 are placeholder divs. The "Use This" button sets view state + passes vector data forward. Verify: view switching works on button click.

5. **File upload component** — Drag-and-drop zone + file input fallback. Accepts PNG, JPG, BMP, TIFF, WebP for raster; SVG for simplification mode. Shows filename + thumbnail after selection. Verify: selecting a file shows preview.

6. **Preset selector + parameter sliders** — Fetch presets from `GET /engine/presets` on mount. Render preset cards. When preset selected, populate slider values from preset defaults. Sliders: Detail Level (epsilon), Threshold, Noise Filter, Smooth Curves, plus an Advanced panel that exposes raw params. Verify: selecting different presets changes slider positions.

7. **Live preview with debounced re-trace** — Core interaction loop. On any parameter change: debounce 300ms → build FormData with file + current params → `POST /engine/trace` → render returned SVG. Use `AbortController` to cancel in-flight request if params change again. Verify: change a slider, see SVG preview update after debounce.

8. **Output info bar** — Render metadata from trace response: path count, total node count, open path count, warnings. Color code: green (good), yellow (high node count), red (open paths). Verify: info bar updates with each trace response.

### Verification Approach

- `cd app && npm run dev` starts Vite dev server at `localhost:5173`
- Engine must be running at `localhost:8000` (either via docker or `cd engine && .venv/bin/uvicorn main:app --port 8000`)
- Upload a PNG image → preset selector appears → select "sign" → sliders populate → SVG preview renders
- Change epsilon slider → preview updates within ~500ms (300ms debounce + API latency)
- Switch to "detailed" preset → sliders change → preview updates with higher detail
- Info bar shows path count, node count, open paths — values change per preset
- Click "Use This" → view state transitions (placeholder View 2 renders)
- Upload an SVG file → routes to simplification mode (only epsilon slider, no preprocessing/vectorization controls)

## Constraints

- **Engine API uses `multipart/form-data`** — cannot POST JSON. Must construct `FormData` with `file` field (Blob/File) and string fields for `mode`, `output_format`, `preset`, `params` (JSON-encoded string).
- **`params` field is a JSON string** — nested parameter overrides are sent as a serialized JSON string in the form data, not as separate form fields. The engine's `resolve_params()` merges these over preset defaults.
- **SVG preview is a raw string** — the engine returns SVG as a string in `response.output`. Rendering it requires either `dangerouslySetInnerHTML` or creating an `<img src="data:image/svg+xml,...">` tag. The former preserves interactivity/scalability.
- **No `python` on system PATH** — engine must run via `.venv/bin/uvicorn` or Docker. Dev workflow needs both processes (engine + vite) running simultaneously.
- **Engine has no CORS** — `engine/main.py` needs `CORSMiddleware` from `fastapi.middleware.cors` added before any browser-based testing works.

## Common Pitfalls

- **Stale previews from race conditions** — If the user changes params rapidly, multiple `/engine/trace` requests fire. The response from an older request may arrive after a newer one, showing stale output. Fix: use `AbortController` — abort previous request before sending a new one, and/or track a request generation counter and discard responses from older generations.
- **SVG viewBox scaling** — Engine-generated SVGs have their own `width`, `height`, and `viewBox`. The preview panel must scale the SVG to fit its container while preserving aspect ratio. Stripping `width`/`height` attributes and relying on `viewBox` + CSS `width: 100%; height: auto` is the standard approach.
- **File reference lost after first trace** — The `File` object from the upload input must be retained in state across re-traces. Each debounced trace call rebuilds `FormData` with the same file. Don't re-read the file from the input element — store it in React state.
- **Advanced panel param names must match engine expectations** — The `params` JSON keys must exactly match what `resolve_params()` looks for (e.g., `turdsize`, `alphamax`, `epsilon`, `filter_speckle`). Typos silently ignored.

## Don't Hand-Roll

| Problem | Existing Solution | Why Use It |
|---------|------------------|------------|
| Project scaffold | `npm create vite@latest -- --template react-ts` | Standard React + TS + Vite in one command |
| Debounce | `setTimeout`/`clearTimeout` pattern or a tiny `useDebouncedCallback` hook | Simple enough to hand-roll as a 10-line hook; no need for lodash |
| Request cancellation | `AbortController` (native browser API) | Built-in, no library needed |
| SVG rendering | `dangerouslySetInnerHTML` on a container div | The SVG is trusted (comes from our engine), so XSS concern is minimal |

## Open Risks

- **Engine latency on large images** — If tracing takes >2s for a large image, the live preview UX degrades. May need a loading spinner overlay on the preview during re-trace. Not a blocker but affects polish.
- **SVG simplification mode routing** — When user uploads an SVG instead of a raster, the UI should switch to a reduced control set (only epsilon slider, no preprocessing/vectorization). This is a separate code path that needs clean conditional rendering.

## Skills Discovered

| Technology | Skill | Status |
|------------|-------|--------|
| React | `react-best-practices` | installed (available in skill list) |
| Vite | `antfu/skills@vite` (11.5K installs) | available — not installed |

## Sources

- Engine API contract: `engine/api/routes.py` (read directly)
- Preset structure: `engine/presets/*.json` (read directly)
- Project brief: `GSD-INITIATE.md` View 1 specification (read directly)
