// ml-nlp.jsx — Natural Language Processing
// ─────────────────────────────────────────────────────────────────
// Document & language intelligence over agreements, inbox emails,
// statement memos, and audit notes. Five capabilities:
//
//   01 ENTITY EXTRACTION — parties, IPIs, ISWC/ISRC, territories,
//      monetary amounts, dates, royalty rates, work titles.
//   02 CLAUSE CLASSIFICATION — segments contract text into 18+
//      clause types (term, territory, royalty, audit, indemnity,
//      MFN, key-man, suspension, etc.) with risk scoring.
//   03 SEMANTIC SEARCH — natural-language query across all docs;
//      ranks by lexical overlap + entity match + clause-type fit.
//   04 FACT EXTRACTION — pulls structured key/value pairs from
//      free text (royalty %, advance $, term length, audit
//      window, MFN status, key territories).
//   05 SENTIMENT & TONE — for inbox emails + statement memos:
//      cooperative / neutral / disputatious, with confidence.
//
// Each agreement → "language card" showing extracted facts,
// classified clauses, risk flags, and inconsistencies.
//
// EXPORT:
//   window.NLPEngine.scan()                 — analyze all docs
//   window.NLPEngine.parse(text, kind?)     — analyze ad-hoc text
//   window.NLPEngine.search(query)          — semantic doc search
//   window.NLPEngine.extract(agreement)     — single-doc full pass
//   window.MLNLPTab                         — full screen UI
//   window.NLPAgreementCard                 — embed for agreement page
// ─────────────────────────────────────────────────────────────────
(function () {
  if (typeof window === 'undefined' || !window.React) return;
  const _S = React.useState, _M = React.useMemo, _E = React.useEffect, _R = React.useRef;

  function Mono({ children, upper, size, color, style, ...rest }) {
    return <span className={'ff-mono' + (upper?' upper':'')} style={{ fontSize: size||11, color: color||'var(--ink)', letterSpacing: upper?'.08em':0, ...style }} {...rest}>{children}</span>;
  }

  // ─── ENTITY PATTERNS ───────────────────────────────────────────
  // Regex-based extractors (deterministic, no model needed).
  const ENTITY_PATTERNS = {
    ipi: {
      label: 'IPI',
      color: '#1a4ed8',
      re: /\b(?:IPI[:\s#]*)?(\d{9,11})\b/g,
      validate: (m) => /^\d{9,11}$/.test(m),
    },
    iswc: {
      label: 'ISWC',
      color: '#0a8754',
      re: /\b(T-?\d{3}\.?\d{3}\.?\d{3}-?\d?)\b/gi,
    },
    isrc: {
      label: 'ISRC',
      color: '#0a8754',
      re: /\b([A-Z]{2}-?[A-Z0-9]{3}-?\d{2}-?\d{5})\b/g,
    },
    money: {
      label: 'AMOUNT',
      color: '#a35418',
      re: /(?:US?\$|GBP|EUR|JPY|£|€|¥)\s*([\d,]+(?:\.\d+)?(?:\s*(?:k|K|m|M|million|thousand))?)/g,
    },
    percent: {
      label: 'RATE',
      color: '#7a3a8c',
      re: /(\d{1,3}(?:\.\d{1,3})?)\s*%/g,
    },
    date: {
      label: 'DATE',
      color: '#7a8590',
      re: /\b(\d{4}-\d{2}-\d{2}|\d{1,2}\s+(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{4})\b/g,
    },
    territory: {
      label: 'TERR',
      color: '#a32a18',
      // matched against territory list
      tokens: ['World','Worldwide','WW','US','USA','United States','UK','GB','United Kingdom','EU','Europe','Germany','DE','France','FR','Japan','JP','Brazil','BR','Mexico','MX','Canada','CA','Australia','AU','India','IN','China','CN','South Korea','KR','Argentina','AR','Spain','ES','Italy','IT','Netherlands','NL','Sweden','SE','Norway','NO','DACH','LATAM','APAC','EMEA'],
    },
    duration: {
      label: 'TERM',
      color: '#1a4ed8',
      re: /(\d+)\s+(year|month|week|day)s?\b/gi,
    },
  };

  // ─── CLAUSE TYPES ──────────────────────────────────────────────
  // Each clause type = name, keyword signals, risk profile, color.
  const CLAUSE_TYPES = [
    { k: 'term',         l: 'Term',                kw: ['term','duration','effective date','expiration','expires','commencement'], risk: 'low',  color: '#0a8754' },
    { k: 'territory',    l: 'Territory',           kw: ['territory','world','geographic','region','excluding'], risk: 'low',  color: '#0a8754' },
    { k: 'royalty',      l: 'Royalty Rate',        kw: ['royalty','percentage','net receipts','gross','share','split'], risk: 'med',  color: '#d4881f' },
    { k: 'mech',         l: 'Mechanical',          kw: ['mechanical','statutory rate','controlled composition','3/4 rate','penny rate'], risk: 'med',  color: '#d4881f' },
    { k: 'sync',         l: 'Sync',                kw: ['synchronization','sync license','audiovisual','master use','most-favored'], risk: 'med',  color: '#d4881f' },
    { k: 'advance',      l: 'Advance',             kw: ['advance','payable upon','recoupable','recoupment','minimum guarantee','MG'], risk: 'med',  color: '#d4881f' },
    { k: 'audit',        l: 'Audit',               kw: ['audit','inspection','books and records','accountant','verification','statement'], risk: 'low',  color: '#0a8754' },
    { k: 'mfn',          l: 'MFN',                 kw: ['most favored nations','most-favored','MFN','equal terms','no less favorable'], risk: 'high', color: '#a32a18' },
    { k: 'indemnity',    l: 'Indemnity',           kw: ['indemnify','indemnification','hold harmless','defend'], risk: 'high', color: '#a32a18' },
    { k: 'warranty',     l: 'Warranty',            kw: ['warrant','represent and warrant','original work','no infringement'], risk: 'med',  color: '#d4881f' },
    { k: 'termination',  l: 'Termination',         kw: ['terminate','termination','breach','material breach','cure period','30 days written notice'], risk: 'high', color: '#a32a18' },
    { k: 'assignment',   l: 'Assignment',          kw: ['assign','assignment','transfer','successor','permitted assignee'], risk: 'med',  color: '#d4881f' },
    { k: 'changecontrol',l: 'Change of Control',   kw: ['change of control','merger','acquisition','sale of substantially'], risk: 'high', color: '#a32a18' },
    { k: 'keyman',       l: 'Key-Man',             kw: ['key-man','key man','principal','founder','death or disability'], risk: 'high', color: '#a32a18' },
    { k: 'suspension',   l: 'Suspension',          kw: ['suspend','suspension','force majeure','frustration'], risk: 'med',  color: '#d4881f' },
    { k: 'governing',    l: 'Governing Law',       kw: ['governing law','jurisdiction','venue','court','arbitration','LCIA','AAA','JAMS'], risk: 'low',  color: '#0a8754' },
    { k: 'confidential', l: 'Confidentiality',     kw: ['confidential','non-disclosure','NDA','proprietary information'], risk: 'low',  color: '#0a8754' },
    { k: 'reversion',    l: 'Reversion',           kw: ['revert','reversion','retain ownership','return of rights','35-year','section 203'], risk: 'high', color: '#a32a18' },
    { k: 'crossrec',     l: 'Cross-Recoupment',    kw: ['cross-recoup','cross recoup','recoupable from','prior advance'], risk: 'high', color: '#a32a18' },
    { k: 'controlcomp',  l: 'Controlled Composition', kw: ['controlled composition','3/4','75%','penny cap','fixed cap'], risk: 'high', color: '#a32a18' },
  ];

  // ─── SENTIMENT LEXICON (for emails/memos) ──────────────────────
  const SENT_LEX = {
    positive: ['agree','approve','glad','thanks','appreciate','cooperative','partner','willing','happy','prompt','accommodate'],
    negative: ['dispute','reject','refuse','breach','default','demand','reservation','without prejudice','litigation','overdue','delinquent','unauthorized','underpaid','withhold'],
  };

  // ─── ENTITY EXTRACTION ─────────────────────────────────────────
  function extractEntities(text) {
    if (!text || typeof text !== 'string') return [];
    const out = [];

    // Regex-based
    Object.entries(ENTITY_PATTERNS).forEach(([k, def]) => {
      if (def.tokens) return; // handled below
      let m; const re = new RegExp(def.re.source, def.re.flags);
      while ((m = re.exec(text)) !== null) {
        const value = m[1] || m[0];
        if (def.validate && !def.validate(value)) continue;
        out.push({ kind: k, label: def.label, value, color: def.color, idx: m.index, len: m[0].length });
      }
    });

    // Token-based (territories)
    const T = ENTITY_PATTERNS.territory;
    T.tokens.forEach(tok => {
      const re = new RegExp('\\b' + tok.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g');
      let m;
      while ((m = re.exec(text)) !== null) {
        out.push({ kind: 'territory', label: T.label, value: tok, color: T.color, idx: m.index, len: m[0].length });
      }
    });

    // Dedupe by (idx, kind)
    const seen = new Set();
    return out.filter(e => {
      const key = e.idx + '·' + e.kind;
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    }).sort((a, b) => a.idx - b.idx);
  }

  // ─── CLAUSE CLASSIFICATION ─────────────────────────────────────
  // Score each clause type against the text using keyword matches,
  // weighted by phrase rarity. Returns top-N with confidence.
  function classifyClauses(text) {
    if (!text || typeof text !== 'string') return [];
    const lower = text.toLowerCase();
    const scores = CLAUSE_TYPES.map(c => {
      let hits = 0, hitTokens = [];
      c.kw.forEach(k => {
        const idx = lower.indexOf(k.toLowerCase());
        if (idx !== -1) {
          hits += 1 + (k.length / 20);  // longer phrases score higher
          hitTokens.push(k);
        }
      });
      return { ...c, hits, hitTokens, conf: Math.min(0.95, 0.4 + hits * 0.18) };
    }).filter(s => s.hits > 0)
      .sort((a, b) => b.hits - a.hits);
    return scores;
  }

  // ─── FACT EXTRACTION ───────────────────────────────────────────
  // Heuristic extraction of structured fields from agreement objects
  // OR free text. Returns { royaltyPct, advanceUsd, termYears, audit, ... }
  function extractFacts(input) {
    const facts = {
      parties: [],
      royaltyPct: null,
      advanceUsd: null,
      termYears: null,
      auditWindow: null,
      mfn: null,
      territoryScope: null,
      governingLaw: null,
      keyDates: {},
      flags: [],
    };

    if (!input) return facts;

    // Object input (an AGREEMENT row)
    if (typeof input === 'object') {
      const ag = input;
      if (Array.isArray(ag.parties)) {
        facts.parties = ag.parties.map(p => ({ name: p.name, role: p.role, ipi: p.ipi, share: p.share }));
      }
      if (ag.share && /\d/.test(ag.share)) {
        const m = String(ag.share).match(/(\d+)/);
        if (m) facts.royaltyPct = parseInt(m[1]);
      }
      if (ag.start) facts.keyDates.start = ag.start;
      if (ag.end) facts.keyDates.end = ag.end;
      if (ag.start && ag.end) {
        const y0 = new Date(ag.start).getFullYear();
        const y1 = new Date(ag.end).getFullYear();
        if (!isNaN(y0) && !isNaN(y1)) facts.termYears = y1 - y0;
      }
      if (ag.territory) facts.territoryScope = ag.territory;
      if (ag.jurisdiction) facts.governingLaw = ag.jurisdiction;
      // Concatenate all text-like fields for further scanning
      const text = [ag.kind, ag.shareNotes, ag.advanceTerms, ag.recoupTerms, ag.notes, ag.specialTerms].filter(Boolean).join(' ');
      if (text) {
        const subFacts = extractFacts(text);
        if (subFacts.advanceUsd && !facts.advanceUsd) facts.advanceUsd = subFacts.advanceUsd;
        if (subFacts.auditWindow && !facts.auditWindow) facts.auditWindow = subFacts.auditWindow;
        if (subFacts.mfn != null && facts.mfn == null) facts.mfn = subFacts.mfn;
      }
      // Flag heuristics
      if (!facts.parties.length) facts.flags.push({ k: 'no-parties', sev: 'high', msg: 'No parties extracted' });
      if (!facts.termYears) facts.flags.push({ k: 'no-term', sev: 'med', msg: 'Term length not parseable' });
      if (!facts.governingLaw) facts.flags.push({ k: 'no-law', sev: 'med', msg: 'Governing law absent' });
      const inbalance = facts.parties.reduce((s, p) => s + (p.share || 0), 0);
      if (facts.parties.length >= 2 && inbalance > 0 && Math.abs(inbalance - 100) > 5 && Math.abs(inbalance - 200) > 5) {
        facts.flags.push({ k: 'share-imbalance', sev: 'high', msg: `Party shares sum to ${inbalance}% (expected ~100% or ~200%)` });
      }
      return facts;
    }

    // Text input
    const text = String(input);
    const lower = text.toLowerCase();

    // Royalty %
    const royM = text.match(/(\d{1,3}(?:\.\d+)?)\s*%/);
    if (royM) facts.royaltyPct = parseFloat(royM[1]);

    // Advance
    const advM = text.match(/(?:advance|MG|minimum guarantee).*?(?:US?\$|£|€)\s*([\d,]+)/i);
    if (advM) facts.advanceUsd = parseInt(advM[1].replace(/,/g, ''));

    // Term
    const termM = text.match(/(\d+)\s*(?:-|\s)?\s*year/i);
    if (termM) facts.termYears = parseInt(termM[1]);

    // Audit
    const audM = text.match(/audit.*?(\d+)\s*year/i);
    if (audM) facts.auditWindow = parseInt(audM[1]);

    // MFN
    if (/most[-\s]favou?red\s+nations|MFN/i.test(text)) facts.mfn = true;

    return facts;
  }

  // ─── SENTIMENT (lexicon) ───────────────────────────────────────
  function sentiment(text) {
    if (!text) return { label: 'neutral', score: 0, conf: 0 };
    const lower = text.toLowerCase();
    let pos = 0, neg = 0;
    SENT_LEX.positive.forEach(w => { if (lower.includes(w)) pos++; });
    SENT_LEX.negative.forEach(w => { if (lower.includes(w)) neg++; });
    const score = (pos - neg) / Math.max(1, pos + neg);
    let label = 'neutral';
    if (score > 0.25) label = 'cooperative';
    else if (score < -0.25) label = 'disputatious';
    return { label, score, pos, neg, conf: Math.min(0.9, (pos + neg) * 0.18 + 0.3) };
  }

  // ─── BUILD CORPUS ──────────────────────────────────────────────
  // Convert AGREEMENTS + inbox + memos into searchable docs.
  function buildCorpus() {
    const docs = [];
    const ags = window.AGREEMENTS || [];
    ags.forEach(ag => {
      const text = [
        ag.id, ag.kind, ag.a, ag.b,
        'Territory: ' + (ag.territory || ''),
        'Share: ' + (ag.share || ''),
        'Term: ' + (ag.start || '') + ' to ' + (ag.end || ''),
        'Jurisdiction: ' + (ag.jurisdiction || ''),
        'Dispute resolution: ' + (ag.disputeResolution || ''),
        (ag.parties || []).map(p => p.role + ': ' + p.name + (p.ipi ? ' (IPI ' + p.ipi + ')' : '')).join('. '),
        ag.shareNotes || '', ag.advanceTerms || '', ag.notes || '', ag.specialTerms || '',
      ].filter(Boolean).join('. ');
      docs.push({
        id: ag.id, kind: 'agreement', title: ag.id + ' · ' + (ag.kind || ''),
        sub: (ag.a || '') + ' ↔ ' + (ag.b || ''),
        text, ag,
      });
    });

    // Inbox emails (if window has any)
    const emails = window.INBOX_ITEMS || window.EMAILS || [];
    emails.forEach((e, i) => {
      const text = [e.subject, e.from, e.body, e.preview, e.summary].filter(Boolean).join('. ');
      if (text) docs.push({
        id: 'email-' + (e.id || i), kind: 'email',
        title: e.subject || 'Email ' + i, sub: e.from || '',
        text, email: e,
      });
    });

    // Statement memos
    const stmts = (window.__STMT_INDEX && window.__STMT_INDEX.statements) || [];
    stmts.slice(0, 30).forEach(s => {
      if (s.notes || s.memo) docs.push({
        id: 'stmt-' + s.id, kind: 'memo',
        title: 'Memo · ' + (s.sourceName || s.sourceId), sub: s.period || '',
        text: (s.notes || '') + ' ' + (s.memo || ''), stmt: s,
      });
    });

    return docs;
  }

  // ─── SEMANTIC SEARCH ───────────────────────────────────────────
  // Lexical TF-IDF-ish ranking with entity/clause boosts.
  function search(query, opts) {
    opts = opts || {};
    if (!query || !query.trim()) return [];
    const q = query.toLowerCase().trim();
    const qTokens = q.split(/\s+/).filter(t => t.length > 1);

    const docs = (opts.corpus) || buildCorpus();
    const qEntities = extractEntities(query);
    const qClauses = classifyClauses(query);

    return docs.map(d => {
      const text = d.text.toLowerCase();
      let score = 0;
      let matchTokens = [];
      qTokens.forEach(t => {
        if (text.includes(t)) {
          // basic term boost; rarer tokens score higher
          score += t.length > 5 ? 3 : 1.5;
          matchTokens.push(t);
        }
      });
      // Entity match boost
      qEntities.forEach(e => {
        if (text.includes(e.value.toLowerCase())) score += 4;
      });
      // Clause match boost
      qClauses.forEach(c => {
        c.kw.forEach(kw => { if (text.includes(kw.toLowerCase())) score += 2; });
      });
      // Snippet around best match
      let snippet = '';
      if (matchTokens.length) {
        const at = text.indexOf(matchTokens[0]);
        const start = Math.max(0, at - 60);
        const end = Math.min(d.text.length, at + 140);
        snippet = (start > 0 ? '…' : '') + d.text.slice(start, end) + (end < d.text.length ? '…' : '');
      } else {
        snippet = d.text.slice(0, 180);
      }
      return { ...d, score, matchTokens, snippet };
    })
      .filter(r => r.score > 0)
      .sort((a, b) => b.score - a.score)
      .slice(0, opts.limit || 30);
  }

  // ─── PER-AGREEMENT FULL EXTRACT ────────────────────────────────
  function extract(ag) {
    if (!ag) return null;
    const text = [
      ag.kind, ag.share, ag.shareNotes, ag.advanceTerms, ag.recoupTerms,
      ag.notes, ag.specialTerms, ag.jurisdiction, ag.territory,
      (ag.parties || []).map(p => p.role + ': ' + p.name + (p.ipi ? ' IPI ' + p.ipi : '')).join('. '),
    ].filter(Boolean).join('. ');

    return {
      id: ag.id,
      entities: extractEntities(text),
      clauses: classifyClauses(text + ' ' + (ag.kind || '') + ' ' + (ag.share || '')),
      facts: extractFacts(ag),
      sentiment: sentiment(text),
    };
  }

  // ─── CORPUS SCAN (overview metrics) ────────────────────────────
  function scan() {
    const corpus = buildCorpus();
    const ags = window.AGREEMENTS || [];

    // Aggregate clause distribution
    const clauseDist = {};
    CLAUSE_TYPES.forEach(c => clauseDist[c.k] = { ...c, count: 0 });
    let entitiesTotal = 0, sentTotals = { cooperative: 0, neutral: 0, disputatious: 0 };
    let highRiskCount = 0;
    const flagged = [];

    ags.forEach(ag => {
      const ex = extract(ag);
      if (!ex) return;
      ex.clauses.forEach(c => {
        if (clauseDist[c.k]) clauseDist[c.k].count++;
      });
      entitiesTotal += ex.entities.length;
      if (ex.facts.flags.length) {
        flagged.push({
          ag, flags: ex.facts.flags,
          highest: ex.facts.flags.reduce((h, f) => f.sev === 'high' ? 'high' : (h === 'high' ? 'high' : f.sev), 'low'),
        });
      }
      if (ex.clauses.some(c => c.risk === 'high')) highRiskCount++;
    });

    // Email sentiment distribution
    (window.INBOX_ITEMS || []).forEach(e => {
      const s = sentiment([e.subject, e.body, e.preview].filter(Boolean).join(' '));
      sentTotals[s.label] = (sentTotals[s.label] || 0) + 1;
    });

    return {
      corpus,
      docCount: corpus.length,
      agCount: ags.length,
      entitiesTotal,
      clauseDist: Object.values(clauseDist).sort((a, b) => b.count - a.count),
      flagged: flagged.sort((a, b) => (b.highest === 'high' ? 1 : 0) - (a.highest === 'high' ? 1 : 0)).slice(0, 50),
      highRiskCount,
      sentTotals,
    };
  }

  window.NLPEngine = {
    parse: (text) => ({ entities: extractEntities(text), clauses: classifyClauses(text), facts: extractFacts(text), sentiment: sentiment(text) }),
    search, extract, scan, buildCorpus,
    ENTITY_PATTERNS, CLAUSE_TYPES,
  };

  // ─────────────────────────── UI ────────────────────────────────

  // Highlighted text — wraps entity matches inline.
  function HighlightedText({ text, entities, dense }) {
    if (!text) return null;
    if (!entities || !entities.length) return <span>{text}</span>;
    const parts = [];
    let cursor = 0;
    entities.forEach((e, i) => {
      if (e.idx > cursor) parts.push(<span key={'t'+i}>{text.slice(cursor, e.idx)}</span>);
      parts.push(
        <span key={'e'+i} title={e.label} style={{
          background: e.color + '22', color: e.color,
          padding: dense ? '0 2px' : '1px 4px',
          borderBottom: '1px solid ' + e.color,
          fontFamily: e.kind === 'ipi' || e.kind === 'iswc' || e.kind === 'isrc' ? 'var(--ff-mono, monospace)' : 'inherit',
        }}>{text.slice(e.idx, e.idx + e.len)}</span>
      );
      cursor = e.idx + e.len;
    });
    if (cursor < text.length) parts.push(<span key="tail">{text.slice(cursor)}</span>);
    return <span>{parts}</span>;
  }

  function ClauseChip({ c, withCount }) {
    return (
      <div style={{
        display: 'inline-flex', alignItems: 'center', gap: 6,
        padding: '4px 9px', border: '1px solid ' + c.color,
        background: c.color + '12', color: c.color,
        fontFamily: 'var(--ff-mono, monospace)', fontSize: 10, letterSpacing: '0.06em',
      }}>
        <span style={{ textTransform: 'uppercase' }}>{c.l}</span>
        {withCount && c.count != null && <span style={{ opacity: 0.65 }}>{c.count}</span>}
      </div>
    );
  }

  function RiskBadge({ risk }) {
    const map = { low: { c: '#0a8754', l: 'LOW' }, med: { c: '#d4881f', l: 'MED' }, high: { c: '#a32a18', l: 'HIGH' } };
    const m = map[risk] || map.low;
    return <span className="ff-mono" style={{ fontSize: 9, padding: '1px 5px', background: m.c, color: '#fff', letterSpacing: '0.08em' }}>{m.l}</span>;
  }

  function SentimentBar({ sent }) {
    const map = { cooperative: { c: '#0a8754', l: 'COOPERATIVE' }, neutral: { c: '#7a8590', l: 'NEUTRAL' }, disputatious: { c: '#a32a18', l: 'DISPUTATIOUS' } };
    const m = map[sent.label] || map.neutral;
    return (
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <span className="ff-mono" style={{ fontSize: 9, padding: '2px 6px', background: m.c, color: '#fff', letterSpacing: '0.08em' }}>{m.l}</span>
        <span className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>conf {Math.round(sent.conf * 100)}%</span>
      </div>
    );
  }

  // ─── MAIN TAB ──────────────────────────────────────────────────
  function MLNLPTab({ go }) {
    const [view, setView] = _S('overview'); // overview | search | inspect
    const [query, setQuery] = _S('');
    const [activeQ, setActiveQ] = _S('');
    const [pickedAg, setPickedAg] = _S(null);
    const [adhoc, setAdhoc] = _S('');

    const result = _M(() => scan(), []);
    const searchResults = _M(() => activeQ ? search(activeQ, { corpus: result.corpus, limit: 30 }) : [], [activeQ, result]);
    const adhocAnalysis = _M(() => adhoc ? window.NLPEngine.parse(adhoc) : null, [adhoc]);
    const pickedExtract = _M(() => pickedAg ? extract(pickedAg) : null, [pickedAg]);

    const queryEntities = _M(() => activeQ ? extractEntities(activeQ) : [], [activeQ]);
    const queryClauses = _M(() => activeQ ? classifyClauses(activeQ) : [], [activeQ]);

    const ags = window.AGREEMENTS || [];

    return (
      <div>
        {/* Headline metrics */}
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)', marginBottom: 24 }}>
          <Cell label="DOCS INDEXED" value={result.docCount} sub="agreements + emails + memos"/>
          <Cell label="ENTITIES EXTRACTED" value={result.entitiesTotal.toLocaleString()} sub="parties / IPI / dates / amounts"/>
          <Cell label="CLAUSE TYPES SEEN" value={result.clauseDist.filter(c => c.count > 0).length} sub={'of ' + CLAUSE_TYPES.length}/>
          <Cell label="HIGH-RISK DEALS" value={result.highRiskCount} sub="≥1 high-risk clause" tone={result.highRiskCount > 0 ? '#a32a18' : undefined}/>
          <Cell label="FLAGGED" value={result.flagged.length} sub="missing/inconsistent fields" tone={result.flagged.length > 0 ? '#d4881f' : undefined}/>
        </div>

        {/* Mode tabs */}
        <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid var(--rule)', marginBottom: 24 }}>
          {[
            { k: 'overview', l: 'Overview' },
            { k: 'search',   l: 'Semantic Search' },
            { k: 'inspect',  l: 'Inspect Document' },
            { k: 'adhoc',    l: 'Parse Text' },
          ].map(t => (
            <button key={t.k} onClick={() => setView(t.k)} className="ff-mono upper" style={{
              fontSize: 11, letterSpacing: '0.08em', padding: '10px 18px',
              background: 'transparent', border: 0,
              borderBottom: '2px solid ' + (view === t.k ? 'var(--ink)' : 'transparent'),
              color: view === t.k ? 'var(--ink)' : 'var(--ink-3)',
              cursor: 'pointer',
            }}>{t.l}</button>
          ))}
        </div>

        {/* OVERVIEW */}
        {view === 'overview' && (
          <div>
            <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 22 }}>
              {/* Clause distribution */}
              <div>
                <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 10 }}>
                  CLAUSE-TYPE DISTRIBUTION · ACROSS {result.agCount} AGREEMENTS
                </Mono>
                {result.clauseDist.filter(c => c.count > 0).map(c => {
                  const max = Math.max(...result.clauseDist.map(x => x.count), 1);
                  return (
                    <div key={c.k} style={{ display: 'grid', gridTemplateColumns: '120px 60px 1fr 50px', gap: 10, alignItems: 'center', padding: '5px 0', borderBottom: '1px solid var(--rule-soft)' }}>
                      <Mono size={11} color={c.color} style={{ fontWeight: 500 }}>{c.l}</Mono>
                      <RiskBadge risk={c.risk}/>
                      <div style={{ height: 6, background: 'var(--bg-2)', position: 'relative' }}>
                        <div style={{ height: '100%', width: (c.count / max * 100) + '%', background: c.color }}/>
                      </div>
                      <Mono size={11} style={{ textAlign: 'right' }}>{c.count}</Mono>
                    </div>
                  );
                })}
              </div>
              {/* Inbox sentiment */}
              <div>
                <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 10 }}>
                  INBOX SENTIMENT · TONE DISTRIBUTION
                </Mono>
                {[
                  { k: 'cooperative', l: 'Cooperative', c: '#0a8754' },
                  { k: 'neutral',     l: 'Neutral',     c: '#7a8590' },
                  { k: 'disputatious',l: 'Disputatious',c: '#a32a18' },
                ].map(t => {
                  const v = result.sentTotals[t.k] || 0;
                  const total = Object.values(result.sentTotals).reduce((s, x) => s + x, 0) || 1;
                  return (
                    <div key={t.k} style={{ padding: '12px 14px', border: '1px solid var(--rule)', marginBottom: 8 }}>
                      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
                        <Mono upper size={10} color={t.c} style={{ fontWeight: 500 }}>{t.l}</Mono>
                        <div className="ff-display" style={{ fontSize: 18, fontWeight: 600, color: t.c }}>{v}</div>
                      </div>
                      <div style={{ height: 4, background: 'var(--bg-2)', marginTop: 6 }}>
                        <div style={{ height: '100%', width: (v / total * 100) + '%', background: t.c }}/>
                      </div>
                    </div>
                  );
                })}

                {/* Flagged */}
                <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginTop: 22, marginBottom: 10 }}>
                  EXTRACTION FLAGS · {result.flagged.length}
                </Mono>
                <div style={{ maxHeight: 280, overflowY: 'auto' }}>
                  {result.flagged.slice(0, 15).map((f, i) => (
                    <div key={i} onClick={() => { setPickedAg(f.ag); setView('inspect'); }} style={{
                      padding: '8px 10px', borderBottom: '1px solid var(--rule-soft)', cursor: 'pointer',
                      display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                    }}>
                      <div>
                        <Mono size={10}>{f.ag.id}</Mono>
                        <div style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 1 }}>
                          {f.flags.map(x => x.msg).join(' · ')}
                        </div>
                      </div>
                      <RiskBadge risk={f.highest === 'high' ? 'high' : f.highest === 'med' ? 'med' : 'low'}/>
                    </div>
                  ))}
                  {result.flagged.length === 0 && (
                    <div style={{ padding: 20, textAlign: 'center', color: 'var(--ink-3)', fontSize: 11 }}>
                      All documents pass extraction checks.
                    </div>
                  )}
                </div>
              </div>
            </div>
          </div>
        )}

        {/* SEMANTIC SEARCH */}
        {view === 'search' && (
          <div>
            <form onSubmit={(e) => { e.preventDefault(); setActiveQ(query); }} style={{ marginBottom: 18 }}>
              <input
                type="text" value={query} onChange={(e) => setQuery(e.target.value)}
                placeholder='e.g. "MFN clauses in sub-publishing deals expiring 2026" or "advances over $50,000 in Latin America"'
                style={{
                  width: '100%', padding: '14px 18px', fontSize: 14,
                  border: '1px solid var(--rule)', background: 'var(--paper)', color: 'var(--ink)',
                  outline: 'none', fontFamily: 'inherit',
                }}
              />
              <div style={{ display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap' }}>
                {[
                  'MFN clauses in publishing deals',
                  'audit window 2 years',
                  'Helado Negro royalty',
                  'sync deals A24',
                  'expiring 2026',
                  'IPI 00578913241',
                ].map(s => (
                  <button key={s} type="button" onClick={() => { setQuery(s); setActiveQ(s); }}
                    className="ff-mono upper" style={{
                      fontSize: 10, padding: '4px 9px',
                      background: 'transparent', color: 'var(--ink-2)',
                      border: '1px solid var(--rule)', cursor: 'pointer',
                    }}>{s}</button>
                ))}
              </div>
            </form>

            {activeQ && (
              <div style={{ padding: '12px 16px', background: 'var(--bg-2)', marginBottom: 16 }}>
                <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 6 }}>
                  QUERY ANALYSIS
                </Mono>
                <div style={{ fontSize: 13, marginBottom: 8 }}>
                  <HighlightedText text={activeQ} entities={queryEntities}/>
                </div>
                {queryClauses.length > 0 && (
                  <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
                    {queryClauses.slice(0, 5).map(c => <ClauseChip key={c.k} c={c}/>)}
                  </div>
                )}
              </div>
            )}

            {activeQ && (
              <div>
                <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 10 }}>
                  {searchResults.length} RESULT{searchResults.length === 1 ? '' : 'S'}
                </Mono>
                {searchResults.map(r => (
                  <div key={r.id} onClick={() => {
                    if (r.kind === 'agreement' && r.ag) { setPickedAg(r.ag); setView('inspect'); }
                  }} style={{
                    padding: '14px 18px', border: '1px solid var(--rule)', marginBottom: 8, cursor: 'pointer',
                  }}>
                    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 }}>
                      <div>
                        <Mono upper size={9} color="var(--ink-3)" style={{ marginRight: 8 }}>{r.kind}</Mono>
                        <span style={{ fontSize: 13, fontWeight: 500 }}>{r.title}</span>
                        <Mono size={10} color="var(--ink-3)" style={{ marginLeft: 8 }}>· {r.sub}</Mono>
                      </div>
                      <Mono size={10} color="var(--ink-3)">score {r.score.toFixed(1)}</Mono>
                    </div>
                    <div style={{ fontSize: 11, color: 'var(--ink-2)', lineHeight: 1.5 }}>
                      {r.snippet}
                    </div>
                    {r.matchTokens.length > 0 && (
                      <div style={{ marginTop: 6, display: 'flex', gap: 4, flexWrap: 'wrap' }}>
                        {r.matchTokens.map((t, i) => (
                          <Mono key={i} size={9} color="var(--ink-3)" upper style={{ background: 'var(--bg-2)', padding: '1px 5px' }}>
                            {t}
                          </Mono>
                        ))}
                      </div>
                    )}
                  </div>
                ))}
                {searchResults.length === 0 && (
                  <div style={{ padding: 30, textAlign: 'center', color: 'var(--ink-3)', fontSize: 12 }}>
                    No matches. Try simpler keywords.
                  </div>
                )}
              </div>
            )}
            {!activeQ && (
              <div style={{ padding: 40, textAlign: 'center', color: 'var(--ink-3)', fontSize: 12 }}>
                Enter a query to search across {result.docCount} documents.
              </div>
            )}
          </div>
        )}

        {/* INSPECT DOCUMENT */}
        {view === 'inspect' && (
          <div>
            <div style={{ marginBottom: 16 }}>
              <select
                value={pickedAg ? pickedAg.id : ''}
                onChange={(e) => setPickedAg(ags.find(a => a.id === e.target.value))}
                style={{
                  width: 400, padding: '10px 14px', fontSize: 12,
                  border: '1px solid var(--rule)', background: 'var(--paper)', color: 'var(--ink)',
                  fontFamily: 'inherit',
                }}
              >
                <option value="">— pick an agreement —</option>
                {ags.slice(0, 100).map(a => (
                  <option key={a.id} value={a.id}>{a.id} · {a.kind} · {a.a} ↔ {a.b}</option>
                ))}
              </select>
            </div>

            {pickedAg && pickedExtract && (
              <NLPDocCard ag={pickedAg} extract={pickedExtract}/>
            )}
            {!pickedAg && (
              <div style={{ padding: 40, textAlign: 'center', color: 'var(--ink-3)', fontSize: 12 }}>
                Pick an agreement to see entities, clauses, facts, and risk flags.
              </div>
            )}
          </div>
        )}

        {/* AD-HOC PARSE */}
        {view === 'adhoc' && (
          <div>
            <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 10 }}>
              PASTE TEXT TO PARSE — ENTITIES · CLAUSES · FACTS · SENTIMENT
            </Mono>
            <textarea
              value={adhoc} onChange={(e) => setAdhoc(e.target.value)}
              placeholder='Paste any contract clause, email, or memo text here…'
              style={{
                width: '100%', minHeight: 160, padding: 14, fontSize: 13,
                border: '1px solid var(--rule)', background: 'var(--paper)', color: 'var(--ink)',
                fontFamily: 'inherit', outline: 'none', resize: 'vertical',
              }}
            />
            <div style={{ display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap' }}>
              <button onClick={() => setAdhoc('Licensee shall pay an advance of US$75,000 against royalties of 15% of net receipts. Term: 5 years from 2024-01-01. Territory: World excluding Japan. Most-favored-nations protection applies. Audit rights for 2 years from each statement. Governing law: New York.')}
                className="ff-mono upper" style={{ fontSize: 10, padding: '4px 9px', background: 'transparent', color: 'var(--ink-2)', border: '1px solid var(--rule)', cursor: 'pointer' }}>
                Sample · Sync deal
              </button>
              <button onClick={() => setAdhoc('Without prejudice to my client\'s rights, we hereby reserve all claims regarding the underpayment of mechanical royalties for Q3 2024 (statement period ending 2024-09-30). The unauthorized use of the controlled composition rate of 75% is in breach of section 4.2.')}
                className="ff-mono upper" style={{ fontSize: 10, padding: '4px 9px', background: 'transparent', color: 'var(--ink-2)', border: '1px solid var(--rule)', cursor: 'pointer' }}>
                Sample · Dispute letter
              </button>
              <button onClick={() => setAdhoc('Thanks for the prompt response. We are happy to accommodate the revised payment schedule and look forward to closing this in time for the Q1 2026 statement run.')}
                className="ff-mono upper" style={{ fontSize: 10, padding: '4px 9px', background: 'transparent', color: 'var(--ink-2)', border: '1px solid var(--rule)', cursor: 'pointer' }}>
                Sample · Cooperative email
              </button>
            </div>

            {adhocAnalysis && adhoc && (
              <div style={{ marginTop: 22, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 22 }}>
                <div>
                  <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 8 }}>
                    HIGHLIGHTED TEXT · {adhocAnalysis.entities.length} ENTITIES
                  </Mono>
                  <div style={{ padding: '14px 16px', border: '1px solid var(--rule)', fontSize: 13, lineHeight: 1.7 }}>
                    <HighlightedText text={adhoc} entities={adhocAnalysis.entities}/>
                  </div>

                  <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginTop: 18, marginBottom: 8 }}>
                    SENTIMENT
                  </Mono>
                  <div style={{ padding: '12px 14px', border: '1px solid var(--rule)' }}>
                    <SentimentBar sent={adhocAnalysis.sentiment}/>
                    <div style={{ fontSize: 11, color: 'var(--ink-2)', marginTop: 6 }}>
                      {adhocAnalysis.sentiment.pos} positive · {adhocAnalysis.sentiment.neg} negative signals
                    </div>
                  </div>
                </div>
                <div>
                  <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 8 }}>
                    CLAUSES IDENTIFIED · {adhocAnalysis.clauses.length}
                  </Mono>
                  <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 18 }}>
                    {adhocAnalysis.clauses.map(c => <ClauseChip key={c.k} c={c}/>)}
                    {adhocAnalysis.clauses.length === 0 && <Mono size={11} color="var(--ink-3)">No clauses recognized.</Mono>}
                  </div>

                  <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 8 }}>
                    EXTRACTED FACTS
                  </Mono>
                  <FactsTable facts={adhocAnalysis.facts}/>
                </div>
              </div>
            )}
          </div>
        )}
      </div>
    );
  }

  // ─── DOC CARD (full agreement extract) ─────────────────────────
  function NLPDocCard({ ag, extract }) {
    const ex = extract;
    const fullText = [
      ag.id + ' · ' + (ag.kind || ''),
      'Parties: ' + (ag.parties || []).map(p => p.role + ' — ' + p.name + (p.ipi ? ' (IPI ' + p.ipi + ')' : '') + (p.share ? ' — ' + p.share + '%' : '')).join('; '),
      'Territory: ' + (ag.territory || '—'),
      'Term: ' + (ag.start || '—') + ' to ' + (ag.end || '—'),
      'Share: ' + (ag.share || '—'),
      'Jurisdiction: ' + (ag.jurisdiction || '—'),
      'Dispute resolution: ' + (ag.disputeResolution || '—'),
      ag.shareNotes ? 'Notes: ' + ag.shareNotes : '',
      ag.advanceTerms ? 'Advance: ' + ag.advanceTerms : '',
      ag.specialTerms ? 'Special: ' + ag.specialTerms : '',
    ].filter(Boolean).join('\n');

    return (
      <div>
        <div style={{ display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: 22 }}>
          {/* Left — highlighted text */}
          <div>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8 }}>
              <Mono upper size={9} color="var(--ink-3)">DOCUMENT TEXT · {ex.entities.length} ENTITIES</Mono>
              <Mono size={10} color="var(--ink-3)">{ag.id}</Mono>
            </div>
            <div style={{ padding: '16px 18px', border: '1px solid var(--rule)', fontSize: 12, lineHeight: 1.7, whiteSpace: 'pre-wrap' }}>
              <HighlightedText text={fullText} entities={extractEntities(fullText)}/>
            </div>

            <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginTop: 22, marginBottom: 8 }}>
              ENTITY BREAKDOWN
            </Mono>
            <EntityList entities={extractEntities(fullText)}/>
          </div>

          {/* Right — facts, clauses, risk */}
          <div>
            <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 8 }}>
              CLAUSES IDENTIFIED · {ex.clauses.length}
            </Mono>
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 18 }}>
              {ex.clauses.map(c => <ClauseChip key={c.k} c={c}/>)}
              {ex.clauses.length === 0 && <Mono size={11} color="var(--ink-3)">No clauses recognized.</Mono>}
            </div>

            <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 8 }}>
              EXTRACTED FACTS
            </Mono>
            <FactsTable facts={ex.facts}/>

            {ex.facts.flags.length > 0 && (
              <>
                <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginTop: 18, marginBottom: 8 }}>
                  EXTRACTION FLAGS · {ex.facts.flags.length}
                </Mono>
                {ex.facts.flags.map((f, i) => (
                  <div key={i} style={{ padding: '8px 12px', borderLeft: '3px solid ' + (f.sev === 'high' ? '#a32a18' : f.sev === 'med' ? '#d4881f' : '#7a8590'), background: 'var(--bg-2)', marginBottom: 5, fontSize: 11 }}>
                    <span style={{ color: 'var(--ink-2)' }}>{f.msg}</span>
                  </div>
                ))}
              </>
            )}

            <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginTop: 18, marginBottom: 8 }}>
              SENTIMENT
            </Mono>
            <div style={{ padding: '12px 14px', border: '1px solid var(--rule)' }}>
              <SentimentBar sent={ex.sentiment}/>
            </div>
          </div>
        </div>
      </div>
    );
  }

  function FactsTable({ facts }) {
    const rows = [];
    if (facts.parties && facts.parties.length) rows.push({ k: 'Parties', v: facts.parties.length + ' (' + facts.parties.map(p => p.role).join(', ') + ')' });
    if (facts.royaltyPct != null) rows.push({ k: 'Royalty %', v: facts.royaltyPct + '%' });
    if (facts.advanceUsd != null) rows.push({ k: 'Advance', v: '$' + facts.advanceUsd.toLocaleString() });
    if (facts.termYears != null) rows.push({ k: 'Term', v: facts.termYears + ' years' });
    if (facts.auditWindow != null) rows.push({ k: 'Audit window', v: facts.auditWindow + ' years' });
    if (facts.mfn != null) rows.push({ k: 'MFN', v: facts.mfn ? 'Yes' : 'No' });
    if (facts.territoryScope) rows.push({ k: 'Territory', v: facts.territoryScope });
    if (facts.governingLaw) rows.push({ k: 'Governing law', v: facts.governingLaw });
    if (facts.keyDates && facts.keyDates.start) rows.push({ k: 'Effective', v: facts.keyDates.start });
    if (facts.keyDates && facts.keyDates.end) rows.push({ k: 'Expires', v: facts.keyDates.end });
    if (rows.length === 0) return <div style={{ padding: 14, color: 'var(--ink-3)', fontSize: 11, fontStyle: 'italic' }}>No structured facts extracted.</div>;
    return (
      <div style={{ border: '1px solid var(--rule)' }}>
        {rows.map((r, i) => (
          <div key={i} style={{ display: 'grid', gridTemplateColumns: '130px 1fr', padding: '8px 12px', borderBottom: i < rows.length - 1 ? '1px solid var(--rule-soft)' : 0 }}>
            <Mono upper size={9} color="var(--ink-3)">{r.k}</Mono>
            <span style={{ fontSize: 12 }}>{r.v}</span>
          </div>
        ))}
      </div>
    );
  }

  function EntityList({ entities }) {
    const groups = {};
    entities.forEach(e => {
      if (!groups[e.kind]) groups[e.kind] = { label: e.label, color: e.color, vals: new Set() };
      groups[e.kind].vals.add(e.value);
    });
    const list = Object.entries(groups);
    if (!list.length) return <div style={{ padding: 14, color: 'var(--ink-3)', fontSize: 11 }}>No entities extracted.</div>;
    return (
      <div>
        {list.map(([k, g]) => (
          <div key={k} style={{ padding: '8px 12px', borderBottom: '1px solid var(--rule-soft)', display: 'grid', gridTemplateColumns: '80px 1fr', gap: 12, alignItems: 'flex-start' }}>
            <Mono upper size={9} color={g.color}>{g.label}</Mono>
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
              {[...g.vals].slice(0, 12).map((v, i) => (
                <span key={i} style={{ fontFamily: 'var(--ff-mono, monospace)', fontSize: 10, padding: '2px 6px', background: g.color + '15', color: g.color, border: '1px solid ' + g.color + '33' }}>{v}</span>
              ))}
              {g.vals.size > 12 && <Mono size={10} color="var(--ink-3)">+{g.vals.size - 12} more</Mono>}
            </div>
          </div>
        ))}
      </div>
    );
  }

  function Cell({ label, value, sub, tone }) {
    return (
      <div style={{ padding: '18px 22px', borderRight: '1px solid var(--rule)' }}>
        <Mono upper size={9} color="var(--ink-3)">{label}</Mono>
        <div className="ff-display" style={{ fontSize: 24, fontWeight: 600, letterSpacing: '-0.02em', marginTop: 4, color: tone || 'var(--ink)' }}>{value}</div>
        <div style={{ fontSize: 11, color: 'var(--ink-2)', marginTop: 3 }}>{sub}</div>
      </div>
    );
  }

  // ─── EMBED: per-agreement card (use on agreement-detail) ──────
  function NLPAgreementCard({ ag }) {
    const ex = _M(() => ag ? extract(ag) : null, [ag && ag.id]);
    if (!ex) return null;
    return (
      <div style={{ border: '1px solid var(--rule)', padding: '16px 18px' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 12 }}>
          <Mono upper size={9} color="var(--ink-3)">NLP EXTRACT</Mono>
          <SentimentBar sent={ex.sentiment}/>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18 }}>
          <div>
            <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 6 }}>CLAUSES</Mono>
            <div style={{ display: 'flex', gap: 5, flexWrap: 'wrap' }}>
              {ex.clauses.slice(0, 8).map(c => <ClauseChip key={c.k} c={c}/>)}
            </div>
          </div>
          <div>
            <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 6 }}>FACTS</Mono>
            <FactsTable facts={ex.facts}/>
          </div>
        </div>
      </div>
    );
  }

  window.MLNLPTab = MLNLPTab;
  window.NLPAgreementCard = NLPAgreementCard;

  console.log('[NLPEngine] loaded · ' + CLAUSE_TYPES.length + ' clause types · ' + Object.keys(ENTITY_PATTERNS).length + ' entity patterns');
})();
