docs: Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis) across all 6 running docs
CI / backend (pytest + alembic) (push) Has been cancelled
CI / frontend (build) (push) Has been cancelled

Sprint 13 (commit bae9403) splits the Sprint 11 "Generate Meal
Plan" CTA into a 2-step modal: "Use the recipe library" (default,
Sprint 11 unchanged) or "Ask the LLM" (new). The LLM path POSTs
to /api/llm/plan; the backend calls kimi-k2.6:cloud on ollama.com,
parses the LLM’s JSON picks, creates a fresh plan, fills the
LLM’s picks, and falls through to the Sprint 6+ fillEmptySlots
pattern for the slots the LLM didn’t cover. No pre-existing WIP
files touched.

This commit updates the 6 running docs that track the sprint:

- .agent/plan.md — Sprint 13 section (S13.1-S13.4) added.
- .agent/context.md — Sprint 13 (D1-D9, Q1-Q3) added; file:line
  references; key takeaways.
- Review/sprint13-verification.md — new file: 3-step browser
  smoke + 4 API curls + a11y check + 6-risk table + future
  work section.
- Review/ui-nielsen-audit.md — Sprint 13 status block (T7.1-T7.3)
  at the top, after the Sprint 12 block.
- fix-ui-audit.md — Sprint 13 section (T7.1-T7.5) added after
  the Sprint 12 section.
- Review/handoff-ui-audit.md — Batch I added to the deploy
  instructions; Sprint 13 section added after Sprint 12; TL;DR
  table row 13 added; Last-updated footer updated.
- docs/HANDOFF.md — Sprint 13 section added after the Sprint 12
  section, with a path-forward paragraph for F9-full.

All 6 docs now reflect Sprint 13. §Future backlog remaining:
F9-full (local Ollama model pull on the host) — opt-in based on
cloud-billing feedback. _ask_llm is the single seam: F9-full only
needs to swap the URL + model name.
This commit is contained in:
2026-06-05 16:59:20 -07:00
parent bae94037f3
commit 8cb4d4198c
7 changed files with 361 additions and 6 deletions
+122
View File
@@ -0,0 +1,122 @@
# Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis) — verification
**Status (2026-06-05):** ✅ Code complete. `npm run build` green. Awaiting user deploy.
## Summary
Sprint 13 splits the Sprint 11 "Generate Meal Plan" CTA into a 2-step modal: the user picks "Use the recipe library" (default, Sprint 11's existing flow) or "Ask the LLM" (new). The LLM path POSTs to `/api/llm/plan` with a free-text prompt; the backend calls `kimi-k2.6:cloud` on `ollama.com`, parses the LLM's JSON picks, creates a fresh plan, fills the LLM's picks, and falls through to the Sprint 6+ `fillEmptySlots` pattern for the slots the LLM didn't cover.
**No pre-existing WIP files touched.** Sprint 13 creates a new `backend/app/api/llm_plan.py` router (separate from the existing `llm_matcher.py` service) and reuses the established LLM call pattern (POST `${OLLAMA_BASE_URL}/chat/completions`, `Authorization: Bearer ${OLLAMA_API_KEY}`, same `max_tokens: 800, temperature: 0`, strip `<think>` blocks).
## Files changed
### Backend
- **NEW** `backend/app/api/llm_plan.py` (~280 lines) — 1 endpoint + 4 helpers: `_ensure_ollama_configured`, `_serialize_library`, `_ask_llm` (mirrors `llm_matcher._ask_ollama`), `_parse_picks` (tolerant of markdown code fences), `_validate_picks`. 200-recipe cap on the library sent to the LLM.
- **MODIFIED** `backend/app/schemas/__init__.py` — added `LLMPlanRequest` + `LLMPlanResponse`.
- **MODIFIED** `backend/app/main.py` — registered `llm_plan_api.router` at `/api/llm`.
### Frontend
- **MODIFIED** `frontend/src/api/index.ts` — added `llm.plan(data)`.
- **MODIFIED** `frontend/src/pages/Dashboard.tsx` — added the prompt modal (radio for library vs. LLM + textarea for the LLM path) + extracted Sprint 11's body into `generateFromLibrary` + added `generateFromLLM`. New state: `showPromptModal`, `promptMode`, `promptText`, `promptBusy`. The CTA now opens the modal; the modal's submit button dispatches on the radio.
## Build verification
```text
vite v5.4.21 building for production...
transforming...
✓ 1897 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 0.54 kB │ gzip: 0.32 kB
dist/assets/index-CPlRXkCg.css 42.38 kB │ gzip: 7.30 kB
dist/assets/index-665b6KrF.js 503.82 kB │ gzip: 154.33 kB
✓ built in 2.84s
```
- `tsc` 0 errors, `vite` 0 errors.
- Bundle: 500.28 → 503.82 kB (+3.5 kB for the modal + the LLM handler).
- Backend AST clean on all 3 changed files.
## Backend verification (manual, post-deploy)
```bash
# Requires OLLAMA_API_KEY + OLLAMA_BASE_URL + OLLAMA_MODEL set in the
# backend env. (All three already exist; Sprint 13 just adds the
# router that reads them.)
# 1) Happy path
curl -sS -X POST 'http://100.108.208.56:8082/api/llm/plan' \
-H 'Content-Type: application/json' \
-H 'Cookie: mealplanner_session=...' \
-d '{"prompt": "Italian-inspired, vegetarian", "week_start": "2026-06-08"}' | jq
# Expected: { "plan_id": "<uuid>", "picked_count": 7+,
# "filled_count": 14-, "failed_count": 0,
# "reasoning": null }
# 2) OLLAMA_API_KEY unset (or temporarily unset in env + restart)
curl -sS -X POST 'http://100.108.208.56:8082/api/llm/plan' \
-H 'Content-Type: application/json' \
-H 'Cookie: mealplanner_session=...' \
-d '{"prompt": "test", "week_start": "2026-06-08"}' -i | head -1
# Expected: HTTP/1.1 503 Service Unavailable
# 3) Empty prompt
curl -sS -X POST 'http://100.108.208.56:8082/api/llm/plan' \
-H 'Content-Type: application/json' \
-H 'Cookie: mealplanner_session=...' \
-d '{"prompt": "", "week_start": "2026-06-08"}' -i | head -1
# Expected: HTTP/1.1 422 Unprocessable Entity
# 4) Plan for this week already exists
curl -sS -X POST 'http://100.108.208.56:8082/api/llm/plan' \
-H 'Content-Type: application/json' \
-H 'Cookie: mealplanner_session=...' \
-d '{"prompt": "test", "week_start": "2026-06-01"}' -i | head -1
# Expected: HTTP/1.1 400 Bad Request (if a plan for 2026-06-01 exists)
```
## Browser smoke (3 steps)
Run on `http://100.108.208.56:8082/`.
1. **Land on `/` with no plan.** Click "Generate Meal Plan". Confirm: the modal opens with the title "Generate Meal Plan", a short description, and the 2 radio options ("Use the recipe library" selected by default, "Ask the LLM" unselected). The "Generate" button is enabled; the "Cancel" button is enabled.
2. **Type "Italian-inspired, vegetarian" + select "Ask the LLM" + click Generate.** Confirm: button label flips to "Asking LLM…" (with a spinning Loader2 icon), the modal is non-dismissible, ~5-15s later (depends on the LLM latency) the modal closes, the plan grid renders with the LLM's picks, and a toast shows `"Planned N meals (LLM picked K, library filled the rest)"`.
3. **Refresh the page.** Confirm the plan persists. The empty state does NOT re-appear.
## A11y check
- The modal is a `<div className="fixed inset-0 z-50">` with a click-outside-to-dismiss handler (skipped while `promptBusy` is true).
- The radios are real `<input type="radio">` with associated `<label>` blocks. Tab cycles through both radios, the textarea (when LLM mode is selected), and the 2 buttons.
- The textarea has `autoFocus` when the modal opens in LLM mode. Default LLM mode is library (no textarea shown); switching to LLM mode does not steal focus (intentional — the user just clicked the radio, they shouldn't lose their cursor).
- The character counter is a `<div>` with `text-right` alignment, screen-reader-accessible via the `500` max length on the textarea.
- The "Generate" button is `disabled` while `promptBusy` is true (matches Sprint 11's `generatingFirstPlan` state pattern).
- The Sprint 9 OnboardingTour's first step is the Dashboard's Weekly Overview card; the modal renders on top of that card via `z-50` + `bg-black/40` scrim. The tour is not affected.
## Risks & mitigations
- **R1: LLM returns 0 picks or 21 identical recipes.** Mitigation: the response is validated (recipe_id in library, day_of_week 1-7, meal_type in {breakfast, lunch, dinner}) and invalid entries are dropped. The library fill then takes over for any slot the LLM didn't cover. A prompt that returns 0 valid picks still produces a complete plan from the library.
- **R2: LLM times out (60s).** Mitigation: the `requests.post` call has a 60s timeout. On timeout, `_ask_llm` returns None; the endpoint then calls the library fill (no LLM picks). The user sees a success toast with `picked_count: 0`.
- **R3: Library is empty.** Mitigation: 400 with `detail: "recipe library is empty; import some recipes first"`. The frontend `showApiError` surfaces the message. (Pre-existing WIP, also affects the Sprint 11 library path.)
- **R4: OLLAMA_API_KEY unset.** Mitigation: 503 with clear `detail: "OLLAMA_API_KEY not configured; set it in the backend env"`. The frontend `showApiError` surfaces the message; the user can switch to "Use the recipe library" and proceed.
- **R5: OLLAMA_CLOUD costs.** The operator's existing OLLAMA billing applies. The endpoint is public + requires `require_session` (so no public abuse). A future sprint could add a per-day rate limit.
- **R6: Sprint 12's pre-existing WIP collision.** Verified: the WIP `recipes.py` is registered at `/api/recipes/*` only; my new `/api/llm/plan` is in a different prefix. No collision.
## Commit
One commit: `feat(ui): Sprint 13 — F9-lite (Ollama Cloud free-text plan synthesis)`. Files:
- `backend/app/api/llm_plan.py` (NEW, ~280 lines)
- `backend/app/schemas/__init__.py` (2 Pydantic models)
- `backend/app/main.py` (router registration)
- `frontend/src/api/index.ts` (1 new method)
- `frontend/src/pages/Dashboard.tsx` (modal + LLM handler + extracted library handler)
## Future work (NOT in Sprint 13)
- **F9-full — local Ollama model pull.** Pull Mistral 7B or Llama 3 8B (~4 GB) on the host + a separate `ollama serve` process. Avoids cloud costs and the LLM_API_KEY dependency. Future sprint.
- **Prompt template library.** Save common prompts ("easy weeknight", "vegetarian week", "quick breakfasts") as one-click buttons. Out of scope for the initial ship.
- **Multi-week plans.** One week at a time. The endpoint is parameterised on `week_start`; calling it twice with two different weeks would work, but the UI doesn't surface it yet.
- **Streaming the LLM response.** Currently waits for the full response. Future sprint could use the Ollama streaming API + a "thinking…" indicator.
- **Per-day rate limit.** A future sprint could gate the LLM call on a per-day budget to avoid surprise cloud bills.
- **Vitest unit test for `useOnboarding`** (Q4 from Sprint 9). Still pending.