Miranda's Project Guide
Project: PlainText Pal Category: Web Development (Flask) + Anthropic Last updated: April 23
Note: This guide reflects the latest state of your project repo. It may not match the most up-to-date version if you've worked since.
In class today we lined up five polish items for Checkpoint 3: load the API key from .env, add a loading state on Analyze, show a "Suggested rewrite" textarea, have the LLM explain each readability score, and (bonus) let the user pick a target audience.
Phase 1: Load the API key from .env
Objective
Stop depending on a shell env var. Keep ANTHROPIC_API_KEY in a gitignored .env file that Flask loads at startup.
Instructions
Hints
.env.example (committed):
ANTHROPIC_API_KEY=
Top of app.py:
from dotenv import load_dotenv
load_dotenv()
from flask import Flask, render_template, request, flash, redirect, url_for
from pal import get_suggestions
from stats import get_readability_stats
Order matters — load_dotenv() must run before pal is imported, because pal.py reads os.getenv("ANTHROPIC_API_KEY") at call time and the Anthropic SDK is configured from that.
Phase 3: "Suggested rewrite" textarea
Objective
Below the suggestions, show a read-only textarea containing the same text rewritten with those suggestions applied. Same LLM call, just an extra JSON field.
Instructions
Hints
pal.py — update prompts:
system_prompt = (
"You classify writing intent and provide plain-language accessibility suggestions. "
"Return JSON only with keys: intent (string), suggestions (array of strings), "
"rewrite (string). "
"Intent examples include: scientific, creative, informative, persuasive, instructional, narrative."
)
user_prompt = (
"Analyze the text below and return only valid JSON.\n\n"
"Requirements:\n"
"1) intent: one short label\n"
"2) suggestions: 3-6 concise, specific ways to improve readability and accessibility\n"
"3) rewrite: the original text rewritten applying your suggestions, in plain language\n\n"
f"Text:\n{cleaned_text}"
)
pal.py — read the new field before the return:
rewrite = str(parsed.get("rewrite", "")).strip()
return {
"intent": intent,
"suggestions": cleaned_suggestions,
"rewrite": rewrite,
}
Also add "rewrite": "" to _fallback_response so the template never crashes.
templates/results.html — new section under the suggestions list:
<h2 class="h5 mt-4">Suggested rewrite</h2>
<textarea class="form-control" rows="8" readonly>{{ pal_results.rewrite }}</textarea>
Phase 4: LLM-written score explanations
Objective
Under each readability stat, show a one-sentence plain-language explanation written by the LLM that references the actual number (e.g. "Your Flesch score of 62 is about 8th-grade reading — most readers will follow it fine.").
Instructions
Hints
app.py — reorder the two calls:
stats = get_readability_stats(text)
pal_results = get_suggestions(text, stats)
pal.py — new signature + prompt addition:
def get_suggestions(text: str, stats: dict | None = None) -> dict[str, Any]:
...
stats_line = ""
if stats:
stats_line = (
"\n\nReadability stats for this text:\n"
f"- Flesch Reading Ease: {stats['flesch_ease']}\n"
f"- Average sentence length: {stats['avg_sentence_length']} words\n"
f"- Long sentences (>25 words): {stats['long_sentences']}\n"
f"- Complex words: {stats['complex_words']}\n"
)
system_prompt = (
"... (same as before) "
"Also return score_explanations: an object with keys flesch_ease, "
"avg_sentence_length, long_sentences, complex_words. Each value is "
"a single plain-language sentence explaining what this specific "
"number means for the reader."
)
user_prompt = (
"Analyze the text below and return only valid JSON.\n\n"
"Requirements:\n"
"1) intent: one short label\n"
"2) suggestions: 3-6 concise, specific ways to improve readability and accessibility\n"
"3) rewrite: the original text rewritten applying your suggestions, in plain language\n"
"4) score_explanations: one-sentence explanation per score"
f"{stats_line}\n"
f"Text:\n{cleaned_text}"
)
pal.py — read the new field before the return:
raw_explanations = parsed.get("score_explanations", {}) or {}
score_explanations = {
key: str(raw_explanations.get(key, "")).strip()
for key in ("flesch_ease", "avg_sentence_length", "long_sentences", "complex_words")
}
return {
"intent": intent,
"suggestions": cleaned_suggestions,
"rewrite": rewrite,
"score_explanations": score_explanations,
}
Add a matching empty score_explanations dict to _fallback_response.
templates/results.html — wrap each stat with its explanation:
<ul>
<li>
<strong>Flesch Reading Ease:</strong> {{ stats.flesch_ease }}
<div class="text-muted small">{{ pal_results.score_explanations.flesch_ease }}</div>
</li>
<li>
<strong>Average sentence length:</strong> {{ stats.avg_sentence_length }}
<div class="text-muted small">{{ pal_results.score_explanations.avg_sentence_length }}</div>
</li>
<li>
<strong>Long sentences:</strong> {{ stats.long_sentences }}
<div class="text-muted small">{{ pal_results.score_explanations.long_sentences }}</div>
</li>
<li>
<strong>Complex words:</strong> {{ stats.complex_words }}
<div class="text-muted small">{{ pal_results.score_explanations.complex_words }}</div>
</li>
</ul>
Phase 5 (Bonus): Audience selector
Objective
Let the user pick a target audience — General, Accessibility (screen readers), ADHD, Dyslexia, Aphantasia — and have the LLM tailor the suggestions and rewrite for that audience.
Instructions
Hints
templates/home.html — new form field above the button:
<div class="mb-3">
<label class="form-label" for="audience">Target audience</label>
<select class="form-select" id="audience" name="audience">
<option value="general">General</option>
<option value="accessibility">Accessibility (screen readers)</option>
<option value="adhd">ADHD</option>
<option value="dyslexia">Dyslexia</option>
<option value="aphantasia">Aphantasia</option>
</select>
</div>
pal.py — audience guidance dict + prompt injection:
AUDIENCE_GUIDANCE = {
"general": "A general adult reader.",
"accessibility": "Readers using screen readers — avoid visual-only cues, keep link text descriptive, prefer concrete verbs.",
"adhd": "Readers with ADHD — short chunks, front-load the key point, strong verbs, avoid long subordinate clauses.",
"dyslexia": "Readers with dyslexia — prefer short common words, break up walls of text, avoid dense punctuation.",
"aphantasia": "Readers with aphantasia — avoid visual metaphors; describe actions and facts literally.",
}
def get_suggestions(text, stats=None, audience="general"):
...
audience_hint = AUDIENCE_GUIDANCE.get(audience, AUDIENCE_GUIDANCE["general"])
user_prompt = (
"... (same prompt as Phase 4) "
f"\n\nTarget audience: {audience_hint}\n"
"Tailor suggestions and rewrite for this audience.\n"
f"{stats_line}\n"
f"Text:\n{cleaned_text}"
)
...
return {
"intent": intent,
"suggestions": cleaned_suggestions,
"rewrite": rewrite,
"score_explanations": score_explanations,
"audience": audience,
}
app.py — read and forward:
audience = request.form.get("audience", "general")
stats = get_readability_stats(text)
pal_results = get_suggestions(text, stats, audience)
return render_template(
"results.html",
title="Analysis Results",
text=text,
pal_results=pal_results,
stats=stats,
audience=audience,
)
templates/results.html — badge next to the intent:
<span class="badge text-bg-primary ms-1">{{ pal_results.intent }}</span>
<span class="badge text-bg-secondary ms-1">Audience: {{ audience }}</span>