From eadefbf7b03e3c72ed95b806a8dbf0e2295e3568 Mon Sep 17 00:00:00 2001 From: Zeeshan Adil Date: Fri, 6 Nov 2026 18:10:10 +0500 Subject: [PATCH 1/3] Labs: one-click filters by kind and topic One grid of labs under two rows of chips: kind (Visual, Guided, Shell) and topic (System Design challenges and ideas, Cloud and DevOps, Networking). Guided and Shell labs show as Soon and open a short card for what is coming. The chips are links with a query parameter, so a filtered view can be shared. /labs/visual redirects to the visual filter. --- .../(learn)/(catalog)/labs/guided/page.tsx | 2 +- .../src/app/(learn)/(catalog)/labs/page.tsx | 178 +++++++++--------- .../app/(learn)/(catalog)/labs/shell/page.tsx | 2 +- .../(learn)/(catalog)/labs/visual/page.tsx | 42 +---- .../src/components/areas/filter-chips.tsx | 74 ++++++++ frontend/src/lib/learn-areas.ts | 4 +- 6 files changed, 171 insertions(+), 131 deletions(-) create mode 100644 frontend/src/components/areas/filter-chips.tsx diff --git a/frontend/src/app/(learn)/(catalog)/labs/guided/page.tsx b/frontend/src/app/(learn)/(catalog)/labs/guided/page.tsx index e640d425..9e30b335 100644 --- a/frontend/src/app/(learn)/(catalog)/labs/guided/page.tsx +++ b/frontend/src/app/(learn)/(catalog)/labs/guided/page.tsx @@ -49,7 +49,7 @@ export default function GuidedLabsPage() { - + They run on the same machines as shell labs, so they arrive with the desktop app. Until then, the visual labs check your designs the same way. diff --git a/frontend/src/app/(learn)/(catalog)/labs/page.tsx b/frontend/src/app/(learn)/(catalog)/labs/page.tsx index ab869a10..61cfc579 100644 --- a/frontend/src/app/(learn)/(catalog)/labs/page.tsx +++ b/frontend/src/app/(learn)/(catalog)/labs/page.tsx @@ -1,24 +1,34 @@ import type { Metadata } from "next"; -import Link from "next/link"; import { SITE_SHARE_IMAGE } from "@/lib/site"; import { LAB_KINDS, areaById } from "@/lib/learn-areas"; import { visualLabGroups } from "@/lib/visual-labs"; import { LAB_KIND_ICON } from "@/components/areas/area-icon"; -import { AreaHero, AreaPage, AreaSection, CardGrid, InfoCard, StepList } from "@/components/areas/area-page"; -import { buttonVariants } from "@/components/ui/button"; +import { AreaHero, AreaPage, CardGrid, InfoCard, StepList } from "@/components/areas/area-page"; +import { FilterChips, pickFilter } from "@/components/areas/filter-chips"; export const metadata: Metadata = { title: "Labs", - description: "Build it, break it, and see the result. Visual labs are open now; guided and shell labs are on the way.", + description: "Build it, break it, and see the result. Filter by kind of lab and by topic: System Design, Cloud and DevOps, Networking.", alternates: { canonical: "/labs" }, openGraph: { title: "Labs on Edorax", url: "/labs", images: [SITE_SHARE_IMAGE] }, }; -export default function LabsPage() { +type SearchParams = Promise>; + +export default async function LabsPage({ searchParams }: { searchParams: SearchParams }) { const area = areaById("labs"); const groups = visualLabGroups(); - const challenges = groups.find((group) => group.id === "system-design-challenges")?.labs ?? []; - const cloud = groups.find((group) => group.id === "cloud-devops")?.labs ?? []; + const query = await searchParams; + const kind = pickFilter(query.kind, LAB_KINDS.map((k) => k.id)); + const topic = kind === "guided" || kind === "shell" ? null : pickFilter(query.topic, groups.map((g) => g.id)); + const params = { kind, topic }; + const total = groups.reduce((sum, group) => sum + group.labs.length, 0); + + const labs = groups + .filter((group) => topic === null || group.id === topic) + .flatMap((group) => group.labs.map((lab) => ({ ...lab, topic: group.title }))); + const soonKinds = LAB_KINDS.filter((k) => k.status !== "live" && (kind === null ? topic === null : k.id === kind)); + return ( - - Open the visual labs - - - Desktop app docs - - + intro="Reading tells you how a system should behave. A lab lets you find out. Pick a kind and a topic." + /> + +
+ ({ value: k.id, label: k.name.replace(/ labs$/, ""), count: k.status === "live" ? total : undefined, soon: k.status !== "live" })), + ]} + /> + {kind === null || kind === "visual" ? ( + ({ value: g.id, label: g.title, count: g.labs.length }))]} + /> + ) : null} +
- - - {LAB_KINDS.map((kind) => { - const Icon = LAB_KIND_ICON[kind.id]; - return ( + {kind === null || kind === "visual" ? ( +
+ + {labs.map((lab) => ( } - cta={kind.status === "live" ? "Open visual labs" : "See how it will work"} + key={lab.slug} + title={lab.title} + body={lab.purpose} + href={`/labs/visual/${lab.slug}`} + cta="Open lab" + meta={lab.challenge ? `${lab.topic} · ${lab.checks} checks` : lab.topic} /> - ); - })} - - + ))} + +
+ ) : null} - - - {challenges.map((lab) => ( - 0 ? `${lab.checks} checks` : undefined} - /> - ))} - -

- - See every visual lab, including networking and cloud - -

-
+ {soonKinds.length > 0 ? ( +
+

+ {kind ? "Not open yet" : "Coming next"} +

+
+ + {soonKinds.map((k) => { + const Icon = LAB_KIND_ICON[k.id]; + return ( + } cta="See how it will work" /> + ); + })} + +
+
+ ) : null} - - - {cloud.map((lab) => ( - + + How a visual lab works + +
+ - ))} - -

- - Cloud and DevOps courses - -

- - - - - +
+ + ) : null}
); } diff --git a/frontend/src/app/(learn)/(catalog)/labs/shell/page.tsx b/frontend/src/app/(learn)/(catalog)/labs/shell/page.tsx index 6948ee0f..8cfdcf65 100644 --- a/frontend/src/app/(learn)/(catalog)/labs/shell/page.tsx +++ b/frontend/src/app/(learn)/(catalog)/labs/shell/page.tsx @@ -68,7 +68,7 @@ export default function ShellLabsPage() {

- + Visual labs are open today in the browser, with no install. diff --git a/frontend/src/app/(learn)/(catalog)/labs/visual/page.tsx b/frontend/src/app/(learn)/(catalog)/labs/visual/page.tsx index ecc4e618..39f60990 100644 --- a/frontend/src/app/(learn)/(catalog)/labs/visual/page.tsx +++ b/frontend/src/app/(learn)/(catalog)/labs/visual/page.tsx @@ -1,42 +1,6 @@ -import type { Metadata } from "next"; -import { SITE_SHARE_IMAGE } from "@/lib/site"; -import { visualLabGroups } from "@/lib/visual-labs"; -import { AreaHero, AreaPage, AreaSection, CardGrid, InfoCard } from "@/components/areas/area-page"; - -export const metadata: Metadata = { - title: "Visual labs", - description: "No code. Move parts, press Run, and watch every test case pass or fail on the board.", - alternates: { canonical: "/labs/visual" }, - openGraph: { title: "Visual labs on Edorax", url: "/labs/visual", images: [SITE_SHARE_IMAGE] }, -}; +import { redirect } from "next/navigation"; +// Visual labs are a filter on /labs now. Old links (and group anchors such as #networking) land on it. export default function VisualLabsPage() { - const groups = visualLabGroups(); - return ( - - - {groups.map((group) => ( - - - {group.labs.map((lab) => ( - - ))} - - - ))} - - ); + redirect("/labs?kind=visual"); } diff --git a/frontend/src/components/areas/filter-chips.tsx b/frontend/src/components/areas/filter-chips.tsx new file mode 100644 index 00000000..a8dc9240 --- /dev/null +++ b/frontend/src/components/areas/filter-chips.tsx @@ -0,0 +1,74 @@ +import Link from "next/link"; +import { cn } from "@/lib/utils"; + +export type FilterOption = { value: string | null; label: string; count?: number; soon?: boolean }; + +/** + * A row of one-tap filters. Each chip is a plain link that sets one search parameter and keeps the + * others, so a filtered view can be shared, and it works before the page's scripts load. + */ +export function FilterChips({ + label, + param, + options, + current, + basePath, + params, +}: { + /** Read out to screen readers and shown on wide screens before the chips. */ + label: string; + param: string; + options: FilterOption[]; + current: string | null; + basePath: string; + /** The page's other filters, kept when this one changes. */ + params: Record; +}) { + function href(value: string | null) { + const query = new URLSearchParams(); + for (const [key, val] of Object.entries({ ...params, [param]: value })) if (val) query.set(key, val); + const text = query.toString(); + return text ? `${basePath}?${text}` : basePath; + } + + return ( + + ); +} + +/** Reads one filter from search params, accepting only the listed values. */ +export function pickFilter(value: string | string[] | undefined, allowed: readonly string[]): string | null { + const one = Array.isArray(value) ? value[0] : value; + return one && allowed.includes(one) ? one : null; +} diff --git a/frontend/src/lib/learn-areas.ts b/frontend/src/lib/learn-areas.ts index 84987176..d6803484 100644 --- a/frontend/src/lib/learn-areas.ts +++ b/frontend/src/lib/learn-areas.ts @@ -124,7 +124,7 @@ export const LAB_KINDS: readonly LabKind[] = [ { id: "visual", name: "Visual labs", - href: "/labs/visual", + href: "/labs?kind=visual", status: "live", tagline: "No code. Move parts, press Run, watch the checks.", summary: @@ -150,7 +150,7 @@ export const LAB_KINDS: readonly LabKind[] = [ }, ]; -/** Visual labs grouped for the catalogue. Slugs come from the lab registry; `id` is the anchor on /labs/visual. */ +/** Visual labs grouped for the catalogue. Slugs come from the lab registry; `id` is the topic filter on /labs (?topic=). */ export const VISUAL_LAB_GROUPS: readonly { id: string; title: string; description: string; prefix: (slug: string) => boolean }[] = [ { id: "system-design-challenges", From 88ec03113949765bacec582717e1ab81a0bc7728 Mon Sep 17 00:00:00 2001 From: Zeeshan Adil Date: Fri, 6 Nov 2026 18:10:10 +0500 Subject: [PATCH 2/3] Practice, Puzzles and Tutors: one chip row and one panel each Each page had four or five stacked sections. Now a subject, puzzle kind or tutor topic is one tap, and it shows one preview next to a short list of how it works. The longer walkthroughs fold away. --- .../app/(learn)/(catalog)/practice/page.tsx | 123 +++++++++++++----- .../app/(learn)/(catalog)/puzzles/page.tsx | 90 +++++++++---- .../src/app/(learn)/(catalog)/voice/page.tsx | 118 ++++++++++++----- 3 files changed, 245 insertions(+), 86 deletions(-) diff --git a/frontend/src/app/(learn)/(catalog)/practice/page.tsx b/frontend/src/app/(learn)/(catalog)/practice/page.tsx index 5ad790b5..e50c20a8 100644 --- a/frontend/src/app/(learn)/(catalog)/practice/page.tsx +++ b/frontend/src/app/(learn)/(catalog)/practice/page.tsx @@ -1,18 +1,63 @@ import type { Metadata } from "next"; -import { BookOpen, MessageCircleQuestion, Repeat, Timer } from "lucide-react"; import { SITE_SHARE_IMAGE } from "@/lib/site"; import { areaById } from "@/lib/learn-areas"; -import { AreaHero, AreaPage, AreaSection, CardGrid, ComingSoonPanel, InfoCard, StepList } from "@/components/areas/area-page"; +import { AreaHero, AreaPage, ComingSoonPanel, StepList } from "@/components/areas/area-page"; +import { FilterChips, pickFilter } from "@/components/areas/filter-chips"; export const metadata: Metadata = { title: "Five-minute practice", - description: "Master one topic five minutes a day: today's question, quick quizzes, quick reads and a review queue.", + description: "Master one subject five minutes a day: today's question, quick quizzes, quick reads and a review queue.", alternates: { canonical: "/practice" }, openGraph: { title: "Five-minute practice on Edorax", url: "/practice", images: [SITE_SHARE_IMAGE] }, }; -export default function PracticePage() { +// One practice app per subject. System Design is built first; the rest follow the courses they practise. +const SUBJECTS = [ + { + id: "system-design", + label: "System Design", + note: "Built first.", + question: "A page loads fast in the morning and slowly at night. Which part do you check first?", + options: ["The database", "The cache hit rate", "The load balancer", "I am not sure yet"], + }, + { + id: "ai-engineering", + label: "AI engineering interview", + note: "After System Design.", + question: "Your RAG answer cites the right document but quotes the wrong number. Where do you look first?", + options: ["How the document was chunked", "The retrieval ranking", "The prompt", "I am not sure yet"], + }, + { + id: "networking", + label: "Networking", + note: "Planned.", + question: "A site opens when you type its IP address but not its name. What is broken?", + options: ["DNS", "The router", "The TLS certificate", "I am not sure yet"], + }, + { + id: "databases", + label: "Databases", + note: "Planned.", + question: "Two people book the last seat at the same moment. What stops both of them getting it?", + options: ["A row lock in a transaction", "A cache", "An index", "I am not sure yet"], + }, +] as const; + +const FORMATS = [ + ["Today's question", "One a day. Answer first, read after."], + ["Quick quizzes", "Three to five questions in about two minutes."], + ["Quick reads", "One idea on one screen, ending with a check."], + ["Review queue", "Misses come back a few days later."], +] as const; + +type SearchParams = Promise>; + +export default async function PracticePage({ searchParams }: { searchParams: SearchParams }) { const area = areaById("practice"); + const query = await searchParams; + const id = pickFilter(query.subject, SUBJECTS.map((s) => s.id)) ?? SUBJECTS[0].id; + const subject = SUBJECTS.find((s) => s.id === id)!; + return ( - - - } title="Today's question" body="One question a day for your topic. Answer it first; read about it after." /> - } title="Quick quizzes" body="Three to five questions in about two minutes. No timer, no penalty for a wrong answer." /> - } title="Quick reads" body="One idea on one screen, with a diagram where it helps. Each ends with a question to check it stuck." /> - } title="Review queue" body="What you got wrong comes back a few days later, then less often as you get it right." /> - - - - - + ({ value: s.id, label: s.label }))} /> - + - -
-
System Design · Day 12
-

A page loads fast in the morning and slowly at night. Which part do you check first?

+
+
+
+ {subject.label} · today's question + {subject.note} Preview, not interactive yet. +
+

{subject.question}

    - {["The database", "The cache hit rate", "The load balancer", "I am not sure yet"].map((option) => ( + {subject.options.map((option) => (
  • {option}
  • ))}
- +
+ {FORMATS.map(([title, body]) => ( +
+
{title}
+
{body}
+
+ ))} +
+
+ +
+ + A session, start to finish + +
+ +
+
- System Design comes first. Until then, every course chapter ends with a short quiz you can retake. + Until then, every course chapter ends with a short quiz you can retake. ); diff --git a/frontend/src/app/(learn)/(catalog)/puzzles/page.tsx b/frontend/src/app/(learn)/(catalog)/puzzles/page.tsx index c66991ae..ce9f4025 100644 --- a/frontend/src/app/(learn)/(catalog)/puzzles/page.tsx +++ b/frontend/src/app/(learn)/(catalog)/puzzles/page.tsx @@ -1,8 +1,8 @@ import type { Metadata } from "next"; -import { CalendarDays, Gauge, Route, Trophy } from "lucide-react"; import { SITE_SHARE_IMAGE } from "@/lib/site"; import { areaById } from "@/lib/learn-areas"; -import { AreaHero, AreaPage, AreaSection, CardGrid, ComingSoonPanel, InfoCard } from "@/components/areas/area-page"; +import { AreaHero, AreaPage, ComingSoonPanel } from "@/components/areas/area-page"; +import { FilterChips, pickFilter } from "@/components/areas/filter-chips"; export const metadata: Metadata = { title: "Puzzles", @@ -11,8 +11,48 @@ export const metadata: Metadata = { openGraph: { title: "Daily puzzles on Edorax", url: "/puzzles", images: [SITE_SHARE_IMAGE] }, }; -export default function PuzzlesPage() { +// The kinds take turns through the week. Each has a one-line goal and an example of the day's prompt. +const KINDS = [ + { + id: "bottleneck", + label: "Find the bottleneck", + goal: "A small system is struggling. Tap the one part that gives out first.", + example: "Users → load balancer → two app servers → one database at 95% CPU. Checkout takes 9 seconds.", + }, + { + id: "trace", + label: "Trace the request", + goal: "Put the steps of a request in order, from the tap on a phone to the answer on screen.", + example: "DNS lookup · TLS handshake · cache miss · database read · response rendered", + }, + { + id: "fix", + label: "Fix the diagram", + goal: "One link or one part is wrong. Find it and fix it in as few moves as you can.", + example: "A queue sits between the browser and the load balancer. One move fixes it.", + }, + { + id: "odd", + label: "Odd one out", + goal: "Four ideas, one does not belong. Say which, and the reason appears.", + example: "Read replica · write-ahead log · CDN edge cache · sticky session", + }, +] as const; + +const RULES = [ + ["One a day", "New at midnight in your time zone. Yesterday's stays open to practise."], + ["A few tries", "Each wrong try shows a clue, so you learn something either way."], + ["Daily leaderboard", "Fewer tries and a faster solve rank higher. It resets every day; joining is your choice."], +] as const; + +type SearchParams = Promise>; + +export default async function PuzzlesPage({ searchParams }: { searchParams: SearchParams }) { const area = areaById("puzzles"); + const query = await searchParams; + const id = pickFilter(query.kind, KINDS.map((k) => k.id)) ?? KINDS[0].id; + const kind = KINDS.find((k) => k.id === id)!; + return ( - - - } title="Find the bottleneck" body="A small system is struggling. Tap the one part that gives out first." /> - } title="Trace the request" body="Put the steps of a request in order, from the tap on a phone to the answer on screen." /> - - - - - - - - } title="One a day" body="A new puzzle at midnight in your time zone. Missed one? Yesterday's stays open to practise." /> - - } title="Daily leaderboard" body="Fewer tries and a faster solve put you higher on today's board. It resets every day, so everyone starts level. Joining it is your choice, like every leaderboard on Edorax." /> - - - - +
+ ({ value: k.id, label: k.label }))} /> +
+ +
+
+ {kind.label} +

{kind.goal}

+

+ Example: + {kind.example} +

+
+
+ {RULES.map(([title, body]) => ( +
+
{title}
+
{body}
+
+ ))} +
+
+ + The visual labs are the closest thing today: a design, a set of checks, and a reason for every result.
diff --git a/frontend/src/app/(learn)/(catalog)/voice/page.tsx b/frontend/src/app/(learn)/(catalog)/voice/page.tsx index 581a7329..97c25a15 100644 --- a/frontend/src/app/(learn)/(catalog)/voice/page.tsx +++ b/frontend/src/app/(learn)/(catalog)/voice/page.tsx @@ -1,7 +1,8 @@ import type { Metadata } from "next"; import { SITE_SHARE_IMAGE } from "@/lib/site"; import { areaById } from "@/lib/learn-areas"; -import { AreaHero, AreaPage, AreaSection, ComingSoonPanel, StepList } from "@/components/areas/area-page"; +import { AreaHero, AreaPage, ComingSoonPanel } from "@/components/areas/area-page"; +import { FilterChips, pickFilter } from "@/components/areas/filter-chips"; export const metadata: Metadata = { title: "On-call tutors", @@ -10,16 +11,59 @@ export const metadata: Metadata = { openGraph: { title: "On-call tutors on Edorax", url: "/voice", images: [SITE_SHARE_IMAGE] }, }; -const CALL: { who: "agent" | "you"; text: string }[] = [ - { who: "agent", text: "Hi, I'm your System Design tutor. What do you already know, and what feels hard?" }, - { who: "you", text: "Yes. I know what a server is, but not much more." }, - { who: "agent", text: "Then we'll start as a beginner. Module 2 is about why waiting suddenly gets long. Want a two-minute version first?" }, - { who: "you", text: "Wait, what is a load balancer for?" }, - { who: "agent", text: "It spreads requests over several servers, so one busy server does not slow everyone down. That is module 4. Shall we go there next?" }, +type Line = { who: "agent" | "you"; text: string }; + +// One tutor per topic. The sample calls are written out; calls are not open yet. +const TOPICS: { id: string; label: string; note: string; call: Line[] }[] = [ + { + id: "system-design", + label: "System Design", + note: "The first tutor.", + call: [ + { who: "agent", text: "Hi, I'm your System Design tutor. What do you already know, and what feels hard?" }, + { who: "you", text: "I know what a server is, but not much more." }, + { who: "agent", text: "Then we start as a beginner. Module 2 is about why waiting suddenly gets long. Want the two-minute version first?" }, + { who: "you", text: "Wait, what is a load balancer for?" }, + { who: "agent", text: "It spreads requests over several servers, so one busy server does not slow everyone down. That is module 4. Shall we go there next?" }, + ], + }, + { + id: "networking", + label: "Networking", + note: "Planned.", + call: [ + { who: "agent", text: "Hi, I'm your Networking tutor. Where would you like to start?" }, + { who: "you", text: "My laptop says connected, but nothing loads." }, + { who: "agent", text: "Good place to start. Let's check three things in order: your address, the way out of your network, then names. Can you open a terminal?" }, + ], + }, + { + id: "ai-engineering", + label: "AI engineering", + note: "Planned.", + call: [ + { who: "agent", text: "Hi, I'm your AI engineering tutor. Building something, or getting ready for an interview?" }, + { who: "you", text: "Interview. I always get stuck on evals." }, + { who: "agent", text: "Then let's practise one answer out loud: how would you know your RAG system got better after a change?" }, + ], + }, ]; -export default function VoicePage() { +const HOW = [ + ["Call the topic", "Press Call and say where you want to start."], + ["Say your level", "It checks with one or two questions instead of trusting a label."], + ["Ask anything", "Interrupt at any point. It answers briefly, then brings you back."], + ["Your place is kept", "The next call picks up where you stopped."], +] as const; + +type SearchParams = Promise>; + +export default async function VoicePage({ searchParams }: { searchParams: SearchParams }) { const area = areaById("voice"); + const query = await searchParams; + const id = pickFilter(query.topic, TOPICS.map((t) => t.id)) ?? TOPICS[0]!.id; + const topic = TOPICS.find((t) => t.id === id)!; + return ( - -
    - {CALL.map((line, i) => ( -
  1. -
    -
    - {line.who === "agent" ? "System Design tutor" : "You"} +
    + ({ value: t.id, label: t.label }))} /> +
    + +
    +
    +

    + {topic.label} tutor. {topic.note} A sample call, written out. +

    +
      + {topic.call.map((line, i) => ( +
    1. +
      +
      + {line.who === "agent" ? `${topic.label} tutor` : "You"} +
      +

      {line.text}

      -

      {line.text}

      -
    -
  2. + + ))} +
+
+
+ {HOW.map(([title, body]) => ( +
+
{title}
+
{body}
+
))} - - - - - - +
+ - - System Design gets the first tutor. Every chapter can already be heard word by word, at any speed from 0.75x to 4x. + + System Design gets the first tutor. Until then, every narrated course is on the Audiobooks shelf.
); From 09c5f8ece8ccb322a353c608d6740c88c3bcf05c Mon Sep 17 00:00:00 2001 From: Zeeshan Adil Date: Fri, 6 Nov 2026 18:17:51 +0500 Subject: [PATCH 3/3] Tutors: on call, shown as a phone call Each tutor is a contact you can call, marked on call. Picking one shows an in-progress call screen: the tutor on the line, a call timer, the sample call as captions, mute, speaker and end controls, and the Call button (opens soon). --- .../src/app/(learn)/(catalog)/voice/page.tsx | 105 ++++++++++++++---- 1 file changed, 83 insertions(+), 22 deletions(-) diff --git a/frontend/src/app/(learn)/(catalog)/voice/page.tsx b/frontend/src/app/(learn)/(catalog)/voice/page.tsx index 97c25a15..58ab65ab 100644 --- a/frontend/src/app/(learn)/(catalog)/voice/page.tsx +++ b/frontend/src/app/(learn)/(catalog)/voice/page.tsx @@ -2,7 +2,10 @@ import type { Metadata } from "next"; import { SITE_SHARE_IMAGE } from "@/lib/site"; import { areaById } from "@/lib/learn-areas"; import { AreaHero, AreaPage, ComingSoonPanel } from "@/components/areas/area-page"; -import { FilterChips, pickFilter } from "@/components/areas/filter-chips"; +import Link from "next/link"; +import { MicOff, Phone, PhoneOff, Volume2 } from "lucide-react"; +import { pickFilter } from "@/components/areas/filter-chips"; +import { cn } from "@/lib/utils"; export const metadata: Metadata = { title: "On-call tutors", @@ -13,12 +16,12 @@ export const metadata: Metadata = { type Line = { who: "agent" | "you"; text: string }; -// One tutor per topic. The sample calls are written out; calls are not open yet. +// One tutor per topic, on call: you ring them, they pick up. The sample calls are written out; calls are not open yet. const TOPICS: { id: string; label: string; note: string; call: Line[] }[] = [ { id: "system-design", label: "System Design", - note: "The first tutor.", + note: "Opens first.", call: [ { who: "agent", text: "Hi, I'm your System Design tutor. What do you already know, and what feels hard?" }, { who: "you", text: "I know what a server is, but not much more." }, @@ -30,7 +33,7 @@ const TOPICS: { id: string; label: string; note: string; call: Line[] }[] = [ { id: "networking", label: "Networking", - note: "Planned.", + note: "Opens later.", call: [ { who: "agent", text: "Hi, I'm your Networking tutor. Where would you like to start?" }, { who: "you", text: "My laptop says connected, but nothing loads." }, @@ -40,7 +43,7 @@ const TOPICS: { id: string; label: string; note: string; call: Line[] }[] = [ { id: "ai-engineering", label: "AI engineering", - note: "Planned.", + note: "Opens later.", call: [ { who: "agent", text: "Hi, I'm your AI engineering tutor. Building something, or getting ready for an interview?" }, { who: "you", text: "Interview. I always get stuck on evals." }, @@ -50,7 +53,7 @@ const TOPICS: { id: string; label: string; note: string; call: Line[] }[] = [ ]; const HOW = [ - ["Call the topic", "Press Call and say where you want to start."], + ["Pick a tutor and call", "One tutor per topic. Press Call and say where you want to start."], ["Say your level", "It checks with one or two questions instead of trusting a label."], ["Ask anything", "Interrupt at any point. It answers briefly, then brings you back."], ["Your place is kept", "The next call picks up where you stopped."], @@ -71,30 +74,88 @@ export default async function VoicePage({ searchParams }: { searchParams: Search title={area.name} status={area.status} mood="happy" - intro="A tutor for every topic, one call away. Say how much you already know and start talking. It answers in plain words and keeps your place." + intro="A tutor on call for every topic. Pick one, press Call and start talking. Ask anything that feels hard; it answers in plain words and keeps your place for the next call." /> -
- ({ value: t.id, label: t.label }))} /> -
+ {/* Tutors are on call: pick one, then call. The list reads like contacts, each with its line status. */} +
-
-

- {topic.label} tutor. {topic.note} A sample call, written out. -

-
    + {/* The call screen: what it looks like with the tutor on the line. Captions are the sample call. */} +
    +
    + + + + +
    +

    {topic.label} tutor

    +

    + + On the call · 02:14 +

    +
    + Sample +
    +
      {topic.call.map((line, i) => ( -
    1. -
      -
      - {line.who === "agent" ? `${topic.label} tutor` : "You"} -
      -

      {line.text}

      -
      +
    2. +

      {line.who === "agent" ? "Tutor" : "You"}

      +

      {line.text}

    3. ))}
    +
    + + + + + + + + + +
    +
    + +
    {HOW.map(([title, body]) => (