/* BWolf Data MCP — landing app */
const { useState, useEffect, useRef } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "light",
  "accent": "#2a6f4a",
  "showCharts": true
}/*EDITMODE-END*/;

/* ---------- i18n ---------- */
const STR = {
  en: {
    navCaps: "Capabilities", navTools: "Tools", navAsk: "Ask", navSecurity: "Security", navConnect: "Connect",
    navStatus: "Live · Asia/Shanghai",
    navDemo: "Agent demo",
    eyebrow: "MCP server · Bilibili livestream signals",
    h1Pre: "Explore livestream signals with your ", h1Ital: "AI agent", h1Post: ".",
    lede: "Rooms, sessions, online counts, title changes, danmaku, gifts, SuperChats, followers, and fan medals — exposed as safe, read-only MCP tools you can query in natural language.",
    primary: "Connect MCP",
    secondary: "View tools",
    tryDemo: "Try the agent demo",
    statusTitle: "Collection status",
    statusRows: [
      { label: "Rooms monitored",  sub: "active in last 24h",   value: "248" },
      { label: "Live sessions",    sub: "currently streaming",   value: "37" },
      { label: "Last sample",      sub: "viewer & danmaku poll", value: "12s ago" },
      { label: "Database",         sub: "postgres / hyperdrive", value: "Connected" },
    ],
    s1: { tag: "Capabilities", title: "Seven streams of livestream signal.",
          desc: "Everything is collected automatically by the BWolf monitoring pipeline at a steady cadence. Nothing here is captured from video, frames, or human review." },
    s2: { tag: "MCP Tools", title: "Eight read-only tools.",
          desc: "Each returns structured rows your agent can reason over. Inputs are validated, results are paginated, and every call is observation-only." },
    s3: { tag: "Ask the agent", title: "Natural-language questions, real answers.",
          desc: "Click any example to drop it straight into the live agent demo. The agent picks the right bwolf_* tool, runs it, and replies with grounded data." },
    s4: { tag: "Security & scope", title: "Observation-only by construction.",
          desc: "The endpoint is built so even an autonomous agent cannot reach beyond aggregated monitoring data. There is no path to write, no path to private media, and no labelled content." },
    s5: { tag: "Connect", title: "Wire it into your client.",
          desc: ["Any MCP-compatible client works the same way. Below is a Claude Desktop config using ", "mcp-remote", "."] },
    notIncluded: "Not exposed by this MCP",
    notIncludedItems: ["screenshots", "visual capture", "manual annotation", "labeling", "content classification", "private media storage"],
    footerLine: "BWolf Data MCP · Cloudflare Pages + Functions · Postgres via Hyperdrive · Asia/Shanghai",
    legendOk: "operational", legendWarn: "degraded", legendInfo: "informational",
    capsLabel: "Live data",
    caps: [
      { tag: "ROOMS",     title: "Room Discovery",         desc: "Every monitored room with platform id, owner, and last-seen state.", footer: "248 rooms · 37 live now" },
      { tag: "SESSIONS",  title: "Session History",        desc: "Start and end of every stream, with peak online and total duration.", footer: "Last 30 days · 1.2k sessions" },
      { tag: "VIEWERS",   title: "Viewer Trends",          desc: "Real online and popularity polled at 30-second cadence per room.",     footer: "30s sampling · 14d retention" },
      { tag: "TITLES",    title: "Title Changes",          desc: "Stream title revisions with timestamps, in order.",                    footer: "Append-only log" },
      { tag: "DANMAKU",   title: "Danmaku Activity",       desc: "Per-minute danmaku counts and bursts across the session window.",      footer: "≈ 4.3M msgs / day" },
      { tag: "REVENUE",   title: "Gifts & SuperChats",     desc: "Gift events, SuperChat counts, and Guard purchase activity, aggregated.", footer: "Counts only · no PII" },
      { tag: "FANS",      title: "Followers & Medals",     desc: "Follower counts and fan medal totals tracked over time, per room.",    footer: "Daily snapshot" },
      { tag: "QUERY",     title: "Read-only SQL",          desc: "Constrained SELECT against allow-listed views — for power users.",     footer: "Capped & parameter-bound" },
    ],
    qHeader: "Try one of these",
    questions: [
      "Which monitored rooms were live recently?",
      "Show viewer trends for 血狼破军 during the last stream.",
      "When did the stream title change?",
      "Summarize danmaku and SuperChat activity for this session.",
      "Compare follower growth across rooms this week.",
      "Which sessions had the highest real online count?",
    ],
    secCards: [
      { title: "Read-only access",         desc: "The Postgres role can only SELECT from a curated set of views. No INSERT, UPDATE, DELETE, DDL, or function execution exists in any tool." },
      { title: "Open read-only access",     desc: "No authentication required. The endpoint is intentionally public — the data is aggregated monitoring signals with no PII and no write path." },
      { title: "Hyperdrive-backed Postgres", desc: "The Pages Function connects through Cloudflare Hyperdrive, pooling at the edge for low-latency, predictable reads." },
      { title: "Automatic collection only",desc: "Only data captured by the BWolf monitoring pipeline is exposed. No screenshots, no capture frames, no manual annotation, no content labelling." },
    ],
    epDescs: [
      "Streamable HTTP MCP transport for AI agents.",
      "Pages Function liveness probe — version & uptime.",
      "Hyperdrive + Postgres reachability and latency.",
    ],
    demoHead: "Live agent demo",
    demoMeta: "deepseek-v4-flash · Cloudflare AI Gateway · structured bwolf_* tools",
    demoIntro: "Ask a question about Bilibili livestream data. The agent has the eight bwolf_* tools and will pick the right one.",
    demoPlaceholder: "Ask the agent…",
    demoSend: "Send",
    demoThinking: "Thinking",
    demoNote: "Demo · model calls run server-side through Cloudflare AI Gateway. No provider token is exposed to the browser.",
    demoWarn: ["⚠ AI responses may be inaccurate.", " deepseek-v4-flash is a lightweight model with a high error rate — numbers, names, and dates can be wrong. Always verify against the source data."],
  },
  zh: {
    navCaps: "能力", navTools: "工具", navAsk: "提问", navSecurity: "安全", navConnect: "接入",
    navStatus: "在线 · Asia/Shanghai",
    navDemo: "在线 Agent 演示",
    eyebrow: "MCP 服务 · 哔哩哔哩直播信号",
    h1Pre: "用你的 ", h1Ital: "AI Agent", h1Post: " 探索直播数据。",
    lede: "房间、场次、在线人数、标题变更、弹幕、礼物、SuperChat、关注与粉丝勋章 — 全部以安全、只读的 MCP 工具开放，可用自然语言查询。",
    primary: "接入 MCP",
    secondary: "查看工具",
    tryDemo: "试用在线 Agent",
    statusTitle: "采集状态",
    statusRows: [
      { label: "已监控房间",  sub: "近 24 小时活跃", value: "248" },
      { label: "直播场次",    sub: "正在直播中",     value: "37" },
      { label: "最近一次采样", sub: "在线与弹幕轮询", value: "12 秒前" },
      { label: "数据库",      sub: "postgres / hyperdrive", value: "已连接" },
    ],
    s1: { tag: "能力", title: "七路直播信号。",
          desc: "全部数据由 BWolf 监控流水线以稳定频率自动采集 — 不来自视频、画面截取或人工审核。" },
    s2: { tag: "MCP 工具", title: "八个只读工具。",
          desc: "每个工具返回字段稳定的结构化数据；输入会被校验，结果会分页。所有调用都是仅观测的。" },
    s3: { tag: "向 Agent 提问", title: "用自然语言问出真实数据。",
          desc: "点击任意示例即可在 Agent 演示中提问。Agent 会自动选择合适的 bwolf_* 工具并基于数据作答。" },
    s4: { tag: "安全与范围", title: "由结构本身保证仅观测。",
          desc: "端点的设计确保即使是自主 Agent 也无法越界 — 没有任何写入路径，没有私有媒体路径，也没有任何带标注的内容。" },
    s5: { tag: "接入", title: "接入到你的客户端。",
          desc: ["任意 MCP 客户端的接入方式相同。下方是使用 ", "mcp-remote", " 的 Claude Desktop 配置。"] },
    notIncluded: "不在此 MCP 暴露范围内",
    notIncludedItems: ["截图", "画面捕获", "人工标注", "打标签", "内容分类", "私有媒体存储"],
    footerLine: "BWolf Data MCP · Cloudflare Pages + Functions · 通过 Hyperdrive 接入 Postgres · Asia/Shanghai",
    legendOk: "运行中", legendWarn: "降级", legendInfo: "提示",
    capsLabel: "实时数据",
    caps: [
      { tag: "房间",   title: "房间发现",           desc: "已监控房间的平台 ID、主播与最近状态。",         footer: "248 个房间 · 37 个在播" },
      { tag: "场次",   title: "场次历史",           desc: "每场直播的开播与结束、峰值在线与时长。",       footer: "近 30 天 · 1.2k 场" },
      { tag: "在线",   title: "在线趋势",           desc: "每个房间以 30 秒频率采集真实在线与人气值。",   footer: "30 秒采样 · 14 天保留" },
      { tag: "标题",   title: "标题变更",           desc: "按时间顺序记录的标题变更日志。",                footer: "仅追加" },
      { tag: "弹幕",   title: "弹幕活动",           desc: "按分钟统计的弹幕数与突发情况。",                footer: "≈ 430 万条/天" },
      { tag: "营收",   title: "礼物与 SuperChat",   desc: "礼物事件、SC 数量与大航海购买的聚合统计。",    footer: "仅计数 · 无 PII" },
      { tag: "粉丝",   title: "关注与勋章",         desc: "随时间记录每个房间的关注数与粉丝勋章总数。",    footer: "每日快照" },
      { tag: "查询",   title: "只读 SQL",           desc: "面向白名单视图的受约束 SELECT — 给进阶用户。",  footer: "带上限与参数绑定" },
    ],
    qHeader: "试试这些问题",
    questions: [
      "最近哪些房间在直播？",
      "显示血狼破军最近一场的在线趋势。",
      "这场直播什么时候改过标题？",
      "汇总这场的弹幕与 SuperChat 活动。",
      "比较这周各房间的关注数增长。",
      "哪些场次的真实在线人数最高？",
    ],
    secCards: [
      { title: "只读访问",                desc: "Postgres 角色仅可在精选视图上 SELECT。所有工具均无 INSERT / UPDATE / DELETE / DDL / 函数执行能力。" },
      { title: "开放只读访问",              desc: "无需认证，端点公开开放。数据仅为聚合监控信号，不含任何 PII，也没有任何写入路径。" },
      { title: "Hyperdrive + Postgres",  desc: "Pages Function 通过 Cloudflare Hyperdrive 连接，于边缘进行连接池化，确保读延迟稳定。" },
      { title: "仅自动采集数据",          desc: "仅暴露 BWolf 监控流水线自动采集的数据 — 不包含截图、画面、人工标注或内容分类。" },
    ],
    epDescs: [
      "面向 AI Agent 的流式 HTTP MCP 传输。",
      "Pages Function 存活探针 — 返回版本与运行时长。",
      "Hyperdrive + Postgres 连通性与延迟。",
    ],
    demoHead: "在线 Agent 演示",
    demoMeta: "deepseek-v4-flash · Cloudflare AI Gateway · 结构化 bwolf_* 工具",
    demoIntro: "向 Agent 提问 Bilibili 直播相关问题。Agent 拥有八个 bwolf_* 工具，会自动挑选合适的工具。",
    demoPlaceholder: "向 Agent 提问…",
    demoSend: "发送",
    demoThinking: "思考中",
    demoNote: "演示 · 模型调用只在服务端通过 Cloudflare AI Gateway 执行，浏览器不会拿到模型 Token。",
    demoWarn: ["⚠ AI 生成内容可能有误。", " deepseek-v4-flash 是轻量模型，错误率较高 — 数字、名称、日期均可能不准确，请以实际数据源为准。"],
  },
};

const TOOLS = [
  { name: "bwolf_db_health",            desc: "Pings the database through Hyperdrive — latency and pool state.", desc_zh: "经 Hyperdrive 探测数据库 — 延迟与连接池状态。", args: [] },
  { name: "bwolf_list_rooms",           desc: "Monitored rooms with platform, owner, status, last-seen.",        desc_zh: "返回已监控的房间，含平台、主播、状态与最近一次见到。", args: ["platform?", "limit?"] },
  { name: "bwolf_get_room_current",     desc: "Current session, real online count, popularity for a single room.", desc_zh: "返回单个房间当前的场次、真实在线与人气。", args: ["room_id"] },
  { name: "bwolf_list_recent_sessions", desc: "Recent stream sessions with start/end, peak online, duration.",   desc_zh: "最近的直播场次，含开播/结束、峰值在线与时长。", args: ["since?", "limit?"] },
  { name: "bwolf_get_viewer_timeseries",desc: "Real online + popularity time series at 30-second cadence.",      desc_zh: "30 秒一档的真实在线与人气时间序列。", args: ["session_id", "metric?"] },
  { name: "bwolf_get_title_changes",    desc: "Ordered log of stream title changes during a session.",            desc_zh: "一场直播内有序的标题变更日志。", args: ["session_id"] },
  { name: "bwolf_interaction_summary",  desc: "Aggregated danmaku, gift, SuperChat, and Guard counts for a session.", desc_zh: "一场直播的弹幕、礼物、SuperChat、大航海聚合统计。", args: ["session_id"] },
  { name: "bwolf_readonly_sql",         desc: "Constrained SELECT against allow-listed views — capped.",          desc_zh: "面向白名单视图的受约束 SELECT，带上限。", args: ["query", "params?"] },
];

const ENDPOINTS = [
  { method: "POST", path: "/mcp",      status: "live" },
  { method: "GET",  path: "/health",   status: "live" },
  { method: "GET",  path: "/db-check", status: "live" },
];

const SEC_ICONS = [
  <svg className="sec-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><ellipse cx="12" cy="6" rx="8" ry="3"/><path d="M4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6"/><path d="M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"/></svg>,
  <svg className="sec-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="4" y="10" width="16" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg>,
  <svg className="sec-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12c3-4 6-4 9 0s6 4 9 0"/><circle cx="12" cy="12" r="9"/></svg>,
  <svg className="sec-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9"/><path d="m6 6 12 12"/></svg>,
];

/* ---------- helpers ---------- */
function useCopy() {
  const [copied, setCopied] = useState(null);
  function copy(text, key) {
    navigator.clipboard?.writeText(text);
    setCopied(key);
    setTimeout(() => setCopied(c => (c === key ? null : c)), 1400);
  }
  return [copied, copy];
}

function Bars({ seed = 1, n = 24 }) {
  const bars = Array.from({ length: n }, (_, i) => Math.abs(Math.sin((i + 1) * 0.7 + seed * 3.1)) * 0.7 + 0.2);
  return (
    <div className="cap-viz" aria-hidden>
      {bars.map((v, i) => <i key={i} style={{ height: `${Math.round(v * 32)}px`, opacity: 0.35 + v * 0.55 }} />)}
    </div>
  );
}

function Line({ seed = 1, n = 30 }) {
  const w = 220, h = 32;
  const pts = Array.from({ length: n }, (_, i) => {
    const x = (i / (n - 1)) * w;
    const y = h - (Math.sin(i * 0.5 + seed) * 0.5 + 0.55 + Math.sin(i * 0.17 + seed * 2) * 0.25) * h * 0.85 - 2;
    return [x, y];
  });
  const d = pts.map((p, i) => `${i ? "L" : "M"}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(" ");
  const area = `${d} L${w},${h} L0,${h} Z`;
  return (
    <div className="cap-viz line" aria-hidden>
      <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none">
        <path d={area} fill="var(--accent)" opacity="0.12" />
        <path d={d} fill="none" stroke="var(--accent)" strokeWidth="1.4" />
      </svg>
    </div>
  );
}

function Pulse({ seed = 1, n = 32 }) {
  const bars = Array.from({ length: n }, (_, i) => {
    const burst = (i > 8 && i < 14) || (i > 22 && i < 26) ? 0.9 : 0.25;
    return Math.min(1, burst + Math.abs(Math.sin(i * 1.3 + seed)) * 0.3);
  });
  return (
    <div className="cap-viz" aria-hidden>
      {bars.map((v, i) => <i key={i} style={{ height: `${Math.round(v * 32)}px`, opacity: 0.3 + v * 0.55 }} />)}
    </div>
  );
}

/* visual variants per cap */
const CAP_VIS = [
  { type: "chips", chips: ["live: 37", "248 total"] },
  { type: "bars", seed: 11 },
  { type: "line", seed: 2 },
  { type: "titles" },
  { type: "pulse", seed: 5 },
  { type: "stat", value: "¥ 12.4k", trail: "↑ 18% wk" },
  { type: "follow", value: "+ 2,341", trail: "this week" },
  { type: "sql" },
];

function CapVis({ idx, lang }) {
  const v = CAP_VIS[idx];
  if (!v) return null;
  if (v.type === "bars") return <Bars seed={v.seed} />;
  if (v.type === "line") return <Line seed={v.seed} />;
  if (v.type === "pulse") return <Pulse seed={v.seed} />;
  if (v.type === "chips") return (
    <div className="cap-chip-row">{v.chips.map(c => <span key={c} className="cap-chip">{c}</span>)}</div>
  );
  if (v.type === "stat") return (
    <div>
      <div className="cap-stat">{v.value} <small>past 24h</small></div>
      <div className="cap-trail">{v.trail}</div>
    </div>
  );
  if (v.type === "follow") return (
    <div>
      <div className="cap-stat">{v.value}</div>
      <div className="cap-trail">{v.trail}</div>
    </div>
  );
  if (v.type === "titles") return (
    <div style={{display:"flex", flexDirection:"column", gap:4, fontFamily:"var(--fm)", fontSize:11, color:"var(--ink-3)"}}>
      <div><span style={{color:"var(--accent)"}}>20:14</span> · {lang === "zh" ? "今晚跟大家聊聊…" : "Tonight let's chat about…"}</div>
      <div><span style={{color:"var(--accent)"}}>21:02</span> · {lang === "zh" ? "开始打排位了" : "Ranked grind, let's go"}</div>
      <div><span style={{color:"var(--accent)"}}>22:45</span> · {lang === "zh" ? "随便玩玩" : "Just messing around"}</div>
    </div>
  );
  if (v.type === "sql") return (
    <div style={{fontFamily:"var(--fm)", fontSize:11.5, color:"var(--ink-3)", lineHeight:1.5}}>
      <span style={{color:"var(--accent)"}}>SELECT</span> room_id, peak_online<br/>
      <span style={{color:"var(--accent)"}}>FROM</span> v_recent_sessions<br/>
      <span style={{color:"var(--accent)"}}>WHERE</span> started_at &gt; now() - '7 days'
    </div>
  );
  return null;
}

function Endpoint({ method, path, desc, status }) {
  return (
    <div className="ep-row">
      <span className={`ep-method ${method.toLowerCase()}`}>{method}</span>
      <span className="ep-path">{path}</span>
      <span className="ep-desc">{desc}</span>
      <span className="ep-status"><span className="d" /> {status === "live" ? "200 OK" : status}</span>
    </div>
  );
}

function Tool({ idx, name, desc, args }) {
  return (
    <div className="tool">
      <span className="tool-idx">{String(idx).padStart(2, "0")}</span>
      <div>
        <h4 className="tool-name">{name}</h4>
        <p className="tool-desc">{desc}</p>
        {args.length > 0 && (
          <div className="tool-args">{args.map(a => <span key={a}>{a}</span>)}</div>
        )}
      </div>
    </div>
  );
}

function ConfigBlock({ endpoint, onCopy, copied }) {
  const raw = `{
  "mcpServers": {
    "bwolf-data": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "${endpoint}"
      ]
    }
  }
}`;
  const lines = raw.split("\n").map((line, i) => {
    const tokens = []; let rest = line;
    const ws = rest.match(/^\s*/)[0]; tokens.push(<span key={`ws${i}`}>{ws}</span>); rest = rest.slice(ws.length);
    let cursor = 0;
    const re = /("(?:\\.|[^"\\])*")|([{}\[\],:])|(<[^>]+>)/g;
    let m;
    while ((m = re.exec(rest)) !== null) {
      if (m.index > cursor) tokens.push(<span key={`t${i}-${cursor}`}>{rest.slice(cursor, m.index)}</span>);
      if (m[1]) {
        const after = rest.slice(re.lastIndex).trimStart();
        const isKey = after.startsWith(":");
        tokens.push(<span key={`s${i}-${m.index}`} className={isKey ? "tk-key" : "tk-str"}>{m[1]}</span>);
      } else if (m[2]) tokens.push(<span key={`p${i}-${m.index}`} className="tk-pun">{m[2]}</span>);
      else if (m[3]) tokens.push(<span key={`x${i}-${m.index}`} className="tk-com">{m[3]}</span>);
      cursor = re.lastIndex;
    }
    if (cursor < rest.length) tokens.push(<span key={`r${i}`}>{rest.slice(cursor)}</span>);
    return <div key={i}>{tokens.length ? tokens : "\u00A0"}</div>;
  });
  return (
    <div className="code">
      <div className="code-head">
        <div className="file"><span className="lights"><i /><i /><i /></span><span>~/.config/Claude/claude_desktop_config.json</span></div>
        <button onClick={() => onCopy(raw, "cfg")}>{copied === "cfg" ? "Copied" : "Copy"}</button>
      </div>
      <pre>{lines}</pre>
    </div>
  );
}

/* ---------- Markdown renderer (headings, tables, bold, italic, inline code, lists) ---------- */
function parseInline(text) {
  if (!text) return '';
  const parts = [];
  let buf = '', i = 0, k = 0;
  const flush = () => { if (buf) { parts.push(buf); buf = ''; } };
  while (i < text.length) {
    if (text[i] === '*' && text[i+1] === '*') {
      const end = text.indexOf('**', i + 2);
      if (end !== -1) { flush(); parts.push(<strong key={k++}>{text.slice(i+2, end)}</strong>); i = end+2; continue; }
    }
    if (text[i] === '*') {
      const end = text.indexOf('*', i + 1);
      if (end !== -1 && end > i+1) { flush(); parts.push(<em key={k++}>{text.slice(i+1, end)}</em>); i = end+1; continue; }
    }
    if (text[i] === '`') {
      const end = text.indexOf('`', i + 1);
      if (end !== -1) { flush(); parts.push(<code key={k++}>{text.slice(i+1, end)}</code>); i = end+1; continue; }
    }
    buf += text[i]; i++;
  }
  flush();
  return parts.length === 1 && typeof parts[0] === 'string' ? parts[0] : parts;
}

function Markdown({ text }) {
  if (!text) return null;
  const lines = text.split('\n');
  const out = [];
  let i = 0, k = 0;

  const parseRow = l => l.split('|').slice(1, -1).map(c => c.trim());
  const isSep = row => row.every(c => /^[-: ]+$/.test(c));

  while (i < lines.length) {
    const line = lines[i].trim();
    if (!line) { i++; continue; }

    // Heading
    const hm = line.match(/^(#{1,6})\s+(.+)$/);
    if (hm) {
      const lvl = Math.min(hm[1].length, 4);
      const Tag = `h${lvl}`;
      out.push(<Tag key={k++}>{parseInline(hm[2])}</Tag>);
      i++; continue;
    }

    // Table
    if (line.startsWith('|')) {
      const tbl = [];
      while (i < lines.length && lines[i].trim().startsWith('|')) { tbl.push(lines[i]); i++; }
      const header = parseRow(tbl[0]);
      const bodyRows = tbl.slice(1).filter(l => !isSep(parseRow(l)));
      out.push(
        <div key={k++} className="md-table-wrap">
          <table className="md-table">
            <thead><tr>{header.map((h, j) => <th key={j}>{parseInline(h)}</th>)}</tr></thead>
            <tbody>{bodyRows.map((r, ri) => <tr key={ri}>{parseRow(r).map((c, ci) => <td key={ci}>{parseInline(c)}</td>)}</tr>)}</tbody>
          </table>
        </div>
      );
      continue;
    }

    // Unordered list
    if (/^[-*+] /.test(line)) {
      const items = [];
      while (i < lines.length && /^[-*+] /.test(lines[i].trim())) {
        items.push(lines[i].trim().replace(/^[-*+] /, ''));
        i++;
      }
      out.push(<ul key={k++}>{items.map((it, j) => <li key={j}>{parseInline(it)}</li>)}</ul>);
      continue;
    }

    // Paragraph
    const para = [];
    while (i < lines.length) {
      const l = lines[i].trim();
      if (!l || /^#{1,6} /.test(l) || l.startsWith('|') || /^[-*+] /.test(l)) break;
      para.push(l);
      i++;
    }
    if (para.length) {
      out.push(
        <p key={k++}>
          {para.map((l, li) => li === 0 ? parseInline(l) : [<br key={li} />, parseInline(l)])}
        </p>
      );
    }
  }
  return <>{out}</>;
}

function AgentDemo({ lang, initialQuestion, onClose }) {
  const t = STR[lang];
  const [msgs, setMsgs] = useState([{ role: "agent", intro: true }]);
  const [input, setInput] = useState("");
  const [busy, setBusy] = useState(false);
  const bodyRef = useRef(null);
  const askedInitialRef = useRef(false);
  const historyRef = useRef([]); // {role:"user"|"assistant", content:string}[] for multi-turn

  useEffect(() => {
    bodyRef.current?.scrollTo({ top: bodyRef.current.scrollHeight, behavior: "smooth" });
  }, [msgs, busy]);

  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  async function ask(q) {
    if (!q || busy) return;
    setInput("");
    setMsgs(m => [...m, { role: "user", text: q }]);
    setBusy(true);
    const priorHistory = [...historyRef.current];
    try {
      const res = await fetch("/api/demo-chat", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ message: q, locale: lang, history: priorHistory }),
      });
      const parsed = await res.json().catch(() => ({}));
      if (!res.ok) {
        throw new Error(parsed.message || parsed.error || `HTTP ${res.status}`);
      }
      historyRef.current.push({ role: "user", content: q });
      if (parsed.answer) historyRef.current.push({ role: "assistant", content: parsed.answer });
      setMsgs(m => [...m, { role: "agent", calls: parsed.tool_calls || [], answer: parsed.answer || "" }]);
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error);
      setMsgs(m => [...m, { role: "agent", calls: [], answer: lang === "zh" ? `演示暂不可用：${msg}` : `Demo unavailable: ${msg}` }]);
    } finally {
      setBusy(false);
    }
  }

  useEffect(() => {
    if (initialQuestion && !askedInitialRef.current) {
      askedInitialRef.current = true;
      ask(initialQuestion);
    }
  }, [initialQuestion]);

  return (
    <div className="demo-overlay" onClick={onClose}>
      <div className="demo-modal" onClick={e => e.stopPropagation()}>
        <header className="demo-head">
          <div className="title"><span className="live-dot" /> {t.demoHead}</div>
          <span className="meta">{t.demoMeta}</span>
          <button className="close" onClick={onClose} aria-label="Close">✕</button>
        </header>

        <div className="demo-warn">
          <span className="demo-warn-icon">⚠</span>
          <span><strong>{t.demoWarn[0]}</strong>{t.demoWarn[1]}</span>
        </div>

        <div className="demo-body" ref={bodyRef}>
          {msgs.map((m, i) => (
            <div className="demo-msg" key={i}>
              <div className={`demo-avatar ${m.role}`}>{m.role === "user" ? "U" : "B"}</div>
              <div className="demo-bubble">
                {m.intro && <p>{t.demoIntro}</p>}
                {m.text && <p>{m.text}</p>}
                {m.calls && m.calls.map((c, j) => (
                  <div key={j} className="tool-call">
                    <div className="tool-call-head">
                      <span><span className="tk-key">{c.tool}</span>(<span style={{color:"var(--code-com)"}}>{Object.keys(c.args || {}).map(k => `${k}: ${JSON.stringify(c.args[k])}`).join(", ")}</span>)</span>
                      <span className="ok">200 OK</span>
                    </div>
                    <pre>{c.result_summary}</pre>
                  </div>
                ))}
                {m.answer && <Markdown text={m.answer} />}
              </div>
            </div>
          ))}
          {busy && (
            <div className="demo-msg">
              <div className="demo-avatar agent">B</div>
              <div className="demo-bubble">
                <span style={{color:"var(--ink-3)", fontFamily:"var(--fm)", fontSize:12}}>{t.demoThinking}</span>
                <div className="demo-typing"><i /><i /><i /></div>
              </div>
            </div>
          )}
        </div>

        <div className="demo-foot">
          <div className="demo-suggestions">
            {t.questions.slice(0, 4).map(s => (
              <button key={s} onClick={() => ask(s)} disabled={busy}>{s}</button>
            ))}
          </div>
          <form className="demo-input" onSubmit={e => { e.preventDefault(); ask(input.trim()); }}>
            <input type="text" placeholder={t.demoPlaceholder} value={input} onChange={e => setInput(e.target.value)} disabled={busy} />
            <button type="submit" disabled={busy || !input.trim()}>{t.demoSend} →</button>
          </form>
          <div style={{fontFamily:"var(--fm)", fontSize:11, color:"var(--ink-4)"}}>{t.demoNote}</div>
        </div>
      </div>
    </div>
  );
}

/* ---------- main ---------- */
function App() {
  const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [copied, copy] = useCopy();
  const [now, setNow] = useState(() => new Date());
  const [lang, setLang] = useState("en");
  const [demo, setDemo] = useState(null); // null | { q?: string }
  const [status, setStatus] = useState(null);
  const t = STR[lang];

  useEffect(() => {
    const i = setInterval(() => setNow(new Date()), 30_000);
    return () => clearInterval(i);
  }, []);

  useEffect(() => {
    document.documentElement.dataset.theme = tweaks.theme;
    const map = {"#2a6f4a":"moss","#15161a":"ink","#1f4fa8":"cobalt","#a8451f":"rust"};
    document.documentElement.dataset.accent = map[tweaks.accent] || "moss";
  }, [tweaks.theme, tweaks.accent]);

  useEffect(() => { document.body.style.overflow = demo ? "hidden" : ""; }, [demo]);

  useEffect(() => {
    let cancelled = false;
    async function loadStatus() {
      try {
        const res = await fetch("/api/public-status");
        const data = await res.json();
        if (!cancelled) setStatus(data);
      } catch {
        if (!cancelled) setStatus(null);
      }
    }
    loadStatus();
    const i = setInterval(loadStatus, 60_000);
    return () => { cancelled = true; clearInterval(i); };
  }, []);

  const baseUrl = "https://mcp.bwolf.work";
  const mcpUrl  = `${baseUrl}/mcp`;
  const ts = now.toISOString().replace("T", " ").slice(0, 19) + "Z";
  const lastSample = status?.last_sample_ms
    ? (() => {
        const age = Math.max(0, Math.round((Date.now() - Number(status.last_sample_ms)) / 1000));
        if (age < 60) return lang === "zh" ? `${age} 秒前` : `${age}s ago`;
        const mins = Math.round(age / 60);
        if (mins < 60) return lang === "zh" ? `${mins} 分钟前` : `${mins}m ago`;
        const hours = Math.round(mins / 60);
        return lang === "zh" ? `${hours} 小时前` : `${hours}h ago`;
      })()
    : null;
  const statusRows = [
    {
      label: lang === "zh" ? "已监控房间" : "Rooms monitored",
      sub: lang === "zh" ? "enabled rooms" : "enabled rooms",
      value: status?.room_count ?? "—",
    },
    {
      label: lang === "zh" ? "正在直播" : "Live now",
      sub: lang === "zh" ? "active sessions" : "active sessions",
      value: status?.live_now ?? "—",
    },
    {
      label: lang === "zh" ? "最近一次采样" : "Last sample",
      sub: lang === "zh" ? "自动采集流水线" : "collection pipeline",
      value: lastSample ?? "—",
    },
    {
      label: lang === "zh" ? "数据库" : "Database",
      sub: "postgres / hyperdrive",
      value: status?.database === "connected" ? (lang === "zh" ? "已连接" : "Connected") : (lang === "zh" ? "不可用" : "Unavailable"),
    },
  ];

  return (
    <>
      <div className="wrap">
        <nav className="nav">
          <div className="brand">
            <span className="brand-mark">B</span>
            <div>
              <span className="brand-name">BWolf Data MCP</span>
              <span className="brand-sub">v1.0 · cloudflare pages</span>
            </div>
          </div>
          <div className="nav-meta">
            <a href="#tools">{t.navTools}</a>
            <a href="#ask">{t.navAsk}</a>
            <a href="#security">{t.navSecurity}</a>
            <a href="#connect">{t.navConnect}</a>
            <button className="lang-btn" onClick={() => setLang(lang === "en" ? "zh" : "en")} aria-label="Switch language">
              <span className={lang === "en" ? "on" : ""}>EN</span>
              <span className="sep">/</span>
              <span className={lang === "zh" ? "on" : ""}>中文</span>
            </button>
            <button className="demo-btn" onClick={() => setDemo({})}>
              <span className="live-dot" /> {t.navDemo}
            </button>
            <span className="nav-pill"><span className="dot" /> {t.navStatus}</span>
          </div>
        </nav>

        <section className="hero">
          <div>
            <p className="hero-eyebrow">{t.eyebrow}</p>
            <h1>{t.h1Pre}<span className="ital">{t.h1Ital}</span>{t.h1Post}</h1>
            <p className="lede">{t.lede}</p>
            <div className="hero-actions">
              <a href="#connect" className="btn primary">{t.primary} →</a>
              <a href="#tools" className="btn">{t.secondary}</a>
              <button className="btn ghost" onClick={() => setDemo({})}>
                <span className="live-dot" /> {t.tryDemo} →
              </button>
            </div>
          </div>

          <aside className="status" aria-label="Collection status">
            <header className="status-head">
              <span className="ttl">{t.statusTitle}</span>
              <span className="ts"><span className="pulse" />{ts}</span>
            </header>
            {statusRows.map((r, i) => (
              <div className="status-row" key={i}>
                <div className="lbl">{r.label}<small>{r.sub}</small></div>
                <div>
                  <span className={`status-pill ${i === 3 ? "" : (i === 2 ? "muted" : "")}`}>
                    <span className="d" style={i === 2 ? {background:"var(--ink-4)", boxShadow:"none"} : undefined}/>
                    {r.value}
                  </span>
                </div>
              </div>
            ))}
          </aside>
        </section>

        {/* tools */}
        <section className="block" id="tools">
          <div className="section-head">
            <div className="section-tag"><span className="num">02</span> {t.s2.tag}</div>
            <div>
              <h2 className="section-title">{t.s2.title}</h2>
              <p className="section-desc">{t.s2.desc}</p>
            </div>
          </div>
          <div className="tools">
            {TOOLS.map((tool, i) => (
              <Tool key={tool.name} idx={i + 1} name={tool.name} desc={lang === "zh" ? tool.desc_zh : tool.desc} args={tool.args} />
            ))}
          </div>
          <div style={{marginTop: 18}}>
            <div className="endpoints">
              {ENDPOINTS.map((e, i) => <Endpoint key={e.path} {...e} desc={t.epDescs[i]} />)}
            </div>
          </div>
        </section>

        {/* ask */}
        <section className="block" id="ask">
          <div className="section-head">
            <div className="section-tag"><span className="num">03</span> {t.s3.tag}</div>
            <div>
              <h2 className="section-title">{t.s3.title}</h2>
              <p className="section-desc">{t.s3.desc}</p>
            </div>
          </div>
          <div className="qlist">
            {t.questions.map((q, i) => (
              <button className="qcard" key={i} onClick={() => setDemo({ q })}>
                <span className="quote">“</span>
                <span>{q}</span>
                <span className="arr">→</span>
              </button>
            ))}
          </div>
        </section>

        {/* security */}
        <section className="block" id="security">
          <div className="section-head">
            <div className="section-tag"><span className="num">04</span> {t.s4.tag}</div>
            <div>
              <h2 className="section-title">{t.s4.title}</h2>
              <p className="section-desc">{t.s4.desc}</p>
            </div>
          </div>
          <div className="security">
            {t.secCards.map((s, i) => (
              <div className="sec-card" key={i}>
                {SEC_ICONS[i]}
                <h4 className="sec-title">{s.title}</h4>
                <p className="sec-desc">{s.desc}</p>
              </div>
            ))}
          </div>
        </section>

        {/* connect */}
        <section className="block" id="connect">
          <div className="section-head">
            <div className="section-tag"><span className="num">05</span> {t.s5.tag}</div>
            <div>
              <h2 className="section-title">{t.s5.title}</h2>
              <p className="section-desc">{t.s5.desc[0]}<code style={{fontFamily:"var(--fm)"}}>{t.s5.desc[1]}</code>{t.s5.desc[2]}</p>
            </div>
          </div>
          <ConfigBlock endpoint={mcpUrl} onCopy={copy} copied={copied} />
        </section>

        <footer>
          <div>{t.footerLine}</div>
          <div className="legend">
            <span><span style={{width:6,height:6,borderRadius:"50%",background:"var(--accent)",display:"inline-block"}} /> {t.legendOk}</span>
            <span><span style={{width:6,height:6,borderRadius:"50%",background:"var(--warn)",display:"inline-block"}} /> {t.legendWarn}</span>
            <span><span style={{width:6,height:6,borderRadius:"50%",background:"var(--ink-4)",display:"inline-block"}} /> {t.legendInfo}</span>
          </div>
        </footer>
      </div>

      {demo && <AgentDemo lang={lang} initialQuestion={demo.q} onClose={() => setDemo(null)} />}

      <TweaksPanel title="Tweaks">
        <TweakSection title="Appearance">
          <TweakRadio
            label="Theme"
            value={tweaks.theme}
            options={[{value:"light", label:"Light"}, {value:"dark", label:"Dark"}]}
            onChange={v => setTweak("theme", v)}
          />
          <TweakColor
            label="Accent"
            value={tweaks.accent}
            onChange={v => setTweak("accent", v)}
            options={["#2a6f4a", "#15161a", "#1f4fa8", "#a8451f"]}
          />
        </TweakSection>
        <TweakSection title="Capabilities">
          <TweakToggle
            label="Show charts"
            value={tweaks.showCharts}
            onChange={v => setTweak("showCharts", v)}
          />
        </TweakSection>
      </TweaksPanel>
    </>
  );
}

ReactDOM.createRoot(document.getElementById("app")).render(<App />);
