WriteWise is an AI-powered writing platform built for job seekers and professionals. It combines general writing tools (like QuillBot) with job-focused intelligence, powered by the OpenAI API.
WriteWise is a standalone, self-hosted app: a React frontend talks to a Python FastAPI backend, and the backend calls OpenAI. There is no login and no database.
Browser → React frontend (localhost:3000) → FastAPI backend (localhost:8001) → OpenAI API
Fix grammar, rewrite content, generate emails, paraphrase text, summarize documents, and more — all in one place.
| Mode | What it does |
|---|---|
| Auto | AI detects your intent and acts on it automatically |
| Grammar | Fixes grammar and spelling without changing your voice |
| Drafts professional emails and personalized job application emails | |
| Tone | Changes how the text sounds: Professional, Casual, Friendly, Diplomatic, Formal, Confident, Persuasive, or Empathetic |
| Rewrite | Improves how the text is structured and expressed, with an optional goal: Clear & Concise, More Direct, More Polished, More Persuasive, Simplify, or Keep My Style |
| Paraphrase | Rewrites with the same meaning in 5 styles: Standard, Fluency, Formal, Simple, Creative |
| Summarize | Extracts key points as a short summary or bullet points |
| Expand / Shorten | Adds depth or trims text down to what matters |
| Humanize | Removes AI-like phrasing and makes text sound naturally written |
| Prompt | Turns a rough, natural-language message into a clear, LLM-ready request you can paste into ChatGPT, Claude or Gemini, keeping your meaning. See Prompt Mode |
- Personalization Panel — Enter your experience, target role, and skills. The AI uses this to personalize outputs, especially emails and rewrites. Saved in your browser (
localStorage) only, and opened automatically in Email mode while empty - Output Variations — Generate 3 versions at once: Professional, Confident, and Friendly
- Why You're a Good Fit — For job postings, 2-3 bullets that connect background you supplied to the role's requirements. Only shown when you have given your background
- AI Self-Check — The AI's own scores for Clarity and Professionalism (1-10), plus Personalization when a profile was given, and one improvement suggestion. Scores the model does not return validly are left out, never filled in
- Grounded job emails — The AI only states experience, skills and qualifications you supplied; a job posting's requirements are never presented as your background, and names or placeholders like
[Your Name]are not invented - Facts kept on tone changes — Numbers, dates, deadlines, request counts and concrete requests are kept when the tone changes
- Quick Templates — One-click starters for Job Application, Follow-up Email, Cold Outreach, and Referral Request
- Regenerate — Re-run the same request for a fresh output
- Copy to Clipboard — One-click copy with fallback support
- Dynamic placeholders and context hints per mode
- Dynamic loading messages ("Paraphrasing...", "Summarizing...", "Humanizing..."...)
- Button label changes contextually ("Expand" or "Shorten")
- "Try with Example" CTA auto-fills a real job posting and switches to Email mode
- No em dashes or double hyphens in any AI output — enforced at the system prompt level
| Layer | Technology |
|---|---|
| Frontend | React 18, Create React App 5 + CRACO, Tailwind CSS, Shadcn UI, Lucide React |
| Backend | FastAPI, Python |
| AI | OpenAI Responses API via the official openai Python SDK (model set by OPENAI_MODEL, default gpt-5.6-terra) |
| Storage | None. Requests are stateless and nothing is persisted |
| Fonts | IBM Plex Sans, JetBrains Mono |
writewise/
├── api/
│ ├── index.py # Vercel entrypoint; re-exports the FastAPI app from backend/server.py
│ └── requirements.txt # Runtime Python dependencies (used by Vercel and by backend/requirements.txt)
├── backend/
│ ├── server.py # FastAPI app, all writing modes, prompt construction, OpenAI call
│ ├── prompt_mode.py # Prompt mode: its own instructions, URL protection, output cleanup
│ ├── secret_redaction.py # Removes obvious API keys, tokens and passwords (Prompt mode)
│ ├── requirements.txt # Runtime deps (via api/requirements.txt) + uvicorn and test tools
│ ├── tests/ # Mocked API tests; tests/integration/ holds opt-in live OpenAI tests
│ ├── .env.example # Template for backend/.env
│ └── .env # OPENAI_API_KEY, OPENAI_MODEL, CORS_ORIGINS (not committed)
│
├── frontend/
│ ├── src/
│ │ ├── App.js # Main app, state, mode logic
│ │ ├── App.css # All styles (dark theme)
│ │ └── components/
│ │ ├── QuickTemplates.jsx # 4 quick-start template chips
│ │ ├── PersonalizationPanel.jsx # Collapsible experience/role/skills inputs
│ │ ├── TabOptions.jsx # Mode-specific sub-options (pills)
│ │ └── OutputSection.jsx # Output cards, scores, why-good-fit
│ ├── public/
│ │ └── images/bg-texture.png # Background texture
│ ├── package.json
│ ├── package-lock.json
│ ├── .env.example # Template for frontend/.env
│ └── .env # REACT_APP_BACKEND_URL (not committed)
│
└── README.md
- Node.js 20.x (tested with 20.19.4) and npm 10.x (tested with 10.8.2)
- Python 3.10+ (tested with 3.12)
- An OpenAI API key
No database is required.
git clone https://github.com/tymepas/WriteWise_Project.git
cd WriteWise_Projectcd backend
python -m venv .venv
# Windows: .venv\Scripts\activate
# macOS/Linux: source .venv/bin/activate
pip install -r requirements.txtCopy backend/.env.example to backend/.env and fill in your key:
OPENAI_API_KEY=your_openai_api_key_here
OPENAI_MODEL=gpt-5.6-terra
CORS_ORIGINS=http://localhost:3000Start the backend (from the backend directory):
uvicorn server:app --host 127.0.0.1 --port 8001 --reloadCheck it is running: http://localhost:8001/api/ should return {"message": "WriteWise API"}.
In a second terminal:
cd frontend
npm installCopy frontend/.env.example to frontend/.env:
REACT_APP_BACKEND_URL=http://localhost:8001Start the development server:
npm startThe app will be available at http://localhost:3000.
To create a production build in frontend/build/:
npm run buildREACT_APP_BACKEND_URL is baked into the bundle at build time. When it is not set, the frontend calls the API on its own origin (/api/...), which is how the Vercel deployment works.
With the backend virtual environment active, from the repository root:
pytest backend/testsThis runs the mocked tests (backend/tests/test_api_mocked.py and backend/tests/test_prompt_mode.py), which use a fake OpenAI client: no network calls and no API cost. The live tests in backend/tests/integration/ call a running backend and the real OpenAI API, so they are skipped unless you enable them:
WRITEWISE_LIVE_TESTS=1 REACT_APP_BACKEND_URL=http://127.0.0.1:8001 pytest backend/tests/integrationTo run only the Prompt mode tests, use pytest backend/tests/test_prompt_mode.py (mocked) or add -s to the live command with backend/tests/integration/test_prompt_mode_live.py to print each fictional golden input and its output for review.
WriteWise deploys as a single Vercel project: the React build is served as static files and the FastAPI app runs as a Python function on the same domain.
/ → frontend/build (static React app)
/api/* → api/index.py → FastAPI app from backend/server.py
- Import the GitHub repository into Vercel and keep the Root Directory as the repository root.
vercel.jsonsets the install command, build command, output directory and the/api/*rewrite, so no build settings need to be changed in the dashboard. - Add these Environment Variables in the Vercel project settings:
OPENAI_API_KEY: your OpenAI key (mark it as sensitive)OPENAI_MODEL:gpt-5.6-terraCORS_ORIGINS: your deployment URL, e.g.https://writewise.vercel.app(the app itself is same-origin and does not need CORS; this only limits other websites)
- Do not set
REACT_APP_BACKEND_URLon Vercel, so the frontend calls/apion the same domain. - Deploy.
The Python runtime version is pinned by api/.python-version (3.12), and the Python function installs only api/requirements.txt.
/api/generate is public and every request spends OpenAI credit. The app itself bounds the cost of a single request (the input limits above and the 6,000 output-token cap), but it does not rate limit: Vercel runs the API on many short-lived instances, so a counter in application memory would not be a real global limit. Use these two platform controls instead:
-
Vercel Firewall rate limit (available on Hobby: 1 rate-limit rule per project). In the Vercel dashboard, open the project → Firewall → Configure → + New Rule:
- Name:
Limit /api/generate - If: Request Path equals
/api/generate - Then: Rate Limit, Fixed Window, Time Window
60s, Request Limit10, key IP, action Default (429) - Save Rule, then Review Changes → Publish
You can start with the Log action to see real traffic first. Vercel counts rate limits per region, so a client spread across regions can exceed the limit somewhat. The frontend shows "Too many requests. Please wait a minute and try again." when this rule responds with 429.
- Name:
-
OpenAI spending limit. Use a dedicated OpenAI project for WriteWise and set a monthly budget and usage alerts on it in the OpenAI dashboard, so a traffic spike cannot run up an unlimited bill.
Generate writing output based on the selected mode.
Request body:
{
"mode": "email",
"input": "Software Engineer role at TechCorp. Need 3 years React experience.",
"context": "Friendly and direct tone",
"experience": "4 years React developer",
"target_role": "Frontend Engineer",
"skills": "React, TypeScript, Node.js",
"variations": false,
"paraphrase_mode": "standard",
"summary_type": "short"
}| Field | Type | Required | Description |
|---|---|---|---|
mode |
string | Yes | One of: auto, grammar, email, tone, rewrite, paraphrase, summarize, expand, shorten, humanize, prompt |
input |
string | Yes | The text to process |
context |
string | No | Extra instructions (e.g. "Keep it concise for a client email") |
experience |
string | No | User's work experience (for personalization) |
target_role |
string | No | Job role being targeted |
skills |
string | No | Comma-separated skills |
variations |
boolean | No | If true, returns 3 variations instead of 1 |
paraphrase_mode |
string | No | standard, fluency, formal, simple, creative |
summary_type |
string | No | short or bullets |
tone |
string | No | Tone mode only: professional, casual, friendly, diplomatic, formal, confident, persuasive, empathetic. If omitted, the tone is taken from context (default professional) |
rewrite_goal |
string | No | Rewrite mode only: clear_concise, more_direct, more_polished, more_persuasive, simplify, keep_style. If omitted, the default rewrite is used |
Limits: input 12,000 characters, context 2,000, experience 1,000, target_role 300, skills 1,000. Longer values are rejected, never truncated.
paraphrase_mode, summary_type, tone and rewrite_goal are case-insensitive. A missing or empty value uses the default; any other value not in the lists above is rejected with a 400.
Response:
{
"output": "Subject: Frontend Engineer Application...",
"variations": null,
"mode": "email",
"detected_mode": null,
"why_good_fit": [
"4 years of React experience directly matches the job requirement",
"TypeScript proficiency aligns with their modern frontend stack"
],
"evaluation": {
"clarity": 9,
"professionalism": 9,
"personalization": 8,
"suggestion": "Consider adding a specific project or achievement to stand out."
}
}why_good_fitisnullunless the input is a job posting and the user supplied their own background.evaluationisnullif the model did not return valid 1-10 scores.evaluation.personalizationisnullwhen noexperience,target_roleorskillswere sent.evaluation.suggestionmay benull.detected_modeisemailorrewritein Auto mode, otherwisenull.secrets_redacted(Prompt mode only) is the number of possible secrets (API keys, tokens, passwords) removed from the text before it was sent to OpenAI, ornullwhen there were none.- In
promptmode,context,experience,target_role,skillsandvariationsare ignored, andvariations,why_good_fitandevaluationare alwaysnull.
Errors are returned as {"detail": "<message safe to show the user>"}; internal error text is only logged on the server.
| Status | When |
|---|---|
| 400 | Invalid request: empty or too-long text, unknown mode or option, malformed JSON |
| 422 | The result would be too long to generate in full (try shorter text or turn off variations), or the text was refused by the AI service |
| 429 | The OpenAI account is rate limited, or the Vercel Firewall rate limit was hit |
| 500 | The API key or model is not configured correctly, or an unexpected error |
| 502 / 503 / 504 | The AI service failed, could not be reached, or timed out |
Each request to OpenAI is capped at 6,000 output tokens, with a 90 second timeout and one retry.
Health check. Returns { "message": "WriteWise API" }.
| Variable | Description |
|---|---|
OPENAI_API_KEY |
Your OpenAI API key. Required. Used only by the backend and never sent to the browser |
OPENAI_MODEL |
OpenAI model to use. Optional, defaults to gpt-5.6-terra |
CORS_ORIGINS |
Allowed frontend origins (comma-separated), e.g. http://localhost:3000 |
| Variable | Description |
|---|---|
REACT_APP_BACKEND_URL |
Backend API base URL, e.g. http://localhost:8001. Do not put any API keys in the frontend .env: everything in it is visible in the browser |
.env files are git-ignored; only the .env.example templates are committed.
WriteWise was originally generated on the Emergent platform, where the backend called Anthropic's claude-sonnet-4-5-20250929 through Emergent's emergentintegrations wrapper and logged request metadata to MongoDB. The standalone version calls OpenAI directly with the official SDK, keeps the same prompts, request/response shape and output parsing, and stores nothing. The backend asks OpenAI for JSON-mode output so the model's response always parses into the structure the frontend expects. Output wording may differ from the original model.
Detects the best mode automatically from your input. Supports detection of: grammar errors, email intent, tone requests, paraphrase needs, summarization, and humanization.
If input is a job posting, generates a personalized job application email. If input is rough text, polishes it into a professional email. Always includes a subject line. Uses your experience, target role, and skills for personalization.
- Standard — Natural rewrite with varied wording
- Fluency — Smooth, easy-to-read version
- Formal — Professional and academic register
- Simple — Plain language, shorter sentences
- Creative — Inventive rewriting with fresh phrasing
- Short Summary — 2 to 3 sentence overview
- Bullet Points — 4 to 6 key points
Write naturally. Keep your meaning. Prompt mode transforms a rough, natural-language message into a clear, concise, LLM-ready request while preserving your original intent. You type the way you think, copy the result, paste it into ChatGPT, Claude, Gemini or another assistant, and carry on the conversation. It is not a prompt-engineering tool: the result reads like a clear message from you, not a template.
Example (fictional):
You type: hey i have a python script which works fine when i test it with a csv around 5000 rows but when i use around 500000 rows it becomes really slow and sometimes memory usage goes very high i already tried removing some columns but it didnt make much difference please explain what could be causing this and what should i check first
WriteWise returns: I have a Python script that works fine with a CSV of around 5,000 rows, but with around 500,000 rows it becomes very slow and memory usage sometimes gets very high. I already tried removing some columns, but it did not make much difference. Please explain what could be causing this and what I should check first.
Core rule: clarify and organize, never reinterpret.
- It fixes grammar, removes filler and repetition, and orders scattered thoughts. Several distinct requests can become a short list; a simple request stays one paragraph.
- It keeps every question, fact, number, date, constraint, decision, exclusion ("don't include pricing"), what you already tried, your uncertainty, references to files, and links.
- Each part stays the same kind of sentence: a statement is never turned into a question or a belief (no added "right?" or "I believe"), and a question stays a question.
- Technically significant tokens are kept exactly as typed, character for character: code, commands, environment variables, file paths, identifiers, package names, versions, config keys and shorthand ("k8s", "gh actions",
kubectl rollout restart deploy/api-gateway,MAX_RETRIES=5,config.yaml). They are never expanded or re-cased. - Ordinary words and obvious brand names get normal proofreading ("antropic" → "Anthropic", "vercel" → "Vercel", "claude code" → "Claude Code", "power bi" → "Power BI"), but an abbreviation is never expanded and a term is never swapped for a more formal one.
- When you correct yourself ("no wait, first..."), only the final version is kept.
- It keeps instructions to the assistant separate from content the assistant should write for someone else.
- It never answers or performs the request, never claims to have opened a link or file, and never adds a persona ("You are an expert..."), template sections, requirements, questions or advice. Long messages are reorganized, not summarized.
How it differs from the other modes: Grammar only fixes errors, and Rewrite or Paraphrase polish text for a human reader. Prompt mode clarifies a request for an AI reader and is stricter about not adding or dropping anything. It has no style options, extra-context field or variations. In Prompt mode, Try an example fills the editor with a fictional rough message (a different one each click).
How it works:
- It uses its own instructions (
backend/prompt_mode.py) with the same OpenAI client, model and error handling as the other modes. - Links are replaced by placeholders such as
⟦URL_1: docs.example.com⟧before the text is sent and put back where the model placed them, so every URL comes back byte for byte. The model sees only each link's website, never the path or query. - If the model drops, repeats or invents a link placeholder, WriteWise asks once more with a correction. If that also fails, it returns an error instead of guessing where a link belongs.
- Wrappers the model sometimes adds (code fences, "Refined prompt:" labels, surrounding quotes) are removed, and an empty result is reported as an error.
Limitations:
- Intent preservation depends on the model. The golden tests (
backend/tests/prompt_mode_cases.py, all fictional) catch clear failures, but outputs deserve a quick read before you send them. - Links are validated deterministically (each one present once, none invented), but whether the sentence around a link describes it correctly depends on the model, which sees only the website.
- A message whose links are mishandled twice in a row returns an error; trying again usually works.
Before a Prompt mode request reaches OpenAI, the backend replaces obvious credentials with [REDACTED SECRET]:
- OpenAI-style
sk-keys, AWS access key IDs, GitHub, Google, Slack and Stripe keys, JWTs andBearertokens - Private key blocks
- Assignments such as
OPENAI_API_KEY=...orpassword: ...whose value contains a digit or is 16+ characters long
The UI shows a warning when something was removed. Only the number and kind of secrets are logged, never the values. In this version the other modes are unchanged and do not redact. This is a basic safety net, not a complete secret scanner: unusual formats and secrets written as plain words are not detected, and the generic assignment rule skips text inside URLs so query strings are never altered.
- Dark theme (
#09090Bbackground) - IBM Plex Sans for body text
- JetBrains Mono for labels, badges, and code-style UI
- Scrollable tab row for all 10 modes
- Minimal, distraction-free single-page layout
- Fade-in animations on output render
- Animated score bars on evaluation display
- Generation history with saved sessions
- Word and character count on the textarea
- Download output as
.txt - LinkedIn message quick template
- Thank-you note template
- Export as
.docx - Share output via link
MIT