diff --git a/frontend/src/app/(learn)/(catalog)/voice/page.tsx b/frontend/src/app/(learn)/(catalog)/voice/page.tsx index 58ab65ab..24739073 100644 --- a/frontend/src/app/(learn)/(catalog)/voice/page.tsx +++ b/frontend/src/app/(learn)/(catalog)/voice/page.tsx @@ -1,71 +1,94 @@ import type { Metadata } from "next"; +import Link from "next/link"; +import { Phone } from "lucide-react"; import { SITE_SHARE_IMAGE } from "@/lib/site"; import { areaById } from "@/lib/learn-areas"; +import { cn } from "@/lib/utils"; import { AreaHero, AreaPage, ComingSoonPanel } from "@/components/areas/area-page"; -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"; +import { TutorVoicePreview, type TutorBoard } from "@/components/voice/tutor-voice-preview"; export const metadata: Metadata = { title: "On-call tutors", - description: "Call a tutor for any topic, start talking and ask about anything that feels hard. It explains in plain words and keeps your place.", + description: "Call a tutor for any topic and talk it through. It answers out loud and shows you diagrams and code on a board while it talks.", alternates: { canonical: "/voice" }, openGraph: { title: "On-call tutors on Edorax", url: "/voice", images: [SITE_SHARE_IMAGE] }, }; type Line = { who: "agent" | "you"; text: string }; -// 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[] }[] = [ +// One tutor per topic, on call. While it talks it can keep a board open: a diagram it draws, the +// commands it runs, the code it writes. Calls are not open yet; the page shows what one looks like. +const TUTORS: { id: string; label: string; note: string; caption: string; board: TutorBoard; call: Line[] }[] = [ { id: "system-design", label: "System Design", - note: "Opens first.", + note: "opens first", + caption: "This box is the load balancer. Watch what happens when one server slows down.", + board: { kind: "diagram" }, 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: "agent", text: "Then we start as a beginner. I'll draw as we go." }, { 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?" }, + { who: "agent", text: "This box is the load balancer. Watch what happens when one server slows down." }, ], }, { id: "networking", label: "Networking", - note: "Opens later.", + note: "opens later", + caption: "Your address works and the way out works. Names fail, so it is DNS.", + board: { + kind: "terminal", + title: "Terminal", + lines: [ + { prompt: true, text: "ip addr show wlan0" }, + { text: "inet 192.168.1.23/24" }, + { prompt: true, text: "ping -c 1 1.1.1.1" }, + { text: "64 bytes from 1.1.1.1: time=18 ms" }, + { prompt: true, text: "nslookup edorax.com" }, + { text: ";; connection timed out" }, + ], + }, 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?" }, + { who: "agent", text: "Let's check three things in order. I'll run them on the board." }, + { who: "agent", text: "Your address works and the way out works. Names fail, so it is DNS." }, ], }, { id: "ai-engineering", label: "AI engineering", - note: "Opens later.", + note: "opens later", + caption: "Same questions, before and after. If the score moves, that is your answer.", + board: { + kind: "code", + title: "eval.py", + lines: [ + "def score(answer, case):", + " hits = [f for f in case.facts if f in answer]", + " return len(hits) / len(case.facts)", + "", + "before = run_eval(old_pipeline, cases) # 0.62", + "after = run_eval(new_pipeline, cases) # 0.71", + ], + }, 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?" }, + { who: "you", text: "How do I show my RAG change made things better?" }, + { who: "agent", text: "Let's write the check together. I'll type it on the board." }, + { who: "agent", text: "Same questions, before and after. If the score moves, that is your answer." }, ], }, ]; -const HOW = [ - ["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."], -] 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)!; + const id = pickFilter(query.topic, TUTORS.map((t) => t.id)) ?? TUTORS[0]!.id; + const tutor = TUTORS.find((t) => t.id === id)!; return ( @@ -74,13 +97,12 @@ export default async function VoicePage({ searchParams }: { searchParams: Search title={area.name} status={area.status} mood="happy" - 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." + intro="Call a tutor and just talk. It answers out loud, and while it talks it can draw, run commands or write code on a board you keep watching." /> - {/* Tutors are on call: pick one, then call. The list reads like contacts, each with its line status. */} -
- {/* 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" ? "Tutor" : "You"}

    -

    {line.text}

    -
  2. - ))} -
-
- - - - - - - - - -
-
- -
-
-
- {HOW.map(([title, body]) => ( -
-
{title}
-
{body}
-
+
+ +
+ +
+ + Read this call + +
    + {tutor.call.map((line, i) => ( +
  1. + {line.who === "agent" ? "Tutor" : "You"}: + {line.text} +
  2. ))} -
-
+ + System Design gets the first tutor. Until then, every narrated course is on the Audiobooks shelf. diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 23699538..b229a924 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -957,6 +957,26 @@ body:has([data-audiobook-bar]) { --audiobook-bar-space: 4.75rem; } +/* Tutor voice preview (/voice): the breathing orb and the tutor drawing on its board. */ +@keyframes tutor-orb { + 0%, + 100% { + transform: scale(0.94); + } + 50% { + transform: scale(1.04); + } +} + +@keyframes tutor-draw { + from { + stroke-dashoffset: 300; + } + to { + stroke-dashoffset: 0; + } +} + @keyframes audiobook-eq { 0%, 100% { diff --git a/frontend/src/components/voice/tutor-voice-preview.tsx b/frontend/src/components/voice/tutor-voice-preview.tsx new file mode 100644 index 00000000..579b2268 --- /dev/null +++ b/frontend/src/components/voice/tutor-voice-preview.tsx @@ -0,0 +1,130 @@ +import { Mic, PanelLeft, X } from "lucide-react"; +import { cn } from "@/lib/utils"; + +// What a tutor call will look like: a quiet voice view (a breathing orb, one caption, three controls) +// next to a board the tutor draws on while it talks, the way a voice assistant can keep a screen open +// while it speaks. A preview: nothing here is live yet. + +export type TutorBoard = + | { kind: "diagram" } + | { kind: "terminal"; title: string; lines: { prompt?: boolean; text: string }[] } + | { kind: "code"; title: string; lines: string[] }; + +const INK = "#0A0F2C"; +const COBALT = "#2434E0"; +const YELLOW = "#FFC933"; + +/** Users, a load balancer and two servers; the tutor is drawing the second link and has circled the balancer. */ +function DiagramBoard() { + return ( + + + Users + + + Load balancer + {/* The tutor's circle around the part it is talking about. */} + + + + Server A + + + Server B + + ); +} + +function Caret() { + return ; +} + +function Board({ board }: { board: TutorBoard }) { + if (board.kind === "diagram") { + return ( +
+ +
+ ); + } + const terminal = board.kind === "terminal"; + return ( +
+
+ + + + + + {board.title} +
+
+        {terminal
+          ? board.lines.map((line, i) => (
+              
+ {line.prompt ? $ : " "} + {line.text} + {i === board.lines.length - 1 ? : null} +
+ )) + : board.lines.map((line, i) => ( +
+ {line || " "} + {i === board.lines.length - 1 ? : null} +
+ ))} +
+
+ ); +} + +function ControlButton({ label, className, children }: { label: string; className?: string; children: React.ReactNode }) { + return ( + + {children} + {label} + + ); +} + +export function TutorVoicePreview({ name, caption, board }: { name: string; caption: string; board: TutorBoard }) { + return ( +
+
+ {/* The board the tutor keeps on screen while it talks. */} +
+
+ Board + {name} tutor is showing you +
+
+ +
+
+ + {/* The voice view: nothing but the tutor's voice, what it is saying, and three buttons. */} +
+ Preview +
+ + + +
+

{name} tutor is speaking

+
{caption}
+
+ + + + + + + + + +
+
+
+
+ ); +}