03-projects
no-bad-parts-collective
resources
Front End Build Plan

Front End Build Plan

STEP 4 – FRONT-END (detailed, copy-paste friendly)

Assumes you’re now on the video-poc branch inside the private no-bad-parts-landing repo.


4.1 Install LiveKit deps & restart dev server

# still inside repo root
npm i @livekit/components-react @livekit/client
npm run dev          # or `pnpm dev` / `yarn dev`

Open http://localhost:3000 – existing landing page should still load.


4.2 Add a route group for all session pages

Create a new folder:

app/(session)/

(The parentheses tell Next.js to group routes without affecting URL paths.)


4.3 Shared LiveKit wrapper

Create app/livekit-wrapper.tsx

// simple provider for any child route
"use client";
import { LiveKitRoom } from "@livekit/components-react";
 
export default function LKWrapper({
  token,
  children,
}: {
  token: string;
  children: React.ReactNode;
}) {
  return (
    <LiveKitRoom
      token={token}
      serverUrl={process.env.NEXT_PUBLIC_LK_URL}
      connectOptions={{ autoSubscribe: true }}
      style={{ height: "100%" }}
    >
      {children}
    </LiveKitRoom>
  );
}

4.4 /partner page (self-view only)

app/(session)/partner/page.tsx

"use client";
import LKWrapper from "@/livekit-wrapper";
import { ParticipantTile } from "@livekit/components-react";
 
export default function Partner() {
  return (
    <LKWrapper token={process.env.NEXT_PUBLIC_PARTNER_TOKEN!}>
      <div className="flex h-screen items-center justify-center bg-black">
        <ParticipantTile isLocal />
      </div>
    </LKWrapper>
  );
}

Check: visit /partner → you should see your webcam tile.


4.5 /facilitator page (video + hint stream)

app/(session)/facilitator/page.tsx

"use client";
import LKWrapper from "@/livekit-wrapper";
import {
  ParticipantTile,
  useRoomContext,
} from "@livekit/components-react";
import { useEffect, useState } from "react";
 
function HintStream() {
  const { room } = useRoomContext();
  const [lines, setLines] = useState<string[]>([]);
 
  useEffect(() => {
    const handle = room.on("dataReceived", ({ payload, participant }) => {
      if (participant?.identity?.startsWith("agent")) {
        setLines((l) => [...l, new TextDecoder().decode(payload)]);
      }
    });
    return () => {
      room.off("dataReceived", handle);
    };
  }, [room]);
 
  return (
    <pre className="h-screen overflow-auto bg-zinc-900 p-4 text-sm text-zinc-100">
      {lines.join("\n")}
    </pre>
  );
}
 
export default function Facilitator() {
  return (
    <LKWrapper token={process.env.NEXT_PUBLIC_FACILITATOR_TOKEN!}>
      <div className="grid h-screen grid-cols-[2fr_1fr] bg-black">
        <ParticipantTile isLocal />
        <HintStream />
      </div>
    </LKWrapper>
  );
}

Check: open /facilitator – you’ll see your video left, blank panel right (hints appear once the Agent is running).


4.6 Temporary placeholder feed (for Day-1 demo)

If you need scrolling text before wiring the real Agent:

# tiny Node script (save as dummy-hints.js)
const WebSocket = require("ws");
const ws = new WebSocket(process.env.NEXT_PUBLIC_LK_URL, {
  headers: { Authorization: process.env.NEXT_PUBLIC_FACILITATOR_TOKEN },
});
ws.on("open", () => {
  let i = 1;
  setInterval(() => {
    const msg = `Hint ${i++}: breathe and stay curious.`;
    ws.send(msg); // LiveKit server will broadcast to same room
  }, 5000);
});

node dummy-hints.js in another terminal → text appears every 5 s in the panel.

(Comment out once the real Agent is connected.)


4.7 Failure toast (optional polish)

Add to HintStream top:

if (!room.connected) {
  return <div className="p-4 text-red-500">AI offline — continue as normal.</div>;
}

4.8 Commit & push

git add .
git commit -m "feat: partner & facilitator routes with LiveKit"
git push origin video-poc

Vercel will auto-deploy a preview URL.


You’re ready for Step 5 – Fly.io Agent deploy.

Open /partner and /facilitator on the preview link, start the local Agent, and confirm hints flow. Then move the Agent to Fly so anyone can test without your laptop.