// ============ CITRUS HOME · CONVERSATIONAL OS ============
// Conversation-first landing surface for owners/admins.
// Reads from dashboard props + live API (insights, approvals).
// Cross-session LLM memory via localStorage.
/* eslint-disable no-console */
console.log("[citrus-home] v=28 loaded");

function citrusRiskForAction(text) {
  const t = String(text || "").toLowerCase();
  if (/\b(delete|remove|billing|price|pricing|owner|team member|wipe)\b/.test(t)) return "high";
  if (/\b(pause|activate|deactivate|change|update|set|give|invite|create)\b/.test(t)) return "moderate";
  return "low";
}

function citrusCommandMeta(raw) {
  const clean = String(raw || "").trim();
  if (!clean.startsWith("/")) return null;
  const [cmd, ...rest] = clean.split(/\s+/);
  return { cmd: cmd.toLowerCase(), args: rest.join(" ").trim() };
}

function citrusApiBase() {
  const configured = typeof window !== "undefined" && window.CITRUS_CONFIG && window.CITRUS_CONFIG.SERVER_URL;
  if (configured) return configured;
  const origin = window.location.origin || "";
  if (/^https?:\/\/localhost:(8000|8765)$/.test(origin)) return "http://localhost:3001";
  return origin;
}

async function citrusJson(path, opts = {}) {
  const r = await fetch(citrusApiBase().replace(/\/$/, "") + path, {
    credentials: "include",
    ...opts,
    headers: { "content-type": "application/json", ...(opts.headers || {}) },
  });
  const text = await r.text();
  let data = {};
  try { data = text ? JSON.parse(text) : {}; } catch { data = { error: text }; }
  if (!r.ok) throw new Error(data.error || `Request failed (${r.status})`);
  return data;
}

// ---- localStorage keys (scoped per tenant) ----
function citrusHistoryKey(agents) {
  const biz = (agents && agents[0] && agents[0].businessId) ? agents[0].businessId : "default";
  return `citrus-llm-history-v1-${biz}`;
}
function citrusMessagesKey(agents) {
  const biz = (agents && agents[0] && agents[0].businessId) ? agents[0].businessId : "default";
  return `citrus-messages-v1-${biz}`;
}
function citrusUpdateKey(agents) {
  const biz = (agents && agents[0] && agents[0].businessId) ? agents[0].businessId : "default";
  return `citrus-last-update-v1-${biz}`;
}

// ---- health score (0–100) from live agent + thread data ----
function computeHealthScore(agents, threads) {
  const live = agents.filter((a) => a.status === "live");
  if (!live.length) return null;
  const avgAcc = live.reduce((s, a) => s + (a.accuracy || 82), 0) / live.length;
  const escalations = threads.filter((t) => t.status === "needs-you").length;
  const escPenalty  = Math.min((escalations / Math.max(threads.length, 1)) * 30, 20);
  return Math.max(0, Math.min(100, Math.round(avgAcc - escPenalty)));
}

// ---- health breakdown (explains WHY the score is what it is) ----
function buildHealthBreakdown(agents, threads) {
  const live = agents.filter((a) => a.status === "live");
  if (!live.length) return null;
  const avgAcc      = live.reduce((s, a) => s + (a.accuracy || 82), 0) / live.length;
  const escalations = threads.filter((t) => t.status === "needs-you").length;
  const totalConvos = Math.max(threads.length, 1);
  const escRate     = escalations / totalConvos;
  const escPenalty  = Math.min(escRate * 30, 20);
  const score       = Math.max(0, Math.min(100, Math.round(avgAcc - escPenalty)));

  const agentsSorted = live.slice().sort((a, b) => (a.accuracy || 82) - (b.accuracy || 82));
  const weakAgents   = agentsSorted.filter((a) => (a.accuracy || 82) < 80);

  const suggestions = [];
  if (weakAgents.length) {
    const w = weakAgents[0];
    suggestions.push({ icon: "📉", text: `${w.name} has the lowest accuracy at ${w.accuracy || 82}% — add learnings or review recent conversations.`, agentId: w.id });
  }
  if (escRate > 0.1) {
    suggestions.push({ icon: "⚡", text: `${Math.round(escRate * 100)}% of conversations are escalating — identify the common topics and train your agents on them.` });
  }
  if (avgAcc < 80 && !weakAgents.length) {
    suggestions.push({ icon: "📚", text: "Overall accuracy is below 80% — check the Playbooks for gaps and add missing information." });
  }
  if (!suggestions.length) {
    suggestions.push({ icon: "✅", text: "Agents are performing well. Keep approving learnings to push the score higher." });
  }

  return {
    score,
    avgAcc:     Math.round(avgAcc),
    escPenalty: Math.round(escPenalty),
    escalations,
    totalConvos: threads.length,
    escRatePct:  Math.round(escRate * 100),
    agentsSorted,
    weakAgents,
    suggestions,
  };
}

// ---- morning brief text (dynamic) ----
function buildBriefText(agents, threads, pendingApprovals) {
  const live       = agents.filter((a) => a.status === "live");
  const escalations = threads.filter((t) => t.status === "needs-you").length;
  const lowRisk    = pendingApprovals.filter((a) => !a.riskLevel || a.riskLevel === "low").length;
  const health     = computeHealthScore(agents, threads);
  const weakest    = live.slice().sort((a, b) => (a.accuracy || 82) - (b.accuracy || 82))[0];

  const parts = ["Morning. Here's your operating brief:"];
  if (threads.length)       parts.push(`• ${threads.length} conversation${threads.length !== 1 ? "s" : ""} active${escalations ? ` — ${escalations} need${escalations === 1 ? "s" : ""} your attention` : ""}`);
  if (pendingApprovals.length) parts.push(`• ${pendingApprovals.length} learning approval${pendingApprovals.length !== 1 ? "s" : ""} pending${lowRisk ? ` — ${lowRisk} low-risk (approve all below)` : ""}`);
  if (live.length)           parts.push(`• ${live.length} agent${live.length !== 1 ? "s" : ""} live${health !== null ? ` — avg health ${health}%` : ""}${weakest && (weakest.accuracy || 82) < 75 ? ` · ${weakest.name} needs attention` : ""}`);
  if (parts.length === 1)    parts.push("All quiet. No escalations or pending approvals.");
  return parts.join("\n");
}

// ---- briefing-first morning brief (structured object for CitrusMorningBriefing) ----
function buildMorningBriefing(agents, threads, pendingApprovals, userRole, authUser) {
  const now = new Date();
  const DAY_NAMES   = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
  const MONTH_NAMES = ["January","February","March","April","May","June","July","August","September","October","November","December"];
  const name    = authUser?.name ? authUser.name.split(" ")[0] : null;
  const dateStr = `${DAY_NAMES[now.getDay()]}, ${MONTH_NAMES[now.getMonth()]} ${now.getDate()}.`;

  // Viewer: calm greeting only
  if (userRole === "Viewer") {
    return {
      name, dateStr,
      dayFrame: "Everything looks good. No actions needed from you today.",
      priorityItems: [],
      worthKnowing: null,
      pills: ["How are the agents performing?", "Show me recent conversations", "What happened overnight?"],
    };
  }

  const live        = agents.filter((a) => a.status === "live");
  const escalations = threads.filter((t) => t.status === "needs-you");
  const hotLeads    = threads.filter((t) =>
    (t.categories || []).some((c) => String(c).toLowerCase().includes("hot")) ||
    (t.status !== "needs-you" && t.unread)
  );
  const lowRisk     = pendingApprovals.filter((a) => !a.riskLevel || a.riskLevel === "low");
  const agentName   = live[0]?.name || "your agent";
  const total       = threads.length;

  // Dynamic day frame (quiet / normal / busy / crisis)
  let dayFrame;
  if (escalations.length >= 3) {
    dayFrame = `${total} conversation${total !== 1 ? "s" : ""} came in overnight — ${escalations.length} escalated and need your attention before anything else.`;
  } else if (escalations.length > 0) {
    dayFrame = `Busy night — ${agentName} handled ${total} conversation${total !== 1 ? "s" : ""}${hotLeads.length ? ` and captured ${hotLeads.length} hot customer${hotLeads.length !== 1 ? "s" : ""}` : ""}. ${escalations.length} escalation${escalations.length !== 1 ? "s" : ""} to review.`;
  } else if (total >= 10) {
    dayFrame = `Good night overall — ${agentName} handled ${total} conversation${total !== 1 ? "s" : ""}${hotLeads.length ? ` and captured ${hotLeads.length} hot customer${hotLeads.length !== 1 ? "s" : ""}` : ""}. Nothing escalated.`;
  } else if (total > 0) {
    dayFrame = `Quiet night overall — ${agentName} handled ${total} conversation${total !== 1 ? "s" : ""}${hotLeads.length ? ` and captured ${hotLeads.length} hot customer${hotLeads.length !== 1 ? "s" : ""}` : ""}. Nothing escalated.`;
  } else {
    dayFrame = `Quiet night — no new conversations. Your ${live.length > 0 ? `${live.length} agent${live.length !== 1 ? "s are" : " is"}` : "agents are"} standing by.`;
  }

  // Priority items (numbered prose)
  const priorityItems = [];
  if (escalations.length > 0) {
    const who = escalations[0]?.customerName || escalations[0]?.customer || "a customer";
    priorityItems.push(`${escalations.length} conversation${escalations.length !== 1 ? "s" : ""} escalated and need${escalations.length === 1 ? "s" : ""} your reply — ${who} is waiting.`);
  }
  if (hotLeads.length > 0) {
    const leadName = hotLeads[0]?.customerName || hotLeads[0]?.customer || null;
    priorityItems.push(
      hotLeads.length > 1
        ? `${hotLeads.length} hot customers came in overnight — ${agentName} captured them without escalation.`
        : `${leadName ? `${leadName} came in as a hot customer` : "A hot customer came in"} overnight — ${agentName} captured it without escalation.`
    );
  }
  if (pendingApprovals.length > 0) {
    priorityItems.push(`${pendingApprovals.length} learning approval${pendingApprovals.length !== 1 ? "s" : ""} pending${lowRisk.length ? ` — ${lowRisk.length} are low-risk and can be cleared in one tap` : ""}.`);
  }
  if (priorityItems.length === 0 && live.length > 0) {
    priorityItems.push(`${live.length} agent${live.length !== 1 ? "s" : ""} live and handling conversations. All clear, no action needed.`);
  }
  // Fill to 3 with health observation if we have room
  if (priorityItems.length < 3) {
    const health = computeHealthScore(agents, threads);
    if (health !== null) {
      priorityItems.push(
        health >= 85
          ? `Business health is at ${health}% — strong. Keep approving learnings to push it higher.`
          : health >= 70
          ? `Business health is at ${health}% — decent but has room to improve. Check the Agents tab for specifics.`
          : `Business health is at ${health}% — below target. Review agent accuracy and common escalation topics.`
      );
    }
  }

  // Worth knowing — one interesting observation
  const lowAccAgent = live.filter((a) => (a.accuracy || 82) < 75).sort((a, b) => (a.accuracy || 82) - (b.accuracy || 82))[0];
  const worthKnowing = lowAccAgent
    ? `${lowAccAgent.name}'s accuracy has slipped to ${lowAccAgent.accuracy}% — worth checking recent conversations to see what's tripping them up.`
    : null;

  // Follow-up pills (AC8: "Walk me through the N approvals")
  const pills = [];
  if (hotLeads.length > 0)         pills.push(`Show me the hot customer${hotLeads.length > 1 ? "s" : ""}`);
  if (pendingApprovals.length > 0) pills.push(`Walk me through the ${pendingApprovals.length} approval${pendingApprovals.length !== 1 ? "s" : ""}`);
  if (live.length > 0)             pills.push(`How's ${live[0].name} doing this week?`);
  if (pills.length < 3)            pills.push("What happened overnight?");
  if (pills.length < 3)            pills.push("Show me recent conversations");

  return { name, dateStr, dayFrame, priorityItems: priorityItems.slice(0, 4), worthKnowing, pills: pills.slice(0, 3) };
}

// ======================================================================
// CONVERSATIONAL ONBOARDING
// ======================================================================

const OB_PHASES = [
  { id: 1, label: "Business" },
  { id: 2, label: "Contacts" },
  { id: 3, label: "Agent" },
  { id: 4, label: "Playbooks" },
  { id: 5, label: "Escalation" },
  { id: 6, label: "Launch" },
];

function OnboardingProgressStrip({ phase }) {
  return (
    <div className="ob-progress">
      {OB_PHASES.map((p) => (
        <div key={p.id} className={`ob-phase-item${p.id < phase ? " ob-phase-done" : p.id === phase ? " ob-phase-active" : ""}`}>
          <div className="ob-phase-dot">{p.id < phase ? "✓" : p.id}</div>
          <span className="ob-phase-label">{p.label}</span>
        </div>
      ))}
    </div>
  );
}

function OnboardingKnowledgeUpload({ onDoneUploading }) {
  const [files, setFiles] = React.useState([]);
  const [uploading, setUploading] = React.useState(false);
  const inputRef = React.useRef(null);

  const BINARY_EXTS = new Set(["pdf", "docx", "doc", "xlsx", "xls"]);
  const kindFor = (name) => {
    const ext = name.toLowerCase().split(".").pop();
    if (ext === "pdf") return "pdf";
    if (ext === "csv") return "csv";
    if (["xlsx", "xls"].includes(ext)) return "sheet";
    return "doc";
  };

  const upload = async (file) => {
    setUploading(true);
    try {
      const isBinary = BINARY_EXTS.has(file.name.toLowerCase().split(".").pop());
      const content = await new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onerror = reject;
        if (isBinary) {
          reader.onload = (e) => resolve(e.target.result.split(",")[1]);
          reader.readAsDataURL(file);
        } else {
          reader.onload = (e) => resolve(e.target.result);
          reader.readAsText(file);
        }
      });
      const r = await fetch(`${citrusApiBase()}/knowledge`, {
        method: "POST",
        credentials: "include",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          name:        file.name,
          kind:        kindFor(file.name),
          content,
          contentType: file.type || "text/plain",
          encoding:    isBinary ? "base64" : undefined,
        }),
      });
      if (r.ok) {
        const d = await r.json();
        setFiles((prev) => [...prev, { name: file.name, id: d.id || d._id }]);
      }
    } catch {}
    setUploading(false);
  };

  return (
    <div className="ob-upload-block">
      <p className="ob-upload-hint">Upload PDFs, FAQs, or SOPs so your agent can answer customer questions accurately.</p>
      <div className="ob-upload-row">
        <button className="btn btn-ghost btn-sm" disabled={uploading} onClick={() => inputRef.current?.click()}>
          {uploading ? "Uploading…" : "+ Add document"}
        </button>
        <input ref={inputRef} type="file" accept=".pdf,.txt,.docx,.md" style={{ display: "none" }}
          onChange={(e) => { if (e.target.files[0]) upload(e.target.files[0]); }} />
        {files.length > 0 && (
          <button className="btn btn-primary btn-sm" onClick={() => onDoneUploading && onDoneUploading(files)}>
            Done ({files.length} uploaded)
          </button>
        )}
        {files.length === 0 && (
          <button className="btn btn-ghost btn-sm ob-skip-btn" onClick={() => onDoneUploading && onDoneUploading([])}>Skip for now</button>
        )}
      </div>
      {files.map((f) => (
        <div key={f.id} className="ob-upload-file">✓ {f.name}</div>
      ))}
    </div>
  );
}

function OnboardingPhoneInput({ onSubmitted }) {
  const [phoneNumberId, setPhoneNumberId] = React.useState("");
  const [displayPhone, setDisplayPhone] = React.useState("");
  const [accessToken, setAccessToken] = React.useState("");
  const [appSecret, setAppSecret] = React.useState("");
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState("");

  const submit = async () => {
    if (!phoneNumberId.trim() || !accessToken.trim()) {
      setError("Phone Number ID and Access Token are required.");
      return;
    }
    setSaving(true);
    setError("");
    try {
      const r = await fetch(`${citrusApiBase()}/config/whatsapp`, {
        method: "PATCH",
        credentials: "include",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ phoneNumberId: phoneNumberId.trim(), displayPhoneNumber: displayPhone.trim(), accessToken: accessToken.trim(), appSecret: appSecret.trim() }),
      });
      if (!r.ok) {
        const d = await r.json().catch(() => ({}));
        setError(d.error || `Save failed (${r.status})`);
      } else {
        onSubmitted && onSubmitted();
      }
    } catch (e) {
      setError(e.message);
    }
    setSaving(false);
  };

  return (
    <div className="ob-phone-block">
      <p className="ob-phone-hint">Connect your WhatsApp Business number using Meta Cloud API credentials.</p>
      <div className="ob-phone-fields">
        <input className="ob-phone-input" placeholder="Phone Number ID" value={phoneNumberId} onChange={(e) => setPhoneNumberId(e.target.value)} />
        <input className="ob-phone-input" placeholder="Display phone (e.g. +1 555 123 4567)" value={displayPhone} onChange={(e) => setDisplayPhone(e.target.value)} />
        <input className="ob-phone-input" placeholder="Permanent Access Token" type="password" value={accessToken} onChange={(e) => setAccessToken(e.target.value)} />
        <input className="ob-phone-input" placeholder="App Secret (optional)" type="password" value={appSecret} onChange={(e) => setAppSecret(e.target.value)} />
      </div>
      {error && <p className="ob-phone-error">{error}</p>}
      <button className="btn btn-primary" disabled={saving || !phoneNumberId.trim() || !accessToken.trim()} onClick={submit}>
        {saving ? "Connecting…" : "Connect WhatsApp"}
      </button>
    </div>
  );
}

// Live "what we're building" pane — reflects cfg.onboarding.capturedData as
// each phase's CAPTURE marker lands, so the owner watches the agent take
// shape instead of seeing one static summary only at the very end.
function ArchitectBuildPane({ capturedData }) {
  const d = capturedData || {};
  const rows = [];
  if (d.brief?.name || d.brief?.industry) {
    rows.push({ label: "Business", value: [d.brief?.name, d.brief?.industry].filter(Boolean).join(" · ") });
  }
  if (d.brief?.description) rows.push({ label: "What they do", value: d.brief.description });
  if (d.brief?.languages?.length) rows.push({ label: "Languages", value: d.brief.languages.join(", ") });
  if (d.customerProfile?.topUseCases?.length) rows.push({ label: "Top use cases", value: d.customerProfile.topUseCases.join(", ") });
  if (d.agentName) rows.push({ label: "Agent name", value: d.agentName + (d.voice ? ` · ${d.voice}` : "") });
  if (d.knowledgeReady) rows.push({ label: "Knowledge base", value: "Uploaded" });
  if (d.escalation?.contactName) rows.push({ label: "Escalates to", value: d.escalation.contactName });
  if (d.launchState) rows.push({ label: "WhatsApp", value: d.launchState === "pending_channel" ? "Connecting…" : d.launchState });

  return (
    <div className="arch-build-pane">
      <div className="arch-build-head">
        <span className="arch-build-title">Building your agent</span>
      </div>
      {rows.length === 0 ? (
        <p className="arch-build-empty">This fills in as we talk — nothing to show yet.</p>
      ) : (
        <div className="arch-build-rows">
          {rows.map((r) => (
            <div key={r.label} className="arch-build-row">
              <span className="arch-build-label">{r.label}</span>
              <span className="arch-build-value">{r.value}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// One-time welcome moment shown before the phase-1 conversation starts.
// Shown only when the tenant is genuinely fresh (needs:true, inProgress:
// false — no message has been sent yet, since cfg.onboarding.phase is only
// created on the first /api/onboarding/chat/stream call). Once a message is
// sent, inProgress flips true and this is skipped on every future visit —
// no new persistence needed, this reuses the existing needs/inProgress
// signals from GET /onboarding/status.
function ArchitectSplash({ ownerName, onStart, onDefer }) {
  return (
    <div className="arch-splash">
      <div className="arch-splash-card">
        <div className="arch-splash-logo">Citrus</div>
        <h1 className="arch-splash-h">Welcome to Citrus{ownerName ? `, ${ownerName}` : ""}</h1>
        <p className="arch-splash-body">
          Setting up your workspace happens through a natural conversation. Citrus Architect will guide
          you through your business, your customers, and how you want your agent to sound — producing
          a tailored agent in about 15 minutes.
        </p>
        <div className="arch-splash-preview">
          <div className="arch-splash-preview-label">CITRUS ARCHITECT</div>
          <div className="arch-splash-preview-msg">Hi! I'm here to help you configure your first agent. What does your business do, and who are your customers?</div>
        </div>
        <div className="arch-splash-actions">
          <button className="btn btn-primary" onClick={onStart}>Let's start the conversation</button>
          <button className="arch-splash-later" onClick={onDefer}>I'll do this later</button>
        </div>
        <p className="arch-splash-caption">You can pause and come back anytime.</p>
        <div className="arch-splash-trust">
          <span>🛡 Enterprise-grade security</span>
          <span>🔒 End-to-end encrypted</span>
        </div>
      </div>
    </div>
  );
}

function ConversationalOnboarding({ initialStatus, onComplete }) {
  const initPhase = (initialStatus?.phase) || 1;
  const [phase, setPhase] = React.useState(initPhase);
  const [capturedData, setCapturedData] = React.useState(initialStatus?.capturedData || {});
  const [messages, setMessages] = React.useState(() => {
    const greeting = initPhase > 1
      ? `Welcome back! You're on phase ${initPhase}. Let's continue from where we left off.`
      : "Hi, I'm Citrus Architect. Let's set up your AI agent fleet in 6 easy steps. First — tell me about your business. What do you do, and who are your customers?";
    return [{ from: "citrus", text: greeting, kind: "answer" }];
  });
  const [draft, setDraft] = React.useState("");
  const [streaming, setStreaming] = React.useState(false);
  const [channelConfirmed, setChannelConfirmed] = React.useState(false);
  const [showPhone, setShowPhone] = React.useState(phase === 6);
  const streamRef = React.useRef(null);
  const inputRef = React.useRef(null);
  const uploadInputRef = React.useRef(null);
  const msgCounterRef = React.useRef(0);
  const [uploadingFile, setUploadingFile] = React.useState(false);

  const _OB_BINARY = new Set(["pdf", "docx", "doc", "xlsx", "xls"]);
  const _obKind = (n) => { const e = n.toLowerCase().split(".").pop(); return e === "pdf" ? "pdf" : e === "csv" ? "csv" : ["xlsx","xls"].includes(e) ? "sheet" : "doc"; };

  const handleFileUpload = async (file) => {
    if (!file) return;
    setUploadingFile(true);
    let textContent = null;
    let isBinaryFile = false;
    try {
      const isBinary = _OB_BINARY.has(file.name.toLowerCase().split(".").pop());
      isBinaryFile = isBinary;
      const content = await new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onerror = reject;
        if (isBinary) { reader.onload = (e) => resolve(e.target.result.split(",")[1]); reader.readAsDataURL(file); }
        else { reader.onload = (e) => { textContent = e.target.result; resolve(e.target.result); }; reader.readAsText(file); }
      });
      const r = await fetch(`${citrusApiBase()}/knowledge`, {
        method: "POST", credentials: "include",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ name: file.name, kind: _obKind(file.name), content, contentType: file.type || "text/plain", encoding: isBinary ? "base64" : undefined }),
      });
      const doc = r.ok ? await r.json() : null;
      // For binary files (PDF/DOCX), the server extracts text — use it immediately
      if (isBinaryFile && doc?.extractedText) textContent = doc.extractedText;
      await handleKnowledgeDone([{ name: file.name, id: doc?.id || doc?._id || file.name }], textContent, isBinaryFile);
    } catch {
      await handleKnowledgeDone([{ name: file.name, id: file.name }], textContent, isBinaryFile);
    } finally {
      setUploadingFile(false);
      if (uploadInputRef.current) uploadInputRef.current.value = "";
    }
  };

  React.useEffect(() => {
    const handler = () => {
      setChannelConfirmed(true);
      setTimeout(() => onComplete && onComplete(), 1800);
    };
    window.addEventListener("citrus-onboarding-channel-ready", handler);
    return () => window.removeEventListener("citrus-onboarding-channel-ready", handler);
  }, [onComplete]);

  React.useEffect(() => {
    if (streamRef.current) streamRef.current.scrollTop = streamRef.current.scrollHeight;
  }, [messages]);

  React.useEffect(() => {
    setShowPhone(phase === 6);
  }, [phase]);

  const addMsg = (msg) => {
    const id = ++msgCounterRef.current;
    setMessages((prev) => [...prev, { ...msg, _id: id }]);
    return id;
  };

  const updateMsg = (id, patch) => {
    setMessages((prev) => prev.map((m) => m._id === id ? { ...m, ...patch } : m));
  };

  const removeMsg = (id) => {
    setMessages((prev) => prev.filter((m) => m._id !== id));
  };

  const sendToApi = async (userText, extraContext) => {
    setStreaming(true);
    const apiMessages = [
      ...messages
        .filter((m) => m.from === "citrus" || m.from === "you")
        .map((m) => ({ role: m.from === "citrus" ? "assistant" : "user", content: m.text || "" })),
      { role: "user", content: userText + (extraContext ? `\n[${extraContext}]` : "") },
    ];

    const placeholderId = addMsg({ from: "citrus", text: "", kind: "streaming" });

    try {
      const base = citrusApiBase();
      const r = await fetch(`${base}/api/onboarding/chat/stream`, {
        method: "POST",
        credentials: "include",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ messages: apiMessages }),
      });
      if (!r.ok) throw new Error(`${r.status}`);

      const reader = r.body.getReader();
      const decoder = new TextDecoder();
      let accumulated = "";
      let doneData = null;

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        const chunk = decoder.decode(value, { stream: true });
        for (const line of chunk.split("\n")) {
          if (line.startsWith("0:")) {
            try { accumulated += JSON.parse(line.slice(2)); } catch {}
            updateMsg(placeholderId, { text: accumulated.replace(/<<CAPTURE:\{[\s\S]*?\}>>/g, "").replace(/\n{3,}/g, "\n\n").trimEnd() });
          } else if (line.startsWith("d:")) {
            try { doneData = JSON.parse(line.slice(2)); } catch {}
          }
        }
      }

      const cleanText = accumulated.replace(/<<CAPTURE:\{[\s\S]*?\}>>/g, "").replace(/\n{3,}/g, "\n\n").trim();
      updateMsg(placeholderId, { kind: "answer", text: cleanText });

      if (doneData) {
        if (doneData.phaseAdvanced && doneData.phase) setPhase(doneData.phase);
        if (doneData.capturedData) setCapturedData(doneData.capturedData);
      }
    } catch {
      removeMsg(placeholderId);
      addMsg({ from: "citrus", text: "Something went wrong — please try again.", kind: "error" });
    } finally {
      setStreaming(false);
      inputRef.current?.focus();
    }
  };

  const send = async () => {
    const text = draft.trim();
    if (!text || streaming) return;
    setDraft("");
    addMsg({ from: "you", text });
    await sendToApi(text);
  };

  const handleKnowledgeDone = async (uploadedFiles, textContent, isBinary) => {
    if (!uploadedFiles.length) return;
    const docName = uploadedFiles[0].name;
    let context;
    if (textContent) {
      context = `User uploaded a document named "${docName}". Full content for context:\n---\n${textContent.slice(0, 4000)}\n---\nExtract anything relevant to the current phase from this content and incorporate it naturally.`;
    } else {
      context = `User uploaded a ${isBinary ? "binary (PDF/DOCX)" : ""} document named "${docName}" — it has been saved to the knowledge base. Acknowledge it specifically and ask what it covers or if there is anything they want to highlight from it.`;
    }
    addMsg({ from: "you", text: `Uploaded: ${docName}` });
    await sendToApi(`I've uploaded a document: ${docName}`, context);
  };

  const handlePhoneSubmitted = async () => {
    setShowPhone(false);
    addMsg({ from: "you", text: "I've connected my WhatsApp number." });
    await sendToApi("I've submitted my WhatsApp credentials. Please proceed.");
  };

  return (
    <div className="arch-onboarding-layout">
      <div className="ob-page">
        <div className="ob-header">
          <div className="ob-header-row">
            <h2 className="ob-title">Citrus Architect — set up your AI agent fleet</h2>
            <button className="ob-exit" onClick={onComplete} title="Exit setup for now">✕</button>
          </div>
          <OnboardingProgressStrip phase={phase} />
        </div>

        <div className="ob-chat-area" ref={streamRef} role="status" aria-live="polite">
          {messages.map((m, i) => (
            <div key={m._id || i} className={`ob-row ob-row-${m.from}`}>
              {m.from === "citrus" && <div className="ob-avatar">✦</div>}
              <div className={`ob-bubble ob-bubble-${m.from} ob-kind-${m.kind || "answer"}`}>
                <div className="ob-bubble-text">
                  {window.renderChatMarkdown ? window.renderChatMarkdown(m.text || "") : (m.text || "")}
                </div>
              </div>
            </div>
          ))}
          {channelConfirmed && (
            <div className="ob-channel-confirmed">
              <span className="ob-channel-confirmed-icon">✓</span>
              WhatsApp connected — your agent is going live…
            </div>
          )}
        </div>

        <div className="ob-compose-wrap">
          {showPhone && !channelConfirmed && (
            <OnboardingPhoneInput onSubmitted={handlePhoneSubmitted} />
          )}
          <div className="ob-compose">
            <input
              ref={uploadInputRef}
              type="file"
              accept=".pdf,.txt,.docx,.md,.csv,.xlsx"
              style={{ display: "none" }}
              onChange={(e) => { if (e.target.files[0]) handleFileUpload(e.target.files[0]); }}
            />
            {!channelConfirmed && (
              <button
                className="ob-attach-btn"
                disabled={uploadingFile || streaming}
                onClick={() => uploadInputRef.current?.click()}
                title="Upload a document"
              >
                {uploadingFile ? "…" : "+"}
              </button>
            )}
            <input
              ref={inputRef}
              value={draft}
              disabled={streaming || channelConfirmed || showPhone || uploadingFile}
              onChange={(e) => setDraft(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") send(); }}
              placeholder={
                channelConfirmed ? "Setting up your dashboard…" :
                uploadingFile ? "Uploading…" :
                showPhone ? "Fill in the form above to connect WhatsApp…" :
                streaming ? "Thinking…" : "Type your answer…"
              }
            />
            <button onClick={send} disabled={!draft.trim() || streaming || channelConfirmed || showPhone || uploadingFile}>↑</button>
          </div>
        </div>
      </div>
      <ArchitectBuildPane capturedData={capturedData} />
    </div>
  );
}

// ======================================================================
function CitrusHomeTab({ agents = [], threads = [], activity = [], approvalsBadge = 0, onAskCitrus, onSwitchTab, onNew, userRole, authUser, onboardingStatus, onOnboardingComplete }) {
  // Architect splash gate — sessionStorage-backed (clears on browser close,
  // so a fresh login always re-offers the splash if onboarding is still
  // needed), but survives a tab switch within one sitting since CitrusHomeTab
  // unmounts/remounts when the user navigates away and back. "showing" is
  // the default for a fresh tenant; "started" lets the fallthrough below
  // render ConversationalOnboarding; "deferred" skips straight to the
  // regular dashboard content. Flat (non-business-scoped) key: app.jsx's
  // resumeOnboarding() also writes this same key when the owner clicks the
  // Topbar's "Continue setup" banner, so they land back in the chat rather
  // than seeing the splash again.
  const architectSplashKey = "citrus_architect_splash_state";
  const [architectSplashState, setArchitectSplashState] = useState(() => {
    try { return sessionStorage.getItem(architectSplashKey) || "showing"; } catch { return "showing"; }
  });
  const setAndPersistSplashState = (next) => {
    setArchitectSplashState(next);
    try { sessionStorage.setItem(architectSplashKey, next); } catch {}
  };
  const [messages,         setMessages]         = useState(() => [
    { from: "citrus", kind: "morning-loading", text: "Morning — pulling your brief…" },
  ]);
  const [draft,            setDraft]            = useState("");
  const [commandOpen,      setCommandOpen]      = useState(false);
  const [streaming,        setStreaming]        = useState(false);
  const [selectedModel,    setSelectedModel]    = useState(() => {
    try {
      const saved = localStorage.getItem("citrus_operator_model_pref");
      if (saved && ["claude-sonnet-4-6", "gpt-4o", "gemini-2.0-flash"].includes(saved)) return saved;
    } catch {}
    return "claude-sonnet-4-6";
  });
  const [pendingApprovals, setPendingApprovals] = useState([]);
  const [gapSuggestions,   setGapSuggestions]   = useState([]);
  const [briefReady,       setBriefReady]       = useState(false);
  const [lastUpdateAt,     setLastUpdateAt]     = useState(() => {
    try { return localStorage.getItem(citrusUpdateKey(agents)) || null; } catch { return null; }
  });
  // Prospect-analysis attachment (Citi business-proposal feature): a file
  // picked via the attach button, staged here until the next message is
  // sent. Cleared after each send — one doc per turn, matching how the
  // server only reads context.attachedDoc on the request that carries it.
  const [pendingAttachment, setPendingAttachment] = useState(null);
  const [attachingFile,     setAttachingFile]     = useState(false);
  const inputRef               = useRef(null);
  const llmHistoryRef          = useRef([]);
  const streamRef              = useRef(null);
  const threadCardsInjectedRef = useRef(false);
  const attachInputRef         = useRef(null);

  const liveAgents   = agents.filter((a) => a.status === "live");
  const hotLeads     = (threads || []).filter((t) => t.status === "needs-you" || t.unread || (t.categories || []).some((c) => String(c).toLowerCase().includes("hot"))).slice(0, 6);
  const escalations  = (threads || []).filter((t) => t.status === "needs-you");
  const avgHealth    = computeHealthScore(agents, threads);
  const lowHealth    = liveAgents.slice().sort((a, b) => (a.accuracy || 82) - (b.accuracy || 82))[0] || null;
  const canWrite     = !["Viewer"].includes(userRole || "");

  const dismissConfirmCard = (_id) => setMessages((prev) => prev.map((m) => m._id === _id ? { ...m, kind: "answer" } : m));
  const dismissWebLookup = (_id) => setMessages((prev) => prev.filter((m) => m._id !== _id));

  // ---- mount: load memory + saved chat + fetch live data ----
  useEffect(() => {
    // Restore LLM context
    try {
      const saved = localStorage.getItem(citrusHistoryKey(agents));
      if (saved) llmHistoryRef.current = JSON.parse(saved).slice(-20);
    } catch {}

    // Restore prior conversation (everything except the old morning brief)
    let priorConvo = [];
    try {
      const raw = localStorage.getItem(citrusMessagesKey(agents));
      if (raw) {
        priorConvo = JSON.parse(raw)
          .map((m) => m.kind === "streaming" ? { ...m, kind: "answer", text: m.text || "…" } : m)
          .filter((m) => !["morning", "morning-loading", "gap-intro", "approval-card", "thread-card"].includes(m.kind))
          .slice(-50);
      }
    } catch {}

    // Fetch pending approvals, gap suggestions, and priority queue in parallel
    Promise.allSettled([
      citrusJson("/approvals?status=pending"),
      citrusJson("/gap-suggestions"),
      citrusJson("/api/citrus/priority-queue"),
    ]).then(([approvalsResult, gapsResult, queueResult]) => {
      const learnings = approvalsResult.status === "fulfilled"
        ? (Array.isArray(approvalsResult.value) ? approvalsResult.value : []).filter((a) => a.kind === "learning")
        : [];
      const gaps = gapsResult.status === "fulfilled" && Array.isArray(gapsResult.value)
        ? gapsResult.value
        : [];
      const nowQueue = queueResult.status === "fulfilled" && Array.isArray(queueResult.value?.queue)
        ? queueResult.value.queue.slice(0, 3)
        : [];

      setPendingApprovals(learnings);
      setGapSuggestions(gaps);

      // Fresh morning brief always goes first
      const briefing = buildMorningBriefing(agents, threads, learnings, userRole, authUser);
      briefing.nowQueue = nowQueue;
      const msgs = [{
        from: "citrus", kind: "morning", text: briefing.dayFrame,
        _briefing: briefing,
        _approvals: learnings, _agents: agents, _threads: threads,
      }];

      // Inject low-risk approval cards inline (learnings are already fetched here)
      if (learnings.length > 0) {
        learnings
          .filter((a) => !a.riskLevel || a.riskLevel === "low")
          .slice(0, 3)
          .forEach((a) => msgs.push({ from: "citrus", kind: "approval-card", approvalId: a.id, _id: crypto.randomUUID() }));
      }

      // Gap intro right after brief (if any)
      if (gaps.length) {
        msgs.push({
          from: "citrus", kind: "gap-intro",
          text: `I spotted ${gaps.length} area${gaps.length === 1 ? "" : "s"} where I can get smarter. Review and approve what fits:`,
          gaps,
        });
      }

      // Inject brief summary into LLM history so follow-ups are grounded
      // in the same numbers the UI just rendered (not raw backend scores).
      const health = computeHealthScore(agents, threads);
      const briefSummary =
        `I showed the morning briefing. ` +
        `Day frame: "${briefing.dayFrame}" ` +
        `Conversations active: ${threads.length}. ` +
        `Escalations needing attention: ${threads.filter((t) => t.status === "needs-you").length}. ` +
        `Pending approvals: ${learnings.length}. ` +
        `Live agents: ${agents.filter((a) => a.status === "live").length}. ` +
        `Business health score: ${health !== null ? health + "%" : "not enough data"}. ` +
        (briefing.priorityItems.length ? `Priority items shown: ${briefing.priorityItems.join(" | ")}. ` : "") +
        (briefing.worthKnowing ? `Also worth knowing: ${briefing.worthKnowing}. ` : "") +
        (nowQueue.length
          ? `Top priority conversations right now: ${nowQueue.map((q, i) =>
              `${i + 1}. ${q.customerName} — ${q.urgencyReason}${q.waitMs ? ` (waiting ${Math.round(q.waitMs / 60000)}m)` : ""}`
            ).join("; ")}.`
          : "No conversations currently in the priority queue.");
      llmHistoryRef.current = [...llmHistoryRef.current, { role: "assistant", content: briefSummary }];
      saveLlmHistory();

      // Restored prior conversation appended after the fresh brief
      setMessages([...msgs, ...priorConvo]);
      setBriefReady(true);
    });
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  // ---- persist conversation whenever messages change (after brief loads) ----
  useEffect(() => {
    if (!briefReady) return;
    try {
      const toSave = messages
        .map((m) => m.kind === "streaming" ? { ...m, kind: "answer", text: m.text || "…" } : m)
        .filter((m) => !["morning-loading"].includes(m.kind))
        .slice(-60);
      localStorage.setItem(citrusMessagesKey(agents), JSON.stringify(toSave));
    } catch {}
  }, [messages, briefReady]); // eslint-disable-line react-hooks/exhaustive-deps

  // ---- scroll to bottom on initial load and on every new message ----
  // On first load: always jump to bottom so the user sees the latest exchange.
  // While chatting: auto-scroll only if already near bottom (lets user scroll
  // up to read history without getting yanked back during streaming).
  useEffect(() => {
    if (!briefReady) return;
    const el = streamRef.current;
    if (!el) return;
    const lastMsg      = messages[messages.length - 1];
    const isStreaming  = lastMsg?.kind === "streaming";
    const lastIsYou    = lastMsg?.from === "you";
    const nearBottom   = el.scrollHeight - el.scrollTop - el.clientHeight < 200;
    // Always scroll during streaming (response grows chunk by chunk) and
    // whenever the user just sent a message. Otherwise only scroll if already
    // near the bottom (lets the user scroll up to read without being yanked).
    if (isStreaming || lastIsYou || nearBottom) el.scrollTop = el.scrollHeight;
  }, [messages, briefReady]);

  // Jump to bottom the moment the brief+history finishes loading.
  useEffect(() => {
    if (!briefReady || !streamRef.current) return;
    streamRef.current.scrollTop = streamRef.current.scrollHeight;
  }, [briefReady]);

  // Inject escalated thread cards once threads arrive (after brief is ready).
  // Separated from the morning brief effect so it fires even when threads load
  // asynchronously after mount. Ref guard prevents re-injection on re-renders.
  useEffect(() => {
    if (!briefReady || threadCardsInjectedRef.current) return;
    if (!(threads || []).length) return;
    threadCardsInjectedRef.current = true;
    const cards = (threads || [])
      .filter((t) => t.status === "needs-you")
      .slice(0, 3)
      .map((t) => ({ from: "citrus", kind: "thread-card", threadId: t.id, priority: "escalated", _id: crypto.randomUUID() }));
    if (cards.length) setMessages((prev) => [...prev, ...cards]);
  }, [briefReady, threads]); // eslint-disable-line react-hooks/exhaustive-deps

  // ---- save LLM history after each AI turn ----
  const saveLlmHistory = () => {
    try { localStorage.setItem(citrusHistoryKey(agents), JSON.stringify(llmHistoryRef.current.slice(-20))); } catch {}
  };

  const removeMsg = (id) => {
    setMessages((prev) => prev.filter((m) => m._id !== id));
  };

  const addCitrus = (payload) => {
    setMessages((prev) => [...prev, { from: "citrus", ...payload }]);
    // Sync structured card content into LLM history so follow-up questions
    // are grounded in exactly what the UI rendered, not backend raw scores.
    let cardSummary = null;
    if (payload.kind === "health-breakdown" && payload.healthData) {
      const hd = payload.healthData;
      cardSummary =
        `I displayed a health breakdown card. Business health score: ${hd.score}%. ` +
        `Formula: avg agent accuracy (${hd.avgAcc}%) minus escalation penalty (${hd.escPenalty} pts). ` +
        `Live agents: ${hd.agentsSorted.map((a) => `${a.name} at ${a.accuracy || 82}%`).join(", ")}. ` +
        `Escalations: ${hd.escalations} of ${hd.totalConvos} conversations (${hd.escRatePct}%). ` +
        `Suggestions shown: ${hd.suggestions.map((s) => s.text).join(" | ")}`;
    } else if (payload.kind === "morning") {
      cardSummary =
        `I showed the morning brief. Conversations: ${threads.length}, hot leads: ${hotLeads.length}, ` +
        `escalations: ${escalations.length}, business health: ${avgHealth !== null ? avgHealth + "%" : "unknown"}. ` +
        `Pending approvals: ${pendingApprovals.length}.`;
    } else if (payload.kind === "insights") {
      cardSummary =
        `I showed a performance snapshot. Conversations: ${threads.length}, hot leads: ${hotLeads.length}, ` +
        `escalations: ${escalations.length}, health: ${avgHealth !== null ? avgHealth + "%" : "unknown"}.`;
    } else if (payload.kind === "approvals" && payload.approvals) {
      cardSummary = `I showed ${payload.approvals.length} pending approvals. Items: ${payload.approvals.slice(0, 5).map((a) => a.proposedLesson?.what || a.title || "learning").join("; ")}.`;
    } else if (payload.kind === "leads" && payload.leads) {
      cardSummary = `I showed ${payload.leads.length} hot leads: ${payload.leads.map((l) => l.customerName || l.customer || "unnamed").join(", ")}.`;
    } else if (payload.kind === "gap-intro" && payload.gaps) {
      cardSummary = `I showed ${payload.gaps.length} proactive gap suggestions for the owner to review.`;
    }
    if (cardSummary) {
      llmHistoryRef.current = [...llmHistoryRef.current, { role: "assistant", content: cardSummary }];
      saveLlmHistory();
    }
  };

  // ---- approve / reject a single item ----
  const approveItem = async (id) => {
    await citrusJson(`/approvals/${id}/approve`, { method: "POST", body: JSON.stringify({}) });
    const next = pendingApprovals.filter((a) => a.id !== id);
    setPendingApprovals(next);
    return next;
  };
  const rejectItem = async (id) => {
    await citrusJson(`/approvals/${id}/reject`, { method: "POST", body: JSON.stringify({}) });
    const next = pendingApprovals.filter((a) => a.id !== id);
    setPendingApprovals(next);
    return next;
  };

  // ---- bulk approve low-risk ----
  const bulkApproveLowRisk = async () => {
    const lowRisk = pendingApprovals.filter((a) => !a.riskLevel || a.riskLevel === "low");
    if (!lowRisk.length) {
      addCitrus({ kind: "answer", text: "No low-risk approvals in the queue right now." });
      return;
    }
    addCitrus({ kind: "working", text: `Approving ${lowRisk.length} low-risk item${lowRisk.length !== 1 ? "s" : ""}…` });
    let approved = 0;
    for (const a of lowRisk) {
      try { await citrusJson(`/approvals/${a.id}/approve`, { method: "POST", body: JSON.stringify({}) }); approved++; } catch {}
    }
    const remaining = pendingApprovals.filter((a) => a.riskLevel && a.riskLevel !== "low");
    setPendingApprovals(remaining);
    addCitrus({ kind: "answer", text: `Done. ${approved} approval${approved !== 1 ? "s" : ""} cleared. ${remaining.length ? `${remaining.length} medium/high-risk item${remaining.length !== 1 ? "s" : ""} still pending — review those manually.` : "Queue is clear."}` });
  };

  // ---- update button — delta briefing since last press ----
  const runUpdate = () => {
    const now      = new Date();
    const sinceTs  = lastUpdateAt ? new Date(lastUpdateAt) : null;
    const sinceStr = sinceTs
      ? sinceTs.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
      : null;

    // Delta computations
    const newThreads    = sinceTs
      ? (threads || []).filter((t) => new Date(t.updatedAt || t.createdAt || 0) > sinceTs)
      : (threads || []);
    const escalated     = (threads || []).filter((t) => t.status === "needs-you");
    const newEscalated  = sinceTs
      ? escalated.filter((t) => new Date(t.updatedAt || t.createdAt || 0) > sinceTs)
      : escalated;
    const recentActivity = sinceTs
      ? (activity || []).filter((a) => new Date(a.at || a.ts || a.timestamp || 0) > sinceTs)
      : (activity || []).slice(0, 10);
    const health        = computeHealthScore(agents, threads);
    const liveCount     = (agents || []).filter((a) => a.status === "live").length;

    // Build a rich context prompt for the LLM
    const lines = [];
    lines.push(`[CITRUS UPDATE REQUEST — ${now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}]`);
    lines.push(sinceStr
      ? `The operator is pressing the Update button. Scope: changes since ${sinceStr} today.`
      : "The operator is pressing the Update button for the first time. Give a full current-state summary."
    );
    lines.push(`Operator role: ${userRole || "Owner"}.`);
    lines.push("");

    lines.push("CURRENT OPERATIONAL STATE:");
    lines.push(`• ${(threads || []).length} total conversations, ${escalated.length} escalated/needing attention`);
    if (newThreads.length > 0 || newEscalated.length > 0) {
      if (newThreads.length)   lines.push(`• ${newThreads.length} conversation${newThreads.length !== 1 ? "s" : ""} updated since last check`);
      if (newEscalated.length) lines.push(`• ${newEscalated.length} new escalation${newEscalated.length !== 1 ? "s" : ""} since last check — need your reply`);
    }
    lines.push(`• ${pendingApprovals.length} learning approval${pendingApprovals.length !== 1 ? "s" : ""} pending`);
    lines.push(`• ${liveCount} agent${liveCount !== 1 ? "s" : ""} live — business health ${health !== null ? health + "%" : "unknown"}`);

    if (recentActivity.length > 0) {
      lines.push("");
      lines.push("RECENT ACTIVITY:");
      recentActivity.slice(0, 8).forEach((a) => {
        const ts  = a.at || a.ts || a.timestamp;
        const when = ts ? new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
        lines.push(`• ${when ? when + " — " : ""}${a.label || a.description || a.event || JSON.stringify(a)}`);
      });
    }

    if (escalated.length > 0) {
      lines.push("");
      lines.push("ESCALATED CONVERSATIONS (needing operator reply):");
      escalated.slice(0, 5).forEach((t) => {
        const customer = t.customerName || t.customer || "Customer";
        const preview  = t.lastMessage || t.preview || t.last || "";
        lines.push(`• ${customer}${preview ? ': "' + preview.slice(0, 80) + '"' : ""}`);
      });
    }

    lines.push("");
    lines.push(
      "INSTRUCTIONS: Give a crisp, role-appropriate update. Lead with what actually changed or needs action. " +
      "Do NOT recap things already handled or unchanged. " +
      (pendingApprovals.length > 0 ? "Mention approvals if there are any pending. " : "") +
      (userRole === "Viewer" ? "User is a Viewer — read-only, no action prompts." : "End with 1-2 concrete next actions the operator should take right now.")
    );

    // Save the new timestamp
    const nowIso = now.toISOString();
    setLastUpdateAt(nowIso);
    try { localStorage.setItem(citrusUpdateKey(agents), nowIso); } catch {}

    // Add user-facing "Update" marker then stream the LLM response
    setMessages((prev) => [
      ...prev,
      { from: "you", kind: "question", text: sinceStr ? `Update since ${sinceStr}` : "Full update" },
    ]);
    callCitrusLlm(lines.join("\n"));
  };

  // ---- pause / resume agent ----
  const setAgentStatus = async (nameQuery, newStatus) => {
    const q = String(nameQuery || "").toLowerCase().trim();
    const match = agents.find((a) => (a.name || "").toLowerCase().includes(q));
    if (!match) {
      addCitrus({ kind: "answer", text: `No agent matching "${nameQuery}" found. Try using the exact name.` });
      return;
    }
    addCitrus({ kind: "agent-action", agentId: match.id, agentName: match.name, agentStatus: match.status, targetStatus: newStatus });
  };

  const commitAgentStatus = async (agentId, newStatus) => {
    try {
      await citrusJson(`/agents/${agentId}`, { method: "PATCH", body: JSON.stringify({ status: newStatus }) });
      addCitrus({ kind: "answer", text: `${newStatus === "paused" ? "⏸" : "▶"} Done — agent is now ${newStatus}.` });
    } catch (e) {
      addCitrus({ kind: "answer", text: `Could not update agent: ${e.message}` });
    }
  };

  // ---- search ----
  const runSearch = (query) => {
    const q = String(query || "").toLowerCase();
    const agentHits  = agents.filter((a) => `${a.name} ${a.role} ${a.brief || ""}`.toLowerCase().includes(q)).slice(0, 4);
    const threadHits = threads.filter((t) => `${t.customerName || ""} ${t.customer || ""} ${t.last || ""} ${(t.categories || []).join(" ")}`.toLowerCase().includes(q)).slice(0, 4);
    addCitrus({ kind: "search", command: "/search", query, agentHits, threadHits, text: `Search results for "${query}".` });
  };

  const openLearn = () => {
    if (!canWrite) { addCitrus({ kind: "permission", text: "You have view-only access. Contact an Owner or Admin to make changes." }); return; }
    addCitrus({ kind: "learn", command: "/learn", text: "Open the learning workflow below." });
  };

  // ---- LLM streaming call ----
  const callCitrusLlm = (userText) => {
    const internalAgent = (agents || []).find((a) => a.type === "internal");
    if (!internalAgent) {
      addCitrus({ kind: "answer", text: "No internal Citrus agent configured. Create one in Agents with type set to Internal." });
      return;
    }
    const msgId = "stream-" + Date.now();
    setMessages((prev) => [...prev, { from: "citrus", kind: "streaming", text: "", _id: msgId }]);
    setStreaming(true);
    const history = llmHistoryRef.current.slice(-12);
    const actionCtxContent =
      "[CITRUS OS: When your response proposes creating or submitting something on behalf of the owner, " +
      "append exactly one action tag on a new line at the very end of your response — nothing after it. " +
      "Use [ACTION:task] when the action is an internal team task (e.g. upload docs, assign someone, remind team). " +
      "Use [ACTION:support] when the owner is reporting a bug, requesting a feature, or contacting Citrus support. " +
      "Use [ACTION:feature] when it is purely a product idea or feature request for the Citrus platform. " +
      "Only add a tag when you are genuinely proposing to submit something. Never add a tag for informational replies.]";
    // Gemini requires strictly alternating user/model turns — merge actionCtx into the
    // first user message instead of prepending it as a separate user turn.
    const payload = selectedModel === "gemini-2.0-flash"
      ? [...history, { role: "user", content: actionCtxContent + "\n\n" + userText }]
      : [{ role: "user", content: actionCtxContent }, ...history, { role: "user", content: userText }];

    const _attachedDoc = pendingAttachment;
    setPendingAttachment(null);

    (async () => {
      try {
        const response = await fetch(citrusApiBase() + "/api/citrus/chat/stream", {
          method: "POST", credentials: "include",
          headers: { "content-type": "application/json" },
          body: JSON.stringify({
            messages: payload, agentId: internalAgent.id, max_tokens: 4096, model: selectedModel,
            // /research and /deepsearch are detected server-side from the
            // literal message text (see channels.js) — no client-set flag
            // needed here, which keeps this identical across every client.
            ...(_attachedDoc ? { context: { attachedDoc: _attachedDoc } } : {}),
          }),
        });
        if (!response.ok) {
          let errMsg = `${selectedModel} is unavailable (HTTP ${response.status}). Try switching models or contact your admin.`;
          try { const b = await response.json(); if (b?.error) errMsg = b.error; } catch {}
          throw new Error(errMsg);
        }

        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let accumulated = "";
        let lineBuffer = "";
        let doneFrame = null;
        let eventFrame = null;

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          lineBuffer += decoder.decode(value, { stream: true });
          const lines = lineBuffer.split("\n");
          lineBuffer = lines.pop();
          for (const line of lines) {
            if (line.slice(0, 2) === "0:") {
              try {
                accumulated += JSON.parse(line.slice(2));
                setMessages((prev) => prev.map((m) => m._id === msgId ? { ...m, text: accumulated } : m));
              } catch {}
            } else if (line.slice(0, 2) === "d:") {
              try { doneFrame = JSON.parse(line.slice(2)); } catch {}
            } else if (line.slice(0, 2) === "e:") {
              try { eventFrame = JSON.parse(line.slice(2)); } catch {}
            }
          }
        }
        for (const line of lineBuffer.split("\n")) {
          if (line.slice(0, 2) === "0:") { try { accumulated += JSON.parse(line.slice(2)); } catch {} }
          else if (line.slice(0, 2) === "d:") { try { doneFrame = JSON.parse(line.slice(2)); } catch {} }
          else if (line.slice(0, 2) === "e:") { try { eventFrame = JSON.parse(line.slice(2)); } catch {} }
        }

        const raw = accumulated.trim() || "No response. Please try again.";

        // Parse action tags — strip from displayed text, show confirm card or open drawer.
        // Prospect-proposal replies (/research, /deepsearch) are NOT a
        // special action type — they're plain Markdown with a fenced ```json
        // block at the end, rendered as ordinary text like any other answer.
        // This is deliberate: every client hitting /api/citrus/chat/stream
        // (this dashboard tab, the citrus-chat widget, apps/web's Companion)
        // already renders plain text/Markdown, so a custom marker format
        // only one of the three could parse was extra complexity for no
        // reason — see CIT-964.
        const _ACTION_RE = /\[ACTION:(task|support|feature|kb-add|training)\]/i;
        const actionMatch = raw.match(_ACTION_RE);
        const displayText = raw.replace(_ACTION_RE, "").trim();
        const actionType  = actionMatch ? actionMatch[1].toLowerCase() : null;

        const _msgSources = doneFrame?.sources || [];
        const _noKbSources = doneFrame?.noKbSources || false;
        setMessages((prev) => prev.map((m) => {
          if (m._id !== msgId) return m;
          if (actionType === "task") {
            return { ...m, kind: "task-confirm", text: displayText, _proposedTitle: displayText.split("\n").slice(-1)[0].slice(0, 120) };
          }
          if (actionType === "support" || actionType === "feature") {
            return { ...m, kind: "support-confirm", text: displayText, _requestType: actionType === "feature" ? "feature_request" : "support_ticket" };
          }
          return { ...m, kind: "answer", text: displayText, _sources: _msgSources, _noKbSources };
        }));
        // Drawer-based actions — open the relevant panel automatically
        if (actionType === "kb-add") {
          setTimeout(() => window.citrus && window.citrus.openDrawer("kb-add"), 120);
        } else if (actionType === "training") {
          setTimeout(() => window.citrus && window.citrus.openDrawer("training"), 120);
        }
        llmHistoryRef.current = [...llmHistoryRef.current, { role: "user", content: userText }, { role: "assistant", content: displayText }];
        saveLlmHistory();
      } catch (e) {
        setMessages((prev) => prev.map((m) => m._id === msgId ? { ...m, kind: "answer", text: "Error: " + String(e.message || e) } : m));
      } finally {
        setStreaming(false);
      }
    })();
  };

  // ---- prospect-doc attach (Citi business-proposal feature) ----
  // No /knowledge round-trip, deliberately — see the plan: this content is
  // operator-only, single-turn, and never becomes a customer-facing agent's
  // persistent knowledge, so the quota/injection-scan/persistence pipeline
  // that route exists for doesn't apply here. Binary files are staged as
  // base64 (server extracts text via extractTextFromBinary); everything
  // else is staged as plain text.
  const _BP_BINARY = new Set(["pdf", "docx", "xlsx", "xls"]);
  const handleAttachFile = (file) => {
    if (!file) return;
    setAttachingFile(true);
    const ext = file.name.toLowerCase().split(".").pop();
    const isBinary = _BP_BINARY.has(ext);
    const reader = new FileReader();
    reader.onerror = () => setAttachingFile(false);
    reader.onload = (e) => {
      const content = isBinary ? String(e.target.result).split(",")[1] : e.target.result;
      setPendingAttachment({ name: file.name, contentType: file.type || "", encoding: isBinary ? "base64" : "text", content });
      setAttachingFile(false);
    };
    if (isBinary) reader.readAsDataURL(file); else reader.readAsText(file);
  };

  // ---- web search (Sprint 2) ----
  const runWebSearch = (query, confirmCardId) => {
    const internalAgent = (agents || []).find((a) => a.type === "internal");
    if (!internalAgent) return;
    dismissWebLookup(confirmCardId);
    const msgId = "wsearch-" + Date.now();
    setMessages((prev) => [...prev, { from: "citrus", kind: "streaming", text: "", _id: msgId }]);
    setStreaming(true);
    (async () => {
      try {
        console.error("[citrus-ws v22] starting fetch, model:", selectedModel, "query:", query.slice(0, 40));
        const response = await fetch(citrusApiBase() + "/api/citrus/web-search-stream", {
          method: "POST", credentials: "include",
          headers: { "content-type": "application/json" },
          body: JSON.stringify({ query, agentId: internalAgent.id, model: selectedModel, max_tokens: 1024 }),
        });
        console.error("[citrus-ws v22] response status:", response.status, "ok:", response.ok);
        if (!response.ok) {
          let errMsg = `Web search failed (HTTP ${response.status}).`;
          try { const b = await response.json(); if (b?.error) errMsg = b.error; } catch {}
          throw new Error(errMsg);
        }
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let accumulated = "";
        let lineBuffer = "";
        let webDoneFrame = null;
        let serverError = null;
        let awaitingErrorData = false;
        const processLine = (line) => {
          if (line) console.error("[citrus-ws v22]", line);
          if (line.slice(0, 2) === "0:") {
            try {
              const token = JSON.parse(line.slice(2));
              accumulated += token;
              setMessages((prev) => prev.map((m) => m._id === msgId ? { ...m, text: accumulated } : m));
            } catch {}
          } else if (line.slice(0, 2) === "d:") {
            try { webDoneFrame = JSON.parse(line.slice(2)); } catch {}
          } else if (line === "event: error") {
            awaitingErrorData = true;
          } else if (awaitingErrorData && line.startsWith("data: ")) {
            try { const d = JSON.parse(line.slice(6)); serverError = d.error || "Web search failed."; } catch { serverError = "Web search failed."; }
            awaitingErrorData = false;
          }
        };
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          lineBuffer += decoder.decode(value, { stream: true });
          const lines = lineBuffer.split("\n");
          lineBuffer = lines.pop();
          for (const line of lines) processLine(line);
        }
        for (const line of lineBuffer.split("\n")) processLine(line);
        console.error("[citrus-ws v22] stream done — accumulated:", accumulated.length, "chars, serverError:", serverError);
        if (serverError) throw new Error(serverError);
        const finalText = accumulated.trim() || `[v22 debug: status=${response.status} err=${serverError} acc=${accumulated.length}] No web results found. Please try a different query.`;
        const webSources = webDoneFrame?.sources || [];
        setMessages((prev) => prev.map((m) =>
          m._id === msgId ? { ...m, kind: "answer", text: finalText, _sources: webSources, _noKbSources: false } : m
        ));
      } catch (e) {
        setMessages((prev) => prev.map((m) =>
          m._id === msgId ? { ...m, kind: "answer", text: "Error: " + String(e.message || e) } : m
        ));
      } finally {
        setStreaming(false);
      }
    })();
  };

  // ---- intent router ----
  const handleIntent = (text) => {
    const lower = text.toLowerCase().trim();
    const risk  = citrusRiskForAction(text);

    // -- commands --
    if (lower.startsWith("/")) {
      const meta = citrusCommandMeta(text);
      if (meta.cmd === "/search") return runWebSearch(meta.args || "", null);
      if (meta.cmd === "/learn")  return openLearn();
      if (meta.cmd === "/approve") {
        const arg = meta.args.toLowerCase();
        if (!arg || arg === "low-risk" || arg === "lowrisk" || arg === "low") return bulkApproveLowRisk();
        // /approve <id> — approve a specific item
        const match = pendingApprovals.find((a) => a.id === meta.args || (a.proposedLesson?.what || a.title || "").toLowerCase().includes(meta.args.toLowerCase()));
        if (match) { approveItem(match.id).then((remaining) => { setPendingApprovals(remaining); addCitrus({ kind: "answer", text: `Approved. ${remaining.length} item${remaining.length !== 1 ? "s" : ""} remaining in queue.` }); }).catch(() => {}); }
        else addCitrus({ kind: "answer", text: `Couldn't find that approval. Type /approve to see the full list.` });
        return;
      }
      if (meta.cmd === "/pause")  { if (!canWrite) return addCitrus({ kind: "permission", text: "You need Owner or Admin access to pause agents." }); return setAgentStatus(meta.args, "paused"); }
      if (meta.cmd === "/resume") { if (!canWrite) return addCitrus({ kind: "permission", text: "You need Owner or Admin access to resume agents." }); return setAgentStatus(meta.args, "live"); }
      if (meta.cmd === "/approvals" || meta.cmd === "/queue") {
        if (!pendingApprovals.length) return addCitrus({ kind: "answer", text: "No pending approvals right now." });
        return addCitrus({ kind: "approvals", text: `${pendingApprovals.length} approval${pendingApprovals.length !== 1 ? "s" : ""} pending.`, approvals: pendingApprovals });
      }
      if (meta.cmd === "/health") {
        const hd = buildHealthBreakdown(agents, threads);
        addCitrus({ kind: "health-breakdown", text: hd ? `Business health: ${hd.score}%` : "Not enough data to compute health score yet.", healthData: hd });
        return;
      }
      if (meta.cmd === "/research" || meta.cmd === "/deepsearch") {
        if (!canWrite) return addCitrus({ kind: "permission", text: "You need Owner, Admin, or Member access to research a prospect." });
        if (!meta.args) return addCitrus({ kind: "answer", text: `Usage: ${meta.cmd} <company name or URL>` });
        // Forward the full literal text (command prefix included) — the
        // server parses "/research "/"/deepsearch " itself (channels.js),
        // so this is just a pass-through, not a special client-side mode.
        callCitrusLlm(text);
        return;
      }
      addCitrus({ kind: "permission", text: `Unknown command "${meta.cmd}". Try /approve, /pause <agent>, /resume <agent>, /approvals, /search, /learn, /research <company>, /deepsearch <company>, or /health.` });
      return;
    }

    // -- natural language shortcuts --
    if (lower.includes("hot lead") || lower.includes("hot leads")) {
      addCitrus({ kind: "contacts", text: "Hot and attention-needed leads:", leads: hotLeads }); return;
    }

    // Detect action/delegation intent — these should never shortcut to a card,
    // they belong to the LLM so it can reason about what's actually possible.
    const isActionIntent = /\b(send|have|ask|tell|get|make|contact|reach out|follow.?up|message|email|notify|remind|schedule|invite|write|draft|run|execute|trigger)\b/.test(lower);

    // -- agent delegation: "have HANA send…", "ask Sofia to…", "tell Maya to…" --
    if (isActionIntent && /\b(have|ask|tell|get)\s+\w+\b/.test(lower)) {
      const m = lower.match(/\b(?:have|ask|tell|get)\s+(\w+)\b/);
      const targetAgent = m && agents.find((a) => {
        const n = (a.name || "").toLowerCase();
        return n.startsWith(m[1]) || n.includes(m[1]);
      });
      if (targetAgent) {
        const ctx = `[System context: The owner is addressing ${targetAgent.name} (role: ${targetAgent.role || "agent"}, status: ${targetAgent.status}). ` +
          `Agents respond to inbound conversations — they cannot proactively send bulk emails unless the platform supports campaign triggers. ` +
          `If this specific action isn't directly available, explain what IS possible and suggest the nearest alternative action the owner can take from this dashboard.]`;
        callCitrusLlm(ctx + "\n\nOwner: " + text);
        return;
      }
    }

    // Only show the insights card for genuine performance/metrics questions,
    // not action sentences that happen to contain time words like "last week".
    if (!isActionIntent && lower.match(/\b(week|perform|insight|metric|stat)\b/)) {
      addCitrus({ kind: "insights", text: "Performance snapshot.", conversations: threads.length, hotLeads: hotLeads.length, escalations: escalations.length, avgHealth }); return;
    }
    if (lower.match(/\bapproval|approv|queue|pending\b/)) {
      if (!pendingApprovals.length) return addCitrus({ kind: "answer", text: "No pending approvals right now." });
      return addCitrus({ kind: "approvals", text: `${pendingApprovals.length} approval${pendingApprovals.length !== 1 ? "s" : ""} pending.`, approvals: pendingApprovals });
    }
    if (lower.match(/\bapprove\s+(all\s+)?(low.?risk|low)\b/)) {
      return bulkApproveLowRisk();
    }
    if (lower.match(/\bpause\b/) && canWrite) {
      const name = lower.replace(/\bpause\b/, "").trim();
      if (name) return setAgentStatus(name, "paused");
    }
    if (lower.match(/\b(resume|unpause|reactivate)\b/) && canWrite) {
      const name = lower.replace(/\b(resume|unpause|reactivate)\b/, "").trim();
      if (name) return setAgentStatus(name, "live");
    }
    if (lower.match(/\bhealth\b/)) {
      const hd = buildHealthBreakdown(agents, threads);
      addCitrus({ kind: "health-breakdown", text: hd ? `Business health: ${hd.score}%` : "Not enough data to compute health score yet.", healthData: hd });
      return;
    }

    // -- governance tier stubs (risk-classified but not yet command) --
    if (risk === "moderate") {
      if (!canWrite) return addCitrus({ kind: "permission", text: "You have view-only access. Contact an Owner or Admin to make changes." });
    }
    if (risk === "high") {
      if (!canWrite) return addCitrus({ kind: "permission", text: "You have view-only access. Contact an Owner or Admin to make changes." });
    }

    // -- fallthrough to LLM --
    callCitrusLlm(text);
  };

  const submitText = (text) => {
    const clean = String(text || "").trim();
    if (!clean || streaming) return;
    setMessages((prev) => [...prev, { from: "you", text: clean }]);
    setDraft(""); setCommandOpen(false);
    setTimeout(() => handleIntent(clean), 80);
  };

  const send = () => submitText(draft);
  const pickCommand = (cmd) => {
    if (cmd === "/learn")    { submitText("/learn"); return; }
    if (cmd === "/approve")  { submitText("/approve"); return; }
    if (cmd === "/health")   { submitText("/health"); return; }
    // Everything else (/search, /research, /deepsearch) needs an argument —
    // populate the draft with the command and let the operator type it.
    setDraft(cmd + " ");
    setCommandOpen(false);
    setTimeout(() => inputRef.current && inputRef.current.focus(), 0);
  };

  const architectSplashActive = onboardingStatus?.needs && !onboardingStatus?.inProgress && architectSplashState === "showing";

  if (architectSplashActive) {
    return (
      <ArchitectSplash
        ownerName={authUser?.name ? authUser.name.split(" ")[0] : null}
        onStart={() => setAndPersistSplashState("started")}
        onDefer={() => {
          setAndPersistSplashState("deferred");
          // Clear onboardingStatus at the app.jsx level so the Topbar's
          // "Continue setup" banner appears (serverNeedsOnboarding stays
          // true regardless — only onboardingStatus goes null here).
          if (onOnboardingComplete) onOnboardingComplete();
        }}
      />
    );
  }

  if (architectSplashState !== "deferred" && (onboardingStatus?.needs || onboardingStatus?.inProgress)) {
    return (
      <ConversationalOnboarding
        initialStatus={onboardingStatus}
        onComplete={onOnboardingComplete}
      />
    );
  }

  return (
    <div className="co-page co-page--single">
      <div className="co-shell">
        <div className="co-stream" ref={streamRef}>
          {messages.map((m, i) => (
            <div key={i} className={`co-row co-row-${m.from} co-kind-row-${m.kind || "text"}`}>
              {m.from === "citrus" ? <div className="co-avatar">✦</div> : null}
              <div className={`co-bubble co-bubble-${m.from} co-kind-${m.kind || "text"}`}>
                {m.kind === "streaming"
                  ? <div className="co-bubble-text co-streaming">{m.text || "…"}</div>
                  : m.kind !== "morning"
                  ? <div className="co-bubble-text">{m.text}</div>
                  : null}
                {m.kind === "answer" && m._sources?.length > 0 ? (
                  <div className="co-sources-section">
                    <div className="co-sources-label">Sources</div>
                    <div className="ap-source-links">
                      {m._sources.slice(0, 3).map((s) => (
                        <span key={s.id || s.name} style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
                          {s.web ? <span className="cf-badge">Web source</span> : null}
                          <button className="ap-source-link"
                            onClick={() => {
                              if (s.id) { try { sessionStorage.setItem("citrus_knowledge_focus_doc", s.id); } catch {} }
                              onSwitchTab && onSwitchTab("knowledge");
                            }}>
                            {s.name}
                          </button>
                        </span>
                      ))}
                      {m._sources.length > 3 ? <span className="co-sources-more">+ {m._sources.length - 3} more</span> : null}
                    </div>
                  </div>
                ) : m.kind === "answer" && m._noKbSources ? (
                  <div className="co-sources-section">
                    <span className="kn-state-badge kn-state-neutral">No KB sources</span>
                  </div>
                ) : null}
                {CHAT_COMPONENT_REGISTRY[m.kind]
                  ? CHAT_COMPONENT_REGISTRY[m.kind](m, { threads, pendingApprovals, approveItem, rejectItem, onSwitchTab, removeMsg, addCitrus })
                  : null}
                {m.kind === "morning" ? (
                  <CitrusMorningBriefing
                    briefing={m._briefing}
                    onPillClick={submitText}
                  />
                ) : null}
                {m.kind === "contacts"     ? <CitrusContactCards contacts={m.leads || []} onSwitchTab={onSwitchTab} /> : null}
                {m.kind === "insights"  ? <CitrusInsightsCards conversations={threads.length} hotLeads={hotLeads.length} escalations={escalations.length} avgHealth={avgHealth} onSwitchTab={onSwitchTab} /> : null}
                {m.kind === "search"    ? <CitrusSearchResults agentHits={m.agentHits || []} threadHits={m.threadHits || []} onSwitchTab={onSwitchTab} /> : null}
                {m.kind === "learn"     ? <CitrusLearnWorkflow agents={agents} onSwitchTab={onSwitchTab} /> : null}
                {m.kind === "approvals" ? <CitrusApprovalsCard approvals={m.approvals || []} canWrite={canWrite} onApprove={approveItem} onReject={rejectItem} onBulkLowRisk={canWrite ? bulkApproveLowRisk : null} onSwitchTab={onSwitchTab} /> : null}
                {m.kind === "agent-action" ? <CitrusAgentActionCard agentId={m.agentId} agentName={m.agentName} currentStatus={m.agentStatus} targetStatus={m.targetStatus} onCommit={commitAgentStatus} /> : null}
                {m.kind === "health-breakdown" ? <CitrusHealthBreakdown healthData={m.healthData} onSwitchTab={onSwitchTab} /> : null}
                {m.kind === "task-confirm" ? <CitrusTaskConfirmCard text={m.text} proposedTitle={m._proposedTitle} canWrite={canWrite} onSwitchTab={onSwitchTab} onDismiss={() => dismissConfirmCard(m._id)} /> : null}
                {m.kind === "support-confirm" ? <CitrusSupportConfirmCard text={m.text} requestType={m._requestType} canWrite={canWrite} onDismiss={() => dismissConfirmCard(m._id)} /> : null}
                {m.kind === "web-lookup-confirm" ? <CitrusWebLookupConfirmCard query={m._query || ""} onConfirm={() => runWebSearch(m._query || "", m._id)} onDismiss={() => dismissWebLookup(m._id)} disabled={streaming} /> : null}
                {m.kind === "gap-intro" ? <CitrusGapCards gaps={m.gaps || []} canWrite={canWrite} agents={agents} onDismissAll={() => setGapSuggestions([])} /> : null}
              </div>
            </div>
          ))}
        </div>

        <div className="co-compose-wrap">
          {commandOpen ? (
            <div className="co-command-menu">
              <button onMouseDown={(e) => e.preventDefault()} onClick={() => pickCommand("/approve")}><b>/approve</b><span>Approve low-risk learnings in bulk</span></button>
              <button onMouseDown={(e) => e.preventDefault()} onClick={() => pickCommand("/search")}><b>/search</b><span>Search agents and conversations</span></button>
              <button onMouseDown={(e) => e.preventDefault()} onClick={() => pickCommand("/learn")}><b>/learn</b><span>Add a learning to your agents</span></button>
              <button onMouseDown={(e) => e.preventDefault()} onClick={() => pickCommand("/health")}><b>/health</b><span>Business health score</span></button>
              <button onMouseDown={(e) => e.preventDefault()} onClick={() => pickCommand("/research")}><b>/research</b><span>Research a prospect — company name or URL</span></button>
              <button onMouseDown={(e) => e.preventDefault()} onClick={() => pickCommand("/deepsearch")}><b>/deepsearch</b><span>Deeper prospect research — LinkedIn, reviews, news</span></button>
            </div>
          ) : null}
          {canWrite ? (
            <div className="co-model-selector">
              {[
                { id: "claude-sonnet-4-6", label: "Claude" },
                { id: "gpt-4o",            label: "GPT-4o" },
                { id: "gemini-2.0-flash",  label: "Gemini" },
              ].map(({ id, label }) => (
                <button
                  key={id}
                  className={`co-model-pill${selectedModel === id ? " co-model-pill-active" : ""}`}
                  onClick={() => {
                    setSelectedModel(id);
                    try { localStorage.setItem("citrus_operator_model_pref", id); } catch {}
                  }}
                  disabled={streaming}
                >
                  {label}
                </button>
              ))}
            </div>
          ) : null}
          {pendingAttachment ? (
            <div className="co-attach-chip">
              <span>📎 {pendingAttachment.name}</span>
              <button className="co-attach-chip-remove" onClick={() => setPendingAttachment(null)} title="Remove attachment">✕</button>
            </div>
          ) : null}
          <div className="co-compose">
            <button
              className="co-update-btn"
              onClick={runUpdate}
              disabled={streaming}
              title={lastUpdateAt ? `Last updated ${new Date(lastUpdateAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}` : "Get a full status update"}
            >
              {lastUpdateAt
                ? `↻ Update · ${new Date(lastUpdateAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`
                : "↻ Update"}
            </button>
            <button
              type="button"
              className={`co-attach-btn${pendingAttachment ? " co-attach-btn--active" : ""}`}
              onClick={() => attachInputRef.current?.click()}
              disabled={streaming || attachingFile}
              title="Attach a document for prospect research (e.g. a spec sheet)"
            >
              📎
            </button>
            <input
              ref={attachInputRef}
              type="file"
              accept=".pdf,.txt,.md,.docx,.xlsx,.xls"
              style={{ display: "none" }}
              onChange={(e) => { const f = e.target.files?.[0]; handleAttachFile(f); e.target.value = ""; }}
            />
            <input
              ref={inputRef}
              value={draft}
              disabled={streaming}
              onFocus={() => setCommandOpen(draft === "/")}
              onChange={(e) => { setDraft(e.target.value); setCommandOpen(e.target.value === "/"); }}
              onKeyDown={(e) => { if (e.key === "Enter") send(); if (e.key === "Escape") setCommandOpen(false); }}
              placeholder={streaming ? "Citrus is thinking…" : "Ask Citrus anything — try a company name or URL"}
            />
            <button onClick={send} disabled={!draft.trim() || streaming}>↑</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ---- morning cards ----
function CitrusMorningCards({ conversations, hotLeads, escalations, avgHealth, lowHealth, pendingApprovals, onSwitchTab, onBulkApprove }) {
  const lowRiskCount = (pendingApprovals || []).filter((a) => !a.riskLevel || a.riskLevel === "low").length;
  return (
    <div>
      <div className="co-metrics">
        <div><b>{conversations}</b><span>conversations</span></div>
        <div><b>{hotLeads}</b><span>hot leads</span></div>
        <div><b>{escalations}</b><span>escalations</span></div>
        {avgHealth !== null ? <div><b style={{ color: avgHealth >= 80 ? "#1F8A5B" : avgHealth >= 65 ? "#B07410" : "#C04545" }}>{avgHealth}%</b><span>health</span></div> : null}
        {(pendingApprovals || []).length > 0 ? <div><b>{(pendingApprovals || []).length}</b><span>approvals</span></div> : null}
      </div>
      <div className="co-actions">
        {escalations > 0 ? <button onClick={() => onSwitchTab && onSwitchTab("inbox")}>Review escalations ↗</button> : null}
        {lowRiskCount > 0 && onBulkApprove ? <button className="co-action-primary" onClick={onBulkApprove}>Approve {lowRiskCount} low-risk ↗</button> : null}
        {lowHealth ? <button onClick={() => onSwitchTab && onSwitchTab("agents")}>Review {lowHealth.name} ↗</button> : null}
        <button onClick={() => onSwitchTab && onSwitchTab("insights")}>Open Insights ↗</button>
      </div>
    </div>
  );
}

// ---- chat component registry ----
// Each entry maps a message kind to a (message, chatProps) => JSX renderer.
// chatProps = { threads, pendingApprovals, approveItem, rejectItem, onSwitchTab, removeMsg, addCitrus }
// Add new components here — no changes to the message renderer needed.
const CHAT_COMPONENT_REGISTRY = {
  "thread-card":   (m, p) => <ThreadCardInChat   message={m} threads={p.threads}            onSwitchTab={p.onSwitchTab} />,
  "approval-card": (m, p) => <ApprovalCardInChat message={m} approvals={p.pendingApprovals} onApprove={p.approveItem}  onReject={p.rejectItem} removeMsg={p.removeMsg} addCitrus={p.addCitrus} />,
};

function ThreadCardInChat({ message: m, threads, onSwitchTab }) {
  const thread = (threads || []).find((t) => t.id === m.threadId);
  if (!thread) return null;

  const isEscalated = m.priority === "escalated" || thread.status === "needs-you";
  const snippet = thread.lastMessage || thread.preview || "";
  const customerName = thread.customerName || thread.customer || "Customer";
  const ts = thread.updatedAt || thread.lastMessageAt || "";
  const timeStr = ts ? new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";

  const handleReply = () => {
    if (onSwitchTab) onSwitchTab("inbox", { threadId: m.threadId });
  };

  return (
    <div className="co-thread-card">
      <div className="co-thread-card-header">
        <span className="co-thread-card-name">{customerName}</span>
        <span className="co-thread-card-time">{timeStr}</span>
      </div>
      {isEscalated && <span className="co-thread-card-badge co-thread-card-badge--escalated">Escalated</span>}
      {snippet ? <div className="co-thread-card-snippet">{snippet}</div> : null}
      <button className="co-thread-card-reply" onClick={handleReply}>Reply in Inbox →</button>
    </div>
  );
}

function ApprovalCardInChat({ message: m, approvals, onApprove, onReject, removeMsg, addCitrus }) {
  const [working, setWorking] = React.useState(null);
  const [done, setDone]       = React.useState(null);

  const approval = (approvals || []).find((a) => a.id === m.approvalId);
  if (!approval) return null;

  const riskColor = (r) => r === "high" ? "#C04545" : r === "medium" ? "#B07410" : "#2D7A3A";
  const riskLabel = (r) => r === "high" ? "High risk" : r === "medium" ? "Medium risk" : "Low risk";

  const act = async (action) => {
    setWorking(action);
    try {
      if (action === "approve") await onApprove(approval.id);
      else await onReject(approval.id);
      setDone(action);
      if (removeMsg) removeMsg(m._id);
      if (addCitrus) addCitrus({ kind: "answer", text: action === "approve" ? "Approved." : "Rejected." });
    } catch {
      if (addCitrus) addCitrus({ kind: "answer", text: "Couldn't complete that — please try again." });
    } finally {
      setWorking(null);
    }
  };

  if (done) return <div data-testid="approval-done">{done === "approve" ? "Approved." : "Rejected."}</div>;

  const kindLabel = approval.kind === "learning" ? "Proposed learning" : (approval.title || "Pending action");
  const bodyText  = approval.body || approval.suggestedReply || approval.text;

  return (
    <div className="co-thread-card" data-testid="approval-card">
      <div className="co-thread-card-header">
        <span className="co-thread-card-name" data-testid="agent-name">{approval.agentName || "Agent"}</span>
        <span data-testid="risk-badge" style={{ fontSize: "11px", fontWeight: 600, padding: "2px 8px", borderRadius: "999px",
          color: riskColor(approval.riskLevel), background: "color-mix(in oklab, currentColor 10%, transparent)" }}>
          {riskLabel(approval.riskLevel)}
        </span>
      </div>
      <div style={{ fontSize: "12px", color: "var(--ink-2)", fontWeight: 500, marginBottom: "4px" }} data-testid="approval-kind">
        {kindLabel}
      </div>
      {approval.from ? (
        <div style={{ fontSize: "12px", color: "var(--ink-3)", marginBottom: "4px" }} data-testid="approval-from">
          From: {approval.from}
        </div>
      ) : null}
      {bodyText ? (
        <div className="co-thread-card-snippet" data-testid="approval-text">{bodyText}</div>
      ) : null}
      {approval.amount ? (
        <div style={{ fontSize: "12px", fontWeight: 600, color: "var(--ink-2)", marginBottom: "4px" }}>
          Amount: {approval.amount}
        </div>
      ) : null}
      <div style={{ display: "flex", gap: "8px" }}>
        <button className="co-thread-card-reply" data-testid="approve-btn" disabled={!!working} onClick={() => act("approve")}>
          {working === "approve" ? "Approving…" : "Approve"}
        </button>
        <button className="co-thread-card-reply" data-testid="reject-btn" disabled={!!working} onClick={() => act("reject")}
          style={{ color: "var(--ink-3)" }}>
          {working === "reject" ? "Rejecting…" : "Reject"}
        </button>
      </div>
    </div>
  );
}

// ---- morning briefing (replaces CitrusMorningCards on the home screen) ----
function CitrusMorningBriefing({ briefing, onPillClick }) {
  if (!briefing) return null;
  return (
    <div className="co-briefing">
      <div className="co-briefing-greeting">
        Good morning{briefing.name ? `, ${briefing.name}` : ""}. <span className="co-briefing-date">{briefing.dateStr}</span>
      </div>
      <div className="co-briefing-dayframe">{briefing.dayFrame}</div>
      {briefing.priorityItems && briefing.priorityItems.length > 0 && (
        <>
          <div className="co-briefing-section-label">Worth your 10 minutes this morning</div>
          <div className="co-briefing-items">
            {briefing.priorityItems.map((item, i) => (
              <div key={i} className="co-briefing-item">
                <span className="co-briefing-num">{i + 1}</span>
                <span className="co-briefing-item-text">{item}</span>
              </div>
            ))}
          </div>
        </>
      )}
      {briefing.worthKnowing && (
        <div className="co-briefing-worth-knowing">
          <div className="co-briefing-section-label">Also worth knowing</div>
          <div className="co-briefing-worth-knowing-text">{briefing.worthKnowing}</div>
        </div>
      )}
      {briefing.nowQueue && briefing.nowQueue.length > 0 && (
        <div className="co-briefing-now-queue">
          <div className="co-briefing-section-label">Right now</div>
          {briefing.nowQueue.map((item, i) => (
            <div key={i} className="co-briefing-now-row">
              <span className="co-briefing-num">{i + 1}</span>
              <span className="co-briefing-now-name">{item.customerName}</span>
              <span className="co-briefing-now-reason">{item.urgencyReason}</span>
              {item.waitMs != null && (
                <span className="co-briefing-now-wait">{Math.round(item.waitMs / 60000)}m</span>
              )}
            </div>
          ))}
        </div>
      )}
      {briefing.pills && briefing.pills.length > 0 && (
        <div className="co-briefing-pills">
          {briefing.pills.map((pill, i) => (
            <button key={i} className="co-briefing-pill" onClick={() => onPillClick && onPillClick(pill)}>
              {pill} →
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

// ---- live approvals card with inline actions ----
function CitrusApprovalsCard({ approvals, canWrite, onApprove, onReject, onBulkLowRisk, onSwitchTab }) {
  const [working, setWorking] = useState(null);
  const [localList, setLocalList] = useState(approvals);
  useEffect(() => setLocalList(approvals), [approvals]);

  const riskColor = (r) => r === "high" ? "#C04545" : r === "medium" ? "#B07410" : "#6B7280";
  const lowRiskCount = localList.filter((a) => !a.riskLevel || a.riskLevel === "low").length;

  const handle = async (id, action) => {
    setWorking(id);
    try {
      const next = await (action === "approve" ? onApprove(id) : onReject(id));
      setLocalList(next !== undefined ? next : localList.filter((a) => a.id !== id));
    } catch {} finally { setWorking(null); }
  };

  if (!localList.length) return <div className="co-empty">All clear — no pending approvals.</div>;

  return (
    <div className="co-approvals-list">
      {lowRiskCount > 0 && canWrite && onBulkLowRisk && (
        <button className="co-bulk-approve" onClick={onBulkLowRisk}>
          Approve all {lowRiskCount} low-risk ↗
        </button>
      )}
      {localList.slice(0, 6).map((a) => {
        const text = a.proposedLesson?.what || a.title || "Proposed lesson";
        const risk = a.riskLevel || "low";
        return (
          <div key={a.id} className="co-approval-row">
            <span className="co-approval-risk" style={{ color: riskColor(risk) }}>{risk}</span>
            <span className="co-approval-text">{text.length > 90 ? text.slice(0, 90) + "…" : text}</span>
            {canWrite ? (
              <div className="co-approval-btns">
                <button disabled={working === a.id} onClick={() => handle(a.id, "approve")}>✓</button>
                <button disabled={working === a.id} onClick={() => handle(a.id, "reject")}>✕</button>
              </div>
            ) : null}
          </div>
        );
      })}
      {localList.length > 6 && (
        <button className="co-approvals-more" onClick={() => onSwitchTab && onSwitchTab("approvals")}>
          +{localList.length - 6} more — Open Approvals ↗
        </button>
      )}
    </div>
  );
}

// ---- agent pause/resume confirm card ----
function CitrusAgentActionCard({ agentId, agentName, currentStatus, targetStatus, onCommit }) {
  const [working, setWorking] = useState(false);
  const [done, setDone]       = useState(false);
  const commit = async () => {
    setWorking(true);
    await onCommit(agentId, targetStatus);
    setDone(true);
  };
  if (done) return null;
  return (
    <div className="co-agent-action">
      <span>{targetStatus === "paused" ? "⏸" : "▶"} {targetStatus === "paused" ? "Pause" : "Resume"} <b>{agentName}</b>?</span>
      <div className="co-agent-action-btns">
        <button className="btn btn-primary btn-sm" disabled={working} onClick={commit}>{working ? "…" : "Confirm"}</button>
      </div>
    </div>
  );
}

// ---- lead cards ----
function CitrusContactCards({ contacts, onSwitchTab }) {
  return (
    <div className="co-card-list">
      {leads.length
        ? contacts.map((l) => (
            <div className="co-lead-card" key={l.id}>
              <b>{l.customerName || l.customer}</b>
              <span>{l.last || "Awaiting review"}</span>
              <em>hot</em>
              <button onClick={() => onSwitchTab && onSwitchTab("inbox")}>reply ↗</button>
            </div>
          ))
        : <div className="co-empty">No hot leads right now.</div>}
    </div>
  );
}

// ---- insights cards ----
function CitrusInsightsCards({ conversations, hotLeads, escalations, avgHealth, onSwitchTab }) {
  return (
    <>
      <div className="co-metrics">
        <div><b>{conversations}</b><span>Conversations</span></div>
        <div><b>{hotLeads}</b><span>Hot leads</span></div>
        <div><b>{escalations}</b><span>Escalations</span></div>
        {avgHealth !== null ? <div><b>{avgHealth}%</b><span>Health</span></div> : null}
      </div>
      <div className="co-actions"><button onClick={() => onSwitchTab && onSwitchTab("insights")}>Open Insights ↗</button></div>
    </>
  );
}

// ---- health breakdown — explains what's driving the score ----
function CitrusHealthBreakdown({ healthData: hd, onSwitchTab }) {
  if (!hd) return <div className="co-empty">Not enough data to compute health score yet.</div>;

  const scoreColor = hd.score >= 85 ? "#1F8A5B" : hd.score >= 70 ? "#B07410" : "#C04545";
  const escGood    = hd.escPenalty === 0;

  return (
    <div className="co-health-breakdown">
      <div className="co-hb-headline">
        <span className="co-hb-score" style={{ color: scoreColor }}>{hd.score}%</span>
        <span className="co-hb-label">
          {hd.score >= 85 ? "Healthy" : hd.score >= 70 ? "Needs attention" : "Below threshold"}
        </span>
      </div>

      <div className="co-hb-factors">
        <div className="co-hb-factor">
          <span className="co-hb-factor-icon">🎯</span>
          <div className="co-hb-factor-body">
            <b>Agent accuracy</b>
            <span>Avg {hd.avgAcc}% across {hd.agentsSorted.length} live agent{hd.agentsSorted.length !== 1 ? "s" : ""}</span>
            {hd.agentsSorted.length > 0 && (
              <div className="co-hb-agents">
                {hd.agentsSorted.map((a) => (
                  <div key={a.id} className="co-hb-agent-row">
                    <span className="co-hb-agent-name">{a.name}</span>
                    <span className="co-hb-agent-bar">
                      <span style={{ width: `${a.accuracy || 82}%`, background: (a.accuracy || 82) >= 80 ? "#1F8A5B" : (a.accuracy || 82) >= 70 ? "#B07410" : "#C04545" }} />
                    </span>
                    <span className="co-hb-agent-pct" style={{ color: (a.accuracy || 82) >= 80 ? "#1F8A5B" : (a.accuracy || 82) >= 70 ? "#B07410" : "#C04545" }}>
                      {a.accuracy || 82}%
                    </span>
                  </div>
                ))}
              </div>
            )}
          </div>
        </div>

        <div className="co-hb-factor">
          <span className="co-hb-factor-icon">{escGood ? "✅" : "⚡"}</span>
          <div className="co-hb-factor-body">
            <b>Escalation rate</b>
            <span>
              {hd.escalations} of {hd.totalConvos} conversations escalated ({hd.escRatePct}%)
              {hd.escPenalty > 0 ? ` — −${hd.escPenalty} pts penalty` : " — no penalty"}
            </span>
          </div>
        </div>
      </div>

      {hd.suggestions.length > 0 && (
        <div className="co-hb-suggestions">
          <div className="co-hb-suggestions-label">How to improve</div>
          {hd.suggestions.map((s, i) => (
            <div key={i} className="co-hb-suggestion">
              <span>{s.icon}</span>
              <span>{s.text}</span>
            </div>
          ))}
        </div>
      )}

      <div className="co-actions">
        {hd.weakAgents.length > 0 && <button onClick={() => onSwitchTab && onSwitchTab("agents")}>Review {hd.weakAgents[0].name} ↗</button>}
        {hd.escalations > 0 && <button onClick={() => onSwitchTab && onSwitchTab("inbox")}>Review escalations ↗</button>}
        <button onClick={() => onSwitchTab && onSwitchTab("knowledge")}>Open Playbooks ↗</button>
      </div>
    </div>
  );
}

// ---- task confirm card (internal team task → /approvals) ----
function CitrusTaskConfirmCard({ text, proposedTitle, canWrite, onSwitchTab, onDismiss }) {
  const [title,  setTitle]  = useState(proposedTitle || text.split("\n")[0].slice(0, 120));
  const [status, setStatus] = useState("idle");
  const [errMsg, setErrMsg] = useState("");

  const submit = async () => {
    if (!canWrite || status === "submitting") return;
    setStatus("submitting");
    setErrMsg("");
    try {
      await citrusJson("/approvals", {
        method: "POST",
        body: JSON.stringify({ title: title.trim() || text.slice(0, 120), body: text, kind: "task", submittedVia: "citrus-chat" }),
      });
      setStatus("done");
    } catch (e) {
      setStatus("error");
      setErrMsg(e.message);
    }
  };

  if (status === "done") {
    return (
      <div className="co-task-confirm co-task-confirm--done">
        <span className="co-confirm-check">✓</span>
        <span>Task added to your approvals queue.</span>
        <button className="co-confirm-goto" onClick={() => onSwitchTab && onSwitchTab("approvals")}>View approvals ↗</button>
      </div>
    );
  }

  return (
    <div className="co-task-confirm">
      <div className="co-confirm-label">Internal task — confirm to submit</div>
      <input
        className="co-confirm-title-input"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Task title…"
      />
      {errMsg ? <div className="co-confirm-err">{errMsg}</div> : null}
      <div className="co-confirm-actions">
        <button className="btn btn-primary btn-sm" onClick={submit} disabled={!canWrite || status === "submitting"}>
          {status === "submitting" ? "Submitting…" : "Submit task ↗"}
        </button>
        <button className="btn btn-ghost btn-sm" onClick={onDismiss}>Dismiss</button>
      </div>
    </div>
  );
}

// ---- support confirm card (platform support/feature → /auth/support-requests) ----
function CitrusSupportConfirmCard({ text, requestType, canWrite, onDismiss }) {
  const defaultSubject = text.split("\n")[0].replace(/^[^:]+:\s*/, "").slice(0, 80);
  const [type,    setType]    = useState(requestType || "support_ticket");
  const [subject, setSubject] = useState(defaultSubject || text.slice(0, 80));
  const [desc,    setDesc]    = useState(text);
  const [status,  setStatus]  = useState("idle");
  const [errMsg,  setErrMsg]  = useState("");

  const submit = async () => {
    if (!canWrite || status === "submitting") return;
    setStatus("submitting");
    setErrMsg("");
    try {
      await citrusJson("/auth/support-requests", {
        method: "POST",
        body: JSON.stringify({ subject: subject.trim() || text.slice(0, 80), description: desc, type, submittedVia: "citrus-chat" }),
      });
      setStatus("done");
    } catch (e) {
      setStatus("error");
      setErrMsg(e.message);
    }
  };

  if (status === "done") {
    return (
      <div className="co-support-confirm co-support-confirm--done">
        <span className="co-confirm-check">✓</span>
        <span>Sent to Citrus Support.</span>
      </div>
    );
  }

  return (
    <div className="co-support-confirm">
      <div className="co-confirm-label">Send to Citrus Support</div>
      <select className="co-confirm-type-select" value={type} onChange={(e) => setType(e.target.value)}>
        <option value="support_ticket">Support ticket</option>
        <option value="feature_request">Feature request</option>
        <option value="idea">Idea</option>
      </select>
      <input
        className="co-confirm-title-input"
        value={subject}
        onChange={(e) => setSubject(e.target.value)}
        placeholder="Subject…"
      />
      <textarea
        className="co-confirm-desc-input"
        value={desc}
        onChange={(e) => setDesc(e.target.value)}
        rows={3}
        placeholder="Description…"
      />
      {errMsg ? <div className="co-confirm-err">{errMsg}</div> : null}
      <div className="co-confirm-actions">
        <button className="btn btn-primary btn-sm" onClick={submit} disabled={!canWrite || status === "submitting"}>
          {status === "submitting" ? "Sending…" : "Send to Citrus Support ↗"}
        </button>
        <button className="btn btn-ghost btn-sm" onClick={onDismiss}>Dismiss</button>
      </div>
    </div>
  );
}

function CitrusWebLookupConfirmCard({ query, onConfirm, onDismiss, disabled }) {
  const ref = React.useRef(null);
  React.useEffect(() => { ref.current?.focus(); }, []);
  const onKey = (e) => {
    if (disabled) return;
    if (e.key === "Enter") { e.preventDefault(); onConfirm(); }
    if (e.key === "Escape") { e.preventDefault(); onDismiss(); }
  };
  return (
    <div className="co-web-lookup-confirm" role="dialog" aria-modal="true" onKeyDown={onKey}>
      <div className="co-confirm-label">Search the web for more?</div>
      {query ? <div className="co-web-lookup-query">"{query}"</div> : null}
      <div className="co-confirm-actions">
        <button ref={ref} className="btn btn-primary btn-sm" onClick={onConfirm} disabled={disabled}>Confirm — Search web</button>
        <button className="btn btn-ghost btn-sm" onClick={onDismiss} disabled={disabled}>No, KB only</button>
      </div>
    </div>
  );
}


// ---- search results ----
function CitrusSearchResults({ agentHits, threadHits, onSwitchTab }) {
  return (
    <div className="co-card-list">
      <div className="co-search-head">Agents</div>
      {agentHits.length
        ? agentHits.map((a) => <button className="co-result" key={a.id} onClick={() => onSwitchTab && onSwitchTab("agents")}><b>{a.name}</b><span>{a.role}</span></button>)
        : <span className="co-empty">No agent matches</span>}
      <div className="co-search-head">Conversations</div>
      {threadHits.length
        ? threadHits.map((t) => <button className="co-result" key={t.id} onClick={() => onSwitchTab && onSwitchTab("inbox")}><b>{t.customerName || t.customer}</b><span>{t.last}</span></button>)
        : <span className="co-empty">No conversation matches</span>}
    </div>
  );
}

// ---- teach / learn workflow ----
function CitrusLearnWorkflow({ agents = [], onSwitchTab }) {
  const activeAgents = (agents || []).filter((a) => a.status !== "paused");
  const [scope,    setScope]   = useState("global");
  const [agentId,  setAgentId] = useState(activeAgents[0]?.id || "");
  const [draft,    setDraft]   = useState({ what: "", why: "" });
  const [saving,   setSaving]  = useState(false);
  const [result,   setResult]  = useState(null);

  const selectedAgent = activeAgents.find((a) => a.id === agentId) || activeAgents[0] || null;
  const canSave = draft.what.trim() && (scope === "global" || selectedAgent) && !saving;

  const submit = () => {
    if (!draft.what.trim()) { setResult({ tone: "warn", text: "Write the lesson first." }); return; }
    if (scope === "agent" && !selectedAgent) { setResult({ tone: "warn", text: "Choose an agent first." }); return; }
    setSaving(true); setResult(null);
    const body = scope === "global"
      ? { what: draft.what.trim(), why: draft.why.trim() || null }
      : { note: draft.what.trim(), context: draft.why.trim() || "" };
    const path = scope === "global" ? "/learnings" : `/agents/${encodeURIComponent(selectedAgent.id)}/teach`;
    citrusJson(path, { method: "POST", body: JSON.stringify(body) })
      .then(() => {
        const target = scope === "global" ? `all agents (${activeAgents.length})` : selectedAgent.name;
        setResult({ tone: "good", text: `Saved. This lesson now applies to ${target}.` });
        setDraft({ what: "", why: "" });
      })
      .catch((e) => setResult({ tone: "warn", text: `Could not save: ${e.message}` }))
      .finally(() => setSaving(false));
  };

  return (
    <div className="co-form-card">
      <label>Lesson
        <textarea value={draft.what} onChange={(e) => setDraft((d) => ({ ...d, what: e.target.value }))}
          placeholder="When a customer asks about warranty claims, escalate to support before giving a final answer." />
      </label>
      <label>{scope === "global" ? "Why it matters" : "Context or example"}
        <textarea value={draft.why} onChange={(e) => setDraft((d) => ({ ...d, why: e.target.value }))}
          placeholder={scope === "global" ? "Optional reason this should become a business rule." : "Optional conversation snippet or example."} />
      </label>
      <label>Apply to
        <select value={scope} onChange={(e) => setScope(e.target.value)}>
          <option value="global">All agents</option>
          <option value="agent" disabled={activeAgents.length === 0}>One agent</option>
        </select>
      </label>
      {scope === "agent" ? (
        <label>Agent
          <select value={selectedAgent?.id || ""} onChange={(e) => setAgentId(e.target.value)} disabled={activeAgents.length === 0}>
            {activeAgents.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
          </select>
        </label>
      ) : null}
      <div className="co-form-note">
        {scope === "global" ? "Global lessons are high-priority business rules used by every agent." : "Agent lessons are taught directly to the selected agent."}
      </div>
      {result ? <div className={`co-form-status co-form-status-${result.tone}`}>{result.text}</div> : null}
      <div className="co-form-actions">
        <button className="btn btn-primary" disabled={!canSave} onClick={submit}>{saving ? "Saving…" : "Save learning"}</button>
        <button className="btn btn-ghost" type="button" onClick={() => onSwitchTab && onSwitchTab("knowledge")}>Open Knowledge</button>
      </div>
    </div>
  );
}

// ---- gap suggestion cards ----
const GAP_TYPE_LABELS = {
  escalation_pattern: "Escalation pattern",
  low_confidence:     "Low confidence",
  stalled_lead:       "Stalled lead",
  accuracy_drop:      "Accuracy drop",
};
const GAP_TYPE_ICONS = {
  escalation_pattern: "⚡",
  low_confidence:     "💬",
  stalled_lead:       "⏱",
  accuracy_drop:      "📉",
};

function CitrusGapCards({ gaps, canWrite, agents, onDismissAll }) {
  const [localGaps, setLocalGaps] = useState(gaps);
  const [working,   setWorking]   = useState(null);
  const [editId,    setEditId]    = useState(null);
  const [editText,  setEditText]  = useState("");

  useEffect(() => setLocalGaps(gaps), [gaps]);

  const remove = (id) => setLocalGaps((prev) => prev.filter((g) => g.id !== id));

  const approve = async (gap, scope) => {
    if (!canWrite) return;
    setWorking(gap.id + scope);
    try {
      await citrusJson(`/gap-suggestions/${gap.id}/approve`, {
        method: "POST", body: JSON.stringify({ scope }),
      });
      remove(gap.id);
    } catch (e) {
      alert(`Could not apply: ${e.message}`);
    } finally { setWorking(null); }
  };

  const dismiss = async (id) => {
    setWorking(id + "dismiss");
    try {
      await citrusJson(`/gap-suggestions/${id}/dismiss`, { method: "POST", body: JSON.stringify({}) });
      remove(id);
    } catch {} finally { setWorking(null); }
  };

  if (!localGaps.length) return <div className="co-empty">All caught up — no open suggestions.</div>;

  return (
    <div className="co-gap-list">
      {localGaps.map((g) => {
        const busy = working && working.startsWith(g.id);
        const isEditing = editId === g.id;
        const hasAgent = !!g.agentId;
        return (
          <div key={g.id} className="co-gap-card">
            <div className="co-gap-header">
              <span className="co-gap-icon">{GAP_TYPE_ICONS[g.type] || "🔍"}</span>
              <span className="co-gap-type">{GAP_TYPE_LABELS[g.type] || g.type}</span>
              {g.evidenceCount > 1 && <span className="co-gap-evidence">{g.evidenceCount}× in 7 days</span>}
              {g.agentName && <span className="co-gap-agent">{g.agentName}</span>}
            </div>
            {isEditing ? (
              <div className="co-gap-edit">
                <textarea value={editText} onChange={(e) => setEditText(e.target.value)} rows={3} />
                <div className="co-gap-edit-btns">
                  <button className="btn btn-primary btn-sm" disabled={busy}
                    onClick={() => { approve({ ...g, proposedLesson: editText.trim() || g.proposedLesson }, hasAgent ? "agent" : "global"); setEditId(null); }}>
                    Save &amp; approve
                  </button>
                  <button className="btn btn-ghost btn-sm" onClick={() => setEditId(null)}>Cancel</button>
                </div>
              </div>
            ) : (
              <p className="co-gap-lesson">{g.proposedLesson}</p>
            )}
            {!isEditing && canWrite && (
              <div className="co-gap-actions">
                {hasAgent && (
                  <button disabled={busy} onClick={() => approve(g, "agent")}>
                    {busy && working === g.id + "agent" ? "…" : `Teach ${g.agentName}`}
                  </button>
                )}
                <button disabled={busy} onClick={() => approve(g, "global")}>
                  {busy && working === g.id + "global" ? "…" : "Apply to all"}
                </button>
                <button className="co-gap-edit-btn" disabled={busy} onClick={() => { setEditId(g.id); setEditText(g.proposedLesson); }}>Edit</button>
                <button className="co-gap-dismiss" disabled={busy} onClick={() => dismiss(g.id)}>Dismiss</button>
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

window.CitrusHomeTab = CitrusHomeTab;
