Build a React and FastAPI Triage Tool
Build a React and FastAPI app that triages support issues with CORS.
Introduction
30 Second Summary
Support teams lose time when every new ticket arrives as an unlabelled paragraph. Urgent problems can sit beside routine questions until someone reads each one.
In this project, you will build SupportPulse as a local support-ticket triage tool. You will connect a React interface to a FastAPI service that returns the category, priority, and next action.
What You'll Build
The finished SupportPulse app turns I was charged twice for my invoice into a polished result card labeled Billing at High priority, complete with the recommended billing action.
By the end of this project, you'll have:
- A live issue form with a changing character count plus clear loading feedback.
- A triage result card showing the API's category, priority, and recommended action for each issue.
- A working browser-to-API connection that sends JSON from React to FastAPI through an explicit CORS origin rule.
- Secret Mission: Add a Critical security classification that catches breach, hacked, and data leak reports before the existing rules.
Are there any prerequisites?
You need a Windows computer with Python installed plus an internet connection for the initial downloads. The first step checks your Python version and helps you set up Node.js or Visual Studio Code if needed.
Before We Start
Before the hands-on work begins, this checkpoint locks in the tool you are building. It also connects SupportPulse to faster ticket routing for support teams.
Set Up the Windows Workspace
SupportPulse has a React interface plus a FastAPI service. Keeping each half in its own folder prevents their dependencies from colliding.
A Python virtual environment isolates the backend packages. Node.js powers the frontend tools. By the end of this step, Vite serves your first visible SupportPulse page.
In this step, get ready to:
- Verify Python plus the Windows development tools.
- Create the isolated FastAPI backend workspace.
- Install the React frontend dependencies before launching the starter page.
Verify your development tools
Each dependency expects a supported runtime. Checking the installed versions now prevents confusing installation errors when the backend or frontend starts.
Windows PowerShell gives you one place to run every setup command in this project.
- Press the Windows key to open Windows search.
- Type PowerShell into the search field.
- Press Enter to open PowerShell.
- Check your installed Python version by running this command:
python --version
How to read the Python version
The command prints the Python version available to your terminal. SupportPulse requires Python 3.10 or newer because the pinned FastAPI and Pydantic releases use that minimum.
✔️ I see a supported Python version
Your result shows Python 3.10 or newer. Python is ready for the backend environment.
ⓧ I see an older Python version
The installed version cannot run the pinned backend packages. Upgrade Python before creating the virtual environment.
- Visit the official Python downloads for Windows page.
- Download the current stable Windows release.
- Run the installer using its recommended Windows options.
- Close PowerShell after the installation finishes.
- Open a fresh PowerShell window through Windows search.
- Repeat the Python version check from above.
Still seeing the older version?
Windows may still be using an older Python installation from its command search path. Help me find which Python installation Windows is using.
ⓧ Python is not found
Python is installed on your computer according to the project starting state. This result means PowerShell cannot currently find its command.
- Visit the official Python downloads for Windows page.
- Download the current stable Windows release.
- Run the installer using its recommended Windows options.
- Close PowerShell after the installation finishes.
- Open a fresh PowerShell window through Windows search.
- Repeat the Python version check from above.
Python still unavailable?
The installation may not be available through the Windows command search path. Help me make Python available in PowerShell.
The frontend toolchain depends on both Node.js and npm. The pinned versions give everyone the same package installation and Vite behavior.
- Check the installed Node.js version plus the installed npm version by running these commands:
node -v
npm -v
What do these checks show?
- The first command reports the Node.js runtime available to Vite.
- The second command reports the npm package manager used to install the frontend dependencies.
✔️ I see the pinned versions
Node.js shows v24.21.0. npm shows 11.21.0. Your frontend runtime is ready.
ⓧ I see an older version
The project uses Node.js v24.21.0 LTS with npm 11.21.0. Installing the pinned LTS release gives Vite a supported runtime.
- Visit the official Node.js download page.
- Select Node.js v24.21.0 LTS for Windows.
- Run the downloaded installer with its default components.
- Close PowerShell after the installer finishes.
- Open a fresh PowerShell window through Windows search.
- Repeat both version checks from above.
Still seeing the older versions?
An older Node.js installation may still appear first in the Windows command search path. Help me identify the Node.js installation PowerShell is using.
ⓧ The commands are not found
Node.js is not available to PowerShell yet. Its Windows installer also provides npm for the frontend workflow.
- Visit the official Node.js download page.
- Select Node.js v24.21.0 LTS for Windows.
- Run the downloaded installer with its default components.
- Close PowerShell after the installer finishes.
- Open a fresh PowerShell window through Windows search.
- Repeat both version checks from above.
Commands still unavailable?
PowerShell may have opened before the installer updated the Windows command search path. Help me make Node.js available in PowerShell.
Visual Studio Code keeps the backend files plus the frontend files inside one workspace. The Windows User setup installs without administrator permissions.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search field.
✔️ Visual Studio Code appears
- Press Enter to open Visual Studio Code.
Your editor is ready for the SupportPulse workspace.
ⓧ Visual Studio Code is missing
The User setup is the recommended Windows installation for most people. It supports background updates without requiring administrator permissions.
- Visit the official Visual Studio Code download page.
- Download the User setup for Windows.
- Run the installer with its recommended options.
- Press the Windows key after installation finishes.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
Editor still missing?
The installer may need to finish before Windows search refreshes its app list. Help me verify my Visual Studio Code installation.
Create the isolated backend
The backend needs a dedicated folder plus pinned package versions. Its virtual environment keeps FastAPI 0.143.0 separate from packages used by other Python projects.
- Return to the PowerShell window from your version checks.
- Create the SupportPulse workspace on your Desktop by running these commands:
cd ~/Desktop
mkdir supportpulse
cd supportpulse
mkdir backend
mkdir frontend
What does this workspace contain?
- The supportpulse folder keeps the full project in one location on your Desktop.
- The backend folder holds the Python API.
- The frontend folder holds the browser interface.
- Switch back to Visual Studio Code from the earlier check.
- Click File in the top menu.
- Click Open Folder.
- Select the supportpulse folder on your Desktop.
- Click Select Folder.
- Confirm that you trust the folder if Visual Studio Code asks because you created every file in this workspace.
You should see the empty backend folder plus the empty frontend folder in the Explorer sidebar.
The requirements.txt file records the exact backend dependency versions. This makes the environment reproducible on another Windows computer.
- Select the backend folder in the Explorer sidebar.
- Click the New File icon at the top of the Explorer sidebar.
- Enter requirements.txt as the filename.
- Add the pinned backend dependencies by pasting this code:
fastapi[standard]==0.143.0
pydantic==2.14.0
What do these dependencies provide?
- The FastAPI standard package provides the web framework plus its development command.
- Pydantic 2.14.0 validates the JSON issue sent to the triage endpoint.
- Save backend\requirements.txt.
- Click Terminal in the Visual Studio Code menu.
- Click New Terminal.
- Move the terminal into the backend folder by running this command:
cd backend
Where is the terminal now?
The terminal now runs commands inside supportpulse\backend. The virtual environment will live beside requirements.txt.
A virtual environment gives this backend its own Python package location. Creating .venv prevents global package conflicts.
- Create the backend virtual environment by running this command:
python -m venv .venv
What did Python create?
Python created a .venv folder inside backend. That folder contains the isolated interpreter plus its package storage.
- Confirm that .venv appears under backend in the Explorer sidebar.
- Activate the virtual environment by running this command:
.\.venv\Scripts\activate
What does activation change?
Activation makes this terminal use the Python interpreter inside backend\.venv. You should see the environment name at the start of the terminal prompt.
- Install the pinned backend dependencies by running this command:
py -m pip install -r requirements.txt
What does the install command do?
pip reads each pin from requirements.txt. It installs FastAPI 0.143.0 plus Pydantic 2.14.0 inside the active virtual environment.
The first installation can take a minute while pip downloads the packages. The command returns to the active environment prompt when installation finishes.
Backend installation failed?
- Confirm that the terminal path ends with supportpulse\backend.
- Confirm that the active environment name appears at the start of the prompt.
- Check that requirements.txt contains both pinned dependency lines exactly.
- Help me fix my backend dependency installation.
Build and launch the React starter
The frontend begins as a minimal Vite project. Writing the small set of files directly keeps every dependency pin visible.
Why Vite for this frontend?
FastAPI already owns the backend. Vite fits this project because the browser needs one React page plus a fast local development server.
- Click Terminal in the Visual Studio Code menu.
- Click New Terminal to keep the active backend terminal available.
- Move the new terminal into the frontend folder by running this command:
cd frontend
Where is the frontend terminal?
This terminal now runs commands inside supportpulse\frontend. npm installs every browser dependency in this folder.
The package.json file defines the development scripts plus the exact React and Vite package versions.
- Select the frontend folder in the Explorer sidebar.
- Click the New File icon at the top of the Explorer sidebar.
- Enter package.json as the filename.
- Define the frontend packages plus scripts by pasting this code:
{
"name": "supportpulse-frontend",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"@vitejs/plugin-react": "6.1.2",
"vite": "8.3.4"
}
}
What does this package file define?
- The dependencies provide React 19.3.0 plus React DOM 19.3.0 for the browser interface.
- The development dependencies provide Vite 8.3.4 plus its React plugin 6.1.2.
- The development script starts the local Vite server.
- Save frontend\package.json.
- Install the frontend dependencies by running this command:
npm install
What did npm install?
npm reads package.json before downloading the pinned frontend packages. The terminal returns to its prompt after the dependency installation completes.
Frontend installation failed?
- Confirm that the terminal path ends with supportpulse\frontend.
- Confirm that package.json contains valid JSON with every comma shown above.
- Help me fix my npm installation.
Vite needs a small configuration file to process React components. The plugin transforms the component syntax during development.
- Select the frontend folder in the Explorer sidebar.
- Create vite.config.js with the New File icon.
- Configure the React plugin by pasting this code:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})
What does this configuration do?
The configuration registers the React plugin with Vite. This lets the development server process the JSX used by App.jsx.
- Save frontend\vite.config.js.
The browser loads index.html first. Its root element gives React a fixed place to render the interface.
- Select the frontend folder in the Explorer sidebar.
- Create index.html with the New File icon.
- Add the browser document by pasting this code:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SupportPulse</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
What does this page provide?
- The root element becomes the mounting point for the React interface.
- The module script loads src\main.jsx as the frontend entry point.
- Save frontend\index.html.
- Confirm that vite.config.js plus index.html appear under frontend in the Explorer sidebar.
The React entry point connects the browser document to the starter component. It also loads the stylesheet used by that component.
- Create a src folder inside frontend with the New Folder icon.
- Select the new src folder in the Explorer sidebar.
- Create main.jsx with the New File icon.
- Connect React to the browser root by pasting this code:
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './App.css'
createRoot(document.getElementById('root')).render(<App />)
What does the entry point do?
React DOM creates a root from the page element named root. It renders the App component into that element.
- Save frontend\src\main.jsx.
- Create App.jsx inside frontend\src with the New File icon.
- Add the starter component by pasting this code:
export default function App() {
return <h1>SupportPulse</h1>
}
What does the starter component do?
The App component returns one visible heading. This gives you a quick proof that Vite can load React before you build the triage form.
- Save frontend\src\App.jsx.
- Confirm that main.jsx plus App.jsx appear inside frontend\src in the Explorer sidebar.
A small stylesheet removes the browser's default page margin. It also makes the starter heading easier to spot.
- Create App.css inside frontend\src with the New File icon.
- Style the starter page by pasting this code:
body {
margin: 0;
font-family: Arial, sans-serif;
}
h1 {
padding: 2rem;
}
What does the starter CSS change?
- The body rule removes the browser margin plus sets a readable system font.
- The heading rule adds space around the SupportPulse title.
- Save frontend\src\App.css.
✔️ Awesome, I've got everything!
Your backend pins plus frontend starter files are ready. Save every open editor tab before launching Vite.
ⓧ I'd like to double check the full code
Compare each file with the complete workspace state below.
fastapi[standard]==0.143.0
pydantic==2.14.0
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SupportPulse</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
{
"name": "supportpulse-frontend",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"@vitejs/plugin-react": "6.1.2",
"vite": "8.3.4"
}
}
body {
margin: 0;
font-family: Arial, sans-serif;
}
h1 {
padding: 2rem;
}
export default function App() {
return <h1>SupportPulse</h1>
}
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './App.css'
createRoot(document.getElementById('root')).render(<App />)
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})
The Vite development server keeps running until you stop it. Leave this terminal occupied because the next step uses the same live frontend.
Before you start the server, what heading do you expect the browser to show?
- Start the Vite development server from the frontend terminal by running this command:
npm run dev
What does the development server do?
Vite serves index.html from the frontend folder. It keeps watching your React files so saved changes can reach the browser during development.
- Open your browser through Windows search.
- Enter http://localhost:5173 into the address bar.
- Press Enter to load the starter page.
You should see a large SupportPulse heading with space around it. That visible page proves the React entry point plus Vite development server are working.
Starter page not loading?
- Confirm that the frontend terminal still shows the running Vite server.
- Confirm that the browser address is http://localhost:5173.
- Check that App.jsx plus main.jsx are inside frontend\src.
- Help me debug the SupportPulse starter page.
Your Windows workspace is ready. Next, you will turn the empty backend into a working rule-based triage API.
Build the Rule-Based Triage API
Good progress. Your React starter is running at http://localhost:5173.
The frontend now needs a small but real service contract before it can send an issue or receive a useful result. In this step, you'll build a FastAPI service around deterministic triage rules.
In this step, get ready to:
- Define the FastAPI application with a validated request model.
- Classify support issues with ordered keyword rules.
- Test the health and triage endpoints through the interactive API documentation.
Create the API foundation
A Pydantic request model defines the data your API accepts. Here, every triage request must include an issue string.
- Select the backend folder in the left file tree in Visual Studio Code.
- Use the new-file control above the file tree.
- Name the file main.py.
- Create the application foundation in backend\main.py by pasting this code:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class TriageRequest(BaseModel):
issue: str
What does this code do?
- FastAPI creates the web application that receives requests.
- Pydantic checks each request body against the fields in TriageRequest.
- The issue annotation requires the incoming value to be a string.
- Save backend\main.py.
- Return to the backend terminal from the previous step.
- Start the FastAPI development server by running this command:
fastapi dev
What does this command do?
The development command finds the FastAPI app in main.py. It watches the file so saved changes trigger a reload.
You'll see the server listening at http://127.0.0.1:8000.
- Switch back to the browser from the previous step.
- Open a new browser tab.
- Enter http://127.0.0.1:8000/docs in the address bar.
Your interactive documentation page loads from the new FastAPI server. That confirms the backend application can accept routes.
Does the documentation page fail to load?
- Confirm main.py is directly inside the backend folder.
- Check that the backend terminal still shows the active .venv environment.
- Look in the backend terminal for a Python syntax error caused by missing indentation.
- Help me diagnose why my FastAPI development server does not start.
Add the ordered triage rules
The triage engine is a Python function that normalizes each issue before checking its keywords. The first matching branch returns a complete triage result.
- Place your cursor below the TriageRequest class in backend\main.py.
- Add the Account Access and Billing rules by pasting this code:
def triage_issue(issue: str) -> dict[str, str]:
normalized_issue = issue.lower()
if any(keyword in normalized_issue for keyword in ("password", "login", "sign in")):
return {
"category": "Account Access",
"priority": "High",
"next_action": "Verify the user's identity and start the account recovery process.",
}
if any(keyword in normalized_issue for keyword in ("payment", "charged", "refund", "invoice")):
return {
"category": "Billing",
"priority": "High",
"next_action": "Review the transaction and confirm the expected billing outcome.",
}
How do these rules work?
- The normalized_issue value converts the issue to lowercase for consistent keyword matching.
- The Account Access branch looks for three phrases connected to authentication problems.
- The Billing branch looks for four words connected to payments or invoices.
- Each match returns a category with its priority and next action.
- Save backend\main.py.
The backend terminal shows a fresh reload without a Python error. Your first two classification branches are now valid application code.
Does the server report a Python error?
- Align normalized_issue with four spaces inside triage_issue().
- Align each return block with eight spaces inside its matching condition.
- Check that every returned dictionary has matching braces.
- Help me fix the first half of my triage_issue function.
- Place your cursor after the Billing return block in backend\main.py.
- Complete the function with the Technical rule and General fallback by pasting this code:
if any(keyword in normalized_issue for keyword in ("error", "bug", "crash")):
return {
"category": "Technical",
"priority": "Medium",
"next_action": "Collect reproduction steps, environment details, and recent changes.",
}
return {
"category": "General",
"priority": "Low",
"next_action": "Request more detail and route the issue to the support queue.",
}
Why does rule order matter?
- The Technical branch catches issues containing error, bug, or crash.
- The final return handles issues that match none of the earlier keywords.
- The first matching branch ends the function immediately. This makes the rule order part of the classification behavior.
- Save backend\main.py.
The backend terminal reloads without a Python error. The completed function now produces Account Access, Billing, Technical, or General results.
Does the rule function fail to reload?
- Keep the Technical condition indented by four spaces inside triage_issue().
- Keep the General return indented by four spaces inside the same function.
- Confirm the new code sits directly after the Billing return block.
- Help me fix the completed triage_issue function.
Expose and test the endpoints
A REST endpoint connects an HTTP method and path to a Python function. The triage endpoint receives JSON through TriageRequest.
- Place your cursor below triage_issue() in backend\main.py.
- Add the health and triage endpoints by pasting this code:
@app.get("/api/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.post("/api/triage")
def triage(request: TriageRequest) -> dict[str, str]:
return triage_issue(request.issue)
What do the endpoints do?
- The GET /api/health endpoint returns a small status response that proves the API is reachable.
- The POST /api/triage endpoint validates the request body with TriageRequest.
- The triage() route passes the validated issue into triage_issue().
- Save backend\main.py.
The backend terminal reloads the application. Both routes are now available through the running server.
Are the endpoints missing from the documentation?
- Confirm both route decorators begin at the far left of backend\main.py.
- Check that health() appears before triage().
- Refresh the documentation page after the backend terminal finishes reloading.
- Help me find why my FastAPI routes are missing from the documentation.
✔️ Awesome, I've got everything!
Great. Double check that you saved backend\main.py.
ⓧ I'd like to double check the full code
Compare your backend\main.py with this complete version.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class TriageRequest(BaseModel):
issue: str
def triage_issue(issue: str) -> dict[str, str]:
normalized_issue = issue.lower()
if any(keyword in normalized_issue for keyword in ("password", "login", "sign in")):
return {
"category": "Account Access",
"priority": "High",
"next_action": "Verify the user's identity and start the account recovery process.",
}
if any(keyword in normalized_issue for keyword in ("payment", "charged", "refund", "invoice")):
return {
"category": "Billing",
"priority": "High",
"next_action": "Review the transaction and confirm the expected billing outcome.",
}
if any(keyword in normalized_issue for keyword in ("error", "bug", "crash")):
return {
"category": "Technical",
"priority": "Medium",
"next_action": "Collect reproduction steps, environment details, and recent changes.",
}
return {
"category": "General",
"priority": "Low",
"next_action": "Request more detail and route the issue to the support queue.",
}
@app.get("/api/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.post("/api/triage")
def triage(request: TriageRequest) -> dict[str, str]:
return triage_issue(request.issue)
- Return to the browser tab showing http://127.0.0.1:8000/docs.
- Refresh the documentation page.
- Expand GET /api/health.
- Send the health request from its operation panel.
You'll see {"status": "ok"} in the response body. This proves the route is live.
- Expand POST /api/triage.
- Enable request body editing in the operation panel.
- Enter {"issue":"error"} as the request body.
- Submit the triage request.
You'll see Technical with Medium priority. The next action asks for reproduction steps, environment details, and recent changes.
Before you submit the final request, which rule do you expect the word charged to trigger?
- Replace the request body with {"issue":"I was charged twice for my invoice"}.
- Submit the triage request.
You'll see Billing with High priority. The next action tells you to review the transaction and confirm the expected billing outcome.
Does the request return an unexpected category?
- Confirm the request body uses the issue field.
- Check that the Billing branch appears before the Technical branch in triage_issue().
- Verify that charged appears inside the Billing keyword tuple.
- Help me debug an unexpected SupportPulse triage result.
That is the backend contract working from request to response. Your API now validates an issue and returns a deterministic triage result.
Your triage API is live. Next, you'll build the React interface that sends it real support issues.
Create the React Interface
Your FastAPI endpoint can already classify support issues through its interactive documentation. The next goal is to give people a browser interface for that API.
In this step, you'll replace the Vite starter with a React form. The browser will then test the boundary between the frontend origin and the Python API.
In this step, get ready to:
- Build a controlled support-issue form with loading feedback.
- Send the issue to the triage API with a JSON request.
- Style the interface and observe the browser connection barrier.
Build the controlled issue form
A controlled form keeps the textarea value in React state. Each keystroke updates issue. React then uses that value to update the interface.
- In the editor's Explorer sidebar, select frontend\src\App.jsx.
- Replace the starter component with the controlled form below:
import { useState } from 'react'
export default function App() {
const [issue, setIssue] = useState('')
return (
<div className="app-shell">
<header className="hero">
<p className="eyebrow">Local full-stack tool</p>
<h1>SupportPulse</h1>
<p>Turn a support issue into a clear category, priority, and next action.</p>
</header>
<main className="workspace">
<label htmlFor="issue">Support issue</label>
<textarea
id="issue"
rows="8"
value={issue}
onChange={(event) => setIssue(event.target.value)}
placeholder="Example: I was charged twice for my invoice."
required
/>
</main>
</div>
)
}
What does this code do?
- The issue state stores the current textarea value.
- The onChange handler updates that state after each keystroke.
- The value property displays the latest state inside the textarea.
- The header explains what SupportPulse produces from each issue.
- Save frontend\src\App.jsx.
- Return to the frontend page from earlier.
- Type I was charged twice for my invoice. into the textarea.
You'll see the new heading and support-issue field. The text you enter stays visible because React state controls the field.
Don't see the support form?
- Confirm that you edited frontend\src\App.jsx.
- Check that the frontend development process from earlier is still running.
- Compare every opening JSX tag with its closing tag.
- Help me debug my React form.
The form needs distinct states for its request lifecycle. These states let the same button and result area communicate when a request is idle, loading, successful, or unsuccessful.
- In frontend\src\App.jsx, place the following state variables and temporary submit handler below the issue state:
const [result, setResult] = useState(null)
const [status, setStatus] = useState('idle')
const [error, setError] = useState('')
async function handleSubmit(event) {
event.preventDefault()
setStatus('loading')
setError('')
setResult(null)
}
How do these states help?
- The result state holds the category, priority, and next action returned by the API.
- The status state identifies the current request phase.
- The error state holds a readable message when the request fails.
- The temporary handleSubmit() handler changes the interface to its loading state.
- Replace the current <main> section with the form below:
<main className="workspace">
<form className="panel" onSubmit={handleSubmit}>
<label htmlFor="issue">Support issue</label>
<textarea
id="issue"
rows="8"
value={issue}
onChange={(event) => setIssue(event.target.value)}
placeholder="Example: I was charged twice for my invoice."
required
/>
<div className="form-footer">
<span>{issue.trim().length} characters</span>
<button type="submit" disabled={!issue.trim() || status === 'loading'}>
{status === 'loading' ? 'Triaging...' : 'Triage issue'}
</button>
</div>
</form>
</main>
What changes in the form?
- The onSubmit property sends form submissions to handleSubmit().
- The character counter reads the trimmed length of issue.
- The button stays disabled when the textarea is empty.
- The button displays Triaging... while status equals loading.
- Save frontend\src\App.jsx.
- Return to the frontend page.
- Enter I was charged twice for my invoice. in the support-issue field.
- Click Triage issue.
You'll see the button change to Triaging.... The disabled button proves that the loading state is active.
Does the button stay unchanged?
- Check that the form uses onSubmit={handleSubmit}.
- Confirm that setStatus('loading') is inside handleSubmit().
- Verify that the button reads from status.
- Help me debug the loading state.
Connect the form to the triage API
A REST API gives the frontend a URL for sending an issue and receiving a result. The request body uses JSON so its issue field matches the backend request model.
- In frontend\src\App.jsx, find the temporary submit handler:
async function handleSubmit(event) {
event.preventDefault()
setStatus('loading')
setError('')
setResult(null)
}
This current handler prepares the loading state. It does not send the issue beyond the browser.
- Add the API address below the React import:
- Replace the temporary handleSubmit() function with the complete request handler below:
const API_URL = 'http://127.0.0.1:8000/api/triage'
export default function App() {
What is the API address for?
The API_URL constant gives every submission one consistent destination. It points to the existing POST /api/triage endpoint.
async function handleSubmit(event) {
event.preventDefault()
setStatus('loading')
setError('')
setResult(null)
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ issue: issue.trim() }),
})
if (!response.ok) {
throw new Error('The API returned an error.')
}
const data = await response.json()
setResult(data)
setStatus('success')
} catch {
setError('Could not reach the API. Check that FastAPI is running and CORS is configured.')
setStatus('error')
}
}
How does the request work?
- The fetch() call sends a POST request to API_URL.
- The request header identifies the body as application/json.
- The JSON.stringify() call converts the trimmed issue into the API request body.
- The success path stores parsed response data. The error path stores a readable connection message.
- Place the result section below the closing </form> tag:
<section className="panel result-panel" aria-live="polite">
<h2>Triage result</h2>
{status === 'idle' && (
<p className="muted">Submit an issue to see the backend response.</p>
)}
{status === 'loading' && <p className="muted">Checking the triage rules...</p>}
{error && <p className="error">{error}</p>}
{result && (
<div className="result-grid">
<div>
<span className="result-label">Category</span>
<strong>{result.category}</strong>
</div>
<div>
<span className="result-label">Priority</span>
<strong className={`priority ${result.priority.toLowerCase()}`}>
{result.priority}
</strong>
</div>
<div className="action-card">
<span className="result-label">Next action</span>
<p>{result.next_action}</p>
</div>
</div>
)}
</section>
How does the result panel respond?
- The idle branch prompts the learner to submit an issue.
- The loading branch confirms that the request is in progress.
- The error branch displays the message stored in error.
- The result branch displays the returned category, priority, and next action.
- Save frontend\src\App.jsx.
✔️ Awesome, I've got everything!
Your controlled form, request handler, and result states are now saved in frontend\src\App.jsx.
ⓧ I'd like to double check the full code
import { useState } from 'react'
const API_URL = 'http://127.0.0.1:8000/api/triage'
export default function App() {
const [issue, setIssue] = useState('')
const [result, setResult] = useState(null)
const [status, setStatus] = useState('idle')
const [error, setError] = useState('')
async function handleSubmit(event) {
event.preventDefault()
setStatus('loading')
setError('')
setResult(null)
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ issue: issue.trim() }),
})
if (!response.ok) {
throw new Error('The API returned an error.')
}
const data = await response.json()
setResult(data)
setStatus('success')
} catch {
setError('Could not reach the API. Check that FastAPI is running and CORS is configured.')
setStatus('error')
}
}
return (
<div className="app-shell">
<header className="hero">
<p className="eyebrow">Local full-stack tool</p>
<h1>SupportPulse</h1>
<p>Turn a support issue into a clear category, priority, and next action.</p>
</header>
<main className="workspace">
<form className="panel" onSubmit={handleSubmit}>
<label htmlFor="issue">Support issue</label>
<textarea
id="issue"
rows="8"
value={issue}
onChange={(event) => setIssue(event.target.value)}
placeholder="Example: I was charged twice for my invoice."
required
/>
<div className="form-footer">
<span>{issue.trim().length} characters</span>
<button type="submit" disabled={!issue.trim() || status === 'loading'}>
{status === 'loading' ? 'Triaging...' : 'Triage issue'}
</button>
</div>
</form>
<section className="panel result-panel" aria-live="polite">
<h2>Triage result</h2>
{status === 'idle' && (
<p className="muted">Submit an issue to see the backend response.</p>
)}
{status === 'loading' && <p className="muted">Checking the triage rules...</p>}
{error && <p className="error">{error}</p>}
{result && (
<div className="result-grid">
<div>
<span className="result-label">Category</span>
<strong>{result.category}</strong>
</div>
<div>
<span className="result-label">Priority</span>
<strong className={`priority ${result.priority.toLowerCase()}`}>
{result.priority}
</strong>
</div>
<div className="action-card">
<span className="result-label">Next action</span>
<p>{result.next_action}</p>
</div>
</div>
)}
</section>
</main>
</div>
)
}
Compare this reference with your file from top to bottom. Pay close attention to the placement of handleSubmit() before the returned JSX.
- Return to the frontend page.
- Use your browser's menu to display its developer tools.
- Display the browser console.
- Enter I was charged twice for my invoice. in the support-issue field.
Before you submit, do you think the browser will expose the API response to the page?
- Click Triage issue.
You'll see Could not reach the API. Check that FastAPI is running and CORS is configured. in the result panel. The browser console reports that a cross-origin request was blocked.
Why did the browser block the response?
The frontend uses http://localhost:5173. The API uses http://127.0.0.1:8000. These addresses are different browser origins.
The backend currently sends no CORS permission for the frontend origin. The browser therefore keeps the response away from the page.
Seeing a different result?
- Confirm that the API process from earlier is still running at http://127.0.0.1:8000.
- Confirm that the frontend page uses http://localhost:5173.
- Check that API_URL ends with /api/triage.
- Help me inspect the planned browser failure.
Style the form and result card
The interface already exposes its request states. CSS now turns those states into a readable two-panel workspace with clear focus, loading, result, and error treatments.
- In the editor's Explorer sidebar, select frontend\src\App.css.
- Replace the existing starter styles with the foundation below:
:root {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033;
background: #eef3f8;
font-synthesis: none;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
}
button,
textarea {
font: inherit;
}
button:focus-visible,
textarea:focus-visible {
outline: 3px solid #82c8ff;
outline-offset: 2px;
}
What does the style foundation do?
- The root styles establish the page colors and font stack.
- The universal selector makes element dimensions include their borders and padding.
- The body rules remove the browser margin and preserve a usable minimum width.
- The focus rules give keyboard users a visible outline around interactive controls.
- Save frontend\src\App.css.
You'll see a pale page background and updated typography. Selecting the textarea or button shows a blue focus outline.
Is the background unchanged?
- Confirm that frontend\src\main.jsx still imports ./App.css.
- Check that the first selector is :root.
- Help me debug the CSS foundation.
- Append the header styles below the focus rules:
.app-shell {
min-height: 100vh;
}
.hero {
padding: 48px 24px 88px;
color: white;
text-align: center;
background: linear-gradient(135deg, #12365b, #126e82);
}
.hero h1 {
margin: 6px 0 10px;
font-size: clamp(2.4rem, 7vw, 4.6rem);
}
.hero p {
max-width: 650px;
margin: 0 auto;
line-height: 1.6;
}
.eyebrow {
color: #a8e6ee;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.16em;
text-transform: uppercase;
}
How does the header take shape?
- The .hero selector creates the gradient banner and centered layout.
- The heading size scales with the browser width while staying within readable limits.
- The .eyebrow selector creates the compact label above the product name.
- Save frontend\src\App.css.
You'll see SupportPulse inside a blue gradient header. The introductory label appears above the larger product name.
Is the header still plain?
- Check that the header in frontend\src\App.jsx uses className="hero".
- Confirm that the label uses className="eyebrow".
- Help me match the header classes.
- Append the workspace and panel styles below .eyebrow:
.workspace {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
width: min(1040px, calc(100% - 32px));
margin: -48px auto 48px;
}
.panel {
padding: 28px;
border: 1px solid #dce5ee;
border-radius: 18px;
background: white;
box-shadow: 0 18px 45px rgba(18, 54, 91, 0.12);
}
label,
.result-label {
display: block;
margin-bottom: 8px;
color: #53657a;
font-size: 0.8rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
How does the workspace work?
- The .workspace selector places the form beside the result panel.
- The negative top margin pulls both panels over the lower edge of the header.
- The .panel selector gives each area a white card surface.
- The shared label rules create consistent field and result headings.
- Save frontend\src\App.css.
You'll see two white panels arranged side by side. Both panels overlap the bottom of the header.
Are the panels stacked too early?
- Check that .workspace uses two equal grid columns.
- Confirm that both the form and result section use className="panel".
- Help me debug the two-panel grid.
- Append the textarea and form-footer styles below the label rules:
textarea {
width: 100%;
resize: vertical;
padding: 14px;
border: 1px solid #bdcad8;
border-radius: 12px;
color: #172033;
background: #fbfdff;
line-height: 1.5;
}
.form-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 16px;
color: #66788d;
font-size: 0.85rem;
}
What changes around the form?
- The textarea fills its panel while preserving vertical resizing.
- Its padding and border create a clear input area.
- The .form-footer selector places the character count opposite the submit button.
- Save frontend\src\App.css.
You'll see a bordered textarea that fills the form panel. The character count sits on the opposite side from the submit button.
Does the textarea overflow?
- Confirm that the universal selector sets box-sizing: border-box.
- Check that the textarea width is 100%.
- Help me fix the textarea layout.
- Append the button and result-layout styles below .form-footer:
button {
padding: 11px 18px;
border: 0;
border-radius: 999px;
color: white;
background: #126e82;
font-weight: 800;
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.result-panel h2 {
margin-top: 0;
}
.result-grid {
display: grid;
gap: 18px;
}
.result-grid strong {
font-size: 1.15rem;
}
How do the controls communicate state?
- The button uses a rounded shape and a high-contrast background.
- The disabled style changes the cursor and lowers opacity.
- The result grid separates each returned value with consistent spacing.
- Save frontend\src\App.css.
You'll see a rounded blue submit button. Clearing the textarea makes that button visibly disabled.
Is the button unstyled?
- Check that the selector is the lowercase element name button.
- Confirm that the disabled rule includes the :disabled state.
- Help me debug the button styles.
- Append the priority and error colors below the result-grid styles:
.priority {
display: inline-block;
padding: 5px 10px;
border-radius: 999px;
}
.priority.high {
color: #8a2c1b;
background: #ffe1da;
}
.priority.medium {
color: #755300;
background: #fff0bf;
}
.priority.low {
color: #22603c;
background: #dff4e7;
}
.error {
padding: 14px;
border-radius: 12px;
color: #8a2c1b;
background: #ffe1da;
line-height: 1.5;
}
What do the feedback colors show?
- The base priority style creates a compact badge.
- The high, medium, and low selectors give each supported priority a distinct color.
- The error style places failed requests inside a readable warning panel.
- Save frontend\src\App.css.
You'll see the existing connection message inside a pale red panel. Its darker text keeps the failure readable.
Is the error still plain text?
- Confirm that the message in frontend\src\App.jsx uses className="error".
- Check that the stylesheet selector begins with .error.
- Help me match the error class.
- Insert the action-card and muted-text styles directly above .error:
.action-card {
padding: 16px;
border-radius: 12px;
background: #eef7f8;
}
.action-card p {
margin: 0;
line-height: 1.55;
}
.muted {
color: #687b90;
}
How are quiet details styled?
- The action card gives the returned next action its own surface.
- The action paragraph removes the browser's default outer margin.
- The muted style lowers the emphasis of idle and loading messages.
- Save frontend\src\App.css.
- Refresh the frontend page.
You'll see the idle instruction in muted blue-gray text. The refresh resets the interface from the earlier error state to its idle state.
Is the idle message unchanged?
- Confirm that the idle paragraph uses className="muted".
- Check that .muted appears before .error in the stylesheet.
- Help me debug the muted text.
- Append the responsive layout rule at the end of frontend\src\App.css:
@media (max-width: 760px) {
.workspace {
grid-template-columns: 1fr;
}
.hero {
padding-bottom: 76px;
}
}
How does the layout adapt?
The media rule switches the workspace to one column on narrow screens. It also adjusts the header spacing for the stacked panels.
- Save frontend\src\App.css.
- Narrow the browser window below 760px.
You'll see the result panel move below the form. Widening the window restores the two-column layout.
Does the layout stay in two columns?
- Confirm that the media rule appears after the other selectors.
- Check that the workspace rule inside it uses grid-template-columns: 1fr.
- Help me debug the responsive layout.
✔️ Awesome, I've got everything!
Your form, result panel, feedback states, and responsive layout are now styled in frontend\src\App.css.
ⓧ I'd like to double check the full code
:root {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033;
background: #eef3f8;
font-synthesis: none;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
}
button,
textarea {
font: inherit;
}
button:focus-visible,
textarea:focus-visible {
outline: 3px solid #82c8ff;
outline-offset: 2px;
}
.app-shell {
min-height: 100vh;
}
.hero {
padding: 48px 24px 88px;
color: white;
text-align: center;
background: linear-gradient(135deg, #12365b, #126e82);
}
.hero h1 {
margin: 6px 0 10px;
font-size: clamp(2.4rem, 7vw, 4.6rem);
}
.hero p {
max-width: 650px;
margin: 0 auto;
line-height: 1.6;
}
.eyebrow {
color: #a8e6ee;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.workspace {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
width: min(1040px, calc(100% - 32px));
margin: -48px auto 48px;
}
.panel {
padding: 28px;
border: 1px solid #dce5ee;
border-radius: 18px;
background: white;
box-shadow: 0 18px 45px rgba(18, 54, 91, 0.12);
}
label,
.result-label {
display: block;
margin-bottom: 8px;
color: #53657a;
font-size: 0.8rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
textarea {
width: 100%;
resize: vertical;
padding: 14px;
border: 1px solid #bdcad8;
border-radius: 12px;
color: #172033;
background: #fbfdff;
line-height: 1.5;
}
.form-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 16px;
color: #66788d;
font-size: 0.85rem;
}
button {
padding: 11px 18px;
border: 0;
border-radius: 999px;
color: white;
background: #126e82;
font-weight: 800;
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.result-panel h2 {
margin-top: 0;
}
.result-grid {
display: grid;
gap: 18px;
}
.result-grid strong {
font-size: 1.15rem;
}
.priority {
display: inline-block;
padding: 5px 10px;
border-radius: 999px;
}
.priority.high {
color: #8a2c1b;
background: #ffe1da;
}
.priority.medium {
color: #755300;
background: #fff0bf;
}
.priority.low {
color: #22603c;
background: #dff4e7;
}
.action-card {
padding: 16px;
border-radius: 12px;
background: #eef7f8;
}
.action-card p {
margin: 0;
line-height: 1.55;
}
.muted {
color: #687b90;
}
.error {
padding: 14px;
border-radius: 12px;
color: #8a2c1b;
background: #ffe1da;
line-height: 1.5;
}
@media (max-width: 760px) {
.workspace {
grid-template-columns: 1fr;
}
.hero {
padding-bottom: 76px;
}
}
Compare this reference with your stylesheet in order. Confirm that .action-card and .muted appear before .error.
- Return the browser to a wide window.
- Keep the browser console visible.
- Enter I was charged twice for my invoice. in the support-issue field.
Before you submit again, which interface state do you expect the styled result panel to display?
- Click Triage issue.
You'll see the loading label briefly. The styled red panel then displays Could not reach the API. Check that FastAPI is running and CORS is configured..
The browser console reports a cross-origin block. This confirms that the React request reached the browser security boundary as intended.
That's the browser boundary exposed: your React interface can send the request, but it cannot read the response yet. Next up, you'll grant the frontend origin explicit permission and watch the result card come to life.
Connect React to FastAPI with CORS
Last step, your React form sent a JSON request to FastAPI. The browser withheld the response because CORS did not permit the Vite origin.
This step adds an explicit CORS policy for http://localhost:5173. Your frontend gains permission to read API responses without granting that permission to every website.
In this step, get ready to:
- Define a narrow browser access policy for the Vite frontend.
- Resubmit the request that the browser previously blocked.
- Test the interface across its loading, success, and error states.
Configure the CORS policy
CORS middleware adds the response headers that browsers use to decide whether frontend code can read a cross-origin response. The policy can limit access by origin, request method, and request header.
- In the Visual Studio Code Explorer sidebar, return to backend\main.py.
- Find the import section at the top of the file.
- Replace the import section with this three-line group:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
What Does This Import Add?
- The CORSMiddleware import gives FastAPI a browser access policy that you can attach to the application.
- The FastAPI import still creates the application.
- The BaseModel import still validates the request body.
- Save backend\main.py.
- Watch the existing backend terminal until the automatic reload finishes.
You should see the development server reload without an import failure.
Does the Import Fail?
- Confirm that from fastapi.middleware.cors import CORSMiddleware appears on one line.
- Check that your active environment still uses the dependencies from backend\requirements.txt.
- Help me debug the CORS middleware import in my FastAPI project.
The import makes the middleware available. The next block attaches a policy directly after the FastAPI application is created.
- Find app = FastAPI() near the top of backend\main.py.
- Add this middleware configuration directly below that line:
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=False,
allow_methods=["POST"],
allow_headers=["Content-Type"],
)
What Does This Policy Allow?
- The add_middleware() call applies the policy to responses from the FastAPI application.
- The allow_origins list grants browser access only to http://localhost:5173.
- The allow_credentials=False setting keeps credential sharing disabled.
- The allow_methods list permits the frontend's POST request.
- The allow_headers list permits the Content-Type header used for JSON.
- Save backend\main.py.
- Watch the backend terminal until the automatic reload finishes.
The reload should complete with the new CORS policy active.
Does the Server Stop Reloading?
- Confirm that the middleware block appears below app = FastAPI().
- Check that every opening parenthesis has a matching closing parenthesis.
- Check that the four middleware settings remain indented inside app.add_middleware().
- Help me fix my FastAPI CORS middleware configuration.
✔️ Awesome, I've got everything!
Your saved backend\main.py now contains the explicit CORS policy.
ⓧ I'd like to double check the full code
- Compare your complete backend\main.py file with this reference:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=False,
allow_methods=["POST"],
allow_headers=["Content-Type"],
)
class TriageRequest(BaseModel):
issue: str
def triage_issue(issue: str) -> dict[str, str]:
normalized_issue = issue.lower()
if any(keyword in normalized_issue for keyword in ("password", "login", "sign in")):
return {
"category": "Account Access",
"priority": "High",
"next_action": "Verify the user's identity and start the account recovery process.",
}
if any(keyword in normalized_issue for keyword in ("payment", "charged", "refund", "invoice")):
return {
"category": "Billing",
"priority": "High",
"next_action": "Review the transaction and confirm the expected billing outcome.",
}
if any(keyword in normalized_issue for keyword in ("error", "bug", "crash")):
return {
"category": "Technical",
"priority": "Medium",
"next_action": "Collect reproduction steps, environment details, and recent changes.",
}
return {
"category": "General",
"priority": "Low",
"next_action": "Request more detail and route the issue to the support queue.",
}
@app.get("/api/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.post("/api/triage")
def triage(request: TriageRequest) -> dict[str, str]:
return triage_issue(request.issue)
Retry the blocked request
The same frontend request now has permission to cross the browser boundary. Retrying it proves that the fix lives in the backend policy.
- Return to the SupportPulse browser tab at http://localhost:5173.
- Refresh the page.
- Enter I was charged twice for my invoice in the Support issue field.
Before you submit, predict whether the browser can display the API response this time.
- Click Triage issue.
You should briefly see Triaging... while the request is running. The result card should then show Billing with High priority.
The next action should read Review the transaction and confirm the expected billing outcome. That connection is live from the browser through to the Python rule engine.
Still Seeing the Connection Error?
- Confirm that the frontend page uses http://localhost:5173.
- Confirm that allow_origins contains that exact origin.
- Check that the FastAPI terminal is still running after the reload.
- Help me debug why React still cannot read my FastAPI response.
Test every interface state
A useful interface explains what is happening while a request runs. It also recovers clearly when the backend becomes unavailable.
- Replace the issue with My app crashes with an error after login.
- Switch back to the existing backend terminal.
- Press Ctrl+C to stop the FastAPI server.
- Return to the SupportPulse browser tab.
Before you submit, predict whether the interface stays in its loading state or moves to its error state.
- Click Triage issue.
You should briefly see Checking the triage rules... before the request fails. The panel should then show Could not reach the API. Check that FastAPI is running and CORS is configured..
- Restart the backend from the existing backend terminal by running this command:
fastapi dev
What Does This Command Do?
The command starts FastAPI in development mode from the backend folder. It restores the local API at http://127.0.0.1:8000 with automatic reload enabled.
- Wait for the backend startup to finish.
You should see that the development server is available at http://127.0.0.1:8000 again.
Does the Backend Fail to Restart?
- Confirm that the terminal is still inside the backend folder.
- Confirm that the project virtual environment remains active.
- Help me restart the SupportPulse FastAPI development server.
- Return to the SupportPulse browser tab.
- Keep My app crashes with an error after login in the issue field.
Before you resubmit, predict which rule branch the words crashes and error will trigger.
- Click Triage issue.
You should see a Technical result with Medium priority. The next action should read Collect reproduction steps, environment details, and recent changes..
You have now confirmed the Billing and Technical branches. SupportPulse also stays readable while loading and when its backend is unavailable.
Seeing the Wrong Triage Result?
- Confirm that the issue contains crashes or error.
- Check that the Technical rule remains above the General return in backend\main.py.
- Confirm that the browser no longer displays the earlier connection error.
- Help me debug an unexpected SupportPulse triage result.
Secret mission
Escalate Security Incidents
SupportPulse currently sends urgent breach reports to the General queue. Extend the classifier with a Critical security path. Give the new priority a distinct badge that draws attention to immediate escalation.
Clean Up Your Resources
Clean Up Your Resources
This local project has no cloud resources or ongoing costs. Choose whether to keep the workspace active, pause the two servers, or delete the files.
Resources you used:
- The supportpulse workspace, including backend\main.py, backend\requirements.txt, the backend\.venv virtual environment, the frontend folder, and its React files.
- The FastAPI development server process at http://127.0.0.1:8000, currently running from backend.
- The Vite development server process at http://localhost:5173, currently running from frontend.
Keep everything running
No action needed. Choose this if you are still testing SupportPulse or plan to extend its rule engine.
- Keep both terminal processes running in Visual Studio Code while you continue testing SupportPulse.
- Continue submitting issues at http://localhost:5173 to compare the Security branch with the existing classifications.
- Keep the supportpulse workspace so you can reuse it in later deployment projects.
Pause - I'll come back to this later
Stop the local servers to free their terminal sessions. Every project file stays available for your next session.
- Switch back to the Vite terminal in the Terminal panel.
- Press Ctrl+C to stop Vite.
- Switch to the FastAPI terminal in the Terminal panel.
- Press Ctrl+C to stop FastAPI.
You will see a command prompt return in each terminal. The supportpulse workspace remains ready for another session.
Delete - I don't want to use this again
Deleting the workspace permanently removes your backend code, frontend code, installed packages, and virtual environment. Your tested SupportPulse app cannot be recovered from this folder afterward.
- Switch back to the Vite terminal in the Terminal panel.
- Press Ctrl+C to stop Vite.
- Switch to the FastAPI terminal in the Terminal panel.
- Press Ctrl+C to stop FastAPI.
Both local server processes are now stopped. The next actions remove their project files.
- Close Visual Studio Code.
- Press the Windows key to open search.
- Type File Explorer and press Enter.
- Navigate to the location where you created the supportpulse folder during Step 1.
- Select the supportpulse folder.
- Press Shift+Delete to request permanent deletion.
- Confirm the permanent deletion in the Windows dialog.
- Refresh the parent location in File Explorer.
You should no longer see the supportpulse folder. The workspace, virtual environment, installed frontend packages, and application code are now removed.
Windows Cannot Delete the Folder?
- Close any remaining terminal whose current path is inside supportpulse.
- Try the permanent deletion again after Windows releases the project files.
Help me find which Windows process is preventing deletion of my supportpulse folder.
Nice Work!
Nice Work!
You did it! SupportPulse now turns support issues into visible triage decisions through React and FastAPI.
You've learned how to:
- Manage a controlled support form with React state. Render clear loading, success, and error feedback around every request.
- Create a FastAPI REST API that validates incoming JSON with Pydantic. Apply deterministic Python rules to produce a complete triage decision.
- Diagnose a real browser CORS block between separate local origins. Restore the connection with an explicit allowed origin. Send support issues from JavaScript to Python through asynchronous requests.
- Complete the optional Secret Mission by adding security detection ahead of every existing rule. Display security incidents with a Critical badge. Return an incident-response action for immediate escalation.
Ready to quiz yourself?