Week 7 of 16

Build: Add Claude Generation to the Web App

Wire the Anthropic SDK into Flask — type a shot, click Generate, receive a production-ready prompt

Day 32 75 minutes Build

Day 32 of 80

What You're Building

The Generate Flow

A new section appears at the top of the Prompt Vault: a form with a shot description field and a platform selector. You hit "Generate with Claude." Python receives the form data, calls the Anthropic API, and the response comes back as a new variable in your template. The generated prompt appears on the page with a "Save This" button. Clicking Save adds it to prompts.json with one more POST.

You're combining everything from the last six weeks: Flask routes (Week 6), the Anthropic SDK (Week 4), and JSON storage via helpers.py (Week 5).

Step 1: Update app.py — Imports and Client

Add these lines near the top of app.py, after your existing imports:

app.py — updated imports Python
from flask import Flask, render_template, request, redirect
from helpers import load_prompts, save_prompts, validate_platform
import anthropic            # NEW
from dotenv import load_dotenv  # NEW

load_dotenv()             # loads ANTHROPIC_API_KEY from .env

app = Flask(__name__)
claude = anthropic.Anthropic()  # one client instance, reused across requests
load_dotenv() — Reads your .env file and puts its contents into environment variables. Must be called before anthropic.Anthropic(), which reads ANTHROPIC_API_KEY from the environment.

One client instance — You create claude once at module level, not inside the route function. This is more efficient: creating an API client involves some initialization work. Reusing the same object across requests is standard practice.

Step 2: Add the /generate Route

Add this route to app.py after your existing routes:

app.py — /generate route Python
@app.route("/generate", methods=["POST"])
def generate():
    shot = request.form.get("shot", "").strip()
    platform = request.form.get("platform", "Kling")

    if not shot:
        return redirect("/")

    message = claude.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=300,
        system="You are an expert AI video prompt engineer. You write precise, evocative prompts for AI video generation tools. Always include camera movement, lighting, and mood.",
        messages=[{
            "role": "user",
            "content": f"Write a production-ready {platform} AI video prompt for: {shot}. Include camera movement, lighting, and mood. Under 100 words. Return only the prompt text, no preamble."
        }]
    )

    generated = message.content[0].text

    prompts = load_prompts()
    counts = {}
    for p in prompts:
        counts[p["platform"]] = counts.get(p["platform"], 0) + 1

    return render_template(
        "index.html",
        prompts=prompts,
        generated=generated,
        gen_shot=shot,
        gen_platform=platform,
        counts=counts,
        all_count=len(prompts),
        active_filter="all"
    )
model="claude-sonnet-4-6" — Use the same model you've been using since Week 4. For prompt generation, Sonnet is fast and high quality. If you want to reduce API cost, claude-haiku-4-5 is cheaper and still excellent for this task.

System prompt strategy — The system prompt sets Claude's role and output style. "Return only the prompt text, no preamble" prevents Claude from adding "Here is your prompt:" before the actual content. Explicit instructions about format produce cleaner output.

message.content[0].text — The Anthropic API returns a list of content blocks. For standard text responses, content[0] is the text block. This is identical to how you called Claude in Week 4's CLI scripts.

Passing gen_shot and gen_platform — The Save form (coming next) needs these to POST a complete record back. Without them, the template can't build the Save form.

Step 3: Add the Generate Form to index.html

Add this block to templates/index.html — place it before the prompt grid but after the filter bar:

templates/index.html — generate section HTML + Jinja2
{# ── Generate with Claude ── #}
<div class="generate-section">
  <h2>Generate with Claude</h2>
  <form method="POST" action="/generate">
    <div class="gen-row">
      <input type="text" name="shot"
             placeholder="Describe the shot (e.g. aerial over misty golf course at sunrise)..."
             required>
      <select name="platform">
        <option value="Kling">Kling</option>
        <option value="Runway">Runway</option>
        <option value="Veo">Veo</option>
      </select>
      <button type="submit" class="btn-generate">Generate →</button>
    </div>
  </form>

  {# Show the generated result if we have one #}
  {% if generated %}
    <div class="generated-result">
      <div class="result-header">
        <span class="platform-tag platform-{{ gen_platform | lower }}">{{ gen_platform }}</span>
        <span class="result-label">Generated for: <em>{{ gen_shot }}</em></span>
      </div>
      <p class="generated-text">{{ generated }}</p>

      {# Save This form — hidden fields carry all three values #}
      <form method="POST" action="/save-generated">
        <input type="hidden" name="platform" value="{{ gen_platform }}">
        <input type="hidden" name="shot" value="{{ gen_shot }}">
        <input type="hidden" name="prompt" value="{{ generated }}">
        <button type="submit" class="btn-save">Save This Prompt</button>
      </form>
    </div>
  {% endif %}
</div>
{% if generated %} — On the home page, generated is never passed to the template, so this block is invisible. After a /generate POST, generated holds the prompt text and the result section appears.

Hidden inputs<input type="hidden"> is how you pass data through a form that the user doesn't need to see or edit. The browser includes hidden inputs in the POST body just like visible ones. This is the standard way to carry context through a form chain.

Why a second form (not a link)? Because saving the prompt changes data on the server — that's a POST action, not a GET. Links always create GET requests. When an action changes data, always use a form with method="POST".

Step 4: Add the /save-generated Route

app.py — /save-generated route Python
@app.route("/save-generated", methods=["POST"])
def save_generated():
    prompts = load_prompts()
    prompts.append({
        "platform": request.form["platform"],
        "shot": request.form["shot"],
        "prompt": request.form["prompt"]
    })
    save_prompts(prompts)
    return redirect("/")
Simple and clean — the same logic as add_prompt() but without the validation step (the prompt came from Claude, not user-typed input, so it's already in the right shape). Load, append, save, redirect. The pattern never changes.

Run It

Terminal
$ python app.py
 * Running on http://127.0.0.1:5000
 * Debug mode: on
# Open http://localhost:5000
# Type: "aerial drone shot over misty Scottish golf course at sunrise"
# Select: Kling
# Click: Generate →
# Wait ~2 seconds for the Claude API response
# The generated prompt appears below the form
# Click "Save This Prompt" — it appears in your vault cards
You're Reusing Everything

Look at what just happened. The generate() route calls load_prompts() and save_prompts() from helpers.py — the same functions you wrote in Week 5. It calls the Anthropic SDK the same way you did in your Week 4 CLI scripts. It uses Flask patterns from Week 6. Nothing was wasted. Every piece you built is still in play. This is how good software accumulates.

Debugging Tips

Common Issues

API key error — Make sure .env is in your prompt-vault/ folder (same directory as app.py) and contains ANTHROPIC_API_KEY=sk-ant-.... The load_dotenv() call at the top of app.py must happen before anthropic.Anthropic().

Generated result disappears on refresh — Correct behavior. Refreshing re-runs the GET / route, which doesn't pass generated. The result only shows immediately after generation. Save it if you want to keep it.

Template variable not found — If Jinja2 throws a UndefinedError for generated, it means the home route doesn't pass it. Jinja2's {% if generated %} handles this: if generated isn't passed, treat it as falsy. This works in Flask's default Jinja2 configuration.

End of Day Checklist

Tomorrow — Search

Tomorrow you add a search bar. Type "golf" and see only golf-related prompts. Type "kling" and see only Kling prompts. It's a GET route, a list comprehension, and about 20 new lines of HTML. One of the faster build days in the course.