// ---------- Assistants (builder) + Voice Studio ----------

const FIXED_CSV_COLS = ["customer_name", "number"];

function extractPromptVars(text) {
  const matches = [...(text || "").matchAll(/\{\{([^}]+)\}\}/g)];
  return [...new Set(
    matches.map(m => m[1].trim()).filter(v => !v.startsWith("system.") && !v.startsWith("__"))
  )];
}

function buildCsvCols(systemPrompt, firstMessage) {
  const detected = extractPromptVars((systemPrompt || "") + " " + (firstMessage || ""));
  return [...new Set([...FIXED_CSV_COLS, ...detected])];
}

function downloadCsvTemplate(cols) {
  const exampleValues = {
    customer_name: "Ahmad bin Ali",
    number: "+60123456789",
    balance: "500",
    account: "5229988",
  };
  const header = cols.join(",");
  const example = cols.map(c => exampleValues[c] || "example_value").join(",");
  const csv = header + "\n" + example;
  const blob = new Blob([csv], { type: "text/csv" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "campaign_template.csv";
  a.click();
  URL.revokeObjectURL(url);
}

// Loads voice list once; shows dropdown if voices available, text input otherwise
const _voiceCache = { data: null };
function VoicePickerField({ provider, value, onChange }) {
  const [voices, setVoices] = React.useState(_voiceCache.data);

  React.useEffect(() => {
    if (_voiceCache.data) { setVoices(_voiceCache.data); return; }
    voxApiOptional("/voices", {}).then(d => {
      _voiceCache.data = d;
      setVoices(d);
    });
  }, []);

  const list = voices?.[provider] || [];

  if (list.length > 0) {
    return (
      <Field label="Voice">
        <Select value={value} onChange={e => onChange(e.target.value)}>
          <option value="">— Default —</option>
          {list.map(v => (
            <option key={v.id} value={v.id}>{v.name}</option>
          ))}
        </Select>
      </Field>
    );
  }

  return (
    <Field label="Voice ID / Name">
      <Input value={value} onChange={e => onChange(e.target.value)} placeholder="e.g. alloy, nova, pFZP5JQG…" />
    </Field>
  );
}

// Loads available sound files from server, shows dropdown + volume slider
const _soundsCache = { data: null };
function BackgroundNoiseField({ value, volume, onFileChange, onVolumeChange }) {
  const [files, setFiles] = React.useState(_soundsCache.data);
  const [uploading, setUploading] = React.useState(false);
  const [uploadErr, setUploadErr] = React.useState("");
  const uploadRef = React.useRef();

  function refreshSounds(list) {
    _soundsCache.data = list;
    setFiles(list);
  }

  React.useEffect(() => {
    if (_soundsCache.data) { setFiles(_soundsCache.data); return; }
    voxApiOptional("/assistants/sounds", {}).then(d => {
      refreshSounds(d?.files || []);
    });
  }, []);

  function authHeaders() {
    try {
      const s = JSON.parse(localStorage.getItem("voxpro.session") || "null");
      if (s?.token) return { Authorization: `Bearer ${s.token}` };
    } catch {}
    return {};
  }

  async function handleUpload(e) {
    const file = e.target.files?.[0];
    if (!file) return;
    setUploading(true);
    setUploadErr("");
    try {
      const form = new FormData();
      form.append("file", file);
      const res = await fetch("/assistants/sounds", { method: "POST", headers: authHeaders(), body: form });
      const json = await res.json();
      if (!res.ok) throw new Error(json.error || "Upload failed");
      refreshSounds(json.files || []);
      onFileChange(json.filename);
    } catch (err) {
      setUploadErr(err.message);
    } finally {
      setUploading(false);
      e.target.value = "";
    }
  }

  async function handleDelete(filename) {
    if (!confirm(`Delete "${filename}"?`)) return;
    try {
      const res = await fetch(`/assistants/sounds/${encodeURIComponent(filename)}`, { method: "DELETE", headers: authHeaders() });
      const json = await res.json();
      if (!res.ok) throw new Error(json.error || "Delete failed");
      refreshSounds(json.files || []);
      if (value === filename) onFileChange("");
    } catch (err) {
      setUploadErr(err.message);
    }
  }

  const pct = Math.round((volume ?? 0.10) * 100);

  return (
    <div className="mt-3">
      <div className="hr mb-3" />
      <div className="card-title mb-2">Background ambience</div>
      <div className="fld-row">
        <Field label="Sound file" help="Office ambience played quietly under the AI voice — makes the call sound more natural.">
          <div className="row gap-2" style={{ alignItems: "center" }}>
            <Select value={value || ""} onChange={e => onFileChange(e.target.value)} style={{ flex: 1 }}>
              <option value="">— None —</option>
              {(files || []).map(f => (
                <option key={f} value={f}>{f.replace(/\.[^.]+$/, "").replace(/[-_]/g, " ")}</option>
              ))}
              {!files && <option disabled>Loading…</option>}
            </Select>
            {value && (
              <button type="button" className="btn btn-ghost" style={{ padding: "4px 8px", color: "var(--c-danger)", flexShrink: 0 }}
                onClick={() => handleDelete(value)} title="Delete this file">✕</button>
            )}
          </div>
          <div className="row gap-2 mt-2" style={{ alignItems: "center" }}>
            <input ref={uploadRef} type="file" accept=".mp3,.wav,.ogg,.m4a" style={{ display: "none" }} onChange={handleUpload} />
            <button type="button" className="btn btn-ghost" style={{ fontSize: 12 }}
              onClick={() => uploadRef.current?.click()} disabled={uploading}>
              {uploading ? "Uploading…" : "⬆ Upload MP3/WAV"}
            </button>
            {uploadErr && <span style={{ color: "var(--c-danger)", fontSize: 12 }}>{uploadErr}</span>}
          </div>
        </Field>
        <Field label={`Volume — ${pct}%`} help="How loud the background noise is relative to the AI voice. 8–15% is subtle and natural. Above 20% may interfere with speech clarity.">
          <Input type="range" min="0.02" max="0.30" step="0.01" value={volume ?? 0.10} onChange={e => onVolumeChange(Number(e.target.value))} disabled={!value} />
          <div className="row" style={{ justifyContent: "space-between", color: "var(--c-text-4)", fontSize: 11 }}>
            <span>Subtle</span><span className="mono">{pct}%</span><span>Loud</span>
          </div>
        </Field>
      </div>
    </div>
  );
}

// ---------- Knowledge Tab ----------
function KnowledgeTab({ assistant, editData, updateField }) {
  const assistantId = assistant?.id;
  const [files, setFiles] = React.useState(null);
  const [uploading, setUploading] = React.useState(false);
  const [uploadErr, setUploadErr] = React.useState("");
  const [testingIdx, setTestingIdx] = React.useState(null);
  const [testResult, setTestResult] = React.useState({});
  const [addDbOpen, setAddDbOpen] = React.useState(false);
  const [dbForm, setDbForm] = React.useState({ name: "", type: "mysql", host: "", port: "", database: "", user: "", password: "" });
  const [showPassIdx, setShowPassIdx] = React.useState(null);
  const fileInputRef = React.useRef();

  function loadFiles() {
    voxApiOptional(`/knowledge/${assistantId}`, { files: [] })
      .then(d => setFiles(d.files || []));
  }

  React.useEffect(() => { if (assistantId) loadFiles(); }, [assistantId]);

  function kbAuthHeaders() {
    try {
      const s = JSON.parse(localStorage.getItem("voxpro.session") || "null");
      if (s?.token) return { Authorization: `Bearer ${s.token}` };
    } catch {}
    return {};
  }

  async function handleUpload(e) {
    const selected = Array.from(e.target.files || []);
    if (!selected.length) return;
    setUploadErr("");
    setUploading(true);
    try {
      const fd = new FormData();
      selected.forEach(f => fd.append("files", f));
      const resp = await fetch(`/knowledge/${assistantId}/upload`, {
        method: "POST",
        headers: kbAuthHeaders(),
        body: fd,
      });
      const data = await resp.json();
      if (!resp.ok) throw new Error(data.error || "Upload failed");
      setFiles(data.files || []);
    } catch (err) {
      setUploadErr(err.message);
    } finally {
      setUploading(false);
      if (fileInputRef.current) fileInputRef.current.value = "";
    }
  }

  async function deleteFile(name) {
    if (!confirm(`Delete "${name}"?`)) return;
    const resp = await fetch(`/knowledge/${assistantId}/${encodeURIComponent(name)}`, { method: "DELETE", headers: kbAuthHeaders() });
    const data = await resp.json();
    if (data.ok) setFiles(data.files || []);
  }

  function fmtSize(bytes) {
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
    return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
  }

  const dbConnections = editData?.dbConnections || [];

  async function testConn(idx) {
    setTestingIdx(idx);
    setTestResult(r => ({ ...r, [idx]: null }));
    const conn = dbConnections[idx];
    try {
      const resp = await fetch(`/knowledge/${assistantId}/db-test`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "include",
        body: JSON.stringify(conn),
      });
      const data = await resp.json();
      setTestResult(r => ({ ...r, [idx]: data }));
    } catch (err) {
      setTestResult(r => ({ ...r, [idx]: { ok: false, error: err.message } }));
    } finally {
      setTestingIdx(null);
    }
  }

  function addDb() {
    if (!dbForm.name || !dbForm.host || !dbForm.database || !dbForm.user) return;
    const next = [...dbConnections, { ...dbForm, id: Date.now().toString() }];
    updateField("dbConnections", next);
    setDbForm({ name: "", type: "mysql", host: "", port: "", database: "", user: "", password: "" });
    setAddDbOpen(false);
  }

  function removeDb(idx) {
    const next = dbConnections.filter((_, i) => i !== idx);
    updateField("dbConnections", next);
  }

  return (
    <div>
      {/* ── Knowledge Files ── */}
      <div className="card-title mb-1">Knowledge base</div>
      <div className="card-subtitle mb-4">
        Upload CSV or TXT files. Each file gets its own template variable you can paste into the system prompt.
        Use <span className="mono" style={{ background: "var(--c-bg-2)", padding: "1px 5px", borderRadius: 4 }}>{"{{knowledge_base}}"}</span> for all files,
        or <span className="mono" style={{ background: "var(--c-bg-2)", padding: "1px 5px", borderRadius: 4 }}>{"{{kb_filename}}"}</span> for a specific file.
        Max 10 MB per file.
      </div>

      <div className="row gap-2 mb-3">
        <input
          ref={fileInputRef}
          type="file"
          multiple
          accept=".csv,.docx,.txt,.pdf,.xlsx"
          style={{ display: "none" }}
          onChange={handleUpload}
        />
        <button
          className="btn btn-sm"
          onClick={() => fileInputRef.current?.click()}
          disabled={uploading}
        >
          {uploading ? "Uploading…" : "+ Upload files"}
        </button>
      </div>

      {uploadErr && <div className="muted text-12 mb-2" style={{ color: "var(--err)" }}>{uploadErr}</div>}

      {files === null ? (
        <div className="muted text-12">Loading…</div>
      ) : files.length === 0 ? (
        <div className="empty-state" style={{ padding: "24px 0" }}>
          <div className="empty-state-title">No files uploaded yet</div>
          <div>Upload CSV or DOCX files to give the agent a knowledge base.</div>
        </div>
      ) : (
        <div className="col" style={{ gap: 0 }}>
          {files.map(f => {
            const varName = "{{kb_" + f.name.replace(/\.[^.]+$/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/, "") + "}}";
            return (
              <div key={f.name} className="row gap-3" style={{ padding: "10px 0", borderBottom: "1px solid var(--c-line)", alignItems: "center" }}>
                <div style={{ fontSize: 18, width: 24, textAlign: "center", color: "var(--c-text-4)" }}>
                  {f.name.endsWith(".csv") || f.name.endsWith(".xlsx") ? "📊" : f.name.endsWith(".pdf") ? "📄" : "📝"}
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div className="text-13 strong truncate">{f.name}</div>
                  <div className="row gap-2" style={{ alignItems: "center", marginTop: 3 }}>
                    <span
                      className="mono text-11"
                      style={{ background: "var(--c-bg-2)", padding: "1px 6px", borderRadius: 4, color: "var(--c-accent)", cursor: "pointer" }}
                      title="Click to copy"
                      onClick={() => navigator.clipboard.writeText(varName)}
                    >{varName}</span>
                    <span className="muted text-11">{fmtSize(f.size)} · {new Date(f.uploadedAt).toLocaleDateString()}</span>
                  </div>
                </div>
                <button className="btn btn-sm" style={{ color: "var(--err)", borderColor: "var(--err)" }} onClick={() => deleteFile(f.name)}>Delete</button>
              </div>
            );
          })}
        </div>
      )}

      <div className="hr mt-4 mb-4" />

      {/* ── Database Connections ── */}
      <div className="card-title mb-1">Database connections</div>
      <div className="card-subtitle mb-4">
        Connect to MySQL or PostgreSQL databases. The agent can query these during calls to look up live data.
      </div>

      {dbConnections.length > 0 && (
        <div className="col mb-3" style={{ gap: 0 }}>
          {dbConnections.map((conn, idx) => (
            <div key={conn.id || idx} style={{ padding: "12px 0", borderBottom: "1px solid var(--c-line)" }}>
              <div className="row gap-3" style={{ alignItems: "center" }}>
                <div style={{ fontSize: 18, width: 24, textAlign: "center" }}>🗄️</div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div className="text-13 strong">{conn.name}</div>
                  <div className="muted text-11 mono">{conn.type} · {conn.user}@{conn.host}{conn.port ? `:${conn.port}` : ""}/{conn.database}</div>
                </div>
                <button
                  className="btn btn-sm"
                  onClick={() => testConn(idx)}
                  disabled={testingIdx === idx}
                >
                  {testingIdx === idx ? "Testing…" : "Test"}
                </button>
                <button className="btn btn-sm" style={{ color: "var(--err)", borderColor: "var(--err)" }} onClick={() => removeDb(idx)}>Remove</button>
              </div>
              {testResult[idx] && (
                <div className="text-11 mt-1" style={{ paddingLeft: 42, color: testResult[idx].ok ? "var(--ok)" : "var(--err)" }}>
                  {testResult[idx].ok ? "✓ " + testResult[idx].message : "✗ " + testResult[idx].error}
                </div>
              )}
            </div>
          ))}
        </div>
      )}

      {addDbOpen ? (
        <div style={{ background: "var(--c-surface-2)", borderRadius: 10, padding: 16, border: "1px solid var(--c-line)" }}>
          <div className="card-title mb-3" style={{ fontSize: 13 }}>Add database connection</div>
          <div className="fld-row">
            <Field label="Name / alias">
              <Input value={dbForm.name} onChange={e => setDbForm(f => ({ ...f, name: e.target.value }))} placeholder="e.g. Customer DB" />
            </Field>
            <Field label="Type">
              <Select value={dbForm.type} onChange={e => setDbForm(f => ({ ...f, type: e.target.value }))}>
                <option value="mysql">MySQL</option>
                <option value="postgres">PostgreSQL</option>
              </Select>
            </Field>
          </div>
          <div className="fld-row">
            <Field label="Host">
              <Input value={dbForm.host} onChange={e => setDbForm(f => ({ ...f, host: e.target.value }))} placeholder="localhost or IP" />
            </Field>
            <Field label="Port">
              <Input value={dbForm.port} onChange={e => setDbForm(f => ({ ...f, port: e.target.value }))} placeholder={dbForm.type === "postgres" ? "5432" : "3306"} />
            </Field>
          </div>
          <div className="fld-row">
            <Field label="Database name">
              <Input value={dbForm.database} onChange={e => setDbForm(f => ({ ...f, database: e.target.value }))} placeholder="mydb" />
            </Field>
            <Field label="Username">
              <Input value={dbForm.user} onChange={e => setDbForm(f => ({ ...f, user: e.target.value }))} placeholder="root" />
            </Field>
          </div>
          <Field label="Password">
            <div style={{ display: "flex", gap: 8 }}>
              <Input
                type={showPassIdx === "new" ? "text" : "password"}
                value={dbForm.password}
                onChange={e => setDbForm(f => ({ ...f, password: e.target.value }))}
                placeholder="••••••••"
                style={{ flex: 1 }}
              />
              <button className="btn btn-sm" onClick={() => setShowPassIdx(v => v === "new" ? null : "new")}>
                {showPassIdx === "new" ? "Hide" : "Show"}
              </button>
            </div>
          </Field>
          <div className="row gap-2 mt-3">
            <button className="btn btn-sm btn-primary" onClick={addDb} disabled={!dbForm.name || !dbForm.host || !dbForm.database || !dbForm.user}>Save connection</button>
            <button className="btn btn-sm" onClick={() => setAddDbOpen(false)}>Cancel</button>
          </div>
        </div>
      ) : (
        <button className="btn btn-sm" onClick={() => setAddDbOpen(true)}>+ Add database connection</button>
      )}

      <div className="muted text-11 mt-4">
        Database credentials are stored in the assistant config. Save the assistant after adding connections.
      </div>
    </div>
  );
}

// ---------- Embed Tab ----------
function EmbedTab({ assistant }) {
  const [embedSection, setEmbedSection] = React.useState("web");
  const [copied, setCopied] = React.useState("");
  const [tgToken, setTgToken] = React.useState(assistant.telegramBotToken || "");
  const [tgWebhookUrl, setTgWebhookUrl] = React.useState(window.location.origin);
  const [tgStatus, setTgStatus] = React.useState(assistant.telegramBotUsername ? "connected" : "idle");
  const [tgUsername, setTgUsername] = React.useState(assistant.telegramBotUsername || "");
  const [tgLoading, setTgLoading] = React.useState(false);
  const [tgError, setTgError] = React.useState("");
  const serverOrigin = window.location.origin;
  const embedUrl = `${serverOrigin}/embed/chat/${assistant.id}`;

  const iframeSnippet = `<iframe
  src="${embedUrl}"
  width="380"
  height="600"
  style="border:none;border-radius:16px;box-shadow:0 4px 24px rgba(0,0,0,0.12);"
  allow="microphone"
></iframe>`;

  function copy(text, key) {
    navigator.clipboard.writeText(text).then(() => {
      setCopied(key);
      setTimeout(() => setCopied(""), 2000);
    });
  }

  async function connectTelegram() {
    setTgLoading(true); setTgError("");
    try {
      const resp = await voxApi(`/telegram/setup/${assistant.id}`, {
        method: "POST",
        body: JSON.stringify({ token: tgToken.trim(), webhookUrl: tgWebhookUrl.trim() }),
      });
      if (!resp.ok) { setTgError(resp.error || "Connection failed"); }
      else { setTgStatus("connected"); setTgUsername(resp.username); }
    } catch (e) { setTgError(e.message); }
    setTgLoading(false);
  }

  async function disconnectTelegram() {
    setTgLoading(true); setTgError("");
    try {
      await voxApi(`/telegram/disconnect/${assistant.id}`, { method: "POST" });
      setTgStatus("idle"); setTgUsername(""); setTgToken("");
    } catch (e) { setTgError(e.message); }
    setTgLoading(false);
  }

  const sectionBtnStyle = (active) => ({
    padding: "5px 14px", borderRadius: 6, fontSize: 12, fontWeight: 500, cursor: "pointer",
    border: "none", background: active ? "var(--a)" : "transparent",
    color: active ? "#0c0c14" : "var(--c-text-3)", transition: "all 0.15s",
  });

  return (
    <div>
      {/* Channel selector */}
      <div style={{ display: "flex", gap: 4, marginBottom: 24, background: "var(--c-surface-2)", borderRadius: 8, padding: 4, border: "1px solid var(--c-line)", width: "fit-content" }}>
        <button style={sectionBtnStyle(embedSection === "web")} onClick={() => setEmbedSection("web")}>Web Widget</button>
        <button style={sectionBtnStyle(embedSection === "telegram")} onClick={() => setEmbedSection("telegram")}>Telegram</button>
        <button style={{ ...sectionBtnStyle(false), opacity: 0.45, cursor: "not-allowed" }} disabled>WhatsApp</button>
      </div>

      {/* ── Web Widget ── */}
      {embedSection === "web" && (<>
        <div className="card-title mb-1">Embed this assistant</div>
        <div className="card-subtitle mb-4">Copy the snippet below and paste it into any website to add a live chat widget powered by this assistant.</div>
        <div className="fld-row" style={{ gap: 12, marginBottom: 20 }}>
          <div style={{ flex: 1 }}>
            <div className="field-label mb-1" style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.5px", color: "var(--c-text-4)" }}>Direct URL</div>
            <div style={{ display: "flex", gap: 8 }}>
              <Input value={embedUrl} readOnly style={{ fontFamily: "var(--f-mono)", fontSize: 12 }} />
              <button className="btn btn-sm" onClick={() => copy(embedUrl, "url")} style={{ flexShrink: 0 }}>
                {copied === "url" ? "Copied!" : "Copy"}
              </button>
            </div>
            <div className="muted text-11 mt-1">Open in a new tab to preview the widget.</div>
          </div>
        </div>
        <div style={{ marginBottom: 20 }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
            <div className="field-label" style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.5px", color: "var(--c-text-4)" }}>iframe embed code</div>
            <button className="btn btn-sm" onClick={() => copy(iframeSnippet, "iframe")}>
              {copied === "iframe" ? "✓ Copied!" : "Copy code"}
            </button>
          </div>
          <pre style={{ background: "var(--c-surface-2,#f4f6f9)", borderRadius: 8, padding: "14px 16px", fontSize: 12, fontFamily: "var(--f-mono)", overflowX: "auto", whiteSpace: "pre-wrap", wordBreak: "break-all", border: "1px solid var(--c-line)", lineHeight: 1.6 }}>{iframeSnippet}</pre>
        </div>
        <div className="hr mb-4" />
        <div className="card-title mb-2" style={{ fontSize: 13 }}>Preview</div>
        <div style={{ borderRadius: 16, overflow: "hidden", boxShadow: "0 4px 24px rgba(0,0,0,0.10)", display: "inline-block", width: 360, height: 520, border: "1px solid var(--c-line)" }}>
          <iframe src={embedUrl} width="360" height="520" style={{ border: "none", display: "block" }} title="Chat widget preview" />
        </div>
        <div className="muted text-11 mt-2">This preview uses the currently saved version of the assistant. Save changes first to see updates.</div>
      </>)}

      {/* ── Telegram ── */}
      {embedSection === "telegram" && (<>
        <div className="card-title mb-1" style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <svg width="20" height="20" viewBox="0 0 24 24" fill="#2AABEE"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm5.894 8.221-1.97 9.28c-.145.658-.537.818-1.084.508l-3-2.21-1.447 1.394c-.16.16-.295.295-.605.295l.213-3.053 5.56-5.023c.242-.213-.054-.333-.373-.12L7.17 13.989l-2.965-.924c-.643-.204-.657-.643.136-.953l11.57-4.461c.537-.194 1.006.131.983.57z"/></svg>
          Connect Telegram Bot
          {tgStatus === "connected" && <span style={{ fontSize: 11, background: "var(--ok-bg,#dcfce7)", color: "var(--ok,#16a34a)", borderRadius: 20, padding: "2px 10px", fontWeight: 600 }}>Connected @{tgUsername}</span>}
        </div>
        <div className="card-subtitle mb-4">Messages sent to your Telegram bot will be handled by this assistant.</div>

        {/* Step 1 */}
        <div style={{ background: "var(--c-surface-2)", borderRadius: 10, padding: "14px 16px", marginBottom: 16, border: "1px solid var(--c-line)" }}>
          <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 6, color: "var(--c-text-2)" }}>Step 1 — Create a bot with BotFather</div>
          <div style={{ fontSize: 12, color: "var(--c-text-3)", lineHeight: 1.6 }}>
            Open Telegram → search <strong>@BotFather</strong> → send <code style={{ background: "var(--c-surface-3,#e8eaed)", padding: "1px 5px", borderRadius: 4 }}>/newbot</code> → follow instructions → copy the <strong>API token</strong>.
          </div>
        </div>

        {/* Step 2 */}
        <div style={{ background: "var(--c-surface-2)", borderRadius: 10, padding: "14px 16px", marginBottom: 16, border: "1px solid var(--c-line)" }}>
          <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 8, color: "var(--c-text-2)" }}>Step 2 — Enter your bot token</div>
          <Input
            placeholder="110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw"
            value={tgToken}
            onChange={e => setTgToken(e.target.value)}
            style={{ fontFamily: "var(--f-mono)", fontSize: 12, marginBottom: 0 }}
          />
        </div>

        {/* Step 3 */}
        <div style={{ background: "var(--c-surface-2)", borderRadius: 10, padding: "14px 16px", marginBottom: 20, border: "1px solid var(--c-line)" }}>
          <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 4, color: "var(--c-text-2)" }}>Step 3 — Public HTTPS URL of this server</div>
          <div style={{ fontSize: 11, color: "var(--c-text-4)", marginBottom: 8 }}>Telegram requires HTTPS. If your server only has HTTP, use a Cloudflare Tunnel or ngrok URL.</div>
          <Input
            placeholder="https://your-domain.com"
            value={tgWebhookUrl}
            onChange={e => setTgWebhookUrl(e.target.value)}
            style={{ fontFamily: "var(--f-mono)", fontSize: 12 }}
          />
          <div style={{ fontSize: 10, color: "var(--c-text-4)", marginTop: 6 }}>
            Webhook will be registered at: <code style={{ background: "var(--c-surface-3,#e8eaed)", padding: "1px 4px", borderRadius: 3 }}>{tgWebhookUrl.replace(/\/$/, "")}/telegram/hook/{assistant.id}</code>
          </div>
        </div>

        {tgError && <div style={{ fontSize: 12, color: "var(--err,#dc2626)", marginBottom: 12, padding: "8px 12px", background: "rgba(220,38,38,0.08)", borderRadius: 6, border: "1px solid rgba(220,38,38,0.2)" }}>{tgError}</div>}

        <div style={{ display: "flex", gap: 10 }}>
          {tgStatus !== "connected" ? (
            <button className="btn btn-primary" onClick={connectTelegram} disabled={tgLoading || !tgToken.trim()}>
              {tgLoading ? "Connecting…" : "Connect Bot"}
            </button>
          ) : (
            <button className="btn" onClick={disconnectTelegram} disabled={tgLoading} style={{ color: "var(--err)" }}>
              {tgLoading ? "Disconnecting…" : "Disconnect"}
            </button>
          )}
        </div>

        {tgStatus === "connected" && (
          <div style={{ marginTop: 20, padding: "12px 16px", background: "rgba(22,163,74,0.08)", borderRadius: 8, border: "1px solid rgba(22,163,74,0.2)" }}>
            <div style={{ fontSize: 12, fontWeight: 600, color: "var(--ok,#16a34a)", marginBottom: 4 }}>Bot is live</div>
            <div style={{ fontSize: 12, color: "var(--c-text-3)" }}>Messages sent to <strong>@{tgUsername}</strong> on Telegram are now handled by this assistant. Conversations appear in Call Logs.</div>
          </div>
        )}
      </>)}
    </div>
  );
}

// ---------- Structured Output Tab ----------
function StructuredOutputTab({ assistantId }) {
  const [defs, setDefs] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [saving, setSaving] = React.useState(null);
  const [err, setErr] = React.useState("");
  const [addOpen, setAddOpen] = React.useState(false);
  const [form, setForm] = React.useState({ name: "", description: "", type: "string", instructions: "" });
  const [formErr, setFormErr] = React.useState("");
  const [adding, setAdding] = React.useState(false);

  function load() {
    setLoading(true);
    voxApiOptional("/structured-output-defs", { defs: [] })
      .then(d => { setDefs(d.defs || []); setLoading(false); })
      .catch(() => { setDefs([]); setLoading(false); });
  }

  React.useEffect(() => { load(); }, [assistantId]);

  function isAssigned(def) {
    if (!def.assistantIds || def.assistantIds.length === 0) return true;
    return def.assistantIds.includes(assistantId);
  }

  function isGlobal(def) {
    return !def.assistantIds || def.assistantIds.length === 0;
  }

  async function toggle(def) {
    setSaving(def.id);
    setErr("");
    try {
      let nextIds;
      if (isGlobal(def)) {
        nextIds = [assistantId];
      } else if (def.assistantIds.includes(assistantId)) {
        nextIds = def.assistantIds.filter(id => id !== assistantId);
      } else {
        nextIds = [...def.assistantIds, assistantId];
      }
      const updated = await voxApi(`/structured-output-defs/${def.id}`, {
        method: "PUT",
        body: JSON.stringify({ assistantIds: nextIds }),
      });
      setDefs(prev => prev.map(d => d.id === def.id ? updated : d));
    } catch (e) {
      setErr(e.message || "Save failed");
    } finally {
      setSaving(null);
    }
  }

  async function addDef() {
    if (!form.name.trim()) { setFormErr("Name is required"); return; }
    setAdding(true);
    setFormErr("");
    try {
      const created = await voxApi("/structured-output-defs", {
        method: "POST",
        body: JSON.stringify({ ...form, assistantIds: [assistantId] }),
      });
      setDefs(prev => [...prev, created]);
      setForm({ name: "", description: "", type: "string", instructions: "" });
      setAddOpen(false);
    } catch (e) {
      setFormErr(e.message || "Create failed");
    } finally {
      setAdding(false);
    }
  }

  async function removeDef(defId) {
    setSaving(defId);
    try {
      await voxApi(`/structured-output-defs/${defId}`, { method: "DELETE" });
      setDefs(prev => prev.filter(d => d.id !== defId));
    } catch (e) {
      setErr(e.message || "Delete failed");
    } finally {
      setSaving(null);
    }
  }

  if (loading) return <div className="muted text-12" style={{ padding: 24 }}>Loading…</div>;

  const assignedDefs = (defs || []).filter(d => isAssigned(d));
  const otherDefs = (defs || []).filter(d => !isAssigned(d));

  return (
    <div>
      <div className="row" style={{ justifyContent: "space-between", marginBottom: 16 }}>
        <div>
          <div className="card-title">Data capture</div>
          <div className="card-subtitle">Fields extracted from call transcripts after every call ends.</div>
        </div>
        <Btn variant="primary" size="sm" icon="plus" onClick={() => { setAddOpen(true); setFormErr(""); }}>Add field</Btn>
      </div>

      {err && <div style={{ fontSize: 11, color: "var(--err)", marginBottom: 10, padding: "6px 10px", background: "rgba(239,68,68,0.08)", borderRadius: 6, border: "1px solid rgba(239,68,68,0.2)" }}>{err}</div>}

      {addOpen && (
        <div style={{ padding: 16, borderRadius: 10, border: "1px solid var(--a-line)", background: "rgba(99,102,241,0.05)", marginBottom: 16 }}>
          <div className="card-title mb-3" style={{ fontSize: 12 }}>New extraction field</div>
          <div className="fld-row">
            <Field label="Field name">
              <Input value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} placeholder="e.g. expected_payment_date" />
            </Field>
            <Field label="Type">
              <Select value={form.type} onChange={e => setForm(f => ({ ...f, type: e.target.value }))}>
                <option value="string">Text (string)</option>
                <option value="number">Number</option>
                <option value="boolean">Yes / No (boolean)</option>
              </Select>
            </Field>
          </div>
          <Field label="Description">
            <Input value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))} placeholder="e.g. Date customer expects to make payment" />
          </Field>
          <Field label="Instructions for AI" help="How the AI should format or interpret this value">
            <Input value={form.instructions} onChange={e => setForm(f => ({ ...f, instructions: e.target.value }))} placeholder="e.g. save date as dd/mm/yyyy" />
          </Field>
          {formErr && <div style={{ fontSize: 11, color: "var(--err)", marginBottom: 8 }}>{formErr}</div>}
          <div className="row gap-2 mt-3">
            <Btn variant="primary" size="sm" onClick={addDef} disabled={adding}>{adding ? "Saving…" : "Add field"}</Btn>
            <Btn variant="ghost" size="sm" onClick={() => { setAddOpen(false); setFormErr(""); }}>Cancel</Btn>
          </div>
        </div>
      )}

      {assignedDefs.length === 0 && !addOpen && (
        <div className="empty-state">
          <div className="empty-state-title">No fields configured</div>
          <div>Add extraction fields to automatically capture structured data from every call — PTP dates, outcomes, amounts, and more.</div>
        </div>
      )}

      {assignedDefs.map(def => (
        <div key={def.id} style={{ display: "flex", alignItems: "flex-start", gap: 12, padding: "14px 0", borderBottom: "1px solid var(--c-line)" }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div className="row gap-2" style={{ marginBottom: 4 }}>
              <span className="strong text-12 mono">{def.name}</span>
              <Badge tone={def.type === "boolean" ? "warn" : def.type === "number" ? "ok" : "default"}>{def.type}</Badge>
              {isGlobal(def) && <Badge tone="accent">All assistants</Badge>}
            </div>
            {def.description && <div className="text-12 muted">{def.description}</div>}
            {def.instructions && <div className="text-11" style={{ color: "var(--c-text-3)", marginTop: 3, fontStyle: "italic" }}>{def.instructions}</div>}
          </div>
          <div className="row gap-2" style={{ flexShrink: 0 }}>
            {!isGlobal(def) && (
              <Btn variant="ghost" size="sm" style={{ color: "var(--err)", fontSize: 10 }} onClick={() => removeDef(def.id)} disabled={saving === def.id}>Remove</Btn>
            )}
            {isGlobal(def) && (
              <Btn variant="ghost" size="sm" style={{ fontSize: 10 }} onClick={() => toggle(def)} disabled={saving === def.id} title="Click to scope to specific assistants only">Scope →</Btn>
            )}
          </div>
        </div>
      ))}

      {otherDefs.length > 0 && (
        <div style={{ marginTop: 20 }}>
          <div className="card-title mb-2" style={{ fontSize: 11, color: "var(--c-text-4)" }}>OTHER FIELDS (not assigned to this assistant)</div>
          {otherDefs.map(def => (
            <div key={def.id} style={{ display: "flex", alignItems: "center", gap: 12, padding: "10px 0", borderBottom: "1px solid var(--c-line)", opacity: 0.6 }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div className="row gap-2">
                  <span className="text-12 mono">{def.name}</span>
                  <Badge tone="default">{def.type}</Badge>
                </div>
                {def.description && <div className="muted text-11">{def.description}</div>}
              </div>
              <Btn variant="ghost" size="sm" style={{ fontSize: 10 }} onClick={() => toggle(def)} disabled={saving === def.id}>Enable →</Btn>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

const AssistantsView = ({ mode, assistants = [], setAssistants, clients = [], setClients }) => {
  const session = React.useMemo(() => getSession(), []);
  const canEditSystemPrompt = session?.role === "admin";
  const [selectedId, setSelectedId] = React.useState(assistants[0]?.id || null);
  const [view, setView] = React.useState("list");
  const [tab, setTab] = React.useState("prompt");
  const [editData, setEditData] = React.useState(null);

  // Quick test state
  const [qtInput, setQtInput] = React.useState("");
  const [qtMessages, setQtMessages] = React.useState([]);
  const [qtLoading, setQtLoading] = React.useState(false);
  const [qtError, setQtError] = React.useState("");
  const [qtContactId, setQtContactId] = React.useState(() => `assistant-test-${Date.now()}`);
  const [qtCsvRows, setQtCsvRows] = React.useState([]);
  const [qtCsvRowIdx, setQtCsvRowIdx] = React.useState(0);
  const [qtCsvName, setQtCsvName] = React.useState("");
  const [saveError, setSaveError] = React.useState("");
  const [savingAssistant, setSavingAssistant] = React.useState(false);
  const qtScrollRef = React.useRef(null);
  const [qtMode, setQtMode] = React.useState("chat");
  const [callState, setCallState] = React.useState("idle");
  const [micMuted, setMicMuted] = React.useState(false);
  const [callTranscript, setCallTranscript] = React.useState([]);
  const callRoomRef = React.useRef(null);
  const callAudioEls = React.useRef([]);
  const callTranscriptRef = React.useRef(null);

  React.useEffect(() => {
    if (qtScrollRef.current) qtScrollRef.current.scrollTop = qtScrollRef.current.scrollHeight;
  }, [qtMessages, qtLoading]);

  function parseCsvText(text) {
    const lines = text.trim().split(/\r?\n/);
    if (lines.length < 2) return [];
    const headers = lines[0].split(",").map(h => h.trim().replace(/^"|"$/g, ""));
    return lines.slice(1).filter(Boolean).map(line => {
      const vals = line.split(",").map(v => v.trim().replace(/^"|"$/g, ""));
      return Object.fromEntries(headers.map((h, i) => [h, vals[i] ?? ""]));
    });
  }

  function fillVars(template, row) {
    if (!template || !row) return template || "";
    return template.replace(/\{\{([^}]+)\}\}/g, (_, k) => {
      const val = row[k.trim()];
      return val !== undefined ? val : `{{${k.trim()}}}`;
    });
  }

  function handleCsvUpload(e) {
    const file = e.target.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = (ev) => {
      const rows = parseCsvText(ev.target.result || "");
      setQtCsvRows(rows);
      setQtCsvRowIdx(0);
      setQtCsvName(file.name);
    };
    reader.readAsText(file);
    e.target.value = "";
  }

  function qtReset() {
    setQtMessages([]);
    setQtInput("");
    setQtError("");
    setQtContactId(`assistant-test-${Date.now()}`);
  }

  async function startCall() {
    if (callState === "connecting" || callState === "live") return;
    setCallState("connecting");
    setCallTranscript([]);
    setQtError("");

    if (!navigator.mediaDevices) {
      setQtError("Microphone requires HTTPS. Open the admin panel via https://");
      setCallState("idle");
      return;
    }

    let stream, audioCtx;
    let ended = false;
    let isProcessing = false;
    let speechActive = false;
    let speechStart = 0;
    let silenceStart = 0;
    let recorder = null;
    let recordedChunks = [];
    const contactId = `assistant-test-${Date.now()}`;

    const SPEECH_RMS = 0.012;
    const SILENCE_RMS = 0.008;
    const MIN_SPEECH_MS = 300;
    const SILENCE_SEND_MS = 550;

    try {
      stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
      audioCtx = new AudioContext();
      if (audioCtx.state === "suspended") await audioCtx.resume();
      const track = stream.getAudioTracks()[0];
      console.log(`[qt-call] track: ${track?.label} muted=${track?.muted} enabled=${track?.enabled} state=${track?.readyState}`);

      const source = audioCtx.createMediaStreamSource(stream);
      // ScriptProcessorNode reliably delivers audio samples regardless of Chrome
      // graph optimisation — AnalyserNode alone returns zeros without a connected destination.
      const processor = audioCtx.createScriptProcessor(2048, 1, 1);
      let currentRms = 0;
      processor.onaudioprocess = (ev) => {
        const data = ev.inputBuffer.getChannelData(0);
        let sum = 0;
        for (let i = 0; i < data.length; i++) sum += data[i] * data[i];
        currentRms = Math.sqrt(sum / data.length);
      };
      source.connect(processor);
      processor.connect(audioCtx.destination); // required for onaudioprocess to fire
      let lastRmsLog = 0;

      function getRms() { return currentRms; }

      function blobToWavBase64(blob) {
        return new Promise((resolve, reject) => {
          blob.arrayBuffer().then(ab => {
            audioCtx.decodeAudioData(ab.slice(0), decoded => {
              const sr = 16000;
              const offCtx = new OfflineAudioContext(1, Math.ceil(decoded.duration * sr), sr);
              const src = offCtx.createBufferSource();
              src.buffer = decoded;
              src.connect(offCtx.destination);
              src.start();
              offCtx.startRendering().then(rendered => {
                const pcm = rendered.getChannelData(0);
                const int16 = new Int16Array(pcm.length);
                for (let i = 0; i < pcm.length; i++) {
                  int16[i] = Math.max(-32768, Math.min(32767, Math.round(pcm[i] * 32767)));
                }
                const wavBuf = new ArrayBuffer(44 + int16.length * 2);
                const dv = new DataView(wavBuf);
                const ws = (o, s) => { for (let i = 0; i < s.length; i++) dv.setUint8(o + i, s.charCodeAt(i)); };
                ws(0, "RIFF"); dv.setUint32(4, 36 + int16.length * 2, true);
                ws(8, "WAVE"); ws(12, "fmt ");
                dv.setUint32(16, 16, true); dv.setUint16(20, 1, true);
                dv.setUint16(22, 1, true); dv.setUint32(24, sr, true);
                dv.setUint32(28, sr * 2, true); dv.setUint16(32, 2, true);
                dv.setUint16(34, 16, true); ws(36, "data");
                dv.setUint32(40, int16.length * 2, true);
                for (let i = 0; i < int16.length; i++) dv.setInt16(44 + i * 2, int16[i], true);
                const bytes = new Uint8Array(wavBuf);
                let bin = "";
                for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
                resolve(btoa(bin));
              }).catch(reject);
            }, reject);
          }).catch(reject);
        });
      }

      async function sendSpeech(chunks) {
        isProcessing = true;
        const t0 = Date.now();
        try {
          const blob = new Blob(chunks, { type: recorder.mimeType });
          console.log(`[qt-call] converting blob size=${blob.size} type=${blob.type}`);
          const wavBase64 = await blobToWavBase64(blob);
          console.log(`[qt-call] wav base64 len=${wavBase64.length}`);

          const response = await fetch("/interact/stream", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            credentials: "same-origin",
            body: JSON.stringify({
              contactId,
              audioBase64: wavBase64,
              audioMimeType: "audio/wav",
              systemPrompt: activeAssistant?.systemPrompt || "",
              firstMessage: activeAssistant?.firstMessage || "",
              language: activeAssistant?.language || "en",
              voiceProvider: activeAssistant?.voiceProvider || "elevenlabs",
              asrProvider: activeAssistant?.asrProvider || "ytl",
              asrLanguage: activeAssistant?.asrLanguage || "",
              voice: activeAssistant?.voice || "",
              assistantId: activeAssistant?.id || null,
              assistantName: activeAssistant?.name || "",
              temperature: activeAssistant?.temperature,
              reasoningEffort: activeAssistant?.reasoningEffort,
              maxTokens: activeAssistant?.maxTokens,
              promptData: {
                ...(qtCsvRows.length ? qtRow : {}),
                __assistantToolIds: activeAssistant?.selectedToolIds || [],
                __assistantId: activeAssistant?.id || "",
                __assistantName: activeAssistant?.name || "",
              },
            }),
          });

          if (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);

          // sequential audio playback queue
          const audioQueue = [];
          let playingAudio = false;
          let hangupDetected = false;
          let fullReply = "";

          async function playNext() {
            if (playingAudio || audioQueue.length === 0) return;
            playingAudio = true;
            const b64 = audioQueue.shift();
            try {
              const bin = atob(b64);
              const bytes = new Uint8Array(bin.length);
              for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
              const decoded = await audioCtx.decodeAudioData(bytes.buffer.slice(0));
              await new Promise(resolve => {
                const bufSrc = audioCtx.createBufferSource();
                bufSrc.buffer = decoded;
                bufSrc.connect(audioCtx.destination);
                bufSrc.onended = resolve;
                bufSrc.start();
              });
            } catch (e) { console.error("[qt-call] chunk play:", e); }
            playingAudio = false;
            if (audioQueue.length > 0) playNext();
          }

          // parse SSE stream
          const reader = response.body.getReader();
          const decoder = new TextDecoder();
          let lineBuf = "";

          while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            lineBuf += decoder.decode(value, { stream: true });
            const lines = lineBuf.split("\n");
            lineBuf = lines.pop();

            for (const line of lines) {
              if (!line.startsWith("data: ")) continue;
              try {
                const ev = JSON.parse(line.slice(6));
                if (ev.type === "asr") {
                  console.log(`[qt-call] asr at ${Date.now()-t0}ms: "${ev.text}"`);
                  setCallTranscript(prev => [...prev, { role: "user", text: ev.text }]);
                  setTimeout(() => { if (callTranscriptRef.current) callTranscriptRef.current.scrollTop = callTranscriptRef.current.scrollHeight; }, 50);
                } else if (ev.type === "chunk") {
                  console.log(`[qt-call] audio chunk ${ev.index} at ${Date.now()-t0}ms`);
                  audioQueue.push(ev.data);
                  playNext();
                } else if (ev.type === "reply") {
                  fullReply = ev.text || "";
                  const ms = Date.now() - t0;
                  if (fullReply) {
                    setCallTranscript(prev => [...prev, { role: "assistant", text: fullReply, ms }]);
                    setTimeout(() => { if (callTranscriptRef.current) callTranscriptRef.current.scrollTop = callTranscriptRef.current.scrollHeight; }, 50);
                  }
                  if (ev.hangup) hangupDetected = true;
                } else if (ev.type === "done") {
                  console.log(`[qt-call] stream done totalMs=${ev.totalMs}`);
                } else if (ev.type === "error") {
                  console.error("[qt-call] stream error:", ev.error || ev.code);
                }
              } catch (_) {}
            }
          }

          // wait for all audio to finish
          await new Promise(resolve => {
            const poll = () => (!playingAudio && audioQueue.length === 0) ? resolve() : setTimeout(poll, 80);
            poll();
          });

          if (hangupDetected) {
            ended = true;
            stream.getTracks().forEach(t => t.stop());
            audioCtx.close().catch(() => {});
            callAudioEls.current.forEach(a => { try { a.pause(); } catch (_) {} });
            callAudioEls.current = [];
            callRoomRef.current = null;
            setCallState("ended");
            return;
          }
        } catch (e) {
          console.error("[qt-call]", e);
        }
        isProcessing = false;
      }

      function tick() {
        if (ended) return;
        const rms = getRms();
        const now = Date.now();

        if (now - lastRmsLog > 1000) {
          console.log(`[qt-call] ctx=${audioCtx.state} rms=${rms.toFixed(4)} speech=${speechActive} processing=${isProcessing}`);
          lastRmsLog = now;
        }

        if (isProcessing) {
          if (recorder && recorder.state === "recording") {
            recorder.stop();
            speechActive = false;
          }
          speechStart = 0;
          silenceStart = 0;
          requestAnimationFrame(tick);
          return;
        }

        if (!speechActive) {
          if (rms > SPEECH_RMS) {
            if (!speechStart) {
              // Start recording immediately — don't wait for MIN_SPEECH_MS so we capture the first syllable
              speechStart = now;
              recordedChunks = [];
              recorder = new MediaRecorder(stream);
              recorder.ondataavailable = e => { if (e.data.size > 0) recordedChunks.push(e.data); };
              recorder.start(100);
            }
            if (now - speechStart > MIN_SPEECH_MS) {
              console.log(`[qt-call] speech confirmed rms=${rms.toFixed(4)}`);
              speechActive = true;
              silenceStart = 0;
            }
          } else {
            if (speechStart) {
              // Brief noise below MIN_SPEECH_MS — discard recording
              if (recorder && recorder.state === "recording") {
                const discard = recorder;
                recorder = null;
                discard.stop();
              }
              speechStart = 0;
            }
          }
        } else {
          if (rms < SILENCE_RMS) {
            if (!silenceStart) silenceStart = now;
            if (now - silenceStart > SILENCE_SEND_MS) {
              console.log(`[qt-call] silence end → sending ${recordedChunks.length} chunks`);
              speechActive = false;
              silenceStart = 0;
              speechStart = 0;
              const savedChunks = recordedChunks;
              recorder.onstop = () => { sendSpeech(savedChunks); };
              recorder.stop();
            }
          } else {
            silenceStart = 0;
          }
        }

        requestAnimationFrame(tick);
      }

      callRoomRef.current = {
        cleanup() {
          ended = true;
          if (recorder && recorder.state === "recording") recorder.stop();
          try { processor.disconnect(); } catch (_) {}
          stream.getTracks().forEach(t => t.stop());
          audioCtx.close().catch(() => {});
        },
        setMuted(m) {
          stream.getAudioTracks().forEach(t => { t.enabled = !m; });
        },
      };

      if (activeAssistant?.firstMessage) {
        setCallTranscript([{ role: "assistant", text: activeAssistant.firstMessage }]);
      }

      requestAnimationFrame(tick);
      setCallState("live");
      setMicMuted(false);
    } catch (err) {
      console.error("[qt-call]", err);
      setQtError(err.message || "Microphone access failed");
      setCallState("idle");
      if (stream) stream.getTracks().forEach(t => t.stop());
      if (audioCtx) audioCtx.close().catch(() => {});
    }
  }

  async function endCall() {
    if (callRoomRef.current?.cleanup) callRoomRef.current.cleanup();
    callRoomRef.current = null;
    callAudioEls.current.forEach(a => { try { a.pause(); } catch (_) {} });
    callAudioEls.current = [];
    setCallState("idle");
    setMicMuted(false);
    setCallTranscript([]);
  }

  async function toggleMic() {
    const next = !micMuted;
    if (callRoomRef.current?.setMuted) callRoomRef.current.setMuted(next);
    setMicMuted(next);
  }

  const qtRow = qtCsvRows[qtCsvRowIdx] || {};
  const activeAssistant = editData || assistants.find(a => a.id === selectedId);
  const resolvedFirst = fillVars(activeAssistant?.firstMessage || "", qtRow);

  async function sendQtMessage() {
    const text = qtInput.trim();
    if (!text || qtLoading) return;
    setQtInput("");
    setQtError("");
    setQtMessages(m => [...m, { role: "user", text }]);
    setQtLoading(true);
    try {
      const body = {
        contactId: qtContactId,
        transcript: text,
        systemPrompt: activeAssistant?.systemPrompt || "",
        firstMessage: activeAssistant?.firstMessage || "",
        language: activeAssistant?.language || "en",
        voiceProvider: activeAssistant?.voiceProvider || "elevenlabs",
        asrProvider: activeAssistant?.asrProvider || "ytl",
        asrLanguage: activeAssistant?.asrLanguage || "",
        voice: activeAssistant?.voice || "",
        includeAudio: false,
        assistantId: activeAssistant?.id || null,
        assistantName: activeAssistant?.name || "",
        promptData: {
          ...(qtCsvRows.length ? qtRow : {}),
          __assistantToolIds: activeAssistant?.selectedToolIds || [],
          __assistantId: activeAssistant?.id || "",
          __assistantName: activeAssistant?.name || "",
        },
      };
      const resp = await voxApi("/interact", { method: "POST", body: JSON.stringify(body) });
      setQtMessages(m => [...m, { role: "assistant", text: resp.reply || "(no reply)" }]);
    } catch (err) {
      setQtError(err?.message || "Request failed");
      setQtMessages(m => m.slice(0, -1));
    } finally {
      setQtLoading(false);
    }
  }

  function onQtKeyDown(e) {
    if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendQtMessage(); }
  }

  React.useEffect(() => {
    if (assistants.length && !selectedId) setSelectedId(assistants[0].id);
  }, [assistants]);

  function openEditor(id) {
    const a = assistants.find(x => x.id === id) || createAssistantRecord();
    setSelectedId(a.id);
    setEditData({ ...a });
    setSaveError("");
    setView("edit");
    setTab("prompt");
    setQtMessages([]);
    setQtInput("");
    setQtError("");
    setQtContactId(`assistant-test-${Date.now()}`);
  }

  function newAssistant() {
    const a = createAssistantRecord();
    setEditData(a);
    setSelectedId(a.id);
    setSaveError("");
    setView("edit");
    setTab("prompt");
  }

  function updateField(field, val) {
    setEditData(d => ({ ...d, [field]: val }));
  }

  async function saveAssistant() {
    if (!editData) return;
    setSaveError("");
    setSavingAssistant(true);
    const updated = { ...editData, updatedAt: new Date().toISOString() };
    const existing = assistants.find(a => a.id === updated.id);
    try {
      const saved = await saveAssistantToServer(updated, { isNew: !existing });
      const finalList = existing
        ? assistants.map(a => a.id === saved.id ? saved : a)
        : [...assistants, saved];
      setAssistants(finalList);
      saveAssistants(finalList);
      setSelectedId(saved.id);
      setEditData(saved);
      setView("list");
    } catch (err) {
      setSaveError(err?.message || "Assistant save failed");
    } finally {
      setSavingAssistant(false);
    }
  }

  function deleteAssistant(id) {
    const newList = assistants.filter(a => a.id !== id);
    setAssistants(newList);
    saveAssistants(newList);
    deleteAssistantFromServer(id);
    if (selectedId === id) {
      setSelectedId(newList[0]?.id || null);
      setView("list");
    }
  }

  async function duplicateAssistant(e, id) {
    e.stopPropagation();
    try {
      const { assistant: copy } = await voxApi(`/assistants/${id}/duplicate`, { method: "POST" });
      const newList = [...assistants, copy];
      setAssistants(newList);
      saveAssistants(newList);
      openEditor(copy.id);
    } catch (err) {
      alert(err?.message || "Duplicate failed");
    }
  }

  async function toggleClientAssignment(clientId) {
    const client = clients.find(c => c.id === clientId);
    if (!client || !selectedId) return;
    const ids = client.assignedAssistantIds || [];
    const next = ids.includes(selectedId) ? ids.filter(x => x !== selectedId) : [...ids, selectedId];
    const updated = {
      ...client,
      assistantId: client.assistantId || selectedId,
      assignedAssistantIds: next,
      updatedAt: new Date().toISOString(),
    };
    const optimisticClients = clients.map(c => c.id === clientId ? updated : c);
    setClients(optimisticClients);
    saveClients(optimisticClients);
    try {
      const res = await voxApi(`/client-admin/${clientId}`, {
        method: "PUT",
        body: JSON.stringify({
          assistantId: updated.assistantId || null,
          assignedAssistantIds: next,
        }),
      });
      const savedClient = res.client || updated;
      const finalClients = clients.map(c => c.id === clientId ? savedClient : c);
      setClients(finalClients);
      saveClients(finalClients);
    } catch (err) {
      setClients(clients);
      saveClients(clients);
      setSaveError(err?.message || "Client assignment save failed");
    }
  }

  const selected = editData || assistants.find(a => a.id === selectedId) || assistants[0];

  if (view === "list") {
    return (
      <div>
        <div className="ph">
          <div>
            <h1 className="ph-title">Assistants</h1>
            <div className="ph-subtitle">{assistants.length} voice agents across this workspace.</div>
          </div>
          <div className="ph-actions">
            <Btn variant="primary" icon="plus" size="sm" onClick={newAssistant}>New assistant</Btn>
          </div>
        </div>

        <div className="g-3">
          {assistants.map(a => {
            const initials = a.name ? a.name.slice(0, 2).toUpperCase() : "??";
            return (
              <div key={a.id} className="agent-card" onClick={() => openEditor(a.id)}>
                <div className="agent-card-head">
                  <div className="agent-avatar">{initials}</div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div className="row" style={{ justifyContent: "space-between" }}>
                      <span className="agent-name truncate">{a.name}</span>
                      <div className="row gap-1" style={{ alignItems: "center" }}>
                        <Badge tone={a.status === "active" ? "ok" : a.status === "paused" ? "warn" : "default"} dot>{a.status || "active"}</Badge>
                        <button title="Duplicate assistant" onClick={e => duplicateAssistant(e, a.id)}
                          style={{ background: "none", border: "none", cursor: "pointer", color: "var(--c-text-3)", padding: "2px 4px", borderRadius: 4, fontSize: 13, lineHeight: 1 }}
                          onMouseEnter={e => e.currentTarget.style.color = "var(--c-text-1)"}
                          onMouseLeave={e => e.currentTarget.style.color = "var(--c-text-3)"}>⧉</button>
                      </div>
                    </div>
                    <div className="agent-meta truncate">{a.useCase} · {a.language}</div>
                  </div>
                </div>
                <div className="row gap-2" style={{ flexWrap: "wrap" }}>
                  <span className="chip">{a.voiceProvider || "openai"}</span>
                  <span className="chip">{a.llmProvider || "ytl"}</span>
                </div>
                <div className="agent-stats">
                  <div>
                    <div className="agent-stat-label">Language</div>
                    <div className="agent-stat-value">{a.language || "en"}</div>
                  </div>
                  <div>
                    <div className="agent-stat-label">Use case</div>
                    <div className="agent-stat-value" style={{ fontSize: 11 }}>{a.useCase || "—"}</div>
                  </div>
                  <div>
                    <div className="agent-stat-label">Status</div>
                    <div className="agent-stat-value">{a.status || "active"}</div>
                  </div>
                </div>
              </div>
            );
          })}

          <div className="agent-card" style={{ border: "1px dashed var(--c-line-2)", background: "transparent", alignItems: "center", justifyContent: "center", minHeight: 200 }} onClick={newAssistant}>
            <div style={{ color: "var(--c-text-3)", display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }}>
              <div style={{ width: 36, height: 36, borderRadius: 10, background: "var(--c-surface)", display: "grid", placeItems: "center", border: "1px solid var(--c-line-2)" }}>
                <Icon name="plus" />
              </div>
              <div className="text-12 strong">New assistant</div>
              <div className="text-11 muted center" style={{ maxWidth: 180 }}>Build a new voice agent from scratch.</div>
            </div>
          </div>
        </div>
      </div>
    );
  }

  // ===== Builder =====
  if (!selected) return null;
  const initials = selected.name ? selected.name.slice(0, 2).toUpperCase() : "??";

  return (
    <div>
      <div className="ph">
        <div>
          <div className="row gap-2" style={{ marginBottom: 6 }}>
            <button className="muted text-12" onClick={() => { setView("list"); setEditData(null); }}>← Assistants</button>
          </div>
          <div className="row gap-3">
            <div className="agent-avatar">{initials}</div>
            <div>
              <h1 className="ph-title" style={{ display: "flex", alignItems: "center", gap: 10 }}>
                {selected.name}
                <Badge tone={selected.status === "active" ? "ok" : "warn"} dot>{selected.status || "active"}</Badge>
              </h1>
              <div className="ph-subtitle">{selected.useCase} · ID <span className="mono">{selected.id?.slice(-8)}</span></div>
            </div>
          </div>
        </div>
        <div className="ph-actions">
          <Btn variant="ghost" size="sm" onClick={e => duplicateAssistant(e, selected.id)}>⧉ Duplicate</Btn>
          <Btn variant="ghost" icon="trash" size="sm" onClick={() => deleteAssistant(selected.id)} style={{ color: "var(--err)" }}>Delete</Btn>
          <Btn variant="primary" icon="check" size="sm" onClick={saveAssistant} disabled={savingAssistant}>
            {savingAssistant ? "Saving..." : "Save changes"}
          </Btn>
        </div>
      </div>

      {saveError && (
        <div style={{ fontSize: 12, color: "var(--err)", marginBottom: 12, padding: "10px 12px", background: "rgba(239,68,68,0.08)", borderRadius: 8, border: "1px solid rgba(239,68,68,0.2)" }}>
          {saveError}
        </div>
      )}

      <div className="builder">
        <div className="builder-side">
          <div className="builder-side-head">
            <span className="card-title">Library</span>
            <Btn variant="ghost" icon="plus" size="sm" onClick={newAssistant} />
          </div>
          <div className="builder-side-list">
            {assistants.map(a => (
              <div key={a.id} className={`builder-side-item ${a.id === selected.id ? "active" : ""}`} onClick={() => openEditor(a.id)}>
                <div className="builder-side-item-avatar">{a.name ? a.name.slice(0, 2).toUpperCase() : "??"}</div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div className="truncate">{a.name}</div>
                  <div className="builder-side-item-meta truncate">{a.useCase}</div>
                </div>
                <span style={{ width: 6, height: 6, borderRadius: 3, background: a.status === "active" ? "var(--ok)" : "var(--c-text-4)", flex: "none" }} />
              </div>
            ))}
          </div>
        </div>

        <div className="builder-main">
          <div className="builder-tabs">
            {[
              { id: "prompt", label: "Prompt" },
              { id: "voice", label: "Voice & Model" },
              { id: "behavior", label: "Behavior" },
              { id: "clients", label: "Clients", devOnly: true },
              { id: "knowledge", label: "Knowledge", devOnly: true },
              { id: "structured_output", label: "Data Capture", devOnly: true },
              { id: "embed", label: "Embed", devOnly: true },
              { id: "advanced", label: "Advanced", devOnly: true },
            ].filter(t => mode === "dev" || !t.devOnly).map(t => (
              <button key={t.id} className={`builder-tab ${tab === t.id ? "active" : ""}`} onClick={() => setTab(t.id)}>{t.label}</button>
            ))}
          </div>
          <div className="builder-body">
            {tab === "prompt" && editData && (
              <div>
                <div className="row" style={{ justifyContent: "space-between", marginBottom: 14 }}>
                  <div>
                    <div className="card-title">System prompt</div>
                    <div className="card-subtitle">
                      {canEditSystemPrompt
                        ? "Defines the assistant's personality, scope, and behavior."
                        : "Visible for reference only. Only developer/admin can edit the system prompt."}
                    </div>
                  </div>
                </div>
                <Textarea
                  value={editData.systemPrompt || ""}
                  onChange={e => canEditSystemPrompt && updateField("systemPrompt", e.target.value)}
                  readOnly={!canEditSystemPrompt}
                  disabled={!canEditSystemPrompt}
                  style={{
                    minHeight: 260,
                    fontFamily: "var(--f-mono)",
                    fontSize: 12,
                    opacity: canEditSystemPrompt ? 1 : 0.72,
                    cursor: canEditSystemPrompt ? "text" : "not-allowed",
                  }}
                  placeholder="You are a helpful voice assistant…"
                />
                <div className="row mt-2">
                  <span className="chip">{(editData.systemPrompt || "").split(/\s+/).filter(Boolean).length * 1.3 | 0} / 8,000 tokens est.</span>
                </div>
                <div className="hr" />
                <div className="row" style={{ justifyContent: "space-between", marginBottom: 10 }}>
                  <div>
                    <div className="card-title">First message</div>
                    <div className="card-subtitle">Greeting played when the call connects.</div>
                  </div>
                </div>
                <Input value={editData.firstMessage || ""} onChange={e => updateField("firstMessage", e.target.value)} placeholder="Hello, how can I help you today?" />
                <div className="fld-row mt-4">
                  <Field label="Use case">
                    <Select value={editData.useCase || "voiceAssistant"} onChange={e => updateField("useCase", e.target.value)}>
                      <option value="voiceAssistant">Voice assistant</option>
                      <option value="leadQualification">Lead qualification</option>
                      <option value="debtCollection">Debt collection</option>
                      <option value="customerService">Customer service</option>
                      <option value="appointmentBooking">Appointment booking</option>
                      <option value="survey">Survey</option>
                    </Select>
                  </Field>
                  <Field label="Primary language">
                    <Select value={editData.language || "en"} onChange={e => updateField("language", e.target.value)}>
                      <option value="en">English</option>
                      <option value="ms">Bahasa Malaysia</option>
                      <option value="zh">Mandarin</option>
                      <option value="ta">Tamil</option>
                    </Select>
                  </Field>
                </div>
                <div className="fld-row">
                  <Field label="Industry">
                    <Input value={editData.industry || ""} onChange={e => updateField("industry", e.target.value)} placeholder="Collections, Healthcare, Insurance..." />
                  </Field>
                  <Field label="Linked client">
                    <Select value={editData.clientId || ""} onChange={e => updateField("clientId", e.target.value)}>
                      <option value="">— none —</option>
                      {clients.map(c => (
                        <option key={c.id} value={c.id}>{c.company || c.name}</option>
                      ))}
                    </Select>
                  </Field>
                </div>
                {/* CSV columns panel */}
                {(() => {
                  const cols = buildCsvCols(editData.systemPrompt, editData.firstMessage);
                  const detected = cols.filter(c => !FIXED_CSV_COLS.includes(c));
                  return (
                    <div style={{ margin: "16px 0", padding: "14px 16px", borderRadius: 10, border: "1px solid var(--c-line-2)", background: "var(--c-surface)" }}>
                      <div className="row" style={{ justifyContent: "space-between", marginBottom: 10 }}>
                        <div>
                          <div className="card-title" style={{ fontSize: 12 }}>CSV columns for this assistant</div>
                          <div className="muted text-11 mt-1">Your campaign CSV must include these exact column headers.</div>
                        </div>
                        <Btn variant="ghost" size="sm" icon="download" onClick={() => downloadCsvTemplate(cols)}>Download template</Btn>
                      </div>
                      <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
                        {FIXED_CSV_COLS.map(c => (
                          <span key={c} style={{ display: "inline-flex", alignItems: "center", gap: 4, padding: "3px 10px", borderRadius: 20, background: "rgba(59,130,246,0.12)", border: "1px solid rgba(59,130,246,0.3)", fontSize: 11, fontFamily: "var(--f-mono)", color: "var(--a)" }}>
                            {c}
                            <span style={{ fontSize: 9, color: "var(--c-text-4)", fontFamily: "var(--f-sans)", marginLeft: 2 }}>required</span>
                          </span>
                        ))}
                        {detected.map(c => (
                          <span key={c} style={{ display: "inline-flex", alignItems: "center", gap: 4, padding: "3px 10px", borderRadius: 20, background: "rgba(16,185,129,0.10)", border: "1px solid rgba(16,185,129,0.3)", fontSize: 11, fontFamily: "var(--f-mono)", color: "var(--ok)" }}>
                            {c}
                            <span style={{ fontSize: 9, color: "var(--c-text-4)", fontFamily: "var(--f-sans)", marginLeft: 2 }}>from prompt</span>
                          </span>
                        ))}
                        {detected.length === 0 && (
                          <span className="muted text-11">Add {"{{"}<span style={{ color: "var(--ok)" }}>variable</span>{"}}"} to your prompt to see extra columns here.</span>
                        )}
                      </div>
                    </div>
                  );
                })()}

                <Field label="Assistant name">
                  <Input value={editData.name || ""} onChange={e => updateField("name", e.target.value)} />
                </Field>
                <Field label="Status">
                  <Select value={editData.status || "active"} onChange={e => updateField("status", e.target.value)}>
                    <option value="active">Active</option>
                    <option value="paused">Paused</option>
                    <option value="draft">Draft</option>
                  </Select>
                </Field>
              </div>
            )}

            {tab === "voice" && editData && (
              <div>
                <div className="card-title mb-2">Voice & speech</div>
                <div className="fld-row">
                  <Field label="TTS Provider" help="Who generates the voice audio. Edge TTS is free with native Malay voices — no server needed. ElevenLabs has the lowest latency. Self-hosted options (Mesolitica, Kokoro…) need a local server running.">
                    <Select value={editData.voiceProvider || "elevenlabs"} onChange={e => updateField("voiceProvider", e.target.value)}>
                      <option value="openai">OpenAI (~400ms)</option>
                      <option value="elevenlabs">ElevenLabs (~150ms) ⚡</option>
                      <option value="ytl">YTL ILMU TTS (~430ms) — Malay ✓</option>
                      <option value="edge">Edge TTS — Free (~250ms) — Malay ✓</option>
                      <option value="mesolitica">Mesolitica (~80ms) — Malay ✓ — Self-hosted</option>
                      <option value="kokoro">Kokoro (self-hosted)</option>
                      <option value="vibevoice">VibeVoice Realtime (self-hosted)</option>
                      <option value="voxtral">Voxtral 4B (self-hosted)</option>
                      <option value="piper">Piper (self-hosted)</option>
                      <option value="xtts">XTTS v2 (self-hosted)</option>
                      <option value="f5tts">F5-TTS (self-hosted)</option>
                      <option value="styletts2">StyleTTS2 (self-hosted)</option>
                      <option value="chatterbox">Chatterbox (self-hosted)</option>
                      <option value="orpheus">Orpheus (self-hosted)</option>
                      <option value="dia">Dia (self-hosted)</option>
                      <option value="gtts">gTTS — Free (~900ms)</option>
                      <option value="windows">Windows TTS (free)</option>
                    </Select>
                  </Field>
                  <VoicePickerField
                    provider={editData.voiceProvider || "openai"}
                    value={editData.voice || ""}
                    onChange={v => updateField("voice", v)}
                  />
                </div>
                {/* ── TTS Latency Reference ── */}
                <div style={{ background: "var(--c-surface-2)", borderRadius: 8, padding: "10px 14px", border: "1px solid var(--c-line)", marginTop: 8 }}>
                  <div style={{ fontSize: 11, fontWeight: 600, marginBottom: 6, color: "var(--c-text-2)" }}>Voice latency guide — first audio heard by caller</div>
                  <div style={{ display: "grid", gridTemplateColumns: "1fr 70px 60px 50px", gap: "3px 8px", fontSize: 11 }}>
                    <span style={{ color: "var(--c-text-4)", fontWeight: 600 }}>Provider</span>
                    <span style={{ color: "var(--c-text-4)", fontWeight: 600, textAlign: "right" }}>TTS</span>
                    <span style={{ color: "var(--c-text-4)", fontWeight: 600, textAlign: "center" }}>Malay</span>
                    <span style={{ color: "var(--c-text-4)", fontWeight: 600, textAlign: "right" }}>Cost</span>
                    {[
                      ["ElevenLabs ⚡",  "~150ms", "No",  "Paid"],
                      ["Edge TTS (MS)",  "~250ms", "✓",   "Free"],
                      ["YTL ILMU TTS",   "~430ms", "✓",   "Paid"],
                      ["Mesolitica",     "~80ms",  "✓",   "Free*"],
                      ["OpenAI TTS",     "~400ms", "No",  "Paid"],
                      ["gTTS",           "~900ms", "~",   "Free"],
                    ].map(([name, tts, malay, cost]) => (
                      <React.Fragment key={name}>
                        <span style={{ color: "var(--c-text-2)" }}>{name}</span>
                        <span className="mono" style={{ color: "var(--c-primary)", textAlign: "right" }}>{tts}</span>
                        <span style={{ textAlign: "center", color: malay === "✓" ? "var(--ok)" : malay === "No" ? "var(--c-text-4)" : "var(--warn)" }}>{malay}</span>
                        <span style={{ color: "var(--c-text-4)", textAlign: "right" }}>{cost}</span>
                      </React.Fragment>
                    ))}
                  </div>
                  <div style={{ marginTop: 6, fontSize: 10, color: "var(--c-text-4)", borderTop: "1px solid var(--c-line)", paddingTop: 5 }}>
                    Total call latency ≈ ASR (~400ms) + LLM (~200ms) + TTS above. *Self-hosted server required.
                  </div>
                </div>
                <BackgroundNoiseField
                  value={editData.backgroundNoise || ""}
                  volume={editData.backgroundNoiseVolume ?? 0.10}
                  onFileChange={v => updateField("backgroundNoise", v)}
                  onVolumeChange={v => updateField("backgroundNoiseVolume", v)}
                />
                <div className="g-3 mt-2">
                  <Field label="Stability" help="How consistent the voice tone is between sentences. Lower = more natural variation, higher = robotic but predictable. Keep below 0.60 for phone calls.">
                    <Input type="range" min="0" max="1" step="0.05" value={editData.voiceSettings?.stability ?? 0.45} onChange={e => updateField("voiceSettings", { ...editData.voiceSettings, stability: Number(e.target.value) })} />
                    <div className="row" style={{ justifyContent: "space-between", color: "var(--c-text-4)", fontSize: 11 }}>
                      <span>Variable</span><span className="mono">{(editData.voiceSettings?.stability ?? 0.45).toFixed(2)}</span><span>Stable</span>
                    </div>
                  </Field>
                  <Field label="Similarity boost" help="How closely the output stays to the original voice character. Higher = more on-voice but may distort under bad audio. 0.75–0.90 works well for most voices.">
                    <Input type="range" min="0" max="1" step="0.05" value={editData.voiceSettings?.similarity ?? 0.82} onChange={e => updateField("voiceSettings", { ...editData.voiceSettings, similarity: Number(e.target.value) })} />
                    <div className="row" style={{ justifyContent: "space-between", color: "var(--c-text-4)", fontSize: 11 }}>
                      <span>Loose</span><span className="mono">{(editData.voiceSettings?.similarity ?? 0.82).toFixed(2)}</span><span>Tight</span>
                    </div>
                  </Field>
                  <Field label="Style / expression" help="How much emotion and inflection the voice adds. Keep low (0.10–0.25) for professional calls — high values sound over-dramatic and add latency.">
                    <Input type="range" min="0" max="1" step="0.05" value={editData.voiceSettings?.style ?? 0.2} onChange={e => updateField("voiceSettings", { ...editData.voiceSettings, style: Number(e.target.value) })} />
                    <div className="row" style={{ justifyContent: "space-between", color: "var(--c-text-4)", fontSize: 11 }}>
                      <span>Plain</span><span className="mono">{(editData.voiceSettings?.style ?? 0.2).toFixed(2)}</span><span>Expressive</span>
                    </div>
                  </Field>
                  <Field label="Speaking rate" help="How fast the AI talks. 1.00× is natural pace. Slightly slower (0.90–0.95×) often sounds more human on phone calls. Avoid going below 0.80×.">
                    <Input type="range" min="0.7" max="1.3" step="0.05" value={editData.voiceSettings?.speed ?? 0.96} onChange={e => updateField("voiceSettings", { ...editData.voiceSettings, speed: Number(e.target.value) })} />
                    <div className="row" style={{ justifyContent: "space-between", color: "var(--c-text-4)", fontSize: 11 }}>
                      <span>Slower</span><span className="mono">{(editData.voiceSettings?.speed ?? 0.96).toFixed(2)}×</span><span>Faster</span>
                    </div>
                  </Field>
                </div>
                <div className="muted text-11 mt-2">
                  ElevenLabs-style tuning usually sounds more human with lower stability, higher similarity, a little expression, and slightly slower pacing. VoxPro now maps these sliders into provider-specific controls for self-hosted voices too.
                </div>
                <div className="hr" />
                <div className="card-title mb-2">Language model</div>
                <div className="fld-row">
                  <Field label="LLM Provider" help="The AI brain that decides what to say. YTL ILMU is the default and lowest cost. Switch to OpenAI if you need higher accuracy on complex tasks.">
                    <Select value={editData.llmProvider || "ytl"} onChange={e => updateField("llmProvider", e.target.value)}>
                      <option value="ytl">YTL ILMU (primary)</option>
                      <option value="openai">OpenAI</option>
                      <option value="anthropic">Anthropic</option>
                    </Select>
                  </Field>
                  <Field label="Model" help="Override the default model for this assistant. Leave blank to use the server default. Example: ilmu-text-free-v2, gpt-4o-mini.">
                    <Input value={editData.llmModel || ""} onChange={e => updateField("llmModel", e.target.value)} placeholder="e.g. ilmu-text, gpt-4o-mini" />
                  </Field>
                </div>
                <div className="fld-row">
                  <Field label="Reasoning effort" help="YTL ILMU only — how much the model thinks before responding. Low = fastest replies (~1-2s). High = slower but better for complex flows. Use Low for most voice assistants.">
                    <select className="select" value={editData.reasoningEffort ?? "low"} onChange={e => updateField("reasoningEffort", e.target.value)}>
                      <option value="none">None (off)</option>
                      <option value="low">Low — fastest ⚡</option>
                      <option value="medium">Medium</option>
                      <option value="high">High — most thorough</option>
                    </select>
                  </Field>
                  <Field label="Max output tokens" help="Hard cap on reply length. 150–300 is good for voice calls — longer replies cause noticeable delays.">
                    <Input type="number" value={editData.maxTokens ?? 220} onChange={e => updateField("maxTokens", Number(e.target.value))} />
                  </Field>
                </div>
                <div className="fld-row">
                  <Field label="ASR Provider" help="Speech-to-text engine. Deepgram is fastest (100–300ms, streaming WebSocket for live calls). YTL ILMU is optimised for Malay accents. OpenAI Whisper handles the most languages.">
                    <Select value={editData.asrProvider || "ytl"} onChange={e => updateField("asrProvider", e.target.value)}>
                      <option value="deepgram">Deepgram nova-2 (fastest)</option>
                      <option value="ytl">YTL ILMU ASR (best Malay accuracy)</option>
                      <option value="openai">OpenAI Whisper</option>
                    </Select>
                  </Field>
                  <Field label="ASR Language" help="Language the ASR engine listens for. 'Auto-detect' lets the model decide.">
                    <Select value={editData.asrLanguage || ""} onChange={e => updateField("asrLanguage", e.target.value)}>
                      {(editData.asrProvider || "ytl") === "deepgram" ? (<>
                        <option value="">Auto-detect</option>
                        <option value="ms">Malay (ms)</option>
                        <option value="en">English (en)</option>
                        <option value="en-US">English US (en-US)</option>
                        <option value="en-GB">English UK (en-GB)</option>
                        <option value="en-AU">English AU (en-AU)</option>
                        <option value="en-IN">English IN (en-IN)</option>
                        <option value="multi">Multilingual ES+EN (multi)</option>
                        <option value="zh">Chinese Simplified (zh)</option>
                        <option value="zh-TW">Chinese Traditional (zh-TW)</option>
                        <option value="zh-HK">Chinese Cantonese (zh-HK)</option>
                        <option value="id">Indonesian (id)</option>
                        <option value="th">Thai (th)</option>
                        <option value="vi">Vietnamese (vi)</option>
                        <option value="ko">Korean (ko)</option>
                        <option value="ja">Japanese (ja)</option>
                        <option value="hi">Hindi (hi)</option>
                        <option value="ta">Tamil (ta)</option>
                        <option value="fr">French (fr)</option>
                        <option value="fr-CA">French Canada (fr-CA)</option>
                        <option value="de">German (de)</option>
                        <option value="es">Spanish (es)</option>
                        <option value="es-419">Spanish Latin America (es-419)</option>
                        <option value="pt">Portuguese (pt)</option>
                        <option value="pt-BR">Portuguese Brazil (pt-BR)</option>
                        <option value="pt-PT">Portuguese Portugal (pt-PT)</option>
                        <option value="it">Italian (it)</option>
                        <option value="nl">Dutch (nl)</option>
                        <option value="nl-BE">Flemish (nl-BE)</option>
                        <option value="ru">Russian (ru)</option>
                        <option value="pl">Polish (pl)</option>
                        <option value="tr">Turkish (tr)</option>
                        <option value="uk">Ukrainian (uk)</option>
                        <option value="sv">Swedish (sv)</option>
                        <option value="no">Norwegian (no)</option>
                        <option value="da">Danish (da)</option>
                        <option value="fi">Finnish (fi)</option>
                        <option value="el">Greek (el)</option>
                        <option value="hu">Hungarian (hu)</option>
                        <option value="cs">Czech (cs)</option>
                        <option value="sk">Slovak (sk)</option>
                        <option value="ro">Romanian (ro)</option>
                        <option value="bg">Bulgarian (bg)</option>
                        <option value="ca">Catalan (ca)</option>
                        <option value="lv">Latvian (lv)</option>
                        <option value="lt">Lithuanian (lt)</option>
                        <option value="et">Estonian (et)</option>
                      </>) : (editData.asrProvider || "ytl") === "openai" ? (<>
                        <option value="">Auto-detect</option>
                        <option value="ms">Malay (ms)</option>
                        <option value="en">English (en)</option>
                        <option value="zh">Chinese (zh)</option>
                        <option value="id">Indonesian (id)</option>
                        <option value="th">Thai (th)</option>
                        <option value="vi">Vietnamese (vi)</option>
                        <option value="ko">Korean (ko)</option>
                        <option value="ja">Japanese (ja)</option>
                        <option value="hi">Hindi (hi)</option>
                        <option value="ta">Tamil (ta)</option>
                        <option value="fr">French (fr)</option>
                        <option value="de">German (de)</option>
                        <option value="es">Spanish (es)</option>
                        <option value="ar">Arabic (ar)</option>
                      </>) : (<>
                        <option value="auto">Auto-detect</option>
                        <option value="ms">Malay (ms)</option>
                        <option value="en">English (en)</option>
                      </>)}
                    </Select>
                  </Field>
                </div>
                <div className="g-3">
                  <Field label="Temperature" help="How predictable vs creative the AI's replies are. 0.2–0.4 is best for scripted call flows. Higher values make responses vary more — useful for open-ended conversations.">
                    <Input type="range" min="0" max="1" step="0.05" value={editData.temperature ?? 0.4} onChange={e => updateField("temperature", Number(e.target.value))} />
                    <div className="row" style={{ justifyContent: "space-between", color: "var(--c-text-4)", fontSize: 11 }}>
                      <span>Focused</span><span className="mono">{(editData.temperature ?? 0.4).toFixed(2)}</span><span>Creative</span>
                    </div>
                  </Field>
                  <Field label="Max output tokens" help="Hard cap on reply length. 150–300 is good for voice calls — longer replies cause noticeable delays. Increase only if the assistant needs to read out long content.">
                    <Input type="number" value={editData.maxTokens ?? 220} onChange={e => updateField("maxTokens", Number(e.target.value))} />
                  </Field>
                  <Field label="Reasoning effort" help="YTL ILMU only — controls how much the model thinks before responding. Low = fastest, High = most thorough.">
                    <select className="select" value={editData.reasoningEffort ?? "low"} onChange={e => updateField("reasoningEffort", e.target.value)}>
                      <option value="none">None (off)</option>
                      <option value="low">Low (fastest)</option>
                      <option value="medium">Medium</option>
                      <option value="high">High (most thorough)</option>
                    </select>
                  </Field>
                </div>
                <div className="hr" />
                <div className="card-title mb-2">Turn detection (VAD)</div>
                <div className="g-3">
                  <Field label="VAD sensitivity" help="How sensitive the microphone detection is. Higher = picks up quieter speech but may trigger on background noise. Lower = ignores noise but may miss soft-spoken callers. 0.60–0.75 works for most phone lines.">
                    <Input type="range" min="0" max="1" step="0.05" value={editData.vadSensitivity ?? 0.62} onChange={e => updateField("vadSensitivity", Number(e.target.value))} />
                    <div className="row" style={{ justifyContent: "space-between", color: "var(--c-text-4)", fontSize: 11 }}>
                      <span>Low</span><span className="mono">{(editData.vadSensitivity ?? 0.62).toFixed(2)}</span><span>High</span>
                    </div>
                  </Field>
                  <Field label="Min utterance (ms)" help="Minimum audio length before it's treated as real speech. Filters out short clicks and lip smacks. Increase (300–400ms) if the AI is reacting to background noise; decrease if it's cutting off fast speakers.">
                    <Input type="number" value={editData.minUtteranceMs ?? 240} onChange={e => updateField("minUtteranceMs", Number(e.target.value))} />
                  </Field>
                  <Field label="Silence threshold (ms)" help="How long the caller must be quiet before the AI takes its turn. Lower = faster back-and-forth but may interrupt mid-thought. 500–700ms is natural for phone; increase to 900ms+ for elderly callers.">
                    <Input type="number" value={editData.silenceThresholdMs ?? 650} onChange={e => updateField("silenceThresholdMs", Number(e.target.value))} />
                  </Field>
                </div>
              </div>
            )}

            {tab === "behavior" && editData && (
              <div>
                <div className="card-title mb-4">Conversation behavior</div>
                {[
                  { key: "allowInterruption", label: "Allow user to interrupt", desc: "Caller can talk over playback; output stops immediately." },
                  { key: "autoEndOnHangup", label: "Auto end-call on customer hangup", desc: "Disconnect cleanly when caller drops." },
                ].map(r => (
                  <div key={r.key} className="row gap-3" style={{ padding: "14px 0", borderBottom: "1px solid var(--c-line)" }}>
                    <Toggle checked={!!editData[r.key]} onChange={v => updateField(r.key, v)} />
                    <div style={{ flex: 1 }}>
                      <div className="text-12 strong">{r.label}</div>
                      <div className="muted text-11 mt-1">{r.desc}</div>
                    </div>
                  </div>
                ))}
                <Field label="Auto end on silence (seconds)" help="Set 0 to disable">
                  <Input type="number" value={editData.autoEndOnSilenceSec ?? 60} onChange={e => updateField("autoEndOnSilenceSec", Number(e.target.value))} style={{ marginTop: 14 }} />
                </Field>
              </div>
            )}

            {tab === "knowledge" && editData && (
              <KnowledgeTab assistant={editData} editData={editData} updateField={updateField} />
            )}

            {tab === "structured_output" && editData && (
              <StructuredOutputTab assistantId={editData.id} />
            )}

            {tab === "embed" && editData && (
              <EmbedTab assistant={editData} />
            )}

            {tab === "clients" && (
              <div>
                <div className="card-title mb-1">Client assignments</div>
                <div className="card-subtitle mb-4">Toggle which clients can access this assistant.</div>
                {clients.length === 0 ? (
                  <div className="empty-state">
                    <div className="empty-state-title">No clients yet</div>
                    <div>Go to Clients to add your first client.</div>
                  </div>
                ) : (
                  <div className="col">
                    {clients.map(c => {
                      const assigned = (c.assignedAssistantIds || []).includes(selectedId);
                      return (
                        <div key={c.id} className="row gap-3" style={{ padding: "12px 0", borderBottom: "1px solid var(--c-line)", alignItems: "center" }}>
                          <div className="agent-avatar" style={{ width: 32, height: 32, fontSize: 12, borderRadius: 8, flexShrink: 0 }}>
                            {(c.name || "?").slice(0, 2).toUpperCase()}
                          </div>
                          <div style={{ flex: 1, minWidth: 0 }}>
                            <div className="text-13 strong truncate">{c.name}</div>
                            <div className="muted text-11 truncate">{c.company || c.email || "—"}</div>
                          </div>
                          <Toggle checked={assigned} onChange={() => toggleClientAssignment(c.id)} />
                        </div>
                      );
                    })}
                  </div>
                )}
                <div className="muted text-11 mt-4">Changes save immediately. Manage full client details in the Clients section.</div>
              </div>
            )}

            {tab === "advanced" && editData && (
              <div>
                <div style={{ marginBottom: 18 }}>
                  <div className="card-title mb-2">Call log presentation</div>
                  <div className="card-subtitle mb-4">Control which structured outputs are emphasized or hidden for this assistant.</div>
                  <div className="fld-row">
                    <Field label="Display mode">
                      <Select value={editData.logView?.displayMode || "dynamic"} onChange={e => updateField("logView", { ...(editData.logView || {}), displayMode: e.target.value })}>
                        <option value="dynamic">Dynamic by available fields</option>
                        <option value="focused">Focused on selected fields</option>
                      </Select>
                    </Field>
                    <Field label="Table density">
                      <Select value={editData.logView?.tableDensity || "comfortable"} onChange={e => updateField("logView", { ...(editData.logView || {}), tableDensity: e.target.value })}>
                        <option value="comfortable">Comfortable</option>
                        <option value="compact">Compact</option>
                      </Select>
                    </Field>
                  </div>
                  <Field label="Pinned structured fields" help="Comma-separated fields to prioritize in the main table.">
                    <Input
                      value={(editData.logView?.pinnedFields || []).join(", ")}
                      onChange={e => updateField("logView", {
                        ...(editData.logView || {}),
                        pinnedFields: e.target.value.split(",").map(v => v.trim()).filter(Boolean),
                      })}
                      placeholder="expected_payment_date, balance, ptp_amount"
                    />
                  </Field>
                  <Field label="Hidden structured fields" help="Comma-separated fields to hide from the table while keeping them in the raw log and export.">
                    <Input
                      value={(editData.logView?.hiddenFields || []).join(", ")}
                      onChange={e => updateField("logView", {
                        ...(editData.logView || {}),
                        hiddenFields: e.target.value.split(",").map(v => v.trim()).filter(Boolean),
                      })}
                      placeholder="internal_notes, full_summary"
                    />
                  </Field>
                </div>
                <div style={{ marginBottom: 18 }}>
                  <div className="card-title mb-2">Analytics template</div>
                  <div className="card-subtitle mb-4">Used as the starting analytics dashboard for new clients linked to this assistant.</div>
                  <Field label="Default widget order" help="Comma-separated widget ids, for example: volume,outcomes,structured,structuredNumeric">
                    <Input
                      value={(editData.analyticsTemplate?.widgetOrder || []).join(", ")}
                      onChange={e => updateField("analyticsTemplate", {
                        ...(editData.analyticsTemplate || {}),
                        widgetOrder: e.target.value.split(",").map(v => v.trim()).filter(Boolean),
                      })}
                      placeholder="volume, outcomes, structured, structuredNumeric"
                    />
                  </Field>
                  <div className="fld-row">
                    <Field label="Preferred numeric field">
                      <Input
                        value={editData.analyticsTemplate?.numericField || ""}
                        onChange={e => updateField("analyticsTemplate", {
                          ...(editData.analyticsTemplate || {}),
                          numericField: e.target.value,
                        })}
                        placeholder="balance"
                      />
                    </Field>
                    <Field label="Preferred categorical field">
                      <Input
                        value={editData.analyticsTemplate?.categoricalField || ""}
                        onChange={e => updateField("analyticsTemplate", {
                          ...(editData.analyticsTemplate || {}),
                          categoricalField: e.target.value,
                        })}
                        placeholder="intent"
                      />
                    </Field>
                  </div>
                  <div className="fld-row">
                    <Field label="Default range">
                      <Select
                        value={editData.analyticsTemplate?.range || "30d"}
                        onChange={e => updateField("analyticsTemplate", {
                          ...(editData.analyticsTemplate || {}),
                          range: e.target.value,
                        })}
                      >
                        <option value="24h">24h</option>
                        <option value="7d">7d</option>
                        <option value="30d">30d</option>
                        <option value="90d">90d</option>
                      </Select>
                    </Field>
                    <Field label="Default tab">
                      <Select
                        value={editData.analyticsTemplate?.tab || "overview"}
                        onChange={e => updateField("analyticsTemplate", {
                          ...(editData.analyticsTemplate || {}),
                          tab: e.target.value,
                        })}
                      >
                        <option value="overview">Overview</option>
                        <option value="performance">Performance</option>
                        <option value="customer">Customer reporting</option>
                      </Select>
                    </Field>
                  </div>
                </div>
                <div className="card-title mb-2">Raw configuration</div>
                <div className="card-subtitle mb-4">Read-only JSON snapshot of this assistant's config.</div>
                <pre className="code" style={{ fontSize: 11 }}><code>{JSON.stringify(editData, null, 2)}</code></pre>
              </div>
            )}
          </div>
        </div>

        <aside className="builder-right" style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}>
          <div className="builder-right-head" style={{ flexShrink: 0 }}>
            <span className="card-title">Quick test</span>
            <div className="row gap-2">
              <div className="row" style={{ background: "var(--c-surface-2)", borderRadius: 7, padding: 2, border: "1px solid var(--c-line)" }}>
                {[["chat","Chat"],["call","Call"]].map(([m, label]) => (
                  <button key={m} onClick={() => setQtMode(m)} style={{ padding: "2px 10px", borderRadius: 5, fontSize: 11, fontWeight: 500, cursor: "pointer", border: "none", background: qtMode === m ? "var(--a)" : "transparent", color: qtMode === m ? "#0c0c14" : "var(--c-text-4)", transition: "all 0.15s" }}>{label}</button>
                ))}
              </div>
              {qtMode === "chat" && qtMessages.length > 0 && (
                <Btn variant="ghost" size="sm" onClick={qtReset} style={{ fontSize: 10, padding: "2px 8px" }}>Reset</Btn>
              )}
            </div>
          </div>

          {/* CSV upload */}
          <div style={{ padding: "10px 14px", borderBottom: "1px solid var(--c-line)", flexShrink: 0 }}>
            <div className="row gap-2" style={{ marginBottom: 6, justifyContent: "space-between" }}>
              <span className="muted text-11">CSV variables</span>
              {qtCsvRows.length > 0 && (
                <div className="row gap-1" style={{ fontSize: 10 }}>
                  <button className="mono" style={{ padding: "1px 6px", borderRadius: 4, background: "var(--c-surface-2)", border: "1px solid var(--c-line-2)" }}
                    onClick={() => setQtCsvRowIdx(i => Math.max(0, i - 1))} disabled={qtCsvRowIdx === 0}>‹</button>
                  <span className="muted" style={{ padding: "0 4px" }}>Row {qtCsvRowIdx + 1} / {qtCsvRows.length}</span>
                  <button className="mono" style={{ padding: "1px 6px", borderRadius: 4, background: "var(--c-surface-2)", border: "1px solid var(--c-line-2)" }}
                    onClick={() => setQtCsvRowIdx(i => Math.min(qtCsvRows.length - 1, i + 1))} disabled={qtCsvRowIdx === qtCsvRows.length - 1}>›</button>
                </div>
              )}
            </div>
            {qtCsvRows.length === 0 ? (
              <div>
                <label style={{ display: "block", cursor: "pointer" }}>
                  <div style={{ border: "1px dashed var(--c-line-2)", borderRadius: 6, padding: "8px 10px", textAlign: "center", fontSize: 11, color: "var(--c-text-4)" }}>
                    <Icon name="upload" /> Upload CSV to test {"{{"}<span style={{ color: "var(--a)" }}>variables</span>{"}}"}
                  </div>
                  <input type="file" accept=".csv" style={{ display: "none" }} onChange={handleCsvUpload} />
                </label>
                {(() => {
                  const cols = buildCsvCols(activeAssistant?.systemPrompt, activeAssistant?.firstMessage);
                  return cols.length > 0 ? (
                    <div style={{ marginTop: 6, display: "flex", flexWrap: "wrap", gap: 4 }}>
                      {cols.map(c => (
                        <span key={c} style={{ padding: "1px 7px", borderRadius: 10, fontSize: 10, fontFamily: "var(--f-mono)", background: FIXED_CSV_COLS.includes(c) ? "rgba(59,130,246,0.1)" : "rgba(16,185,129,0.1)", color: FIXED_CSV_COLS.includes(c) ? "var(--a)" : "var(--ok)", border: `1px solid ${FIXED_CSV_COLS.includes(c) ? "rgba(59,130,246,0.25)" : "rgba(16,185,129,0.25)"}` }}>
                          {c}
                        </span>
                      ))}
                    </div>
                  ) : null;
                })()}
              </div>
            ) : (
              <div>
                <div className="row gap-2" style={{ marginBottom: 6, justifyContent: "space-between" }}>
                  <span className="text-11 truncate" style={{ color: "var(--ok)", maxWidth: 120 }}>{qtCsvName}</span>
                  <label style={{ cursor: "pointer", fontSize: 10, color: "var(--c-text-4)" }}>
                    Change <input type="file" accept=".csv" style={{ display: "none" }} onChange={handleCsvUpload} />
                  </label>
                </div>
                <div style={{ maxHeight: 70, overflowY: "auto", display: "flex", flexWrap: "wrap", gap: 4 }}>
                  {Object.entries(qtRow).slice(0, 8).map(([k, v]) => (
                    <span key={k} className="chip" style={{ fontSize: 10, maxWidth: 110, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                      <span style={{ color: "var(--a)" }}>{k}</span>: {v}
                    </span>
                  ))}
                  {Object.keys(qtRow).length > 8 && <span className="chip" style={{ fontSize: 10 }}>+{Object.keys(qtRow).length - 8} more</span>}
                </div>
              </div>
            )}
          </div>

          {/* First message preview */}
          {activeAssistant?.firstMessage && (
            <div style={{ padding: "8px 14px", borderBottom: "1px solid var(--c-line)", background: "var(--c-surface)", flexShrink: 0 }}>
              <div className="muted text-10" style={{ marginBottom: 4 }}>First message preview</div>
              <div style={{ fontSize: 11, color: "var(--c-text-2)", lineHeight: 1.5 }}>
                {resolvedFirst || <span className="muted">(empty)</span>}
              </div>
            </div>
          )}

          {qtMode === "chat" ? (<>
          {/* Chat messages */}
          <div ref={qtScrollRef} style={{ flex: 1, overflowY: "auto", padding: "10px 14px", display: "flex", flexDirection: "column", gap: 8 }}>
            {qtMessages.length === 0 && !qtLoading && (
              <div style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 8, color: "var(--c-text-4)", textAlign: "center", padding: 16 }}>
                <div style={{ width: 36, height: 36, borderRadius: 10, background: "var(--c-surface-2)", display: "grid", placeItems: "center", border: "1px solid var(--c-line-2)" }}>
                  <Icon name="message" />
                </div>
                <div className="text-12 strong" style={{ color: "var(--c-text-3)" }}>Chat with {activeAssistant?.name || "this assistant"}</div>
                <div style={{ fontSize: 11 }}>Type a message below to test the agent's responses live.</div>
              </div>
            )}
            {qtMessages.map((msg, i) => (
              <div key={i} style={{ display: "flex", flexDirection: msg.role === "user" ? "row-reverse" : "row", gap: 8, alignItems: "flex-end" }}>
                {msg.role === "assistant" && (
                  <div style={{ width: 22, height: 22, borderRadius: 6, background: "var(--a)", display: "grid", placeItems: "center", fontSize: 9, color: "#0c0c14", flex: "none", fontWeight: 700 }}>
                    {(activeAssistant?.name || "AI").slice(0, 2).toUpperCase()}
                  </div>
                )}
                <div style={{
                  maxWidth: "82%", padding: "7px 11px", borderRadius: msg.role === "user" ? "12px 12px 3px 12px" : "12px 12px 12px 3px",
                  background: msg.role === "user" ? "var(--a)" : "var(--c-surface-2)",
                  color: msg.role === "user" ? "#0c0c14" : "var(--c-text)",
                  fontSize: 12, lineHeight: 1.5,
                  border: msg.role === "assistant" ? "1px solid var(--c-line)" : "none",
                }}>
                  {humanizeTranscript(msg.text)}
                </div>
              </div>
            ))}
            {qtLoading && (
              <div style={{ display: "flex", gap: 8, alignItems: "flex-end" }}>
                <div style={{ width: 22, height: 22, borderRadius: 6, background: "var(--a)", display: "grid", placeItems: "center", fontSize: 9, color: "#0c0c14", flex: "none", fontWeight: 700 }}>
                  {(activeAssistant?.name || "AI").slice(0, 2).toUpperCase()}
                </div>
                <div style={{ padding: "10px 14px", borderRadius: "12px 12px 12px 3px", background: "var(--c-surface-2)", border: "1px solid var(--c-line)", display: "flex", gap: 4, alignItems: "center" }}>
                  {[0,1,2].map(d => <span key={d} style={{ width: 5, height: 5, borderRadius: "50%", background: "var(--c-text-4)", display: "inline-block", animation: "pulse 1.2s ease-in-out infinite", animationDelay: `${d * 0.2}s` }} />)}
                </div>
              </div>
            )}
            {qtError && (
              <div style={{ fontSize: 11, color: "var(--err)", padding: "6px 10px", background: "rgba(239,68,68,0.08)", borderRadius: 6, border: "1px solid rgba(239,68,68,0.2)" }}>
                {qtError}
              </div>
            )}
          </div>

          {/* Input bar */}
          <div style={{ padding: "10px 14px", borderTop: "1px solid var(--c-line)", flexShrink: 0, background: "var(--c-bg)" }}>
            <div className="row gap-2">
              <textarea
                value={qtInput}
                onChange={e => setQtInput(e.target.value)}
                onKeyDown={onQtKeyDown}
                placeholder="Type a message… (Enter to send)"
                rows={2}
                style={{
                  flex: 1, resize: "none", background: "var(--c-surface)", border: "1px solid var(--c-line-2)",
                  borderRadius: 8, padding: "7px 10px", fontSize: 12, color: "var(--c-text)",
                  fontFamily: "var(--f-sans)", outline: "none", lineHeight: 1.5,
                }}
              />
              <button
                onClick={sendQtMessage}
                disabled={!qtInput.trim() || qtLoading}
                style={{
                  width: 36, height: 36, borderRadius: 9, background: qtInput.trim() && !qtLoading ? "var(--a)" : "var(--c-surface-2)",
                  color: qtInput.trim() && !qtLoading ? "#0c0c14" : "var(--c-text-4)",
                  border: "1px solid var(--c-line-2)", cursor: qtInput.trim() && !qtLoading ? "pointer" : "default",
                  display: "grid", placeItems: "center", flex: "none", alignSelf: "flex-end", transition: "all 0.15s",
                }}
              >
                <Icon name="send" />
              </button>
            </div>
            <div className="row mt-1" style={{ justifyContent: "space-between", fontSize: 10, color: "var(--c-text-4)" }}>
              <span>{activeAssistant?.language || "en"} · {activeAssistant?.voiceProvider || "elevenlabs"}</span>
              <span>{qtMessages.filter(m => m.role === "user").length} turns</span>
            </div>
          </div>
          </>) : (<>
          {/* idle / connecting — centered column */}
          {(callState === "idle" || callState === "ended" || callState === "connecting") && (
            <div style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 20, padding: 24 }}>
              {callState === "connecting" ? (<>
                <div style={{ width: 64, height: 64, borderRadius: "50%", border: "3px solid var(--a)", display: "grid", placeItems: "center", color: "var(--a)", animation: "pulse 1.5s ease-in-out infinite" }}>
                  <Icon name="phone" size={24} />
                </div>
                <div style={{ fontSize: 12, color: "var(--c-text-3)" }}>Connecting to agent...</div>
              </>) : (<>
                <div style={{ width: 56, height: 56, borderRadius: 16, background: "var(--c-surface-2)", display: "grid", placeItems: "center", border: "1px solid var(--c-line-2)", color: "var(--c-text-3)" }}>
                  <Icon name="phone" size={24} />
                </div>
                <div style={{ textAlign: "center" }}>
                  <div className="text-12 strong" style={{ color: "var(--c-text-2)", marginBottom: 4 }}>
                    {callState === "ended" ? "Call ended" : `Talk to ${activeAssistant?.name || "the assistant"}`}
                  </div>
                  <div style={{ fontSize: 11, color: "var(--c-text-4)" }}>
                    {callState === "ended" ? "Ready to call again?" : "Your browser mic connects to the AI agent live"}
                  </div>
                </div>
                <button onClick={startCall} style={{ padding: "10px 24px", borderRadius: 24, background: "var(--a)", color: "#0c0c14", border: "none", cursor: "pointer", fontSize: 13, fontWeight: 600, display: "flex", alignItems: "center", gap: 8 }}>
                  <Icon name="phone" /> {callState === "ended" ? "Call again" : "Start Call"}
                </button>
                {qtError && <div style={{ fontSize: 11, color: "var(--err)", padding: "6px 10px", background: "rgba(239,68,68,0.08)", borderRadius: 6, border: "1px solid rgba(239,68,68,0.2)", textAlign: "center", maxWidth: 220 }}>{qtError}</div>}
              </>)}
            </div>
          )}

          {/* live call — transcript scrolls, buttons stay pinned at bottom */}
          {callState === "live" && (
            <div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", overflow: "hidden" }}>
              <div style={{ flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center", gap: 8, padding: "10px 0", borderBottom: "1px solid var(--c-line)" }}>
                <span style={{ width: 8, height: 8, borderRadius: "50%", background: "var(--ok)", display: "inline-block", animation: "pulse 1.5s ease-in-out infinite" }} />
                <span style={{ fontSize: 12, color: "var(--ok)", fontWeight: 600 }}>Live</span>
              </div>
              <div ref={callTranscriptRef} style={{ flex: 1, minHeight: 0, overflowY: "auto", display: "flex", flexDirection: "column", gap: 6, padding: "10px 14px" }}>
                {callTranscript.length === 0 && (
                  <div style={{ textAlign: "center", fontSize: 11, color: "var(--c-text-4)", marginTop: 20 }}>Speak now — transcript will appear here</div>
                )}
                {callTranscript.map((msg, i) => (
                  <div key={i} style={{ display: "flex", flexDirection: msg.role === "user" ? "row-reverse" : "row", gap: 6, alignItems: "flex-end" }}>
                    <div style={{ maxWidth: "85%", padding: "6px 10px", borderRadius: msg.role === "user" ? "10px 10px 3px 10px" : "10px 10px 10px 3px", background: msg.role === "user" ? "var(--a)" : "var(--c-surface-2)", color: msg.role === "user" ? "#0c0c14" : "var(--c-text)", fontSize: 12, lineHeight: 1.5, border: msg.role === "assistant" ? "1px solid var(--c-line)" : "none" }}>
                      {humanizeTranscript(msg.text)}
                      {msg.ms != null && (
                        <div style={{ fontSize: 9, color: msg.role === "user" ? "rgba(12,12,20,0.45)" : "var(--c-text-4)", marginTop: 3, textAlign: "right", letterSpacing: "0.02em" }}>
                          {msg.ms >= 1000 ? `${(msg.ms / 1000).toFixed(1)}s` : `${msg.ms}ms`}
                        </div>
                      )}
                    </div>
                  </div>
                ))}
              </div>
              <div style={{ flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center", gap: 12, padding: "12px 16px", borderTop: "1px solid var(--c-line)" }}>
                <button onClick={toggleMic} title={micMuted ? "Unmute" : "Mute"} style={{ width: 40, height: 40, borderRadius: "50%", background: micMuted ? "rgba(239,68,68,0.12)" : "var(--c-surface-2)", border: micMuted ? "1px solid rgba(239,68,68,0.35)" : "1px solid var(--c-line-2)", cursor: "pointer", display: "grid", placeItems: "center", color: micMuted ? "var(--err)" : "var(--c-text-3)" }}>
                  <Icon name={micMuted ? "micOff" : "mic"} />
                </button>
                <button onClick={endCall} style={{ padding: "8px 18px", borderRadius: 24, background: "#dc2626", color: "#fff", border: "none", cursor: "pointer", fontSize: 13, fontWeight: 600, display: "flex", alignItems: "center", gap: 6 }}>
                  <Icon name="hangup" /> End Call
                </button>
              </div>
            </div>
          )}
          </>)}
        </aside>
      </div>
    </div>
  );
};

// ---------- Voice Studio ----------
const VoiceStudioView = () => {
  const [selected, setSelected] = React.useState("v1");
  const [providerFilter, setProviderFilter] = React.useState("all");

  const voices = [
    { id: "v1", provider: "ElevenLabs", name: "Adam", lang: "EN", gender: "M", duration: "0:08", featured: true },
    { id: "v2", provider: "ElevenLabs", name: "Rachel", lang: "EN", gender: "F", duration: "0:09" },
    { id: "v3", provider: "OpenAI", name: "alloy", lang: "EN", gender: "N", duration: "0:07", featured: true },
    { id: "v4", provider: "OpenAI", name: "nova", lang: "EN", gender: "F", duration: "0:08" },
    { id: "v5", provider: "OpenAI", name: "echo", lang: "EN", gender: "M", duration: "0:08" },
    { id: "v6", provider: "Mesolitica", name: "husein", lang: "MS", gender: "M", duration: "0:11", featured: true },
    { id: "v7", provider: "Mesolitica", name: "haziq", lang: "MS", gender: "M", duration: "0:10" },
    { id: "v8", provider: "Piper", name: "lessac", lang: "EN", gender: "M", duration: "0:09" },
  ];

  const filtered = providerFilter === "all" ? voices : voices.filter(v => v.provider.toLowerCase() === providerFilter);
  const v = voices.find(x => x.id === selected) || voices[0];

  return (
    <div>
      <div className="ph">
        <div>
          <h1 className="ph-title">Voice studio</h1>
          <div className="ph-subtitle">Audition voices, tune cadence — all in one place.</div>
        </div>
      </div>

      <div className="row gap-2 mb-4">
        <Segmented value={providerFilter} onChange={setProviderFilter} options={[
          { value: "all", label: "All" }, { value: "openai", label: "OpenAI" },
          { value: "elevenlabs", label: "ElevenLabs" }, { value: "mesolitica", label: "Mesolitica" },
          { value: "piper", label: "Piper" },
        ]} />
        <span className="chip">{filtered.length} voices</span>
      </div>

      <div className="g-2-1">
        <div className="g-3">
          {filtered.map(voice => (
            <div key={voice.id} className="card" style={{ padding: 16, cursor: "pointer", borderColor: voice.id === selected ? "var(--a-line)" : "var(--c-line)", boxShadow: voice.id === selected ? "var(--shadow-glow)" : "none" }} onClick={() => setSelected(voice.id)}>
              <div className="row gap-3">
                <button style={{ width: 36, height: 36, borderRadius: "50%", background: voice.id === selected ? "var(--a)" : "var(--c-surface-2)", color: voice.id === selected ? "#0c0c14" : "var(--c-text)", display: "grid", placeItems: "center", flex: "none", border: "1px solid var(--c-line-2)" }}>
                  <Icon name="play" />
                </button>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div className="row" style={{ justifyContent: "space-between" }}>
                    <span className="strong text-12">{voice.name}</span>
                    {voice.featured && <Badge tone="accent">Featured</Badge>}
                  </div>
                  <div className="muted text-11 truncate">{voice.provider} · {voice.lang} · {voice.gender}</div>
                </div>
              </div>
              <div className="spark mt-3" style={{ height: 22 }}>
                {Array.from({ length: 30 }).map((_, i) => <i key={i} className={voice.id === selected ? "hi" : ""} style={{ height: `${20 + Math.abs(Math.sin(i*0.5 + voice.id.charCodeAt(0)) * 80)}%` }} />)}
              </div>
              <div className="row mt-3" style={{ justifyContent: "space-between" }}>
                <span className="chip">{voice.duration}</span>
                <Btn variant="ghost" size="sm">Use →</Btn>
              </div>
            </div>
          ))}
        </div>

        <Card title="Try it" subtitle={`${v.provider} · ${v.name}`}>
          <Field label="Sample text">
            <Textarea defaultValue="Salam sejahtera, boleh saya bantu anda hari ini?" style={{ minHeight: 100 }} />
          </Field>
          <div className="fld-row">
            <Field label="Speaking rate"><Input type="range" defaultValue="1" min="0.7" max="1.3" step="0.05" /></Field>
            <Field label="Pitch"><Input type="range" defaultValue="0" min="-1" max="1" step="0.1" /></Field>
          </div>
          <Btn variant="primary" icon="play" style={{ width: "100%", justifyContent: "center" }}>Generate preview</Btn>
          <div className="hr" />
          <div className="card-title mb-2">Cost estimate</div>
          <div className="row" style={{ justifyContent: "space-between", fontSize: 12 }}>
            <span className="muted">Per minute spoken</span>
            <span className="mono">RM 0.040 – 0.090</span>
          </div>
          <div className="row mt-2" style={{ justifyContent: "space-between", fontSize: 12 }}>
            <span className="muted">First-byte latency</span>
            <span className="mono">~180 – 240ms</span>
          </div>
        </Card>
      </div>
    </div>
  );
};

Object.assign(window, { AssistantsView, VoiceStudioView });
