> Source URL: /unit-3/project-paths/duward-a/duward-a-2026-04-24.guide
# Duward's Project Guide

**Project:** GPT Workout Planner  
**Category:** AI / Mobile Web App (Flask + Tailwind)  
**Last updated:** April 24

---

> Note: This guide reflects the latest state of your project repo (`final-project-angldu6`). It may not match the most up-to-date version if you've worked since.

## Where You Are

Your Flask app works: `/` + `/generate`, Tailwind templates, `.env` for the key, and a solid Checkpoint 2 journal entry.

Two pieces of **business logic you already wrote in `planner.py` are not wired into Flask** — they only existed in the old commented-out `main.py` flow:

1. **`select_group_and_workouts(goal, options_data, location_key)`** — picks a focus group using `GOAL_GROUP_WEIGHTS` + `build_group_weights` (e.g. "lose weight" biases toward cardio). `app.py` still calls `pick_focus_group(recent)` + `workouts_for_group(...)`, which **ignores the goal text** for picking.
2. **`load_last_workout()` / `save_workout(goal, plan_text)`** — `workouts/history.json` supports history, but **`app.py` never reads or writes it**.

Checkpoint 3 is about **activating that code**, polishing the repo for portfolio + Demo Day, and optionally deploying.

---

## Phase 1: Wire up smart goal-based focus picking

> **Handwrite the edits.** This is your planner logic — you should understand every line.

### Objective

Use `select_group_and_workouts` in `/generate` so focus-group choice reflects keywords in the user's goal (while still avoiding repeats from `recent_focus_groups`).

### Instructions

- [ ] Open `app.py`
- [ ] Change imports: remove `load_previous_groups`, `save_previous_groups`, `pick_focus_group`, and `workouts_for_group`. Add `select_group_and_workouts` from `planner`.
- [ ] In `/generate`, after `is_valid_goal` passes and you have `options = load_options()`, **replace** the block that does `recent = load_previous_groups()`, `group = pick_focus_group(recent)`, `workouts = workouts_for_group(...)`, and the later `recent.append(group)` + `save_previous_groups(recent[-2:])` with **one** call:

```python
group, workouts = select_group_and_workouts(goal, options, location_key)
```

- [ ] Remove any now-unused imports from `planner`
- [ ] Run the app and submit a few times with goal **"lose weight"** vs **"build muscle"** — over several tries you should see `cardio` / `legs_core` show up more often for weight-loss wording (not every time — it's weighted random)

### Hints

`select_group_and_workouts` already:

- loads recent groups from `workouts/history.json`
- filters out groups in `recent_focus_groups` (or resets if all are used)
- applies `build_group_weights` from `GOAL_GROUP_WEIGHTS`
- picks with `random.choices(..., weights=...)`
- saves the updated `recent_focus_groups` via `save_previous_groups`

So **do not** duplicate `load_previous_groups` / `save_previous_groups` in `app.py` for the focus group — that would double-save or fight the planner.

> **Optional — get help from your agent:**
>
> ```text
> Read my app.py and planner.py. After Phase 1, does /generate still
> update recent_focus_groups exactly once per request? Trace the call
> path into select_group_and_workouts. Don't change my code — explain.
> ```

---

## Phase 2: Show "Your last workout" on the home page

> **Mixed.** Small `app.py` changes + template.

### Objective

After each successful generation, persist the plan to history. On `/`, show the most recent workout in a collapsible block so users see context before submitting again.

### Instructions

- [ ] In `app.py`, import `load_last_workout` and `save_workout` from `planner`
- [ ] In the `/` route, call `last_workout = load_last_workout()` and pass `last_workout=last_workout` to `render_template("home.html", ...)`
- [ ] In `/generate`, after you have `plan = response.output_text.strip()`, call `save_workout(goal, plan)` before `return render_template("result.html", ...)`
- [ ] In `templates/home.html`, **above** the error block / form, add a `<details>` section that only renders when `last_workout` is truthy — show the text inside `<pre class="...">` with `whitespace-pre-wrap` so line breaks from `load_last_workout()` display cleanly

### Hints

**`/` route shape:**

```python
@app.route("/")
def home():
    last = load_last_workout()
    return render_template("home.html", last_workout=last)
```

**Fragment for `home.html` (inside `{% block content %}`, before `{% if error %}`):**

```html
{% if last_workout %}
<details class="mb-6 rounded-xl border border-slate-200 bg-white p-4 text-sm">
  <summary class="cursor-pointer font-semibold text-slate-800">
    Your last workout
  </summary>
  <pre class="mt-3 whitespace-pre-wrap font-sans text-slate-700 text-xs leading-relaxed">{{ last_workout }}</pre>
</details>
{% endif %}
```

If `last_workout` is always empty, check `workouts/history.json` — `save_workout` must run after a successful OpenAI response.

> **Optional — get help from your agent:**
>
> ```text
> My home.html shows last_workout but the <pre> is one long line. Fix
> only the CSS/classes so newlines from load_last_workout() render.
> Don't change planner.py.
> ```

---

## Phase 3: Loading state on submit

> **Agent-assisted OK.** View-only + tiny inline JS.

### Objective

While OpenAI responds, the user should see feedback — disabled button + "Generating…" so they don't double-submit.

### Instructions

- [ ] On `<form method="post" action="/generate" ...>`, add `onsubmit="..."` (or a `<script>` at the bottom of `home.html`) that:
  - finds the submit button
  - sets `disabled = true`
  - replaces button label with "Generating…" and optionally adds a small Tailwind spinner (inline `svg` or `animate-spin` border)
- [ ] Test: slow network or a heavy prompt — button should stay disabled until the new page loads

### Hints

Minimal pattern — add to the `<form>` tag:

```html
<form
  method="post"
  action="/generate"
  class="space-y-6"
  onsubmit="this.querySelector('button[type=submit]').disabled=true; this.querySelector('button[type=submit]').textContent='Generating…';"
>
```

Polish version: keep the button text in a `<span id="gen-label">` and toggle a sibling spinner `hidden` class.

> **Optional — get help from your agent:**
>
> ```text
> Add a Tailwind spinner next to my submit button in home.html. On
> form submit, hide the original label, show spinner, disable button.
> Keep method/action unchanged.
> ```

---

## Phase 4: Portfolio polish (README, spec, `main.py`, `.env.example`, screenshot)

> **Handwrite the prose** in spec/README/journal; file moves are straightforward.

### Objective

Make the GitHub repo demo-ready: no dead `main.py`, documented env vars, accurate spec, visual proof in README.

### Instructions

- [ ] **Delete `main.py`** — it is entirely commented-out CLI code; your entry point is Flask (`app.py`)
- [ ] Create **`.env.example`** at repo root (committed, empty values):

```text
OPENAI_API_KEY=
```

- [ ] Rewrite **`README.md`**: project title, one paragraph on what it does, **How to run** with exact commands:

```bash
uv sync
uv run flask --app app run --debug
```

Mention: open `http://127.0.0.1:5000`, need `OPENAI_API_KEY` in `.env` (not committed). Link to `project.spec.md` / `project.journal.md`.

- [ ] Add a **screenshot**: create `docs/` folder, add `docs/screenshot.png` (or `.jpg`) of the app at phone width — reference it in README with a relative markdown image
- [ ] Update **`project.spec.md`**: fix typo "Planenr" → "Planner"; rewrite **Must have (MVP)** to match reality — mobile-friendly Flask app, goal + location, curated workouts from JSON, memory for last two focus groups, goal-weighted selection (once Phase 1 is done), last-workout history (Phase 2), input validation, Tailwind. Move "web app at the end" out of stretch if you've shipped it

### Hints

**Commit message idea:**

```
checkpoint 3: wire select_group_and_workouts + history UI, polish README
```

> **Optional — get help from your agent:**
>
> ```text
> Draft README.md for my GPT Workout Planner: uv sync, uv run flask,
> .env for OPENAI_API_KEY, link docs/screenshot.png. I'll paste my
> real screenshot path after.
> ```

---

## Phase 5: Checkpoint 3 journal + Demo Day plan

> **Handwrite.**

### Objective

Align `project.journal.md` with what you built and lock a 2–5 minute demo outline.

### Instructions

- [ ] Fill **Checkpoint 3** in `project.journal.md`: what you did (Phases 1–4), what you wrote yourself vs. agent-assisted, one thing you're proud of (e.g. goal-weighting finally live), one thing you'd do next (e.g. BMI from original stretch list)
- [ ] Add a **Demo Day plan** (bullets OK): one-sentence pitch → live walk (home → submit goal → result → back) → mention last-workout panel → close with "proud of" + "next week"

### Hints

Demo flow that shows off your work:

1. Reset or note `workouts/history.json` if you want a clean "first visit"
2. Submit **"build muscle"** + gym — show plan + focus on screen
3. Return home — last workout appears in `<details>`
4. Submit **"lose weight"** — show different vibe / focus bias over a couple runs

---

## Phase 6 (Bonus): Deploy to Render

> **Only after Phases 1–5 work locally.** Skip if time is short.

### Objective

Public URL for Demo Day; same pattern as other class guides.

### Instructions

- [ ] Read the full [Render Deploy Guide](../../resources/render-deploy.guide.md)
- [ ] Run `uv add gunicorn` so the production server is listed in `pyproject.toml` and `uv.lock`
- [ ] Add `render.yaml` with `sync: false` for `OPENAI_API_KEY`
- [ ] Commit `pyproject.toml`, `uv.lock`, and `render.yaml`
- [ ] In Render dashboard, set env vars; test live URL end-to-end
- [ ] **Caveat:** free tier disk resets on deploy — `workouts/history.json` may reset; fine for demo — submit a fresh plan before presenting

### Hints

**`render.yaml` sketch:**

```yaml
services:
  - type: web
    name: gpt-workout-planner
    env: python
    plan: free
    buildCommand: uv sync --frozen && uv cache prune --ci
    startCommand: uv run gunicorn app:app
    autoDeploy: true
    envVars:
      - key: OPENAI_API_KEY
        sync: false
```

> **Optional — get help from your agent:**
>
> ```text
> My Render deploy builds but crashes on start. Here's my render.yaml
> and startCommand. What's wrong?
> ```

---

## Checkpoint 3 Readiness

By **Friday, May 1st at 3:30pm (Demo Day)**:

- [ ] `/generate` uses `select_group_and_workouts` — no duplicate focus-group persistence in `app.py`
- [ ] `save_workout` runs after each successful plan; `/` shows `last_workout` when present
- [ ] Form submit shows loading/disabled state (Phase 3)
- [ ] `main.py` deleted; `.env.example` committed; `.env` still gitignored
- [ ] `README.md`: real title, paragraph, run commands, env vars, screenshot in `docs/`
- [ ] `project.spec.md` MVP matches the shipped app
- [ ] Checkpoint 3 + demo plan filled in `project.journal.md`
- [ ] Everything committed and pushed
- [ ] (Bonus) Live Render URL + note about history file on redeploy

---

## Helpful Resources

- [Checkpoint 3 Instructions](../../projects/final-project-checkpoint-3.project.md)
- [Render Deploy Guide](../../resources/render-deploy.guide.md)
- [Tailwind CSS — utility reference](https://tailwindcss.com/docs/utility-first)
- [Flask — Application setup](https://flask.palletsprojects.com/en/stable/tutorial/factory/)


---

## Backlinks

The following sources link to this document:

- [April 24 -- Checkpoint 3 (Wire up hidden features + polish)](/unit-3/project-paths/duward-a/duward-a.path.llm.md)
