Build an AI Code Review Dashboard
Build a full-stack AI tool that streams live code reviews with FastAPI and React.
Introduction
30 Second Summary
Every developer writes code that could use a second pair of eyes. Code reviews catch bugs, flag security holes, and improve style. But getting timely, thorough feedback is hard, especially when you're working solo or learning on your own.
In this project, you will build CodeLens, a full-stack AI-powered code review dashboard. You paste in a code snippet, and Google's Gemini API analyzes it for bugs, security issues, performance problems, and style improvements. The results stream in live and land in a structured dashboard with severity ratings.
What You'll Build
Paste a code snippet into CodeLens, click Review Code, and watch color-coded issues stream onto your screen in real time. Then flip to the history tab to browse every review you have ever run, filterable by category.
By the end of this project, you'll have:
- A Python REST API built with FastAPI that accepts code snippets, sends them to Gemini for analysis, and persists every review in a SQLite database.
- Real-time Server-Sent Events (SSE) streaming that pushes AI-generated review chunks to the browser as they arrive, so the learner never stares at a loading spinner.
- A React frontend dashboard where you paste code, watch issues appear with color-coded severity badges, and browse a searchable, filterable review history.
- Secret Mission: Add a "Fix It" button that sends an issue back to Gemini and streams a corrected code snippet in a side-by-side diff view.
Are there any prerequisites?
You should have basic Python knowledge and a general understanding of web development concepts. Step 1 walks you through installing Node.js and getting a free Gemini API key, so no prior setup is needed.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Development Environment
CodeLens is a full-stack app with two halves. The React frontend needs Node.js to run its build tools. The Python backend needs access to the Gemini API for AI-powered code analysis.
In this step, you'll install Node.js, grab a free Gemini API key, and create the Python backend project with all its dependencies.
In this step, get ready to:
- Install Node.js for the React frontend tooling.
- Get a free Gemini API key from Google AI Studio.
- Create the Python backend project with a virtual environment and dependencies.
Install Node.js
Node.js is the JavaScript runtime that powers the React build toolchain. You'll use it later to scaffold and run the frontend. This project requires version 24 (the current LTS release).
- Open a terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check whether Node.js is already installed by running this command:
node --version
✔️ I see version 24 or higher
Node.js is ready to go. Skip ahead to the next substep.
ⓧ I see an older version
You have Node.js installed, but this project needs version 24 or higher. You'll need to upgrade.
- Go to the Node.js download page in your browser.
- The site auto-detects your operating system. Click the LTS download button (it should show v24.19.0 or similar).
- Run the downloaded installer and accept the default settings at each prompt.
- Close and reopen your terminal so it picks up the new version.
- Verify the upgrade by running:
node --version
You should see v24 or higher.
ⓧ Command not found
Node.js is not installed yet. Let's fix that.
- Go to the Node.js download page in your browser.
- The site auto-detects your operating system. Click the LTS download button (it should show v24.19.0 or similar).
- Run the downloaded installer and accept the default settings at each prompt.
- Close and reopen your terminal so it picks up the new installation.
- Verify the install by running:
node --version
You should see v24 or higher printed in the output.
Still seeing command not found?
Make sure you fully closed and reopened your terminal after installing. The terminal needs a fresh session to detect the new Node.js installation.
On macOS, try opening a new Terminal window (not just a new tab). On Windows, try opening a new Command Prompt window.
help me troubleshoot why node --version is not working after installing Node.js
Get your Gemini API key
The Gemini API is the AI engine behind CodeLens. It will analyze code snippets and return structured reviews. Google offers a free tier for the Gemini API, so you can use it at no cost with no billing setup required.
- Open Google AI Studio in your browser.
- Sign in with your Google account.
- If this is your first time, Google AI Studio may automatically create a default API key for you after you accept the Terms of Service. You can copy it directly from the API keys page.
- If you don't see an existing key, click Create API key and follow the dialog to generate one.
- Copy your API key and save it somewhere safe, like a text file or a password manager. You'll need this key in a later step.
Why is this free?
Google offers free input and output tokens for select Gemini models, including gemini-2.5-flash (the model this project uses). No credit card or billing account is required.
Content sent through the free tier may be used to improve Google products. For a personal learning project, this is a great trade-off for free access to a powerful AI model.
Keep your API key private
Never commit your API key to a Git repository or share it publicly. You'll set it as an environment variable later, which keeps it out of your source code.
Having trouble creating a key?
If you see a permissions error, try signing out and signing back in to Google AI Studio. Make sure you're using a personal Google account, not a managed workspace account that may have restrictions.
help me troubleshoot creating a Gemini API key in Google AI Studio
Create the Python backend project
Now you'll create the project folder structure and install the two Python packages that power the backend: FastAPI for the web server, and google-genai for communicating with the Gemini API.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folders and move into the backend folder by running these commands:
mkdir codelens
cd codelens
mkdir backend
cd backend
- Confirm you're in the right place by running:
pwd
You should see a path ending in codelens/backend.
A virtual environment keeps your project's Python packages isolated from the rest of your system. This prevents version conflicts between projects.
- Create a virtual environment by running this command:
python -m venv venv
What does this command do?
python -m venv venv tells Python to create a virtual environment in a folder called venv. This folder contains its own copy of the Python interpreter and a dedicated space for installing packages.
- Confirm the venv folder was created by running:
ls
You should see a venv folder listed in the output.
- Activate the virtual environment. The command differs by operating system:
macOS
- Activate the virtual environment by running:
source venv/bin/activate
Your terminal prompt now starts with (venv). This confirms the virtual environment is active.
Windows
- Activate the virtual environment by running:
venv\Scripts\activate
Your terminal prompt now starts with (venv). This confirms the virtual environment is active.
- Install the two backend dependencies by running:
pip install "fastapi[standard]" google-genai
What are these packages?
fastapi[standard] installs FastAPI along with its recommended dependencies, including Uvicorn (the web server that runs your API). The [standard] part is what pulls in those extras.
google-genai is the official Python SDK for Google's Gemini API. It handles authentication, request formatting, and streaming responses.
You should see pip downloading and installing several packages. The final output confirms everything was installed successfully.
Seeing an error during pip install?
Make sure your terminal prompt starts with (venv). If it doesn't, the virtual environment is not active. Re-run the activation command from the platform tab above before retrying the install.
help me troubleshoot pip install errors for fastapi and google-genai in a Python virtual environment
Before you move on, what do you expect to see if you run python --version right now?
- Verify your Python version by running:
python --version
You should see a Python version number printed (3.10 or higher is required for this project's dependencies).
Your development environment is fully set up. You have Node.js ready for the frontend, a Gemini API key for AI-powered reviews, and a Python virtual environment with FastAPI and google-genai installed. Next up, you'll use FastAPI to build your first working API endpoint.
Build a Basic Code Review API
Your development environment is set up with Python, Node.js, and a Gemini API key ready to go. Before connecting to any AI service, you need a working API that can accept code snippets and return a response.
In this step, you'll create a FastAPI backend with a POST endpoint and a SQLite database module. By the end, you'll have a live API you can test through an interactive browser interface.
In this step, get ready to:
- Create a FastAPI app with a POST /review endpoint that accepts code snippets.
- Set up a SQLite database to store and retrieve code reviews.
- Test your API using FastAPI's built-in Swagger UI.
Create the FastAPI app with a review endpoint
Every web API starts with a server that can listen for requests. FastAPI handles routing, request validation, and auto-generated documentation out of the box.
- Create a new file called main.py inside the backend folder in your code editor.
- Add the following code to set up the app and configure CORS middleware:
import json
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
# Create the FastAPI app
app = FastAPI(title="CodeLens API")
# Allow the React frontend to make requests to this API
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
What does this code do?
FastAPI(title="CodeLens API") creates your web server with a name that appears in the auto-generated docs.
The CORS middleware allows the React frontend (which will run on port 5173) to send requests to this API. Without it, the browser blocks requests between different origins.
- Add the following code below the CORS middleware block in main.py:
# Define the expected shape of incoming review requests
class ReviewRequest(BaseModel):
code: str
# Accept a code snippet and return a placeholder review
@app.post("/review")
def create_review(request: ReviewRequest):
return {"message": "Placeholder review response", "code": request.code}
What does this code do?
ReviewRequest uses Pydantic's BaseModel to validate that every incoming request includes a code field. If someone sends a request without it, FastAPI automatically returns an error.
The @app.post("/review") decorator creates a POST endpoint at /review. For now, it returns a hardcoded placeholder instead of real AI analysis.
- Save main.py.
- In the terminal where your virtual environment is activated, start the API server by running this command:
fastapi dev main.py
You should see output showing the Uvicorn server running at http://127.0.0.1:8000.
Server not starting?
- Make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
- Make sure you are inside the backend folder. Run cd to check your current directory.
- help me debug why fastapi dev main.py is not starting
- Stop the server by pressing Ctrl+C in your terminal. You'll restart it after creating the database module.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete main.py so far:
import json
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
app = FastAPI(title="CodeLens API")
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class ReviewRequest(BaseModel):
code: str
@app.post("/review")
def create_review(request: ReviewRequest):
return {"message": "Placeholder review response", "code": request.code}
Set up the database module
Code reviews are only useful if you can look back at them later. SQLite is built into Python's standard library, so you get a real database with zero extra setup.
- Create a new file called database.py inside the backend folder in your code editor.
- Add the following code to create a database connection helper and the table schema:
import sqlite3
import json
from datetime import datetime
# Connect to the SQLite database file
def get_connection():
conn = sqlite3.connect("reviews.db")
conn.row_factory = sqlite3.Row
return conn
# Create the reviews table if it doesn't exist yet
def init_db():
conn = get_connection()
conn.execute(
"""
CREATE TABLE IF NOT EXISTS reviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL,
result TEXT NOT NULL,
created_at TEXT NOT NULL
)
"""
)
conn.commit()
conn.close()
What does this code do?
get_connection() opens a connection to a file called reviews.db. Setting row_factory = sqlite3.Row lets you access columns by name (like row["code"]) instead of by index number.
init_db() creates a reviews table with four columns: a unique id, the submitted code snippet, the review result, and a created_at timestamp.
- Save database.py.
- Verify the module works by initializing the database. Run this command in your terminal:
python -c "from database import init_db; init_db(); print('Database initialized')"
You should see Database initialized printed with no errors.
- Confirm the database file was created by running:
ls reviews.db
You should see reviews.db listed in the output.
Seeing a ModuleNotFoundError?
- Make sure you are running the command from inside the backend folder, where database.py lives.
- Check that the file is named exactly database.py with no typos.
- help me fix a ModuleNotFoundError when importing database
- Now add the function to save a review. Add the following code below init_db() in database.py:
# Save a new review to the database and return its ID
def save_review(code: str, result: list) -> int:
conn = get_connection()
cursor = conn.execute(
"INSERT INTO reviews (code, result, created_at) VALUES (?, ?, ?)",
(code, json.dumps(result), datetime.now().isoformat()),
)
conn.commit()
review_id = cursor.lastrowid
conn.close()
return review_id
What does this code do?
save_review() inserts a new row into the reviews table. It converts the result list to a JSON string using json.dumps() so it can be stored as text in SQLite.
The ? placeholders prevent SQL injection by letting SQLite handle value escaping. The function returns the new row's id so the API can reference it later.
- Add the final function to retrieve all reviews. Add the following code below save_review() in database.py:
# Retrieve all reviews, newest first
def get_all_reviews() -> list:
conn = get_connection()
rows = conn.execute(
"SELECT id, code, result, created_at FROM reviews ORDER BY created_at DESC"
).fetchall()
conn.close()
return [
{
"id": row["id"],
"code": row["code"],
"result": json.loads(row["result"]),
"created_at": row["created_at"],
}
for row in rows
]
What does this code do?
get_all_reviews() queries every row in the reviews table, sorted by newest first. It converts each row into a dictionary and parses the stored JSON string back into a Python list using json.loads().
- Save database.py.
- Verify both new functions work by running this quick test:
python -c "from database import save_review, get_all_reviews; save_review('x = 1', []); print(get_all_reviews())"
You should see a list with one dictionary entry containing the code snippet x = 1 and an empty result list.
Seeing a NameError or AttributeError?
- Check that save_review and get_all_reviews are at the top level of database.py, not accidentally indented inside another function.
- Verify that import json and from datetime import datetime are both at the top of the file.
- help me fix an error when testing save_review and get_all_reviews
✔️ Awesome, I've got everything!
Great. Confirm you've saved database.py before moving on.
ⓧ I'd like to double check the full code
Your database.py should have four sections in this order. Scroll up and confirm each one is present:
- The three imports at the top: sqlite3, json, and datetime.
- The get_connection() function that opens reviews.db, followed by init_db() that creates the reviews table.
- The save_review() function that inserts a row and returns its ID.
- The get_all_reviews() function that queries all rows and returns them as dictionaries.
Test the API with Swagger UI
FastAPI automatically generates an interactive documentation page where you can send test requests without any extra tools. This is one of the biggest advantages of using FastAPI over other Python frameworks.
- Start the API server again by running:
fastapi dev main.py
- Open your browser and go to http://localhost:8000/docs.
You should see the Swagger UI page with your CodeLens API title and the POST /review endpoint listed.
Before you send a test request, what do you think the API will return when you submit a code snippet? Will it analyze the code or do something else?
- Click the POST /review row to expand it.
- Click Try it out.
- Replace the request body with the following JSON:
{
"code": "password = input()\nquery = 'SELECT * FROM users WHERE pass = ' + password"
}
- Click Execute.
The response body shows the hardcoded placeholder. The API echoes your code back but does not analyze it yet. That is exactly the gap you will close in the next step by connecting Gemini.
Why start with a placeholder?
Building the API skeleton first lets you confirm that routing, request validation, and CORS all work before adding AI complexity. If something breaks after you add Gemini, you'll know the issue is in the AI integration, not the API structure.
Not seeing the Swagger page?
- Make sure the server is still running in your terminal. You should see the Uvicorn log output with no errors.
- Try http://127.0.0.1:8000/docs if localhost does not resolve on your machine.
- help me access the FastAPI Swagger UI at /docs
Your API skeleton is alive and responding to requests. In the next step, you'll replace that placeholder with real AI-powered code analysis from Gemini.
Connect Gemini for AI-Powered Reviews
In the last step, you got a working API with a placeholder response and a SQLite database ready to store reviews. The placeholder proved the plumbing works, but it returns the same canned text no matter what code you send.
Time to plug in real intelligence. In this step, you will connect Gemini so the API analyzes actual code for bugs, security holes, performance issues, and style problems. The response will come back as one big block of text after a noticeable wait.
In this step, get ready to:
- Create a Gemini client module that sends code to the AI for review.
- Wire the client into your API and add database persistence.
- Test with a real code snippet and observe the response.
Create the Gemini client module
You need a dedicated module that handles all communication with the Gemini API. This keeps your API logic clean and makes the AI client reusable.
- In the terminal where your server is running, press Ctrl+C to stop it.
- Create a new file called gemini_client.py in your backend folder.
- Add the imports, client initialization, and system prompt by pasting this code:
import os
from google import genai
from google.genai import types
# Initialize the Gemini client with your API key
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
# Instructions telling Gemini how to review code
SYSTEM_PROMPT = (
"You are an expert code reviewer. Analyze the provided code snippet and "
"identify issues across four categories: bug, security, performance, and style. "
"For each issue, assign a severity level: critical (will cause failures or "
"vulnerabilities), warning (should be fixed but not immediately dangerous), "
"or info (suggestions for improvement). Be specific and actionable."
)
What does this code do?
The genai.Client line creates a connection to Google's Gemini API using a key stored in an environment variable. You will set that variable before restarting the server.
The SYSTEM_PROMPT is a set of instructions that tells Gemini what kind of reviewer to be. It scopes the analysis to four categories (bug, security, performance, style) and three severity levels (critical, warning, info).
- Below the SYSTEM_PROMPT, add the function that sends code to Gemini:
def review_code(code: str) -> dict:
# Send the code to Gemini with the system prompt
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=f"Review this code:\n\n```\n{code}\n```",
config=types.GenerateContentConfig(
system_instruction=SYSTEM_PROMPT,
),
)
return response.text
What does this function do?
The review_code function wraps the code snippet in markdown fences and sends it to gemini-2.5-flash along with the system prompt. It returns the raw text of Gemini's response.
Notice there is no constraint on the response format. Gemini can return whatever text it wants. Keep this in mind as you test later.
- Save gemini_client.py.
- Confirm the file exists by running:
ls
You should see database.py, gemini_client.py, and main.py listed in the backend folder.
Don't see gemini_client.py?
Make sure you created the file inside the backend folder, not in the parent codelens folder. Run pwd to check your current directory.
help me find where gemini_client.py was created
✔️ Awesome, I've got everything!
Great. Double check you have saved gemini_client.py before moving on.
ⓧ I'd like to double check the full code
import os
from google import genai
from google.genai import types
# Initialize the Gemini client with your API key
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
# Instructions telling Gemini how to review code
SYSTEM_PROMPT = (
"You are an expert code reviewer. Analyze the provided code snippet and "
"identify issues across four categories: bug, security, performance, and style. "
"For each issue, assign a severity level: critical (will cause failures or "
"vulnerabilities), warning (should be fixed but not immediately dangerous), "
"or info (suggestions for improvement). Be specific and actionable."
)
def review_code(code: str) -> dict:
# Send the code to Gemini with the system prompt
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=f"Review this code:\n\n```\n{code}\n```",
config=types.GenerateContentConfig(
system_instruction=SYSTEM_PROMPT,
),
)
return response.text
Wire Gemini into your API
With the client module ready, you need to update main.py to use it instead of the placeholder response. You will also hook up the database and add an endpoint to retrieve past reviews.
- In main.py, find this line near the top of the file:
from pydantic import BaseModel
- Add these two import lines directly below it:
from pydantic import BaseModel
from database import init_db, save_review, get_all_reviews
from gemini_client import review_code
What are these imports?
You are pulling in the three database functions you built in the last step (init_db, save_review, get_all_reviews) plus the review_code function you just created.
Now replace the existing endpoint and add the new ones.
- Find the existing @app.post("/review") handler at the bottom of main.py:
@app.post("/review")
def create_review(request: ReviewRequest):
return {"message": "Placeholder review response", "code": request.code}
- Delete those three lines and replace them with all of the following code:
# Initialize the database when the server starts
@app.on_event("startup")
def startup():
init_db()
# Send code to Gemini, save the result, and return it
@app.post("/review")
def create_review(request: ReviewRequest):
result = review_code(request.code)
parsed = json.loads(result)
review_id = save_review(request.code, parsed.get("issues", []))
return {"id": review_id, "issues": parsed.get("issues", [])}
# Return all past reviews from the database
@app.get("/reviews")
def list_reviews():
return get_all_reviews()
What does this code do?
The @app.on_event("startup") handler runs init_db() when the server boots, creating the reviews table if it does not already exist.
The updated POST /review endpoint now calls review_code() to get Gemini's analysis, parses the response as JSON, saves it to SQLite, and returns the result.
The new GET /reviews endpoint fetches all saved reviews from the database, sorted newest first.
- Save main.py.
Seeing a syntax error?
Make sure you deleted the entire old @app.post("/review") handler (all three lines) before pasting the new code. Having two create_review functions will cause an error.
help me fix syntax errors in my main.py
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py before moving on.
ⓧ I'd like to double check the full code
Your main.py should now have these sections in order. Scroll up and confirm each is present:
- The import block at the top, including from database import init_db, save_review, get_all_reviews and from gemini_client import review_code.
- The app initialization and CORS middleware (unchanged from the last step).
- The ReviewRequest model (unchanged).
- The @app.on_event("startup") handler calling init_db().
- The updated @app.post("/review") handler calling review_code() and save_review().
- The new @app.get("/reviews") handler calling get_all_reviews().
Test with a real code snippet
Your code changes are saved, but the server needs your Gemini API key to start. You will set it as an environment variable so the key stays out of your source code.
- Set your GEMINI_API_KEY environment variable by running this command in your terminal (replace your-api-key-here with the key you saved in Step 1):
macOS
export GEMINI_API_KEY=your-api-key-here
Windows
set GEMINI_API_KEY=your-api-key-here
Why an environment variable?
Storing the key in an environment variable keeps it out of your code files. If you ever push this project to GitHub, the key stays safe because it only lives in your terminal session.
- Start the server by running:
fastapi dev main.py
- Open http://localhost:8000/docs in your browser.
You should see the Swagger UI with two endpoints now: POST /review and GET /reviews.
Server won't start?
If you see an error mentioning the API key, double check that you ran the export (macOS) or set (Windows) command in the same terminal window before starting the server. The variable only exists in the session where you set it.
Also confirm your virtual environment is still active (you should see (venv) in your terminal prompt).
help me fix my Gemini API key environment variable
Time for the real test. Before you send a request, think about this: you are sending code to an AI with no constraints on the response format. What do you expect the response to look like?
- Click on POST /review to expand it.
- Click Try it out.
- Replace the request body with this code snippet that has an obvious SQL injection vulnerability:
{
"code": "def get_user(username):\n query = 'SELECT * FROM users WHERE name = ' + username\n cursor.execute(query)\n return cursor.fetchone()"
}
- Click Execute.
Watch the Swagger UI while you wait. Notice how long the spinner sits there with no feedback.
✔️ I see a response with issues
Gemini returned JSON-formatted text and the endpoint parsed it successfully. You should see an id field and an issues array containing review items.
It worked this time, but the format is not guaranteed. Without explicit JSON constraints, Gemini might return plain prose, markdown, or inconsistent structures on the next request.
ⓧ I see a 500 Internal Server Error
This is expected. Gemini returned free-form text instead of valid JSON, so json.loads() in your endpoint threw an error. The AI responded with real analysis, but not in a format your code could parse.
This is exactly the problem you will fix in the next step with structured output.
Why is this experience rough?
Two problems just surfaced. First, you waited several seconds staring at a spinner with zero feedback. For the person using your tool, that silence feels broken.
Second, the response format is unreliable. Without telling Gemini to return JSON, it might return markdown, prose, or a mix. Your code needs a predictable structure to display results in a dashboard.
Your API now talks to a real AI and can save reviews to a database. Next up, you will add structured JSON output and real-time streaming so the results arrive in a predictable format and show up token by token instead of after a long wait.
Add Structured Output and Streaming
Your API returns real Gemini analysis now, but the experience has two problems. The response is a wall of unstructured text that a frontend cannot easily display. The learner also waits several seconds with zero feedback before anything appears.
Structured JSON output solves the first problem by telling Gemini the exact shape of data to return. Server-Sent Events (SSE) streaming solves the second by pushing chunks to the client as Gemini generates them.
In this step, get ready to:
- Define a JSON schema that forces Gemini to return categorized, severity-rated issues.
- Add an SSE streaming endpoint that delivers review chunks in real time.
- Test the streaming endpoint and see events arrive one by one.
Define a JSON schema for structured reviews
Right now Gemini returns free-form text because nothing tells it what shape the output should be. A JSON schema acts as a contract: you describe the exact fields you want, and Gemini guarantees the response matches.
- In gemini_client.py, add the following schema between the client variable and SYSTEM_PROMPT:
# JSON schema that tells Gemini the exact structure to return
REVIEW_SCHEMA = {
"type": "object",
"properties": {
"issues": {
"type": "array",
"items": {
"type": "object",
"properties": {
# Each issue gets a category label
"category": {
"type": "string",
"enum": ["bug", "security", "performance", "style"],
},
# Severity helps the frontend color-code results
"severity": {
"type": "string",
"enum": ["critical", "warning", "info"],
},
"title": {"type": "string"},
"description": {"type": "string"},
},
"required": ["category", "severity", "title", "description"],
},
}
},
"required": ["issues"],
}
What does this schema do?
The schema defines an issues array where every item must have four fields: category, severity, title, and description.
The enum constraints lock categories to bug, security, performance, and style. Severities are locked to critical, warning, and info. This makes the data predictable enough for the React frontend to render badges and filters.
Now you need to tell review_code() to use this schema. The Gemini SDK accepts two config parameters that enforce structured output.
- In gemini_client.py, scroll down to the review_code() function.
- Find these lines inside the GenerateContentConfig:
config=types.GenerateContentConfig(
system_instruction=SYSTEM_PROMPT,
),
- Add response_mime_type and response_schema so the block looks like this:
config=types.GenerateContentConfig(
system_instruction=SYSTEM_PROMPT,
response_mime_type="application/json",
response_schema=REVIEW_SCHEMA,
),
- Save gemini_client.py.
The dev server auto-reloads when you save, so the changes take effect immediately.
- Switch back to http://localhost:8000/docs in your browser.
- Expand the POST /review endpoint, click Try it out, and submit the same code snippet you used in the previous step.
Instead of a wall of text, you should now see clean JSON with an issues array. Each issue has category, severity, title, and description fields.
Still seeing plain text instead of JSON?
- Check your terminal to confirm the server reloaded after you saved. Look for a log line containing Reloading.
- Make sure REVIEW_SCHEMA is defined above SYSTEM_PROMPT in gemini_client.py, not inside a function.
- Verify that both response_mime_type and response_schema are inside the GenerateContentConfig call, not outside it.
- help me debug my structured output configuration
Stream reviews in real time with SSE
The structured response is a big improvement, but the user still waits in silence until Gemini finishes. SSE keeps the HTTP connection open and pushes data chunks as they are generated, so the frontend can display partial results immediately.
- In gemini_client.py, add the following streaming function below the existing review_code() function:
def review_code_stream(code: str):
# Use generate_content_stream for real-time chunk delivery
for chunk in client.models.generate_content_stream(
model="gemini-2.5-flash",
contents=f"Review this code:\n\n```\n{code}\n```",
config=types.GenerateContentConfig(
system_instruction=SYSTEM_PROMPT,
response_mime_type="application/json",
response_schema=REVIEW_SCHEMA,
),
):
# Yield each text chunk as it arrives from Gemini
if chunk.text:
yield chunk.text
What does this function do?
Instead of generate_content() (which waits for the full response), generate_content_stream() returns chunks as they arrive from Gemini.
The yield keyword makes this a Python generator. Each time the caller iterates, it gets the next chunk of text. The schema config is the same, so the final assembled output is still valid structured JSON.
- Save gemini_client.py.
✔️ Awesome, I've got everything!
Great. Confirm you saved gemini_client.py and check your terminal for a reload log.
ⓧ I'd like to double check the full code
Scroll up and confirm each block is present in gemini_client.py, in this order:
- Imports (os, genai, types).
- The client variable.
- The REVIEW_SCHEMA dictionary.
- The SYSTEM_PROMPT string.
- The review_code() function with response_mime_type and response_schema in its config.
- The review_code_stream() generator function.
Now you need an endpoint in main.py that uses this streaming function and pushes chunks to the browser as SSE events.
- In main.py, find this import line near the top:
from fastapi.middleware.cors import CORSMiddleware
- Add a new line directly below it:
from fastapi.responses import StreamingResponse
- Next, find this line:
from gemini_client import review_code
- Update it to also import the streaming function:
from gemini_client import review_code, review_code_stream
- Now add the streaming endpoint below the existing POST /review route. Copy this code after the create_review function:
@app.post("/review/stream")
def stream_review(request: ReviewRequest):
def event_generator():
# Accumulate chunks to parse the full response at the end
full_response = ""
for chunk in review_code_stream(request.code):
full_response += chunk
# Send each chunk as an SSE event
yield f"data: {json.dumps({'chunk': chunk})}\n\n"
try:
# Parse the complete response and save to the database
parsed = json.loads(full_response)
issues = parsed.get("issues", [])
save_review(request.code, issues)
yield f"data: {json.dumps({'done': True, 'issues': issues})}\n\n"
except json.JSONDecodeError:
yield f"data: {json.dumps({'done': True, 'error': 'Failed to parse response'})}\n\n"
return StreamingResponse(event_generator(), media_type="text/event-stream")
What does this endpoint do?
The event_generator() inner function loops over chunks from Gemini and yields each one as an SSE event. The SSE format requires each event to start with data: and end with two newlines.
While streaming, it also accumulates the full text. After the loop finishes, it parses the complete JSON and saves the review to SQLite. The final event carries done: true and the parsed issues, so the frontend knows streaming is complete.
The StreamingResponse wrapper from FastAPI keeps the HTTP connection open and flushes each yielded string to the client immediately.
- Save main.py.
✔️ Awesome, I've got everything!
Great. Confirm you saved main.py and check your terminal for a reload log.
ⓧ I'd like to double check the full code
Scroll up and confirm each block is present in main.py, in this order:
- Imports: json, FastAPI, CORSMiddleware, StreamingResponse, BaseModel, database functions, and review_code, review_code_stream.
- The app instance and CORS middleware.
- The ReviewRequest model and startup() function.
- The POST /review endpoint (create_review).
- The new POST /review/stream endpoint (stream_review).
- The GET /reviews endpoint (list_reviews).
Before you test, what do you expect to see differently from the /review endpoint you just tried?
- Open http://localhost:8000/docs in your browser.
- Expand the new POST /review/stream endpoint and click Try it out.
- Paste a code snippet (the same one you used before works fine) and click Execute.
You should see the response body filled with SSE events. Each line starts with data: followed by a JSON object containing a chunk field. The very last event contains done: true and the full issues array with categorized results.
Why does Swagger show everything at once?
Swagger UI waits for the full response before displaying it, so you see all the SSE events at once rather than watching them arrive. The React frontend you build in the next step reads the stream in real time and shows each chunk as it lands.
Seeing a 500 error or no response?
- Check that StreamingResponse is imported from fastapi.responses, not from fastapi directly.
- Confirm that review_code_stream appears in the import line alongside review_code. A missing import causes an immediate NameError.
- Make sure your GEMINI_API_KEY environment variable is still set in your terminal session.
- help me debug my SSE streaming endpoint
Both problems from the previous step are solved. Reviews arrive as structured JSON with categorized issues, and the streaming endpoint pushes chunks in real time instead of making the user wait. Next up, you will build the React frontend that consumes this stream and renders a live dashboard.
Build the React Frontend
Your API returns streaming, AI-powered reviews. Now it needs a face. Without a frontend, the only way to use CodeLens is through Swagger docs and terminal commands.
In this step, you will build a React dashboard using Vite where you can paste code, watch review results stream in live, and browse past reviews.
In this step, get ready to:
- Scaffold a React app with Vite and create a code input form.
- Build streaming review results with color-coded severity badges.
- Add a review history view with category filtering and dashboard styling.
Scaffold the React app and create the component files
You need a separate React project for the frontend that talks to the FastAPI backend. Vite is the standard tool for scaffolding React projects with instant hot-module replacement.
- Open a new terminal window. Your FastAPI backend should still be running in the other terminal.
- Navigate to your project folder by running this command:
cd ~/Desktop/codelens
- Scaffold the React frontend by running this command:
npm create vite@latest frontend -- --template react
What does this command do?
This tells npm to use the create-vite scaffolding tool to generate a new React project in a folder called frontend. The -- separates npm's own arguments from Vite's --template react flag, which selects the plain React template.
npm may ask you to install the create-vite package. Enter y to proceed.
- Confirm the scaffold worked by running:
ls
You should see both backend and frontend folders listed.
- Move into the frontend folder and install the dependencies by running:
cd frontend
npm install
- Start the Vite dev server to verify everything installed correctly by running:
npm run dev
- Open http://localhost:5173 in your browser. You should see the default Vite + React starter page with a counter button.
- Go back to your terminal and press Ctrl+C to stop the dev server. You will replace the default files with your own code next.
Don't see the Vite page?
- Make sure npm install completed without errors. Look for a node_modules folder inside frontend.
- Confirm you are inside the frontend folder before running npm run dev.
- If port 5173 is already in use, Vite will pick the next available port and print it in the terminal output.
Still stuck? Help me troubleshoot npm run dev not working in my Vite project.
- Create the components directory for your React components by running:
mkdir src/components
- Open the frontend folder in your code editor.
- Open src/index.css, select all the content inside it, and delete it. Save the empty file.
Why clear index.css?
Vite's default index.css contains starter styles that would conflict with the custom dashboard theme you will add later. Clearing it now prevents unexpected styling.
- Create a new file called CodeInput.jsx inside src/components/ and paste the following code:
import { useState } from "react";
function CodeInput({ onSubmit, isStreaming }) {
const [code, setCode] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (code.trim() && !isStreaming) {
onSubmit(code);
}
};
return (
<form className="code-input" onSubmit={handleSubmit}>
<textarea
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Paste your code here for review..."
rows={12}
disabled={isStreaming}
/>
<button type="submit" disabled={isStreaming || !code.trim()}>
{isStreaming ? "Reviewing..." : "Review Code"}
</button>
</form>
);
}
export default CodeInput;
What does this component do?
- It renders a <textarea> where the user pastes code and a submit button.
- The onSubmit prop sends the code to the parent component. While streaming, the button shows "Reviewing..." and the textarea is disabled to prevent double-submissions.
- Save CodeInput.jsx.
- Create a new file called ReviewResults.jsx inside src/components/ and paste the following code:
function ReviewResults({ issues, streamingText, isStreaming }) {
const severityColors = {
critical: "#e74c3c",
warning: "#f39c12",
info: "#3498db",
};
const categoryLabels = {
bug: "Bug",
security: "Security",
performance: "Performance",
style: "Style",
};
if (!isStreaming && issues.length === 0 && !streamingText) {
return (
<div className="results-empty">
<p>Submit code above to see AI-powered review results.</p>
</div>
);
}
What does this code do?
- The severityColors map assigns red to critical, yellow to warning, and blue to info issues.
- The categoryLabels map converts API category keys like bug into display-friendly labels.
- If nothing is streaming and no issues exist, it shows a placeholder message.
- Below the code you just pasted in ReviewResults.jsx, add the rest of the component:
return (
<div className="results">
{isStreaming && (
<div className="streaming-indicator">
<span className="pulse"></span> Analyzing code...
{streamingText && (
<pre className="streaming-preview">{streamingText}</pre>
)}
</div>
)}
{issues.length > 0 && (
<div className="issues-list">
<h2>Found {issues.length} issue{issues.length !== 1 ? "s" : ""}</h2>
{issues.map((issue, index) => (
<div key={index} className="issue-card">
<div className="issue-header">
<span
className="severity-badge"
style={{ backgroundColor: severityColors[issue.severity] }}
>
{issue.severity}
</span>
<span className="category-badge">
{categoryLabels[issue.category] || issue.category}
</span>
<h3>{issue.title}</h3>
</div>
<p>{issue.description}</p>
</div>
))}
</div>
)}
</div>
);
}
export default ReviewResults;
What does this JSX render?
- While streaming, a pulsing "Analyzing code..." indicator appears with a live preview of the raw JSON coming from Gemini.
- Once streaming completes, each issue renders as a card with a color-coded severity badge, a category label, a title, and a description.
- Save ReviewResults.jsx. Confirm the file exists by running:
ls src/components/
You should see CodeInput.jsx and ReviewResults.jsx listed.
Missing a file?
Double-check that you saved both files inside src/components/ and not directly in src/. The folder must be components (lowercase, plural).
Still stuck? Help me figure out why my component files are not showing up in src/components.
✔️ Awesome, I've got everything!
Great. Make sure you have saved both files before continuing.
ⓧ I'd like to double check the full code
Scroll up and confirm each section is present in your ReviewResults.jsx, in this order:
- The severityColors and categoryLabels maps at the top of the function.
- The empty-state guard returning a results-empty div.
- The main return with the streaming indicator and the issues list.
- The export default ReviewResults at the very end.
- Create a new file called ReviewHistory.jsx inside src/components/ and paste the following code:
import { useState, useEffect } from "react";
function ReviewHistory() {
const [reviews, setReviews] = useState([]);
const [selectedReview, setSelectedReview] = useState(null);
const [filter, setFilter] = useState("all");
useEffect(() => {
fetch("http://localhost:8000/reviews")
.then((res) => res.json())
.then((data) => setReviews(data))
.catch((err) => console.error("Failed to fetch reviews:", err));
}, []);
const filteredIssues = selectedReview
? selectedReview.result.filter(
(issue) => filter === "all" || issue.category === filter
)
: [];
What does this setup code do?
- Three pieces of state track the review list, the currently selected review, and the active category filter.
- The useEffect hook fetches all past reviews from GET /reviews when the component first mounts.
- The filteredIssues array filters the selected review's issues by category so the user can narrow results.
- Below the code you just pasted in ReviewHistory.jsx, add the review list rendering:
return (
<div className="history">
<div className="history-list">
<h2>Past Reviews</h2>
{reviews.length === 0 && <p>No reviews yet. Submit your first code review.</p>}
{reviews.map((review) => (
<div
key={review.id}
className={`history-item ${selectedReview?.id === review.id ? "selected" : ""}`}
onClick={() => setSelectedReview(review)}
>
<pre className="code-preview">
{review.code.slice(0, 100)}
{review.code.length > 100 ? "..." : ""}
</pre>
<span className="review-date">
{new Date(review.created_at).toLocaleDateString()}
</span>
<span className="issue-count">
{review.result.length} issue{review.result.length !== 1 ? "s" : ""}
</span>
</div>
))}
</div>
What does this render?
Each past review appears as a clickable card showing a 100-character code preview, the review date, and the number of issues found. Clicking a card sets it as the selected review.
- Confirm your component files are building up by running:
ls src/components/
You should now see CodeInput.jsx, ReviewHistory.jsx, and ReviewResults.jsx.
- Below the code you just pasted in ReviewHistory.jsx, add the detail panel with category filters:
{selectedReview && (
<div className="history-detail">
<div className="filter-bar">
{["all", "bug", "security", "performance", "style"].map((cat) => (
<button
key={cat}
className={filter === cat ? "active" : ""}
onClick={() => setFilter(cat)}
>
{cat.charAt(0).toUpperCase() + cat.slice(1)}
</button>
))}
</div>
{filteredIssues.map((issue, index) => (
<div key={index} className="issue-card">
<div className="issue-header">
<span className={`severity-badge ${issue.severity}`}>
{issue.severity}
</span>
<span className="category-badge">{issue.category}</span>
<h3>{issue.title}</h3>
</div>
<p>{issue.description}</p>
</div>
))}
</div>
)}
</div>
);
}
export default ReviewHistory;
What does the filter panel do?
When a review is selected, filter buttons appear for All, Bug, Security, Performance, and Style. Clicking a filter updates the filter state, and only matching issues display below. Each issue renders the same card layout as the streaming results view.
- Save ReviewHistory.jsx.
✔️ Awesome, I've got everything!
Great. Confirm you saved the file before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm each section is present in your ReviewHistory.jsx, in this order:
- The import line, state hooks, useEffect fetch, and filteredIssues computation.
- The review list rendering with history-list div and clickable review cards.
- The detail panel with filter buttons and filtered issue cards.
- The export default ReviewHistory at the very end.
Connect the app and test streaming
All three components are ready. Now you need the main App.jsx file that wires them together, manages state, and handles the SSE streaming connection to your backend.
- Open src/App.jsx in your code editor. Select all the existing Vite default content and delete it.
- Paste the following code as the start of your new App.jsx:
import { useState } from "react";
import CodeInput from "./components/CodeInput";
import ReviewResults from "./components/ReviewResults";
import ReviewHistory from "./components/ReviewHistory";
import "./App.css";
function App() {
const [issues, setIssues] = useState([]);
const [streamingText, setStreamingText] = useState("");
const [isStreaming, setIsStreaming] = useState(false);
const [activeTab, setActiveTab] = useState("review");
const handleSubmit = async (code) => {
setIssues([]);
setStreamingText("");
setIsStreaming(true);
try {
const response = await fetch("http://localhost:8000/review/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullText = "";
What does this set up?
- Four pieces of state track the parsed issues, raw streaming text, whether streaming is active, and which tab (review or history) is visible.
- The handleSubmit function POSTs the code to your /review/stream endpoint and opens a ReadableStream reader to process Server-Sent Events chunks.
- Below the code you just pasted in App.jsx, add the streaming read loop:
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = JSON.parse(line.slice(6));
if (data.chunk) {
fullText += data.chunk;
setStreamingText(fullText);
}
if (data.done && data.issues) {
setIssues(data.issues);
setStreamingText("");
}
}
}
}
} catch (error) {
console.error("Stream error:", error);
} finally {
setIsStreaming(false);
}
};
How does the SSE parsing work?
- The while (true) loop reads chunks from the stream until the server closes the connection.
- Each chunk is decoded and split by newlines. Lines starting with data: are SSE events containing JSON.
- If the event has a chunk field, it is raw streaming text that gets appended to the preview. When done is true, the final parsed issues replace the streaming preview.
- Below the code you just pasted in App.jsx, add the JSX header with navigation tabs:
return (
<div className="app">
<header className="header">
<h1>CodeLens</h1>
<p>AI-Powered Code Review Dashboard</p>
<nav className="tabs">
<button
className={activeTab === "review" ? "active" : ""}
onClick={() => setActiveTab("review")}
>
New Review
</button>
<button
className={activeTab === "history" ? "active" : ""}
onClick={() => setActiveTab("history")}
>
History
</button>
</nav>
</header>
What does the header render?
The header shows the app title and two navigation buttons. Clicking a button updates activeTab state, and the currently active button gets highlighted with an active CSS class.
- Below the code you just pasted in App.jsx, add the main content area and export:
<main>
{activeTab === "review" ? (
<>
<CodeInput onSubmit={handleSubmit} isStreaming={isStreaming} />
<ReviewResults
issues={issues}
streamingText={streamingText}
isStreaming={isStreaming}
/>
</>
) : (
<ReviewHistory />
)}
</main>
</div>
);
}
export default App;
How does tab switching work?
When activeTab is "review", the code input form and streaming results display. Otherwise, the review history component loads and fetches past reviews from the API.
- Save App.jsx.
Seeing import errors in your editor?
- Verify all three component files (CodeInput.jsx, ReviewResults.jsx, ReviewHistory.jsx) are saved inside src/components/.
- Check that the filenames match exactly, including capitalization. codeInput.jsx will not match the import ./components/CodeInput.
Still stuck? Help me fix import errors in my React App.jsx.
✔️ Awesome, I've got everything!
Great. Make sure App.jsx is saved before starting the dev server.
ⓧ I'd like to double check the full code
Scroll up and confirm each section is present in your App.jsx, in this order:
- Imports for useState, the three components, and App.css.
- Four useState hooks for issues, streamingText, isStreaming, and activeTab.
- The handleSubmit function with fetch, reader, and SSE parsing loop.
- The JSX return with header, nav tabs, and conditional <main> rendering.
- The export default App at the very end.
Time to see your frontend in action. Make sure your FastAPI backend is still running in the other terminal. If it stopped, switch to that terminal, activate your virtual environment, and run:
GEMINI_API_KEY=your-api-key-here fastapi dev main.py
- In your frontend terminal (still inside the frontend folder), start the Vite dev server by running:
npm run dev
Before you open the browser, what do you expect to see? Will it look like the default Vite page, or something different?
- Open http://localhost:5173 in your browser.
You should see the CodeLens header with "New Review" and "History" tabs, a large textarea, and a "Review Code" button. The page looks unstyled (white background, default fonts) because you have not added the CSS yet.
- Paste this test snippet into the textarea:
import sqlite3
def get_user(username):
conn = sqlite3.connect('users.db')
query = f"SELECT * FROM users WHERE name = '{username}'"
return conn.execute(query).fetchone()
- Click Review Code.
You should see "Analyzing code..." appear with raw JSON streaming in. After a few seconds, the streaming preview disappears and individual issue cards replace it, each showing a severity level, category, title, and description.
Not seeing any results?
- Open your browser's developer console (F12 or Cmd+Option+I). Look for CORS errors or network failures pointing to localhost:8000.
- Confirm your FastAPI backend is running in the other terminal and that the GEMINI_API_KEY environment variable is set.
- Check that the CORS middleware in main.py allows http://localhost:5173.
Still stuck? Help me debug why my React frontend can't connect to the FastAPI backend.
Style the dashboard
The dashboard works but looks bare. A dark-theme CSS file will turn it into something you can demo in an interview. Since the dev server is running, every CSS save will update the browser instantly.
- Open src/App.css in your code editor. Select all the existing Vite default content and delete it.
- Paste the following CSS as the beginning of your new App.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #0f172a;
color: #e2e8f0;
}
.app {
max-width: 960px;
margin: 0 auto;
padding: 2rem 1rem;
}
.header {
text-align: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
color: #38bdf8;
}
What do these styles do?
The reset removes default margins and padding. The body gets a dark navy background with light text. The .app container is centered at 960px wide, and the header title turns sky blue.
- Save App.css and check your browser. The page background should now be dark navy with the CodeLens title in blue.
- Below the previous CSS in App.css, add the header subtitle and tab navigation styles:
.header p {
color: #94a3b8;
margin-top: 0.25rem;
}
.tabs {
margin-top: 1rem;
display: flex;
justify-content: center;
gap: 0.5rem;
}
.tabs button {
padding: 0.5rem 1.5rem;
border: 1px solid #334155;
background: transparent;
color: #94a3b8;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
}
.tabs button.active {
background: #1e293b;
color: #38bdf8;
border-color: #38bdf8;
}
What changed?
The subtitle text turns muted gray. The tab buttons center horizontally with rounded borders, and the active tab glows blue.
- Save App.css. Your New Review and History buttons should now be styled with borders and the active one highlighted blue.
- Add the code input form styles below the previous CSS:
.code-input {
margin-bottom: 1.5rem;
}
.code-input textarea {
width: 100%;
padding: 1rem;
background: #1e293b;
color: #e2e8f0;
border: 1px solid #334155;
border-radius: 8px;
font-family: "Fira Code", "Consolas", monospace;
font-size: 0.9rem;
resize: vertical;
}
.code-input button {
margin-top: 0.75rem;
padding: 0.75rem 2rem;
background: #38bdf8;
color: #0f172a;
border: none;
border-radius: 6px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
}
What changed?
The textarea gets a dark background with monospace font. The submit button turns bright blue with dark text to stand out as the primary action.
- Save App.css. Your textarea should now have a dark background and the button should be bright blue.
- Add the button disabled state and streaming indicator styles:
.code-input button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.results-empty {
text-align: center;
padding: 3rem;
color: #64748b;
}
.streaming-indicator {
padding: 1rem;
background: #1e293b;
border-radius: 8px;
margin-bottom: 1rem;
}
.pulse {
display: inline-block;
width: 8px;
height: 8px;
background: #38bdf8;
border-radius: 50%;
margin-right: 0.5rem;
animation: pulse 1s infinite;
}
What changed?
Disabled buttons dim to 50% opacity. The streaming indicator gets a dark card background, and the pulse dot animates with a blue glow.
- Save App.css. Click Review Code with some code to see the styled streaming indicator with the pulsing dot.
- Add the animation keyframes and streaming preview styles:
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.streaming-preview {
margin-top: 0.75rem;
padding: 0.75rem;
background: #0f172a;
border-radius: 4px;
font-size: 0.8rem;
overflow-x: auto;
white-space: pre-wrap;
color: #94a3b8;
max-height: 200px;
overflow-y: auto;
}
.issues-list h2 {
margin-bottom: 1rem;
color: #e2e8f0;
}
What changed?
The keyframes make the pulse dot fade in and out. The streaming preview area caps at 200px height with scrolling, and the issues heading gets spacing.
- Save App.css. The pulsing animation and streaming preview text should now be visible during a review.
- Add the issue card and severity badge styles:
.issue-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.75rem;
}
.issue-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.severity-badge {
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
color: white;
text-transform: uppercase;
}
.severity-badge.critical { background-color: #e74c3c; }
.severity-badge.warning { background-color: #f39c12; }
.severity-badge.info { background-color: #3498db; }
What changed?
Issue cards get dark backgrounds with borders. Severity badges are color-coded: red for critical, yellow for warning, and blue for info.
- Save App.css. If you have review results visible, the issue cards should now show styled severity badges.
- Add the category badge and issue text styles:
.category-badge {
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
background: #334155;
color: #94a3b8;
text-transform: capitalize;
}
.issue-card h3 {
font-size: 0.95rem;
color: #e2e8f0;
}
.issue-card p {
color: #94a3b8;
font-size: 0.9rem;
line-height: 1.5;
}
.history {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 1rem;
}
.history-list h2 {
margin-bottom: 1rem;
}
What changed?
Category badges get a subtle gray pill. Issue titles and descriptions have proper sizing. The history view splits into a 1:2 grid layout with the review list on the left and details on the right.
- Save App.css. Click the History tab to see the grid layout take shape.
- Add the history item card styles:
.history-item {
background: #1e293b;
border: 1px solid #334155;
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.5rem;
cursor: pointer;
}
.history-item.selected {
border-color: #38bdf8;
}
.code-preview {
font-size: 0.75rem;
color: #94a3b8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
What changed?
Past review items render as dark cards. The selected review gets a blue border highlight. Code previews truncate with ellipsis to keep the list tidy.
- Save App.css. Click a past review in the History tab to see the blue selection highlight.
- Add the review metadata and filter bar styles:
.review-date {
font-size: 0.75rem;
color: #64748b;
display: block;
margin-top: 0.25rem;
}
.issue-count {
font-size: 0.75rem;
color: #38bdf8;
}
.filter-bar {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.filter-bar button {
padding: 0.35rem 0.75rem;
background: #1e293b;
color: #94a3b8;
border: 1px solid #334155;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
What changed?
Review dates appear muted, issue counts glow blue, and filter buttons line up horizontally above the issue detail view.
- Save App.css. The filter buttons (All, Bug, Security, Performance, Style) should now be visible when you select a review.
- Add the final CSS for active filter buttons and the detail panel:
.filter-bar button.active {
background: #334155;
color: #38bdf8;
border-color: #38bdf8;
}
.history-detail {
padding: 0.5rem;
}
What changed?
Active filter buttons highlight with blue text and border. The detail panel gets minimal padding to let the issue cards breathe.
- Save App.css. Click different filter buttons to see the active state change.
✔️ Awesome, I've got everything!
Great. Make sure you saved App.css with all sections present.
ⓧ I'd like to double check the full code
Scroll up and confirm each CSS section is present in your App.css, in this order:
- Reset (*), body, .app, .header, .header h1.
- Header paragraph, .tabs, .tabs button, .tabs button.active.
- Code input form: .code-input, textarea, and button.
- Disabled button, .results-empty, .streaming-indicator, .pulse.
- Keyframes, .streaming-preview, .issues-list h2.
- Issue card, header, severity badge, and severity color modifiers.
- Category badge, issue card text, .history grid, .history-list h2.
- History items, selected state, .code-preview.
- Review date, issue count, filter bar, filter bar buttons.
- Active filter button and .history-detail.
Time for the full end-to-end test. Do you think everything will work together: pasting code, streaming results, and browsing history with filters?
- Click the New Review tab.
- Paste a code snippet into the textarea and click Review Code.
- Watch the pulsing indicator and raw JSON stream in. After a few seconds, color-coded issue cards should appear.
- Click the History tab. Your review should appear in the left panel. Click it to see its issues on the right.
- Click a category filter (like Security) to narrow the displayed issues.
You should see a fully styled, dark-themed dashboard with working streaming, history, and filtering.
History tab showing no reviews?
- Make sure you submitted at least one review on the New Review tab first. The history pulls from GET /reviews which only returns reviews that were streamed and saved.
- Check your browser console for fetch errors pointing to localhost:8000/reviews.
Still stuck? Help me debug why the History tab in my CodeLens dashboard is empty.
Your full-stack code review dashboard is live. You have a Python API powered by Gemini, structured streaming over SSE, and a React frontend that makes the whole thing demo-ready. Next up, there is an optional Secret Mission to push your skills even further.
Secret mission
Add a "Fix It" Button
Your code review dashboard finds problems, but what if it could also fix them? In this mission, you'll add a Fix It button that sends each issue back to Gemini, streams a corrected code snippet, and displays it in a side-by-side diff view with color-coded highlighting.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely locally with a free-tier API key, so there are no ongoing costs.
Resources you used:
- A Gemini API key (free tier, no billing attached).
- A local SQLite database file (reviews.db) inside the codelens/backend folder.
- The codelens project folder on your Desktop, including the Python virtual environment and node_modules.
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend the project.
- Your Gemini API key stays active on the free tier at no cost.
- The reviews.db file is a local SQLite database with no ongoing charges.
- The codelens folder, virtual environment, and node_modules stay on your Desktop for future use.
Pause - I'll come back to this later
Nothing to pause. All resources are local files and a free-tier API key, so nothing is running or costing money while you are away.
- Stop the FastAPI dev server by pressing Ctrl+C in the backend terminal if it is still running.
- Stop the Vite dev server by pressing Ctrl+C in the frontend terminal if it is still running.
- Everything else stays in place for when you return.
Delete - I don't want to use this again
Remove all project files and optionally revoke your API key to start completely fresh.
- Stop any running dev servers by pressing Ctrl+C in each terminal.
- Delete the entire codelens project folder (this removes all code, the virtual environment, node_modules, and the reviews.db database). Run the command for your operating system below.
macOS
cd ~/Desktop
rm -rf codelens
Windows
cd ~\Desktop
rmdir /s /q codelens
- Optionally, revoke your Gemini API key. Go to Google AI Studio API keys, find the key you created for this project, and click the trash icon to delete it.
Nice Work!
Nice Work!
Nice work! You just built CodeLens, a full-stack AI-powered code review dashboard from scratch.
You've learned how to:
- Built a full-stack application with a FastAPI backend and a React frontend, connected through a REST API you designed with proper endpoint patterns for creating and retrieving reviews.
- Integrated the Gemini API for intelligent code analysis, using structured JSON output to turn raw AI responses into categorized issues with severity ratings that the frontend can render as color-coded cards.
- Implemented real-time Server-Sent Events (SSE) streaming from backend to frontend so review results appear live. You persisted every review in a SQLite database and built a searchable, filterable history dashboard.
- Secret Mission: Added a "Fix It" button that sends a specific issue back to Gemini and streams a corrected code snippet in a side-by-side diff view.
Ready to quiz yourself?