Build an AI Study Focus Detector
Train a webcam classifier and build a browser dashboard with live predictions.
Introduction
30 Second Summary
A study session can feel productive until a phone break stretches longer than expected. Without an outside signal, distraction is easy to underestimate.
In this project, you will build StudyGuard by training a custom image classifier with Google Teachable Machine. A local browser dashboard will display live webcam predictions from your improved model.
What You'll Build
Picture StudyGuard shifting among Focused, Using Phone, Away, and Other after you press Start Camera, with every probability bar moving as the webcam scene changes.
By the end of this project, you'll have:
- A four-state webcam demo you can use to demonstrate Focused, Using Phone, Away, and Other classifications.
- Direct evidence of model improvement that you can demonstrate by repeating the same unrelated-scene test after expanding the training data.
- A local StudyGuard dashboard you can run in Chrome to show the webcam, the top prediction, and every class probability.
- Secret Mission: An uncertainty gate that makes StudyGuard display Unsure when the top probability falls below 0.60.
Are there any prerequisites?
A Mac with a webcam plus basic Java and HTML knowledge is enough to begin. The free toolset requires no previous JavaScript or machine learning experience.
Before We Start
This is your moment to decide what StudyGuard should recognize before you start building. You are treating it as an experiment in how training data shapes an AI classifier, not as a tool for judging anyone else's behavior.
Prepare Your Mac and Local Workspace
StudyGuard needs a supported browser before it can access your webcam. It also needs a local origin before it can load model files reliably.
You'll prepare Google Chrome. You'll also prepare Visual Studio Code.
You'll verify Python 3.15.0. Python will run a local development server for your first workspace page.
In this step, get ready to:
- Prepare Google Chrome and Visual Studio Code for the project.
- Verify Python 3.15.0 on your Mac.
- Serve the StudyGuard workspace page locally.
Check Chrome and Visual Studio Code
Chrome displays your local app. Visual Studio Code gives you one place to edit its files and run its server.
- Press Cmd+Space to open your Mac search bar.
- Type Google Chrome.
- Check whether Google Chrome appears in the results.
- Press Escape to close the search bar.
- Press Cmd+Space to open the search bar again.
- Type Visual Studio Code.
- Check whether Visual Studio Code appears in the results.
Which result matches your Mac?
✔️ Both apps are installed
Both tools are ready to launch. No installation is needed.
ⓧ One or both apps are missing
If Google Chrome is missing:
- Visit the Google Chrome installation page.
- Download the macOS installer.
- Open googlechrome.dmg from Downloads in Finder.
- Drag Google Chrome into Applications.
If Visual Studio Code is missing:
- Visit the Visual Studio Code macOS setup page.
- Download the macOS installer.
- Open the downloaded .dmg from Downloads in Finder.
- Drag Visual Studio Code into Applications.
- Press Cmd+Space to open your Mac search bar.
- Type Google Chrome.
- Press Enter to launch Chrome.
- Press Cmd+Space to open the search bar again.
- Type Visual Studio Code.
- Press Enter to launch Visual Studio Code.
Your browser and editor are ready. You now have the two desktop tools used throughout StudyGuard.
App missing from search?
- Check that the app was dragged into the Applications folder.
- Reopen the downloaded disk image if the drag did not finish.
Need help finding an installed app? Help me find Google Chrome or Visual Studio Code on my Mac after installation.
Verify Python 3.15.0
Python provides the small web server used in this project. A version check tells you whether the required release is already available.
- Press Cmd+Space to open your Mac search bar.
- Type Terminal.
- Press Enter to launch Terminal.
- Check your installed Python version by running this command:
python3 --version
What does this command check?
The python3 command asks the installed Python interpreter to report its version. The result determines whether you can continue or need the current macOS package.
Choose the tab that matches the terminal result.
✔️ I see version 3.15.0 or higher
Python is ready. Your Mac can run the local server command used later in this step.
ⓧ I see an older version
Your installed release is below the project requirement. Install Python 3.15.0 before continuing.
- Close Terminal.
- Visit the Python 3.15.0 official release page.
- Download the macOS installer.
- Open the downloaded installer from Downloads in Finder.
- Complete the installation prompts.
Why reopen Terminal?
A new Terminal session picks up the command path installed with Python. This gives the version check access to the new interpreter.
- Press Cmd+Space to open your Mac search bar.
- Type Terminal.
- Press Enter to reopen Terminal.
- Verify the new Python installation by running this command:
python3 --version
What should I see?
The command prints the Python version available to the new Terminal session. Continue when it reports 3.15.0.
ⓧ Command not found
Python is unavailable from your current Terminal session. Install Python 3.15.0 from Python.org.
- Close Terminal.
- Visit the Python 3.15.0 official release page.
- Download the macOS installer.
- Open the downloaded installer from Downloads in Finder.
- Complete the installation prompts.
Why reopen Terminal?
The installation adds the Python command to your Mac. A new Terminal session loads that updated command path.
- Press Cmd+Space to open your Mac search bar.
- Type Terminal.
- Press Enter to reopen Terminal.
- Verify the Python installation by running this command:
python3 --version
What should I see?
The command now prints the installed Python version. Continue when it reports 3.15.0.
Create and serve the workspace
The studyguard folder keeps every project file in one known place. Opening that folder as a workspace also makes the Visual Studio Code terminal start from the correct location.
- Click your Mac Desktop to make it active.
- Open File in the menu bar.
- Select New Folder.
- Enter studyguard as the folder name.
- Press Return.
You'll see the studyguard folder on your Desktop. That visible folder confirms the workspace now exists.
Visual Studio Code may show a Workspace Trust dialog when this folder opens. You created this empty folder yourself.
- Switch back to Visual Studio Code.
- Open File in the menu bar.
- Select Open Folder....
- Select the studyguard folder from your Desktop.
- Click Open.
- Select Yes, I trust the authors if the Workspace Trust dialog appears.
The Explorer view now shows studyguard as the open workspace. This confirms Visual Studio Code is working inside the correct folder.
- Select the Explorer view in the left Activity Bar.
- Click New File... in the Explorer toolbar.
- Enter index.html.
- Press Enter to create the file.
You'll see index.html inside the Explorer. The empty file also opens in the editor.
- Add the workspace heading to index.html by replacing its empty contents with the code below:
<h1>StudyGuard workspace ready</h1>
What does this code do?
The <h1> element creates the main heading for the temporary workspace page. Seeing that heading in Chrome proves the browser is receiving the correct file.
- Save index.html by pressing Cmd+S.
✔️ Awesome, I've got everything!
Your workspace file is saved. Continue to the local server.
ⓧ I'd like to double check the full code
<h1>StudyGuard workspace ready</h1>
The integrated terminal runs commands from the open workspace folder. A local server stays active while its terminal session is running.
- Open View in the Visual Studio Code menu bar.
- Select Terminal.
- Start the local server from the open studyguard workspace by running this command:
python3 -m http.server 8000 --bind 127.0.0.1
What does this command do?
The http.server module serves files from the open studyguard folder on port 8000. The --bind 127.0.0.1 option keeps the listener on your Mac.
This server is intended for local development. Closing its terminal session stops the page from being served.
Before you load the page, what heading do you expect Chrome to show?
- Switch back to Google Chrome.
- Select the address bar.
- Type http://localhost:8000.
- Press Return.
You'll see StudyGuard workspace ready as the page heading. You'll also see request activity in the Visual Studio Code terminal.
Your first StudyGuard page is live. Its local origin is ready for the model and webcam work ahead.
Workspace page not loading?
- Confirm the server command is still running in the Visual Studio Code terminal.
- Check that index.html appears directly inside the studyguard folder.
- Confirm that Chrome is using http://localhost:8000.
Need help with the local page? Help me troubleshoot my StudyGuard local server.
Your Mac is ready for local AI development. Next, you'll train the first version of your classifier and test where its limited data falls short.
Train an Initial Three-Class Model
Your StudyGuard workspace is already live in Chrome. Its local server is ready for the dashboard you build later.
Google Teachable Machine lets you train an image classification model from webcam examples.
This first model knows only three labels. You will test what happens when the camera sees something outside that set.
In this step, get ready to:
- Create an image project with three study-state classes.
- Gather balanced webcam examples in one lighting condition.
- Test how the trained model handles an unfamiliar scene.
Create the three classes
A class is a named category that the model can predict. The exact class names matter because StudyGuard displays them later.
- In Chrome, visit the official Teachable Machine image-project page.
- Start an image project from that page.
- Rename the first class to Focused.
- Rename the second class to Using Phone.
- Add a third class to the project.
- Name the third class Away.
- Remove any additional classes from the project.
You should see exactly three classes named Focused, Using Phone, and Away.
Why Teachable Machine?
Teachable Machine keeps the training loop in your browser. You can focus on labels, examples, and testing.
A notebook workflow adds programming setup before you can see the model react. This approach makes your data choices visible first.
Collect balanced webcam examples
Training examples are webcam snapshots that show the model what each class looks like. A balanced dataset gives each class a similar amount of evidence.
Before you enable the webcam
Teachable Machine can process webcam data entirely on your device. Your camera feed does not need to leave your computer.
- Choose Allow while visiting the site when Chrome asks for camera access.
- Set your desk lighting to one condition for this first training round.
- Capture Focused webcam examples of yourself seated at your desk.
- Capture Using Phone webcam examples of yourself holding your phone.
- Capture Away webcam examples of the desk after you leave the frame.
- Compare the example count shown for each class.
- Add examples to the smallest class until the three counts are reasonably balanced.
Each class should now show a similar example count. The snapshots should all come from the same lighting condition.
Webcam examples not appearing?
- Check Chrome's camera permission for the Teachable Machine page.
- Close any other app that is using your Mac webcam.
Ask for help with a missing Teachable Machine webcam preview.
Training can take a little while, so a brief wait is expected.
- Start training with the project training control.
When training finishes, you should see the live preview react to your webcam. Your three class names should appear with changing probability values.
Test the model's limits
How does the live preview decide?
Inference is the model's process of classifying a new webcam frame.
The live preview assigns each class a probability. The highest value becomes the top prediction.
- Present a seated study scene to the live preview.
You should see Focused rise to the top.
- Present a phone-use scene to the live preview.
You should see Using Phone rise to the top.
- Move out of the webcam frame.
You should see Away rise to the top.
Before you try an unfamiliar scene, what do you think the preview will do with an input outside its three classes?
- Cover the webcam for the unrelated-input test.
- Wait for the live preview's top result to settle.
The preview still chooses Focused, Using Phone, or Away. The unfamiliar scene receives an incorrect study-state label.
Why did the model choose a study state?
Your model has no category for this frame. Its only possible answers are the three classes in its label set.
The winning label describes the closest known pattern. This result exposes the limit of the current training data.
- Record the incorrect winning label here: the incorrect winning study label.
- Keep the trained model in Teachable Machine without exporting it.
You caught the model making a confident choice from an incomplete label set. Next, you will improve the dataset and repeat the same test.
Expand the Dataset and Retrain
Your first model recognized the three intended study states. The unrelated scene exposed a gap in its classification: the model had only three known classes for every input.
This step improves dataset coverage in your existing Google Teachable Machine project. A fourth class gives unfamiliar scenes a category the model can learn.
In this step, get ready to:
- Add an Other class for unfamiliar scenes.
- Expand the three original classes across more viewing conditions.
- Retrain the four-class model.
Add the Other class
The original labels describe study states only. Other gives unrelated scenes a place in the model.
- Switch back to your Teachable Machine image project from the previous step.
You should see the trained classes named Focused, Using Phone, and Away.
- Use the control in the class list to add a fourth class.
- Name the new class Other.
Your project now shows four classes. The new Other class is ready for examples that do not represent a study state.
- Use the webcam capture control inside Other to collect examples of unrelated objects.
- Collect covered-camera examples inside Other.
You should see the example count rise inside Other. Its thumbnails should include objects plus dark covered-camera frames.
- Collect non-study close-ups inside Other.
The fourth class now covers the three unfamiliar-scene categories that the first model was missing.
Broaden the original classes
Broader coverage includes new angles. It also includes different distances.
A second lighting condition completes the variation. These changes help each class represent more than one camera setup.
- Capture additional Focused examples across those three variations.
- Capture additional Using Phone examples across those three variations.
You should see fresh thumbnails from different viewpoints inside both classes.
- Capture additional Away examples across those three variations.
All three original classes should now include varied angles. They should also include varied distances plus more than one lighting condition.
- Compare the visible example counts across all four classes.
The counts should remain reasonably close. This prevents one class from dominating the dataset through volume alone.
- Capture extra examples for any class with a noticeably smaller count.
Your four class counts should now look reasonably balanced.
Why balance the classes?
Each training example contributes evidence about its class. A reasonably balanced dataset gives every class a comparable opportunity to influence the model.
Compare the four-class predictions
Retraining rebuilds the model from the expanded dataset. The updated live preview can then perform inference across all four classes.
- Retrain the model using the project's training control.
When training finishes, you should see the live preview ready to classify scenes with all four labels.
Before you repeat the original unrelated-scene test, which class do you expect to win now?
- Present the same unrelated object or covered-camera scene that failed in the previous step.
You should see Other become the top prediction. That result shows the model has learned a fallback for scenes outside the three study states.
- Present a Focused scene from one of your tested conditions.
- Present a Using Phone scene from one of your tested conditions.
- Present an Away scene from one of your tested conditions.
- Move to a second angle or lighting condition.
- Repeat all three intended-state tests.
Each intended class should become the top prediction in both conditions. The model now responds beyond the single setup used for its first training run.
- Compare the new Other result with the incorrect study label you recorded earlier.
That comparison gives you direct evidence that a wider label set can change how a classifier handles unfamiliar input.
Still seeing a study label?
- Add more examples of that unrelated scene to Other.
- Check that all four class counts remain reasonably balanced.
- Retrain the model with the revised dataset.
The preview then gives you a fresh result for the same scene. Help me improve an Other class that still loses to a study label.
That's a meaningful improvement: your model now recognizes varied study states plus unfamiliar scenes. Next, you'll build the static StudyGuard dashboard that presents its predictions.
Build the StudyGuard Dashboard
Your improved model now recognizes four study scenes in Teachable Machine. You also proved that broader training examples help its classification behavior handle unfamiliar input.
The model preview proves the classifier works. A custom interface gives you control over how people start the camera and inspect each prediction.
In this step, you build that interface in Visual Studio Code with semantic HTML structure and responsive CSS styling.
In this step, get ready to:
- Replace the temporary workspace page with the StudyGuard dashboard structure.
- Style the dashboard as a responsive two-column interface.
- Inspect the static camera controls and prediction panel in the browser.
Build the dashboard structure
The page structure separates the introduction from the camera area. It also gives prediction results their own panel.
- In the Visual Studio Code editor from earlier, switch back to index.html.
- Replace the temporary heading with this document shell:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>StudyGuard Focus Detector</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main class="app-shell">
<header class="hero">
<p class="eyebrow">Custom browser AI</p>
<h1>StudyGuard</h1>
<p>Train the data, test the limits, and inspect every prediction.</p>
</header>
</main>
</body>
</html>
What Does This Structure Do?
- The document metadata gives the page a browser title and a mobile-friendly viewport.
- The styles.css link connects the stylesheet you create next.
- The app-shell element contains the complete interface.
- The hero introduces the dashboard and its purpose.
- Save index.html.
- Return to the local StudyGuard page in your browser.
- Refresh the page.
You should see the StudyGuard heading with its eyebrow text and description. The page still uses plain browser styling.
Still Seeing the Temporary Heading?
Check that you replaced the contents of the existing index.html file inside studyguard. Confirm that the file is saved before refreshing.
If the old page remains visible, use Help me find why Chrome still shows my old index.html content.
The header now gives StudyGuard an identity. The next structure adds the controls and result area that make the project demonstrable.
- In index.html, find the closing </header> line.
- Paste this structure below that line:
<section class="dashboard" aria-label="StudyGuard dashboard">
<div class="camera-card">
<div id="webcam-container" class="webcam-container">
<p id="camera-placeholder">Your webcam will appear here.</p>
</div>
<button id="start-button" type="button">Start Camera</button>
<p id="error-message" class="error-message" role="alert"></p>
</div>
<div class="result-card">
<p class="result-label">Current prediction</p>
<h2 id="status-text">Waiting</h2>
<p id="confidence-text">Press Start Camera to begin.</p>
<div id="label-container" class="prediction-list" aria-live="polite"></div>
</div>
</section>
<footer>
<p>Predictions are estimates from your training examples, not facts about a person.</p>
</footer>
How Is the Dashboard Organized?
- The camera-card holds the webcam placeholder and start control.
- The result-card holds the current status and probability list.
- The empty label-container gives the live prediction code a destination in the next step.
- The footer states that model predictions depend on training examples.
- Save index.html.
- Refresh the local StudyGuard page.
You should see the camera placeholder and the Start Camera button. You should also see a Waiting result with an empty area below it.
Dashboard Content Missing?
Confirm that the new section sits after </header> and before </main>. A section outside the main element can make the document structure differ from the reference.
Use Help me compare my StudyGuard HTML structure with the expected dashboard.
✔️ Awesome, I've got everything!
Your semantic StudyGuard dashboard now has a header, camera card, result card, and footer.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>StudyGuard Focus Detector</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main class="app-shell">
<header class="hero">
<p class="eyebrow">Custom browser AI</p>
<h1>StudyGuard</h1>
<p>Train the data, test the limits, and inspect every prediction.</p>
</header>
<section class="dashboard" aria-label="StudyGuard dashboard">
<div class="camera-card">
<div id="webcam-container" class="webcam-container">
<p id="camera-placeholder">Your webcam will appear here.</p>
</div>
<button id="start-button" type="button">Start Camera</button>
<p id="error-message" class="error-message" role="alert"></p>
</div>
<div class="result-card">
<p class="result-label">Current prediction</p>
<h2 id="status-text">Waiting</h2>
<p id="confidence-text">Press Start Camera to begin.</p>
<div id="label-container" class="prediction-list" aria-live="polite"></div>
</div>
</section>
<footer>
<p>Predictions are estimates from your training examples, not facts about a person.</p>
</footer>
</main>
</body>
</html>
Style the dashboard
The HTML establishes meaning and order. Styling turns those elements into a dashboard with clear visual groups.
- In the Visual Studio Code Explorer sidebar, use the new-file control.
- Name the file styles.css.
- Add the page foundation by pasting this code:
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #08111f;
color: #e8eef8;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
background:
radial-gradient(circle at top left, rgba(46, 213, 115, 0.16), transparent 35%),
linear-gradient(145deg, #08111f, #101b31);
}
button {
font: inherit;
}
What Does the Foundation Control?
- The root styles establish the dark color scheme and shared typography.
- The universal selector makes element dimensions include their borders and padding.
- The body fills the browser height with layered gradients.
- The button inherits the page font for a consistent interface.
- Save styles.css.
- Refresh the local page.
You should see a dark blue background with a green glow in the top-left corner. The page text should use a clean system font.
Page Still Using a White Background?
Confirm that styles.css sits beside index.html inside studyguard. Check that the stylesheet link still spells the filename exactly.
Use Help me debug why styles.css is not loading on my StudyGuard page.
The page now has its visual foundation. The next rules constrain the content width and distinguish the introductory text.
- At the bottom of styles.css, append these layout and label styles:
.app-shell {
width: min(100% - 2rem, 980px);
margin: 0 auto;
padding: 3rem 0 2rem;
}
.hero {
margin-bottom: 1.5rem;
}
.eyebrow,
.result-label {
margin: 0 0 0.35rem;
color: #81e6b0;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
How Does the Page Gain Structure?
- The shell keeps the content centered within a maximum width.
- The shell padding gives the page breathing room above and below the dashboard.
- The eyebrow and result label share a compact green style.
- Save styles.css.
- Refresh the browser.
You should see the content centered with space around its edges. The introductory label and Current prediction label should appear as small green uppercase text.
Content Not Centered?
Check that the selector begins with .app-shell. Confirm that index.html still uses the matching app-shell class.
Use Help me find why my StudyGuard app-shell styles are not applying.
The shell now controls the page boundary. Typography and a grid will make the dashboard hierarchy visible.
- Append these heading and dashboard rules to styles.css.
h1,
h2,
p {
margin-top: 0;
}
h1 {
margin-bottom: 0.5rem;
font-size: clamp(2.5rem, 8vw, 5rem);
line-height: 0.95;
}
.hero > p:last-child,
#confidence-text,
footer {
color: #aab8cd;
}
.dashboard {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(280px, 0.9fr);
gap: 1rem;
}
Why Use a Grid?
- The heading scales with the available browser width.
- Muted text separates supporting information from the main status.
- The grid gives the camera area slightly more width than the result panel.
- The gap keeps both dashboard columns visually separate.
- Save styles.css.
- Refresh the page.
You should see a large StudyGuard heading. The camera and result areas should now sit in two columns.
Dashboard Still Stacked?
Make sure the browser window is wider than the responsive breakpoint used later. Check that both cards remain inside the element with the dashboard class.
Use Help me debug why the StudyGuard dashboard grid is not forming two columns.
The grid positions the two main areas. Card surfaces make that separation clear at a glance.
- Append these card and state styles to styles.css.
.camera-card,
.result-card {
padding: 1rem;
border: 1px solid #263653;
border-radius: 1.25rem;
background: rgba(10, 22, 40, 0.88);
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.24);
}
.result-card {
transition: border-color 180ms ease, box-shadow 180ms ease;
}
body[data-state="focused"] .result-card {
border-color: #2ed573;
box-shadow: 0 0 2rem rgba(46, 213, 115, 0.14);
}
body[data-state="using-phone"] .result-card {
border-color: #ffa502;
}
body[data-state="away"] .result-card,
body[data-state="other"] .result-card {
border-color: #70a1ff;
}
How Do the Cards Communicate State?
- Both cards receive padding and rounded borders on a dark surface.
- The result card can transition smoothly between state colors.
- The state selectors prepare different border colors for live predictions.
- Save styles.css.
- Refresh the page.
You should see the camera and result areas as separate rounded cards. Each card should have a subtle border and shadow.
Cards Missing Their Borders?
Confirm that camera-card and result-card appear in both files with the same spelling. Class names must match for these shared styles to apply.
Use Help me fix the missing card styles in my StudyGuard dashboard.
The two panels now read as separate parts of one dashboard. The camera area and start control need stronger visual treatment next.
- Append these camera and button styles to styles.css.
.webcam-container {
display: grid;
min-height: 320px;
place-items: center;
overflow: hidden;
border-radius: 0.9rem;
background: #030812;
color: #71809a;
}
.webcam-container canvas {
width: 100%;
max-width: 420px;
height: auto;
border-radius: 0.9rem;
}
#start-button {
width: 100%;
margin-top: 1rem;
padding: 0.85rem 1rem;
border: 0;
border-radius: 0.8rem;
background: #2ed573;
color: #04120a;
font-weight: 800;
cursor: pointer;
}
What Changes in the Camera Card?
- The webcam container reserves a stable square-like area before a camera starts.
- A future webcam canvas scales to the card width without stretching its height.
- The start button fills the card width with a high-contrast green surface.
- Save styles.css.
- Refresh the page.
You should see a dark camera placeholder above a full-width green Start Camera button. The placeholder should reserve enough space for the future webcam view.
Camera Area Has No Height?
Check that the HTML uses both webcam-container class values exactly. Confirm that the CSS selector starts with a full stop.
Use Help me debug the size of my StudyGuard webcam placeholder.
The camera side now looks ready for interaction. The result side needs enough visual hierarchy for a changing prediction.
- Append these status and prediction-list styles to styles.css.
#start-button:disabled {
cursor: wait;
opacity: 0.65;
}
#status-text {
min-height: 2.8rem;
margin-bottom: 0.2rem;
font-size: clamp(1.9rem, 5vw, 3rem);
}
.prediction-list {
display: grid;
gap: 0.8rem;
margin-top: 1.5rem;
}
.prediction-row {
display: grid;
gap: 0.35rem;
}
.prediction-heading {
display: flex;
justify-content: space-between;
gap: 1rem;
font-size: 0.92rem;
}
How Does the Result Panel Stay Readable?
- The disabled-button style prepares visual feedback for model loading.
- The status receives enough size and height for different class names.
- Prediction rows will appear as a spaced vertical list.
- Each prediction heading will separate the class name from its percentage.
- Save styles.css.
- Refresh the page.
You should see Waiting as the dominant text in the result card. The prediction panel beneath it remains empty until live results arrive.
Waiting Text Still Looks Small?
Confirm that the HTML status uses id="status-text". Check that the stylesheet selector begins with #status-text.
Use Help me find why the StudyGuard status-text rule is not applying.
The result hierarchy is ready. The remaining rules prepare probability bars and supporting messages.
- Append these prediction and message styles to styles.css.
.prediction-track {
height: 0.65rem;
overflow: hidden;
border-radius: 999px;
background: #1f2d45;
}
.prediction-fill {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #2ed573, #70a1ff);
transition: width 120ms linear;
}
.error-message {
min-height: 1.2rem;
margin: 0.8rem 0 0;
color: #ff7f7f;
}
footer {
margin-top: 1rem;
font-size: 0.86rem;
}
What Will the Live Output Use?
- Each prediction track provides a dark background for its probability.
- Each prediction fill uses color and width to represent the current value.
- The error area reserves space for a readable message.
- The footer stays smaller than the dashboard content.
- Save styles.css.
- Refresh the page.
You should see the footer separated beneath the cards in smaller muted text. The empty error and prediction areas should preserve the clean layout.
Footer Crowding the Cards?
Check that the footer remains inside app-shell and below the dashboard section. Confirm that the footer rule includes its top margin.
Use Help me fix the spacing around my StudyGuard footer.
The desktop dashboard is complete. One media rule lets the same interface fit a narrower browser window.
- Append this responsive rule to the bottom of styles.css.
@media (max-width: 760px) {
.app-shell {
padding-top: 2rem;
}
.dashboard {
grid-template-columns: 1fr;
}
}
How Does the Responsive Rule Work?
- The media query activates when the available width reaches its breakpoint.
- The shell uses less top padding on the narrower layout.
- The dashboard changes from two columns to one column.
- Save styles.css.
- Refresh the local page.
At a wide browser width, you should still see the camera card beside the result card. The complete stylesheet now supports both wide and narrow layouts.
Responsive Rule Not Taking Effect?
Confirm that the media query is outside every earlier selector block. Check that its closing brace appears at the end of styles.css.
Use Help me debug the responsive CSS in my StudyGuard dashboard.
✔️ Awesome, I've got everything!
Your complete stylesheet now covers the page foundation, cards, controls, prediction output, and responsive layout.
ⓧ I'd like to double check the full code
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #08111f;
color: #e8eef8;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
background:
radial-gradient(circle at top left, rgba(46, 213, 115, 0.16), transparent 35%),
linear-gradient(145deg, #08111f, #101b31);
}
button {
font: inherit;
}
.app-shell {
width: min(100% - 2rem, 980px);
margin: 0 auto;
padding: 3rem 0 2rem;
}
.hero {
margin-bottom: 1.5rem;
}
.eyebrow,
.result-label {
margin: 0 0 0.35rem;
color: #81e6b0;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
h1,
h2,
p {
margin-top: 0;
}
h1 {
margin-bottom: 0.5rem;
font-size: clamp(2.5rem, 8vw, 5rem);
line-height: 0.95;
}
.hero > p:last-child,
#confidence-text,
footer {
color: #aab8cd;
}
.dashboard {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(280px, 0.9fr);
gap: 1rem;
}
.camera-card,
.result-card {
padding: 1rem;
border: 1px solid #263653;
border-radius: 1.25rem;
background: rgba(10, 22, 40, 0.88);
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.24);
}
.result-card {
transition: border-color 180ms ease, box-shadow 180ms ease;
}
body[data-state="focused"] .result-card {
border-color: #2ed573;
box-shadow: 0 0 2rem rgba(46, 213, 115, 0.14);
}
body[data-state="using-phone"] .result-card {
border-color: #ffa502;
}
body[data-state="away"] .result-card,
body[data-state="other"] .result-card {
border-color: #70a1ff;
}
.webcam-container {
display: grid;
min-height: 320px;
place-items: center;
overflow: hidden;
border-radius: 0.9rem;
background: #030812;
color: #71809a;
}
.webcam-container canvas {
width: 100%;
max-width: 420px;
height: auto;
border-radius: 0.9rem;
}
#start-button {
width: 100%;
margin-top: 1rem;
padding: 0.85rem 1rem;
border: 0;
border-radius: 0.8rem;
background: #2ed573;
color: #04120a;
font-weight: 800;
cursor: pointer;
}
#start-button:disabled {
cursor: wait;
opacity: 0.65;
}
#status-text {
min-height: 2.8rem;
margin-bottom: 0.2rem;
font-size: clamp(1.9rem, 5vw, 3rem);
}
.prediction-list {
display: grid;
gap: 0.8rem;
margin-top: 1.5rem;
}
.prediction-row {
display: grid;
gap: 0.35rem;
}
.prediction-heading {
display: flex;
justify-content: space-between;
gap: 1rem;
font-size: 0.92rem;
}
.prediction-track {
height: 0.65rem;
overflow: hidden;
border-radius: 999px;
background: #1f2d45;
}
.prediction-fill {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #2ed573, #70a1ff);
transition: width 120ms linear;
}
.error-message {
min-height: 1.2rem;
margin: 0.8rem 0 0;
color: #ff7f7f;
}
footer {
margin-top: 1rem;
font-size: 0.86rem;
}
@media (max-width: 760px) {
.app-shell {
padding-top: 2rem;
}
.dashboard {
grid-template-columns: 1fr;
}
}
Inspect the responsive interface
The static dashboard now shows every interface state available before the model is connected. This final check confirms that the layout works without requesting webcam access.
Before you check the finished page, do you expect the camera and result cards to remain side by side at every browser width?
- In Chrome, return to http://localhost:8000.
- Refresh the page.
You should see the styled StudyGuard page with two dashboard columns. The camera card should show its placeholder and Start Camera button.
The result card should show Waiting above an empty prediction panel. Chrome should not ask for webcam permission because the page has no camera code yet.
- Narrow the Chrome window until the dashboard changes layout.
You should see the result card move below the camera card. That single-column layout confirms the responsive rule is active.
- Widen the Chrome window until the two-column layout returns.
Great work. StudyGuard now has a responsive interface with a clear camera area and a dedicated prediction panel.
Final Layout Looks Different?
Compare both files with the full-code tabs above. Focus on missing braces and mismatched class names because either issue can interrupt several later styles.
Use Help me compare my final static StudyGuard dashboard with the target layout.
Your dashboard is ready to display real model output. Next, you connect the exported classifier and turn the static controls into live webcam predictions.
Connect Live Webcam Predictions
Your improved four-class Teachable Machine model now recognizes varied study scenes. Its live preview also routes unrelated scenes to Other.
The static dashboard cannot perform inference yet. This step connects your exported model to the webcam through TensorFlow.js.
In this step, get ready to:
- Export the improved model into the local StudyGuard workspace.
- Load the model through the dashboard's JavaScript libraries.
- Display live webcam predictions with four probability bars.
Export the improved model
A Teachable Machine web export separates the model structure from its learned weights. StudyGuard needs every exported file in one folder so the browser can load the complete classifier.
- Return to your improved image-classification project in Teachable Machine.
- Move to the Export stage.
- Choose the web download option that exports the model for TensorFlow.js.
- Download the model archive to your Mac.
- In Finder, open your Downloads folder.
- Double-click the downloaded archive to extract its files.
- Inside the existing studyguard folder, create a folder named model using Finder's folder controls.
- Move model.json, metadata.json, and weights.bin into the model folder.
What do the model files contain?
- The model.json file describes the model structure. It also identifies the weights file that the model needs.
- The metadata.json file stores information about the model. This includes its class labels.
- The weights.bin file contains the values learned from your training examples.
- Switch back to Visual Studio Code.
- Expand the model folder in the Explorer sidebar.
You should see model.json, metadata.json, and weights.bin together. Good progress. Your trained classifier is now part of the local StudyGuard workspace.
Missing an exported model file?
Extract the downloaded archive before moving its contents. Keep the weights file named inside model.json beside the two JSON files.
If the export still looks incomplete, help me check my Teachable Machine model export.
Connect the libraries and camera
The dashboard needs two browser libraries before your own JavaScript can load the model. Their script tags must appear before app.js so its code can use the Teachable Machine webcam helpers.
- In Visual Studio Code, create app.js inside the existing studyguard folder from the Explorer sidebar.
- Confirm that app.js appears beside index.html and styles.css.
- In index.html, scroll to the closing lines.
- Find this code:
</main>
</body>
</html>
What is here now?
The current file closes the page immediately after the main interface. No browser scripts load yet.
- Replace those closing lines with this version:
</main>
<script src="https://unpkg.com/@tensorflow/tfjs@1.3.1/dist/tf.min.js"></script>
<script src="https://unpkg.com/@teachablemachine/image@0.8.5/dist/teachablemachine-image.min.js"></script>
<script src="app.js"></script>
</body>
</html>
What do these scripts load?
- TensorFlow.js 1.3.1 provides the browser machine-learning runtime.
- The Teachable Machine image library 0.8.5 provides the model loader and webcam helper.
- The final script loads your project logic from app.js.
- Save index.html.
- Refresh the StudyGuard tab in Chrome.
You should still see the complete static dashboard. The page should show Waiting without displaying an error message.
Does the page look incomplete?
Check that all three script tags sit after </main> and before </body>. Confirm that app.js exists beside index.html.
If the page still fails to load, help me inspect my StudyGuard script tags.
✔️ Awesome, I've got everything!
Great. Save index.html before continuing.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>StudyGuard Focus Detector</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main class="app-shell">
<header class="hero">
<p class="eyebrow">Custom browser AI</p>
<h1>StudyGuard</h1>
<p>Train the data, test the limits, and inspect every prediction.</p>
</header>
<section class="dashboard" aria-label="StudyGuard dashboard">
<div class="camera-card">
<div id="webcam-container" class="webcam-container">
<p id="camera-placeholder">Your webcam will appear here.</p>
</div>
<button id="start-button" type="button">Start Camera</button>
<p id="error-message" class="error-message" role="alert"></p>
</div>
<div class="result-card">
<p class="result-label">Current prediction</p>
<h2 id="status-text">Waiting</h2>
<p id="confidence-text">Press Start Camera to begin.</p>
<div id="label-container" class="prediction-list" aria-live="polite"></div>
</div>
</section>
<footer>
<p>Predictions are estimates from your training examples, not facts about a person.</p>
</footer>
</main>
<script src="https://unpkg.com/@tensorflow/tfjs@1.3.1/dist/tf.min.js"></script>
<script src="https://unpkg.com/@teachablemachine/image@0.8.5/dist/teachablemachine-image.min.js"></script>
<script src="app.js"></script>
</body>
</html>
This reference preserves the static dashboard. It adds the three required scripts at the bottom of the page.
Your app now has access to the model runtime. Next, asynchronous JavaScript coordinates model loading with webcam access without freezing the page.
- In app.js, add the model path and page references by pasting this code:
const MODEL_URL = "./model/";
let model;
let webcam;
let isRunning = false;
const startButton = document.getElementById("start-button");
const webcamContainer = document.getElementById("webcam-container");
const statusText = document.getElementById("status-text");
const confidenceText = document.getElementById("confidence-text");
const labelContainer = document.getElementById("label-container");
const errorMessage = document.getElementById("error-message");
What does this setup do?
- The MODEL_URL constant points to the folder containing your exported model.
- The three mutable values track the loaded model. They also track the webcam and whether classification is running.
- The page references connect JavaScript to the existing button and dashboard elements.
- Save app.js.
- Refresh the StudyGuard tab in Chrome.
You should see the styled dashboard with the Start Camera button and Waiting status. This confirms that every page reference matches the existing HTML.
Does the page fail after adding the references?
Compare each quoted element ID with index.html. A missing quote or bracket in app.js can stop the entire file from loading.
If you cannot find the mismatch, help me debug my StudyGuard page references.
- Below the page references in app.js, add the initial button handler and webcam loop by pasting this code:
startButton.addEventListener("click", startClassifier);
async function startClassifier() {
if (isRunning) {
return;
}
startButton.disabled = true;
startButton.textContent = "Loading...";
statusText.textContent = "Loading model";
confidenceText.textContent = "Chrome may ask for camera permission.";
errorMessage.textContent = "";
}
async function loop() {
if (!isRunning) {
return;
}
webcam.update();
window.requestAnimationFrame(loop);
}
How does the first interaction work?
- The click listener runs startClassifier() when you press the button.
- The running check prevents a second camera session from starting.
- The loading messages give immediate feedback while the asynchronous work begins.
- The loop() function schedules the next webcam frame while the app is running.
- Save app.js.
- Refresh the StudyGuard tab in Chrome.
- Click Start Camera.
The button should change to Loading.... The main result should change to Loading model.
Does the button stay unchanged?
Confirm that startButton.addEventListener("click", startClassifier); appears after the page references. Check that the function name uses the same capitalization in both places.
If the click still has no effect, help me debug the StudyGuard start button.
Each prediction eventually needs its own visible row. The next helper rebuilds that list from the latest probabilities on every frame.
- Below loop() in app.js, add the prediction-row renderer by pasting this code:
function renderPredictions(predictions) {
labelContainer.replaceChildren();
predictions.forEach((prediction) => {
const percentage = Math.round(prediction.probability * 100);
const row = document.createElement("div");
row.className = "prediction-row";
const heading = document.createElement("div");
heading.className = "prediction-heading";
const name = document.createElement("span");
name.textContent = prediction.className;
const value = document.createElement("span");
value.textContent = `${percentage}%`;
const track = document.createElement("div");
track.className = "prediction-track";
const fill = document.createElement("div");
fill.className = "prediction-fill";
fill.style.width = `${percentage}%`;
heading.append(name, value);
track.append(fill);
row.append(heading, track);
labelContainer.append(row);
});
}
How are probability bars created?
- The function clears the previous rows before drawing the latest predictions.
- Each prediction becomes a labeled row with a rounded percentage.
- The fill width uses the same percentage. This turns the numeric probability into a visible bar.
- Save app.js.
- Refresh the StudyGuard tab in Chrome.
- Click Start Camera.
The loading state should still respond immediately. This confirms that the new renderer has loaded without breaking the existing click handler.
Did the loading state stop working?
Check the closing braces at the bottom of renderPredictions(). Confirm that every element name matches the code shown above.
If the file still stops running, help me find the syntax error in renderPredictions.
- In startClassifier(), find this line:
errorMessage.textContent = "";
Why start here?
This line clears an earlier failure before a new startup attempt. The model-loading block belongs directly below it.
- Paste this block directly below that line and before the function's closing brace:
try {
model = await tmImage.load(
`${MODEL_URL}model.json`,
`${MODEL_URL}metadata.json`
);
webcam = new tmImage.Webcam(320, 320, true);
await webcam.setup();
await webcam.play();
webcamContainer.replaceChildren(webcam.canvas);
isRunning = true;
startButton.textContent = "Camera Running";
statusText.textContent = "Reading scene";
window.requestAnimationFrame(loop);
} catch (error) {
startButton.disabled = false;
startButton.textContent = "Try Again";
statusText.textContent = "Could not start";
confidenceText.textContent = "Check the model folder and camera permission.";
errorMessage.textContent = error instanceof Error ? error.message : String(error);
}
How does startup work?
- The model loader reads model.json and metadata.json from the local model folder.
- The webcam helper requests a square camera stream. It mirrors the image for a natural preview.
- A successful setup replaces the placeholder with the webcam canvas.
- The error path restores the button. It also displays the failure inside the dashboard.
- Save app.js.
Camera permission can feel personal. Chrome only starts the local webcam stream after you approve access for this page.
- Refresh the StudyGuard tab in Chrome.
- Click Start Camera.
- Choose Allow while visiting the site when Chrome requests camera access.
You should see your webcam replace the placeholder. The button should read Camera Running while the status reads Reading scene.
Could not start the camera?
- Confirm that model.json, metadata.json, and weights.bin remain together inside model.
- Allow camera access for the local StudyGuard page in Chrome's site permissions.
- Return to the local URL served by the Python server instead of opening index.html directly from Finder.
If the dashboard still reports a startup failure, help me debug the StudyGuard model or camera setup.
Display live predictions
The webcam now supplies frames, but the loop only refreshes its canvas. The final change sends each frame through the model and chooses the class with the highest probability.
- In app.js, find the current webcam loop:
async function loop() {
if (!isRunning) {
return;
}
webcam.update();
window.requestAnimationFrame(loop);
}
What does the current loop do?
The current loop refreshes the webcam canvas once per animation frame. It does not ask the model for a prediction.
- Replace the current loop with this inference loop and prediction function:
async function loop() {
if (!isRunning) {
return;
}
webcam.update();
await predict();
window.requestAnimationFrame(loop);
}
async function predict() {
const predictions = await model.predict(webcam.canvas);
const topPrediction = predictions.reduce((best, current) =>
current.probability > best.probability ? current : best
);
const topPercentage = Math.round(topPrediction.probability * 100);
statusText.textContent = topPrediction.className;
confidenceText.textContent = `${topPercentage}% top probability`;
document.body.dataset.state = topPrediction.className
.toLowerCase()
.replaceAll(" ", "-");
renderPredictions(predictions);
}
How does live inference work?
- The loop waits for predict() before scheduling the next frame. This keeps each dashboard update tied to a completed result.
- The model returns every class probability for the current webcam canvas.
- The reduction keeps the prediction with the highest probability.
- The top class updates the main result. The full prediction array remains visible through the probability rows.
- Save app.js.
✔️ Awesome, I've got everything!
Your completed app.js now loads the model and updates the dashboard from live webcam frames.
ⓧ I'd like to double check the full code
const MODEL_URL = "./model/";
let model;
let webcam;
let isRunning = false;
const startButton = document.getElementById("start-button");
const webcamContainer = document.getElementById("webcam-container");
const statusText = document.getElementById("status-text");
const confidenceText = document.getElementById("confidence-text");
const labelContainer = document.getElementById("label-container");
const errorMessage = document.getElementById("error-message");
startButton.addEventListener("click", startClassifier);
async function startClassifier() {
if (isRunning) {
return;
}
startButton.disabled = true;
startButton.textContent = "Loading...";
statusText.textContent = "Loading model";
confidenceText.textContent = "Chrome may ask for camera permission.";
errorMessage.textContent = "";
try {
model = await tmImage.load(
`${MODEL_URL}model.json`,
`${MODEL_URL}metadata.json`
);
webcam = new tmImage.Webcam(320, 320, true);
await webcam.setup();
await webcam.play();
webcamContainer.replaceChildren(webcam.canvas);
isRunning = true;
startButton.textContent = "Camera Running";
statusText.textContent = "Reading scene";
window.requestAnimationFrame(loop);
} catch (error) {
startButton.disabled = false;
startButton.textContent = "Try Again";
statusText.textContent = "Could not start";
confidenceText.textContent = "Check the model folder and camera permission.";
errorMessage.textContent = error instanceof Error ? error.message : String(error);
}
}
async function loop() {
if (!isRunning) {
return;
}
webcam.update();
await predict();
window.requestAnimationFrame(loop);
}
async function predict() {
const predictions = await model.predict(webcam.canvas);
const topPrediction = predictions.reduce((best, current) =>
current.probability > best.probability ? current : best
);
const topPercentage = Math.round(topPrediction.probability * 100);
statusText.textContent = topPrediction.className;
confidenceText.textContent = `${topPercentage}% top probability`;
document.body.dataset.state = topPrediction.className
.toLowerCase()
.replaceAll(" ", "-");
renderPredictions(predictions);
}
function renderPredictions(predictions) {
labelContainer.replaceChildren();
predictions.forEach((prediction) => {
const percentage = Math.round(prediction.probability * 100);
const row = document.createElement("div");
row.className = "prediction-row";
const heading = document.createElement("div");
heading.className = "prediction-heading";
const name = document.createElement("span");
name.textContent = prediction.className;
const value = document.createElement("span");
value.textContent = `${percentage}%`;
const track = document.createElement("div");
track.className = "prediction-track";
const fill = document.createElement("div");
fill.className = "prediction-fill";
fill.style.width = `${percentage}%`;
heading.append(name, value);
track.append(fill);
row.append(heading, track);
labelContainer.append(row);
});
}
This reference matches the completed application logic. Compare it with your saved app.js file before testing.
Before you run the final check, do you expect the main result to stay on Reading scene or change to one of your trained labels?
- Return to the Chrome tab at http://localhost:8000.
- Refresh the page.
- Click Start Camera.
- Approve camera access if Chrome requests it again.
- Present your Focused pose to the webcam.
- Present your Using Phone pose to the webcam.
- Move into your Away scene.
- Show the unrelated scene that your model learned as Other.
You should see the webcam canvas and four live probability bars for Focused, Using Phone, Away, and Other. The main result should follow whichever class currently has the highest probability.
You did it. StudyGuard now turns live webcam frames into visible predictions from the model you trained and improved.
Are the webcam and bars out of sync?
- Confirm that await predict(); appears between webcam.update(); and the next animation-frame request.
- Check that renderPredictions(predictions); remains at the bottom of predict().
- Confirm that your exported model contains exactly the Focused, Using Phone, Away, and Other classes.
If the output still does not update correctly, help me debug StudyGuard's live prediction loop.
Secret mission
Add an Uncertainty Gate
StudyGuard always displays its highest-scoring class, even when every score is weak. Add a confidence threshold so the dashboard can show Unsure while keeping the evidence visible.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep StudyGuard running, pause it for later, or remove it from your Mac. This project runs entirely on your computer. It has no ongoing cloud or API costs.
Resources you used:
- A running Python local server serving StudyGuard at http://localhost:8000.
- The local studyguard folder containing your page files and exported four-class model.
Keep everything running
No action is needed. Choose this if you are still testing classifications or experimenting with the uncertainty threshold.
- Leave the Visual Studio Code terminal running.
- Leave the StudyGuard tab open in Chrome to keep the webcam session active.
- Continue adjusting MIN_CONFIDENCE in app.js while comparing weak predictions.
Pause - I'll come back to this later
Stop the running processes to free your webcam and terminal. Your code and exported model stay inside the studyguard folder.
- Switch back to the Visual Studio Code terminal from earlier.
- Press Ctrl+C to stop the Python local server.
- Close the StudyGuard tab in Chrome to end the webcam session.
The terminal prompt returns when the server stops. Your project files remain ready for your return.
Delete - I don't want to use this again
This choice permanently removes your StudyGuard code and trained model. The server and webcam session stop before the folder is deleted.
- Switch back to the Visual Studio Code terminal from earlier.
- Press Ctrl+C to stop the Python local server.
- Close the StudyGuard tab in Chrome to end the webcam session.
- Move to the folder above studyguard and delete the workspace by running these commands:
cd ..
rm -rf studyguard
What do these commands do?
The first line moves the terminal to the folder above studyguard. The second line permanently removes the entire workspace folder.
- Confirm that the workspace is gone by running this command:
ls
What should I see?
The command lists the contents of the folder that previously contained your workspace. You should no longer see studyguard in that list.
Your StudyGuard workspace is now removed from your Mac. No project server remains running.
Still see the folder?
Confirm that your terminal moved to the folder above studyguard before deletion.
Ask for help if the folder remains: Help me remove my local StudyGuard folder safely.
Nice Work!
Nice Work!
StudyGuard is complete! Your browser dashboard now recognizes four study scenes from live webcam frames.
You've learned how to:
- Train a custom four-class classification model in Google Teachable Machine. Test all four labels: Focused, Using Phone, Away, Other.
- Reveal the designed failure caused by an incomplete label set. Improve dataset coverage with varied training examples plus a fallback class.
- Build a local browser dashboard with live webcam output. Run TensorFlow.js inference to display the top prediction plus every class probability.
- Complete the optional Secret Mission by adding an uncertainty gate at 0.60. Show Unsure for weak predictions while keeping the raw probability bars visible.
Ready to quiz yourself?