// ============================================================
// Lessons — each module exports a component receiving { state, setState, next, prev, accent }
// ============================================================
const { useState, useEffect, useMemo, useRef } = React;

/* ---------- Shared lesson layout primitives ---------- */
function LessonStep({ tag, title, prose, children }) {
  return (
    <div>
      <span className="lesson-step-tag">{tag}</span>
      <h2 className="lesson-heading">{title}</h2>
      {prose && <p className="lesson-prose">{prose}</p>}
      {children}
    </div>
  );
}

function Tips({ title, children, icon = "💡" }) {
  return (
    <div className="tips">
      <div className="tips-title"><span>{icon}</span> {title}</div>
      {children}
    </div>
  );
}

// ============================================================
// MODULE 1 — Meet Your Voice Agent (Voice / Brain / Hands)
// ============================================================
function Lesson1({ next }) {
  const [revealed, setRevealed] = useState([]);
  const cards = [
    { id: "voice", color: "var(--c-voice)", icon: window.I.Mic, name: "The Voice", lead: "How it sounds",
      detail: "Text-to-speech that doesn't sound like a robot. 30+ natural voices across accents, with sub-300ms latency so conversations feel real." },
    { id: "brain", color: "var(--c-brain)", icon: window.I.Brain, name: "The Brain", lead: "How it thinks",
      detail: "An LLM (GPT-4o, Claude, or Gemini) handles reasoning. It follows your system prompt, draws on your knowledge base, and decides when to call tools." },
    { id: "hands", color: "var(--c-hands)", icon: window.I.Hand, name: "The Hands", lead: "How it acts",
      detail: "Tool calls let the agent actually do things mid-conversation — look up an order, book a meeting, take payment, escalate to a human." },
  ];
  const allRevealed = revealed.length === cards.length;
  return (
    <>
      <LessonStep tag="Module 1 · Concepts" title="Every voice agent is three pieces."
        prose="Before we build one, you need the mental model. A voice agent is a voice (mouth), a brain (LLM), and hands (tools that do things). Tap each card to see how they fit together.">
      </LessonStep>

      <div className="sim">
        <div className="sim-label">Anatomy of a voice agent</div>
        <div className="choice-row" style={{ marginTop: 10 }}>
          {cards.map(c => {
            const Icon = c.icon;
            const open = revealed.includes(c.id);
            return (
              <button key={c.id}
                className={"choice" + (open ? " selected" : "")}
                style={{ "--accent": c.color, minHeight: 200, display: "flex", flexDirection: "column", alignItems: "flex-start" }}
                onClick={() => setRevealed(r => r.includes(c.id) ? r : [...r, c.id])}>
                <div className="choice-icon" style={{ background: `color-mix(in oklab, ${c.color} 12%, white)`, color: c.color }}>
                  <Icon />
                </div>
                <div style={{ fontFamily: "Fraunces, serif", fontWeight: 500, fontSize: 22, letterSpacing: "-0.01em", color: c.color }}>{c.name}</div>
                <div className="choice-desc" style={{ marginTop: 2, marginBottom: 8, fontWeight: 500, color: "var(--ink)" }}>{c.lead}</div>
                <div className="choice-desc" style={{ lineHeight: 1.5, opacity: open ? 1 : 0.4, transition: "opacity 280ms" }}>
                  {open ? c.detail : "Tap to reveal →"}
                </div>
              </button>
            );
          })}
        </div>
      </div>

      <div className="row between mt-6">
        <div className="muted" style={{ fontSize: 13 }}>{revealed.length}/3 revealed</div>
        <button className="btn btn-primary" disabled={!allRevealed} onClick={next}>
          {allRevealed ? "Got it — let's build" : "Reveal all three to continue"}
          <window.I.ArrowRight />
        </button>
      </div>
    </>
  );
}

function Lesson1Side() {
  return (
    <>
      <Tips title="Why voice agents now">
        <p>Two things changed in 2024–25: LLMs got fast enough to feel conversational, and TTS got good enough to sound human. The combo unlocks phone-replaceable agents.</p>
      </Tips>
      <Tips title="What you'll have by the end" icon="🎯">
        <ul className="tips-list">
          <li>A configured agent with a persona</li>
          <li>A knowledge base it draws from</li>
          <li>Tool connections that let it act</li>
          <li>A test report before going live</li>
        </ul>
      </Tips>
    </>
  );
}

// ============================================================
// MODULE 2 — Create Your Agent
// ============================================================
const INDUSTRIES = ["Healthcare", "Real estate", "E-commerce", "Restaurants", "Legal", "Education", "Fitness", "Auto", "Finance", "SaaS", "Insurance", "Travel", "Beauty", "Home services", "Non-profit", "Other"];

function Lesson2({ state, setState, next }) {
  const types = [
    { id: "blank", name: "Blank canvas", desc: "Start from scratch. Maximum control.", icon: window.I.Edit },
    { id: "personal", name: "Personal assistant", desc: "Errands, scheduling, reminders.", icon: window.I.User },
    { id: "business", name: "Business agent", desc: "Customer-facing, on-brand, tooled-up.", icon: window.I.Briefcase },
  ];
  const useCases = [
    { id: "support", name: "Customer support", icon: window.I.Headset },
    { id: "sales", name: "Sales / outbound", icon: window.I.Megaphone },
    { id: "scheduling", name: "Scheduling", icon: window.I.Calendar },
    { id: "leadgen", name: "Lead qualification", icon: window.I.Filter },
    { id: "receptionist", name: "Receptionist", icon: window.I.Phone },
    { id: "personal", name: "Personal", icon: window.I.User },
  ];
  const ready = state.agentType && state.industry && state.useCase;
  return (
    <>
      <LessonStep tag="Module 2 · Setup" title="Spin up your agent in three picks."
        prose="Every voice agent starts with three choices: what kind, what industry, what job. These set sensible defaults you can override later — don't overthink it." />

      <div className="sim">
        <div className="sim-label">Step 1 — Agent type</div>
        <div className="choice-row">
          {types.map(t => {
            const Icon = t.icon;
            return (
              <button key={t.id}
                className={"choice" + (state.agentType === t.id ? " selected" : "")}
                onClick={() => setState({ agentType: t.id })}>
                <div className="choice-icon"><Icon /></div>
                <p className="choice-title">{t.name}</p>
                <p className="choice-desc">{t.desc}</p>
              </button>
            );
          })}
        </div>
      </div>

      <div className="sim">
        <div className="sim-label">Step 2 — Industry (17 to pick from)</div>
        <div className="toggle-row">
          {INDUSTRIES.map(ind => (
            <button key={ind}
              className={"chip" + (state.industry === ind ? " on" : "")}
              onClick={() => setState({ industry: ind })}>
              {ind}
            </button>
          ))}
        </div>
      </div>

      <div className="sim">
        <div className="sim-label">Step 3 — Use case</div>
        <div className="tool-grid">
          {useCases.map(u => {
            const Icon = u.icon;
            const on = state.useCase === u.id;
            return (
              <button key={u.id} className={"tool-tile" + (on ? " on" : "")} onClick={() => setState({ useCase: u.id })}>
                <div className="tool-logo" style={{ background: "color-mix(in oklab, var(--accent) 14%, white)", color: "var(--accent)" }}>
                  <Icon />
                </div>
                <div>
                  <div className="tool-name">{u.name}</div>
                  <div className="tool-sub">Templated prompt + tools</div>
                </div>
                <div className="tool-check">{on && <window.I.Check />}</div>
              </button>
            );
          })}
        </div>
      </div>

      <div className="row between mt-6">
        <div className="muted" style={{ fontSize: 13 }}>
          {ready
            ? <>Picked: <strong style={{ color: "var(--ink)" }}>{types.find(t=>t.id===state.agentType)?.name}</strong> · {state.industry} · {useCases.find(u=>u.id===state.useCase)?.name}</>
            : "Pick one in each step to continue"}
        </div>
        <button className="btn btn-primary" disabled={!ready} onClick={next}>
          Configure it <window.I.ArrowRight />
        </button>
      </div>
    </>
  );
}

function Lesson2Side({ state }) {
  return (
    <>
      <Tips title="Pro tip">
        <p>The industry pick isn't cosmetic — it pre-loads vocabulary, compliance defaults (e.g. HIPAA flags for healthcare), and example tool integrations.</p>
      </Tips>
      <Tips title="If you're stuck" icon="🤔">
        <p>Don't pick "Blank" yet. Start with "Business agent" + your closest industry — you can always strip things back. Templates teach you the shape.</p>
      </Tips>
      <div className="sim" style={{ marginTop: 0 }}>
        <div className="sim-label">Live preview</div>
        <div style={{ fontFamily: "Fraunces, serif", fontSize: 20, letterSpacing: "-0.01em", marginTop: 4 }}>
          {state.useCase ? "Your " + (state.industry || "...") + " " + (state.useCase || "agent") : "Untitled agent"}
        </div>
        <div className="muted" style={{ fontSize: 12, marginTop: 4 }}>Draft · Not deployed</div>
      </div>
    </>
  );
}

// ============================================================
// MODULE 3 — Configure Everything (system prompt + voice + LLM + language)
// ============================================================
const VOICES = [
  { id: "aria",   name: "Aria",   tag: "Warm · Female · American",       hue: "#ff5b9a", lang: "en-US", gender: "female", rate: 1.02, pitch: 1.15, elId: "DODLEQrClDo8wCz460ld", sample: "Hi, this is Aria from Northwind Dental — how can I help you today?" },
  { id: "luca",   name: "Luca",   tag: "Confident · Male · British",     hue: "#7c3aed", lang: "en-GB", gender: "male",   rate: 0.98, pitch: 0.92, elId: "lUTamkMw7gOzZbFIwmq4", sample: "Good afternoon. Luca here — pleasure to assist with whatever you need." },
  { id: "june",   name: "June",   tag: "Friendly · Female · Australian", hue: "#06b6d4", lang: "en-AU", gender: "female", rate: 1.05, pitch: 1.10, elId: "cvpTJfe9LINpHIOmB2Hp", sample: "G'day! This is June. Ready when you are — what can I sort out for you?" },
  { id: "miles",  name: "Miles",  tag: "Calm · Male · American",         hue: "#84cc16", lang: "en-US", gender: "male",   rate: 0.95, pitch: 0.88, elId: "7WFXnV3RliG36epJXuCr", sample: "Hey, this is Miles. Take your time — I'm here to help whenever you're ready." },
  { id: "nova",   name: "Nova",   tag: "Bright · Female · Canadian",     hue: "#f59e0b", lang: "en-CA", gender: "female", rate: 1.08, pitch: 1.20, elId: "ycvyTVVIzO2xfIGZC7tZ", sample: "Hi there — Nova speaking! Excited to help you get this sorted out." },
  { id: "kenji",  name: "Kenji",  tag: "Crisp · Female · Japanese-EN",   hue: "#ef4444", lang: "en-US", gender: "female", rate: 1.00, pitch: 1.10, elId: "u6a6bRv82Zfi9NzoIqvt", sample: "Hello, this is Kenji. Thank you for calling — how may I assist you today?" },
];

// ---- ElevenLabs preview ----
const EL_KEY_STORAGE = "vaa_elevenlabs_key";

async function speakViaElevenLabs(voice, apiKey, onStart, onEnd, textOverride) {
  const url = `https://api.elevenlabs.io/v1/text-to-speech/${voice.elId}?output_format=mp3_44100_128`;
  try {
    const res = await fetch(url, {
      method: "POST",
      headers: {
        "xi-api-key": apiKey,
        "Content-Type": "application/json",
        "Accept": "audio/mpeg",
      },
      body: JSON.stringify({
        text: textOverride || voice.sample,
        model_id: "eleven_turbo_v2_5",
        voice_settings: { stability: 0.45, similarity_boost: 0.75, style: 0.15, use_speaker_boost: true },
      }),
    });
    if (!res.ok) {
      const errText = await res.text();
      onEnd && onEnd({ error: `ElevenLabs ${res.status}: ${errText.slice(0, 160)}` });
      return null;
    }
    const blob = await res.blob();
    const audioUrl = URL.createObjectURL(blob);
    const audio = new Audio(audioUrl);
    audio.onplay = () => onStart && onStart();
    audio.onended = () => { URL.revokeObjectURL(audioUrl); onEnd && onEnd(); };
    audio.onerror = () => { URL.revokeObjectURL(audioUrl); onEnd && onEnd({ error: "Audio playback failed" }); };
    audio.play();
    return audio;
  } catch (e) {
    onEnd && onEnd({ error: e.message || "Network error" });
    return null;
  }
}

// Web Speech API helpers — picks the best system voice matching lang+gender hints
function pickSystemVoice(lang, gender) {
  if (typeof window.speechSynthesis === "undefined") return null;
  const all = window.speechSynthesis.getVoices();
  if (!all.length) return null;
  // 1) exact lang match + name hints gender
  const langMatches = all.filter(v => v.lang && v.lang.toLowerCase().startsWith(lang.toLowerCase()));
  const femaleHints = /female|samantha|victoria|karen|moira|tessa|fiona|nicky|allison|ava|kate|serena|zoe|susan/i;
  const maleHints   = /male|alex|daniel|fred|tom|oliver|rishi|aaron|arthur|gordon|ralph|reed/i;
  const pool = langMatches.length ? langMatches : all;
  const pref = pool.find(v => gender === "female" ? femaleHints.test(v.name) : maleHints.test(v.name));
  if (pref) return pref;
  // 2) fall back to first matching lang
  return langMatches[0] || all.find(v => v.lang.startsWith("en")) || all[0];
}

function speakSample(voice, onStart, onEnd, textOverride) {
  if (typeof window.speechSynthesis === "undefined") {
    onEnd && onEnd({ error: "Your browser doesn't support speech synthesis." });
    return null;
  }
  window.speechSynthesis.cancel();
  const u = new SpeechSynthesisUtterance(textOverride || voice.sample);
  const sys = pickSystemVoice(voice.lang, voice.gender);
  if (sys) u.voice = sys;
  u.lang = voice.lang;
  u.rate = voice.rate;
  u.pitch = voice.pitch;
  u.volume = 1;
  u.onstart = () => onStart && onStart();
  u.onend = () => onEnd && onEnd();
  u.onerror = (e) => onEnd && onEnd({ error: e.error });
  window.speechSynthesis.speak(u);
  return u;
}
const LLMS = [
  { id: "gpt4o",   name: "GPT-4o",       sub: "Fast, witty, balanced" },
  { id: "claude",  name: "Claude 4.5",   sub: "Long context, careful" },
  { id: "gemini",  name: "Gemini 2.5",   sub: "Cheap, multilingual"   },
];
const LANGUAGES = ["English", "Spanish", "French", "German", "Portuguese", "Japanese", "Mandarin", "Hindi"];

function VoicePicker({ voices, selected, onSelect }) {
  const [playing, setPlaying] = useState(null); // voice id currently speaking
  const [loadingId, setLoadingId] = useState(null);
  const [supported, setSupported] = useState(true);
  const [apiKey, setApiKey] = useState(() => localStorage.getItem(EL_KEY_STORAGE) || "");
  const [keyInput, setKeyInput] = useState("");
  const [showKeyForm, setShowKeyForm] = useState(false);
  const [error, setError] = useState(null);
  const audioRef = useRef(null);

  const hasKey = !!apiKey;

  // ensure voices load (Chrome quirk)
  useEffect(() => {
    if (typeof window.speechSynthesis === "undefined") {
      setSupported(false);
      return;
    }
    const ensure = () => window.speechSynthesis.getVoices();
    ensure();
    window.speechSynthesis.onvoiceschanged = ensure;
    return () => {
      window.speechSynthesis.cancel();
      if (audioRef.current) { try { audioRef.current.pause(); } catch (e) {} }
      window.speechSynthesis.onvoiceschanged = null;
    };
  }, []);

  const stopAll = () => {
    if (window.speechSynthesis) window.speechSynthesis.cancel();
    if (audioRef.current) { try { audioRef.current.pause(); audioRef.current.currentTime = 0; } catch (e) {} audioRef.current = null; }
  };

  const preview = (v, e) => {
    e.stopPropagation();
    setError(null);
    if (playing === v.id || loadingId === v.id) {
      stopAll();
      setPlaying(null);
      setLoadingId(null);
      return;
    }
    stopAll();
    if (hasKey) {
      setLoadingId(v.id);
      speakViaElevenLabs(v, apiKey,
        () => { setLoadingId(null); setPlaying(v.id); },
        (res) => {
          setLoadingId(null); setPlaying(null);
          if (res && res.error) setError(res.error);
        }
      ).then(audio => { if (audio) audioRef.current = audio; });
    } else {
      speakSample(v,
        () => setPlaying(v.id),
        (res) => { setPlaying(null); if (res && res.error) setError(res.error); });
    }
  };

  const saveKey = () => {
    if (!keyInput.trim()) return;
    localStorage.setItem(EL_KEY_STORAGE, keyInput.trim());
    setApiKey(keyInput.trim());
    setKeyInput("");
    setShowKeyForm(false);
    setError(null);
  };
  const removeKey = () => {
    localStorage.removeItem(EL_KEY_STORAGE);
    setApiKey("");
    setError(null);
  };

  return (
    <>
      <div className="voice-source">
        <div className="voice-source-status">
          {hasKey ? (
            <>
              <span className="voice-source-dot real" />
              <span><strong>ElevenLabs voices</strong> · using your API key</span>
            </>
          ) : (
            <>
              <span className="voice-source-dot fake" />
              <span><strong>Browser TTS preview</strong> · approximate sound</span>
            </>
          )}
        </div>
        <div className="row" style={{ gap: 6 }}>
          {hasKey
            ? <button className="btn btn-ghost" style={{ fontSize: 11, padding: "5px 9px" }} onClick={removeKey}>Disconnect</button>
            : <button className="btn" style={{ fontSize: 11, padding: "5px 10px" }} onClick={() => setShowKeyForm(s => !s)}>
                <window.I.Sparkle /> Use real ElevenLabs voices
              </button>}
        </div>
      </div>

      {showKeyForm && !hasKey && (
        <div className="voice-key-form">
          <div className="field-label" style={{ marginBottom: 4 }}>Paste your ElevenLabs API key</div>
          <div className="field-hint" style={{ marginBottom: 8 }}>
            Get one at <a href="https://elevenlabs.io/app/settings/api-keys" target="_blank" rel="noopener" style={{ color: "var(--accent)" }}>elevenlabs.io → Settings → API Keys</a>.
            Stored only in this browser. Calls go directly from your browser to ElevenLabs.
          </div>
          <div className="row">
            <input className="input" type="password" placeholder="sk_..." value={keyInput}
              onChange={e => setKeyInput(e.target.value)}
              onKeyDown={e => e.key === "Enter" && saveKey()} />
            <button className="btn btn-primary" onClick={saveKey} disabled={!keyInput.trim()}>Save</button>
            <button className="btn btn-ghost" onClick={() => setShowKeyForm(false)}>Cancel</button>
          </div>
        </div>
      )}

      {error && (
        <div className="voice-error">
          <strong>Preview failed.</strong> {error}
        </div>
      )}

      <div className="voice-grid">
        {voices.map(v => {
          const on = selected === v.id;
          const isPlaying = playing === v.id;
          const isLoading = loadingId === v.id;
          return (
            <div key={v.id}
              className={"voice-card" + (on ? " on" : "") + (isPlaying ? " playing" : "")}
              style={{ "--vc": v.hue }}
              onClick={() => onSelect(v.id)}
              role="button"
              tabIndex={0}>
              <div className="voice-avatar">
                <div className={"wave" + (isPlaying ? " live" : "")}>
                  <span style={{ animationDelay: "0s"   }} />
                  <span style={{ animationDelay: ".1s"  }} />
                  <span style={{ animationDelay: ".2s"  }} />
                  <span style={{ animationDelay: ".3s"  }} />
                  <span style={{ animationDelay: ".15s" }} />
                </div>
              </div>
              <div className="voice-meta">
                <div className="voice-name">{v.name}{hasKey && <span className="voice-real-badge">REAL</span>}</div>
                <div className="voice-tag">{v.tag}</div>
              </div>
              <button className={"voice-play" + (isPlaying ? " on" : "") + (isLoading ? " loading" : "")}
                onClick={(e) => preview(v, e)}
                disabled={!supported && !hasKey}
                aria-label={isPlaying ? "Stop preview" : "Preview voice"}>
                {isLoading
                  ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"><path d="M12 3a9 9 0 1 0 9 9" /></svg>
                  : isPlaying
                  ? <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>
                  : <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="7 4 20 12 7 20 7 4"/></svg>}
                <span>{isLoading ? "Loading…" : isPlaying ? "Stop" : "Preview"}</span>
              </button>
              {on && <div className="voice-check"><window.I.Check /></div>}
            </div>
          );
        })}
      </div>
      <div className="field-hint" style={{ marginTop: 10 }}>
        {hasKey
          ? <>Click a card to pick · Press <strong>Preview</strong> for the real ElevenLabs voice.</>
          : <>Click a card to pick · Press <strong>Preview</strong> for a browser-TTS approximation, or connect ElevenLabs above for the real thing.</>}
      </div>
    </>
  );
}

function Lesson3({ state, setState, next }) {
  const ready = state.systemPrompt && state.voice && state.llm && state.languages.length > 0;
  const promptStarter = "You are a warm, efficient front-desk agent for Northwind Dental. Greet callers by name when you know it, never invent appointment times, and always confirm phone numbers back. Stay under 25 words per turn.";
  return (
    <>
      <LessonStep tag="Module 3 · Configure" title="Define personality, pick a mouth."
        prose="This is where your agent becomes yours. Write a system prompt (this is the single highest-leverage thing you'll do today). Pick a voice. Pick a brain. Pick languages." />

      <div className="sim">
        <div className="sim-label">System prompt — who is this agent?</div>
        <textarea className="input" placeholder={promptStarter}
          value={state.systemPrompt}
          onChange={e => setState({ systemPrompt: e.target.value })} />
        <div className="row between mt-2">
          <button className="btn btn-ghost" style={{ padding: "6px 10px", fontSize: 12 }}
            onClick={() => setState({ systemPrompt: promptStarter })}>
            <window.I.Sparkle /> Use starter prompt
          </button>
          <div className="muted" style={{ fontSize: 12 }}>{(state.systemPrompt || "").length} / 4000 chars</div>
        </div>
      </div>

      <div className="sim">
        <div className="sim-label">Pick a voice (30+ available, 6 shown)</div>
        <VoicePicker
          voices={VOICES}
          selected={state.voice}
          onSelect={(id) => setState({ voice: id })} />
      </div>

      <div className="sim">
        <div className="sim-label">Choose your LLM (the brain)</div>
        <div className="choice-row">
          {LLMS.map(l => (
            <button key={l.id} className={"choice" + (state.llm === l.id ? " selected" : "")}
              onClick={() => setState({ llm: l.id })}>
              <div className="choice-icon"><window.I.Chip /></div>
              <p className="choice-title">{l.name}</p>
              <p className="choice-desc">{l.sub}</p>
            </button>
          ))}
        </div>
      </div>

      <div className="sim">
        <div className="sim-label">Languages (auto-switching enabled)</div>
        <div className="toggle-row">
          {LANGUAGES.map(lang => {
            const on = state.languages.includes(lang);
            return (
              <button key={lang}
                className={"chip" + (on ? " on" : "")}
                onClick={() => {
                  const langs = on ? state.languages.filter(l => l !== lang) : [...state.languages, lang];
                  setState({ languages: langs });
                }}>
                <window.I.Globe /> {lang}
              </button>
            );
          })}
        </div>
        <div className="field-hint">The agent will auto-detect and switch when callers speak a different selected language.</div>
      </div>

      <div className="row between mt-6">
        <div className="muted" style={{ fontSize: 13 }}>
          {ready ? "✓ All four configured" : "Finish all four sections to continue"}
        </div>
        <button className="btn btn-primary" disabled={!ready} onClick={next}>
          Give it knowledge <window.I.ArrowRight />
        </button>
      </div>
    </>
  );
}

function Lesson3Side({ state }) {
  return (
    <>
      <Tips title="The 80/20 of system prompts" icon="✍️">
        <ul className="tips-list">
          <li><strong>Persona:</strong> one line. Who & vibe.</li>
          <li><strong>Boundaries:</strong> what NOT to do.</li>
          <li><strong>Format:</strong> length per turn, tone.</li>
          <li><strong>Escalation:</strong> when to hand off.</li>
        </ul>
      </Tips>
      <Tips title="Voice latency" icon="⚡">
        <p>Aim for sub-400ms time-to-first-audio. Smaller LLMs feel snappier; bigger ones feel smarter. Most production agents pick the middle.</p>
      </Tips>
      <div className="sim" style={{ marginTop: 0 }}>
        <div className="sim-label">Configuration snapshot</div>
        <div className="snippet" style={{ marginTop: 8 }}>
{`{
  `}<span className="k">"voice"</span>{`: `}<span className="s">"{state.voice || "—"}"</span>{`,
  `}<span className="k">"llm"</span>{`:   `}<span className="s">"{state.llm || "—"}"</span>{`,
  `}<span className="k">"langs"</span>{`: `}<span className="s">{JSON.stringify(state.languages || [])}</span>{`,
  `}<span className="c">// + system prompt</span>{`
}`}
        </div>
      </div>
    </>
  );
}

// ============================================================
// MODULE 4 — Knowledge Base
// ============================================================
const SAMPLE_FILES = [
  { name: "appointment-policy.md", kind: "MD", size: "3.2 KB", url: "templates/appointment-policy.md" },
  { name: "services.md",            kind: "MD", size: "3.5 KB", url: "templates/services.md" },
  { name: "insurance-faq.md",       kind: "MD", size: "4.0 KB", url: "templates/insurance-faq.md" },
];

function Lesson4({ state, setState, next }) {
  const [activeTab, setTab] = useState("url");
  const [urlValue, setUrlValue] = useState("");
  const [textValue, setTextValue] = useState("");

  const addUrl = () => {
    if (!urlValue.trim()) return;
    setState({ kbItems: [...state.kbItems, { type: "url", label: urlValue.trim() }] });
    setUrlValue("");
  };
  const addFile = (f) => setState({ kbItems: [...state.kbItems, { type: "file", label: f.name, kind: f.kind, size: f.size, content: (window.TEMPLATES && window.TEMPLATES[f.name]) || "" }] });
  const addText = () => {
    if (!textValue.trim()) return;
    const label = textValue.trim().slice(0, 38) + (textValue.length > 38 ? "…" : "");
    setState({ kbItems: [...state.kbItems, { type: "text", label, content: textValue.trim().slice(0, 8000) }] });
    setTextValue("");
  };

  const [dragOver, setDragOver] = useState(false);
  const [indexing, setIndexing] = useState(0); // count of files being processed
  const fileInputRef = useRef(null);
  const TEXT_EXTS = /\.(md|markdown|txt|csv|html?|json)$/i;

  const loadScript = (src) => new Promise((res, rej) => {
    if (document.querySelector(`script[src="${src}"]`)) return res();
    const s = document.createElement("script");
    s.src = src; s.onload = res; s.onerror = () => rej(new Error("Failed to load " + src));
    document.head.appendChild(s);
  });

  const extractPdf = async (file) => {
    await loadScript("https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.min.js");
    const pdfjs = window["pdfjs-dist/build/pdf"] || window.pdfjsLib;
    pdfjs.GlobalWorkerOptions.workerSrc = "https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.worker.min.js";
    const buf = await file.arrayBuffer();
    const doc = await pdfjs.getDocument({ data: buf }).promise;
    let text = "";
    for (let p = 1; p <= doc.numPages && text.length < 8000; p++) {
      const page = await doc.getPage(p);
      const tc = await page.getTextContent();
      text += tc.items.map(it => it.str).join(" ") + "\n";
    }
    return text.replace(/\s{3,}/g, " ").slice(0, 8000);
  };

  const extractDocx = async (file) => {
    await loadScript("https://unpkg.com/jszip@3.10.1/dist/jszip.min.js");
    const zip = await window.JSZip.loadAsync(await file.arrayBuffer());
    const xml = await zip.file("word/document.xml").async("string");
    return xml
      .replace(/<w:p[ >]/g, "\n<w:p ")
      .replace(/<[^>]+>/g, "")
      .replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"')
      .replace(/[ \t]{2,}/g, " ").trim().slice(0, 8000);
  };

  const pushItem = (item) => setState(prev => ({ kbItems: [...(prev && prev.kbItems ? prev.kbItems : state.kbItems), item] }));

  const handleFiles = (fileList) => {
    Array.from(fileList).forEach(async (file) => {
      const kind = (file.name.split(".").pop() || "file").toUpperCase();
      const size = file.size > 1024 * 1024 ? (file.size / 1048576).toFixed(1) + " MB" : Math.max(1, Math.round(file.size / 1024)) + " KB";
      let content = "";
      setIndexing(n => n + 1);
      try {
        if (TEXT_EXTS.test(file.name)) {
          content = String(await file.text()).slice(0, 8000);
        } else if (/\.pdf$/i.test(file.name)) {
          content = await extractPdf(file);
        } else if (/\.docx$/i.test(file.name)) {
          content = await extractDocx(file);
        }
      } catch (e) {
        console.warn("Extraction failed for", file.name, e);
      }
      setIndexing(n => n - 1);
      setState({ kbItems: [...stateRef.current.kbItems, { type: "file", label: file.name, kind, size, content }] });
    });
  };

  // keep a live ref so async extractions don't clobber each other
  const stateRef = useRef(state);
  stateRef.current = state;
  const remove = (i) => setState({ kbItems: state.kbItems.filter((_, idx) => idx !== i) });

  const ready = state.kbItems.length >= 2;
  return (
    <>
      <LessonStep tag="Module 4 · Knowledge" title="Feed it your expertise."
        prose="An out-of-the-box LLM doesn't know your pricing, your SOPs, or that you closed early on Tuesdays. Knowledge base = your business in the agent's head. Add at least two sources to continue." />

      <div className="sim">
        <div className="sim-label">Add a source</div>
        <div className="toggle-row" style={{ marginBottom: 12 }}>
          {[
            { id: "url",  icon: window.I.Link,   label: "Add URL" },
            { id: "file", icon: window.I.Upload, label: "Upload file" },
            { id: "text", icon: window.I.Edit,   label: "Paste text" },
          ].map(t => {
            const Icon = t.icon;
            return (
              <button key={t.id}
                className={"chip" + (activeTab === t.id ? " on" : "")}
                onClick={() => setTab(t.id)}>
                <Icon /> {t.label}
              </button>
            );
          })}
        </div>

        {activeTab === "url" && (
          <div className="col">
            <label className="field-label">Website or docs URL</label>
            <div className="row">
              <input className="input" placeholder="https://northwinddental.com/services"
                value={urlValue} onChange={e => setUrlValue(e.target.value)}
                onKeyDown={e => e.key === "Enter" && addUrl()} />
              <button className="btn btn-primary" onClick={addUrl}>Crawl</button>
            </div>
            <div className="field-hint">We crawl up to 200 pages, extract clean text, and chunk it for retrieval.</div>
          </div>
        )}

        {activeTab === "file" && (
          <div>
            <label className="field-label">Upload your own files</label>
            <div
              onClick={() => fileInputRef.current && fileInputRef.current.click()}
              onDragOver={e => { e.preventDefault(); setDragOver(true); }}
              onDragLeave={() => setDragOver(false)}
              onDrop={e => { e.preventDefault(); setDragOver(false); handleFiles(e.dataTransfer.files); }}
              style={{
                border: "1.5px dashed " + (dragOver ? "var(--accent)" : "var(--line)"),
                background: dragOver ? "color-mix(in oklab, var(--accent) 6%, white)" : "var(--paper)",
                borderRadius: 12, padding: "22px 16px", textAlign: "center", cursor: "pointer", marginBottom: 14,
              }}>
              <div style={{ fontWeight: 600, fontSize: 14 }}>{indexing > 0 ? "Indexing… (" + indexing + " file" + (indexing > 1 ? "s" : "") + ")" : "Drop files here, or click to browse"}</div>
              <div className="field-hint" style={{ marginTop: 4 }}>
                .md, .txt, .html, .csv, .json, .pdf and .docx are read and given to your agent (first ~8,000 characters each).
              </div>
              <input ref={fileInputRef} type="file" multiple style={{ display: "none" }}
                accept=".md,.markdown,.txt,.csv,.html,.htm,.json,.pdf,.docx"
                onChange={e => { handleFiles(e.target.files); e.target.value = ""; }} />
            </div>
            <label className="field-label">Or start from a template</label>
            <div className="field-hint" style={{ marginBottom: 10 }}>
              These are <strong>real starter templates</strong> for a dental practice — download to use as a starting point for your own knowledge base, or just add them to this simulation.
            </div>
            <div className="tool-grid">
              {SAMPLE_FILES.map(f => (
                <div key={f.name} className="tool-tile" style={{ cursor: "default" }}>
                  <div className="tool-logo" style={{ background: "var(--accent)", color: "white", fontSize: 10 }}>{f.kind}</div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div className="tool-name">{f.name}</div>
                    <div className="tool-sub">{f.size} · dental starter template</div>
                  </div>
                  <div className="row" style={{ gap: 6 }}>
                    <button className="btn" onClick={(e) => { e.stopPropagation(); window.downloadTemplate(f.name); }}
                      style={{ padding: "6px 10px", fontSize: 12 }}>
                      <window.I.Upload style={{ transform: "rotate(180deg)" }} /> Download
                    </button>
                    <button className="btn btn-primary" style={{ padding: "6px 10px", fontSize: 12 }} onClick={() => addFile(f)}>
                      Add
                    </button>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}

        {activeTab === "text" && (
          <div className="col">
            <label className="field-label">Paste any notes, FAQs, scripts</label>
            <textarea className="input" placeholder="Q: Do you accept walk-ins?&#10;A: Only before 11am, and only for cleanings…"
              value={textValue} onChange={e => setTextValue(e.target.value)} />
            <div className="row between">
              <div className="field-hint">Markdown is fine. Bullet lists work great.</div>
              <button className="btn btn-primary" onClick={addText}>Add to knowledge</button>
            </div>
          </div>
        )}
      </div>

      <div className="sim">
        <div className="sim-label">Knowledge base · {state.kbItems.length} item{state.kbItems.length === 1 ? "" : "s"}</div>
        {state.kbItems.length === 0 && <div className="muted" style={{ fontSize: 14, padding: "16px 0" }}>Empty — add at least 2 sources above.</div>}
        <div className="col">
          {state.kbItems.map((item, i) => {
            const IconCmp = item.type === "url" ? window.I.Link : item.type === "file" ? window.I.Upload : window.I.Edit;
            return (
              <div key={i} className="tool-tile" style={{ cursor: "default" }}>
                <div className="tool-logo" style={{ background: "color-mix(in oklab, var(--accent) 16%, white)", color: "var(--accent)" }}>
                  <IconCmp />
                </div>
                <div style={{ flex: 1 }}>
                  <div className="tool-name">{item.label}</div>
                  <div className="tool-sub">{item.type === "url" ? "Crawled · chunked" : item.type === "file" ? `${item.kind} · ${item.content ? "indexed · " + item.content.length + " chars" : "⚠ no text extracted — remove & re-upload"}` : "Pasted note · indexed"}</div>
                </div>
                <button className="btn btn-ghost" style={{ padding: "4px 8px", fontSize: 12 }} onClick={() => remove(i)}>Remove</button>
              </div>
            );
          })}
        </div>
      </div>

      <div className="row between mt-6">
        <div className="muted" style={{ fontSize: 13 }}>
          {ready ? "✓ Looking good" : `${state.kbItems.length}/2 minimum`}
        </div>
        <button className="btn btn-primary" disabled={!ready} onClick={next}>
          Give it hands <window.I.ArrowRight />
        </button>
      </div>
    </>
  );
}

function Lesson4Side() {
  return (
    <>
      <Tips title="Real starter templates" icon="📥">
        <p>We've prepared three dental-practice starter docs you can download, customize, and re-upload to your real voice agent platform.</p>
        <div className="col" style={{ gap: 6, marginTop: 10 }}>
          <button className="btn" onClick={() => window.downloadTemplate("appointment-policy.md")} style={{ padding: "8px 10px", fontSize: 12, justifyContent: "flex-start" }}>
            <window.I.Upload style={{ transform: "rotate(180deg)" }} /> appointment-policy.md
          </button>
          <button className="btn" onClick={() => window.downloadTemplate("services.md")} style={{ padding: "8px 10px", fontSize: 12, justifyContent: "flex-start" }}>
            <window.I.Upload style={{ transform: "rotate(180deg)" }} /> services.md
          </button>
          <button className="btn" onClick={() => window.downloadTemplate("insurance-faq.md")} style={{ padding: "8px 10px", fontSize: 12, justifyContent: "flex-start" }}>
            <window.I.Upload style={{ transform: "rotate(180deg)" }} /> insurance-faq.md
          </button>
        </div>
      </Tips>
      <Tips title="What to upload" icon="📚">
        <ul className="tips-list">
          <li>FAQ documents (highest ROI)</li>
          <li>Pricing pages, service menus</li>
          <li>SOPs, scripts, escalation rules</li>
          <li>Policies — refunds, hours, locations</li>
        </ul>
      </Tips>
      <Tips title="Don't upload" icon="🚫">
        <ul className="tips-list">
          <li>Customer PII (PCI, HIPAA risk)</li>
          <li>Anything you wouldn't repeat to a stranger on a call</li>
          <li>Marketing copy without the facts</li>
        </ul>
      </Tips>
    </>
  );
}

// ============================================================
// MODULE 5 — Connect Tools
// ============================================================
const TOOLS = [
  { id: "salesforce", name: "Salesforce", category: "CRM", bg: "#00a1e0", initials: "SF",
    tier: "paid", priceNote: "From $25/user/mo (30-day trial)",
    connect: "OAuth", connectUrl: "https://login.salesforce.com",
    setup: [
      "In your voice platform → Tools → Add → search Salesforce.",
      "Click Connect → sign in to your Salesforce org → approve scopes.",
      "Map the agent action (e.g. lookupContact) to Salesforce Contact + AccountId fields.",
      "Test in the tool playground before exposing to live calls.",
    ],
    docs: "https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_what_is_rest_api.htm" },

  { id: "hubspot", name: "HubSpot", category: "CRM", bg: "#ff7a59", initials: "HS",
    tier: "free", priceNote: "Free CRM tier — unlimited contacts",
    connect: "OAuth", connectUrl: "https://app.hubspot.com/signup-hubspot/crm",
    setup: [
      "Create a free HubSpot account at hubspot.com/products/get-started.",
      "In your voice platform → Tools → HubSpot → Connect via OAuth.",
      "Grant Contacts + Deals + Tickets scopes.",
      "Define tool: upsertContact(name, email, phone, source).",
    ],
    docs: "https://developers.hubspot.com/docs/api/overview" },

  { id: "gcal", name: "Google Calendar", category: "Scheduling", bg: "#4285f4", initials: "GC",
    tier: "free", priceNote: "Completely free with any Google account",
    connect: "OAuth", connectUrl: "https://calendar.google.com",
    setup: [
      "In your voice platform → Tools → Google Calendar → Connect.",
      "Sign in with the Google account whose calendar should receive bookings.",
      "Grant 'See, edit, share, and permanently delete all calendars' scope.",
      "Define tools: listAvailability(date, durationMin), createEvent(start, end, attendee, summary).",
      "Always pass attendee email so the user gets the calendar invite.",
    ],
    docs: "https://developers.google.com/calendar/api/quickstart/js" },

  { id: "calendly", name: "Calendly", category: "Scheduling", bg: "#006bff", initials: "CV",
    tier: "freemium", priceNote: "Free plan (1 event type) · Pro $12/mo",
    connect: "Personal Access Token", connectUrl: "https://calendly.com/integrations/api_webhooks",
    setup: [
      "Get a Personal Access Token at calendly.com → Integrations → API & Webhooks.",
      "In your voice platform → Tools → Custom Webhook → paste the token.",
      "Define listEventTypes() and scheduleEvent(eventTypeUuid, inviteeName, inviteeEmail, startTime).",
      "Pro tip: use Calendly's 'Single Use' scheduling links for high-value callers.",
    ],
    docs: "https://developer.calendly.com/api-docs" },

  { id: "zendesk", name: "Zendesk", category: "Support", bg: "#03363d", initials: "ZD",
    tier: "paid", priceNote: "From $19/agent/mo (14-day trial)",
    connect: "API Token", connectUrl: "https://www.zendesk.com/register/",
    setup: [
      "In Zendesk Admin → Apps and integrations → APIs → Add API Token.",
      "In your voice platform → Tools → Zendesk → paste subdomain + email + token.",
      "Define createTicket(subject, body, requesterEmail, priority).",
      "Add escalation rule: if call sentiment turns negative → auto-create P1 ticket.",
    ],
    docs: "https://developer.zendesk.com/api-reference/" },

  { id: "intercom", name: "Intercom", category: "Support", bg: "#1f8ded", initials: "IC",
    tier: "paid", priceNote: "From $39/mo (14-day trial)",
    connect: "OAuth", connectUrl: "https://www.intercom.com/start/free-trial",
    setup: [
      "In Intercom → Settings → Developers → New app → install on workspace.",
      "Copy Access Token; paste into voice platform → Tools → Intercom.",
      "Define createConversation(userId, body) and tagUser(userId, tag).",
      "Route post-call summaries into Intercom as a conversation note for human follow-up.",
    ],
    docs: "https://developers.intercom.com/docs" },

  { id: "stripe", name: "Stripe", category: "Payments", bg: "#635bff", initials: "$",
    tier: "free", priceNote: "Free setup · 2.9% + 30¢ per successful charge",
    connect: "API Key", connectUrl: "https://dashboard.stripe.com/register",
    setup: [
      "Create a Stripe account → Dashboard → Developers → API keys.",
      "Copy the Restricted key (NOT the secret key) with payment_intents:write scope.",
      "In voice platform → Tools → Stripe → paste key.",
      "Define createPaymentLink(amount, description, customerEmail) — agent reads link aloud or SMSes it.",
      "NEVER let the agent read CVV/full card numbers — use payment links only.",
    ],
    docs: "https://stripe.com/docs/api" },

  { id: "shopify", name: "Shopify", category: "Payments", bg: "#7ab55c", initials: "SH",
    tier: "paid", priceNote: "From $29/mo · 3-day free trial",
    connect: "OAuth", connectUrl: "https://www.shopify.com/free-trial",
    setup: [
      "In Shopify Admin → Apps → Develop apps → Create app.",
      "Configure Admin API scopes: read_orders, write_customers, read_products.",
      "Install the app on your store, copy Admin API access token.",
      "Define lookupOrder(orderNumber), trackShipment(orderId), createDraftOrder(items, customerEmail).",
    ],
    docs: "https://shopify.dev/docs/api/admin-rest" },

  { id: "twilio", name: "Twilio", category: "Telephony", bg: "#f22f46", initials: "TW",
    tier: "freemium", priceNote: "$15 free trial credit · ~1¢/min after",
    connect: "Account SID + Auth Token", connectUrl: "https://www.twilio.com/try-twilio",
    setup: [
      "Sign up at twilio.com (free trial includes ~$15 credit + a verified test number).",
      "Console → Account → Auth Token & SID. Copy both.",
      "Console → Phone Numbers → Buy a number (~$1.15/mo).",
      "In voice platform → Phone Numbers → Add Twilio number → paste SID/token.",
      "Point the number's Voice webhook at your platform's inbound URL — calls now go live.",
    ],
    docs: "https://www.twilio.com/docs/voice" },

  { id: "whatsapp", name: "WhatsApp", category: "Telephony", bg: "#25d366", initials: "WA",
    tier: "freemium", priceNote: "1,000 free service conversations/mo via Meta",
    connect: "Meta Business / Twilio", connectUrl: "https://business.facebook.com/",
    setup: [
      "Easiest path: enable WhatsApp on an existing Twilio number (5 min).",
      "Harder/cheaper: register directly with Meta Business → WhatsApp Business Platform.",
      "Define sendWhatsappMessage(to, templateName, vars) — must use approved templates for outbound.",
      "Inbound is free-form for 24h after a user messages you.",
    ],
    docs: "https://developers.facebook.com/docs/whatsapp" },

  { id: "zapier", name: "Zapier", category: "Automation", bg: "#ff4f00", initials: "ZP",
    tier: "freemium", priceNote: "Free: 100 tasks/mo · Starter $19.99/mo",
    connect: "Webhook", connectUrl: "https://zapier.com/sign-up",
    setup: [
      "Create a Zap → Trigger = 'Webhooks by Zapier' → 'Catch Hook'. Copy the URL.",
      "In voice platform → Tools → Custom Webhook → paste URL → name it trigger_zap.",
      "Define args the agent should pass (caller_name, intent, transcript).",
      "Pick any of 6,000+ apps as the action — Slack alert, Notion row, Airtable record, etc.",
    ],
    docs: "https://zapier.com/help/create/code-webhooks/trigger-zaps-from-webhooks" },

  { id: "n8n", name: "n8n", category: "Automation", bg: "#ea4b71", initials: "N8",
    tier: "free", priceNote: "Free self-hosted · Cloud €20/mo for managed",
    connect: "Webhook", connectUrl: "https://n8n.io/",
    setup: [
      "Self-host: docker run -p 5678:5678 n8nio/n8n (free forever).",
      "Create workflow → Webhook trigger → copy the production URL.",
      "In voice platform → Tools → Custom Webhook → paste URL.",
      "Add downstream nodes: Slack, Postgres, send-email, OpenAI summarization — whatever you need.",
      "Best free choice if you want full control and no per-task limits.",
    ],
    docs: "https://docs.n8n.io/" },
];

function Lesson5({ state, setState, next }) {
  const [expanded, setExpanded] = useState(null);

  const toggle = (id) => {
    const on = state.tools.includes(id);
    setState({ tools: on ? state.tools.filter(t => t !== id) : [...state.tools, id] });
  };
  const ready = state.tools.length >= 2;

  const tierLabel = (tier) => {
    if (tier === "free")     return { label: "FREE",      cls: "tier-free" };
    if (tier === "freemium") return { label: "FREEMIUM",  cls: "tier-freemium" };
    return                          { label: "PAID",      cls: "tier-paid" };
  };

  return (
    <>
      <LessonStep tag="Module 5 · Tools" title="Make it do things during calls."
        prose="A voice agent without tools is just a smart FAQ. Pick at least two — and click any tile's ‹ + › to see the real setup steps and pricing. Most platforms (ElevenLabs Agents, Vapi, Retell) follow the same pattern." />

      <div className="sim">
        <div className="row between" style={{ marginBottom: 8 }}>
          <div className="sim-label" style={{ margin: 0 }}>Available integrations · {state.tools.length} connected</div>
          <div className="tier-legend">
            <span className="tier-pill tier-free">FREE</span>
            <span className="tier-pill tier-freemium">FREEMIUM</span>
            <span className="tier-pill tier-paid">PAID</span>
          </div>
        </div>
        <div className="tool-grid">
          {TOOLS.map(t => {
            const on = state.tools.includes(t.id);
            const isOpen = expanded === t.id;
            const tier = tierLabel(t.tier);
            return (
              <div key={t.id} className={"integration-card" + (on ? " on" : "") + (isOpen ? " open" : "")}>
                <div className="integration-head">
                  <button className="integration-toggle" onClick={() => toggle(t.id)}>
                    <div className="tool-logo" style={{ background: t.bg }}>{t.initials}</div>
                    <div style={{ flex: 1, minWidth: 0, textAlign: "left" }}>
                      <div className="row" style={{ gap: 6, alignItems: "center" }}>
                        <div className="tool-name">{t.name}</div>
                        <span className={"tier-pill " + tier.cls}>{tier.label}</span>
                      </div>
                      <div className="tool-sub">{t.category} · {t.priceNote}</div>
                    </div>
                    <div className="tool-check">{on && <window.I.Check />}</div>
                  </button>
                  <button className="integration-expand"
                    onClick={() => setExpanded(isOpen ? null : t.id)}
                    aria-label={isOpen ? "Hide setup" : "Show setup"}>
                    {isOpen
                      ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"><path d="M5 12h14" /></svg>
                      : <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"><path d="M12 5v14M5 12h14" /></svg>}
                  </button>
                </div>
                {isOpen && (
                  <div className="integration-body">
                    <div className="integration-meta">
                      <div><span className="tag">Auth</span> {t.connect}</div>
                      <a href={t.connectUrl} target="_blank" rel="noopener" className="integration-link">
                        Sign up / sign in <window.I.ArrowRight />
                      </a>
                      <a href={t.docs} target="_blank" rel="noopener" className="integration-link">
                        API docs <window.I.ArrowRight />
                      </a>
                    </div>
                    <div className="integration-steps-title">How to wire this up</div>
                    <ol className="integration-steps">
                      {t.setup.map((step, i) => <li key={i}>{step}</li>)}
                    </ol>
                  </div>
                )}
              </div>
            );
          })}
        </div>
      </div>

      <div className="sim">
        <div className="sim-label">What the agent can do once connected</div>
        <div className="col">
          {[
            state.tools.includes("gcal") || state.tools.includes("calendly") ? "Book and reschedule appointments end-to-end" : null,
            state.tools.includes("salesforce") || state.tools.includes("hubspot") ? "Pull caller history, log new contacts, update deal stage" : null,
            state.tools.includes("stripe") || state.tools.includes("shopify") ? "Take payment over the phone, check order status" : null,
            state.tools.includes("twilio") || state.tools.includes("whatsapp") ? "Make/receive calls, send confirmations via SMS" : null,
            state.tools.includes("zapier") || state.tools.includes("n8n") ? "Trigger any downstream workflow on call events" : null,
            state.tools.includes("zendesk") || state.tools.includes("intercom") ? "Open tickets and route to humans" : null,
          ].filter(Boolean).map((line, i) => (
            <div key={i} className="row" style={{ fontSize: 14 }}>
              <div style={{ width: 22, height: 22, borderRadius: 6, background: "var(--accent)", color: "white", display: "grid", placeItems: "center", flexShrink: 0 }}>
                <window.I.Check />
              </div>
              <div>{line}</div>
            </div>
          ))}
          {state.tools.length === 0 && <div className="muted" style={{ fontSize: 14 }}>Pick a couple of integrations above to see what unlocks.</div>}
        </div>
      </div>

      <div className="row between mt-6">
        <div className="muted" style={{ fontSize: 13 }}>{ready ? "✓ Tooled up" : `${state.tools.length}/2 minimum`}</div>
        <button className="btn btn-primary" disabled={!ready} onClick={next}>
          Test it before launch <window.I.ArrowRight />
        </button>
      </div>
    </>
  );
}

function Lesson5Side() {
  return (
    <>
      <Tips title="The $0 starter stack" icon="💸">
        <ul className="tips-list">
          <li><strong>HubSpot Free</strong> — CRM</li>
          <li><strong>Google Calendar</strong> — scheduling</li>
          <li><strong>Stripe</strong> — payments (free until you charge)</li>
          <li><strong>n8n self-hosted</strong> — workflows</li>
          <li><strong>Twilio trial</strong> — phone ($15 credit)</li>
        </ul>
        <p style={{ marginTop: 8 }}>Total setup cost: <strong>$0</strong>. You pay only for LLM/TTS minutes (~$0.06–$0.15/min) once you exceed your platform's free tier.</p>
      </Tips>
      <Tips title="Tool-calling 101" icon="🔧">
        <p>The LLM doesn't connect to your tools directly — it decides <em>when</em> to call a tool, fills in the arguments, and the agent runtime makes the API call. Then it speaks the result.</p>
      </Tips>
      <Tips title="Permission scope" icon="🔒">
        <p>Start read-only. Once you trust the agent, grant write access. Most platforms let you require human confirmation for high-stakes calls (charges, cancellations).</p>
      </Tips>
    </>
  );
}

// ============================================================
// MODULE 6 — Test Before Launch
// ============================================================
const TEST_SCENARIOS = [
  { id: "happy", name: "Happy path: a typical request", expected: "Helpful · on-topic · follows your prompt",
    lines: ["Hi! I had a quick question — can you help me?", "What exactly can you help me with?"] },
  { id: "objection", name: "Caller pushes back", expected: "Empathizes · doesn't get defensive",
    lines: ["Honestly, I'd rather talk to a real person. Why should I talk to a bot?"] },
  { id: "wrong", name: "Wrong number / off-topic", expected: "Politely redirects or ends fast",
    lines: ["Is this Bob's Tires? I need a quote on some all-seasons."] },
  { id: "lang", name: "Caller switches to Spanish mid-call", expected: "Switches language if enabled",
    lines: ["Hola, ¿hablan español? Necesito ayuda con algo."] },
];

function buildAgentSystem(state) {
  const parts = [];
  parts.push(state.systemPrompt && state.systemPrompt.trim()
    ? state.systemPrompt.trim()
    : "You are a helpful voice agent. (The builder has not written a system prompt yet — mention this once, gently.)");
  parts.push("\n--- Runtime context (from the builder's configuration) ---");
  if (state.voice) parts.push(`Your voice preset is \"${state.voice}\".`);
  if (state.languages && state.languages.length) parts.push(`Languages you support: ${state.languages.join(", ")}. If the caller speaks one of these, respond in it. If they speak a language NOT in this list, politely say you can't support it.`);
  const kbDocs = (state.kbItems || []).filter(k => k.content);
  if (kbDocs.length) {
    parts.push("\n--- Knowledge base (uploaded by the builder — treat as your source of truth) ---");
    kbDocs.forEach(k => parts.push(`\n[${k.label}]\n${k.content}`));
  }
  parts.push("This is a simulated PHONE call. Reply as the agent would speak aloud: conversational, warm, and under 40 words per turn. Never use markdown, lists, or emoji. If the caller has the wrong number or is off-topic, politely wrap up quickly.");
  return parts.join("\n");
}

function Lesson6({ state, setState, next }) {
  const [running, setRunning] = useState(null);   // scenario id currently auto-running
  const [thinking, setThinking] = useState(false); // waiting on Claude
  const [transcript, setTranscript] = useState([]);
  const [results, setResults] = useState(state.testResults || {});
  const [draft, setDraft] = useState("");
  const [voiceOn, setVoiceOn] = useState(true);
  const voiceOnRef = useRef(true);
  voiceOnRef.current = voiceOn;
  const historyRef = useRef([]); // [{role:'user'|'assistant', content}]

  const speakReply = (text) => {
    if (!voiceOnRef.current) return;
    const v = VOICES.find(x => x.id === state.voice) || VOICES[0];
    const key = localStorage.getItem(EL_KEY_STORAGE);
    if (key) {
      speakViaElevenLabs(v, key, null, (res) => {
        // fall back to browser TTS if ElevenLabs rejects (bad key/permissions)
        if (res && res.error) speakSample(v, null, null, text);
      }, text);
    } else {
      speakSample(v, null, null, text);
    }
  };

  const askAgent = async () => {
    setThinking(true);
    try {
      const text = await window.claude.complete({
        system: buildAgentSystem(state),
        messages: historyRef.current,
        max_tokens: 300,
      });
      historyRef.current = [...historyRef.current, { role: "assistant", content: text }];
      setTranscript(t => [...t, { who: "agent", text }]);
      speakReply(text);
      return true;
    } catch (e) {
      setTranscript(t => [...t, { who: "system", text: "⚠ Agent error: " + (e.message || e) + " — try again in a moment." }]);
      return false;
    } finally {
      setThinking(false);
    }
  };

  const sayAsCaller = (text) => {
    historyRef.current = [...historyRef.current, { role: "user", content: text }];
    setTranscript(t => [...t, { who: "user", text }]);
  };

  const runScenario = async (id) => {
    const s = TEST_SCENARIOS.find(x => x.id === id);
    setRunning(id);
    historyRef.current = [];
    setTranscript([{ who: "system", text: "📞 Live call connecting — your agent is running on your Module 3 system prompt…" }]);
    let ok = true;
    for (const line of s.lines) {
      sayAsCaller(line);
      ok = await askAgent();
      if (!ok) break;
    }
    if (ok) {
      const r = { ...results, [id]: "passed" };
      setResults(r);
      setState({ testResults: r });
      setTranscript(t => [...t, { who: "system", text: "✓ Scenario complete — keep typing below to continue the call, or run another." }]);
    }
    setRunning(null);
  };

  const sendDraft = async () => {
    const text = draft.trim();
    if (!text || thinking || running) return;
    setDraft("");
    if (transcript.length === 0) setTranscript([{ who: "system", text: "📞 Live call connecting…" }]);
    sayAsCaller(text);
    await askAgent();
  };

  const passedCount = Object.values(results).filter(v => v === "passed").length;
  const ready = passedCount >= 2;

  return (
    <>
      <LessonStep tag="Module 6 · Test" title="Don't ship it blind."
        prose="These calls are LIVE — the agent runs on the system prompt, voice, and languages you configured in Module 3. Run at least two scenarios, then keep typing as the caller to probe it yourself." />

      {!(state.systemPrompt && state.systemPrompt.trim()) && (
        <div className="sim" style={{ borderColor: "var(--accent)" }}>
          <div style={{ fontSize: 13 }}>⚠ You haven't written a system prompt yet — go back to <strong>Module 3</strong> first, or these tests will run a generic agent.</div>
        </div>
      )}

      <div className="sim">
        <div className="sim-label">Scenarios to run</div>
        <div className="col">
          {TEST_SCENARIOS.map(s => {
            const done = results[s.id] === "passed";
            const isRunning = running === s.id;
            return (
              <div key={s.id} className="tool-tile" style={{ cursor: "default", borderColor: done ? "var(--accent)" : "var(--line)", background: done ? "color-mix(in oklab, var(--accent) 6%, white)" : "var(--paper)" }}>
                <div className="tool-logo" style={{ background: done ? "var(--accent)" : "color-mix(in oklab, var(--accent) 14%, white)", color: done ? "white" : "var(--accent)" }}>
                  {done ? <window.I.Check /> : <window.I.Play />}
                </div>
                <div style={{ flex: 1 }}>
                  <div className="tool-name">{s.name}</div>
                  <div className="tool-sub">Expects: {s.expected}</div>
                </div>
                <button className="btn" disabled={isRunning || !!running || thinking}
                  onClick={() => runScenario(s.id)}>
                  {done ? "Re-run" : isRunning ? "Running…" : "Run"}
                </button>
              </div>
            );
          })}
        </div>
      </div>

      <div className="sim">
        <div className="row between">
          <div className="sim-label">Live transcript {(running || thinking) && <span style={{ color: "var(--accent)" }}>· live</span>}</div>
          <button className={"chip" + (voiceOn ? " on" : "")} style={{ fontSize: 12 }}
            onClick={() => { setVoiceOn(o => !o); if (voiceOn) { window.speechSynthesis && window.speechSynthesis.cancel(); } }}>
            {voiceOn ? "🔊 Voice on" : "🔇 Voice off"}
          </button>
        </div>
        <div className="chat">
          {transcript.length === 0 && !running && (
            <div className="bubble system">Run a scenario above — or just type below to call your agent directly.</div>
          )}
          {transcript.map((line, i) => (
            <div key={i} className={"bubble " + line.who}>{line.text}</div>
          ))}
          {thinking && (
            <div className="bubble agent" style={{ width: "auto" }}>
              <div className="typing"><span /><span /><span /></div>
            </div>
          )}
        </div>
        <div className="row mt-2" style={{ gap: 8 }}>
          <input className="input" style={{ flex: 1 }}
            placeholder="Type as the caller — e.g. 'What is INFO 399 about?'"
            value={draft}
            onChange={e => setDraft(e.target.value)}
            onKeyDown={e => { if (e.key === "Enter") sendDraft(); }}
            disabled={!!running} />
          <button className="btn btn-primary" onClick={sendDraft} disabled={!draft.trim() || thinking || !!running}>
            Send
          </button>
        </div>
      </div>

      <div className="row between mt-6">
        <div className="muted" style={{ fontSize: 13 }}>
          {ready ? `✓ ${passedCount}/4 passed — ready to launch` : `${passedCount}/2 minimum`}
        </div>
        <button className="btn btn-primary" disabled={!ready} onClick={next}>
          Launch <window.I.ArrowRight />
        </button>
      </div>
    </>
  );
}

function Lesson6Side() {
  return (
    <>
      <Tips title="What we check" icon="🔍">
        <ul className="tips-list">
          <li>Did it follow the system prompt?</li>
          <li>Did it call the right tools, with right args?</li>
          <li>Did it stay under turn-length limits?</li>
          <li>Did it escalate when it should?</li>
        </ul>
      </Tips>
      <Tips title="Beyond simulations" icon="📞">
        <p>Once you ship, every real call gets transcribed, scored, and flagged if it deviates. Most teams catch issues within the first 50 calls and iterate.</p>
      </Tips>
    </>
  );
}

// ============================================================
// MODULE 7 — Launch
// ============================================================
const LAUNCH_TEMPLATES = [
  { id: "support",    name: "24/7 Customer Support",     desc: "Handle tickets, FAQs, and order status overnight.",     icon: window.I.Headset,    hue: "#06b6d4" },
  { id: "front",      name: "Receptionist that books",   desc: "Answer the phone, qualify, book straight to calendar.", icon: window.I.Calendar,   hue: "#7c3aed" },
  { id: "leadgen",    name: "Lead Qualification Agent",  desc: "Outbound or inbound — score leads, hand off hot ones.", icon: window.I.Filter,     hue: "#f59e0b" },
  { id: "outbound",   name: "Outbound Sales Rep",        desc: "Cold calls with empathy. Tracks objections.",            icon: window.I.Megaphone,  hue: "#ef4444" },
  { id: "personal",   name: "Personal Voice Assistant",  desc: "Errands, scheduling, reminders, smart filters.",         icon: window.I.User,       hue: "#84cc16" },
  { id: "concierge",  name: "Concierge / Reservations",  desc: "Restaurant, hotel, services — natural and unhurried.",   icon: window.I.Sparkle,    hue: "#ff5b9a" },
];

function Lesson7({ state, setState, complete }) {
  const [launched, setLaunched] = useState(false);
  return (
    <>
      <LessonStep tag="Module 7 · Launch" title="What will you build?"
        prose="You've built one agent — but this stack runs anything voice-shaped. Pick the role you'll deploy first. (You can always run multiple agents from the same dashboard.)" />

      <div className="sim">
        <div className="sim-label">Pick your launch template</div>
        <div className="tool-grid">
          {LAUNCH_TEMPLATES.map(t => {
            const Icon = t.icon;
            const on = state.launchPick === t.id;
            return (
              <button key={t.id} className={"tool-tile" + (on ? " on" : "")} onClick={() => setState({ launchPick: t.id })}
                style={{ "--accent": t.hue }}>
                <div className="tool-logo" style={{ background: t.hue }}><Icon /></div>
                <div>
                  <div className="tool-name">{t.name}</div>
                  <div className="tool-sub">{t.desc}</div>
                </div>
                <div className="tool-check">{on && <window.I.Check />}</div>
              </button>
            );
          })}
        </div>
      </div>

      {launched ? (
        <div className="completion-banner">
          <h2>🎉 Your agent is live.</h2>
          <p>It's answering calls. You've gone from zero to a deployed voice AI in roughly 30 minutes.</p>
          <button className="btn" style={{ background: "white", color: "var(--ink)" }} onClick={complete}>
            Finish tutorial <window.I.Check />
          </button>
        </div>
      ) : (
        <div className="row between mt-6">
          <div className="muted" style={{ fontSize: 13 }}>{state.launchPick ? "Ready when you are." : "Pick a template to launch."}</div>
          <button className="btn btn-primary" disabled={!state.launchPick} onClick={() => setLaunched(true)}>
            <window.I.Rocket /> Deploy agent
          </button>
        </div>
      )}
    </>
  );
}

function Lesson7Side({ state }) {
  return (
    <>
      <Tips title="Before you tell the world" icon="📋">
        <ul className="tips-list">
          <li>Disclose AI on greeting (in many states it's required)</li>
          <li>Always offer a human off-ramp</li>
          <li>Set call-volume caps for week 1</li>
          <li>Review the first 50 transcripts manually</li>
        </ul>
      </Tips>
      <Tips title="What's next" icon="🚀">
        <p>Iterate on the system prompt weekly. Mine the transcripts. Add tools as new patterns emerge. The best agents get rebuilt 3× in the first month.</p>
      </Tips>
    </>
  );
}

// ============================================================
// Registry
// ============================================================
window.LESSONS = {
  1: { Main: Lesson1, Side: Lesson1Side },
  2: { Main: Lesson2, Side: Lesson2Side },
  3: { Main: Lesson3, Side: Lesson3Side },
  4: { Main: Lesson4, Side: Lesson4Side },
  5: { Main: Lesson5, Side: Lesson5Side },
  6: { Main: Lesson6, Side: Lesson6Side },
  7: { Main: Lesson7, Side: Lesson7Side },
};
