Here's how everything connects together:
Basically the flow is: user types in browser → frontend sends to backend → backend calls gemini → gemini sends back json → backend sends to frontend → frontend shows results
The frontend is built with React + TypeScript using Vite. The main file is App.tsx and it does a bunch of stuff:
State Management:
inputText- stores what the user typestone- which tone they picked (Professional, Friendly, Concise, or More Polite)loading- shows spinner while waiting for apierror- displays error messages if something goes wrongresult- stores the response from the backendshowValidation- for inline validation errorscopied- tracks if they copied to clipboard
User Interactions:
- Textarea for entering the message
- Radio buttons for selecting tone (took me a while to style these nicely)
- Submit button that calls the api
- Keyboard shortcut - ctrl+enter or cmd+enter to submit (this was a nice touch)
- Copy button for the rewritten message
API Calls:
- Sends POST request to
http://localhost:3000/api/message - Body is
{ text: string, tone: string } - Handles errors (400, 500, network failures)
- Shows loading state while waiting
UI Rendering:
- Input form with textarea and tone selector
- Results section that only shows when we have data:
- Side by side comparison (original on left, rewritten on right)
- Analysis card with:
- Perceived tone (what the ai thinks the original sounds like)
- Clarity score out of 100
- Risk flags (potential issues, or "no risks" if empty)
- Explanation card with bullet points about what changed
I used inline styles instead of a separate css file cause it keeps everything in one place and makes it easier to see what styles apply to what. Plus it's simpler for a small project like this.
The backend is a Node.js Express server in server/index.js. It's pretty straightforward:
Middleware Setup:
- CORS enabled for
http://localhost:5173(my vite dev server) - JSON body parsing so i don't have to parse manually
- dotenv to load the api key from .env file
Main Endpoint: /api/message (POST)
This is where all the magic happens. Here's what it does step by step:
-
Input Validation:
- Checks if text exists and isn't empty (returns 400 if invalid)
- Validates tone is one of the 4 allowed values
- Defaults to "Professional" if they send something weird
-
Build the Prompt:
- Gemini doesn't use separate system/user messages like openai
- So i combine everything into one prompt
- Includes the json schema, original message, target tone, and instructions
- Tells it to analyze tone/clarity/risks, rewrite, and explain changes
-
Call Gemini API:
- Using
gemini-2.5-flashcause it's fast and cheap - Tried gemini-pro first but it was too slow
- Endpoint:
https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent - Sends the prompt with temperature 0.7 (not too creative, not too boring)
- Uses
responseMimeType: 'application/json'to force json output
- Using
-
Parse and Validate:
- Gemini wraps the response in
candidates[0].content.parts[0].text - Had to debug this structure for a while
- Parse the json from that text
- Check that all required fields are present (rewritten, analysis, explanation)
- Return 500 error if parsing fails or structure is wrong
- Gemini wraps the response in
-
Return Response:
- Send the parsed json back to frontend
- All errors are caught and returned as 500 with error messages
The backend is stateless - no database, no sessions. Each request is independent. This keeps it simple and focused on the ai feature.
Single REST endpoint instead of GraphQL: For a small app like this, REST is way simpler. GraphQL would be overkill and add complexity i don't need.
Strict JSON schema: I defined exactly what the response should look like. This makes the frontend/backend integration predictable. The frontend knows exactly what fields to expect, so no need for defensive checks everywhere.
No database or auth: This is a demo project, so i kept it simple. No saving messages, no user accounts. Just paste, rewrite, done. Could add those later but wanted to focus on the ai integration first.
Inline styles in React: I know css modules or styled-components would be more "professional" but inline styles keep everything together. For a small project this is fine. Plus it's easier to see what styles apply to what component.
Error handling:
Backend returns { error: string } on errors. Frontend shows it in a red banner. Simple but effective. Could add more sophisticated error handling but this works for now.
Model choice (gemini-2.5-flash): Chose this cause it's fast and cheap. Good quality too. Could easily switch to gemini-pro or another model by changing one line, but this one works great.
Performance optimizations: Added a bunch of stuff to make scrolling smooth:
- Hardware acceleration with
transform: translateZ(0) - CSS containment to isolate rendering
- Memoized styles to prevent re-creation
- Optimized transitions (only animate what needs to animate)
- Fixed background using pseudo-element instead of
background-attachment: fixed
The architecture is intentionally simple. It's a focused student project that demonstrates ai integration without unnecessary complexity. Everything is in one repo, easy to understand, and easy to run locally.
