const { useState, useEffect, useRef, useCallback } = React;

// ttsProxyUrl: set to your deployed Cloudflare Worker URL (see workers/tts-proxy.js).
// Leave empty to fall back to the browser's speechSynthesis.
const TWEAKS = /*EDITMODE-BEGIN*/{
  "theme": "ivory",
  "timelineMode": "compact",
  "accentHue": 150,
  "showStarters": true,
  "voiceOn": true,
  "persona": "measured",
  "voiceURI": "",
  "pitchOverride": null,
  "rateOverride": null,
  "ttsProxyUrl": "https://tts-proxy.jonathankosar.workers.dev",
  "ttsVoice": "onyx"
}/*EDITMODE-END*/;

const PERSONAS = {
  measured: {
    label: "Measured",
    blurb: "Calm, deliberate, dry.",
    pitch: 0.55,
    rate: 0.82,
    voiceMatch: /daniel.*enhanced|alex|ralph.*premium|reed.*premium|rocko.*premium|enhanced|premium|neural|daniel|fred|google uk english male|microsoft david|microsoft mark|male/i,
    instructions: "Speak measured, deliberate, with quiet authority. Calm pace. A touch dry. Let the specifics — numbers, years, projects — land cleanly. Never rushed, never preamble.",
  },
  baritone: {
    label: "Cinematic",
    blurb: "Deep, slow, weighty.",
    pitch: 0.3,
    rate: 0.74,
    voiceMatch: /reed.*premium|rocko.*premium|ralph.*premium|daniel.*enhanced|fred|alex|premium|enhanced|microsoft david|male/i,
    instructions: "Speak with cinematic gravitas. Low, slow, resonant. Brief pauses between thoughts. Each word carries weight. Think narrator of a serious documentary.",
  },
  dry: {
    label: "Dry-witted",
    blurb: "Understated, wry.",
    pitch: 0.85,
    rate: 0.95,
    voiceMatch: /daniel.*enhanced|alex|enhanced|premium|neural|daniel|google uk english male|microsoft david|male/i,
    instructions: "Speak understated and faintly amused. Wry, matter-of-fact delivery. Mild dry humor without forcing it. Treat impressive things casually. Even pace.",
  },
  warm: {
    label: "Warm",
    blurb: "Friendly, grounded.",
    pitch: 1.0,
    rate: 0.95,
    voiceMatch: /samantha.*enhanced|samantha|allison|ava|karen|kate|moira|tessa|enhanced|premium|google uk english female|microsoft zira|female/i,
    instructions: "Warm, grounded, conversational. Natural contractions. Talk like you're explaining something to a smart friend over coffee. Genuine, never performative.",
  },
  newscaster: {
    label: "Newscaster",
    blurb: "Crisp, articulate.",
    pitch: 0.95,
    rate: 1.0,
    voiceMatch: /serena.*premium|serena|kate|enhanced|premium|google uk english female|microsoft zira|female/i,
    instructions: "Crisp, articulate, broadcast cadence. Each sentence stands cleanly on its own. Confident pace, clean enunciation. Lead with the headline.",
  }
};

// Two themes only: Ivory (light) and Obsidian (dark).
// On first visit (no localStorage), the OS prefers-color-scheme picks one.
const THEMES = [
  { id: "ivory",    label: "Light · Ivory",    bg: "#f4f0e6", ink: "#1a1814" },
  { id: "obsidian", label: "Dark · Obsidian",  bg: "#16140f", ink: "#e8e2d2" },
];

function detectSystemTheme() {
  try {
    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
      return "obsidian";
    }
  } catch (e) {}
  return "ivory";
}

const LS_KEY = "resume-settings-v1";
function loadUserSettings() {
  try { return JSON.parse(localStorage.getItem(LS_KEY) || "{}"); } catch (e) { return {}; }
}

function useSystemVoices() {
  const [voices, setVoices] = useState([]);
  useEffect(() => {
    if (typeof window.speechSynthesis === 'undefined') return;
    const load = () => setVoices(window.speechSynthesis.getVoices() || []);
    load();
    window.speechSynthesis.onvoiceschanged = load;
    return () => { try { window.speechSynthesis.onvoiceschanged = null; } catch(e){} };
  }, []);
  return voices;
}

function useTweaks() {
  const [tweaks, setTweaks] = useState(() => {
    const saved = loadUserSettings();
    // First-time visitors fall through to the OS-level color preference.
    // Returning visitors keep whatever they last picked.
    const base = { ...TWEAKS };
    if (!saved.theme) base.theme = detectSystemTheme();
    return { ...base, ...saved };
  });
  const [open, setOpen] = useState(false);

  // Mirror theme onto <html> so body and edges pick up the right background.
  useEffect(() => {
    document.documentElement.setAttribute('data-theme', tweaks.theme || 'ivory');
  }, [tweaks.theme]);

  useEffect(() => {
    const onMsg = (e) => {
      const d = e.data || {};
      if (d.type === "__activate_edit_mode") { setOpen(true); }
      if (d.type === "__deactivate_edit_mode") { setOpen(false); }
    };
    window.addEventListener("message", onMsg);
    if (typeof window.speechSynthesis !== 'undefined') {
      window.speechSynthesis.getVoices();
      window.speechSynthesis.onvoiceschanged = () => window.speechSynthesis.getVoices();
    }
    window.parent.postMessage({ type: "__edit_mode_available" }, "*");
    return () => window.removeEventListener("message", onMsg);
  }, []);

  const update = (patch) => {
    setTweaks(prev => {
      const next = { ...prev, ...patch };
      try { localStorage.setItem(LS_KEY, JSON.stringify(next)); } catch (e) {}
      window.parent.postMessage({ type: "__edit_mode_set_keys", edits: patch }, "*");
      return next;
    });
  };

  return { tweaks, update, open, setOpen };
}

async function askAI(messages, persona) {
  try {
    const r = await fetch("/api/chat", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ messages, persona })
    });
    if (!r.ok) throw new Error("chat " + r.status);
    const data = await r.json();
    return (data.reply || "").trim();
  } catch (e) {
    return "Signal lost. Try asking again.";
  }
}

// Parse the model reply into:
//   spoken    — text without {{cite}} markers or FOLLOW: lines
//   cites     — ordered cite ids
//   positions — char offsets in `spoken` where each cite appeared
//   followups — array of suggested follow-up questions
function parseCitations(text) {
  // Pull FOLLOW: lines off the end first
  const followups = [];
  const lines = text.split(/\r?\n/);
  while (lines.length && /^\s*FOLLOW:\s*/i.test(lines[lines.length - 1])) {
    const q = lines.pop().replace(/^\s*FOLLOW:\s*/i, "").trim().replace(/^["']|["']$/g, "");
    if (q) followups.unshift(q);
  }
  // Also strip any FOLLOW: lines mid-text (model may slip them in earlier)
  const body = lines.filter(l => !/^\s*FOLLOW:\s*/i.test(l)).join("\n");

  const re = /\{\{([a-zA-Z0-9\-_]+)\}\}/g;
  const cites = [];
  const positions = [];
  let cleaned = "";
  let lastEnd = 0;
  let m;
  while ((m = re.exec(body)) !== null) {
    cleaned += body.slice(lastEnd, m.index);
    cites.push(m[1]);
    positions.push(cleaned.length);
    lastEnd = m.index + m[0].length;
  }
  cleaned += body.slice(lastEnd);
  return {
    spoken: cleaned.replace(/\s+/g, " ").trim(),
    cites,
    positions,
    followups: followups.slice(0, 4),
  };
}

const STARTERS = [
  "What does Jonathan do?",
  "Tell me about his experience",
  "What languages does he know?",
  "Any notable projects?"
];

function App() {
  const { tweaks, update, open, setOpen } = useTweaks();
  const voices = useSystemVoices();

  const previewVoice = useCallback(async (personaId, voiceURIOverride) => {
    if (typeof window.speechSynthesis !== 'undefined') {
      try { window.speechSynthesis.cancel(); } catch(e){}
    }
    try { window.PremiumTTS.cancel(); } catch(e){}
    const persona = PERSONAS[personaId] || PERSONAS.measured;
    const sample = "Hello. This is how Jonathan's resume sounds.";

    if (tweaks.ttsProxyUrl) {
      try { window.VoiceFX.unlock(); } catch(e){}
      const ok = await window.PremiumTTS.speak({
        text: sample,
        voice: tweaks.ttsVoice || "onyx",
        instructions: persona.instructions,
        proxyUrl: tweaks.ttsProxyUrl
      });
      if (ok) return;
    }

    if (typeof window.speechSynthesis === 'undefined') return;
    const u = new SpeechSynthesisUtterance(sample);
    u.pitch = tweaks.pitchOverride != null ? tweaks.pitchOverride : persona.pitch;
    u.rate = tweaks.rateOverride != null ? tweaks.rateOverride : persona.rate;
    const uri = voiceURIOverride !== undefined ? voiceURIOverride : tweaks.voiceURI;
    let chosen = null;
    if (uri) chosen = voices.find(v => v.voiceURI === uri);
    if (!chosen) chosen = voices.find(v => persona.voiceMatch.test(v.name)) || voices.find(v => /enhanced|premium|neural/i.test(v.name)) || voices[0];
    if (chosen) u.voice = chosen;
    try { window.VoiceFX.unlock(); } catch(e){}
    window.speechSynthesis.speak(u);
  }, [voices, tweaks.voiceURI, tweaks.pitchOverride, tweaks.rateOverride, tweaks.ttsProxyUrl, tweaks.ttsVoice]);

  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState("");
  const [state, setState] = useState("idle"); // idle | listening | thinking | speaking
  const [typing, setTyping] = useState("");
  const [activeCompany, setActiveCompany] = useState(null); // pulses timeline node
  const [activeCite, setActiveCite] = useState(null);       // resume-doc spotlight target
  const [allCites, setAllCites] = useState([]);             // cumulative ids referenced
  const [suggestions, setSuggestions] = useState([]);       // follow-up question buttons
  const [resumeOpen, setResumeOpen] = useState(false);
  const [resumeFullscreen, setResumeFullscreen] = useState(false);
  const citeTimersRef = useRef([]);

  const clearCiteTimers = useCallback(() => {
    citeTimersRef.current.forEach((t) => clearTimeout(t));
    citeTimersRef.current = [];
  }, []);

  // Schedule citation highlights to land at fractional points across durationMs.
  // positions[i] = char offset in spoken text where cite[i] should appear.
  const scheduleCites = useCallback((cites, positions, totalChars, durationMs) => {
    clearCiteTimers();
    if (!cites || cites.length === 0) { setActiveCite(null); return; }
    setAllCites(cites);
    setActiveCite(cites[0]);
    cites.forEach((id, i) => {
      const charPos = positions[i] || 0;
      const t = Math.max(0, (charPos / Math.max(1, totalChars)) * durationMs);
      const timer = setTimeout(() => setActiveCite(id), t);
      citeTimersRef.current.push(timer);
    });
    const endTimer = setTimeout(() => setActiveCite(null), durationMs + 1500);
    citeTimersRef.current.push(endTimer);
  }, [clearCiteTimers]);

  // Auto-open the resume panel the first time a citation fires.
  useEffect(() => {
    if (activeCite && !resumeOpen) setResumeOpen(true);
  }, [activeCite, resumeOpen]);
  const inputRef = useRef(null);
  const scrollRef = useRef(null);

  useEffect(() => {
    if (state === "thinking" || state === "speaking") return;
    if (document.activeElement === inputRef.current && input.length > 0) setState("listening");
    else setState("idle");
  }, [input, state]);

  useEffect(() => {
    if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
  }, [messages, typing]);

  const send = useCallback(async (text) => {
    if (!text.trim()) return;
    const userMsg = { role: "user", content: text.trim() };
    const next = [...messages, userMsg];
    setMessages(next);
    setInput("");
    setState("thinking");
    setTyping("");
    setSuggestions([]);
    try { window.speechSynthesis && window.speechSynthesis.cancel(); } catch (e) {}
    try { window.PremiumTTS.cancel(); } catch (e) {}

    const personaId = tweaks.persona || "measured";
    const persona = PERSONAS[personaId] || PERSONAS.measured;
    const rawReply = await askAI(next, personaId);

    // Strip citation markers and FOLLOW: lines before speech/typewriter.
    const { spoken: reply, cites, positions, followups } = parseCitations(rawReply);
    setSuggestions(followups);
    // Rough estimate of how long the reply will take to read aloud.
    // Tuned so a 3-sentence persona-styled answer (~180 chars) gets ~9s.
    const estDurationMs = Math.max(2000, reply.length * 55);
    scheduleCites(cites, positions, reply.length, estDurationMs);

    let speechStarted = false;
    if (tweaks.voiceOn && tweaks.ttsProxyUrl) {
      try { window.VoiceFX.unlock(); } catch (e) {}
      speechStarted = await window.PremiumTTS.speak({
        text: reply,
        voice: tweaks.ttsVoice || "onyx",
        instructions: persona.instructions,
        proxyUrl: tweaks.ttsProxyUrl,
        onStart: () => { try { window.VoiceFX.thinkPing(); window.VoiceFX.startAmbient(); } catch(e){} },
        onEnd:   () => { try { window.VoiceFX.stopAmbient(); } catch(e){}; setState("idle"); },
        onError: () => { try { window.VoiceFX.stopAmbient(); } catch(e){}; }
      });
    }

    let utter = null;
    if (tweaks.voiceOn && !speechStarted && typeof window.speechSynthesis !== 'undefined') {
      try {
        utter = new SpeechSynthesisUtterance(reply);
        utter.pitch = tweaks.pitchOverride != null ? tweaks.pitchOverride : persona.pitch;
        utter.rate = tweaks.rateOverride != null ? tweaks.rateOverride : persona.rate;
        utter.volume = 1;
        const sysVoices = window.speechSynthesis.getVoices();
        let chosen = null;
        if (tweaks.voiceURI) {
          chosen = sysVoices.find(v => v.voiceURI === tweaks.voiceURI);
        }
        if (!chosen) {
          const byName = (re) => sysVoices.find(v => re.test(v.name));
          chosen =
            byName(persona.voiceMatch) ||
            byName(/enhanced|premium|neural/i) ||
            sysVoices[0];
        }
        if (chosen) utter.voice = chosen;
        utter.onstart = () => { try { window.VoiceFX.thinkPing(); window.VoiceFX.startAmbient(); } catch(e){} };
        utter.onend   = () => { try { window.VoiceFX.stopAmbient(); } catch(e){}; setState("idle"); };
        utter.onerror = () => { try { window.VoiceFX.stopAmbient(); } catch(e){}; setState("idle"); };
        try { window.VoiceFX.unlock(); } catch(e){}
        window.speechSynthesis.speak(utter);
      } catch (e) { /* fall through to text-only */ }
    }

    // Find a timeline company mentioned in the reply and pulse its node.
    try {
      const roles = [
        ...((window.RESUME && window.RESUME.history) || []),
        ...((window.RESUME && window.RESUME.experience) || []),
      ];
      const hit = roles.find(r => r.company && reply.toLowerCase().includes(r.company.toLowerCase()));
      if (hit) {
        setActiveCompany(hit.company);
        setTimeout(() => setActiveCompany(null), 1200);
      }
    } catch (e) { /* ignore */ }

    setState("speaking");
    let i = 0;
    const step = () => {
      i += Math.max(1, Math.round(reply.length / 140));
      if (i >= reply.length) {
        setTyping("");
        setMessages(m => [...m, { role: "assistant", content: reply }]);
        if (!utter && !speechStarted) setState("idle");
      } else {
        setTyping(reply.slice(0, i));
        setTimeout(step, 28);
      }
    };
    step();
  }, [messages, tweaks.voiceOn, tweaks.persona, tweaks.voiceURI, tweaks.pitchOverride, tweaks.rateOverride, tweaks.ttsProxyUrl, tweaks.ttsVoice, scheduleCites]);

  const onSubmit = (e) => {
    e.preventDefault();
    send(input);
  };

  const stateLabel = {
    idle: "STANDBY",
    listening: "LISTENING",
    thinking: "PROCESSING",
    speaking: "RESPONDING"
  }[state];

  const activePersona = PERSONAS[tweaks.persona || "measured"];

  return (
    <div
      className={
        "shell" +
        (resumeOpen ? " resume-visible" : "") +
        (resumeFullscreen ? " resume-fullscreen" : "")
      }
      data-theme={tweaks.theme || "obsidian"}
      style={{ "--accent-hue": tweaks.accentHue }}
    >
      <div className="grid" aria-hidden="true" />
      <div className="vignette" aria-hidden="true" />
      <div className="scanlines" aria-hidden="true" />

      <header className="topbar">
        <div className="brand">
          <a href="/" className="home-link" title="Back to dakosar.com">← dakosar.com</a>
          <span className="divider">/</span>
          <span>{window.RESUME.name.toUpperCase()}</span>
        </div>
        <nav className="nav">
          <span className="chip-meta">{window.RESUME.title}</span>
          <span className="divider">/</span>
          <span className="chip-meta">{window.RESUME.location}</span>
        </nav>
        <div className="links">
          <button
            className={"resume-toggle" + (resumeOpen ? " active" : "")}
            onClick={() => { setResumeOpen(o => !o); setResumeFullscreen(false); }}
            title={resumeOpen ? "Hide resume" : "View resume"}
          >
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
              <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
              <polyline points="14 2 14 8 20 8" />
              <line x1="16" y1="13" x2="8" y2="13" />
              <line x1="16" y1="17" x2="8" y2="17" />
            </svg>
            RESUME
          </button>
          <button
            className="voice-toggle"
            onClick={() => {
              const nextOn = !tweaks.voiceOn;
              update({ voiceOn: nextOn });
              if (!nextOn) {
                try { window.speechSynthesis.cancel(); } catch (e) {}
                try { window.PremiumTTS.cancel(); } catch (e) {}
              }
            }}
            title={tweaks.voiceOn ? "Mute voice" : "Enable voice"}
          >
            {tweaks.voiceOn ? "● VOICE ON" : "○ VOICE OFF"}
          </button>
          {Object.entries(window.RESUME.links).map(([k, v]) => (
            <a key={k} href={v} target="_blank" rel="noreferrer">{k}</a>
          ))}
        </div>
      </header>

      {/* Resume document panel — slides in from left */}
      <aside
        className={"resume-panel" + (resumeOpen ? " open" : "")}
        style={{ transform: resumeOpen ? "translateX(0)" : "translateX(-100%)" }}
      >
        <div className="resume-panel-toolbar">
          <a
            className="resume-panel-btn"
            href="/JonathanKosarResume.pdf"
            download="Jonathan-Kosar-Resume.pdf"
            title="Download resume PDF"
          >
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
              <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
              <polyline points="7 10 12 15 17 10" />
              <line x1="12" y1="15" x2="12" y2="3" />
            </svg>
          </a>
          <button
            className="resume-panel-btn"
            onClick={() => setResumeFullscreen(f => !f)}
            title={resumeFullscreen ? "Exit fullscreen" : "Expand resume"}
          >
            {resumeFullscreen ? (
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
                <polyline points="4 14 10 14 10 20" />
                <polyline points="20 10 14 10 14 4" />
              </svg>
            ) : (
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
                <polyline points="15 3 21 3 21 9" />
                <polyline points="9 21 3 21 3 15" />
              </svg>
            )}
          </button>
          <button
            className="resume-panel-btn"
            onClick={() => { setResumeOpen(false); setResumeFullscreen(false); }}
            title="Close resume"
          >
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
              <line x1="18" y1="6" x2="6" y2="18" />
              <line x1="6" y1="6" x2="18" y2="18" />
            </svg>
          </button>
        </div>
        <ResumeDocument activeCite={activeCite} allCites={allCites} />
      </aside>

      <main className="stage">
        <div className="prompt-zone">
          <div
            className={"conversation" + (messages.length === 0 && !typing ? " empty" : "")}
            ref={scrollRef}
          >
            {messages.length === 0 && !typing && (
              <div className="greeting">
                <div className="greeting-halo">
                  <Halo state={state} style="orb" accentHue={tweaks.accentHue} />
                </div>
                <div className="greeting-kicker">Hello, I am the resume of</div>
                <h1 className="greeting-name">{window.RESUME.name}</h1>
                <p className="greeting-sub">Ask me anything about his work, experience, or projects.</p>
                <div className="greeting-hud">SYS.{stateLabel}</div>
              </div>
            )}

            {messages.map((m, i) => (
              <div key={i} className={`msg msg-${m.role}`}>
                {m.role === "assistant" && <span className="msg-tag">RESUME</span>}
                {m.role === "user" && <span className="msg-tag">YOU</span>}
                <div className="msg-body">{m.content}</div>
              </div>
            ))}

            {typing && (
              <div className="msg msg-assistant">
                <span className="msg-tag">RESUME</span>
                <div className="msg-body">{typing}<span className="caret">▍</span></div>
              </div>
            )}
          </div>

          <form className="composer" onSubmit={onSubmit}>
            {messages.length > 0 && (
              <div className="composer-halo" title={`SYS.${stateLabel}`}>
                <Halo state={state} style="orb" accentHue={tweaks.accentHue} />
              </div>
            )}
            <span className="composer-prompt">&gt;</span>
            <input
              ref={inputRef}
              className="composer-input"
              placeholder={state === "thinking" ? "…processing" : "Ask about Jonathan's work…"}
              value={input}
              onChange={e => setInput(e.target.value)}
              disabled={state === "thinking"}
              autoFocus
            />
            <button type="submit" className="composer-send" disabled={state === "thinking" || !input.trim()}>
              TRANSMIT
            </button>
          </form>

          {tweaks.showStarters && messages.length === 0 && (
            <div className="starters">
              {STARTERS.map(s => (
                <button key={s} className="starter" onClick={() => send(s)} disabled={state === "thinking"}>
                  {s}
                </button>
              ))}
            </div>
          )}

          {messages.length > 0 && suggestions.length > 0 && state !== "thinking" && (
            <div className="starters followups">
              <span className="followups-label">Follow up</span>
              {suggestions.map((q, i) => (
                <button key={i} className="starter" onClick={() => send(q)} disabled={state === "thinking"}>
                  {q}
                </button>
              ))}
            </div>
          )}
        </div>
      </main>

      <footer className="footmeta">
        <span>PORTFOLIO.v1</span>
        <span className="divider">·</span>
        <span>AI-assisted · responses generated live from resume data</span>
      </footer>

      <button
        className="settings-fab"
        onClick={() => setOpen(o => !o)}
        title="Customize"
        aria-label="Customize appearance"
      >
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
          <circle cx="12" cy="12" r="3" />
          <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
        </svg>
      </button>

      {open && (
        <div className="tweaks">
          <div className="tweaks-head">
            <span>Customize</span>
            <button onClick={() => setOpen(false)}>×</button>
          </div>

          <div className="tweak-row">
            <label>Theme</label>
            <div className="theme-swatches">
              {THEMES.map(t => (
                <button
                  key={t.id}
                  className={"theme-swatch " + ((tweaks.theme || "obsidian") === t.id ? "on" : "")}
                  onClick={() => update({ theme: t.id })}
                  title={t.label}
                  aria-label={t.label}
                >
                  <span className="theme-swatch-chip" style={{ background: t.bg, color: t.ink }}>
                    <span className="theme-swatch-dot" style={{ background: t.ink }} />
                  </span>
                  <span className="theme-swatch-label">{t.label}</span>
                </button>
              ))}
            </div>
          </div>

          <div className="tweak-row">
            <label>
              <input
                type="checkbox"
                checked={tweaks.showStarters}
                onChange={e => update({ showStarters: e.target.checked })}
              /> Show starter prompts
            </label>
          </div>

          <div className="tweak-row">
            <label>
              <input
                type="checkbox"
                checked={tweaks.voiceOn}
                onChange={e => {
                  update({ voiceOn: e.target.checked });
                  if (!e.target.checked) {
                    try { window.speechSynthesis.cancel(); } catch(_){}
                    try { window.PremiumTTS.cancel(); } catch(_){}
                  }
                }}
              /> Voice responses
            </label>
          </div>

          <div className="tweak-row">
            <label>Persona</label>
            <select
              className="voice-select"
              value={tweaks.persona || "measured"}
              onChange={e => {
                const id = e.target.value;
                update({ persona: id, pitchOverride: null, rateOverride: null });
                if (tweaks.voiceOn) setTimeout(() => previewVoice(id), 50);
              }}
            >
              {Object.entries(PERSONAS).map(([id, p]) => (
                <option key={id} value={id}>{p.label} — {p.blurb}</option>
              ))}
            </select>
          </div>

          <div className="tweak-row premium-row">
            <label>
              <span>Premium voice {tweaks.ttsProxyUrl ? <span className="badge-live">LIVE</span> : <span className="badge-off">OFF</span>}</span>
              <button
                className="mini-preview"
                onClick={() => previewVoice(tweaks.persona || "measured")}
                disabled={!tweaks.voiceOn || !tweaks.ttsProxyUrl}
                title={tweaks.ttsProxyUrl ? "Preview premium voice" : "Premium TTS not configured"}
              >test</button>
            </label>
            <select
              className="voice-select"
              value={tweaks.ttsVoice || "onyx"}
              onChange={e => update({ ttsVoice: e.target.value })}
              disabled={!tweaks.ttsProxyUrl}
            >
              <optgroup label="OpenAI voices">
                <option value="onyx">onyx · deep, measured (HAL-ish)</option>
                <option value="ash">ash · grounded, low</option>
                <option value="ballad">ballad · warm, narrator</option>
                <option value="sage">sage · calm, thoughtful</option>
                <option value="verse">verse · expressive</option>
                <option value="echo">echo · neutral, clean</option>
                <option value="alloy">alloy · balanced</option>
                <option value="fable">fable · British, lively</option>
                <option value="coral">coral · bright, friendly</option>
                <option value="nova">nova · upbeat, female</option>
                <option value="shimmer">shimmer · soft, female</option>
              </optgroup>
            </select>
            <div className="voice-hint">
              OpenAI <code>gpt-4o-mini-tts</code> via <code>workers/tts-proxy.js</code>. Proxy URL is set in <code>TWEAKS.ttsProxyUrl</code>.
            </div>
          </div>

          <details className="tweak-row tweak-collapse">
            <summary>
              <span>System voice (fallback)</span>
              <span className="tweak-collapse-caret" aria-hidden="true">▾</span>
            </summary>
            <div className="tweak-collapse-body">
              <button
                className="mini-preview"
                onClick={(e) => { e.preventDefault(); previewVoice(tweaks.persona || "measured"); }}
                title="Preview voice"
                disabled={!tweaks.voiceOn}
              >play</button>
              <select
                className="voice-select"
                value={tweaks.voiceURI || ""}
                onChange={e => {
                  update({ voiceURI: e.target.value });
                  if (tweaks.voiceOn) setTimeout(() => previewVoice(tweaks.persona || "measured", e.target.value), 50);
                }}
              >
                <option value="">Auto (best for persona)</option>
                {voices.map(v => (
                  <option key={v.voiceURI} value={v.voiceURI}>
                    {v.name} {v.lang ? `· ${v.lang}` : ""}{/enhanced|premium|neural/i.test(v.name) ? " ★" : ""}
                  </option>
                ))}
              </select>
              <div className="voice-hint">
                Used only when the premium proxy is unreachable. ★ = high-quality system voice.
              </div>
            </div>
          </details>

          <div className="tweak-row">
            <label>Pitch <span className="mono">{(tweaks.pitchOverride != null ? tweaks.pitchOverride : activePersona.pitch).toFixed(2)}</span></label>
            <input
              type="range" min="0" max="2" step="0.05"
              value={tweaks.pitchOverride != null ? tweaks.pitchOverride : activePersona.pitch}
              onChange={e => update({ pitchOverride: +e.target.value })}
            />
          </div>

          <div className="tweak-row">
            <label>Rate <span className="mono">{(tweaks.rateOverride != null ? tweaks.rateOverride : activePersona.rate).toFixed(2)}</span></label>
            <input
              type="range" min="0.5" max="1.5" step="0.05"
              value={tweaks.rateOverride != null ? tweaks.rateOverride : activePersona.rate}
              onChange={e => update({ rateOverride: +e.target.value })}
            />
          </div>

          <div className="tweaks-foot">Settings save on this device.</div>
        </div>
      )}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
