Employee Seating System with AI

Build a full-stack seating manager with an AI natural language assistant.

Introduction

30 Second Summary

Picture a growing office where people switch desks every week. Keeping track of who sits where turns into a messy spreadsheet that nobody wants to maintain.

In this project, you will build an AI-powered seating management app. Type a command like "Move Alice to seat B3" and watch the office floor plan update in real time.

What You'll Build

Type "Swap Alice and Bob" into an AI chat box and watch two employees trade seats on a live floor plan, all powered by a Next.js app you built and deployed yourself.

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

  • A visual seating grid where you can see every employee's desk assignment and click any seat to reassign them on the spot.
  • An AI chat assistant powered by Google Gemini that understands natural language commands like "Move Alice to B3" and updates the grid instantly.
  • A live app deployed on Railway with a public URL and a clean GitHub repository, both ready to share.
  • Secret Mission: A search bar that highlights matching employees' seats on the grid and dims everyone else.

Are there any prerequisites?

You'll need Node.js installed, VS Code as your code editor, and a GitHub account. Step 1 handles everything else, including your free Gemini API key and Railway account.

Before We Start

Before jumping into any code, take a moment to think about the end product you are building and why it matters.

Scaffold the Next.js Project and Get Your API Key

You have a clear picture of what you are building. Now it is time to lay the foundation.

Every full-stack project starts with two essentials: a working project skeleton and access to the external services it depends on. In this step, you will scaffold a Next.js project, grab a free Gemini API key, and confirm the dev server runs.

In this step, get ready to:
  • Create a Next.js project with TypeScript, Tailwind CSS, App Router, and a src/ directory.
  • Obtain a free Gemini API key and install the Google Gen AI SDK.
  • Start the dev server and verify the starter page loads.
Create the Next.js project

Next.js 16 requires Node.js 20.9.0 or newer. Before scaffolding, confirm your Node.js version is compatible.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Check your Node.js version by running this command:
node --version

✔️ I see version 20.9.0 or higher

Your Node.js version is compatible with Next.js 16. You are ready to scaffold the project.

ⓧ I see an older version

Next.js 16 needs Node.js 20.9.0 or higher. Your current version is too old.

  • Go to the official Node.js downloads page at https://nodejs.org and download the latest LTS version.
  • Run the installer and follow the prompts.
  • Close and reopen your terminal, then run node --version again to confirm the update.

ⓧ Command not found

Node.js is not installed on your machine.

  • Go to the official Node.js downloads page at https://nodejs.org and download the latest LTS version.
  • Run the installer and follow the prompts.
  • Close and reopen your terminal, then run node --version again to confirm the install.

Now move to your Desktop and run the scaffolding command. The CLI is interactive and will ask you a series of setup questions.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your Next.js project by running this command:
npx create-next-app@latest seating-manager

What prompts will I see?

The CLI first asks if you want to use recommended defaults. The defaults include TypeScript, ESLint, Tailwind CSS, and App Router, but they do not include a src/ directory. This project needs a src/ directory, so you need to choose the customize path.

  • When prompted with Would you like to use the recommended Next.js defaults?, select No, customize settings.
  • Enter Yes for the Would you like to use TypeScript? prompt.
  • Select ESLint for the Which linter would you like to use? prompt.
  • Enter No for the Would you like to use React Compiler? prompt.
  • Enter Yes for the Would you like to use Tailwind CSS? prompt.
  • Enter Yes for the Would you like your code inside a src/ directory? prompt.
  • Enter Yes for the Would you like to use App Router? prompt.
  • Enter No for the Would you like to customize the import alias? prompt.
  • Enter Yes for the Would you like to include AGENTS.md? prompt.

The CLI creates the seating-manager folder on your Desktop and installs all dependencies automatically. This takes about a minute.

  • Move into the project folder by running this command:
cd seating-manager
  • Open the project in VS Code by running this command:
code .

code command not found?

If you see an error, open VS Code manually. Press Cmd+Space (macOS) or the Windows key (Windows), type VS Code, and press Enter.

  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the seating-manager folder on your Desktop, then click Open.

Need more help?

You should see the project files in the VS Code Explorer sidebar, including the src/app/ folder.

Get a free Gemini API key

The AI assistant in your app needs a Gemini API key to send prompts and receive responses. Google provides a free tier with no credit card required.

  • Open Google AI Studio in your browser.
  • Sign in with your Google account if prompted.
  • Click Create API Key.
  • Copy the generated key. You will need it in the next step.

Is this really free?

Yes. The Gemini API free tier provides free input and output tokens with no credit card required. The usage from this project stays well within the free tier limits.

Now save the key in a file that Next.js reads automatically for server-side environment variables.

  • In VS Code, create a new file in the project root called .env.local by clicking the new file icon in the Explorer sidebar.
  • Paste the following into .env.local, replacing the placeholder with the key you just copied:
GEMINI_API_KEY=your-api-key-here
  • Save the file.

Why .env.local?

Next.js automatically reads environment variables from .env.local and makes them available via process.env in server-side code like API routes. This file is already included in .gitignore by default, so your key stays out of version control.

Next, install the SDK your API route will use to talk to Gemini.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
  • Install the Google Gen AI SDK by running this command:
npm install @google/genai

You should see the package added to your package.json dependencies.

Install failing?

Make sure your terminal is inside the seating-manager folder. Run pwd to check your current directory. If you are somewhere else, run:

cd ~/Desktop/seating-manager

Then try the install command again. Still stuck? Get help here.

Update the page metadata and start the dev server

Before verifying everything works, update the default page metadata so it reflects your project instead of the generic Next.js starter text.

  • In the VS Code Explorer sidebar, open src/app/layout.tsx.
  • Find this line:
export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};
  • Replace it with:
export const metadata: Metadata = {
  title: "Office Seating Manager",
  description: "AI-powered employee seating management system",
};
  • Save layout.tsx.

Before you run the dev server, what do you expect to see in the browser? Take a moment to predict.

  • Start the development server by running this command in the VS Code terminal:
npm run dev
  • Open http://localhost:3000 in your browser.

You should see the default Next.js starter page with the Next.js logo and some getting-started links. The browser tab title should read Office Seating Manager.

Page not loading?

Check your terminal for error messages. If you see a port conflict, another process is already using port 3000. Stop the other process or run npx next dev -p 3001 to use a different port.

Make sure the npm run dev process is still running in your terminal. If you closed it, run the command again. Help me debug this.

✔️ Awesome, I've got everything!

Great. Double check you have saved both .env.local and layout.tsx.

ⓧ I'd like to double check the full code

Here is the complete .env.local file:

GEMINI_API_KEY=your-api-key-here

Here is the complete src/app/layout.tsx file:

import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Office Seating Manager",
  description: "AI-powered employee seating management system",
};
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

Your project skeleton is ready and the dev server is running. Next up, you will build the visual seating grid that turns this blank page into an interactive office floor plan.

Build the Seating Grid UI

Your project is scaffolded and the dev server is running. Now it is time to build the first thing anyone will see when they open your app: the seating grid.

The grid displays 12 office seats across three rows, with employee names in occupied seats and an "Empty" label for open ones. Starting with the UI gives you a visible result you can iterate on right away.

In this step, get ready to:
  • Create the seed data and type definitions for seats and employees.
  • Build a reusable seating grid component that renders seat cards.
  • Wire the grid into the home page and confirm it renders in the browser.
Create the seed data and shared types

Every seat in the office needs a data shape: an ID, a label like A1, and an optional employee name. You will define this shape as a TypeScript interface and then create an array of 12 seats that acts as the app's in-memory database.

  • In VS Code, expand the src/app folder in the Explorer sidebar on the left.
  • Right-click the app folder and select New File.
  • Name the file data.ts and press Enter.
  • Paste the following code into data.ts to define the seat shape and create the initial seating data:
export interface Seat {
  id: string;
  label: string;
  employee: string | null;
}

export const initialSeats: Seat[] = [
  { id: "a1", label: "A1", employee: "Alice" },
  { id: "a2", label: "A2", employee: "Bob" },
  { id: "a3", label: "A3", employee: null },
  { id: "a4", label: "A4", employee: "Charlie" },
  { id: "b1", label: "B1", employee: null },
  { id: "b2", label: "B2", employee: "Diana" },
  { id: "b3", label: "B3", employee: null },
  { id: "b4", label: "B4", employee: "Eve" },
  { id: "c1", label: "C1", employee: "Frank" },
  { id: "c2", label: "C2", employee: null },
  { id: "c3", label: "C3", employee: "Grace" },
  { id: "c4", label: "C4", employee: null },
];

What does this code do?

The Seat interface defines the shape of every seat object: a unique id, a human-readable label like A1, and an employee field that is either a name or null for empty seats.

The initialSeats array creates a 4-column, 3-row office layout (rows A, B, C) with seven employees already assigned and five seats left empty.

  • Still in data.ts, add the full roster of employees below the initialSeats array by pasting this code at the bottom of the file:
export const allEmployees = [
  "Alice",
  "Bob",
  "Charlie",
  "Diana",
  "Eve",
  "Frank",
  "Grace",
  "Hank",
  "Ivy",
  "Jack",
];

Why a separate employee list?

The allEmployees array contains ten names, including three (Hank, Ivy, Jack) who are not currently seated. This list powers the dropdown in the assignment modal you will build in the next step, so admins can pick from everyone in the company.

  • Save data.ts by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Check the terminal where your dev server is running. There should be no new errors.

Seeing a TypeScript error in the terminal?

  • Make sure the file is saved as data.ts (not data.tsx or data.js) inside the src/app folder.
  • Check for missing commas between objects in the initialSeats array.
  • Help me debug my data.ts file.

✔️ Awesome, I've got everything!

Great. Double check you have saved data.ts and you are ready to build the grid component.

ⓧ I'd like to double check the full code

Your data.ts file contains two code chunks you pasted above. Scroll up and confirm both are present in this order:

  • The Seat interface and initialSeats array (first chunk).
  • The allEmployees array (second chunk).
Build the seating grid component

The grid component takes the seats array and renders each seat as a clickable card. Occupied seats get a blue background and the employee's name. Empty seats get a dashed border and an "Empty" label.

  • In the Explorer sidebar, right-click the src/app folder and select New Folder.
  • Name the folder components and press Enter.
  • Right-click the new components folder and select New File.
  • Name the file SeatingGrid.tsx and press Enter.
  • Paste the following code into SeatingGrid.tsx to set up the component and its grid-mapping logic:
"use client";

import { Seat } from "../data";

// Define the props the grid component accepts
interface SeatingGridProps {
  seats: Seat[];
  onSeatClick: (seat: Seat) => void;
  highlightedEmployees?: string[];
}

export default function SeatingGrid({
  seats,
  onSeatClick,
  highlightedEmployees,
}: SeatingGridProps) {
  // Check if any employees are being highlighted
  const isHighlighting = highlightedEmployees && highlightedEmployees.length > 0;

  return (
    <div className="grid grid-cols-4 gap-3">
      {seats.map((seat) => {
        const isOccupied = seat.employee !== null;
        const isMatch =
          isHighlighting &&
          seat.employee &&
          highlightedEmployees.includes(seat.employee);
        const isDimmed = isHighlighting && !isMatch;

What does this code do?

  • "use client" tells Next.js this component runs in the browser, which is required for click handlers and state.
  • SeatingGridProps defines three props: the seats array, a click handler, and an optional highlightedEmployees array (used later in the Secret Mission).
  • Inside the map, three boolean variables (isOccupied, isMatch, isDimmed) determine how each seat card is styled.
  • Still in SeatingGrid.tsx, add the seat card JSX directly below the const isDimmed line by pasting this code:
        return (
          <button
            key={seat.id}
            onClick={() => onSeatClick(seat)}
            className={`p-4 rounded-lg border-2 text-center transition-all cursor-pointer
              ${
                isOccupied
                  ? "bg-blue-100 border-blue-300 hover:border-blue-500"
                  : "bg-gray-50 border-dashed border-gray-300 hover:border-gray-400"
              }
              ${isMatch ? "ring-2 ring-yellow-400 border-yellow-400 bg-yellow-50" : ""}
              ${isDimmed ? "opacity-30" : ""}
            `}
          >
            <div className="text-xs font-mono text-gray-500">{seat.label}</div>
            <div
              className={`text-sm font-medium mt-1 ${
                isOccupied ? "text-blue-800" : "text-gray-400"
              }`}
            >
              {seat.employee || "Empty"}
            </div>
          </button>
        );
      })}
    </div>
  );
}

What does this JSX render?

Each seat is a <button> element inside a 4-column Tailwind CSS grid. The seat label (like A1) appears in small monospace text at the top of each card.

Occupied seats show the employee name in blue. Empty seats show "Empty" in gray with a dashed border. The isMatch and isDimmed classes are wired up for the search feature you can add in the Secret Mission.

  • Save SeatingGrid.tsx.
  • Check the terminal. There should be no new errors.

Seeing an import error for Seat?

  • Confirm data.ts is saved in src/app/ (not inside the components folder). The import path "../data" goes up one level from components to app.
  • Make sure you exported the Seat interface with the export keyword in data.ts.
  • Help me debug my import paths.

✔️ Awesome, I've got everything!

Great. Confirm you have saved SeatingGrid.tsx and move on to wire it into the page.

ⓧ I'd like to double check the full code

Your SeatingGrid.tsx file contains two code chunks you pasted above. Scroll up and confirm both are present in this order:

  • The "use client" directive, imports, interface, and function declaration through the isDimmed variable (first chunk).
  • The return statement with the <button> JSX through the closing } of the function (second chunk).
Wire up the home page

The data and the grid component are ready. Now you need to connect them on the home page so the browser actually renders the seating chart.

  • In the Explorer sidebar, open src/app/page.tsx.
  • Select all the existing code in this file and replace it with the following:
"use client";

import { useState } from "react";
import { initialSeats, Seat } from "./data";
import SeatingGrid from "./components/SeatingGrid";

export default function Home() {
  // Store the seats in state so we can update them later
  const [seats, setSeats] = useState<Seat[]>(initialSeats);

  return (
    <main className="min-h-screen bg-gray-50 p-8">
      <h1 className="text-2xl font-bold text-gray-900 mb-6">
        Office Seating Manager
      </h1>
      <div className="bg-white rounded-xl shadow-sm p-6">
        <h2 className="text-lg font-semibold mb-4">Floor Plan</h2>
        <SeatingGrid seats={seats} onSeatClick={() => {}} />
      </div>
    </main>
  );
}

What does this page do?

  • useState holds the seats array in React state. This makes it possible to update the grid later when employees are assigned or moved.
  • The SeatingGrid component receives seats as a prop and renders the grid you just built.
  • onSeatClick is set to an empty function for now. You will connect it to an assignment modal in the next step.

Before you check the browser, take a guess: what do you expect to see at http://localhost:3000 now?

  • Save page.tsx.
  • Switch to your browser and refresh http://localhost:3000.

You should see a white card titled "Floor Plan" containing a 4-column grid of 12 seat cards. Seats with employees (like Alice in A1, Bob in A2) show names in blue. Empty seats (like A3, B1) show "Empty" in gray with a dashed border.

Don't see the grid?

  • Make sure all three files are saved: data.ts, SeatingGrid.tsx, and page.tsx.
  • Check the terminal for errors. A common issue is a typo in the import path. The page imports from "./data" and "./components/SeatingGrid".
  • Confirm SeatingGrid.tsx is inside src/app/components/, not directly in src/app/.
  • Help me troubleshoot my seating grid.

✔️ Awesome, I've got everything!

Great. Double check you have saved page.tsx and that the grid is visible in your browser.

ⓧ I'd like to double check the full code

Here is the complete page.tsx for this step:

"use client";

import { useState } from "react";
import { initialSeats, Seat } from "./data";
import SeatingGrid from "./components/SeatingGrid";

export default function Home() {
  // Store the seats in state so we can update them later
  const [seats, setSeats] = useState<Seat[]>(initialSeats);

  return (
    <main className="min-h-screen bg-gray-50 p-8">
      <h1 className="text-2xl font-bold text-gray-900 mb-6">
        Office Seating Manager
      </h1>
      <div className="bg-white rounded-xl shadow-sm p-6">
        <h2 className="text-lg font-semibold mb-4">Floor Plan</h2>
        <SeatingGrid seats={seats} onSeatClick={() => {}} />
      </div>
    </main>
  );
}

Your office floor plan is alive in the browser. Next up, you will make those seat cards interactive by adding a click-to-assign modal.

Add Manual Seat Assignment

In the last step you built a visual seating grid that shows who sits where. It looks good, but clicking on a seat does nothing. An admin needs a way to actually move people around.

In this step, you will build a modal dialog that pops up when any seat is clicked. The modal lets you assign an available employee to an empty seat or remove someone from an occupied one.

In this step, get ready to:
  • Build an assignment modal component with a dropdown of available employees.
  • Wire the modal to the seating grid so clicks trigger seat assignments.
Build the assignment modal

The modal needs to know which seat was clicked, which employees are still unassigned, and how to send the assignment change back to the parent page.

  • Create a new file at src/app/components/AssignModal.tsx and paste the following code to set up the component skeleton:
"use client";

import { useState } from "react";
import { Seat } from "../data";

interface AssignModalProps {
  seat: Seat;
  availableEmployees: string[];
  onAssign: (seatId: string, employee: string | null) => void;
  onClose: () => void;
}

export default function AssignModal({
  seat,
  availableEmployees,
  onAssign,
  onClose,
}: AssignModalProps) {
  // Track which employee the admin selects from the dropdown
  const [selected, setSelected] = useState("");

  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
      <div className="bg-white rounded-xl p-6 w-80 shadow-xl">
        <h3 className="text-lg font-semibold mb-1">Seat {seat.label}</h3>
      </div>
    </div>
  );
}

What does this code do?

The AssignModalProps interface defines the four pieces of data the modal needs from its parent: the clicked seat object, a list of availableEmployees not currently assigned, an onAssign callback to apply changes, and an onClose callback to dismiss the modal.

The outer div creates a semi-transparent dark overlay covering the full screen. The inner div is the white card centered on that overlay. Right now it only shows the seat label.

  • In AssignModal.tsx, find the <h3> line. Add the following code directly below it, above the closing </div> tags:
        {seat.employee ? (
          <p className="text-sm text-gray-600 mb-4">
            Currently assigned to{" "}
            <span className="font-medium">{seat.employee}</span>
          </p>
        ) : (
          <p className="text-sm text-gray-600 mb-4">This seat is empty</p>
        )}

        {seat.employee && (
          <button
            onClick={() => {
              onAssign(seat.id, null);
              onClose();
            }}
            className="w-full mb-3 px-4 py-2 bg-red-100 text-red-700 rounded-lg hover:bg-red-200 transition-colors"
          >
            Unassign {seat.employee}
          </button>
        )}

What does this code do?

The first block conditionally renders different text depending on whether the seat already has someone assigned. Occupied seats show the employee's name. Empty seats show "This seat is empty".

The unassign button only appears for occupied seats. When clicked, it calls onAssign(seat.id, null) to clear the assignment, then closes the modal.

  • Still in AssignModal.tsx, add the employee dropdown, assign button, and cancel button directly below the unassign button's closing )}, above the closing </div> tags:
        {availableEmployees.length > 0 && (
          <>
            <select
              value={selected}
              onChange={(e) => setSelected(e.target.value)}
              className="w-full p-2 border border-gray-300 rounded-lg mb-3"
            >
              <option value="">Select an employee</option>
              {availableEmployees.map((emp) => (
                <option key={emp} value={emp}>
                  {emp}
                </option>
              ))}
            </select>
            <button
              onClick={() => {
                if (selected) {
                  onAssign(seat.id, selected);
                  onClose();
                }
              }}
              disabled={!selected}
              className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
            >
              Assign
            </button>
          </>
        )}

        <button
          onClick={onClose}
          className="w-full mt-2 px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors"
        >
          Cancel
        </button>

What do the dropdown and buttons do?

The <select> dropdown renders one <option> for each unassigned employee. The selected state tracks the admin's current choice.

The blue Assign button is disabled until an employee is selected. Once clicked, it calls onAssign with the seat ID and the chosen employee, then closes the modal. The Cancel button simply calls onClose to dismiss without changes.

  • Save AssignModal.tsx.

The modal component is complete, but it does not appear on screen yet. You will wire it into the page next.

✔️ Awesome, I've got everything!

Great. Double check you have saved AssignModal.tsx before moving on.

ⓧ I'd like to double check the full code

Your AssignModal.tsx file is longer than 30 lines, so confirm each section is present in this order:

  • The "use client" directive, useState import, and Seat import at the top.
  • The AssignModalProps interface with seat, availableEmployees, onAssign, and onClose.
  • The AssignModal function with selected state and a return block.
  • Inside the return: dark overlay div, white card div, <h3> with seat label.
  • The seat.employee conditional showing "Currently assigned to" or "This seat is empty".
  • The red Unassign button (only rendered when the seat is occupied).
  • The <select> dropdown mapping availableEmployees, plus the blue Assign button.
  • The Cancel button at the bottom.
  • Closing </div> tags and the function's closing brace.
Connect the modal to the seating grid

The modal component exists but nothing renders it yet. You need to track which seat was clicked, calculate which employees are available, and handle the assignment logic in page.tsx.

  • In src/app/page.tsx, find these two import lines at the top of the file:
import { initialSeats, Seat } from "./data";
import SeatingGrid from "./components/SeatingGrid";
  • Replace them with these three lines to add allEmployees and the AssignModal import:
import { initialSeats, allEmployees, Seat } from "./data";
import SeatingGrid from "./components/SeatingGrid";
import AssignModal from "./components/AssignModal";
  • Inside the Home function, find the seats state line. Add the following code directly below it:
  const [selectedSeat, setSelectedSeat] = useState<Seat | null>(null);

  // Build list of employees currently sitting in seats
  const assignedEmployees = seats
    .filter((s) => s.employee !== null)
    .map((s) => s.employee as string);

  // Only show unassigned employees in the dropdown
  const availableEmployees = allEmployees.filter(
    (emp) => !assignedEmployees.includes(emp)
  );

  // Handle assigning or unassigning an employee
  const handleAssign = (seatId: string, employee: string | null) => {
    setSeats((prev) =>
      prev.map((s) => {
        if (s.id === seatId) return { ...s, employee };
        if (employee && s.employee === employee) return { ...s, employee: null };
        return s;
      })
    );
  };

What does this code do?

The selectedSeat state holds whichever seat the admin just clicked. When it is null, no modal appears.

The availableEmployees list is derived by filtering allEmployees against whoever is already seated. This prevents assigning the same person twice.

The handleAssign function updates two seats at once: it places the employee in the target seat and removes them from their old seat if they had one. Passing null as the employee clears the seat.

  • Now replace the entire return block in the Home function with this updated version:
  return (
    <main className="min-h-screen bg-gray-50 p-8">
      <h1 className="text-2xl font-bold text-gray-900 mb-6">
        Office Seating Manager
      </h1>
      <div className="bg-white rounded-xl shadow-sm p-6">
        <h2 className="text-lg font-semibold mb-4">Floor Plan</h2>
        {/* Pass setSelectedSeat so clicking a seat opens the modal */}
        <SeatingGrid seats={seats} onSeatClick={setSelectedSeat} />
      </div>
      {/* Render modal only when a seat is selected */}
      {selectedSeat && (
        <AssignModal
          seat={selectedSeat}
          availableEmployees={availableEmployees}
          onAssign={handleAssign}
          onClose={() => setSelectedSeat(null)}
        />
      )}
    </main>
  );

What changed in the return block?

Two things changed. First, onSeatClick now passes setSelectedSeat instead of an empty function. Every seat click stores the clicked seat in state.

Second, the AssignModal renders conditionally below the grid. When selectedSeat is not null, the modal appears. Clicking Cancel or completing an assignment sets selectedSeat back to null, which hides the modal.

Before you check the browser, predict: what do you think will happen when you click on a seat that currently shows "Empty"?

  • Save page.tsx.
  • Switch to your browser at http://localhost:3000.
  • Click on an empty seat like A3. A white modal should pop up showing "Seat A3" and "This seat is empty" with a dropdown of unassigned employees.
  • Select an employee from the dropdown and click Assign. The modal closes and the grid updates with the employee in their new seat.
  • Click on an occupied seat like A1 (Alice). The modal should show "Currently assigned to Alice" with a red Unassign button. Click it to remove Alice from the seat.

Modal not appearing when you click a seat?

  • Make sure you replaced onSeatClick={() => {}} with onSeatClick={setSelectedSeat} in the return block of page.tsx. The empty arrow function silently swallows clicks.
  • Check that the AssignModal import path is "./components/AssignModal" (capital A, capital M). File name casing must match exactly.
  • Confirm you saved both AssignModal.tsx and page.tsx. Unsaved changes in either file will prevent the modal from rendering.

Help me debug my assignment modal.

✔️ Awesome, I've got everything!

Great. Double check you have saved page.tsx before moving on.

ⓧ I'd like to double check the full code

Your page.tsx file is longer than 30 lines, so confirm each section is present in this order:

  • The "use client" directive at the top.
  • Imports for useState, initialSeats, allEmployees, Seat, SeatingGrid, and AssignModal.
  • The seats state and selectedSeat state declarations.
  • The assignedEmployees and availableEmployees derived values.
  • The handleAssign function using setSeats with prev.map.
  • In the return: SeatingGrid with onSeatClick={setSelectedSeat}.
  • The conditional AssignModal render with selectedSeat, availableEmployees, handleAssign, and the onClose arrow function.

Manual assignment works. You can click any seat and reassign employees one at a time. Now imagine doing this for 50 people after an office reshuffle. In the next step, you will build an AI assistant that handles seat changes through plain English commands like "Swap Alice and Bob."

Build the AI Assistant Admin Panel

In the last step, you built manual seat assignment with click-to-assign modals. It works for a quick swap, but imagine an office admin needing to reorganize 50 people across 50 seats. Clicking through each one is painful.

In this step, you'll build an AI assistant that understands natural language commands like "Move Alice to seat B2" and applies them to the grid instantly. You'll create a server-side API route that sends prompts to Google Gemini, plus a chat panel where admins type their seat-change commands.

In this step, get ready to:
  • Create a Next.js API route that sends seat-change prompts to Google Gemini and returns structured JSON.
  • Build an AI chat panel component with a text input, message history, and loading state.
  • Wire both into a two-column page layout so AI changes update the seating grid in real time.
Create the API route

Next.js App Router lets you build server-side endpoints by creating route.ts files inside the app/api/ directory. The folder path becomes the URL, so app/api/ai-assign/route.ts maps to /api/ai-assign.

  • In the VS Code Explorer sidebar, right-click the src/app folder and select New Folder.
  • Name the folder api.
  • Right-click the new api folder and create another folder called ai-assign.
  • Right-click the ai-assign folder and select New File. Name it route.ts.
  • Set up the imports, the Gemini AI client, and the system prompt by pasting this code into route.ts:
import { GoogleGenAI } from "@google/genai";
import { NextRequest, NextResponse } from "next/server";
import { Seat } from "../../data";

// Initialize the Gemini AI client using the API key from .env.local
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

export async function POST(request: NextRequest) {
  try {
    // Extract the user's prompt and current seat data from the request body
    const { prompt, seats }: { prompt: string; seats: Seat[] } =
      await request.json();

    // Build a system prompt that tells Gemini how to interpret seat commands
    const systemPrompt = `You are an office seating manager AI. You receive a natural language command about changing employee seat assignments, along with the current seating data.

Current seating data:
${JSON.stringify(seats, null, 2)}

Your job:
1. Parse the user's command to understand what seat changes they want.
2. Return a JSON response with this exact structure:
{
  "seats": [the full updated seats array with changes applied],
  "message": "A brief confirmation of what you changed"
}

Rules:
- Only return valid JSON, no markdown, no code fences.
- If an employee is moved to a new seat, remove them from their old seat (set employee to null).
- If the user asks to swap two employees, exchange their seats.
- If the command is unclear or references employees/seats that do not exist, return the original seats unchanged and explain in the message field.
- Preserve all seat objects in the array even if unchanged.`;

What does this code do?

GoogleGenAI initializes a connection to Google's Gemini API using the key you saved in .env.local. The POST function runs whenever your frontend sends a request to /api/ai-assign.

The systemPrompt is the key to making the AI work. It injects the current seat data into the prompt and gives Gemini strict rules: return valid JSON, handle moves and swaps, and preserve all seats in the array even if unchanged.

  • Directly below the system prompt, add the code that sends the prompt to Gemini, parses the response, and handles errors:
    // Send the system prompt and the user's command to Gemini
    const response = await ai.models.generateContent({
      model: "gemini-2.5-flash",
      contents: `${systemPrompt}\n\nUser command: ${prompt}`,
    });

    const text = response.text ?? "";

    // Strip any markdown code fences Gemini might add around the JSON
    const cleaned = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
    const parsed = JSON.parse(cleaned);

    // Return the updated seats and a confirmation message
    return NextResponse.json({
      seats: parsed.seats,
      message: parsed.message,
    });
  } catch (error) {
    console.error("AI assignment error:", error);
    return NextResponse.json(
      { error: "Failed to process AI command. Please try again." },
      { status: 500 }
    );
  }
}

What does this code do?

ai.models.generateContent sends the combined system prompt and user command to the gemini-2.5-flash model. Gemini returns a text response containing JSON with the updated seats array.

The cleaned line strips any markdown code fences Gemini might wrap around the JSON. The catch block returns a user-friendly error if anything goes wrong.

  • Save route.ts.
  • Switch to the terminal where your dev server is running. You should see no error messages in the output, which means Next.js compiled the new route successfully.

Seeing a compilation error?

  • Make sure the file is at src/app/api/ai-assign/route.ts. The folder structure must be api then ai-assign then route.ts.
  • Check that the import path for Seat is ../../data (two levels up from the ai-assign folder).
  • Confirm that @google/genai is installed. If not, run npm install @google/genai in a separate terminal.

Why isn't my API route compiling in Next.js?

Build the AI chat panel component

The AI panel gives admins a text input to type natural language commands and displays a chat-style history of prompts and responses. It sends each command to the API route you just created.

  • In the Explorer sidebar, right-click the src/app/components folder and select New File. Name it AiPanel.tsx.
  • Start the component by pasting the imports, interface, state declarations, and the beginning of the submit handler:
"use client";

import { useState } from "react";
import { Seat } from "../data";

// Define the props this component needs from its parent
interface AiPanelProps {
  seats: Seat[];
  onSeatsUpdate: (seats: Seat[]) => void;
}

export default function AiPanel({ seats, onSeatsUpdate }: AiPanelProps) {
  const [prompt, setPrompt] = useState("");
  const [loading, setLoading] = useState(false);
  const [messages, setMessages] = useState<
    { role: "user" | "ai"; text: string }[]
  >([]);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!prompt.trim() || loading) return;

    // Save the message, clear the input, and show the loading state
    const userMessage = prompt.trim();
    setMessages((prev) => [...prev, { role: "user", text: userMessage }]);
    setPrompt("");
    setLoading(true);

What does this code do?

The component tracks three pieces of state: the current text in the input (prompt), whether a request is in progress (loading), and the full chat history (messages).

onSeatsUpdate is a callback passed from the parent page. When the AI returns updated seats, this callback pushes them into the shared state so the grid re-renders instantly.

  • Directly below the code you just pasted, add the API call logic that sends the prompt to your route and handles the response:
    try {
      // Send the user's prompt and current seats to the API route
      const res = await fetch("/api/ai-assign", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ prompt: userMessage, seats }),
      });

      const data = await res.json();

      if (data.error) {
        setMessages((prev) => [
          ...prev,
          { role: "ai", text: `Error: ${data.error}` },
        ]);
      } else {
        // Update the parent's seats state and show the AI's message
        onSeatsUpdate(data.seats);
        setMessages((prev) => [
          ...prev,
          { role: "ai", text: data.message },
        ]);
      }
    } catch {
      setMessages((prev) => [
        ...prev,
        { role: "ai", text: "Something went wrong. Please try again." },
      ]);
    } finally {
      setLoading(false);
    }
  };

What does this code do?

The fetch call posts the user's command and the current seats array to /api/ai-assign. If the API returns an error, it displays the error in the chat. On success, it calls onSeatsUpdate with the new seats so the grid updates, and adds the AI's confirmation message to the chat history.

  • Below the submit handler, add the return statement with the message display area:
  return (
    <div className="flex flex-col h-full">
      <h2 className="text-lg font-semibold mb-3">AI Seating Assistant</h2>

      {/* Scrollable message area showing the chat history */}
      <div className="flex-1 overflow-y-auto space-y-3 mb-4 min-h-0">
        {messages.length === 0 && (
          <p className="text-sm text-gray-400 italic">
            Try: &quot;Move Alice to seat B3&quot; or &quot;Swap Bob and
            Charlie&quot;
          </p>
        )}
        {messages.map((msg, i) => (
          <div
            key={i}
            className={`p-3 rounded-lg text-sm ${
              msg.role === "user"
                ? "bg-blue-100 text-blue-900 ml-8"
                : "bg-gray-100 text-gray-800 mr-8"
            }`}
          >
            {msg.text}
          </div>
        ))}
        {loading && (
          <div className="bg-gray-100 text-gray-500 p-3 rounded-lg text-sm mr-8 animate-pulse">
            Thinking...
          </div>
        )}
      </div>

What does this code do?

When the chat is empty, a hint message suggests example commands. Each message renders as a styled bubble. User messages align right with a blue background. AI messages align left with a gray background. A pulsing "Thinking..." indicator appears while waiting for Gemini's response.

  • Directly below the messages </div> you just pasted, add the input form and close the component:
      {/* Text input and send button */}
      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          type="text"
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder="e.g. Move Alice to B3"
          className="flex-1 p-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
          disabled={loading}
        />
        <button
          type="submit"
          disabled={loading || !prompt.trim()}
          className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
        >
          Send
        </button>
      </form>
    </div>
  );
}

What does this code do?

The form captures the Enter key or button click and calls handleSubmit. The input and button are both disabled while a request is in progress so the admin can't fire multiple prompts at once.

  • Save AiPanel.tsx.
  • Check the terminal for no compilation errors. The component compiles on its own, but it isn't visible in the browser yet because nothing imports it.

Seeing a TypeScript error?

  • Make sure all four code blocks are pasted in order with no gaps or duplicate lines between them.
  • Check that the try block opens inside handleSubmit (after setLoading(true);) and the return statement comes after the closing }; of handleSubmit.

Help me fix a TypeScript error in my AiPanel component.

Awesome, I've got everything!

Great. Double check you've saved AiPanel.tsx before moving on.

I'd like to double check the full code

The full AiPanel.tsx file is built from four code blocks. Scroll up and confirm each one is present in your file, in this order:

  • Block 1: Imports, interface, state declarations, and the start of handleSubmit (through setLoading(true);).
  • Block 2: The try/catch/finally block with the fetch call (through the closing }; of handleSubmit).
  • Block 3: The return statement with the message display area.
  • Block 4: The input form and closing brackets.
Add the AI panel to the page layout

Both the API route and the chat panel component are ready. Now you need to import the panel into page.tsx and restructure the layout into two columns so the seating grid and AI panel sit side by side.

  • In src/app/page.tsx, find the import lines at the top of the file:
import SeatingGrid from "./components/SeatingGrid";
import AssignModal from "./components/AssignModal";
  • Add the AiPanel import directly below the AssignModal import, so the three imports look like this:
import SeatingGrid from "./components/SeatingGrid";
import AssignModal from "./components/AssignModal";
import AiPanel from "./components/AiPanel";
  • Now find the seating grid section in the return statement. It currently looks like this:
      <div className="bg-white rounded-xl shadow-sm p-6">
        <h2 className="text-lg font-semibold mb-4">Floor Plan</h2>
        <SeatingGrid seats={seats} onSeatClick={setSelectedSeat} />
      </div>
  • Replace that entire block with a two-column grid layout that includes the AiPanel:
      {/* Two-column layout: seating grid on the left, AI panel on the right */}
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
        <div className="lg:col-span-2">
          <div className="bg-white rounded-xl shadow-sm p-6">
            <h2 className="text-lg font-semibold mb-4">Floor Plan</h2>
            <SeatingGrid seats={seats} onSeatClick={setSelectedSeat} />
          </div>
        </div>

        <div className="bg-white rounded-xl shadow-sm p-6 h-[500px] flex flex-col">
          <AiPanel seats={seats} onSeatsUpdate={setSeats} />
        </div>
      </div>

What's happening here?

lg:grid-cols-3 creates a 3-column grid on large screens. The seating grid takes 2 of those columns (lg:col-span-2) and the AI panel takes the remaining 1. On smaller screens it stacks vertically.

Both components share the same seats state. When the AI panel calls onSeatsUpdate (which is wired to setSeats), React re-renders the grid with the new assignments.

  • Save page.tsx.
  • Switch to your browser at http://localhost:3000. You should see the seating grid on the left and the AI Seating Assistant panel on the right.

Before you test the AI, what do you think will happen when you type "Move Alice to seat B2" and press Send? Will Alice move, or will something go wrong?

  • Type Move Alice to seat B2 in the AI chat input and click Send.

You should see "Thinking..." appear briefly, then the AI responds with a confirmation message. The seating grid updates to show Alice in seat B2. Her old seat (A1) is now empty.

  • Try another command: type Swap Bob and Charlie and click Send. Bob and Charlie should exchange seats on the grid.

AI panel not responding?

  • Open your browser's developer console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows). Check for any error messages there.
  • Verify your .env.local file has GEMINI_API_KEY set to a valid key. If you changed it after starting the dev server, you need to stop (Ctrl+C) and restart with npm run dev for environment variable changes to take effect.
  • Check the terminal running your dev server for error messages. A common one is an authentication error if the API key is incorrect or missing.

My AI seating assistant isn't working. Help me debug.

Awesome, I've got everything!

Great. Make sure you've saved all three files before moving on.

I'd like to double check the full code

Your page.tsx should have three component imports (SeatingGrid, AssignModal, AiPanel) and a grid grid-cols-1 lg:grid-cols-3 wrapper. Scroll up and confirm:

  • The AiPanel import is present below the AssignModal import.
  • The return statement wraps SeatingGrid in lg:col-span-2 and places AiPanel in a separate column with h-[500px].
  • AiPanel receives seats={seats} and onSeatsUpdate={setSeats} as props.

For route.ts, confirm both code blocks are in place: the imports and system prompt in the first block, and the Gemini API call with response parsing in the second.

Your AI-powered seating manager is working. You can reassign seats by clicking or by telling the AI what to do. Next up, you'll deploy the whole thing to Railway and push your code to GitHub so your submission is complete.

Deploy to Railway and Push to GitHub

Your seating manager is fully functional. The grid shows employees, manual assignment works via the modal, and the AI assistant parses natural language commands into real seat changes.

The assessment requires two deliverables beyond working code: a public GitHub repository and a live Railway URL. Without both, your submission is incomplete regardless of how good the code is.

In this step, get ready to:
  • Push all code to a public GitHub repository.
  • Deploy the app to Railway with the Gemini API key configured.
  • Verify the live deployment works end to end.
Push to GitHub

Your code lives only on your machine right now. You need it in a public GitHub repository so the assessment reviewers can see it and so Railway can pull it for deployment.

  • Stop the dev server in your terminal by pressing Ctrl+C.
  • Initialize a Git repository and commit all your code by running these commands:
git init
git add .
git commit -m "Employee Seating Management System"

You should see a summary of all the files committed.

What about the API key?

The default Next.js .gitignore file already excludes .env.local. Your Gemini API key will not be pushed to GitHub.

Now you need a GitHub repository to push to.

  • Open github.com in your browser and sign in.
  • Click the + icon in the top right corner, then select New repository.
  • Enter seating-manager as the repository name.
  • Set the visibility to Public.
  • Leave all other options unchecked (no README, no .gitignore, no license).
  • Click Create repository.

GitHub shows a quick setup page with commands. You already have a commit, so you only need to connect and push.

  • Connect your local repo to GitHub and push by running these commands:
git remote add origin [[REPO_URL="https://github.com/your-username/seating-manager.git"]]
git branch -M main
git push -u origin main

What do these commands do?

  • git remote add origin links your local repository to the GitHub repository you just created.
  • git branch -M main renames your default branch to main.
  • git push -u origin main uploads all your committed code to GitHub.
  • Refresh your GitHub repository page in the browser.

You should see all your project files listed, including src/app/page.tsx, src/app/components/, and src/app/api/. The .env.local file should NOT appear.

Push rejected or authentication failed?

  • If you see an authentication error, GitHub requires a personal access token instead of a password. Create one at GitHub token settings and use it as your password when prompted.
  • Make sure the repository name in the URL matches seating-manager and your GitHub username is correct.

Help me push my code to GitHub

Deploy on Railway

Railway is a cloud platform that can pull your code from GitHub, build it, and serve it at a public URL. The free trial gives you $5 in credits for 30 days with no credit card required.

  • Open railway.com in your browser.
  • Sign up for a new account using your GitHub login.

Why sign up with GitHub?

Signing up with your GitHub account automatically links the two services. This lets Railway pull code from your repositories without extra configuration.

Once signed in, you land on the Railway dashboard. Now create a project from your GitHub repo.

  • Click New Project on the dashboard.
  • Select Deploy from GitHub repo.
  • If Railway asks you to install the GitHub App, accept all permissions so it can access your repositories.
  • Search for and select your seating-manager repository.
  • Click Add Variables (not Deploy Now).

You chose Add Variables because the app needs your Gemini API key to work. Without it, the AI assistant would fail on every request.

  • In the Variables tab, click New Variable.
  • Enter GEMINI_API_KEY as the variable name.
  • Paste your Gemini API key (the same one from your .env.local file) as the value.
  • Click Deploy at the top of the canvas to start the deployment.

Railway auto-detects that your project is a Next.js app and starts building it. You can watch the build logs in real time on the Project Canvas. The build typically takes 1-3 minutes.

What is Railway doing?

Railway pulls your code from GitHub, runs npm install and npm run build automatically, and starts the production server. It injects your GEMINI_API_KEY environment variable so your API route can access it via process.env.GEMINI_API_KEY.

Once the deployment finishes successfully, your app is running but it does not have a public URL yet. Railway services are private by default.

  • Click on your service on the Project Canvas.
  • Open the Settings tab.
  • Scroll down to the Networking section.
  • Click Generate Domain.

Railway generates a public URL for your app. Copy this URL. You will need it for verification and your assessment submission.

Build failed on Railway?

  • Click View logs on the failed deployment to see the exact error message.
  • If you see a TypeScript error, make sure your GitHub repo has the latest code. Run git add ., git commit, and git push again. Railway will automatically redeploy when it detects a new push.
  • Check that the GEMINI_API_KEY variable is set correctly in the Variables tab.

Help me fix my Railway deployment

Verify the live deployment

Before you submit, you need to confirm the live app works exactly like your local version. Testing in an incognito tab proves the app runs independently of your local environment.

Before you open the URL, think about what you expect to see. Will the seating grid load with employees in their seats? Will the AI assistant respond to commands?

  • Open an incognito or private browser tab.
  • Paste your Railway-generated URL into the address bar and press Enter.

You should see the full Office Seating Manager app load. The seating grid displays employees in their assigned seats with empty seats clearly marked.

  • Click an empty seat to confirm the assignment modal opens.
  • Close the modal, then type a command in the AI assistant panel, such as "Move Alice to seat B3".
  • Press Send and confirm the grid updates with Alice in her new seat.

The AI assistant should respond with a confirmation message and the seating grid should reflect the change immediately.

AI assistant returning errors on Railway?

  • The most common cause is a missing or incorrect GEMINI_API_KEY in Railway's Variables tab. Double-check that you pasted the full key without any leading or trailing spaces.
  • If the seating grid loads but AI commands fail, click on your service in the Railway dashboard and check View logs for the exact error message.

Help me debug my deployed AI assistant

You now have a live, deployed seating management app with a public URL and a public GitHub repository. Both deliverables are ready for your assessment submission.

Secret mission

Add Employee Search and Seat Highlighting

Your seating app works, but imagine an office with 200 seats. An admin searching for one employee would have to scan every single seat card. In this secret mission, you will add a search bar that filters employees by name and highlights their seat on the grid, dimming everything else so the match jumps out instantly.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running or delete them entirely. This project uses the Gemini API free tier (no credit card required) and Railway's free trial ($5 in credits for 30 days, no credit card required).

No surprise charges

The Gemini API free tier has no charges at all. Railway's free trial provides $5 in credits. After the trial, the free plan gives $1 per month in credits. If your usage ever exceeds that, Railway automatically shuts down your deployments. You will not be charged.

Resources you used:

  • Railway deployment with a public domain.
  • Gemini API key stored in .env.local and in Railway's environment variables.
  • GitHub seating-manager repository.
  • Local seating-manager project folder on your Desktop.

Keep everything running

No action needed. Choose this if you are still waiting for your assessment to be reviewed.

  • Your Railway deployment stays live. The free trial credits are more than enough for a low-traffic demo app.
  • Your Gemini API key continues to work within the free tier rate limits.
  • Your GitHub repository remains public and accessible for evaluators.

Pause - I'll come back to this later

Free up local resources while keeping your cloud services intact for later.

  • Stop the local dev server by pressing Ctrl+C in the terminal where it is running.
  • Your Railway deployment stays live automatically. It will shut itself down if it ever exceeds the free credit allowance.
  • Your Gemini API key and GitHub repository remain as they are.

Delete - I don't want to use this again

Remove all project resources and start fresh.

Delete the Railway project:

  • Open the Railway dashboard and select your seating-manager project.
  • Click Settings at the top right of the project canvas.
  • Click the Danger tab.
  • Click Delete Project and confirm the deletion.

Revoke your Gemini API key:

Delete the GitHub repository:

  • Go to your seating-manager repository on GitHub.
  • Click Settings (under the repository name).
  • Scroll down to the Danger Zone section and click Delete this repository.
  • Confirm by typing the repository name when prompted, then click Delete this repository.

Delete your local project files:

macOS

  • Delete the local project folder by running:
rm -rf ~/Desktop/seating-manager

Windows

  • Delete the local project folder by running:
rmdir /s /q %USERPROFILE%\Desktop\seating-manager

Nice Work!

Nice Work!

You just built and deployed a full-stack employee seating management system powered by AI. From an empty scaffold to a live URL, you now have a working app that proves you can ship a Next.js project with AI integration end to end.

You've learned how to:

  • Build a full-stack Next.js application with React components, API routes, and an interactive seating grid UI with click-to-assign modals.
  • Integrate Google Gemini AI to parse natural language commands into structured seat changes that update the grid in real time.
  • Deploy a live application to Railway with a public URL and push the complete codebase to a public GitHub repository.
  • Secret Mission: Added a search and filter bar to highlight matching employee seats on the grid.

Ready to quiz yourself?