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:
select_group_and_workouts(goal, options_data, location_key)— picks a focus group usingGOAL_GROUP_WEIGHTS+build_group_weights(e.g. "lose weight" biases toward cardio).app.pystill callspick_focus_group(recent)+workouts_for_group(...), which ignores the goal text for picking.load_last_workout()/save_workout(goal, plan_text)—workouts/history.jsonsupports history, butapp.pynever 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
group, workouts = select_group_and_workouts(goal, options, location_key)
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_weightsfromGOAL_GROUP_WEIGHTS - picks with
random.choices(..., weights=...) - saves the updated
recent_focus_groupsviasave_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:
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.pychanges + 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
Hints
/ route shape:
@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 %}):
{% 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:
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
- finds the submit button
- sets
disabled = true - replaces button label with "Generating…" and optionally adds a small Tailwind spinner (inline
svgoranimate-spinborder)
Hints
Minimal pattern — add to the <form> tag:
<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:
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
OPENAI_API_KEY=
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.
Hints
Commit message idea:
checkpoint 3: wire select_group_and_workouts + history UI, polish README
Optional — get help from your agent:
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
Hints
Demo flow that shows off your work:
- Reset or note
workouts/history.jsonif you want a clean "first visit" - Submit "build muscle" + gym — show plan + focus on screen
- Return home — last workout appears in
<details> - 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
Hints
render.yaml sketch:
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:
My Render deploy builds but crashes on start. Here's my render.yaml and startCommand. What's wrong?