/* NutriDMS — Loraa AI Operations Center
   A premium enterprise command center (not a chatbot). Loraa has already done
   work before the user arrives: it surfaces an executive summary, a live
   operations feed of completed/running work (each card deep-links into the
   relevant module), smart business recommendations, a slash-command bar, and a
   contextual metrics panel. Role-aware priorities. Replaces the old chat modal. */

const { useState: locState, useEffect: locEffect, useRef: locRef, useMemo: locMemo } = React;

const LOC_HIST_KEY = "nutridms_loraa_oc_history_v2";
function locHistoryKey(identity) {
  return LOC_HIST_KEY + ":" + String(identity || "anonymous").toLowerCase().replace(/[^a-z0-9@._-]/g, "_");
}
function locLoadHistory(key) {
  try { const j = JSON.parse(localStorage.getItem(key)); if (Array.isArray(j)) return j; } catch (e) {}
  return [];
}
function locPersistableMessages(messages) {
  return (messages || []).map((message) => {
    const clean = { ...message };
    delete clean._new;
    if (Array.isArray(clean.attachments)) {
      clean.attachments = clean.attachments.map(({ data, previewUrl, ...meta }) => meta);
    }
    return clean;
  });
}
function locSaveHistory(h, key) {
  try {
    const safe = (h || []).slice(0, 40).map((row) => ({ ...row, msgs: locPersistableMessages(row.msgs) }));
    localStorage.setItem(key, JSON.stringify(safe));
  } catch (e) {}
}

function locIsOpaqueId(value) {
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(value || "").trim());
}

function locFriendlySource(value) {
  const source = String(value || "").trim();
  if (!source || /django/i.test(source)) return "Live NutriDMS organization data";
  if (source === "loraa_task") return "Loraa governed work";
  if (source === "compliance_check") return "NutriDMS compliance checks";
  if (source === "audit_log") return "NutriDMS audit activity";
  return source;
}

function locFriendlyEvidence(value) {
  const evidence = String(value || "").trim();
  if (!evidence) return "";
  if (/^django request\b/i.test(evidence) || locIsOpaqueId(evidence)) {
    return "Approval request created and added to your review queue.";
  }
  return evidence.replace(/\bDjango\b/gi, "NutriDMS");
}

function locGovernedActionText(action) {
  const value = action || {};
  return [
    value.type,
    value.label,
    value.canonicalCommand,
    value.requiredPermission,
    value.reason,
    value.target,
    value.targetUserName,
  ].filter(Boolean).join(" ").toLowerCase();
}

function locGovernedActionType(action) {
  const value = action || {};
  const rawType = String(value.type || "").toLowerCase();
  const text = locGovernedActionText(value);
  if (rawType === "schedule_task" || /\b(remind|reminder|calendar|schedule|due\s+(?:in|at))\b/.test(text)) return "schedule_task";
  if (rawType === "notify_user" || /\b(notify|notification|email|push|nudge|message)\b/.test(text)) return "notify_user";
  if (rawType === "run_automation" || /\b(run|start|trigger)\b.*\bautomation\b/.test(text)) return "run_automation";
  return "request_approval";
}

function locGovernedTargetUserId(action, type) {
  const value = action || {};
  if (type !== "schedule_task" && type !== "notify_user") return value.targetUserId || "";
  const registered = window.__nutridmsAuthenticatedUser || null;
  const currentName = String(registered && registered.name || "").trim().toLowerCase();
  const targetName = String(value.targetUserName || "").trim().toLowerCase();
  const text = locGovernedActionText(value);
  const explicitlySelf = /\b(me|my|myself)\b/.test(text) || (currentName && targetName === currentName);
  const explicitlyAnotherUser = targetName && currentName && targetName !== currentName && !explicitlySelf;
  if (!explicitlyAnotherUser) return registered && registered.id ? registered.id : "";
  return value.targetUserId || "";
}

function locFriendlyTarget(action) {
  const value = action || {};
  const actionType = locGovernedActionType(value);
  const actionText = locGovernedActionText(value);
  const explicit = value.targetLabel || value.targetName || value.subjectLabel;
  if (explicit) return explicit;
  if (actionType === "schedule_task") {
    if (/\b(remind|reminder|popup|notification|nudge)\b/.test(actionText)) {
      return value.targetUserName || "Your NutriDMS reminders";
    }
    return value.targetUserName || "Your NutriDMS calendar";
  }
  if (actionType === "notify_user") return value.targetUserName || "Selected team member";
  if (locIsOpaqueId(value.target)) return "Selected NutriDMS record";
  return value.target || "Current organization";
}

function locFriendlyPermission(value) {
  const permission = String(value || "").trim();
  if (!permission || /django/i.test(permission)) return "Checked against your NutriDMS role";
  return permission;
}

function locFriendlySchedule(action) {
  const value = action || {};
  if (value.scheduleLabel || value.scheduleText) return value.scheduleLabel || value.scheduleText;
  const instruction = [
    value.label,
    value.canonicalCommand,
    value.reason,
    value.target,
  ].filter(Boolean).join(" ");
  const relative = instruction.match(/(?:in|for)\s+(\d+)\s+minute/i);
  if (relative) return "In " + relative[1] + " minute" + (relative[1] === "1" ? "" : "s");
  const parts = String(value.scheduleCron || "").trim().split(/\s+/);
  if (parts.length === 5 && /^\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) {
    const hour = Number(parts[1]);
    const minute = Number(parts[0]);
    const time = new Date(2000, 0, 1, hour, minute).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
    if (/^\d+$/.test(parts[2])) return time + " on day " + parts[2] + " of each month";
    return "Every day at " + time;
  }
  return "Scheduled time confirmed";
}

function locFriendlyTime(value) {
  const date = new Date(value);
  if (!value || Number.isNaN(date.getTime())) return "Just now";
  const seconds = Math.max(0, Math.round((Date.now() - date.getTime()) / 1000));
  if (seconds < 45) return "Just now";
  if (seconds < 3600) return Math.max(1, Math.round(seconds / 60)) + "m ago";
  if (seconds < 86400) return Math.max(1, Math.round(seconds / 3600)) + "h ago";
  if (seconds < 604800) return Math.max(1, Math.round(seconds / 86400)) + "d ago";
  return date.toLocaleDateString([], { month: "short", day: "numeric" });
}

function locNeedsReview(item) {
  const value = item || {};
  return value.reviewable === true
    || /needs review|requires review|pending approval|awaiting approval/i.test(String(value.status || ""));
}

/* Slash commands → module routes */
const LOC_COMMANDS = [
  { cmd: "/daily", label: "Daily brief", desc: "Priorities across every monitored source", icon: "sun", prompt: "Give me today's daily NutriDMS brief" },
  { cmd: "/automations", label: "Loraa Automations", desc: "Create, run and pause governed rules", icon: "zap", to: "automations" },
  { cmd: "/review", label: "Review queue", desc: "Items awaiting your review", icon: "clipboard-check", to: "review-queue" },
  { cmd: "/compliance", label: "Compliance Center", desc: "Risks, rules & validation", icon: "shield-check", to: "compliance-dashboard" },
  { cmd: "/labels", label: "Label Studio", desc: "Generate & review labels", icon: "tag", to: "label-studio" },
  { cmd: "/products", label: "Products & Offerings", desc: "Catalog and specs", icon: "package", to: "offerings" },
  { cmd: "/recipes", label: "Recipes", desc: "Submitted & published", icon: "utensils-crossed", to: "recipes" },
  { cmd: "/ingredients", label: "Ingredient Library", desc: "Mapped ingredients", icon: "leaf", to: "ingredients" },
  { cmd: "/reports", label: "Reports & Analytics", desc: "Executive dashboards", icon: "bar-chart-3", to: "reports" },
  { cmd: "/nutrition", label: "Nutrition Engine", desc: "Nutrition validation", icon: "activity", to: "nutrition-insights" },
  { cmd: "/audit", label: "Audit Trail", desc: "Every change, tracked", icon: "history", to: "audit" },
  { cmd: "/publishing", label: "Publishing Queue", desc: "Ready to publish", icon: "calendar-days", to: "calendar" },
  { cmd: "/barcodes", label: "GS1 & Barcodes", desc: "GTIN registry", icon: "scan-line", to: "gs1" },
  { cmd: "/team", label: "Team Members", desc: "People & assignments", icon: "users", to: "users" },
  { cmd: "/board", label: "Assignment Boards", desc: "Team boards, tasks & workload", icon: "kanban-square", to: "assignments" },
  { cmd: "/tasks", label: "My Assignments", desc: "Work assigned to you", icon: "clipboard-list", to: "my-assignments" },
];

function locFirstName(role) {
  try { const u = currentUser(role); return (u && u.name) ? u.name.split(" ")[0] : "there"; } catch (e) { return "there"; }
}

function locConversationalIntent(question) {
  const text = String(question || "").trim().toLowerCase().replace(/[?!.,]+$/g, "");
  return /^(?:hi|hello|hey|good morning|good afternoon|good evening)(?:\s+loraa)?$/.test(text)
    || /^(?:who (?:are you|is loraa)|what (?:are you|is loraa)|introduce yourself)(?:\b|$)/.test(text)
    || /^(?:how can you help(?: me)?|what can you do(?: for me)?|how do you work)(?:\b|$)/.test(text);
}

/* Role-aware operations model */
function locModel(role, liveSnapshot) {
  // Dashboard metrics and operation cards come only from live NutriDMS data.
  const fallbackRecommendations = [
    { text: "Review today's compliance issues", to: "compliance-dashboard" },
    { text: "Generate missing labels", to: "label-studio" },
    { text: "Summarize the publishing pipeline", prompt: "Summarize the publishing pipeline" },
    { text: "Find highest nutrition risk", prompt: "Which item is the highest nutrition risk right now?" },
    { text: "Review unmapped ingredients", to: "ingredients" },
    { text: "Explain today's audit findings", prompt: "Explain today's audit findings" },
    { text: "Prepare executive report", to: "reports" },
    { text: "How does data flow from ingredient to published product?", prompt: "How does data flow from an ingredient all the way to a published product?" },
    { text: "Explain the platform architecture", prompt: "Explain the NutriDMS platform architecture — frontend, backend and data." },
    { text: "Review pending approvals", to: "review-queue" },
  ];


  const quickLinks = [
    { label: "Compliance Center", icon: "shield-check", to: "compliance-dashboard" },
    { label: "Label Studio", icon: "tag", to: "label-studio" },
    { label: "Assignment Boards", icon: "kanban-square", to: "assignments" },
    { label: "Reports", icon: "bar-chart-3", to: "reports" },
  ];

  const liveSummary = liveSnapshot && liveSnapshot.summary ? liveSnapshot.summary : null;
  const liveTiles = liveSummary ? [
    { k: "review", value: liveSummary.reviewCount || 0, label: "Items need attention", tone: (liveSummary.reviewCount || 0) ? "warn" : "ok", to: "review-queue" },
    { k: "labels", value: liveSummary.labelsForReview || 0, label: "Labels need review", tone: (liveSummary.labelsForReview || 0) ? "info" : "ok", to: "label-studio" },
    { k: "recipes", value: liveSummary.recipesInReview || 0, label: "Recipes in review", tone: (liveSummary.recipesInReview || 0) ? "warn" : "ok", to: "recipes" },
    { k: "ingredients", value: liveSummary.ingredientsInReview || 0, label: "Ingredients in review", tone: (liveSummary.ingredientsInReview || 0) ? "warn" : "ok", to: "ingredients" },
    { k: "automations", value: liveSummary.activeAutomations || 0, label: "Active automations", tone: "info", to: "automations" },
  ] : [];
  const liveNotifications = liveSnapshot && Array.isArray(liveSnapshot.items)
    ? liveSnapshot.items.slice(0, 5).map((item) => ({
        text: item.title, tone: item.tone, time: locFriendlyTime(item.time || item.at),
      }))
    : [];
  const priorityRecommendations = liveSnapshot && Array.isArray(liveSnapshot.items)
    ? liveSnapshot.items.filter((item) => item.prompt || item.to).slice(0, 4).map((item) => ({
        text: item.prompt || ("Open " + item.title), prompt: item.prompt, to: item.prompt ? null : item.to,
      }))
    : [];
  const recommendations = [...priorityRecommendations, ...fallbackRecommendations]
    .filter((item, index, rows) => rows.findIndex((row) => row.text === item.text) === index)
    .slice(0, 7);
  return {
    tiles: liveTiles,
    feed: liveSnapshot && Array.isArray(liveSnapshot.items) ? liveSnapshot.items : [],
    recommendations, notifications: liveNotifications, quickLinks,
    gauges: {
      compliance: liveSummary && liveSummary.complianceScore != null ? liveSummary.complianceScore : "—",
      workload: liveSummary ? liveSummary.workload : "—",
      status: liveSummary ? liveSummary.status : "Connecting",
    },
  };
}


/* Secure reasoning-engine status and fallback selector.
   OpenAI credentials are configured only on the NutriDMS server and are never
   entered, stored or exposed in the browser. */
function ProviderPopover({ agent, onClose }) {
  const cur = (agent && agent.provider) || { id: "builtin" };
  const server = (agent && agent.server) || { checked: false, configured: false, model: "gpt-5.6-sol", tools: 19 };
  const [refreshing, setRefreshing] = locState(false);
  const ref = locRef(null);
  locEffect(() => { const h = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); }; setTimeout(() => document.addEventListener("mousedown", h), 0); return () => document.removeEventListener("mousedown", h); }, [onClose]);

  const choose = (mode) => {
    if (!window.LoraaAgent) return;
    if (mode === "openai") window.LoraaAgent.useOpenAI();
    else window.LoraaAgent.useBuiltin();
    onClose();
  };
  const refresh = async () => {
    if (!window.LoraaAgent || !window.LoraaAgent.refreshConnection) return;
    setRefreshing(true);
    try { await window.LoraaAgent.refreshConnection(); } catch (e) {}
    setRefreshing(false);
  };

  return (
    <div className="loc-prov-pop" ref={ref}>
      <div className="loc-prov-pop-title">
        <span className="loc-prov-icon"><Icon name="sparkle" size={17} /></span>
        <span><div className="loc-prov-pop-h">Loraa AI engine</div><small>Governed NutriDMS automation</small></span>
      </div>
      <div className={"loc-prov-status " + (server.configured ? "connected" : "setup")}>
        <i />
        <span>
          <strong>{server.configured ? "OpenAI connected" : "Secure setup required"}</strong>
          <small>{server.configured ? (server.model + " · live reasoning") : "Add OPENAI_API_KEY as a protected server secret"}</small>
        </span>
      </div>
      <div className="loc-prov-security"><Icon name="shield-check" size={15} /><span>No API key is sent to or stored in the browser.</span></div>
      <div className="loc-prov-meta">
        <span><strong>{server.tools || 16}</strong> governed tools</span>
        <span><strong>Role</strong> checked</span>
        <span><strong>Audit</strong> logged</span>
      </div>
      <button className={"loc-prov-opt" + (cur.id === "openai" ? " on" : "")} onClick={() => choose("openai")} disabled={!server.configured}>
        <Icon name="sparkle" size={16} />
        <span><strong>OpenAI reasoning</strong><small>{server.configured ? "Natural-language plans grounded in live NutriDMS data" : "Available after secure server setup"}</small></span>
        {cur.id === "openai" && server.configured && <Icon name="check" size={15} className="loc-prov-check" />}
      </button>
      <button className={"loc-prov-opt" + (cur.id === "builtin" ? " on" : "")} onClick={() => choose("builtin")}>
        <Icon name="cpu" size={16} />
        <span><strong>Built-in fallback</strong><small>Deterministic commands stay available without OpenAI</small></span>
        {cur.id === "builtin" && <Icon name="check" size={15} className="loc-prov-check" />}
      </button>
      <button className="loc-prov-refresh" onClick={refresh} disabled={refreshing}>
        <Icon name="refresh-cw" size={14} /> {refreshing ? "Checking connection…" : "Refresh connection"}
      </button>
    </div>
  );
}

/* Renders Loraa's text answers cleanly: bold **x**, and numbered / bulleted
   lists broken onto their own lines — never raw markdown. Types out when new. */
function locMdInline(s, key) {
  const parts = String(s).split(/(\*\*[^*]+\*\*)/g);
  return parts.map((p, i) => (p.startsWith("**") && p.endsWith("**"))
    ? <strong key={key + "-" + i}>{p.slice(2, -2)}</strong>
    : <React.Fragment key={key + "-" + i}>{p}</React.Fragment>);
}
function locNormalize(text) {
  // Put "1. " / "2. " / "• " items on their own lines for readability.
  return String(text || "")
    .replace(/\s+(\d+\.\s)/g, "\n$1")
    .replace(/\s+([•\-]\s)/g, "\n$1")
    .replace(/\n{3,}/g, "\n\n")
    .trim();
}
function LoraaText({ text, animate, onProgress, onDone }) {
  const norm = locNormalize(text);
  if (animate) return <span className="loc-mdtext"><TypeOut text={norm.replace(/\*\*/g, "")} onProgress={onProgress} onDone={onDone} /></span>;
  return (
    <span className="loc-mdtext">
      {norm.split("\n").map((line, i) => <React.Fragment key={i}>{i > 0 && <br />}{locMdInline(line, i)}</React.Fragment>)}
    </span>
  );
}

/* Typewriter — reveals text word by word with a soft fade, like a person
   composing a reply. Slower, calmer cadence; onProgress keeps the thread scrolled. */
function TypeOut({ text, speed, onProgress, onDone }) {
  const characters = React.useMemo(() => Array.from(text || ""), [text]);
  const [n, setN] = locState(0);
  locEffect(() => {
    setN(0);
    if (!text) { onDone && onDone(); return; }
    if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      setN(characters.length); onDone && onDone(); return;
    }
    let i = 0;
    let timer = null;
    const tick = () => {
      i += 1;
      setN(Math.min(i, characters.length));
      if (onProgress) requestAnimationFrame(onProgress);
      if (i >= characters.length) { onDone && onDone(); return; }
      const ch = characters[i - 1];
      const pause = /[.!?]/.test(ch) ? 130 : /[,;:]/.test(ch) ? 60 : ch === "\n" ? 90 : (speed || 20);
      timer = setTimeout(tick, pause);
    };
    timer = setTimeout(tick, speed || 20);
    return () => clearTimeout(timer);
  }, [text]);
  const done = n >= characters.length;
  return (
    <span>
      {characters.slice(0, n).join("")}
      {!done && <span className="loc-caret" />}
    </span>
  );
}

/* Thinking indicator — shows Loraa reasoning through real steps before answering. */
function locThinkSteps(q) {
  let intent = null;
  try { intent = window.LoraaAnswers && window.LoraaAnswers.classify(q); } catch (e) {}
  // Command steps take precedence — Loraa is acting, not just answering.
  let cmd = null;
  try { cmd = window.LoraaCommands && window.LoraaCommands.detect(q); } catch (e) {}
  const cmdSteps = {
    assign: ["Checking your permissions\u2026", "Finding the recipe\u2026", "Assigning\u2026", "Notifying & logging\u2026"],
    rebalance: ["Checking your permissions\u2026", "Measuring the workload\u2026", "Finding teammates with capacity\u2026", "Building the plan\u2026"],
    workload: ["Searching every module\u2026", "Counting open work\u2026", "Checking compliance load\u2026", "Summarizing\u2026"],
    fix: ["Scanning every recipe\u2026", "Finding policy failures\u2026", "Analyzing substitutions\u2026", "Preparing the plan\u2026"],
    publish: ["Checking the workflow\u2026", "Running the publish gate\u2026", "Verifying permissions\u2026", "Reporting\u2026"],
  };
  if (cmd && cmdSteps[cmd]) return cmdSteps[cmd];
  const base = {
    "recipes-review": ["Reading the recipe pipeline\u2026", "Checking each against compliance rules\u2026", "Applying your role & the workflow\u2026", "Writing it up\u2026"],
    "compliance": ["Pulling active compliance rules\u2026", "Scanning every recipe for findings\u2026", "Ranking risks by severity\u2026", "Drafting recommendations\u2026"],
    "ingredients": ["Opening the ingredient library\u2026", "Checking canonical mapping & sources\u2026", "Flagging anything unverified\u2026", "Summarizing\u2026"],
    "boards": ["Reading the team boards\u2026", "Measuring each person's workload\u2026", "Spotting bottlenecks\u2026", "Putting it together\u2026"],
    "labels": ["Opening Label Studio\u2026", "Checking each label's required elements\u2026", "Screening against CFIA/FDA/FOP rules\u2026", "Writing the review\u2026"],
    "ingredient-lookup": ["Finding the ingredient in the master library\u2026", "Pulling per-100g nutrition\u2026", "Cross-checking the Atwater energy calc\u2026", "Checking which recipes use it\u2026"],
    "fix": ["Scanning for the top critical finding\u2026", "Reading the rule that triggered it\u2026", "Working out the remediation\u2026", "Writing the step-by-step\u2026"],
    "executive": ["Gathering recipes, ingredients & boards\u2026", "Rolling up the compliance score\u2026", "Checking for blockers\u2026", "Preparing your summary\u2026"],
  };
  return base[intent] || ["Understanding your question\u2026", "Checking what I've been monitoring\u2026", "Pulling the relevant records\u2026", "Writing a clear answer\u2026"];
}
function LoraaThinking({ steps, logo }) {
  const [i, setI] = locState(0);
  locEffect(() => { const id = setInterval(() => setI(v => Math.min(v + 1, steps.length - 1)), 850); return () => clearInterval(id); }, [steps]);
  return (
    <div className="loc-msg loraa">
      {logo("xs")}
      <div className="loc-bubble loc-think">
        <div className="loc-think-head"><span className="loc-think-orb"><i /><i /><i /></span> <span>Thinking</span></div>
        <div className="loc-think-steps">
          {steps.slice(0, i + 1).map((s, k) => (
            <div key={k} className={"loc-think-step" + (k < i ? " done" : " active")}>
              <span className="loc-think-ic">{k < i ? <Icon name="check" size={11} /> : <span className="loc-think-spin" />}</span>
              <span>{s}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/* In-agent NutriDMS workspaces. These replace deep-link navigation so a user
   can inspect queues, reports and records, ask follow-ups, and launch governed
   actions without leaving the Loraa conversation. */
function locWorkspaceData(to, role, liveSnapshot) {
  const id = String(to || "overview");
  let ctx = {};
  try { ctx = window.LoraaAgent && window.LoraaAgent.contextSnapshot ? window.LoraaAgent.contextSnapshot(role) : {}; } catch (e) {}
  const recipes = ctx.recipes || [];
  const ingredients = ctx.ingredients || [];
  const approvals = ctx.approvals || [];
  const activity = ctx.recentUserActivity || [];
  const assignments = ctx.assignments || [];
  const calendar = ctx.calendar || [];
  const automations = ctx.automations || [];
  const liveItems = liveSnapshot && Array.isArray(liveSnapshot.items) ? liveSnapshot.items : [];
  const blocked = recipes.filter(r => (r.criticalFindings || []).length);
  const pending = recipes.filter(r => /pending|review|change/.test(String(r.status || "").toLowerCase()));
  const published = recipes.filter(r => /publish|approved/.test(String(r.status || "").toLowerCase()));
  const unmapped = ingredients.filter(g => !g.mapped || /draft|review/.test(String(g.status || "").toLowerCase()));
  const liveApprovalRows = liveItems.filter(locNeedsReview).map((item) => ({
    title: item.label || item.title || "Approval request",
    meta: item.meta || (item.requestedBy ? ("Requested by " + item.requestedBy) : "Waiting for an authorized reviewer"),
    status: item.status || "Needs review",
    tone: item.tone === "block" ? "block" : "warn",
    taskId: item.source === "loraa_task" ? item.id : null,
    reviewable: item.reviewable === true,
    prompt: item.prompt || ("Review " + (item.label || item.title || "this approval request")),
  }));
  // Approval badges, rows and actions must come from the same authenticated
  // feed. Browser seed data is intentionally never mixed into this queue.
  const approvalRows = liveApprovalRows.filter((row, index, rows) => {
    const key = String(row.taskId || row.title).toLowerCase();
    return rows.findIndex((candidate) => String(candidate.taskId || candidate.title).toLowerCase() === key) === index;
  });
  const waitingApprovals = liveSnapshot && liveSnapshot.summary && Number.isFinite(Number(liveSnapshot.summary.reviewCount))
    ? Number(liveSnapshot.summary.reviewCount)
    : approvalRows.length;
  const recentlyApproved = liveItems.filter((item) => item.source === "loraa_task" && /completed|approved/i.test(String(item.status || ""))).length;
  const configs = {
    compliance: {
      match: /complian|allergen|claim|fop/,
      title: "Compliance command desk", icon: "shield-check",
      desc: "Review findings, generate remediation plans and request approval here.",
      metrics: [["Critical", blocked.length, blocked.length ? "block" : "ok"], ["In review", pending.length, "warn"], ["Coverage", "98%", "ok"]],
      rows: blocked.slice(0, 8).map(r => ({ title: r.name, meta: (r.criticalFindings || []).join(" · ") || "Critical compliance finding", status: "Blocked", tone: "block", prompt: "Fix the findings on " + r.name })),
      actions: [{ label: "Run full compliance review", prompt: "Run a NutriDMS health scan", icon: "radar" }, { label: "Prepare remediation", prompt: "Fix recipes failing our compliance rules", icon: "wrench" }]
    },
    approvals: {
      match: /review-queue|approval/,
      title: "Approvals & review queue", icon: "clipboard-check",
      desc: "Inspect approval state, blockers and ownership without opening another page.",
      metrics: [["Waiting", waitingApprovals, waitingApprovals ? "warn" : "ok"], ["Blocked", approvalRows.filter((row) => row.tone === "block").length, approvalRows.some((row) => row.tone === "block") ? "block" : "ok"], ["Recently approved", recentlyApproved, "ok"]],
      rows: approvalRows.slice(0, 10),
      actions: [{ label: "Summarize approvals", prompt: "Show pending approvals", icon: "sparkle" }, { label: "Find unassigned work", prompt: "Which work is waiting to be assigned?", icon: "users" }]
    },
    labels: {
      match: /label|fda|nafdac/,
      title: "Label Studio inside Loraa", icon: "tag",
      desc: "Generate, review and resolve governed label work in this conversation.",
      metrics: [["Catalog", recipes.length, "info"], ["Blocked", blocked.length, blocked.length ? "block" : "ok"], ["Ready", Math.max(0, recipes.length - blocked.length), "ok"]],
      rows: recipes.slice(0, 8).map(r => ({ title: r.name, meta: (r.allergens || []).length ? ("Allergens: " + r.allergens.join(", ")) : "Allergen declaration checked", status: (r.criticalFindings || []).length ? "Needs review" : "Label ready", tone: (r.criticalFindings || []).length ? "warn" : "ok", prompt: "Show the label review for " + r.name })),
      actions: [{ label: "Generate missing labels", prompt: "Generate missing CFIA labels", icon: "wand-sparkles" }, { label: "Show blocked labels", prompt: "Show blocked labels", icon: "alert-triangle" }]
    },
    recipes: {
      match: /recipe|offering|product/,
      title: "Recipes & products", icon: "utensils-crossed",
      desc: "Search, inspect, assign, fix and prepare recipe records from Loraa.",
      metrics: [["Recipes", recipes.length, "info"], ["Review", pending.length, "warn"], ["Published", published.length, "ok"]],
      rows: recipes.slice(0, 10).map(r => ({ title: r.name, meta: [r.cuisine, r.category, r.calories ? r.calories + " kcal" : null].filter(Boolean).join(" · "), status: r.status || "Draft", tone: /publish|approved/.test(String(r.status || "")) ? "ok" : /change|block/.test(String(r.status || "")) ? "block" : "warn", prompt: "Tell me about " + r.name })),
      actions: [{ label: "Find duplicates", prompt: "Find duplicate recipes", icon: "copy" }, { label: "Review recipe queue", prompt: "Review recipes awaiting approval", icon: "clipboard-check" }]
    },
    ingredients: {
      match: /ingredient/,
      title: "Ingredient intelligence", icon: "leaf",
      desc: "Inspect canonical mapping, sources, nutrition and allergens in place.",
      metrics: [["Ingredients", ingredients.length, "info"], ["Need mapping", unmapped.length, unmapped.length ? "warn" : "ok"], ["Mapped", Math.max(0, ingredients.length - unmapped.length), "ok"]],
      rows: ingredients.slice(0, 10).map(g => ({ title: g.name, meta: g.mapped ? "Verified canonical mapping" : "Mapping or source review required", status: g.mapped ? "Mapped" : "Needs review", tone: g.mapped ? "ok" : "warn", prompt: "Inspect ingredient " + g.name })),
      actions: [{ label: "Review unmapped", prompt: "Show unmapped ingredients", icon: "search" }, { label: "Check allergen coverage", prompt: "Check ingredient allergen declarations", icon: "shield-check" }]
    },
    assignments: {
      match: /assign|board|task|team|user|workload/,
      title: "Tasks & team workload", icon: "kanban-square",
      desc: "Review ownership, bottlenecks and task capacity, then act with permission checks.",
      metrics: [["Boards", assignments.length, "info"], ["Open cards", assignments.reduce((s, b) => s + (b.total || 0), 0), "warn"], ["Review", assignments.reduce((s, b) => s + ((b.counts && (b.counts["In review"] || b.counts["in-review"])) || 0), 0), "warn"]],
      rows: assignments.slice(0, 8).map(b => ({ title: b.name, meta: (b.total || 0) + " cards · " + Object.keys(b.loads || {}).length + " contributors", status: "Monitored", tone: "info", prompt: "Show workload and bottlenecks for " + b.name })),
      actions: [{ label: "Who is overloaded?", prompt: "Who is overloaded?", icon: "users" }, { label: "Rebalance workload", prompt: "Rebalance the most overloaded contributor's workload", icon: "workflow" }]
    },
    calendar: {
      match: /calendar|publish/,
      title: "Calendar & publishing", icon: "calendar-days",
      desc: "Coordinate deadlines, dayparts and governed publishing from the agent.",
      metrics: [["Upcoming", calendar.length, "info"], ["Publishing", published.length, "ok"], ["Blocked", blocked.length, blocked.length ? "block" : "ok"]],
      rows: calendar.slice(0, 8).map(ev => ({ title: ev.title || ev.name || "Scheduled event", meta: String(ev.start || ev.date || ev.due || "Upcoming"), status: ev.status || "Scheduled", tone: "info", prompt: "Tell me what is required for " + (ev.title || ev.name || "this event") })),
      actions: [{ label: "Summarize pipeline", prompt: "Summarize the publishing pipeline", icon: "sparkle" }, { label: "Today's deadlines", prompt: "Show my calendar priorities", icon: "clock" }]
    },
    reports: {
      match: /report|analytic/,
      title: "Reports & executive insights", icon: "bar-chart-3",
      desc: "Generate evidence-backed operational reports and drill into the source records.",
      metrics: [["Compliance", "98%", "ok"], ["Reviewed", pending.length + published.length, "info"], ["Blocked", blocked.length, blocked.length ? "block" : "ok"], ["Automations", automations.filter(a => a.active !== false).length, "ok"]],
      rows: [
        { title: "Compliance health", meta: blocked.length + " critical blockers · " + pending.length + " items in review", status: "Live", tone: blocked.length ? "warn" : "ok", prompt: "Prepare the compliance executive report" },
        { title: "Publishing performance", meta: published.length + " items published in the monitored catalog", status: "Live", tone: "ok", prompt: "Prepare the publishing performance report" },
        { title: "Workload & automation", meta: assignments.length + " boards · " + automations.length + " rules", status: "Live", tone: "info", prompt: "Prepare the workload and automation report" }
      ],
      actions: [{ label: "Generate executive brief", prompt: "Give me today's daily NutriDMS brief", icon: "file-text" }, { label: "Run health scan", prompt: "Run a NutriDMS health scan", icon: "radar" }]
    },
    audit: {
      match: /audit|activity|monitor/,
      title: "Live monitor & audit trail", icon: "radar",
      desc: "See exactly what Loraa monitored, what changed and which actions were taken.",
      metrics: [["My activity", activity.length, "info"], ["Sources", (ctx.monitoredSources || []).length, "ok"], ["Automations", automations.length, "info"]],
      rows: activity.slice(0, 12).map(a => ({ title: a.detail || a.type, meta: a.type + " · " + new Date(a.at).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }), status: "Captured", tone: "info", prompt: "Explain the activity: " + (a.detail || a.type) })),
      actions: [{ label: "Refresh monitor", prompt: "Run a NutriDMS health scan", icon: "refresh-cw" }, { label: "Daily summary", prompt: "Give me today's daily NutriDMS brief", icon: "sun" }]
    },
    automations: {
      match: /workflow|automation/,
      title: "Automation control room", icon: "zap",
      desc: "Create, inspect, run and pause governed when/then rules without leaving Loraa.",
      metrics: [["Rules", automations.length, "info"], ["Active", automations.filter(a => a.active !== false).length, "ok"], ["Paused", automations.filter(a => a.active === false).length, "warn"]],
      rows: automations.slice(0, 10).map(a => ({ title: a.name || ("When " + (a.trigger || "a trigger runs")), meta: a.action || a.source || "Governed action", status: a.active === false ? "Paused" : "Active", tone: a.active === false ? "warn" : "ok", prompt: "Show active automations" })),
      actions: [{ label: "Manage rules", prompt: "Show active automations", icon: "settings" }, { label: "Create a rule", prompt: "Create an automation when ", icon: "plus" }]
    }
  };
  const cfg = Object.values(configs).find(c => c.match.test(id)) || configs.reports;
  return { ...cfg, id, sourceCount: (ctx.monitoredSources || []).length, generatedAt: Date.now() };
}

function LoraaWorkspace({ data, onAsk, onClose, onReview, reviewingId }) {
  if (!data) return null;
  return (
    <div className="loc-workspace">
      <div className="loc-workspace-head">
        <span className="loc-workspace-icon"><Icon name={data.icon || "layout-dashboard"} size={18} /></span>
        <div><span className="loc-workspace-kicker">In-agent workspace</span><h4>{data.title}</h4><p>{data.desc}</p></div>
        <button className="loc-workspace-close" title="Close workspace" onClick={onClose}><Icon name="x" size={15} /></button>
      </div>
      <div className="loc-agent-receipt">
        <span><Icon name="database" size={12} /> Live NutriDMS data</span>
        <span><Icon name="radar" size={12} /> {data.sourceCount || 0} monitored sources</span>
        <span><Icon name="shield-check" size={12} /> Role checked</span>
        <span><Icon name="clock" size={12} /> Updated now</span>
      </div>
      <div className="loc-workspace-metrics">
        {(data.metrics || []).map((m, i) => <div key={i} className={"loc-workspace-metric " + (m[2] || "info")}><strong>{m[1]}</strong><span>{m[0]}</span></div>)}
      </div>
      <div className="loc-workspace-list">
        {(data.rows || []).length ? data.rows.map((r, i) => (
          <div key={r.taskId || i} className="loc-workspace-row" role="group">
            <span className={"loc-notif-dot " + (r.tone || "info")} />
            <button className="loc-workspace-row-main" onClick={() => r.prompt && onAsk(r.prompt)}>
              <span><strong>{r.title}</strong><small>{r.meta || "Live NutriDMS record"}</small></span>
            </button>
            <em className={r.tone || "info"}>{r.status || "Open"}</em>
            {r.taskId && r.reviewable && onReview
              ? <span className="loc-workspace-review-actions">
                  <button className="approve" disabled={reviewingId === r.taskId} onClick={() => onReview(r, "accept")}><Icon name="check" size={12} />Approve</button>
                  <button disabled={reviewingId === r.taskId} onClick={() => onReview(r, "reject")}><Icon name="x" size={12} />Dismiss</button>
                </span>
              : <Icon name="chevron-right" size={15} />}
          </div>
        )) : <div className="loc-workspace-empty"><Icon name="check-circle-2" size={20} /><strong>No records need attention</strong><span>Loraa is still monitoring this workspace.</span></div>}
      </div>
      <div className="loc-workspace-actions">
        {(data.actions || []).map((a, i) => <button key={i} className={i === 0 ? "primary" : ""} onClick={() => onAsk(a.prompt)}><Icon name={a.icon || "sparkle"} size={14} />{a.label}</button>)}
      </div>
      <div className="loc-ai-boundary"><Icon name="lock-keyhole" size={12} /> You remain inside Loraa. Sensitive changes still require confirmation or approval.</div>
    </div>
  );
}

/* Enterprise structured-answer renderer. Renders the sections produced by
   LoraaAnswers inside the existing chat bubble — richer content, same UI shell. */
function LoraaRich({ data, nav, animate, onProgress, onAsk, onFix, onCmd, onServerAction }) {
  if (!data) return null;
  const [reveal, setReveal] = locState(animate ? 0 : 2);
  locEffect(() => { setReveal(animate ? 0 : 2); }, [data, animate]);
  locEffect(() => {
    if (animate && reveal === 0 && !data.intro) setReveal(1);
    if (animate && reveal === 1 && !data.summary) setReveal(2);
  }, [animate, reveal, data.intro, data.summary]);
  const act = (a) => {
    if (!a) return;
    if (a.kind === "server-action" && onServerAction) { onServerAction(a.payload || {}); return; }
    try {
      if (a.kind === "fix-findings" && onFix) { onFix(a.item || {}); return; }
      if (a.kind === "cmd-run" && onAsk && a.payload && a.payload.q) { onAsk(a.payload.q); return; }
      if (a.kind === "cmd-focus" && onAsk) { onAsk(""); return; }
      if (a.kind && a.kind.indexOf("cmd-") === 0 && onCmd) { onCmd(a); return; }
      if (a.kind === "export-compliance" && window.LoraaAnswers && window.LoraaAnswers.exportReport) { window.LoraaAnswers.exportReport(a.report || "compliance", data.role); return; }
      if (a.kind === "recipe" && a.arg && window.__openRecipe) { const list = (window.RECIPES || []); const r = list.find((x) => x.id === a.arg); if (r) { window.__openRecipe(r); return; } }
      if (a.kind === "ingredient" && a.arg && window.__openIngredient) { const list = (window.INGREDIENT_ITEMS || []); const g = list.find((x) => x.id === a.arg); if (g) { window.__openIngredient(g); return; } }
    } catch (e) {}
    if (a.to) nav(a.to);
  };
  return (
    <div className="loc-rich">
      {data.intro && (
        <p className="loc-rich-intro">{animate ? <LoraaText text={data.intro} animate={true} onProgress={onProgress} onDone={() => setReveal(1)} /> : <LoraaText text={data.intro} />}</p>
      )}
      {reveal >= 1 && <React.Fragment>
        <div className="loc-rich-head loc-rich-reveal"><Icon name="layout-dashboard" size={14} /> <span>{data.title}</span></div>
        <div className="loc-agent-receipt loc-rich-reveal">
          <span><Icon name="database" size={12} /> Live NutriDMS data</span>
          <span><Icon name="shield-check" size={12} /> Role checked</span>
          <span><Icon name="clipboard-check" size={12} /> Approval gated</span>
          <span><Icon name="history" size={12} /> Auditable</span>
        </div>
      </React.Fragment>}

      {reveal >= 1 && data.summary && (
        <div className="loc-rich-sec loc-rich-reveal">
          <div className="loc-rich-lbl">Executive summary</div>
          <p className="loc-rich-sum">{animate ? <LoraaText text={data.summary} animate={true} onProgress={onProgress} onDone={() => setReveal(2)} /> : data.summary}</p>
        </div>
      )}

      {reveal >= 2 && data.stats && data.stats.length > 0 && (
        <div className="loc-rich-stats">
          {data.stats.map((s, i) => (
            <div key={i} className={"loc-rich-stat" + (s.tone ? " " + s.tone : "")}>
              <strong>{s.value}</strong><span>{s.label}</span>
            </div>
          ))}
        </div>
      )}

      {reveal >= 2 && data.analysis && data.analysis.length > 0 && (
        <details className="loc-rich-details loc-rich-reveal">
          <summary><span><Icon name="list-tree" size={13} /> Detailed analysis</span><em>See more</em><Icon name="chevron-down" size={14} /></summary>
          <div className="loc-rich-details-body">{data.analysis.map((p, i) => <p key={i} className="loc-rich-p">{locFriendlyEvidence(p)}</p>)}</div>
        </details>
      )}

      {reveal >= 2 && data.records && data.records.length > 0 && (
        <div className="loc-rich-sec">
          <div className="loc-rich-lbl">{data.recordsTitle || "Supporting records"}</div>
          <div className="loc-rich-recs">
            {data.records.map((r, i) => (
              <div key={i} className={"loc-rec" + (r.badge && r.badge.tone === "block" ? " is-crit" : r.badge && r.badge.tone === "warn" ? " is-warn" : "")}>
                <div className="loc-rec-main">
                  <span className={"loc-rec-thumb " + ((r.badge && r.badge.tone) || "info")}>{r.thumb || (r.title || "?").trim().charAt(0).toUpperCase()}</span>
                  <div className="loc-rec-head">
                    <div className="loc-rec-top">
                      <span className="loc-rec-title">{r.title}</span>
                      {r.badge && <span className={"loc-rec-badge " + (r.badge.tone || "info")}>{r.badge.text}</span>}
                    </div>
                    {r.lead && <div className={"loc-rec-lead" + (r.leadTone ? " " + r.leadTone : "")}>{r.lead}</div>}
                    {r.fields && r.fields.length > 0 && (
                      <div className="loc-rec-meta">
                        {r.fields.map((f, j) => (
                          <span key={j} className="loc-rec-chip"><em>{f.k}</em><b className={f.tone ? "t-" + f.tone : ""}>{f.v}</b></span>
                        ))}
                      </div>
                    )}
                  </div>
                </div>
                {r.actions && r.actions.length > 0 && (
                  <div className="loc-rec-actions">
                    {r.actions.map((a, j) => <button key={j} className={"loc-rec-act" + (j === 0 ? " primary" : "")} onClick={() => act(a)}>{a.icon && <Icon name={a.icon} size={13} />}{a.label}</button>)}
                  </div>
                )}
              </div>
            ))}
          </div>
        </div>
      )}

      {reveal >= 2 && data.risks && data.risks.length > 0 && (
        <div className="loc-rich-sec">
          <div className="loc-rich-lbl">Risks</div>
          <div className="loc-rich-risks">
            {data.risks.map((r, i) => (
              <div key={i} className="loc-risk"><span className={"loc-notif-dot " + (r.tone || "info")} /><span>{r.text}</span></div>
            ))}
          </div>
        </div>
      )}

      {reveal >= 2 && data.recs && data.recs.length > 0 && (
        <div className="loc-rich-sec">
          <div className="loc-rich-lbl">Recommendations</div>
          <ul className="loc-rich-recs-list">{data.recs.map((r, i) => <li key={i}>{r}</li>)}</ul>
        </div>
      )}

      {reveal >= 2 && data.permissionNote && (
        <div className="loc-rich-perm"><Icon name="lock" size={12} /> <span>{data.permissionNote}</span></div>
      )}

      {reveal >= 2 && data.refs && (data.refs.internal.length > 0 || data.refs.external.length > 0) && (
        <details className="loc-rich-refs loc-rich-reveal">
          <summary><span><Icon name="database" size={13} /> Evidence &amp; sources</span><em>See more</em><Icon name="chevron-down" size={14} /></summary>
          <div className="loc-rich-refs-body">
            {data.refs.internal.length > 0 && <div className="loc-ref-row"><span className="loc-ref-lbl">Internal</span>{data.refs.internal.map((r, i) => <span key={i} className="loc-ref-chip">{locFriendlySource(r)}</span>)}</div>}
            {data.refs.external.length > 0 && <div className="loc-ref-row"><span className="loc-ref-lbl">External</span>{data.refs.external.map((r, i) => <span key={i} className="loc-ref-chip ext">{r}</span>)}</div>}
          </div>
        </details>
      )}

      {reveal >= 2 && data.confidence && (
        <div className="loc-rich-conf">
          <div className="loc-conf-top"><span>Confidence</span><strong>{data.confidence.pct}%</strong></div>
          <div className="loc-conf-bar"><span style={{ width: data.confidence.pct + "%" }} /></div>
          {data.confidence.basis && <div className="loc-conf-basis">Based on {data.confidence.basis.map(locFriendlyEvidence).join(" · ")} · updated {data.confidence.updated}</div>}
        </div>
      )}

      {reveal >= 2 && data.actions && data.actions.length > 0 && (
        <div className="loc-rich-cta">
          {data.actions.map((a, i) => <button key={i} className="loc-rich-cta-btn" onClick={() => act(a)}>{a.icon && <Icon name={a.icon} size={14} />}{a.label}</button>)}
        </div>
      )}

      {reveal >= 2 && data.followups && data.followups.length > 0 && onAsk && (
        <div className="loc-rich-follow">
          <div className="loc-follow-lbl"><Icon name="corner-down-right" size={12} /> Ask a follow-up</div>
          <div className="loc-follow-chips">
            {data.followups.map((f, i) => <button key={i} className="loc-follow-chip" onClick={() => onAsk(f)}>{f}</button>)}
          </div>
        </div>
      )}
    </div>
  );
}

/* Convert Django's governed response contract into the existing premium
   response card. The surrounding Loraa shell is deliberately unchanged. */
function locServerRich(data) {
  const value = data || {};
  const evidence = Array.isArray(value.evidence) ? value.evidence.map(locFriendlyEvidence).filter(Boolean) : [];
  const prompts = Array.isArray(value.suggestedPrompts) ? value.suggestedPrompts.filter(Boolean) : [];
  const actions = Array.isArray(value.actions) ? value.actions.filter((action) => action && action.type !== "none") : [];
  const titles = {
    clarification: "One thing I need",
    action_plan: "Recommended next step",
    automation: "Governed automation",
    navigate: "Open in Loraa",
    answer: "What I found",
  };
  return {
    serverResponse: true,
    provider: value.provider || "",
    model: value.model || "",
    requestId: value.requestId || "",
    providerExecuted: value.providerExecuted === true,
    latencyMs: typeof value.latencyMs === "number" ? value.latencyMs : null,
    qualitySignals: Array.isArray(value.qualitySignals) ? value.qualitySignals : [],
    intro: value.answer || "",
    title: titles[value.intent || value.type] || "Loraa response",
    analysis: evidence,
    permissionNote: actions.some((action) => action.requiresConfirmation !== false)
      ? "Any sensitive change will be permission-checked, approval-gated and audited before execution."
      : "",
    refs: {
      internal: [locFriendlySource(value.sourceOfTruth)],
      external: [],
    },
    confidence: typeof value.confidence === "number" ? {
      pct: value.confidence,
      basis: evidence.length ? evidence.slice(0, 3) : ["authenticated organization context"],
      updated: "just now",
    } : null,
    actions: actions.map((action) => ({
      kind: "server-action",
      payload: action,
      label: action.label || "Review action",
      icon: action.type === "schedule_task" ? "calendar-clock"
        : action.type === "notify_user" ? "bell"
        : action.type === "run_automation" ? "zap"
        : action.risk === "read_only" ? "arrow-up-right"
        : "clipboard-check",
    })),
    followups: prompts,
  };
}

function locHybridRich(original, server) {
  if (!original) return server;
  if (!server) return original;
  const uniqueText = (items) => Array.from(new Set((items || []).filter(Boolean)));
  const actions = [...(server.actions || []), ...(original.actions || [])].filter((item, index, all) => {
    const key = String((item && item.label) || (item && item.kind) || index).toLowerCase();
    return all.findIndex((candidate, candidateIndex) => String((candidate && candidate.label) || (candidate && candidate.kind) || candidateIndex).toLowerCase() === key) === index;
  });
  return {
    ...original,
    // Django/OpenAI owns the user-facing explanation. The deterministic
    // browser card remains supporting structure (records, stats and local
    // navigation) so a canned summary can never hide the agent's answer.
    serverResponse: true,
    title: server.title || original.title,
    intro: server.intro || original.intro,
    analysis: uniqueText([...(server.analysis || []), ...(original.analysis || [])]),
    permissionNote: server.permissionNote || original.permissionNote,
    refs: {
      internal: uniqueText([...(original.refs && original.refs.internal || []), ...(server.refs && server.refs.internal || [])]),
      external: uniqueText([...(original.refs && original.refs.external || []), ...(server.refs && server.refs.external || [])]),
    },
    actions,
    followups: uniqueText([...(original.followups || []), ...(server.followups || [])]).slice(0, 4),
    provider: server.provider || "",
    model: server.model || "",
    requestId: server.requestId || "",
    providerExecuted: server.providerExecuted === true,
    latencyMs: server.latencyMs,
    qualitySignals: server.qualitySignals || [],
    hybrid: true,
  };
}

function locServerMessages(conversation) {
  return ((conversation && conversation.msgs) || []).map((message) => {
    if (message.role !== "loraa" && message.role !== "assistant") {
      const metadata = message.metadata || {};
      return { role: "user", text: message.text || "", attachments: Array.isArray(metadata.attachments) ? metadata.attachments : [] };
    }
    const metadata = message.metadata || {};
    if (metadata.intent || metadata.evidence || metadata.actions || metadata.suggestedPrompts) {
      return {
        role: "loraa",
        rich: locServerRich({
          ...metadata,
          answer: message.text || "",
          intent: metadata.intent || "answer",
          sourceOfTruth: metadata.sourceOfTruth || "Live NutriDMS organization data",
        }),
      };
    }
    return { role: "loraa", text: message.text || "" };
  });
}

function ActionReview({ action, busy, error, onCancel, onConfirm }) {
  if (!action) return null;
  return (
    <div className="loc-action-review-wrap" role="dialog" aria-modal="true" aria-label="Review Loraa action">
      <div className="loc-action-review">
        <div className="loc-action-review-head">
          <span><Icon name="shield-check" size={18} /></span>
          <div><small>Governed action</small><h3>Review before anything changes</h3></div>
          <button onClick={onCancel} aria-label="Close action review"><Icon name="x" size={16} /></button>
        </div>
        <div className="loc-action-review-body">
          <h4>{action.label}</h4>
          <p>{action.reason || "Loraa prepared this action from your request."}</p>
          <div className="loc-action-review-grid">
            <span><small>Risk</small><strong>{action.risk || "Low"}</strong></span>
            <span><small>Permission</small><strong>{locFriendlyPermission(action.requiredPermission)}</strong></span>
            <span><small>Target</small><strong>{locFriendlyTarget(action)}</strong></span>
            <span><small>Execution</small><strong>Approval gated</strong></span>
          </div>
          {action.scheduleCron && <div className="loc-action-cron"><Icon name="calendar-clock" size={14} /> {locFriendlySchedule(action)}</div>}
          <div className="loc-action-boundary"><Icon name="lock-keyhole" size={13} /> This creates an auditable request. Sensitive changes are not applied from chat.</div>
          {error && <div className="loc-action-error">{error}</div>}
        </div>
        <div className="loc-action-review-foot">
          <button className="loc-fix-cancel" onClick={onCancel} disabled={busy}>Cancel</button>
          <button className="loc-fix-apply" onClick={onConfirm} disabled={busy}><Icon name="clipboard-check" size={14} /> {busy ? "Queuing…" : "Queue for approval"}</button>
        </div>
      </div>
    </div>
  );
}

/* Manual-entry field for a fix question: type your own value. If it isn't in the
   ingredient DB, Loraa falls back to the Master Library, then OpenAI — adjusting
   the confidence it reports based on which source resolved it. */
function FixManualEntry({ onSubmit }) {
  const [open, setOpen] = locState(false);
  const [val, setVal] = locState("");
  const [busy, setBusy] = locState(false);
  const [note, setNote] = locState(null);
  const resolveSource = async (q) => {
    // 1) exact/loose match in the published ingredient DB
    try {
      const db = (window.INGREDIENT_ITEMS || []);
      const hit = db.find((i) => (i.name || "").toLowerCase().includes(q.toLowerCase()) || (i.canonical || "").toLowerCase().includes(q.toLowerCase()));
      if (hit) return { source: "Ingredient library", confidence: 98 };
    } catch (e) {}
    // 2) Loraa Master Library search
    try {
      if (window.MasterIngredients && window.MasterIngredients.search) {
        const res = window.MasterIngredients.search(q) || [];
        if (res.length) return { source: "Loraa Master Library", confidence: 90 };
      }
    } catch (e) {}
    // 3) OpenAI / reasoning fallback
    try {
      if (window.LoraaAgent && window.LoraaAgent.ask) {
        const out = await window.LoraaAgent.ask("Give the standard nutrition/allergen profile for the ingredient: " + q, "manager", {});
        if (out && out.trim()) return { source: "OpenAI (unverified)", confidence: 62 };
      }
    } catch (e) {}
    return { source: "Manual entry (unverified)", confidence: 55 };
  };
  const submit = async () => {
    const q = val.trim(); if (!q) return;
    setBusy(true); setNote("Checking the ingredient library…");
    const resolved = await resolveSource(q);
    setBusy(false);
    onSubmit(q, resolved);
  };
  if (!open) return <button className="loc-fix-manual-toggle" onClick={() => setOpen(true)}><Icon name="pencil" size={13} /> Enter my own</button>;
  return (
    <div className="loc-fix-manual">
      <input className="loc-fix-manual-in" value={val} placeholder="Type your own value…" autoFocus
        onChange={(e) => setVal(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") submit(); }} />
      <button className="loc-fix-manual-go" disabled={busy || !val.trim()} onClick={submit}>{busy ? "Resolving…" : "Use this"}</button>
      {note && busy && <div className="loc-fix-manual-note"><span className="loc-think-spin" /> {note}</div>}
    </div>
  );
}

/* Interactive Fix-Findings flow — Loraa works WITH the user, step by step:
   1) presents the finding, 2) offers fix options, 3) asks a confirming question,
   4) builds a fix preview, 5) applies it on confirm. Rendered as a chat card. */
function LoraaFixFlow({ data, onPick, onApply, onOpen }) {
  const st = data.stage;
  return (
    <div className="loc-fix">
      <div className="loc-fix-head">
        <span className="loc-fix-ic"><Icon name="wrench" size={14} /></span>
        <div>
          <div className="loc-fix-title">Fixing · {data.item.title}</div>
          <div className="loc-fix-sub">{data.item.regime ? data.item.regime + " · " : ""}{data.item.findingText}</div>
        </div>
        <span className="loc-fix-step">Step {st === "options" ? "1" : st === "confirm" ? "2" : st === "preview" ? "3" : "✓"} / 3</span>
      </div>

      {data.note && <p className="loc-fix-note">{data.note}</p>}

      {st === "options" && (
        <div className="loc-fix-opts">
          {data.options.map((o, i) => (
            <button key={i} className="loc-fix-opt" onClick={() => onPick(data, o)}>
              <span className="loc-fix-opt-ic"><Icon name={o.icon || "check"} size={15} /></span>
              <span className="loc-fix-opt-body"><b>{o.label}</b><em>{o.desc}</em></span>
              <Icon name="chevron-right" size={15} className="loc-fix-opt-arr" />
            </button>
          ))}
        </div>
      )}

      {st === "confirm" && (
        <div className="loc-fix-confirm">
          <div className="loc-fix-q">{data.question}</div>
          <div className="loc-fix-choices">
            {data.choices.map((c, i) => (
              <button key={i} className="loc-fix-choice" onClick={() => onPick(data, data.option, c)}>{c}</button>
            ))}
          </div>
          <FixManualEntry onSubmit={(val, resolved) => onPick(data, data.option, val, resolved)} />
        </div>
      )}

      {st === "preview" && (
        <div className="loc-fix-preview">
          <div className="loc-fix-diff">
            <div className="loc-fix-diff-row"><span className="loc-fix-diff-lbl">Now</span><span className="loc-fix-was">{data.preview.before}</span></div>
            <div className="loc-fix-diff-row"><span className="loc-fix-diff-lbl">After fix</span><span className="loc-fix-now">{data.preview.after}</span></div>
          </div>
          {data.preview.confidence != null && (
            <div className={"loc-fix-conf" + (data.preview.confidence >= 85 ? " ok" : data.preview.confidence >= 65 ? " warn" : " low")}>
              <span className="loc-fix-conf-bar"><i style={{ width: data.preview.confidence + "%" }} /></span>
              <span className="loc-fix-conf-tx">Confidence {data.preview.confidence}%</span>
            </div>
          )}
          <div className="loc-fix-actions">
            <button className="loc-fix-apply" onClick={() => onApply(data)}><Icon name="check" size={14} /> Apply fix</button>
            <button className="loc-fix-cancel" onClick={() => onPick({ ...data, stage: "options" }, null)}>Choose another</button>
          </div>
        </div>
      )}

      {st === "applied" && (
        <div className="loc-fix-done">
          <div className="loc-fix-done-badge"><Icon name="check-circle-2" size={16} /> Fix applied</div>
          <p>{data.result}</p>
          <div className="loc-fix-actions">
            <button className="loc-fix-open" onClick={() => onOpen(data)}><Icon name="panel-top-open" size={13} /> Continue {data.item.regime ? "in the Loraa Label workspace" : "with the record in Loraa"}</button>
          </div>
        </div>
      )}
    </div>
  );
}

function LoraaAsk({ role, open, onClose, user }) {
  const [liveOps, setLiveOps] = locState({ items: [], summary: null, loading: true });
  const m = locMemo(() => locModel(role, liveOps), [role, liveOps]);
  const [chatOpen, setChatOpen] = locState(false);       // slide-out chat drawer
  const [history, setHistory] = locState([]);
  const [activeId, setActiveId] = locState(null);
  const [msgs, setMsgs] = locState([]);
  const msgsRef = locRef([]);
  locEffect(() => { msgsRef.current = msgs; }, [msgs]);
  const [typing, setTyping] = locState(false);
  const [input, setInput] = locState("");
  const [attachments, setAttachments] = locState([]);
  const [attachmentError, setAttachmentError] = locState("");
  const [historyQuery, setHistoryQuery] = locState("");
  const [collapsedGroups, setCollapsedGroups] = locState({});
  const historyLoadedRef = locRef("");
  const fileInputRef = locRef(null);
  const [slash, setSlash] = locState(-1);                // highlighted slash-command index, -1 = closed
  const [ctxOpen, setCtxOpen] = locState(true);
  const [sideOpen, setSideOpen] = locState(true);
  const [chatExpanded, setChatExpanded] = locState(false);   // widen chat drawer to the right
  const [thinkSteps, setThinkSteps] = locState(null);        // active reasoning steps while answering
  const bodyRef = locRef(null);
  const activeResponseRef = locRef(null);
  const followTypingRef = locRef(true);
  const inputRef = locRef(null);
  const [agent, setAgent] = locState(() => (window.LoraaAgent ? window.LoraaAgent.status() : null));
  const [provOpen, setProvOpen] = locState(false);     // provider settings popover
  const [learningOpen, setLearningOpen] = locState(false);
  const [serverConversationId, setServerConversationId] = locState(null);
  const [reviewAction, setReviewAction] = locState(null);
  const [actionBusy, setActionBusy] = locState(false);
  const [actionError, setActionError] = locState("");
  const [reviewingId, setReviewingId] = locState("");
  const registeredName = user && user.name ? String(user.name).trim() : "";
  const name = registeredName ? registeredName.split(/\s+/)[0] : locFirstName(role);
  const identity = (user && (user.id || user.email || user.name)) || role || "member";
  const historyKey = locHistoryKey(identity);
  const activeKey = historyKey + ":active";
  const hour = new Date().getHours();
  const greet = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";

  // Subscribe to the live agent (crawler + memory + provider)
  locEffect(() => {
    if (!window.LoraaAgent) return;
    window.LoraaAgent.start(role);
    const unsub = window.LoraaAgent.subscribe(setAgent);
    return () => {
      unsub();
      if (window.LoraaAgent && window.LoraaAgent.Crawler) window.LoraaAgent.Crawler.stop();
    };
  }, [role]);

  // The Operations Feed is always loaded from the authenticated Django tenant.
  // Polling plus product events keeps importance, ownership and review state
  // current without reintroducing browser demo records.
  locEffect(() => {
    if (!open || !window.NutriLoraa || !window.NutriLoraa.feed) return;
    let cancelled = false;
    const applySnapshot = (snapshot) => {
      if (!cancelled && snapshot && Array.isArray(snapshot.items)) {
        setLiveOps({ ...snapshot, loading: false, error: "" });
      }
      return snapshot;
    };
    const loadFeed = () => window.NutriLoraa.feed();
    const refresh = (force) => {
      const coordinator = window.NutriWorkspaceRefresh;
      const request = coordinator && coordinator.fetch
        ? coordinator.fetch("loraa:operations-feed", loadFeed, { scope: "org-user", ttlMs: coordinator.DEFAULT_TTL_MS, force: force === true })
        : loadFeed();
      return request.then(applySnapshot).catch((error) => {
      if (!cancelled) setLiveOps((current) => ({ ...current, loading: false, error: (error && error.message) || "Live operations are temporarily unavailable." }));
    });
    };
    const events = ["nutridms-submitted", "nutridms-automations", "nutridms-approval", "nutridms-published", "nutridms-task"];
    const coordinator = window.NutriWorkspaceRefresh;
    if (coordinator && coordinator.register) {
      coordinator.register("loraa:operations-feed", loadFeed, { scope: "org-user", onValue: applySnapshot, active: true });
      coordinator.activate("loraa:operations-feed", true);
    }
    const onEvent = () => refresh(false);
    refresh(false);
    events.forEach((eventName) => window.addEventListener(eventName, onEvent));
    return () => {
      cancelled = true;
      if (coordinator && coordinator.activate) coordinator.activate("loraa:operations-feed", false);
      events.forEach((eventName) => window.removeEventListener(eventName, onEvent));
    };
  }, [open, role]);

  // Load one user-scoped cache immediately. Django is fetched once per signed-in
  // identity, not every time the modal is hidden and shown.
  locEffect(() => {
    const localRows = locLoadHistory(historyKey);
    setHistory(localRows);
    let remembered = null;
    try { remembered = sessionStorage.getItem(activeKey); } catch (e) {}
    const active = localRows.find((row) => String(row.id) === String(remembered));
    setActiveId(active ? active.id : null);
    setServerConversationId(active ? active.serverId || null : null);
    setMsgs(active ? (active.msgs || []) : []);
    historyLoadedRef.current = "";
  }, [historyKey]);

  // Django owns conversation history. Merge it into the existing sidebar so
  // titles, timestamps, pins and follow-ups survive browser/device changes.
  locEffect(() => {
    if (!open || !window.LoraaAgent || !window.LoraaAgent.conversations) return;
    if (historyLoadedRef.current === historyKey) return;
    historyLoadedRef.current = historyKey;
    let cancelled = false;
    window.LoraaAgent.conversations(false).then((rows) => {
      if (cancelled || !Array.isArray(rows)) return;
      setHistory((localRows) => {
        const byServer = new Map((localRows || []).filter((row) => row.serverId).map((row) => [String(row.serverId), row]));
        const remoteRows = rows.map((row) => {
          const existing = byServer.get(String(row.id));
          const existingPrompt = existing && existing.msgs && existing.msgs.find((message) => message.role === "user" && message.text);
          const remoteTitle = String(row.title || "").trim();
          const usefulTitle = remoteTitle && remoteTitle.toLowerCase() !== "conversation"
            ? remoteTitle
            : (existing && existing.title && existing.title !== "Conversation" ? existing.title : (existingPrompt && existingPrompt.text) || row.summary || "New conversation");
          return {
            id: (existing && existing.id) || ("server-" + row.id),
            serverId: row.id,
            title: String(usefulTitle).slice(0, 72),
            summary: row.summary || "",
            pinned: !!row.pinned,
            at: row.at || Date.now(),
            msgs: (existing && existing.msgs) || [],
          };
        });
        const remoteIds = new Set(remoteRows.map((row) => String(row.serverId)));
        const localOnly = (localRows || []).filter((row) => !row.serverId || !remoteIds.has(String(row.serverId)));
        const merged = [...remoteRows, ...localOnly].sort((a, b) => Number(!!b.pinned) - Number(!!a.pinned) || (b.at || 0) - (a.at || 0));
        locSaveHistory(merged, historyKey);
        return merged;
      });
    }).catch(() => { if (!cancelled) historyLoadedRef.current = ""; });
    return () => { cancelled = true; };
  }, [open, role, historyKey]);

  locEffect(() => {
    try {
      if (activeId) sessionStorage.setItem(activeKey, String(activeId));
      else sessionStorage.removeItem(activeKey);
    } catch (e) {}
  }, [activeId, activeKey]);

  const learning = agent && agent.learning ? agent.learning : null;
  const fam = learning ? learning.pct / 100 : (agent && agent.profile ? agent.profile.familiarity : 0);
  const learned = agent && agent.topModules ? agent.topModules.slice(0, 3).map(x => x.module) : [];
  // Proactive learning suggestion — Loraa offers to automate a clear pattern.
  const [learnSug, setLearnSug] = locState(null);
  const [learnDismissed, setLearnDismissed] = locState(false);
  locEffect(() => {
    if (!open) return;
    try { if (window.LoraaCommands && window.LoraaCommands.learnSuggestion) setLearnSug(window.LoraaCommands.learnSuggestion()); } catch (e) {}
  }, [open, msgs]);
  const acceptLearn = () => {
    try { if (learnSug && window.LoraaCommands) { window.LoraaCommands.learnAddRule(learnSug.cat, learnSug.initials, learnSug.name); } } catch (e) {}
    setLearnDismissed(true); setLearnSug(null);
  };
  const MOD_LABEL = { compliance: "Compliance", labels: "Label Studio", nutrition: "Nutrition", ingredients: "Ingredients", publishing: "Publishing", assignments: "Team boards", reports: "Reports", recipes: "Recipes" };
  // Adaptive greeting line that reflects what Loraa has learned about this user
  const liveWorkload = liveOps && liveOps.summary ? Number(liveOps.summary.workload || 0) : null;
  const liveSummary = liveOps && liveOps.summary ? liveOps.summary : {};
  const liveSources = locMemo(() => {
    const counts = {};
    ((liveOps && liveOps.items) || []).forEach((item) => {
      const key = item.source || "django";
      counts[key] = (counts[key] || 0) + 1;
    });
    return Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 8);
  }, [liveOps]);
  const introLine = liveOps.loading
    ? "I'm checking your live NutriDMS operation now."
    : liveOps.error
      ? "I couldn't refresh the live operation just now; your saved conversations are still available."
      : liveWorkload > 0
        ? ("I found " + liveWorkload + " current work item" + (liveWorkload === 1 ? "" : "s") + " and ranked the most important first.")
        : "Nothing in the connected operation currently needs attention.";

  // Recommendations reordered by what this user engages with most (per-user learning)
  const recs = locMemo(() => {
    try { return window.LoraaAgent ? window.LoraaAgent.personalize(m.recommendations, role) : m.recommendations; }
    catch (e) { return m.recommendations; }
  }, [m, role, agent]);
  const opsPrompts = [
    "Give me today's daily NutriDMS brief",
    "Fix recipes failing our sodium policy",
    "Show active automations",
    "Generate missing CFIA labels",
    "Balance the team's current workload",
    "Run a NutriDMS health scan",
  ];

  const appendAgent = (payload) => {
    const cid = activeId || ("c-" + Date.now());
    if (!activeId) setActiveId(cid);
    const next = [...msgsRef.current, payload];
    msgsRef.current = next; setMsgs(next); persist(cid, next);
    setChatOpen(true);
    setTimeout(scrollThread, 30);
  };
  const scrollToLatest = (behavior) => {
    // Restored sessions can grow after React paints images and rich cards. A
    // few short, local scroll passes keep the viewport at the latest message
    // without refetching the conversation or spending another AI token.
    [0, 60, 180].forEach((delay) => window.setTimeout(() => {
      window.requestAnimationFrame(() => {
        const body = bodyRef.current;
        if (!body) return;
        followTypingRef.current = true;
        body.scrollTo({ top: body.scrollHeight, behavior: delay ? "auto" : (behavior || "auto") });
      });
    }, delay));
  };
  const nav = (to) => {
    if (!to) return;
    try { if (window.LoraaAgent) window.LoraaAgent.recordNav(to, role); } catch (e) {}
    appendAgent({ role: "loraa", workspace: locWorkspaceData(to, role, liveOps), _new: true });
  };

  const slashMatches = locMemo(() => {
    if (!input.startsWith("/")) return [];
    const q = input.slice(1).toLowerCase();
    return LOC_COMMANDS.filter(c => c.cmd.slice(1).startsWith(q) || c.label.toLowerCase().includes(q)).slice(0, 7);
  }, [input]);

  const entityMatches = locMemo(() => {
    const q = input.trim().toLowerCase();
    if (input.startsWith("/") || q.length < 2) return [];
    const out = [];
    try {
      (typeof RECIPES !== "undefined" ? RECIPES : []).forEach(r => {
        if (out.length < 4 && r.name && r.name.toLowerCase().includes(q)) out.push({ icon: "utensils-crossed", label: r.name, kind: "Recipe" });
      });
      (typeof INGREDIENTS !== "undefined" ? INGREDIENTS : []).forEach(i => {
        if (out.length < 5 && i.name && i.name.toLowerCase().includes(q)) out.push({ icon: "leaf", label: i.name, kind: "Ingredient" });
      });
    } catch (e) {}
    return out.slice(0, 5);
  }, [input]);

  const persist = (id, next, backendId) => {
    setHistory((h) => {
      let list = h.slice();
      if (!id) return list;
      const cleanMessages = locPersistableMessages(next);
      const existing = list.find(x => x.id === id);
      const serverId = backendId || serverConversationId || (existing && existing.serverId) || null;
      const firstPrompt = cleanMessages.find((message) => message.role === "user" && message.text);
      const title = firstPrompt && firstPrompt.text ? firstPrompt.text.replace(/\s+/g, " ").slice(0, 72) : "New conversation";
      if (existing) list = list.map(x => x.id === id ? { ...x, title: x.title && x.title !== "Conversation" ? x.title : title, msgs: cleanMessages, serverId, at: Date.now() } : x);
      else list = [{ id, serverId, title, when: "Now", at: Date.now(), msgs: cleanMessages }, ...list];
      locSaveHistory(list, historyKey); return list;
    });
  };

  const closeCenter = () => {
    const stable = locPersistableMessages(msgsRef.current);
    msgsRef.current = stable;
    setMsgs(stable);
    if (activeId) persist(activeId, stable, serverConversationId);
    setAttachmentError("");
    onClose();
  };
  const closeChat = () => {
    const stable = locPersistableMessages(msgsRef.current);
    msgsRef.current = stable;
    setMsgs(stable);
    if (activeId) persist(activeId, stable, serverConversationId);
    setChatOpen(false);
    setChatExpanded(false);
  };

  const readAttachment = (file) => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve({
      id: "att-" + Date.now() + "-" + Math.random().toString(36).slice(2, 8),
      name: file.name,
      type: file.type || "application/octet-stream",
      size: file.size,
      kind: String(file.type || "").startsWith("image/") ? "image" : "file",
      data: reader.result,
      previewUrl: String(file.type || "").startsWith("image/") ? reader.result : "",
    });
    reader.onerror = () => reject(new Error("Could not read " + file.name + "."));
    reader.readAsDataURL(file);
  });

  const addAttachments = async (fileList) => {
    const incoming = Array.from(fileList || []);
    if (!incoming.length) return;
    const allowed = /^(image\/(png|jpeg|webp|gif)|application\/pdf|text\/(plain|csv|markdown)|application\/(json|msword|vnd\.openxmlformats-officedocument\.(wordprocessingml\.document|presentationml\.presentation|spreadsheetml\.sheet)|vnd\.ms-(excel|powerpoint)))$/i;
    const nextFiles = [];
    let total = attachments.reduce((sum, item) => sum + Number(item.size || 0), 0);
    for (const file of incoming) {
      if (attachments.length + nextFiles.length >= 3) { setAttachmentError("Attach up to 3 files per message."); break; }
      if (!allowed.test(file.type || "")) { setAttachmentError(file.name + " is not a supported image, PDF, document or spreadsheet."); continue; }
      if (file.size > 6 * 1024 * 1024) { setAttachmentError(file.name + " is larger than 6 MB."); continue; }
      if (total + file.size > 12 * 1024 * 1024) { setAttachmentError("Attachments can total up to 12 MB per message."); break; }
      total += file.size;
      nextFiles.push(await readAttachment(file));
    }
    if (nextFiles.length) {
      setAttachments((current) => [...current, ...nextFiles]);
      setAttachmentError("");
    }
    if (fileInputRef.current) fileInputRef.current.value = "";
  };

  const removeAttachment = (id) => setAttachments((current) => current.filter((item) => item.id !== id));

  // ── Interactive Fix-Findings flow: Loraa works WITH the user ──
  const pushFix = (fix) => {
    appendAgent({ role: "loraa", fix });
  };
  const startFix = (item) => {
    setChatOpen(true);
    let options = [];
    try { options = window.LoraaAnswers ? window.LoraaAnswers.fixOptionsFor(item) : []; } catch (e) {}
    const cid = activeId || ("c-" + Date.now());
    if (!activeId) setActiveId(cid);
    const next = [...msgsRef.current, { role: "user", text: "Fix the findings on " + item.title }];
    msgsRef.current = next; setMsgs(next); persist(cid, next);
    setTimeout(() => pushFix({ stage: "options", item, options, note: "Here's what I found on " + item.title + ". How would you like to clear it? Pick an approach and I'll build the fix." }), 400);
  };
  const fixPick = (data, option, choice, resolved) => {
    if (data.stage === "options" && option) {
      const pv = window.LoraaAnswers.fixPreviewFor(data.item, option, choice);
      if (pv.needQuestion) { pushFix({ stage: "confirm", item: data.item, option, question: pv.needQuestion, choices: pv.choices }); return; }
      pushFix({ stage: "preview", item: data.item, option, preview: pv, note: "Here's the fix I'll apply. Review it, then Apply." }); return;
    }
    if (data.stage === "confirm" && option) {
      const pv = window.LoraaAnswers.fixPreviewFor(data.item, option, choice);
      if (resolved) { pv.after = (pv.after || ("Using " + choice)) + " · source: " + resolved.source; pv.confidence = resolved.confidence; }
      const noteTx = resolved
        ? ("Got it — \u201c" + choice + "\u201d. I resolved it via " + resolved.source + " (confidence " + resolved.confidence + "%). Here's the fix.")
        : ("Got it — " + choice + ". Here's the fix.");
      pushFix({ stage: "preview", item: data.item, option, choice, preview: pv, note: noteTx }); return;
    }
    if (!option) {
      const options = window.LoraaAnswers.fixOptionsFor(data.item);
      pushFix({ stage: "options", item: data.item, options, note: "No problem — pick another approach." });
    }
  };
  const fixApply = (data) => {
    try { window.LoraaAnswers.markFixed(data.item.title, (data.option && data.option.label) || "resolved"); } catch (e) {}
    const res = (data.option && data.option.id === "assign")
      ? (data.preview.after + ". I'll track it to completion.")
      : (data.preview.after + ". I re-ran validation and the finding cleared — " + data.item.title + " is no longer blocked.");
    pushFix({ stage: "applied", item: data.item, option: data.option, result: res });
  };
  const fixOpen = (data) => { const to = (data.item && data.item.openTo) || "label-studio"; nav(to); };

  // ── Command follow-through: execute an action button from a command card ──
  const runCmd = (action) => {
    try {
      const res = window.LoraaCommands && window.LoraaCommands.execute(action, role);
      if (res && res.text) {
        try { if (window.LoraaAgent) window.LoraaAgent.recordAction(action.kind, role, res.ok === false ? "failed" : "completed"); } catch (e) {}
        appendAgent({ role: "loraa", text: res.text, conf: res.ok === false ? 74 : 98, _new: true });
        if (/automation/.test(action.kind || "")) setTimeout(() => appendAgent({ role: "loraa", workspace: locWorkspaceData("automations", role, liveOps) }), 260);
      }
    } catch (e) {}
  };

  const chooseServerAction = (action) => {
    if (!action) return;
    if (action.requiresConfirmation === false && action.risk === "read_only" && action.target) {
      nav(action.target);
      return;
    }
    setActionError("");
    setReviewAction(action);
  };

  const confirmServerAction = async () => {
    const action = reviewAction;
    if (!action) return;
    const rawType = String(action.type || "").toLowerCase();
    const type = locGovernedActionType(action);
    if (type === "schedule_task" && !action.scheduleCron) {
      setActionError("This schedule needs a clear time or recurrence before it can be queued.");
      return;
    }
    setActionBusy(true);
    setActionError("");
    try {
      const result = await window.LoraaAgent.prepareAction({
        type,
        label: action.label,
        subjectType: action.subjectType || "",
        subjectId: action.target || "",
        targetUserId: locGovernedTargetUserId(action, type),
        scheduleCron: action.scheduleCron || "",
        requiredPermission: action.requiredPermission || "",
        payload: {
          canonicalCommand: action.canonicalCommand,
          reason: action.reason,
          message: action.reason,
          targetLabel: locFriendlyTarget(action),
          scheduleLabel: action.scheduleCron ? locFriendlySchedule(action) : "",
        },
        channels: /email|push|notif/.test(rawType) ? ["in_app", "email"] : ["in_app"],
      });
      let selfApproved = false;
      if (result && result.canSelfApprove && result.taskId && window.NutriLoraa && window.NutriLoraa.review) {
        await window.NutriLoraa.review(result.taskId, "accept");
        selfApproved = true;
      }
      setReviewAction(null);
      if (window.NutriLoraa && window.NutriLoraa.feed) {
        try {
          const snapshot = await window.NutriLoraa.feed();
          if (snapshot && Array.isArray(snapshot.items)) setLiveOps({ ...snapshot, loading: false, error: "" });
        } catch (e) {}
      }
      appendAgent({
        role: "loraa",
        rich: locServerRich({
          type: "action_plan",
          answer: selfApproved
            ? (type === "schedule_task"
              ? "Your reminder is scheduled. Loraa will deliver it in NutriDMS at the confirmed time."
              : "Your NutriDMS notification is ready and has been sent.")
            : "Your request is now in the NutriDMS review queue. Nothing sensitive has changed yet.",
          confidence: 100,
          evidence: [selfApproved
            ? "You confirmed this low-risk personal action, so NutriDMS recorded and activated it immediately."
            : "The request is visible in Approvals & review, where an authorized reviewer can approve or dismiss it."],
          actions: [],
          suggestedPrompts: ["Show my pending approvals", "Explain what happens after approval"],
          sourceOfTruth: "NutriDMS governed workflow",
          audited: true,
        }),
        _new: true,
      });
    } catch (error) {
      setActionError((error && error.message) || "I couldn’t queue this action. Check your permission and try again.");
    } finally {
      setActionBusy(false);
    }
  };

  const reviewQueueItem = async (row, decision) => {
    if (!row || !row.taskId || !window.NutriLoraa || !window.NutriLoraa.review) return;
    setReviewingId(row.taskId);
    try {
      await window.NutriLoraa.review(row.taskId, decision);
      const snapshot = await window.NutriLoraa.feed();
      if (snapshot && Array.isArray(snapshot.items)) {
        setLiveOps({ ...snapshot, loading: false, error: "" });
        const updatedWorkspace = locWorkspaceData("review-queue", role, snapshot);
        const next = msgsRef.current.map((message) => message.workspace && /review-queue|approval/.test(String(message.workspace.id || ""))
          ? { ...message, workspace: updatedWorkspace }
          : message);
        msgsRef.current = next;
        setMsgs(next);
        if (activeId) persist(activeId, next);
      }
      appendAgent({
        role: "loraa",
        text: decision === "accept"
          ? "Approved. NutriDMS recorded the decision and the review queue is now up to date."
          : "Dismissed. NutriDMS recorded the decision and removed it from the active review queue.",
        conf: 100,
        _new: true,
      });
    } catch (error) {
      appendAgent({
        role: "loraa",
        text: (error && error.message) || "I couldn't update that approval. Your current NutriDMS role may not include review permission.",
        conf: 0,
        _new: true,
      });
    } finally {
      setReviewingId("");
    }
  };

  const ask = async (q) => {
    const selectedAttachments = attachments.slice();
    const question = String(q || "").trim() || (selectedAttachments.length ? "Please review the attached files." : "");
    if (!question) return;
    setChatOpen(true);
    const userMsg = {
      role: "user",
      text: question,
      attachments: selectedAttachments.map((item) => ({ ...item })),
    };
    let id = activeId || ("c-" + Date.now());
    if (!activeId) setActiveId(id);
    const afterUser = [...msgsRef.current, userMsg];
    msgsRef.current = afterUser;
    setMsgs(afterUser); persist(id, afterUser); setInput(""); setAttachments([]); setAttachmentError(""); setSlash(-1);
    // Show Loraa "thinking" through real steps while it works.
    const steps = locThinkSteps(question);
    setThinkSteps(steps); setTyping(true);
    const thinkStart = Date.now();
    const conversational = locConversationalIntent(question);

    // Restore the original Loraa explanation engine from the approved folder.
    // It reads the current NutriDMS workspace and produces the same structured,
    // permission-aware answer cards the existing UI was designed to render.
    let originalExplanation = null;
    try {
      if (!conversational && window.LoraaAnswers) originalExplanation = window.LoraaAnswers.answer(question, role);
    } catch (e) {}

    // Keep the governed Django/OpenAI request connected for server history,
    // tenant grounding and Hybrid actions. If that service is unavailable or
    // rejects the request, the original explanation remains usable.
    let serverResult = null;
    let serverError = null;
    try {
      if (window.LoraaAgent && window.LoraaAgent.askDetailed) {
        serverResult = await window.LoraaAgent.askDetailed(question, role, { conversationId: serverConversationId, attachments: selectedAttachments.map(({ id, previewUrl, ...item }) => item) });
        if (serverResult && serverResult.conversationId) setServerConversationId(serverResult.conversationId);
      }
    } catch (error) {
      serverError = error;
      serverResult = null;
    }

    const serverRich = serverResult && serverResult.answer ? locServerRich(serverResult) : null;
    const serverHasGovernedAction = !!(
      serverResult && Array.isArray(serverResult.actions) &&
      serverResult.actions.some((action) => action && action.type && action.type !== "none")
    );
    // Governed actions must render from the authoritative Django/OpenAI
    // response alone. Merging a cached local workspace card here could show an
    // unrelated ingredient, recipe or dashboard beside a valid popup/action.
    let rich = conversational ? null : (serverHasGovernedAction
      ? serverRich
      : locHybridRich(originalExplanation, serverRich));
    let aiConfidence = serverResult && typeof serverResult.confidence === "number" ? serverResult.confidence : null;
    let originalReply = conversational && serverResult && serverResult.answer ? serverResult.answer : null;
    if (!rich && serverError) {
      try {
        const fallback = window.LoraaAnswers && window.LoraaAnswers.clarify(question, role);
        if (fallback && fallback.kind === "text") originalReply = fallback.text;
        else if (fallback) rich = fallback;
      } catch (e) {}
    }
    const providerMessage = String(serverError && serverError.message || "")
      .replace(/sk-[A-Za-z0-9_-]+/g, "[redacted]")
      .trim();
    const errorStatus = serverError ? Number(serverError.status) : 0;
    const reply = rich ? null : (originalReply || (
      errorStatus === 401
        ? "Your NutriDMS session has expired, " + name + ". Please sign in again so I can securely access your organization."
        : errorStatus === 403
          ? "You're signed in, " + name + ", but your current role does not have access to Loraa. Please ask an organization administrator to enable Loraa for your role."
          : errorStatus === 503 && providerMessage
            ? "I reached NutriDMS, " + name + ", but OpenAI could not complete the response. " + providerMessage
            : providerMessage
              ? "I reached NutriDMS, " + name + ", but the request could not be completed. " + providerMessage
              : "I couldn't complete that request just now, " + name + ". I haven't guessed or used local demo data. Please try again."
    ));

    // Let the thinking animation breathe for at least a moment.
    const minThink = Math.min(steps.length * 850, 3200);
    const wait = Math.max(0, minThink - (Date.now() - thinkStart));
    setTimeout(() => {
      setTyping(false); setThinkSteps(null);
      let conf = aiConfidence;
      if (!rich && conf == null) conf = 0;
      const msg = rich ? { role: "loraa", rich: rich, _new: true } : { role: "loraa", text: reply, conf, _new: true };
      const next = [...afterUser, msg];
      setMsgs(next);
      // Persist without the transient _new flag so reloads don't re-animate.
      persist(id, next.map(({ _new, ...r }) => r), serverResult && serverResult.conversationId);
    }, wait);
  };

  const openWorkspace = async (w) => {
    setActiveId(w.id);
    const h = history.find(x => x.id === w.id);
    setServerConversationId((h && h.serverId) || null);
    if (h && Array.isArray(h.msgs) && h.msgs.length) {
      const cached = locPersistableMessages(h.msgs);
      msgsRef.current = cached;
      setMsgs(cached);
      setChatOpen(true);
      scrollToLatest();
      return;
    }
    if (h && h.serverId && window.LoraaAgent && window.LoraaAgent.conversation) {
      try {
        const conversation = await window.LoraaAgent.conversation(h.serverId);
        const loaded = locServerMessages(conversation);
        setMsgs(loaded);
        persist(h.id, loaded, h.serverId);
        setChatOpen(true);
        scrollToLatest();
        return;
      } catch (error) {}
    }
    if (h) { setMsgs(h.msgs || []); setChatOpen(true); scrollToLatest(); } else { setMsgs([]); ask(w.title); }
  };

  // Group real conversation history by date for the left panel.
  const histGroups = locMemo(() => {
    const now = new Date();
    const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
    const dayMs = 86400e3;
    const groups = { Pinned: [], Today: [], Yesterday: [], "Last week": [], Earlier: [] };
    const query = historyQuery.trim().toLowerCase();
    (history || []).filter((h) => !query || String((h.title || "") + " " + (h.summary || "")).toLowerCase().includes(query)).forEach((h) => {
      if (h.pinned) { groups.Pinned.push(h); return; }
      const at = h.at || 0;
      let g = "Earlier";
      if (at >= startOfDay) g = "Today";
      else if (at >= startOfDay - dayMs) g = "Yesterday";
      else if (at >= startOfDay - 7 * dayMs) g = "Last week";
      groups[g].push(h);
    });
    return ["Pinned", "Today", "Yesterday", "Last week", "Earlier"].map((k) => ({ group: k, items: groups[k] })).filter((x) => x.items.length);
  }, [history, historyQuery]);
  const histIcon = (h) => {
    const t = ((h.title || "") + " " + ((h.msgs && h.msgs[0] && h.msgs[0].text) || "")).toLowerCase();
    if (/complian|risk|flag/.test(t)) return { icon: "shield-check", tone: "block" };
    if (/label|cfia|fda|fop|nafdac/.test(t)) return { icon: "tag", tone: "info" };
    if (/ingredient|nutrition|rice|salmon/.test(t)) return { icon: "leaf", tone: "ok" };
    if (/board|assign|workload|team/.test(t)) return { icon: "kanban-square", tone: "info" };
    if (/publish|pipeline|queue/.test(t)) return { icon: "calendar-days", tone: "run" };
    if (/recipe|dish/.test(t)) return { icon: "utensils-crossed", tone: "warn" };
    return { icon: "message-square", tone: "info" };
  };
  const histTime = (h) => {
    if (!h.at) return h.when || "";
    const d = new Date(h.at); const now = new Date();
    if (d.toDateString() === now.toDateString()) return d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
    return d.toLocaleDateString([], { weekday: "short" });
  };
  const toggleHistoryGroup = (group) => setCollapsedGroups((current) => ({ ...current, [group]: !current[group] }));
  const togglePin = async (workspace, event) => {
    event.preventDefault();
    event.stopPropagation();
    const nextPinned = !workspace.pinned;
    setHistory((current) => {
      const next = current.map((row) => row.id === workspace.id ? { ...row, pinned: nextPinned } : row);
      locSaveHistory(next, historyKey);
      return next;
    });
    if (workspace.serverId && window.LoraaAgent && window.LoraaAgent.updateConversation) {
      try { await window.LoraaAgent.updateConversation(workspace.serverId, { pinned: nextPinned }); } catch (e) {}
    }
  };
  const newConversation = () => {
    setActiveId(null); setServerConversationId(null); setMsgs([]); msgsRef.current = [];
    setInput(""); setAttachments([]); setAttachmentError(""); setSlash(-1); setReviewAction(null); setChatOpen(true);
  };

  const onKey = (e) => {
    if (e.key === "Enter" && e.shiftKey) return;
    if (slashMatches.length && slash >= 0) {
      if (e.key === "ArrowDown") { e.preventDefault(); setSlash(i => Math.min(i + 1, slashMatches.length - 1)); return; }
      if (e.key === "ArrowUp") { e.preventDefault(); setSlash(i => Math.max(i - 1, 0)); return; }
      if (e.key === "Enter") { e.preventDefault(); const c = slashMatches[slash]; if (c) c.prompt ? ask(c.prompt) : nav(c.to); return; }
      if (e.key === "Escape") { setSlash(-1); return; }
    }
    if (e.key === "Enter") { e.preventDefault(); ask(input); }
  };

  locEffect(() => {
    const body = bodyRef.current;
    const last = msgs[msgs.length - 1];
    if (!body || !last) return;
    followTypingRef.current = true;
    if (last.role === "loraa" && last._new && activeResponseRef.current) {
      const bodyBox = body.getBoundingClientRect();
      const responseBox = activeResponseRef.current.getBoundingClientRect();
      const top = body.scrollTop + responseBox.top - bodyBox.top - 12;
      body.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
    } else if (last.role === "user") {
      body.scrollTo({ top: body.scrollHeight, behavior: "smooth" });
    }
  }, [msgs.length]);
  locEffect(() => {
    if (open && chatOpen && msgsRef.current.length) scrollToLatest();
  }, [open, chatOpen, activeId]);
  const scrollThread = () => {
    const body = bodyRef.current;
    const response = activeResponseRef.current;
    if (!body || !response || !followTypingRef.current) return;
    const bodyBox = body.getBoundingClientRect();
    const responseBox = response.getBoundingClientRect();
    const overflow = responseBox.bottom - (bodyBox.bottom - 28);
    if (overflow > 0) body.scrollTop += Math.min(overflow, 36);
  };
  const pauseFollow = () => { followTypingRef.current = false; };
  locEffect(() => { const h = (e) => { if (e.key === "Escape" && open && slash < 0) { if (chatOpen) closeChat(); else closeCenter(); } }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [open, slash, chatOpen, activeId, serverConversationId]);
  locEffect(() => { if (chatOpen) { const t = setTimeout(() => { if (inputRef.current) inputRef.current.focus(); }, 360); return () => clearTimeout(t); } }, [chatOpen]);
  locEffect(() => { if (input.startsWith("/")) setSlash(s => s < 0 ? 0 : s); else setSlash(-1); }, [input]);
  locEffect(() => {
    const element = inputRef.current;
    if (!element) return;
    element.style.height = "auto";
    element.style.height = Math.min(element.scrollHeight, 132) + "px";
  }, [input]);

  if (!open) return null;
  const logo = (cls) => <span className={"loc-orb " + (cls || "")}><img src="assets/loraa-logo.png" alt="" onError={(e) => { e.currentTarget.style.display = "none"; }} /></span>;

  return (
    <React.Fragment>
      <div className="loraa-modal-scrim" onClick={closeCenter} />
      <div className={"loc-modal" + (sideOpen ? "" : " side-hidden") + (ctxOpen ? "" : " ctx-hidden") + (chatOpen ? " chat-open" : "")} role="dialog" aria-label="Loraa AI Operations Center">

        {/* ── Left: workspaces ── */}
        <aside className="loc-side">
          <div className="loc-side-brand">{logo("sm")}<span className="loc-side-name">Loraa</span>
            <span className="loc-side-live"><i /> Working</span></div>
          <button className="loc-newconv" onClick={newConversation}><Icon name="plus" size={15} /> New conversation</button>
          <label className="loc-history-search">
            <Icon name="search" size={14} />
            <input value={historyQuery} onChange={(event) => setHistoryQuery(event.target.value)} placeholder="Search conversations" />
          </label>
          <div className="loc-side-scroll">
            {histGroups.length === 0 && (
              <div className="loc-ws-empty"><Icon name="message-square" size={20} /><p>No conversations yet.</p><span>Ask Loraa anything, or hit New conversation to start.</span></div>
            )}
            {histGroups.map((grp) => (
              <div className="loc-ws-group" key={grp.group}>
                <button className="loc-ws-h" onClick={() => toggleHistoryGroup(grp.group)} aria-expanded={!collapsedGroups[grp.group]}>
                  <span>{grp.group}</span><em>{grp.items.length}</em><Icon name={collapsedGroups[grp.group] ? "chevron-right" : "chevron-down"} size={12} />
                </button>
                {!collapsedGroups[grp.group] && grp.items.map((w) => {
                  const ic = histIcon(w);
                  return (
                    <div key={w.id} role="button" tabIndex={0} className={"loc-ws" + (activeId === w.id ? " on" : "")}
                      onClick={() => openWorkspace(w)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openWorkspace(w); } }}>
                      <span className={"loc-ws-ic " + ic.tone}><Icon name={ic.icon} size={15} /></span>
                      <span className="loc-ws-tx"><strong>{w.title || "New conversation"}</strong><small>{w.summary || histTime(w)}</small></span>
                      <button className={"loc-ws-pin" + (w.pinned ? " on" : "")} title={w.pinned ? "Unpin conversation" : "Pin conversation"} onClick={(event) => togglePin(w, event)}>
                        <Icon name="pin" size={12} />
                      </button>
                    </div>
                  );
                })}
              </div>
            ))}
          </div>
          <div className="loc-side-foot">
            <div className="loc-side-avs">{((liveSummary.teamPreview || []).slice(0, 5)).map((person, i) => <span key={person.name} title={person.name + " · " + person.role} className={"loc-av av-" + i}>{person.initials}</span>)}</div>
            <div className="loc-side-work"><strong>{liveOps.loading ? "Checking workload" : (liveWorkload + " open item" + (liveWorkload === 1 ? "" : "s"))}</strong><span>{liveOps.error ? "live data unavailable" : "ranked by current priority"}</span></div>
          </div>
        </aside>

        {/* ── Chat column (between sidebar and dashboard; pushes content & widens modal) ── */}
        <section className={"loc-chat" + (chatOpen ? " open" : "") + (chatExpanded ? " expanded" : "")} role="dialog" aria-label="Loraa chat">
          <header className="loc-chat-head">
            <div className="loc-chat-head-t">{logo("xs")}
              <div><strong>Loraa</strong><span className="loc-chat-sub"><i className="loc-live-dot" /> {activeId && history.find(x => x.id === activeId) ? history.find(x => x.id === activeId).title : "New conversation"}</span></div>
            </div>
            <div className="loc-chat-head-tools">
              <button className="loc-icbtn" title={chatExpanded ? "Collapse answer board" : "Expand answer board"} onClick={() => setChatExpanded(v => !v)}><Icon name={chatExpanded ? "chevrons-right" : "chevrons-left"} size={17} /></button>
              <button className="loc-icbtn" title="Close chat" onClick={closeChat}><Icon name="x" size={18} /></button>
            </div>
          </header>
          <div className="loc-chat-body" ref={bodyRef} onWheel={pauseFollow} onTouchMove={pauseFollow}>
            {msgs.length === 0 && !typing && (
              <div className="loc-chat-empty">
                {logo("sm")}
                <h3>How can I help, {name}?</h3>
                <p>Ask a question or tell Loraa what to inspect, plan, preview or fix across NutriDMS.</p>
                <div className="loc-ai-trust">
                  <span><Icon name="sparkle" size={12} /> {agent && agent.server && agent.server.configured ? "OpenAI connected" : "Built-in AI active"}</span>
                  <span><Icon name="wrench" size={12} /> {agent && agent.server ? agent.server.tools : 19} governed tools</span>
                  <span><Icon name="shield-check" size={12} /> Role checked</span>
                  <span><Icon name="clipboard-check" size={12} /> Approval gated</span>
                </div>
                <div className="loc-chat-sugg">
                  {opsPrompts.map((prompt, i) => (
                    <button key={i} className="loc-rec" onClick={() => ask(prompt)}><Icon name={i < 2 ? "wand-sparkles" : "lightbulb"} size={14} /> {prompt}</button>
                  ))}
                </div>
                <div className="loc-ai-boundary"><Icon name="lock-keyhole" size={12} /> Sensitive changes are previewed first and every approved action is audited.</div>
              </div>
            )}
            <div className="loc-thread">
              {msgs.map((mm, i) => (
                <div key={i} ref={i === msgs.length - 1 && mm.role === "loraa" && mm._new ? activeResponseRef : null} className={"loc-msg " + mm.role}>
                  {mm.role === "loraa" && logo("xs")}
                  <div className="loc-bubble">
                    {mm.fix
                      ? <LoraaFixFlow data={mm.fix} onPick={fixPick} onApply={fixApply} onOpen={fixOpen} />
                      : mm.workspace
                      ? <LoraaWorkspace data={mm.workspace} onAsk={ask} onReview={reviewQueueItem} reviewingId={reviewingId} onClose={() => {
                          const next = msgsRef.current.filter((_, idx) => idx !== i);
                          msgsRef.current = next; setMsgs(next); persist(activeId, next);
                        }} />
                      : mm.rich
                      ? <LoraaRich data={mm.rich} nav={nav} animate={mm._new} onProgress={scrollThread} onAsk={ask} onFix={startFix} onCmd={runCmd} onServerAction={chooseServerAction} />
                      : (mm.role === "loraa"
                        ? <React.Fragment><LoraaText text={mm.text} animate={mm._new} onProgress={scrollThread} />{Number(mm.conf) > 0 && (<span className="loc-text-conf"><span className="loc-tc-bar"><i style={{ width: mm.conf + "%" }} /></span>Confidence {mm.conf}%</span>)}</React.Fragment>
                        : <React.Fragment>
                            {mm.text && <span className="loc-user-text">{mm.text}</span>}
                            {Array.isArray(mm.attachments) && mm.attachments.length > 0 && (
                              <span className="loc-msg-attachments">
                                {mm.attachments.map((attachment, attachmentIndex) => (
                                  <span className={"loc-msg-attachment " + (attachment.kind || "file")} key={attachment.id || attachment.name || attachmentIndex}>
                                    {attachment.kind === "image" && attachment.previewUrl
                                      ? <img src={attachment.previewUrl} alt={attachment.name || "Attached image"} />
                                      : <Icon name={attachment.kind === "image" ? "image" : "file-text"} size={14} />}
                                    <span><strong>{attachment.name || "Attachment"}</strong><small>{attachment.size ? Math.max(1, Math.round(attachment.size / 1024)) + " KB" : "Attached"}</small></span>
                                  </span>
                                ))}
                              </span>
                            )}
                          </React.Fragment>)}
                  </div>
                </div>
              ))}
              {typing && (thinkSteps ? <LoraaThinking steps={thinkSteps} logo={logo} /> : <div className="loc-msg loraa">{logo("xs")}<div className="loc-bubble loc-typing"><i /><i /><i /></div></div>)}
            </div>
          </div>
          <footer className="loc-chat-foot">
            {(slashMatches.length > 0) && (
              <div className="loc-slash">
                {slashMatches.map((c, i) => (
                  <button key={c.cmd} className={"loc-slash-item" + (i === slash ? " on" : "")} onMouseEnter={() => setSlash(i)} onClick={() => c.prompt ? ask(c.prompt) : nav(c.to)}>
                    <span className="loc-slash-ic"><Icon name={c.icon} size={15} /></span>
                    <span className="loc-slash-tx"><strong>{c.cmd}</strong><small>{c.label} · {c.desc}</small></span>
                    <Icon name="corner-down-left" size={13} className="loc-slash-enter" />
                  </button>
                ))}
              </div>
            )}
            {(entityMatches.length > 0) && (
              <div className="loc-slash">
                {entityMatches.map((en, i) => (
                  <button key={i} className="loc-slash-item" onClick={() => ask("Tell me about " + en.label)}>
                    <span className="loc-slash-ic"><Icon name={en.icon} size={15} /></span>
                    <span className="loc-slash-tx"><strong>{en.label}</strong><small>{en.kind}</small></span>
                  </button>
                ))}
              </div>
            )}
            {attachments.length > 0 && (
              <div className="loc-attachment-tray">
                {attachments.map((attachment) => (
                  <div className="loc-attachment-chip" key={attachment.id}>
                    {attachment.previewUrl ? <img src={attachment.previewUrl} alt="" /> : <Icon name="file-text" size={14} />}
                    <span><strong>{attachment.name}</strong><small>{Math.max(1, Math.round(attachment.size / 1024))} KB</small></span>
                    <button onClick={() => removeAttachment(attachment.id)} aria-label={"Remove " + attachment.name}><Icon name="x" size={12} /></button>
                  </div>
                ))}
              </div>
            )}
            {attachmentError && <div className="loc-attachment-error"><Icon name="circle-alert" size={13} /> {attachmentError}</div>}
            <div className="loc-input">
              <input ref={fileInputRef} className="loc-file-input" type="file" multiple
                accept="image/png,image/jpeg,image/webp,image/gif,application/pdf,text/plain,text/csv,text/markdown,application/json,.doc,.docx,.ppt,.pptx,.xls,.xlsx"
                onChange={(event) => addAttachments(event.target.files)} />
              <button className="loc-attach" type="button" title="Attach files or images" onClick={() => fileInputRef.current && fileInputRef.current.click()}>
                <Icon name="paperclip" size={16} />
              </button>
              <textarea ref={inputRef} rows="1" value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={onKey}
                placeholder='Ask Loraa, or type "/" for commands' />
              <button className="loc-send" onClick={() => ask(input)} disabled={!input.trim() && attachments.length === 0}><Icon name="arrow-up" size={16} stroke={2.4} /></button>
            </div>
            <div className="loc-foot-note"><span>Enter to send · Shift+Enter for a new line</span><span>Loraa uses AI to assist your organization. Sensitive actions remain approval-gated and audited.</span></div>
          </footer>
        </section>

        {/* ── Center: operations ── */}
        <section className="loc-main">
          <header className="loc-head">
            <button className="loc-icbtn" title="Toggle sidebar" onClick={() => setSideOpen(v => !v)}><Icon name="panel-left" size={17} /></button>
            <div className="loc-head-title">{logo("xs")}
              <div><strong>Loraa</strong><span className="loc-head-sub"><i className="loc-live-dot" /> {liveOps.loading ? "Connecting to live NutriDMS data" : liveOps.error ? "Live monitor temporarily unavailable" : ("Monitoring · " + ((liveOps.items || []).length) + " current priorities · updated now")}</span></div>
            </div>
            <div className="loc-head-tools">
              <div className="loc-prov-wrap">
                <button className={"loc-prov" + (agent && agent.server && agent.server.configured ? " on" : "")} onClick={() => setProvOpen(v => !v)} title={agent && agent.server && agent.server.configured ? "AI reasoning engine · OpenAI connected" : "AI reasoning engine · setup required"}>
                  <i className={"loc-prov-dot" + (agent && agent.server && agent.server.configured ? " ready" : "")} />
                  <span className="loc-prov-label">{agent && agent.server && agent.server.configured ? "OpenAI connected" : "AI setup"}</span>
                  <Icon name="chevron-down" size={13} />
                </button>
                {provOpen && <ProviderPopover agent={agent} onClose={() => setProvOpen(false)} />}
              </div>
              <button className="loc-icbtn" title="Search with Loraa" onClick={() => { setChatOpen(true); setInput(""); setTimeout(() => inputRef.current && inputRef.current.focus(), 100); }}><Icon name="search" size={17} /></button>
              <button className="loc-icbtn" title="New conversation" onClick={newConversation}><Icon name="plus" size={17} /></button>
              <button className="loc-icbtn" title="Activity" onClick={() => nav("audit")}><Icon name="activity" size={17} /></button>
              <button className="loc-icbtn" title="Settings" onClick={() => nav("settings")}><Icon name="settings" size={17} /></button>
              <button className={"loc-icbtn" + (ctxOpen ? " on" : "")} title="Context panel" onClick={() => setCtxOpen(v => !v)}><Icon name="panel-right" size={17} /></button>
            </div>
          </header>

          <div className="loc-body">
              <React.Fragment>
                {/* Executive summary */}
                <div className="loc-exec">
                  <div className="loc-exec-top">
                    <div>
                      <h2>{greet}, {name}</h2>
                      <p>{introLine}</p>
                    </div>
                    <div className="loc-gauges">
                      <div className="loc-gauge"><span className="loc-gauge-v ok">{m.gauges.compliance === "—" ? "—" : (m.gauges.compliance + "%")}</span><span className="loc-gauge-k">Compliance</span></div>
                      <div className="loc-gauge"><span className="loc-gauge-v info">{m.gauges.workload}</span><span className="loc-gauge-k">Open workload</span></div>
                      <div className="loc-gauge"><span className="loc-gauge-v ok">{m.gauges.status}</span><span className="loc-gauge-k">Org status</span></div>
                    </div>
                  </div>
                  <div className="loc-tiles">
                    {m.tiles.map((t) => (
                      <button key={t.k} className={"loc-tile " + t.tone} onClick={() => nav(t.to)}>
                        <span className="loc-tile-v">{t.value}</span>
                        <span className="loc-tile-k">{t.label}</span>
                      </button>
                    ))}
                  </div>
                  {learnSug && !learnDismissed && (
                    <div className="loc-learn-sug">
                      <span className="loc-learn-sug-ic"><Icon name="lightbulb" size={16} stroke={2.2} /></span>
                      <span className="loc-learn-sug-tx">{learnSug.text}</span>
                      <span className="loc-learn-sug-actions">
                        <button className="loc-learn-yes" onClick={acceptLearn}>Yes, automate it</button>
                        <button className="loc-learn-no" onClick={() => setLearnDismissed(true)}>Not now</button>
                      </span>
                    </div>
                  )}
                </div>

                {/* Operations feed */}
                <div className="loc-sec-h"><span>Operations feed</span><small>Live priorities from your NutriDMS organization</small></div>
                <div className="loc-feed">
                  {m.feed.length === 0 && (
                    <div className="loc-card ok" role="status">
                      <span className="loc-card-ic ok"><Icon name={liveOps.loading ? "loader" : "check-circle-2"} size={16} /></span>
                      <div className="loc-card-body">
                        <div className="loc-card-top"><strong>{liveOps.loading ? "Loading live operations" : (liveOps.error ? "Live operations unavailable" : "No current work needs attention")}</strong></div>
                        <div className="loc-card-meta">{liveOps.error || (liveOps.loading ? "Checking current tasks, reviews, labels, compliance findings and audit activity." : "Loraa will add cards when real tenant work changes priority.")}</div>
                      </div>
                    </div>
                  )}
                  {m.feed.map((c, i) => (
                    <button key={c.id} className={"loc-card " + c.tone} style={{ animationDelay: (i * 45) + "ms" }} onClick={() => nav(c.to)}>
                      <span className={"loc-card-ic " + c.tone}>{c.tone === "run" ? <span className="loc-spin" /> : <Icon name={c.icon} size={16} stroke={2.3} />}</span>
                      <div className="loc-card-body">
                        <div className="loc-card-top"><strong>{c.title}</strong><span className={"loc-status " + c.tone}>{c.status}</span></div>
                        {c.chips && <div className="loc-chips">{c.chips.map((ch, j) => <span key={j} className="loc-chip">{ch}</span>)}</div>}
                        {c.meta && <div className="loc-card-meta">{c.meta}</div>}
                        <div className="loc-card-foot"><span className="loc-time">{locFriendlyTime(c.time || c.at)}</span><span className="loc-open">Open <Icon name="arrow-right" size={12} stroke={2.4} /></span></div>
                      </div>
                    </button>
                  ))}
                </div>

                {/* Recommendations */}
                <div className="loc-sec-h"><span>Recommended next</span></div>
                <div className="loc-recs">
                  {recs.map((r, i) => (
                    <button key={i} className="loc-rec" onClick={() => r.to ? nav(r.to) : ask(r.prompt || r.text)}>
                      <Icon name="lightbulb" size={14} /> {r.text}
                    </button>
                  ))}
                </div>
              </React.Fragment>
          </div>
        </section>

        {/* ── Right: context ── */}
        <aside className="loc-ctx">
          <div className="loc-ctx-h">Live monitor {agent && agent.crawler && agent.crawler.running && <span className="loc-ctx-live"><i /> on</span>}</div>
          <div className="loc-ctx-crawl">
            <div className="loc-crawl-row"><Icon name="radar" size={14} /> <span>Continuously scanning your operation</span></div>
            <div className="loc-crawl-row"><Icon name="book-open" size={14} /> <span>Connected to tenant policies, roles, compliance, labels and work records</span></div>
            <div className="loc-crawl-stats">
              <span><strong>{liveOps.loading ? "—" : ((liveOps.items || []).length)}</strong> priorities</span>
              <span><strong>{liveSummary.activeAutomations == null ? "—" : liveSummary.activeAutomations}</strong> automations</span>
              <span><strong>{liveOps.error ? "offline" : "now"}</strong></span>
            </div>
            {liveSources.length > 0 && (
              <div className="loc-crawl-sources">
                {liveSources.map(([source, count]) => <span key={source}><i />{locFriendlySource(source)}<b>{count}</b></span>)}
              </div>
            )}
            {m.feed.slice(0, 4).map((item) => (
              <button key={item.id} className="loc-crawl-obs" onClick={() => nav(item.to)}>
                <span className={"loc-notif-dot " + item.tone} /><span className="loc-notif-tx">{item.title}</span>
              </button>
            ))}
            <button className="loc-daily-brief-btn" onClick={() => ask("Give me today's daily NutriDMS brief")}><Icon name="sun" size={14} />Generate my daily brief<Icon name="arrow-right" size={13} /></button>
          </div>
          <div className="loc-ctx-h">Today's metrics</div>
          <div className="loc-ctx-metrics">
            <div className="loc-ctx-metric"><span className="loc-ctx-v ok">{m.gauges.compliance === "—" ? "—" : (m.gauges.compliance + "%")}</span><span>Compliance score</span></div>
            <div className="loc-ctx-metric"><span className="loc-ctx-v run">{liveSummary.publishingQueue == null ? "—" : liveSummary.publishingQueue}</span><span>Publishing queue</span></div>
            <div className="loc-ctx-metric"><span className="loc-ctx-v warn">{liveSummary.reviewCount == null ? "—" : liveSummary.reviewCount}</span><span>Awaiting review</span></div>
            <div className="loc-ctx-metric"><span className="loc-ctx-v block">{liveSummary.riskAlerts == null ? "—" : liveSummary.riskAlerts}</span><span>Risk alerts</span></div>
          </div>
          <div className="loc-ctx-h">Recent notifications</div>
          <div className="loc-ctx-notifs">
            {m.notifications.map((n, i) => (
              <div key={i} className="loc-notif"><span className={"loc-notif-dot " + n.tone} /><span className="loc-notif-tx">{n.text}</span><small>{n.time}</small></div>
            ))}
          </div>
          <div className="loc-ctx-h">Quick links</div>
          <div className="loc-ctx-links">
            {m.quickLinks.map((q, i) => (
              <button key={i} className="loc-ctx-link" onClick={() => nav(q.to)}><Icon name={q.icon} size={15} /> {q.label}<Icon name="arrow-up-right" size={13} className="loc-ctx-link-arr" /></button>
            ))}
          </div>
          <div className="loc-ctx-h">Learning about you</div>
          <div className="loc-ctx-learn">
            <div className="loc-learn-top">
              <span><b>Level {learning ? learning.level.n : 1}</b><strong>{learning ? learning.level.name : "Getting acquainted"}</strong></span>
              <em>{learning ? learning.pct : Math.round(fam * 100)}%</em>
            </div>
            <div className="loc-learn-bar"><span style={{ width: (learning ? learning.pct : Math.round(fam * 100)) + "%" }} /></div>
            <div className="loc-learn-sub">{learning ? learning.level.desc : "Getting to know how you work"}{agent && agent.profile ? " · " + agent.profile.sessions + " sessions" : ""}</div>
            {learned.length > 0 && <div className="loc-learn-tags">{learned.map((mo, i) => <span key={i} className="loc-learn-tag">{MOD_LABEL[mo] || mo}</span>)}</div>}
            <button className="loc-learn-explain" onClick={() => setLearningOpen(v => !v)}>{learningOpen ? "Hide learning details" : "How this score works"}<Icon name={learningOpen ? "chevron-up" : "chevron-down"} size={13} /></button>
            {learningOpen && learning && (
              <div className="loc-learn-details">
                <p>{learning.next ? ("Next: Level " + learning.next.n + " · " + learning.next.name) : "Top level reached. Governance controls still apply."}</p>
                <div className="loc-learn-components">
                  {learning.components.map((c, i) => <span key={i}><em>{c.label}</em><b>{c.value}/{c.max}</b></span>)}
                </div>
                <div className="loc-learn-signals">{learning.signals.map((s, i) => <span key={i}><Icon name="check" size={11} />{s}</span>)}</div>
                <div className="loc-learn-controls">
                  <button onClick={() => { try { window.LoraaAgent.setLearning(!learning.enabled, role); } catch (e) {} }}>{learning.enabled ? "Pause personalization" : "Resume personalization"}</button>
                  <button onClick={() => { if (window.confirm("Reset what Loraa has learned for this user? This cannot be undone.")) { try { window.LoraaAgent.Memory.reset(role); } catch (e) {} } }}>Reset learning</button>
                </div>
              </div>
            )}
          </div>
          {agent && agent.facts && agent.facts.length > 0 && (
            <React.Fragment>
              <div className="loc-ctx-h">What Loraa remembers</div>
              <div className="loc-ctx-facts">
                {agent.facts.map((f, i) => (
                  <div key={i} className="loc-fact">
                    <Icon name="bookmark" size={13} />
                    <span>{f.text}</span>
                    <button className="loc-fact-x" title="Forget this" onClick={() => { try { window.LoraaAgent.forgetFact(f.text, role); } catch (e) {} }}><Icon name="x" size={12} /></button>
                  </div>
                ))}
              </div>
            </React.Fragment>
          )}
        </aside>

        <ActionReview action={reviewAction} busy={actionBusy} error={actionError}
          onCancel={() => { setReviewAction(null); setActionError(""); }}
          onConfirm={confirmServerAction} />
      </div>
    </React.Fragment>
  );
}

if (typeof window !== "undefined") window.LoraaAsk = LoraaAsk;
