Design LensCheck Object Detection

Design and deploy a browser app that detects objects in uploaded images.

Introduction

30 Second Summary

When an app studies a photo, a short delay can feel like a failure. Unclear confidence or a blank result makes the answer harder to trust.

In this project, you will design LensCheck in Figma before building it as a responsive browser-based AI app. TensorFlow.js with COCO-SSD will detect common objects before you publish the finished experience with Firebase Hosting.

What You'll Build

You will choose a photo in a live LensCheck page, watch the analysis state respond, and see common objects outlined on the image with readable confidence results.

By the end of this project, you'll have:

  • A clickable AI flow that you can present from upload through analyzing, results, no-results, and error states.
  • A responsive image inspection page where you choose a local photo while the upload controls, image stage, and results stay readable at narrow or desktop widths.
  • A public LensCheck link that marks recognized objects on the image, reports confidence in plain language, and guides the user when detection finds nothing.
  • Secret Mission: Add a confidence control that reruns detection from 50% to 90% so users can see lower-certainty results disappear.

Do I need any experience before starting?

You should be comfortable creating layouts in Figma plus editing basic HTML, CSS, and JavaScript. A Google Account is required for the Firebase work.

Before We Start

Start by defining who LensCheck helps and why its AI interaction states matter. This commitment keeps loading, uncertainty, empty, and error feedback at the heart of the experience before any hands-on work begins.

Set Up the Windows Workspace

LensCheck needs a stable local foundation before its interface or model can earn trust. This setup keeps every later design decision attached to a page you can run in the browser.

A pinned Node.js toolchain prevents environment drift. Vite gives you a running page before the design work begins.

In this step, get ready to:
  • Verify the pinned Node.js runtime through the VS Code PowerShell terminal.
  • Serve the LensCheck starter page from its pinned Vite workspace.
  • Authenticate Firebase CLI against a new Firebase project.
Install and verify the runtime

The Visual Studio Code terminal keeps setup commands beside your project files. PowerShell is the Windows shell you use throughout this step.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into Windows search.
  • Press Enter to open Visual Studio Code.
  • Select Terminal from the top menu.
  • Select New Terminal.
  • Choose PowerShell from the terminal profile picker.
  • Check the installed Node.js version and npm version by running:
node -v
npm -v

What do these checks prove?

  • The node -v command prints the installed Node.js version.
  • The npm -v command confirms that npm is available for installing the project packages.

✔️ I see v24.21.0 LTS

Your terminal should show v24.21.0 for Node.js. It should also show an npm version number.

Your pinned runtime is ready. You can move on to the browser check.

ⓧ I see an older version

The installed Node.js version is older than the project pin. Updating it now prevents Vite compatibility problems later.

  • Open the official Node.js download page in Microsoft Edge.
  • Download the Windows installer for Node.js v24.21.0 LTS.
  • Open the downloaded Windows installer.
  • Approve the Windows administrator request if it appears.
  • Keep the installer defaults on each screen.
  • Finish the installation.
  • Close Visual Studio Code so it can refresh the system path.
  • Reopen Visual Studio Code through Windows search.
  • Create a new PowerShell terminal from the Terminal menu.
  • Verify the updated runtime by running:
node -v
npm -v

What confirms the update?

The first line should now show v24.21.0. The second line should show an npm version number.

ⓧ Command not found

Windows cannot find the Node.js commands yet. Installing the pinned LTS release adds Node.js and npm to your system.

  • Open the official Node.js download page in Microsoft Edge.
  • Download the Windows installer for Node.js v24.21.0 LTS.
  • Open the downloaded Windows installer.
  • Approve the Windows administrator request if it appears.
  • Keep the installer defaults on each screen.
  • Finish the installation.
  • Close Visual Studio Code so it can refresh the system path.
  • Reopen Visual Studio Code through Windows search.
  • Create a new PowerShell terminal from the Terminal menu.
  • Verify the installed runtime by running:
node -v
npm -v

What confirms the installation?

The first line should show v24.21.0. The second line should show an npm version number.

You use Microsoft Edge to test LensCheck locally. A quick page check confirms that the browser is available before the development server starts.

  • Press the Windows key to open Windows search.
  • Type Microsoft Edge into Windows search.
  • Press Enter to open Microsoft Edge.
  • Open the official Node.js download page.
  • Confirm that the page shows Node.js v24.21.0 LTS as the current LTS release.

Still seeing the wrong Node.js version?

Close every Visual Studio Code window after the installer finishes. Reopen the editor so its terminal receives the updated Windows system path.

If either version command still fails, rerun the Windows installer. Keep the default installation options selected.

Help me verify my Windows Node.js installation

Create the pinned LensCheck workspace

The workspace collects your source files and exact package versions in one folder. Opening that folder in Visual Studio Code keeps every file action anchored to lenscheck.

  • Return to Visual Studio Code.
  • Click File in the top menu.
  • Select Open Folder.
  • Select Desktop in the Windows folder picker.
  • Click New folder.
  • Enter lenscheck as the folder name.
  • Select the new lenscheck folder.
  • Click Select Folder.

Visual Studio Code may ask whether you trust the folder. You created this empty workspace yourself.

  • Confirm that you trust the folder if Visual Studio Code asks.
  • Select the New File button in the Explorer sidebar.
  • Enter package.json as the file name.
  • Replace the file contents with the pinned project configuration below:
{
  "name": "lenscheck",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@tensorflow-models/coco-ssd": "2.2.3",
    "@tensorflow/tfjs": "4.22.0"
  },
  "devDependencies": {
    "vite": "8.3.4"
  }
}

What does this configuration control?

This JSON file pins Vite to 8.3.4. It also defines the commands used for local development and production builds.

The dependencies pin TensorFlow.js to 4.22.0. They pin COCO-SSD to 2.2.3.

  • Save package.json by pressing Ctrl+S.
  • Confirm that package.json remains visible inside the lenscheck folder in the Explorer sidebar.

Seeing red markers in package.json?

Check each quotation mark and comma against the code block. The final property in each object has no trailing comma.

Help me fix my LensCheck package.json

  • Install the pinned project dependencies by running:
npm install

What does npm install create?

npm reads the exact versions in package.json. It downloads them into node_modules.

The installation also creates package-lock.json. That lock file records the resolved dependency tree.

The terminal returns to the PowerShell prompt after the packages finish installing. You should see node_modules and package-lock.json in the Explorer sidebar.

Did the dependency installation fail?

Confirm that the terminal is attached to the open lenscheck workspace. Check your internet connection before trying the installation again.

Help me troubleshoot npm install

The starter page needs a source folder for its JavaScript entry module and stylesheet. Creating those files in the Explorer sidebar makes their locations visible immediately.

  • Select the lenscheck folder in the Explorer sidebar.
  • Click the New Folder button in the Explorer toolbar.
  • Enter src as the folder name.
  • Select the new src folder.
  • Click the New File button in the Explorer toolbar.
  • Enter main.js as the file name.
  • Click the New File button again.
  • Enter style.css as the file name.

Your source structure

The Explorer sidebar should now show src/main.js and src/style.css inside lenscheck.

  • Select the top-level lenscheck folder in the Explorer sidebar.
  • Click the New File button.
  • Enter index.html as the file name.

A small stylesheet makes the starter page visibly different from an unconfigured browser document. This gives you a concrete change to confirm when the module entry loads.

  • Select src/style.css in the Explorer sidebar.
  • Add the temporary starter styles below:
:root {
  color: #171329;
  background: #f6f3ff;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  background:
    radial-gradient(circle at top left, rgba(196, 181, 253, 0.55), transparent 34rem),
    #f8f7fc;
}

What do these styles establish?

The CSS gives the document a readable system font. It adds the pale violet background used by LensCheck.

The minimum dimensions keep the page usable at narrow browser widths. Later interface styles build on this foundation.

  • Save src/style.css by pressing Ctrl+S.

The unsaved indicator disappears from the src/style.css tab. The saved file remains listed under src.

Is the stylesheet in the wrong folder?

Check that style.css is nested directly inside src. Drag it into that folder in the Explorer sidebar if it appears beside package.json.

Help me check my LensCheck file structure

The browser starts with index.html. Its module script hands control to src/main.js.

  • Select index.html in the Explorer sidebar.
  • Add the temporary LensCheck starter document below:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LensCheck</title>
  </head>
  <body>
    <h1 id="page-title">See what the model notices.</h1>

    <script type="module" src="/src/main.js"></script>
  </body>
</html>

What does this document provide?

The HTML gives the browser a LensCheck page title and a visible starter heading. The viewport setting lets the page scale correctly on narrow screens.

The module script preserves Vite's entry point at /src/main.js. That entry module loads the rest of the application.

  • Save index.html by pressing Ctrl+S.

Before you start the development server, what heading do you expect Microsoft Edge to show?

  • Start the Vite development server by running:
npm run dev

What does the development command do?

The command runs the dev script from package.json. Vite serves the workspace through a local browser address.

The server keeps using this terminal until you stop it. Leave this terminal running while you test the page.

The terminal should show the local address http://localhost:5173 without a startup error.

  • Switch back to Microsoft Edge from earlier.
  • Enter http://localhost:5173 in the address bar.
  • Press Enter to load the local page.

You should see the heading See what the model notices. on the page. The starter document is now running through Vite.

Does the local page fail to load?

Keep the terminal with the Vite server open. Confirm that the address bar contains http://localhost:5173.

If the heading is missing, compare index.html with the starter document above. Confirm that the module path points to /src/main.js.

Help me troubleshoot the LensCheck starter page

The entry module connects the stylesheet to the running page. Saving it gives you an immediate visual confirmation through Vite's live development workflow.

  • Switch back to Visual Studio Code.
  • Select src/main.js in the Explorer sidebar.
  • Add the stylesheet import below:
import './style.css';

What does this import connect?

The JavaScript entry module loads src/style.css into the Vite page. This keeps the stylesheet attached to the application entry point.

  • Save src/main.js by pressing Ctrl+S.
  • Switch back to Microsoft Edge.

The page should now use the system font on a pale violet background. Your starter heading remains visible without terminal errors.

Are the starter styles missing?

Confirm that src/main.js contains the stylesheet import exactly as shown. Check that style.css sits beside main.js inside src.

Help me connect the LensCheck stylesheet

✔️ Awesome, I've got everything!

Your starter files are saved. Keep the Vite development server running for the final verification.

ⓧ I'd like to double check the full code

Compare each starter file with the complete workspace state below.

{
  "name": "lenscheck",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@tensorflow-models/coco-ssd": "2.2.3",
    "@tensorflow/tfjs": "4.22.0"
  },
  "devDependencies": {
    "vite": "8.3.4"
  }
}

Package file checkpoint

This is the complete pinned package.json for the workspace. Its scripts and package versions should match character for character.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LensCheck</title>
  </head>
  <body>
    <h1 id="page-title">See what the model notices.</h1>

    <script type="module" src="/src/main.js"></script>
  </body>
</html>

Starter document checkpoint

This temporary document displays the LensCheck heading. It preserves the Vite module entry point for /src/main.js.

import './style.css';

Entry module checkpoint

This temporary entry module loads the starter stylesheet. Later steps expand the same file with interface behavior.

:root {
  color: #171329;
  background: #f6f3ff;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  background:
    radial-gradient(circle at top left, rgba(196, 181, 253, 0.55), transparent 34rem),
    #f8f7fc;
}

Starter stylesheet checkpoint

This temporary stylesheet provides the visible font and background change. It also establishes the page's minimum dimensions.

Prepare Firebase access

Firebase access links your local workspace to the project you deploy later. You install the command-line tool before authenticating it with your Google Account.

The Vite server is still using the first terminal. A second PowerShell terminal lets the local page stay available while you prepare Firebase.

  • Switch back to Visual Studio Code.
  • Select Terminal from the top menu.
  • Select New Terminal to create a second PowerShell terminal.
  • Install Firebase CLI globally by running:
npm install -g firebase-tools

What does the global installation do?

The command installs the globally available firebase command. The current package version is 15.33.0.

Global installation makes the Firebase CLI available outside a single project folder. You can use it from the LensCheck terminal.

npm should finish the installation and return to the PowerShell prompt without a failure message.

Did the Firebase CLI installation fail?

Check your internet connection before running the installation again. If Windows reports a permissions problem, close Visual Studio Code and reopen it with administrator approval for this installation.

Help me install Firebase CLI on Windows

Signing in can feel sensitive. You complete the Google Account flow in the browser instead of entering account credentials into the terminal.

  • Start Firebase authentication by running:
firebase login

What happens during authentication?

Firebase CLI opens a browser-based Google Account flow. The terminal waits for the browser to confirm authentication.

  • Choose the Google Account you want to use for this project in the browser window.
  • Complete the requested Google authentication steps.
  • Return to the Visual Studio Code terminal after the browser confirms access.

The terminal should confirm that Firebase authentication completed. Your CLI can now access projects linked to that Google Account.

Did the browser login fail?

Confirm that Microsoft Edge can reach Google sign-in pages. Restart the login command if the browser window was closed before authentication finished.

Help me complete Firebase login

A Firebase project holds the Hosting site you deploy later. Its project ID must be globally unique across Firebase.

  • Choose a globally unique project ID for LensCheck.
  • Create the new Firebase project through the interactive flow by running:
firebase projects:create

What does the project command ask for?

The command starts an interactive project creation flow. Firebase CLI asks for the required project information because no project ID was included in the command.

  • Enter your chosen globally unique project ID when prompted.
  • Revise the project ID if Firebase reports that it is unavailable.
  • Complete the remaining required project information prompts.
  • Record the accepted project ID here: your globally unique Firebase project ID.

The terminal should finish the creation flow without an error. Your accepted project ID identifies the Firebase project used later for Hosting.

Before you check the project list, do you expect the accepted project ID to appear under your authenticated account?

  • Verify access to the new Firebase project by running:
firebase projects:list

What does the project list prove?

The command lists Firebase projects available to the authenticated Google Account. Seeing the new project confirms both authentication and project creation.

You should see your globally unique Firebase project ID in the project list. That completes the cloud access setup for LensCheck.

Is the new project missing?

Confirm that project creation finished successfully before checking the list again. Make sure the authenticated Google Account is the account you selected in the browser.

Help me find my Firebase project

Your pinned workspace is running in Microsoft Edge. Next, you use Figma to design the complete AI interaction before adding model behavior.

Prototype the AI Experience in Figma

Your local LensCheck page is already running. Now you can shape its experience in Figma before implementation begins.

AI responses take time. A state-based prototype gives each delay or uncertain outcome clear feedback before implementation begins.

In this step, get ready to:
  • Map five desktop states around one consistent interface.
  • Build reusable feedback components with uncertainty-aware copy.
  • Connect the states into a playable flow that never strands the user.
Map the state flow

An interaction state captures one moment in the user's journey. LensCheck needs five states because the model can be ready, busy, successful, uncertain, or unable to continue.

What belongs in each state?

  • The Ready frame shows an available upload control. Its status says Choose an image to begin.
  • The Analyzing frame shows working feedback. Its status says Analyzing the image....
  • The Results frame shows success feedback above the marked image. Its result card uses Likely bottle plus a confidence percentage.
  • The No results frame explains that no confident objects were found. Its recovery copy asks for a brighter or closer photo.
  • The Error frame explains that the image could not be analyzed. Its recovery control offers another image choice.
  • Press the Windows key to open Windows search.
  • Type Figma into the search field.

You should see Figma in the Windows search results.

  • Press Enter to open Figma.
  • Create a blank design file from the Figma home screen.

You should see an empty canvas ready for the LensCheck flow.

  • Use the frame tool in the top toolbar to create a desktop-sized frame.
  • Rename the frame to Ready in the layers panel.

You should see one desktop frame named Ready on the canvas.

  • Place the upload area near the top of the Ready frame.
  • Place the status message directly below the upload area.

The first two regions should make the starting action clear. The nearby status should explain that LensCheck is ready.

  • Place a large image stage below the status message.
  • Place the result region below the image stage.

The Ready frame should now contain all four regions. Their vertical order becomes the template for every later state.

  • Duplicate the Ready frame four times.
  • Rename the duplicate frames to Analyzing, Results, No results, and Error.

You should now see five named desktop frames with matching layouts.

  • Arrange the five frames from Ready through Error in a left-to-right sequence.
  • Update each frame with the matching feedback from the state map.

Each frame should now communicate a distinct moment. The upload area, status, image stage, and result region should remain in consistent positions.

  • Compare the five frames at the same canvas zoom level.

The interface should feel stable while the feedback changes. That visual continuity keeps model activity understandable.

Build reusable feedback components

Component variants store related visual states in one reusable set. They keep repeated feedback consistent across the prototype.

Match status to state

  • Use ready in the Ready frame.
  • Use working in the Analyzing frame.
  • Use success in the Results frame.
  • Use ready in the No results frame because the user can recover immediately.
  • Use error in the Error frame.
  • Select the status message layer in the Ready frame.
  • Use the component action in the top toolbar to turn the selection into a reusable component.

The status message should now appear as a main component on the canvas.

  • Add three variants to the status component set.
  • Set the four variant values to ready, working, success, and error.

You should see four related status variants grouped into one component set.

  • Replace each frame's original status layer with an instance from the component set.
  • Apply the matching variant from the status map to each instance.

The five frames should now use the same status structure. Each state should still have distinct feedback.

  • Select the label and confidence layers in the Results frame.
  • Turn the selected layers into a reusable result-card component.

The result card should contain one label layer plus one confidence layer. This separates the model's guess from its certainty.

  • Keep Likely bottle in the result-card label layer.
  • Place one result-card instance in the Results frame.

You should see a reusable result card that communicates uncertainty without claiming perfect recognition.

  • Select the upload control in the Ready frame.
  • Turn the selected control into an upload-button component.

The upload control should now exist as a reusable main component.

  • Add a pressed variant to the upload-button component set.
  • Adjust the pressed variant so activation is visually clear.

You should see distinct default and pressed button variants in the component set.

  • Place upload-button instances in the Ready, No results, and Error frames.

The starting state and both recovery states should now offer the same familiar image-choice control.

Components not updating together?

Check that you edited the main component instead of one frame-specific instance. Confirm that the four status options belong to one component set.

Use the layers panel to confirm that each frame contains component instances.

Help me troubleshoot my Figma components

Connect and play the flow

Prototype connections turn the five static frames into one testable experience. The flow begins at Ready so every test starts with a clear image-choice action.

  • Select the default upload-button variant in its component set.
  • Open the Prototype tab in the right sidebar.

You should see prototype connection controls for the selected button variant.

  • Drag the default variant's prototype connection to the pressed variant.
  • Set the interaction action to Change to.

A prototype connection should now link the default upload-button variant to its pressed state.

  • Set the Ready frame as the flow's starting frame in the Prototype tab.
  • Connect the upload-button instance in Ready to the Analyzing frame.

The canvas should show a frame connection from Ready to Analyzing.

  • Create a timed frame connection from Analyzing to Results.
  • Connect the next-action control in Results to No results.

The main path should now move from image choice to working feedback. It should continue into a readable result.

  • Connect the recovery control in No results to Error.
  • Connect the recovery control in Error to Ready.

Every frame should now have an outgoing path. The final recovery action should return the prototype to its starting point.

Before you play the flow, do you expect any state to leave you without a clear next move?

  • Start Presentation view from the prototype preview control.
  • Activate the upload control in the Ready frame.
  • Use each next-action control whenever the flow pauses.

You should move through Ready, Analyzing, Results, No results, and Error without reaching a dead end. Each screen should explain what happened or what the user can do next.

Flow stops on one frame?

Return to the canvas and select the frame where playback stopped. Check that its next-action control has a visible prototype connection.

Confirm that the flow starting point is attached to Ready.

Help me fix my Figma prototype flow

Your five-state LensCheck flow is now clickable from start to finish. Next, you'll translate that experience logic into the responsive LensCheck interface.

Build the Responsive Interface

Your Figma prototype maps every important interaction state. The running Vite page now needs a functional interface that carries those design decisions into the browser.

In this step, you will translate the prototype into semantic HTML. You will add responsive CSS plus browser-side JavaScript for a local image preview.

In this step, get ready to:
  • Build the semantic upload interface plus its image and results regions.
  • Translate the LensCheck visual system into a responsive layout.
  • Draw a selected local image onto the preview canvas.
Build the semantic page structure

Semantic structure gives every part of LensCheck a clear role. A live status region can communicate changing states while the native file input preserves keyboard access.

  • Switch back to the open index.html tab in Visual Studio Code.
  • Select the temporary starter markup.
  • Replace it with this document shell:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta
      name="description"
      content="LensCheck finds common objects in your images with browser-based machine learning."
    >
    <title>LensCheck</title>
  </head>
  <body>
    <header class="site-header">
      <a class="brand" href="#main">LensCheck</a>
      <span class="eyebrow">AI image inspection</span>
    </header>
    <main id="main" class="page-shell"></main>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

What Does This Structure Do?

  • The header gives LensCheck a consistent identity above the application.
  • The main landmark provides a clear destination for the brand link.
  • The module script keeps the existing src/main.js entry point connected.
  • Save index.html.
  • Refresh the existing LensCheck page in Microsoft Edge.

You should see the LensCheck brand in the header. The temporary starter heading should be gone.

Still Seeing the Starter Heading?

Confirm that you edited the index.html file inside the open lenscheck folder. Check that the file is saved.

Make sure the existing Vite terminal is still running.

Help me find why my updated LensCheck HTML is not appearing.

The hero sets expectations before someone selects a file. Its copy makes confidence part of the experience from the beginning.

  • In index.html, locate the empty main element.
  • Insert this hero section inside that element:
      <section class="hero" aria-labelledby="page-title">
        <p class="eyebrow">Designed for understandable AI</p>
        <h1 id="page-title">See what the model notices.</h1>
        <p class="hero-copy">
          Choose a photo to find common objects and review the model's confidence.
        </p>
      </section>

How Does the Hero Set Context?

  • The eyebrow frames LensCheck as an understandable AI experience.
  • The heading states the user's goal in direct language.
  • The supporting copy introduces model confidence before results appear.
  • Save index.html.
  • Refresh the LensCheck page.

You should see the LensCheck headline with its supporting sentence beneath the header.

Hero Copy Missing?

Check that the hero section sits between the opening and closing main tags. Confirm that every hero tag has a matching closing tag.

Help me correct the LensCheck hero structure.

The upload panel carries the user from guidance to system feedback. Its native file input retains the browser's built-in file picker behavior.

  • In index.html, locate the closing tag for the hero section.
  • Insert this upload panel directly beneath that tag:
      <section class="app-card" aria-labelledby="upload-title">
        <div class="control-panel">
          <div>
            <p class="step-label">Step 1</p>
            <h2 id="upload-title">Choose an image</h2>
            <p class="supporting-copy">
              Try a clear photo containing a person, bottle, chair, dog, or car.
            </p>
          </div>

          <label class="file-label" for="image-input">Image file</label>
          <input
            id="image-input"
            class="file-input"
            type="file"
            accept="image/*"
            disabled
          >
          <p class="privacy-note">The selected image is processed in this browser session.</p>

          <div id="status" class="status" data-state="working" role="status" aria-live="polite">
            Loading the recognition model...
          </div>
        </div>
      </section>

How Does the Upload Panel Help?

  • The label connects the words Image file to the #image-input control.
  • The accept="image/*" value guides the file picker toward images.
  • The #status region uses live status semantics for changing feedback.
  • The privacy note explains where LensCheck processes the selected image.
  • Save index.html.
  • Refresh the LensCheck page.

You should see an image input plus a privacy note beneath the hero. The input remains disabled until the preview script enables it.

Upload Panel Missing?

Check that the app-card section remains inside main. It should follow the hero section.

Help me fix the missing LensCheck upload panel.

The visual panel provides a stable stage for the chosen image. Its hidden results region preserves the hierarchy needed for model output in the next step.

  • In index.html, locate the closing tag for .control-panel.
  • Insert this visual panel directly after that closing tag:
        <div class="visual-panel">
          <div class="image-stage">
            <p class="stage-placeholder">Your analyzed image will appear here.</p>
            <img id="preview" class="preview-source" alt="">
            <canvas
              id="detection-canvas"
              class="preview-canvas"
              aria-label="Uploaded image with detected objects highlighted"
              hidden
            ></canvas>
          </div>

          <section id="results" class="results" aria-labelledby="results-title" hidden>
            <div class="results-heading">
              <p class="step-label">Step 2</p>
              <h2 id="results-title">What LensCheck found</h2>
            </div>
            <div id="results-grid" class="results-grid"></div>
          </section>
        </div>

What Does the Visual Panel Add?

  • The hidden #preview image becomes the source for the canvas.
  • The #detection-canvas becomes the visible image surface.
  • The stage placeholder explains what appears after file selection.
  • The hidden #results section keeps interpretation separate from the image.
  • Save index.html.
  • Refresh the LensCheck page.

You should see the image-stage placeholder beneath the upload controls. The results region should remain hidden.

Image Stage in the Wrong Place?

Confirm that .visual-panel sits inside .app-card. It should follow .control-panel.

Help me correct the LensCheck visual panel structure.

✔️ Awesome, I've got everything!

Your semantic LensCheck interface is complete. Double-check that index.html is saved.

ⓧ 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.0">
    <meta
      name="description"
      content="LensCheck finds common objects in your images with browser-based machine learning."
    >
    <title>LensCheck</title>
  </head>
  <body>
    <header class="site-header">
      <a class="brand" href="#main">LensCheck</a>
      <span class="eyebrow">AI image inspection</span>
    </header>

    <main id="main" class="page-shell">
      <section class="hero" aria-labelledby="page-title">
        <p class="eyebrow">Designed for understandable AI</p>
        <h1 id="page-title">See what the model notices.</h1>
        <p class="hero-copy">
          Choose a photo to find common objects and review the model's confidence.
        </p>
      </section>

      <section class="app-card" aria-labelledby="upload-title">
        <div class="control-panel">
          <div>
            <p class="step-label">Step 1</p>
            <h2 id="upload-title">Choose an image</h2>
            <p class="supporting-copy">
              Try a clear photo containing a person, bottle, chair, dog, or car.
            </p>
          </div>

          <label class="file-label" for="image-input">Image file</label>
          <input
            id="image-input"
            class="file-input"
            type="file"
            accept="image/*"
            disabled
          >
          <p class="privacy-note">The selected image is processed in this browser session.</p>

          <div id="status" class="status" data-state="working" role="status" aria-live="polite">
            Loading the recognition model...
          </div>
        </div>

        <div class="visual-panel">
          <div class="image-stage">
            <p class="stage-placeholder">Your analyzed image will appear here.</p>
            <img id="preview" class="preview-source" alt="">
            <canvas
              id="detection-canvas"
              class="preview-canvas"
              aria-label="Uploaded image with detected objects highlighted"
              hidden
            ></canvas>
          </div>

          <section id="results" class="results" aria-labelledby="results-title" hidden>
            <div class="results-heading">
              <p class="step-label">Step 2</p>
              <h2 id="results-title">What LensCheck found</h2>
            </div>
            <div id="results-grid" class="results-grid"></div>
          </section>
        </div>
      </section>
    </main>

    <script type="module" src="/src/main.js"></script>
  </body>
</html>
Translate the visual system

The prototype uses spacing plus state colors to make progress understandable. The stylesheet carries that hierarchy into a responsive grid while keeping the native file input accessible.

  • Switch to the open src/style.css tab.
  • Select the temporary starter styles.
  • Replace them with this global foundation:
:root {
  color: #171329;
  background: #f6f3ff;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

* {
  box-sizing: border-box;
}

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  background:
    radial-gradient(circle at top left, rgba(196, 181, 253, 0.55), transparent 34rem),
    #f8f7fc;
}

button,
input {
  font: inherit;
}

[hidden] {
  display: none !important;
}

What Does the Foundation Control?

  • The root rules establish the page colors and type system.
  • The universal box-sizing rule keeps declared widths predictable.
  • The hidden selector keeps inactive interface regions out of view.
  • Save src/style.css.
  • Refresh the LensCheck page.

You should see a pale background with a soft violet glow in the top-left corner.

Background Still Unstyled?

Confirm that src/main.js still imports ./style.css. Check the running Vite terminal for a stylesheet error.

Help me troubleshoot why my LensCheck CSS is not loading.

  • Append the header styles to src/style.css with this code:
.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  max-width: 1180px;
  margin: 0 auto;
  padding: 1.25rem clamp(1rem, 4vw, 3rem);
}

.brand {
  color: #4c1d95;
  font-size: 1.2rem;
  font-weight: 850;
  text-decoration: none;
}

.eyebrow,
.step-label {
  margin: 0;
  color: #6d28d9;
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

How Does the Header Gain Hierarchy?

  • The header separates the brand from the product category.
  • The shared eyebrow style creates compact labels for the hero and numbered steps.
  • Save src/style.css.
  • Refresh the LensCheck page.

You should see the brand aligned left with the category label aligned right.

Header Items Still Stacked?

Check that the selector begins with .site-header. Confirm that it matches the class in index.html.

Help me fix the LensCheck header alignment.

  • Append the page and hero layout styles with this code:
.page-shell {
  width: min(1180px, calc(100% - 2rem));
  margin: 0 auto;
  padding: 3.5rem 0 5rem;
}

.hero {
  max-width: 760px;
  margin-bottom: 2.5rem;
}

h1,
h2,
h3,
p {
  margin-top: 0;
}

h1 {
  max-width: 700px;
  margin: 0.6rem 0 1rem;
  font-size: clamp(2.6rem, 7vw, 5.8rem);
  line-height: 0.98;
  letter-spacing: -0.055em;
}

How Does the Hero Scale?

  • The page shell caps the content width while preserving space at the screen edges.
  • The clamped headline grows with the viewport until it reaches its maximum size.
  • Save src/style.css.
  • Refresh the LensCheck page.

You should see a large headline inside a centered page column.

Headline Size Unchanged?

Confirm that the page title remains an h1 element. Check the latest CSS for a missing closing brace.

Help me diagnose the LensCheck hero styles.

  • Append the supporting copy and application-card styles with this code:
h2 {
  margin-bottom: 0.7rem;
  font-size: clamp(1.45rem, 3vw, 2rem);
}

.hero-copy,
.supporting-copy,
.privacy-note,
.empty-state p {
  color: #625d73;
  line-height: 1.65;
}

.hero-copy {
  max-width: 590px;
  font-size: 1.08rem;
}

.app-card {
  display: grid;
  grid-template-columns: minmax(250px, 0.78fr) minmax(0, 1.5fr);
  overflow: hidden;
  border: 1px solid #ded8ed;
  border-radius: 28px;
  background: #ffffff;
  box-shadow: 0 24px 70px rgba(55, 38, 90, 0.12);
}

What Creates the Main Card?

  • The muted copy color separates guidance from headings.
  • The application card gives the image more room than the controls.
  • The border plus shadow separates the task from the page background.
  • Save src/style.css.
  • Refresh the LensCheck page.

You should see the controls and image placeholder grouped inside one rounded white card.

Card Still Looks Like Plain Text?

Check that class="app-card" remains on the section around both panels. Confirm that the .app-card rule has a closing brace.

Help me fix the LensCheck application card styles.

  • Append the panel and file-input styles with this code:
.control-panel,
.visual-panel {
  padding: clamp(1.25rem, 4vw, 2.25rem);
}

.control-panel {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  border-right: 1px solid #e8e3f2;
  background: #fcfbff;
}

.file-label {
  color: #2f2850;
  font-size: 0.84rem;
  font-weight: 800;
}

.file-input {
  width: 100%;
  padding: 0.8rem;
  border: 2px dashed #a78bfa;
  border-radius: 14px;
  color: #3b315d;
  background: #f5f0ff;
  cursor: pointer;
}

How Are the Panels Organized?

  • Shared padding aligns both panels inside the card.
  • The control panel stacks guidance with consistent spacing.
  • The dashed border gives the native input a clear upload role.
  • Save src/style.css.
  • Refresh the LensCheck page.

You should see balanced spacing around both panels. The file input should have a dashed violet border.

Panel Spacing Looks Uneven?

Confirm that the grouped selector includes .control-panel plus .visual-panel. Check that the comma remains between them.

Help me correct the LensCheck panel spacing.

Visible focus is essential for keyboard navigation. The next rules preserve the native control while making keyboard position clear.

  • Append the disabled, focus, privacy, and status styles with this code:
.file-input:disabled {
  cursor: wait;
  opacity: 0.58;
}

.file-input:focus-visible,
.brand:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}

.privacy-note {
  margin-bottom: 0;
  font-size: 0.82rem;
}

.status {
  padding: 0.9rem 1rem;
  border-radius: 12px;
  font-size: 0.9rem;
  font-weight: 750;
}

Why Keep the Native Input?

The native input already supports keyboard interaction plus the operating system's file picker. Its custom outline makes keyboard focus visible.

  • Save src/style.css.
  • Refresh the LensCheck page.
  • Press Tab until the brand link receives focus.

You should see an amber outline around the focused brand link.

Focus Outline Missing?

Use the keyboard to reach the brand link because :focus-visible responds to keyboard focus. Check that both selectors include their leading periods.

Help me debug the LensCheck keyboard focus styles.

  • Append the four status-state styles with this code:
.status[data-state="working"] {
  color: #5b21b6;
  background: #ede9fe;
}

.status[data-state="ready"] {
  color: #075985;
  background: #e0f2fe;
}

.status[data-state="success"] {
  color: #166534;
  background: #dcfce7;
}

.status[data-state="error"] {
  color: #991b1b;
  background: #fee2e2;
}

How Do State Colors Help?

  • Each data-state value maps to a distinct foreground and background pair.
  • The message remains the primary explanation while color supports quick recognition.
  • Save src/style.css.
  • Refresh the LensCheck page.

You should see the loading message inside a violet status panel.

Status Has No Violet Background?

Confirm that the status element still has data-state="working" in index.html. Compare that value with the CSS selector.

Help me fix the LensCheck status-state styling.

  • Append the image-stage styles with this code:
.visual-panel {
  min-width: 0;
}

.image-stage {
  display: grid;
  min-height: 360px;
  overflow: hidden;
  place-items: center;
  border-radius: 20px;
  background: #eeeaf7;
}

.stage-placeholder {
  max-width: 260px;
  margin: 0;
  color: #746d85;
  text-align: center;
}

.preview-source {
  display: none;
}

How Does the Image Stage Work?

  • The stage centers its placeholder within a stable preview area.
  • The source image stays hidden because the canvas becomes the visible surface.
  • The rounded container keeps large images inside the card.
  • Save src/style.css.
  • Refresh the LensCheck page.

You should see the placeholder centered inside a rounded pale-violet stage.

Image Stage Has No Height?

Check that min-height: 360px remains inside .image-stage. Confirm that its closing brace follows the background rule.

Help me restore the LensCheck image stage.

  • Append the canvas and result-region styles with this code:
.preview-canvas {
  display: block;
  width: 100%;
  height: auto;
}

.results {
  margin-top: 1.75rem;
}

.results-heading {
  margin-bottom: 1rem;
}

.results-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 0.75rem;
}

.result-card,
.empty-state {
  border: 1px solid #ded7ef;
  border-radius: 16px;
  background: #ffffff;
}

How Will Results Reflow?

  • The canvas scales to the panel width while preserving the image ratio.
  • The result grid creates as many readable columns as the available width supports.
  • Result cards plus empty feedback share one border treatment.
  • Save src/style.css.
  • Refresh the LensCheck page.

The visible stage should retain its shape with no stylesheet errors in the running Vite terminal.

Seeing a Stylesheet Error?

Check the latest selectors for a missing closing brace. Confirm that .result-card plus .empty-state are separated by a comma.

Help me locate the syntax error in my result-region CSS.

  • Append the result-card content styles with this code:
.result-card {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 1rem;
}

.result-label {
  font-weight: 850;
}

.result-score {
  color: #6d28d9;
  font-size: 0.86rem;
  font-weight: 750;
}

.empty-state {
  grid-column: 1 / -1;
  padding: 1.2rem;
}

.empty-state h3 {
  margin-bottom: 0.35rem;
}

.empty-state p {
  margin-bottom: 0;
}

How Will Predictions Read?

  • Each result card stacks the object label above its confidence value.
  • The empty state spans the full result grid so recovery guidance remains prominent.
  • Save src/style.css.
  • Refresh the LensCheck page.

The interface should retain its spacing with no errors in the running Vite terminal.

Vite Reports a CSS Error?

Inspect the latest selectors for missing periods or braces. The .result-score and .empty-state names must match the HTML structure.

Help me fix my LensCheck result-card CSS.

A responsive breakpoint lets the task follow the available screen width. The two panels become one vertical flow on narrow screens.

  • Append the narrow-screen rules with this code:
@media (max-width: 760px) {
  .site-header {
    align-items: flex-start;
    gap: 1rem;
  }

  .app-card {
    grid-template-columns: 1fr;
  }

  .control-panel {
    border-right: 0;
    border-bottom: 1px solid #e8e3f2;
  }

  .image-stage {
    min-height: 260px;
  }
}

What Changes on Narrow Screens?

  • The application card changes from two columns to one column below 760px.
  • The divider moves beneath the control panel.
  • The image stage becomes shorter for smaller screens.
  • Save src/style.css.
  • Refresh the LensCheck page.
  • Narrow the Microsoft Edge window below 760px.

You should see the upload panel move above the image stage. The page should remain readable without horizontal scrolling.

Card Refuses to Stack?

Confirm that the media query closes after the .image-stage rule. Check that .app-card uses one grid column inside the query.

Help me debug the LensCheck responsive breakpoint.

✔️ Awesome, I've got everything!

The LensCheck visual system now adapts from desktop to narrow screens. Double-check that src/style.css is saved.

ⓧ I'd like to double check the full code

:root {
  color: #171329;
  background: #f6f3ff;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

* {
  box-sizing: border-box;
}

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  background:
    radial-gradient(circle at top left, rgba(196, 181, 253, 0.55), transparent 34rem),
    #f8f7fc;
}

button,
input {
  font: inherit;
}

[hidden] {
  display: none !important;
}

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  max-width: 1180px;
  margin: 0 auto;
  padding: 1.25rem clamp(1rem, 4vw, 3rem);
}

.brand {
  color: #4c1d95;
  font-size: 1.2rem;
  font-weight: 850;
  text-decoration: none;
}

.eyebrow,
.step-label {
  margin: 0;
  color: #6d28d9;
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

.page-shell {
  width: min(1180px, calc(100% - 2rem));
  margin: 0 auto;
  padding: 3.5rem 0 5rem;
}

.hero {
  max-width: 760px;
  margin-bottom: 2.5rem;
}

h1,
h2,
h3,
p {
  margin-top: 0;
}

h1 {
  max-width: 700px;
  margin: 0.6rem 0 1rem;
  font-size: clamp(2.6rem, 7vw, 5.8rem);
  line-height: 0.98;
  letter-spacing: -0.055em;
}

h2 {
  margin-bottom: 0.7rem;
  font-size: clamp(1.45rem, 3vw, 2rem);
}

.hero-copy,
.supporting-copy,
.privacy-note,
.empty-state p {
  color: #625d73;
  line-height: 1.65;
}

.hero-copy {
  max-width: 590px;
  font-size: 1.08rem;
}

.app-card {
  display: grid;
  grid-template-columns: minmax(250px, 0.78fr) minmax(0, 1.5fr);
  overflow: hidden;
  border: 1px solid #ded8ed;
  border-radius: 28px;
  background: #ffffff;
  box-shadow: 0 24px 70px rgba(55, 38, 90, 0.12);
}

.control-panel,
.visual-panel {
  padding: clamp(1.25rem, 4vw, 2.25rem);
}

.control-panel {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  border-right: 1px solid #e8e3f2;
  background: #fcfbff;
}

.file-label {
  color: #2f2850;
  font-size: 0.84rem;
  font-weight: 800;
}

.file-input {
  width: 100%;
  padding: 0.8rem;
  border: 2px dashed #a78bfa;
  border-radius: 14px;
  color: #3b315d;
  background: #f5f0ff;
  cursor: pointer;
}

.file-input:disabled {
  cursor: wait;
  opacity: 0.58;
}

.file-input:focus-visible,
.brand:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}

.privacy-note {
  margin-bottom: 0;
  font-size: 0.82rem;
}

.status {
  padding: 0.9rem 1rem;
  border-radius: 12px;
  font-size: 0.9rem;
  font-weight: 750;
}

.status[data-state="working"] {
  color: #5b21b6;
  background: #ede9fe;
}

.status[data-state="ready"] {
  color: #075985;
  background: #e0f2fe;
}

.status[data-state="success"] {
  color: #166534;
  background: #dcfce7;
}

.status[data-state="error"] {
  color: #991b1b;
  background: #fee2e2;
}

.visual-panel {
  min-width: 0;
}

.image-stage {
  display: grid;
  min-height: 360px;
  overflow: hidden;
  place-items: center;
  border-radius: 20px;
  background: #eeeaf7;
}

.stage-placeholder {
  max-width: 260px;
  margin: 0;
  color: #746d85;
  text-align: center;
}

.preview-source {
  display: none;
}

.preview-canvas {
  display: block;
  width: 100%;
  height: auto;
}

.results {
  margin-top: 1.75rem;
}

.results-heading {
  margin-bottom: 1rem;
}

.results-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 0.75rem;
}

.result-card,
.empty-state {
  border: 1px solid #ded7ef;
  border-radius: 16px;
  background: #ffffff;
}

.result-card {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 1rem;
}

.result-label {
  font-weight: 850;
}

.result-score {
  color: #6d28d9;
  font-size: 0.86rem;
  font-weight: 750;
}

.empty-state {
  grid-column: 1 / -1;
  padding: 1.2rem;
}

.empty-state h3 {
  margin-bottom: 0.35rem;
}

.empty-state p {
  margin-bottom: 0;
}

@media (max-width: 760px) {
  .site-header {
    align-items: flex-start;
    gap: 1rem;
  }

  .app-card {
    grid-template-columns: 1fr;
  }

  .control-panel {
    border-right: 0;
    border-bottom: 1px solid #e8e3f2;
  }

  .image-stage {
    min-height: 260px;
  }
}
Wire the local image preview

A temporary object URL lets the browser display a selected local file. The canvas copies that image into the visible stage while the hidden image remains the source.

Why Use a Local Object URL?

An object URL gives the current browser session access to the selected file. Revoking the URL after loading releases that temporary reference.

This preview loop stays inside the browser before image interpretation is added.

  • Switch to the open src/main.js tab.
  • Select the temporary entry-module code.
  • Replace it with these interface references:
import './style.css';

const imageInput = document.querySelector('#image-input');
const preview = document.querySelector('#preview');
const canvas = document.querySelector('#detection-canvas');
const status = document.querySelector('#status');
const results = document.querySelector('#results');
const resultsGrid = document.querySelector('#results-grid');
const stagePlaceholder = document.querySelector('.stage-placeholder');
const context = canvas.getContext('2d');

let objectUrl = null;

function setStatus(state, message) {
  status.dataset.state = state;
  status.textContent = message;
}

imageInput.disabled = false;

What Do These References Hold?

  • The interface constants hold the elements used during file selection and preview.
  • The context variable provides the canvas drawing surface.
  • The objectUrl variable tracks the temporary file reference.
  • The setStatus() helper updates both the message and its visual state.
  • Save src/main.js.
  • Refresh the LensCheck page.

You should see the file input at full opacity. You should also be able to activate it.

File Input Still Disabled?

Check that imageInput.disabled = false appears after the imageInput reference. Confirm that the selector matches the HTML identifier.

Help me enable the LensCheck image input.

File selection needs a guard for cancelled choices plus unsupported files. A new object URL can then point the hidden preview element at a valid image.

  • Append this file-selection listener to src/main.js.
imageInput.addEventListener('change', () => {
  const file = imageInput.files[0];

  if (!file) {
    return;
  }

  if (!file.type.startsWith('image/')) {
    setStatus('error', 'Choose an image file to continue.');
    return;
  }

  if (objectUrl) {
    URL.revokeObjectURL(objectUrl);
  }

  objectUrl = URL.createObjectURL(file);
  preview.alt = `Uploaded image named ${file.name}`;
  preview.src = objectUrl;
});

What Happens After File Selection?

  • The listener reads the first selected file.
  • The file-type check stops unsupported input before preview loading begins.
  • Any older object URL is released before a new one is created.
  • The hidden preview receives the image source plus a descriptive alternative label.
  • Save src/main.js.
  • Refresh the LensCheck page.
  • Activate the file input.
  • Choose a local image.

You should see the selected file name beside the native input. The stage should still show its placeholder.

File Selection Causes an Error?

Confirm that the listener sits below the interface references. Check that the selected file is an image.

Help me troubleshoot the LensCheck file-selection listener.

The image load event provides reliable natural dimensions for the canvas. Matching those dimensions prevents the browser from stretching the preview during drawing.

  • Append this preview-load listener to src/main.js.
preview.addEventListener('load', async () => {
  canvas.width = preview.naturalWidth;
  canvas.height = preview.naturalHeight;
  canvas.hidden = false;
  stagePlaceholder.hidden = true;
  results.hidden = true;
  context.drawImage(preview, 0, 0, canvas.width, canvas.height);

  if (objectUrl) {
    URL.revokeObjectURL(objectUrl);
    objectUrl = null;
  }
});

How Does the Canvas Preview Appear?

  • The canvas adopts the image's natural width and height.
  • The stage hides its placeholder before revealing the canvas.
  • The drawing context copies the complete image into the canvas.
  • The object URL is revoked after the browser finishes loading the image.
  • Save src/main.js.
  • Refresh the LensCheck page.
  • Choose a local image again.

You should see the selected image replace the stage placeholder. The results region should remain hidden.

Canvas Preview Missing?

Check that #preview plus #detection-canvas match the identifiers in index.html. Confirm that the chosen file is an image.

Help me debug the LensCheck canvas preview.

✔️ Awesome, I've got everything!

Your local preview code is ready. Double-check that src/main.js is saved.

ⓧ I'd like to double check the full code

import './style.css';

const imageInput = document.querySelector('#image-input');
const preview = document.querySelector('#preview');
const canvas = document.querySelector('#detection-canvas');
const status = document.querySelector('#status');
const results = document.querySelector('#results');
const resultsGrid = document.querySelector('#results-grid');
const stagePlaceholder = document.querySelector('.stage-placeholder');
const context = canvas.getContext('2d');

let objectUrl = null;

function setStatus(state, message) {
  status.dataset.state = state;
  status.textContent = message;
}

imageInput.disabled = false;

imageInput.addEventListener('change', () => {
  const file = imageInput.files[0];

  if (!file) {
    return;
  }

  if (!file.type.startsWith('image/')) {
    setStatus('error', 'Choose an image file to continue.');
    return;
  }

  if (objectUrl) {
    URL.revokeObjectURL(objectUrl);
  }

  objectUrl = URL.createObjectURL(file);
  preview.alt = `Uploaded image named ${file.name}`;
  preview.src = objectUrl;
});

preview.addEventListener('load', async () => {
  canvas.width = preview.naturalWidth;
  canvas.height = preview.naturalHeight;
  canvas.hidden = false;
  stagePlaceholder.hidden = true;
  results.hidden = true;
  context.drawImage(preview, 0, 0, canvas.width, canvas.height);

  if (objectUrl) {
    URL.revokeObjectURL(objectUrl);
    objectUrl = null;
  }
});

Before the final check, do you expect the two-column card to keep its layout when the browser becomes narrow?

  • Refresh the LensCheck page in Microsoft Edge.
  • Choose a clear local image through the file input.
  • Narrow the Microsoft Edge window to a mobile-like width.
  • Press Tab to move through the available controls.

You should see the selected image inside the canvas. The upload panel should stack above the image at narrow widths.

That is the responsive shell working. LensCheck now accepts a local image while preserving readable spacing and visible keyboard focus.

You have turned the Figma hierarchy into a working local image preview. Next, you will connect the browser model and inspect the raw predictions it returns.

Connect the Model and Inspect Raw Results

Your responsive LensCheck interface can preview a local image. It still has no way to interpret the pixels.

TensorFlow.js runs COCO-SSD in Microsoft Edge. You will compare its untouched prediction objects with the designed result state in your Figma prototype.

In this step, get ready to:
  • Load the object-detection model before enabling image uploads.
  • Analyze an uploaded image with a fixed confidence threshold.
  • Inspect the raw prediction objects against the designed results experience.
Load the browser model

The upload input starts disabled because detection depends on a loaded model. The first load downloads model assets before LensCheck can report that it is ready.

  • Return to src/main.js in Visual Studio Code.
  • Replace everything from the current style import through let objectUrl = null; with this initialization section:
import '@tensorflow/tfjs';
import * as cocoSsd from '@tensorflow-models/coco-ssd';
import './style.css';

const imageInput = document.querySelector('#image-input');
const preview = document.querySelector('#preview');
const canvas = document.querySelector('#detection-canvas');
const status = document.querySelector('#status');
const results = document.querySelector('#results');
const resultsGrid = document.querySelector('#results-grid');
const stagePlaceholder = document.querySelector('.stage-placeholder');
const context = canvas.getContext('2d');

let model = null;
let objectUrl = null;

function setStatus(state, message) {
  status.dataset.state = state;
  status.textContent = message;
}

What Does This Initialization Do?

  • The first import prepares TensorFlow.js for browser-based machine learning.
  • The second import exposes cocoSsd.load() through the cocoSsd namespace.
  • The model variable starts as null because the model is not ready yet.
  • The setStatus() helper keeps the visible message aligned with the status color.
  • Scroll to the bottom of src/main.js.
  • Add the model-loading function beneath the existing preview listener:
async function loadModel() {
  setStatus('working', 'Loading the recognition model...');

  try {
    model = await cocoSsd.load();
    imageInput.disabled = false;
    setStatus('ready', 'Model ready. Choose an image to begin.');
  } catch (error) {
    console.error(error);
    setStatus('error', 'The model could not load. Check your connection and refresh.');
  }
}

loadModel();

How Does Model Loading Work?

  • The working state tells the user why image selection remains unavailable.
  • The await expression pauses this function until cocoSsd.load() finishes.
  • A successful load enables imageInput and displays the ready state.
  • The catch block provides visible feedback if the model cannot load.
  • Save src/main.js.
  • Refresh LensCheck in Microsoft Edge.

The first model load may take longer than a normal page refresh because the browser downloads its assets. You will see Model ready. Choose an image to begin. when loading succeeds.

Does the Model Stay in Its Loading State?

Confirm that your internet connection is active. Check that both import lines match the installed package names in package.json.

Look in the running Vite terminal for an import or syntax problem. Correct the referenced line in src/main.js before refreshing Microsoft Edge.

Help me diagnose why the COCO-SSD model does not finish loading in my LensCheck Vite app.

Analyze an uploaded image

The detector accepts the hidden preview image as its input. The fixed score threshold keeps predictions below 0.5 out of the returned array.

  • Find the existing preview.addEventListener('load', section in src/main.js.
  • Replace that preview-load section with the detection section below:
async function detectObjects() {
  canvas.width = preview.naturalWidth;
  canvas.height = preview.naturalHeight;
  canvas.hidden = false;
  stagePlaceholder.hidden = true;
  results.hidden = true;
  context.drawImage(preview, 0, 0, canvas.width, canvas.height);
  setStatus('working', 'Analyzing the image...');

  try {
    const predictions = await model.detect(preview, 10, 0.5);
    results.hidden = false;
    resultsGrid.innerHTML = `<pre>${JSON.stringify(predictions, null, 2)}</pre>`;
    setStatus('success', 'Analysis complete. Raw model output is shown below.');
  } catch (error) {
    console.error(error);
    setStatus('error', 'LensCheck could not analyze this image. Try another file.');
  }
}

preview.addEventListener('load', async () => {
  await detectObjects();

  if (objectUrl) {
    URL.revokeObjectURL(objectUrl);
    objectUrl = null;
  }
});

What Does This Detection Code Do?

  • The Canvas matches the uploaded image dimensions before drawing the local preview.
  • The working state communicates that analysis is in progress.
  • The model.detect(preview, 10, 0.5) call analyzes the image with a maximum of ten returned boxes.
  • The JSON.stringify(predictions, null, 2) expression exposes each prediction without designing its presentation.
  • The updated load listener waits for detection before releasing the local object URL.
  • Save src/main.js.
  • Refresh LensCheck in Microsoft Edge.

You should see the model-ready message again. This confirms that the new detection function compiles before you test it with an image.

Does LensCheck Stop Before the Ready State?

Compare the braces around detectObjects() with the code above. Confirm that the updated preview listener appears only once in src/main.js.

Help me find the syntax or duplicate-listener problem in my LensCheck detection code.

✔️ Awesome, I've got everything!

Your model loader and raw detection path are saved in src/main.js.

ⓧ I'd like to double check the full code

Compare your complete src/main.js with this raw-model version.

import '@tensorflow/tfjs';
import * as cocoSsd from '@tensorflow-models/coco-ssd';
import './style.css';

const imageInput = document.querySelector('#image-input');
const preview = document.querySelector('#preview');
const canvas = document.querySelector('#detection-canvas');
const status = document.querySelector('#status');
const results = document.querySelector('#results');
const resultsGrid = document.querySelector('#results-grid');
const stagePlaceholder = document.querySelector('.stage-placeholder');
const context = canvas.getContext('2d');

let model = null;
let objectUrl = null;

function setStatus(state, message) {
  status.dataset.state = state;
  status.textContent = message;
}

imageInput.addEventListener('change', () => {
  const file = imageInput.files[0];

  if (!file) {
    return;
  }

  if (!file.type.startsWith('image/')) {
    setStatus('error', 'Choose an image file to continue.');
    return;
  }

  if (objectUrl) {
    URL.revokeObjectURL(objectUrl);
  }

  objectUrl = URL.createObjectURL(file);
  preview.alt = `Uploaded image named ${file.name}`;
  preview.src = objectUrl;
});

async function detectObjects() {
  canvas.width = preview.naturalWidth;
  canvas.height = preview.naturalHeight;
  canvas.hidden = false;
  stagePlaceholder.hidden = true;
  results.hidden = true;
  context.drawImage(preview, 0, 0, canvas.width, canvas.height);
  setStatus('working', 'Analyzing the image...');

  try {
    const predictions = await model.detect(preview, 10, 0.5);
    results.hidden = false;
    resultsGrid.innerHTML = `<pre>${JSON.stringify(predictions, null, 2)}</pre>`;
    setStatus('success', 'Analysis complete. Raw model output is shown below.');
  } catch (error) {
    console.error(error);
    setStatus('error', 'LensCheck could not analyze this image. Try another file.');
  }
}

preview.addEventListener('load', async () => {
  await detectObjects();

  if (objectUrl) {
    URL.revokeObjectURL(objectUrl);
    objectUrl = null;
  }
});

async function loadModel() {
  setStatus('working', 'Loading the recognition model...');

  try {
    model = await cocoSsd.load();
    imageInput.disabled = false;
    setStatus('ready', 'Model ready. Choose an image to begin.');
  } catch (error) {
    console.error(error);
    setStatus('error', 'The model could not load. Check your connection and refresh.');
  }
}

loadModel();
Inspect the raw result

LensCheck can now pass an uploaded image into the model. The next upload reveals the response exactly as the detector returns it.

  • Return to LensCheck in Microsoft Edge.
  • Wait for the status region to show Model ready. Choose an image to begin..

Before you choose a photo, consider this question: will the model output resemble the result cards in your Figma prototype.

  • Activate the Image file control.
  • Choose a clear photo containing a common object.

You will see the selected image above a raw JSON array. Each prediction object contains bbox, class, and score fields.

What Did the Raw Result Reveal?

  • The bbox array provides coordinates without showing which part of the image they describe.
  • The score value appears as a decimal without a readable confidence label.
  • The array gives every field equal visual weight, so users receive no clear hierarchy.
  • An empty array provides no guidance for choosing a better image.
  • Switch back to the Figma prototype from earlier.
  • Select the Results frame.
  • Compare its result-card hierarchy with the raw class and score fields.
  • Compare its image annotations with the raw bbox coordinate arrays.

The model connection works, but the untouched response falls short of the designed experience. That shortfall is the intended obstacle you needed to see before shaping the predictions for users.

Do You See an Empty Array or Analysis Error?

Try a brighter photo with one common object in clear view. Confirm that the model-ready message appeared before you selected the file.

If the image appears without raw output, compare your detectObjects() function with the full-code tab. Check that the preview listener calls it with await.

Help me determine why LensCheck does not show a raw prediction array after I choose an image.

You have proved that LensCheck can detect objects inside the browser. Next, you will turn its raw coordinates and decimal scores into labeled boxes, readable confidence cards, and recovery guidance.

Design Results and Deploy LensCheck

Your COCO-SSD model now returns valid predictions. The raw JSON array still makes people interpret coordinates and decimal scores for themselves.

This step turns those predictions into visible explanations and recovery states. You will test the experience against your prototype before publishing it with Firebase Hosting.

In this step, get ready to:
  • Replace raw predictions with bounding boxes and confidence cards.
  • Test the success state and the no-results state.
  • Build the production app and deploy it to Firebase Hosting.
Design the live results

Each prediction contains a bbox array with the box position and size. A Canvas overlay can connect that prediction directly to the object in the image.

  • In src/main.js, find the closing brace of setStatus().
  • Add these formatting and drawing functions below setStatus():
function formatLabel(label) {
  return label.charAt(0).toUpperCase() + label.slice(1);
}

function drawDetections(predictions) {
  const fontSize = Math.max(16, Math.round(canvas.width / 45));

  context.drawImage(preview, 0, 0, canvas.width, canvas.height);
  context.lineWidth = Math.max(3, Math.round(canvas.width / 220));
  context.font = `${fontSize}px system-ui`;
  context.strokeStyle = '#6d28d9';
  context.fillStyle = '#6d28d9';

  predictions.forEach((prediction) => {
    const [x, y, width, height] = prediction.bbox;
    const label = `${formatLabel(prediction.class)} ${Math.round(prediction.score * 100)}%`;
    const textY = y > fontSize + 10 ? y - 8 : y + fontSize + 8;

    context.strokeRect(x, y, width, height);
    context.fillText(label, x + 4, textY);
  });
}

What does this code do?

  • The formatLabel() function gives each detected class a readable capital letter.
  • The drawDetections() function redraws the uploaded image before adding its annotations.
  • Each bbox value becomes a violet rectangle with a class label and rounded confidence percentage.
  • In detectObjects(), find the current model detection line:
const predictions = await model.detect(preview, 10, 0.5);

What does this line control?

The model checks the uploaded image for up to 10 objects. The fixed 0.5 threshold keeps predictions with at least 50% confidence.

  • Add the drawing call directly below the model detection line so both lines look like this:
const predictions = await model.detect(preview, 10, 0.5);
drawDetections(predictions);

How does the image change?

The first line returns the model predictions. The second line connects those predictions to the image with visible annotations.

  • Save src/main.js.

Before you test this change, do you expect the boxes to match the objects represented by the raw array?

  • Switch back to Microsoft Edge.
  • Wait for the model-ready status.
  • Choose a clear photo containing a supported object.

You should see violet boxes and labels drawn over the image. The raw predictions remain below it for one last comparison.

Don't see the violet boxes?

Confirm that drawDetections(predictions) sits immediately below the model detection call. Check that drawDetections() appears before detectObjects() in the file.

Make sure the selected photo contains a common object in clear view.

Help me debug the missing LensCheck bounding boxes.

The image now explains where each prediction belongs. The results region still needs readable hierarchy and guidance for images with no confident detections.

  • Add renderResults() below drawDetections() in src/main.js:
function renderResults(predictions) {
  results.hidden = false;

  if (predictions.length === 0) {
    resultsGrid.innerHTML = `
      <div class="empty-state">
        <h3>No confident objects found</h3>
        <p>Try a brighter or closer photo with one common object in clear view.</p>
      </div>
    `;
    setStatus('ready', 'Analysis complete. Try another image for a clearer result.');
    return;
  }

  resultsGrid.innerHTML = predictions
    .map(
      (prediction) => `
        <article class="result-card">
          <span class="result-label">${formatLabel(prediction.class)}</span>
          <span class="result-score">${Math.round(prediction.score * 100)}% confidence</span>
        </article>
      `,
    )
    .join('');

  setStatus(
    'success',
    `Analysis complete. ${predictions.length} ${predictions.length === 1 ? 'object' : 'objects'} found.`,
  );
}

What does this code do?

  • An empty prediction array produces recovery guidance that suggests a clearer image.
  • Each successful prediction becomes a result card with a formatted class and rounded confidence.
  • The live status reports whether the analysis found one object or several objects.
  • Find the existing detectObjects() function in src/main.js.
  • Replace that entire function with this final version:
async function detectObjects() {
  canvas.width = preview.naturalWidth;
  canvas.height = preview.naturalHeight;
  canvas.hidden = false;
  stagePlaceholder.hidden = true;
  results.hidden = true;
  context.drawImage(preview, 0, 0, canvas.width, canvas.height);
  setStatus('working', 'Analyzing the image...');

  try {
    const predictions = await model.detect(preview, 10, 0.5);
    drawDetections(predictions);
    renderResults(predictions);
  } catch (error) {
    console.error(error);
    setStatus('error', 'LensCheck could not analyze this image. Try another file.');
  }
}

How does the analysis flow work?

  • The canvas adopts the uploaded image dimensions before displaying the local preview.
  • The status region announces the analyzing state while the model works.
  • A successful detection sends the same predictions to the canvas annotations and result cards.
  • A failed detection produces an error message with a clear recovery action.
  • Save src/main.js.
  • Switch back to Microsoft Edge.
  • Wait for the model-ready status.
  • Choose the clear supported-object photo again.

You should see violet annotations over the image and readable confidence cards below it. The live status should report the number of objects found.

Still seeing raw prediction data?

Confirm that the old line containing JSON.stringify(predictions, null, 2) is gone. The final detectObjects() function should call renderResults(predictions) instead.

Help me replace the raw LensCheck output.

✔️ Awesome, I've got everything!

Your saved src/main.js now turns model predictions into annotated images and readable result states.

ⓧ I'd like to double check the full code

import '@tensorflow/tfjs';
import * as cocoSsd from '@tensorflow-models/coco-ssd';
import './style.css';

const imageInput = document.querySelector('#image-input');
const preview = document.querySelector('#preview');
const canvas = document.querySelector('#detection-canvas');
const status = document.querySelector('#status');
const results = document.querySelector('#results');
const resultsGrid = document.querySelector('#results-grid');
const stagePlaceholder = document.querySelector('.stage-placeholder');
const context = canvas.getContext('2d');

let model = null;
let objectUrl = null;

function setStatus(state, message) {
  status.dataset.state = state;
  status.textContent = message;
}

function formatLabel(label) {
  return label.charAt(0).toUpperCase() + label.slice(1);
}

function drawDetections(predictions) {
  const fontSize = Math.max(16, Math.round(canvas.width / 45));

  context.drawImage(preview, 0, 0, canvas.width, canvas.height);
  context.lineWidth = Math.max(3, Math.round(canvas.width / 220));
  context.font = `${fontSize}px system-ui`;
  context.strokeStyle = '#6d28d9';
  context.fillStyle = '#6d28d9';

  predictions.forEach((prediction) => {
    const [x, y, width, height] = prediction.bbox;
    const label = `${formatLabel(prediction.class)} ${Math.round(prediction.score * 100)}%`;
    const textY = y > fontSize + 10 ? y - 8 : y + fontSize + 8;

    context.strokeRect(x, y, width, height);
    context.fillText(label, x + 4, textY);
  });
}

function renderResults(predictions) {
  results.hidden = false;

  if (predictions.length === 0) {
    resultsGrid.innerHTML = `
      <div class="empty-state">
        <h3>No confident objects found</h3>
        <p>Try a brighter or closer photo with one common object in clear view.</p>
      </div>
    `;
    setStatus('ready', 'Analysis complete. Try another image for a clearer result.');
    return;
  }

  resultsGrid.innerHTML = predictions
    .map(
      (prediction) => `
        <article class="result-card">
          <span class="result-label">${formatLabel(prediction.class)}</span>
          <span class="result-score">${Math.round(prediction.score * 100)}% confidence</span>
        </article>
      `,
    )
    .join('');

  setStatus(
    'success',
    `Analysis complete. ${predictions.length} ${predictions.length === 1 ? 'object' : 'objects'} found.`,
  );
}

async function detectObjects() {
  canvas.width = preview.naturalWidth;
  canvas.height = preview.naturalHeight;
  canvas.hidden = false;
  stagePlaceholder.hidden = true;
  results.hidden = true;
  context.drawImage(preview, 0, 0, canvas.width, canvas.height);
  setStatus('working', 'Analyzing the image...');

  try {
    const predictions = await model.detect(preview, 10, 0.5);
    drawDetections(predictions);
    renderResults(predictions);
  } catch (error) {
    console.error(error);
    setStatus('error', 'LensCheck could not analyze this image. Try another file.');
  }
}

imageInput.addEventListener('change', () => {
  const file = imageInput.files[0];

  if (!file) {
    return;
  }

  if (!file.type.startsWith('image/')) {
    setStatus('error', 'Choose an image file to continue.');
    return;
  }

  if (objectUrl) {
    URL.revokeObjectURL(objectUrl);
  }

  objectUrl = URL.createObjectURL(file);
  preview.alt = `Uploaded image named ${file.name}`;
  preview.src = objectUrl;
});

preview.addEventListener('load', async () => {
  await detectObjects();

  if (objectUrl) {
    URL.revokeObjectURL(objectUrl);
    objectUrl = null;
  }
});

async function loadModel() {
  setStatus('working', 'Loading the recognition model...');

  try {
    model = await cocoSsd.load();
    imageInput.disabled = false;
    setStatus('ready', 'Model ready. Choose an image to begin.');
  } catch (error) {
    console.error(error);
    setStatus('error', 'The model could not load. Check your connection and refresh.');
  }
}

loadModel();
Test every result state

Your Figma prototype defines how LensCheck communicates success and uncertainty. The live app now needs to prove those states work with real images and keyboard input.

  • Choose a clear photo containing a person, bottle, chair, dog, car, or another supported object.

You should see the analyzing status first. You should then see labeled boxes and confidence cards with a success message.

  • Choose a visually ambiguous image without a clear common object.

When no prediction meets the fixed confidence threshold, you should see the no-results card with guidance to try a brighter or closer photo.

Still getting confident results?

Try a close-up texture or an image without a recognizable foreground object. The goal is to produce an empty prediction array at the fixed threshold.

Help me test the LensCheck no-results state.

The native file input preserves keyboard access without adding custom button behavior. This test checks that the coded interface keeps that benefit.

  • Click an empty area of the LensCheck page.
  • Press Tab until the image file input receives the visible focus outline.
  • Press Enter to activate the file input.
  • Choose a supported-object photo in the Windows file picker.

You should see the same analyzing and results flow without using the mouse to activate the upload control. Your coded states now match the interaction promises from the prototype.

Can't activate the input with Enter?

Confirm that the focus outline reaches the native file input before you press Enter. Check that the input is enabled after the model-ready status appears.

Help me troubleshoot the LensCheck keyboard flow.

Build and deploy LensCheck

The development server serves source files for local iteration. A production build uses Vite to create optimized static files in dist for Hosting.

  • Switch back to the Visual Studio Code PowerShell terminal from earlier.
  • Stop the local development server by pressing Ctrl+C.
  • Build the production app by running:
npm run build

What does this command do?

The build script runs vite build from package.json. Vite writes the deployable application into dist.

The terminal should report a completed build. You should also see a new dist folder in the Visual Studio Code Explorer sidebar.

Did the production build fail?

Read the first file and line reported by the build output. Compare that location in src/main.js with the full-code reference above.

Help me fix my LensCheck production build.

Choices for the Hosting initializer

The initializer asks you to connect an existing Firebase project. It also asks which directory contains the public files.

Use the Firebase project you created earlier. Use dist as the public directory. Keep the deployment configured as a static site.

  • Initialize Firebase Hosting by running:
firebase init hosting

What does initialization create?

The initializer connects the local lenscheck workspace to your existing Firebase project. It creates firebase.json and a project alias file named .firebaserc.

  • Return to the Visual Studio Code Explorer sidebar.
  • Select the generated firebase.json file.
  • Replace its contents with this Hosting configuration:
{
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}

What does this configuration do?

  • The public setting tells Hosting to deploy the production files from dist.
  • The ignore list keeps configuration files and installed dependencies out of the upload.
  • Save firebase.json.

Publishing gives the app a public URL that anyone can open. This static project stays within Firebase Hosting's documented no-cost storage and data-transfer quotas for a small demo.

Before you deploy, do you expect the public site to behave differently from the tested production build?

  • Deploy the production files by running:
firebase deploy --only hosting

What does this command do?

Firebase uploads the contents of dist using the Hosting configuration. The completed deployment returns public addresses that include PROJECT_ID.web.app.

Did the deployment fail?

Confirm that the build created dist before deployment. Check that your terminal is still authenticated to the Firebase project from earlier.

Help me troubleshoot my LensCheck deployment.

  • Open the returned web.app URL in Microsoft Edge.
  • Choose a clear supported-object photo on the public site.

You should see the working status followed by labeled violet boxes and confidence cards on the public site.

  • Choose the ambiguous test image on the public site.

You should see the no-results recovery guidance when no prediction reaches the fixed confidence threshold. That completes the full ready-to-recovery flow on your deployed LensCheck site.

You have turned raw model output into a public AI experience that explains its results and helps people recover from uncertainty.

Secret mission

Let Users Control Model Confidence

LensCheck currently decides which detections are certain enough to display. Add a confidence control that lets users compare broader results at 50% with more selective results at 90%.

Clean Up Your Resources

Clean Up Your Resources

LensCheck has no expected ongoing cost while its Firebase Hosting usage stays within 10 GB of storage and 10 GB of monthly data transfer. Choose whether to keep the demo live, pause public access, or delete the resources you no longer need.

Resources you used:

  • A clickable LensCheck prototype in Figma.
  • A local lenscheck folder containing the source files and generated dist folder.
  • A Firebase Hosting site serving LensCheck at https://PROJECT_ID.web.app.

Keep everything running

No action needed. Choose this if you want to keep demonstrating LensCheck or continue improving its AI interaction states.

  • Keep the public LensCheck site available at https://PROJECT_ID.web.app.
  • Retain the local lenscheck folder for future edits.
  • Retain the Figma prototype as a record of the interaction design.

Pause - I'll come back to this later

Pause public access while keeping your Firebase configuration and local files. A later deployment restores the site when you are ready to continue.

  • Switch back to the Visual Studio Code terminal for your existing lenscheck workspace.
  • Disable public Hosting access by running this command:
firebase hosting:disable

This stops web traffic to the current Firebase Hosting site. Your local files and Firebase configuration remain available.

  • Refresh https://PROJECT_ID.web.app to confirm that LensCheck is no longer being served.
  • Restore the public site later by running this command:
firebase deploy --only hosting

This deploys the contents of dist and re-enables Firebase Hosting.

  • Open https://PROJECT_ID.web.app after the deployment finishes.

You should see the public LensCheck experience with its confidence control available again.

Firebase Command Failed?

Confirm that you are still signed in to the Google Account connected to the LensCheck Firebase project. Run the command again from the existing lenscheck workspace.

Help me troubleshoot Firebase Hosting access.

Delete - I don't want to use this again

Deleting the Hosting site is permanent. The local code and Figma prototype can also be removed independently.

Delete the Firebase Hosting site.

  • Switch back to the Visual Studio Code terminal for your existing lenscheck workspace.
  • List the Hosting sites connected to your Firebase project by running:
firebase hosting:sites:list

This lists each Hosting site ID with its default URL. Match the URL to the LensCheck site before selecting an ID.

  • Copy the LensCheck site ID here: your Firebase Hosting site ID.

Take a moment to check the site ID before continuing. The next command permanently removes that Hosting site.

  • Delete the selected Hosting site by running:
firebase hosting:sites:delete [[FIREBASE_SITE_ID="your Firebase Hosting site ID"]]

This targets the Hosting site whose ID you recorded. Firebase asks for confirmation before completing the deletion.

  • Confirm the deletion when prompted.
  • Open https://PROJECT_ID.web.app to verify the site is gone.

The address should no longer serve LensCheck. Your Firebase project remains available in your account.

Hosting Site Still Available?

Check that the site ID matched the LensCheck URL from the site list. Confirm that the deletion command completed before testing the public address again.

Help me verify my Firebase Hosting site deletion.

Delete the local project.

  • Switch back to the Visual Studio Code window containing your existing lenscheck workspace.
  • Stop the local development server with Ctrl+C if it is still running.
  • Move above the lenscheck folder and remove it by running these PowerShell commands:
cd ..
Remove-Item -Recurse -Force lenscheck

The first command moves the terminal into the folder that contains lenscheck. The second command removes the project source and generated dist folder.

  • Check the parent folder with Windows file management.

You should no longer see the lenscheck folder.

Folder Refuses to Delete?

Stop any LensCheck development process that still uses the folder. Close any file from lenscheck that remains open in another application.

Help me remove my local LensCheck folder.

Delete the Figma prototype.

  • Return to the Figma file browser that contains your LensCheck prototype.
  • Use the LensCheck file's context menu to delete the prototype.
  • Confirm that the prototype no longer appears among your active files.

Nice Work!

Nice Work!

You did it! Your deployed LensCheck experience now makes browser-based object detection understandable from upload through confidence control.

You've learned how to:

  • Design a complete AI interaction lifecycle in Figma with ready, analyzing, results, empty, and error states.
  • Run object detection locally with COCO-SSD through TensorFlow.js inside a responsive interface that keeps image selection keyboard accessible.
  • Translate raw predictions into labeled bounding boxes plus confidence-aware result cards. Publish the finished experience with Firebase Hosting.
  • Secret Mission: Add a confidence control that reruns detection from 50% to 90%. Users can see lower-confidence predictions disappear as certainty rises.

Ready to quiz yourself?