// ══════════════════════════════════════════════
//  Constants & utils
// ══════════════════════════════════════════════
const TOKEN_KEY = 'ag_token';
const THEME_KEY = 'ag_theme';

const DEFAULT_TEMPLATES = [
  { id:'xiaohongshu', name:'小红书风格', builtIn:true,
    system:'你是一位擅长写小红书爆款内容的创作者。风格：口语化、有情绪、多用 emoji、结构清晰（痛点+干货+金句）。',
    userTpl:'请根据以下选题写一篇小红书风格的文章：{topic}\n补充说明：{note}' },
  { id:'wechat', name:'公众号长文', builtIn:true,
    system:'你是一位专业的公众号作者，擅长深度内容创作。风格：有观点、有数据、有案例，结构完整（引言+正文+结尾）。',
    userTpl:'请根据以下选题写一篇公众号深度长文：{topic}\n补充说明：{note}' },
  { id:'news', name:'简讯 / 资讯', builtIn:true,
    system:'你是一位资讯编辑，擅长写简洁客观的信息类内容。风格：简洁、客观、重点突出，200-400 字。',
    userTpl:'请根据以下选题写一段资讯简讯：{topic}\n补充说明：{note}' },
  { id:'zhihu_gptplus', name:'知乎 gptplus', builtIn:true,
    system:`你是一位知乎内容创作者，擅长写兼具干货价值与软性推广的教程文章。写作风格参考知乎高赞回答：口语化、第一人称、有温度、像朋友在聊天，不像教程也不像广告。

## 推广信息
- 网站：gptplus.fun
- 服务：代充值（用户提供账号，平台代为升级，账号完全归用户所有）

## 核心卖点（从中挑选2-3个，自然嵌入正文，切勿堆砌罗列）
- 无需海外银行卡，支付宝/微信直接付人民币
- 价格比官方便宜，性价比高
- 操作简单，全程2分钟内完成，下单即充
- 大平台有保障，充值失败全额退款，包售后

## 文章结构（严格按此顺序写）

**第一段：开篇引导（最重要）**
开头必须同时做到三件事：
① 共鸣痛点：描述读者遇到的真实困境（国内信用卡不支持、折腾魔法环境失败、被各种教程绕晕等），让读者感到"说的就是我"
② 亮出结论 + 引导读到文末：第一段内必须出现类似"我已经找到了一个目前最稳的方案，放在文末了，可以直接拉到最后看"的句子，制造悬念，让读者有动力读完
③ 预告内容：一句话说清楚"下面先把几种主流方法都过一遍，帮你避坑，最后给出我现在在用的方案"

示例句式（根据选题自由发挥，不要照抄）：
- "折腾了好几次，踩了不少坑，最后找到了一个又稳又省心的方式——我把它放在文末了，等不及的可以直接划到最后。"
- "网上教程一堆，真正好用的就那么一个，我帮你筛好了放在文末，这篇先把几种方法的坑都说清楚。"

**正文：介绍2-4种主流方式**
每种方法都要写：原理 + 真实操作体验 + 局限/坑在哪（魔法自购、虚拟卡、苹果礼品卡、代充等）

**对比表格**
| 方法 | 价格 | 成功率 | 操作难度 | 封号风险 |
横向对比，帮读者一眼做决策

**推荐方案（自然植入）**
以"用了X个月"的亲测口吻引出 gptplus.fun，从卖点中挑选1-2个最贴合上下文的细节带出，语气是"朋友推荐"而非"广告"

**结尾**
一句话总结 + 再次自然提及网址，配上"有问题评论区聊"之类拉互动的话

## 语言禁忌
- 禁止出现"梯子"，统一用"魔法"替代
- 禁止出现"软广""推广""广告""恰饭"等暴露意图的字眼
- 禁止任何违规/违法表述

## 格式要求
- 字数：1000-1300字
- 适当使用 emoji 增加亲切感（不要滥用）
- 重点处可加粗，但不要每句都加粗`,
    userTpl:'请根据以下主题写一篇知乎风格的干货教程文章。\n\n要求：第一行用 Markdown 标题格式输出文章标题（即 # 标题文字），空一行后再写正文。\n\n主题：{topic}\n补充说明：{note}' },
];

const SLOGANS = [
  '🔥 一篇出10单，一百篇就出1000单',
  '⚡ 正在狠狠地生成中，财富正在涌来',
  '💰 一篇赚1000，一百篇就赚一百万',
  '🚀 多线程全速运转，效率拉满',
  '🔥 量变引起质变，狠狠地生成',
  '⚡ 内容就是资产，正在为你疯狂创作',
  '💰 批量出单，让钱自己来找你',
];

async function apiFetch(path, opts={}) {
  const token = localStorage.getItem(TOKEN_KEY);
  const res = await fetch(path, {
    ...opts,
    headers: { 'Content-Type':'application/json', ...(token?{Authorization:`Bearer ${token}`}:{}), ...opts.headers },
    body: opts.body ? JSON.stringify(opts.body) : undefined,
  });
  const data = await res.json();
  if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
  return data;
}

// Proxy stream — uses server-side platform API key (never exposed to client)
async function streamProxy({ messages, onChunk, signal, threadIdx }) {
  const token = localStorage.getItem(TOKEN_KEY);
  const res = await fetch('/api/proxy/stream', {
    method:'POST',
    headers:{'Content-Type':'application/json', Authorization:`Bearer ${token}`},
    body: JSON.stringify({ messages, ...(typeof threadIdx==='number'?{threadIdx}:{}) }),
    signal,
  });
  if (!res.ok) {
    let msg=`HTTP ${res.status}`;
    try { const d=await res.json(); msg=d?.error?.message||msg; } catch {}
    throw new Error(msg);
  }
  const reader=res.body.getReader(), dec=new TextDecoder();
  let buf='';
  while(true){
    const {done,value}=await reader.read();
    if(done) break;
    buf+=dec.decode(value,{stream:true});
    const lines=buf.split('\n'); buf=lines.pop();
    for(const line of lines){
      const t=line.trim();
      if(!t.startsWith('data:')) continue;
      const p=t.slice(5).trim();
      if(p==='[DONE]') return;
      try{
        const j=JSON.parse(p);
        if(j?.error) throw new Error(j.error);
        const c=j?.choices?.[0]?.delta?.content??j?.choices?.[0]?.text??'';
        if(c) onChunk(c);
      }catch(e){ if(e.message&&!e.message.startsWith('{')) throw e; }
    }
  }
}

async function streamContent({ apiConfig, messages, onChunk, signal }) {
  const url = apiConfig.baseUrl.replace(/\/$/,'') + '/chat/completions';
  const key = apiConfig.apiKey;
  const res = await fetch(url, {
    method:'POST',
    headers:{'Content-Type':'application/json', Authorization:`Bearer ${key}`},
    body: JSON.stringify({ model:apiConfig.model, messages, stream:true }),
    signal,
  });
  if (!res.ok) {
    let msg=`HTTP ${res.status}`;
    try { const d=await res.json(); msg=d?.error?.message||msg; } catch {}
    throw new Error(msg);
  }
  const reader=res.body.getReader(), dec=new TextDecoder();
  let buf='';
  while(true){
    const {done,value}=await reader.read();
    if(done) break;
    buf+=dec.decode(value,{stream:true});
    const lines=buf.split('\n'); buf=lines.pop();
    for(const line of lines){
      const t=line.trim();
      if(!t.startsWith('data:')) continue;
      const p=t.slice(5).trim();
      if(p==='[DONE]') return;
      try{ const j=JSON.parse(p); const c=j?.choices?.[0]?.delta?.content??j?.choices?.[0]?.text??''; if(c) onChunk(c); }catch{}
    }
  }
}

// Insert images after headings using image search API
async function insertImages(text) {
  const lines = text.split('\n');
  const result = [];
  for (const line of lines) {
    result.push(line);
    if (/^#{1,3}\s/.test(line)) {
      const kw = line.replace(/^#{1,3}\s+/, '').replace(/[*_`]/g, '').trim().slice(0, 60);
      try {
        const r = await apiFetch('/api/image/search?q=' + encodeURIComponent(kw));
        if (r.url) { result.push('', `![${r.alt || kw}](${r.url})`, ''); }
      } catch {}
    }
  }
  return result.join('\n');
}

function stripMd(md){
  return md.replace(/```[\s\S]*?```/g,m=>m.replace(/```\w*\n?/g,'').trim())
    .replace(/`([^`]+)`/g,'$1').replace(/^#{1,6}\s+/gm,'')
    .replace(/\*\*(.+?)\*\*/g,'$1').replace(/\*(.+?)\*/g,'$1')
    .replace(/!\[.*?\]\(.*?\)/g,'').replace(/\[(.+?)\]\(.*?\)/g,'$1')
    .replace(/^\s*[-*+]\s+/gm,'').replace(/^\s*\d+\.\s+/gm,'')
    .replace(/^\s*>\s*/gm,'').replace(/\n{3,}/g,'\n\n').trim();
}
function buildHTML(md,title){
  const body=marked.parse(md);
  return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>${title||'文章'}</title>
<style>body{font-family:-apple-system,system-ui,sans-serif;max-width:760px;margin:48px auto;padding:0 24px;line-height:1.8;color:#1a1816;background:#fff}h1{font-size:1.9em;font-weight:700;margin:0 0 .5em}h2{font-size:1.35em;font-weight:700;margin:1.5em 0 .5em}p{margin:.6em 0}ul,ol{margin:.5em 0 .5em 1.5em}blockquote{border-left:3px solid #c96a3a;padding-left:1em;color:#78716c;margin:.8em 0}code{background:#fdf0e8;padding:2px 6px;border-radius:4px;font-family:monospace;color:#c96a3a}pre{background:#fdf0e8;padding:1em;border-radius:8px;overflow-x:auto}a{color:#c96a3a}</style>
</head><body>${body}</body></html>`;
}
function download(content,filename){
  const a=Object.assign(document.createElement('a'),{href:URL.createObjectURL(new Blob([content],{type:'text/plain;charset=utf-8'})),download:filename});
  document.body.appendChild(a); a.click(); document.body.removeChild(a);
}
function safeName(t){ return (t||'文章').replace(/[\\/:*?"<>|]/g,'').replace(/\s+/g,'_').slice(0,50); }
function timeLeft(expiresAt){
  const ms=new Date(expiresAt)-Date.now();
  if(ms<=0) return '已过期';
  const h=Math.floor(ms/3600000), m=Math.floor((ms%3600000)/60000);
  return h>0?`${h}h ${m}m`:`${m}m`;
}
function fmtTime(iso){ return new Date(iso).toLocaleString('zh-CN',{month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'}); }
function workFilename(work){
  const b=safeName(work.title);
  return work.format==='md' ? b+'.md' : b+'.txt';
}
function workContent(work){
  if(work.format==='txt')  return stripMd(work.content);
  if(work.format==='html') return buildHTML(work.content, work.title);
  return work.content;
}

// ══════════════════════════════════════════════
//  Icons
// ══════════════════════════════════════════════
const Ic={
  Cog:   <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>,
  Eye:   <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>,
  EyeOff:<svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/></svg>,
  Check: <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.2}><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7"/></svg>,
  X:     <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>,
  Spin:  <svg width="15" height="15" className="animate-spin" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>,
  Stop:  <svg width="15" height="15" fill="currentColor" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>,
  Bolt:  <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>,
  Pen:   <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>,
  Doc:   <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>,
  Fmt:   <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h7"/></svg>,
  Edit:  <svg width="13" height="13" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>,
  Trash: <svg width="13" height="13" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>,
  Plus:  <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4"/></svg>,
  Copy:  <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>,
  Dl:    <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>,
  DlAll: <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1M8 12l4 4 4-4M12 4v12"/></svg>,
  Sun:   <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707M12 8a4 4 0 100 8 4 4 0 000-8z"/></svg>,
  Moon:  <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/></svg>,
  User:  <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>,
  Shield:<svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>,
  Lock:  <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>,
  Logout:<svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>,
  Img:   <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>,
  Zip:   <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"/></svg>,
  Books: <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>,
  List:  <svg width="15" height="15" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>,
  Clock: <svg width="13" height="13" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}><path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>,
  Chev:  (open)=><svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} style={{transition:'transform .18s',transform:open?'rotate(180deg)':'none'}}><path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7"/></svg>,
};

// ══════════════════════════════════════════════
//  Shared UI
// ══════════════════════════════════════════════
function Card({icon,title,badge,defaultOpen=true,children}){
  const [open,setOpen]=React.useState(defaultOpen);
  return(
    <div className="card">
      <div className="card-header" onClick={()=>setOpen(v=>!v)}>
        <span style={{color:'var(--accent)',display:'flex'}}>{icon}</span>
        <span style={{flex:1,fontSize:13,fontWeight:500,color:'var(--text-2)'}}>{title}</span>
        {badge&&<span className="tag">{badge}</span>}
        <span style={{color:'var(--text-4)'}}>{Ic.Chev(open)}</span>
      </div>
      {open&&<div style={{padding:'12px 14px 14px'}}>{children}</div>}
    </div>
  );
}
function FL({label,hint,req,children}){
  return(
    <div style={{display:'flex',flexDirection:'column',gap:5}}>
      {label&&(
        <div style={{display:'flex',alignItems:'center',gap:5}}>
          <label style={{fontSize:11,fontWeight:600,color:'var(--text-3)',textTransform:'uppercase',letterSpacing:'.04em'}}>{label}</label>
          {req&&<span style={{fontSize:10,color:'var(--accent)'}}>*</span>}
          {hint&&<span style={{fontSize:11,color:'var(--text-4)',marginLeft:'auto'}}>{hint}</span>}
        </div>
      )}
      {children}
    </div>
  );
}

// ══════════════════════════════════════════════
//  Auth screen
// ══════════════════════════════════════════════
function AuthScreen({onAuth}){
  const [user,setUser]=React.useState('');
  const [pass,setPass]=React.useState('');
  const [err,setErr]=React.useState('');
  const [loading,setLoading]=React.useState(false);
  const [showPw,setShowPw]=React.useState(false);

  const submit=async(e)=>{
    e.preventDefault();
    if(!user.trim()||!pass){setErr('请填写用户名和密码');return;}
    setLoading(true);setErr('');
    try{
      const data=await apiFetch('/api/login',{method:'POST',body:{username:user,password:pass}});
      localStorage.setItem(TOKEN_KEY,data.token);
      onAuth(data);
    }catch(e){setErr(e.message);}
    finally{setLoading(false);}
  };

  return(
    <div style={{height:'100vh',display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',padding:16}}>
      <div style={{marginBottom:28,textAlign:'center'}}>
        <div style={{width:56,height:56,borderRadius:16,background:'#0071e3',display:'flex',alignItems:'center',justifyContent:'center',margin:'0 auto 16px'}}>
          <svg width="24" height="24" fill="#fff" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
        </div>
        <h1 style={{fontSize:20,fontWeight:700,color:'var(--text-1)',marginBottom:4}}>南阳市择宸科技内容流</h1>
        <p style={{fontSize:13,color:'var(--text-3)'}}>AI 驱动的内容创作平台</p>
      </div>
      <div className="auth-card anim-fade">
        <h2 style={{fontSize:16,fontWeight:600,color:'var(--text-1)',marginBottom:6}}>登录</h2>
        <p style={{fontSize:12,color:'var(--text-4)',marginBottom:20}}>账号由管理员分配，如需开通请联系管理员</p>
        <form onSubmit={submit} style={{display:'flex',flexDirection:'column',gap:12}}>
          <FL label="用户名">
            <input className="input" value={user} onChange={e=>{setUser(e.target.value);setErr('');}} placeholder="输入用户名" autoFocus autoComplete="username"/>
          </FL>
          <FL label="密码">
            <div style={{position:'relative'}}>
              <input className="input" style={{paddingRight:36}} type={showPw?'text':'password'}
                value={pass} onChange={e=>{setPass(e.target.value);setErr('');}} placeholder="输入密码" autoComplete="current-password"/>
              <button type="button" onClick={()=>setShowPw(v=>!v)} style={{position:'absolute',right:10,top:'50%',transform:'translateY(-50%)',background:'none',border:'none',cursor:'pointer',color:'var(--text-3)',display:'flex'}}>
                {showPw?Ic.EyeOff:Ic.Eye}
              </button>
            </div>
          </FL>
          {err&&<div style={{display:'flex',alignItems:'center',gap:6,padding:'8px 10px',borderRadius:8,background:'var(--error-bg)',border:'1px solid color-mix(in srgb,var(--error) 30%,transparent)'}}><span style={{color:'var(--error)',flexShrink:0}}>{Ic.X}</span><p style={{fontSize:12,color:'var(--error)'}}>{err}</p></div>}
          <button className="btn-primary" type="submit" disabled={loading} style={{width:'100%',justifyContent:'center',marginTop:4}}>
            {loading?Ic.Spin:Ic.User} {loading?'登录中…':'登录'}
          </button>
        </form>
      </div>
    </div>
  );
}

// ══════════════════════════════════════════════
//  Works panel  (已生成作品)
// ══════════════════════════════════════════════
function WorksPanel({works,loadingWorks,onRefresh,onDelete,onDeleteAll,onClose}){
  const [dlLoading,setDlLoading]=React.useState(null);

  const handleDownload=async(w)=>{
    setDlLoading(w.id);
    try{
      const full=await apiFetch(`/api/works/${w.id}`);
      download(workContent(full), workFilename(full));
    }catch(e){alert(e.message);}
    finally{setDlLoading(null);}
  };

  const fmtBadge=(f)=>{
    const map={md:['Markdown','var(--accent)'],txt:['纯文本','var(--text-3)'],html:['HTML','var(--accent-mid)']};
    const [label,color]=map[f]||['?','var(--text-4)'];
    return <span style={{fontSize:10,padding:'1px 6px',borderRadius:99,border:`1px solid ${color}`,color,fontWeight:600}}>{label}</span>;
  };

  const expColor=(exp)=>{
    const h=(new Date(exp)-Date.now())/3600000;
    if(h<1) return 'var(--error)';
    if(h<4) return 'var(--warning,#fbbf24)';
    return 'var(--text-4)';
  };

  return(
    <div className="overlay" onClick={onClose}>
      <div className="modal anim-modal" style={{maxWidth:640,maxHeight:'80vh',display:'flex',flexDirection:'column'}} onClick={e=>e.stopPropagation()}>
        <div style={{display:'flex',alignItems:'center',gap:10,marginBottom:16,flexShrink:0}}>
          <span style={{color:'var(--accent)'}}>{Ic.Books}</span>
          <span style={{flex:1,fontSize:15,fontWeight:600,color:'var(--text-1)'}}>已生成作品</span>
          <span style={{fontSize:11,color:'var(--text-4)',display:'flex',alignItems:'center',gap:4}}>{Ic.Clock} 24h 自动清理</span>
          <button className="btn-ghost" style={{padding:'4px 8px',fontSize:11}} onClick={onRefresh}>{Ic.Spin} 刷新</button>
          {works.length>0&&<button className="btn-ghost" style={{padding:'4px 8px',fontSize:11,color:'var(--error)'}} onClick={()=>{if(confirm('删除全部作品？'))onDeleteAll();}}>清空</button>}
          <button className="btn-ghost" style={{padding:'4px 8px'}} onClick={onClose}>{Ic.X}</button>
        </div>

        <div style={{flex:1,overflowY:'auto'}}>
          {loadingWorks?(
            <div style={{textAlign:'center',padding:40,color:'var(--text-3)',display:'flex',alignItems:'center',justifyContent:'center',gap:8}}>{Ic.Spin} 加载中…</div>
          ):works.length===0?(
            <div style={{textAlign:'center',padding:48}}>
              <div style={{width:48,height:48,borderRadius:12,background:'rgba(0,113,227,0.10)',display:'flex',alignItems:'center',justifyContent:'center',margin:'0 auto 12px'}}>
                <span style={{color:'#409CFF'}}>{Ic.Books}</span>
              </div>
              <p style={{fontSize:13,color:'var(--text-3)'}}>暂无作品</p>
              <p style={{fontSize:12,color:'var(--text-4)',marginTop:4}}>生成的文章会自动保存在这里</p>
            </div>
          ):(
            <div style={{display:'flex',flexDirection:'column',gap:6}}>
              {works.map((w,i)=>(
                <div key={w.id} className="anim-fade" style={{background:'var(--bg-sunken)',border:'1px solid var(--border)',borderRadius:10,padding:'10px 12px',display:'flex',alignItems:'center',gap:10,animationDelay:`${i*0.03}s`}}>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{display:'flex',alignItems:'center',gap:7,marginBottom:4}}>
                      {fmtBadge(w.format)}
                      <span style={{fontSize:13,fontWeight:500,color:'var(--text-1)',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{w.title}</span>
                    </div>
                    <div style={{display:'flex',alignItems:'center',gap:10,fontSize:11,color:'var(--text-4)'}}>
                      {w.templateName&&<span>{w.templateName}</span>}
                      <span>{(w.charCount||0).toLocaleString()} 字符</span>
                      <span>{fmtTime(w.createdAt)}</span>
                      <span style={{color:expColor(w.expiresAt),display:'flex',alignItems:'center',gap:3}}>{Ic.Clock} {timeLeft(w.expiresAt)}</span>
                    </div>
                  </div>
                  <button className="btn-ghost" style={{padding:'5px 9px',flexShrink:0}} onClick={()=>handleDownload(w)} disabled={dlLoading===w.id} title="下载">
                    {dlLoading===w.id?Ic.Spin:Ic.Dl}
                  </button>
                  <button className="btn-ghost" style={{padding:'5px 9px',flexShrink:0,color:'var(--error)'}} onClick={()=>onDelete(w.id)} title="删除">
                    {Ic.Trash}
                  </button>
                </div>
              ))}
            </div>
          )}
        </div>

        {works.length>0&&(
          <div style={{flexShrink:0,borderTop:'1px solid var(--border-soft)',paddingTop:12,marginTop:12,display:'flex',justifyContent:'space-between',alignItems:'center'}}>
            <span style={{fontSize:11,color:'var(--text-4)'}}>共 {works.length} 篇 · 过期后自动删除</span>
          </div>
        )}
      </div>
    </div>
  );
}

// ══════════════════════════════════════════════
//  Batch card  (批量生成)
// ══════════════════════════════════════════════
function BatchCard({titleLibrary,onLibraryChange,batchState,onRun,onStop,onDownloadZip,zipLoading,maxThreads,platformThreadCount,threadCount,setThreadCount,usePlatform,threadProgress}){
  const {running,done,total,current,errors,workIds}=batchState;
  const [mode,setMode]=React.useState('each_once');   // each_once | each_n | one_n
  const [count,setCount]=React.useState(3);
  const [singleIdx,setSingleIdx]=React.useState(0);
  const [sloganIdx,setSloganIdx]=React.useState(0);

  // Textarea-as-library: local string state, synced with titleLibrary
  const [libText,setLibText]=React.useState(()=>titleLibrary.map(t=>t.title).join('\n'));
  const taFocused=React.useRef(false);

  // Sync external changes (e.g. topic generation adding titles) → textarea
  React.useEffect(()=>{
    if(!taFocused.current){
      setLibText(titleLibrary.map(t=>t.title).join('\n'));
    }
  },[titleLibrary]);

  const syncLibrary=(text)=>{
    const titles=text.split('\n').map(s=>s.trim()).filter(Boolean);
    onLibraryChange(titles.map((title,i)=>({
      id:titleLibrary[i]?.id||(Date.now()+Math.random()).toString(36),
      title,sel:true
    })));
  };

  const titles=libText.split('\n').map(s=>s.trim()).filter(Boolean);

  React.useEffect(()=>{
    if(!running) return;
    const t=setInterval(()=>setSloganIdx(i=>(i+1)%SLOGANS.length),2500);
    return ()=>clearInterval(t);
  },[running]);

  const buildQueue=()=>{
    const n=Math.max(1,Math.min(50,Number(count)||1));
    if(mode==='each_once') return titles;
    if(mode==='each_n') return titles.flatMap(t=>Array(n).fill(t));
    if(mode==='one_n'){
      const t=titles[singleIdx]||titles[0];
      return t?Array(n).fill(t):[];
    }
    return [];
  };

  const queue=buildQueue();
  const pct=total>0?Math.round(done/total*100):0;

  return(
    <Card icon={Ic.Bolt} title="批量生成" defaultOpen={false}>
      <div style={{display:'flex',flexDirection:'column',gap:10}}>

        {/* Textarea library */}
        <div>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:5}}>
            <span style={{fontSize:11,color:'var(--text-4)',letterSpacing:'.04em',textTransform:'uppercase',fontWeight:600}}>
              标题库{titles.length>0?` · ${titles.length} 篇`:''}
            </span>
            {titles.length>0&&(
              <button className="btn-ghost" style={{padding:'2px 8px',fontSize:11,color:'var(--error)'}}
                onClick={()=>{if(confirm('清空标题库？')){setLibText('');onLibraryChange([]);}}}>清空</button>
            )}
          </div>
          <textarea className="input" value={libText}
            onChange={e=>setLibText(e.target.value)}
            onFocus={()=>{taFocused.current=true;}}
            onBlur={()=>{taFocused.current=false;syncLibrary(libText);}}
            placeholder={'每行一个标题，粘贴多行批量添加'}
            rows={6} style={{width:'100%',resize:'vertical',fontSize:12,lineHeight:1.7,fontFamily:'inherit'}}/>
        </div>

        {/* Generation config */}
        {titles.length>0&&(
          <>
            <div style={{height:1,background:'var(--border-soft)'}}/>
            <FL label="生成模式">
              <div style={{display:'flex',flexDirection:'column',gap:5}}>
                {[
                  ['each_once','所有标题，各生成一篇'],
                  ['each_n',   '所有标题，各重复生成'],
                  ['one_n',    '指定一个标题，重复生成'],
                ].map(([val,label])=>(
                  <div key={val} className={`option-row${mode===val?' selected':''}`} onClick={()=>setMode(val)} style={{padding:'7px 10px'}}>
                    <div className="radio-dot"/>
                    <span style={{fontSize:12,color:'var(--text-1)',flex:1}}>{label}</span>
                    {(val==='each_n'||val==='one_n')&&mode===val&&(
                      <input type="number" className="input" min={1} max={50} value={count} onChange={e=>setCount(e.target.value)}
                        onClick={e=>e.stopPropagation()} style={{width:56,padding:'3px 8px',fontSize:12,textAlign:'center'}}/>
                    )}
                  </div>
                ))}
              </div>
            </FL>

            {mode==='one_n'&&titles.length>0&&(
              <FL label="选择标题">
                <select className="input" style={{fontSize:12}} value={singleIdx} onChange={e=>setSingleIdx(Number(e.target.value))}>
                  {titles.map((t,i)=><option key={i} value={i}>{t}</option>)}
                </select>
              </FL>
            )}

            {/* Thread count selector */}
            {(maxThreads>1)&&(()=>{
              const maxSel=usePlatform?Math.min(maxThreads,platformThreadCount||1):maxThreads;
              return(
              <div style={{display:'flex',alignItems:'center',gap:8,padding:'8px 10px',borderRadius:8,background:'var(--bg-sunken)',border:'1px solid var(--border)'}}>
                <span style={{fontSize:12,color:'var(--text-3)',flex:1}}>⚡ 并发线程数</span>
                <div style={{display:'flex',gap:4}}>
                  {Array.from({length:maxSel},(_,i)=>i+1).map(n=>(
                    <button key={n} onClick={()=>setThreadCount(n)}
                      style={{width:28,height:28,borderRadius:6,border:'1px solid',fontSize:12,fontWeight:500,cursor:'pointer',fontFamily:'inherit',transition:'all .12s',
                        background:threadCount===n?'var(--accent)':'var(--bg-card-2)',
                        color:threadCount===n?'#fff':'var(--text-3)',
                        borderColor:threadCount===n?'var(--accent)':'var(--border)'}}>
                      {n}
                    </button>
                  ))}
                </div>
                {usePlatform&&platformThreadCount>0&&<span style={{fontSize:10,color:'var(--text-4)',whiteSpace:'nowrap'}}>{platformThreadCount}个Key</span>}
              </div>
              );
            })()}

            {/* Progress */}
            {running?(
              <div style={{display:'flex',flexDirection:'column',gap:8,padding:'10px 12px',borderRadius:9,background:'var(--bg-sunken)',border:'1px solid var(--border)'}}>
                <div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
                  <span style={{fontSize:12,fontWeight:600,color:'var(--text-1)'}}>批量生成中 {done}/{total}</span>
                  <span style={{fontSize:11,color:'var(--accent)'}}>{pct}%</span>
                </div>
                <div style={{height:4,borderRadius:99,background:'var(--border)',overflow:'hidden'}}>
                  <div style={{height:'100%',borderRadius:99,background:'var(--accent)',width:`${pct}%`,transition:'width .3s'}}/>
                </div>
                {errors.length>0&&<span style={{fontSize:11,color:'var(--error)'}}>{errors.length} 个失败</span>}
                {threadProgress.length<=1&&current&&<span style={{fontSize:11,color:'var(--text-3)',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>正在生成：{current}</span>}

                <div style={{overflow:'hidden',borderRadius:8,padding:'7px 12px',background:'linear-gradient(135deg,#7c2d12,#92400e,#78350f)',border:'1px solid #b45309'}}>
                  <p style={{fontSize:11,fontWeight:600,color:'#fde68a',textAlign:'center'}}>{SLOGANS[sloganIdx]}</p>
                </div>
                <button className="btn-danger" style={{justifyContent:'center',padding:'7px'}} onClick={onStop}>{Ic.Stop} 停止</button>
              </div>
            ):(
              <div style={{display:'flex',flexDirection:'column',gap:8}}>
                {errors.length>0&&(
                  <div style={{padding:'7px 10px',borderRadius:8,background:'var(--error-bg)',border:'1px solid color-mix(in srgb,var(--error) 30%,transparent)'}}>
                    <p style={{fontSize:11,color:'var(--error)',fontWeight:600}}>上次批量 {errors.length} 篇失败</p>
                    {errors.slice(0,3).map((e,i)=><p key={i} style={{fontSize:11,color:'var(--error)',marginTop:2,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{e}</p>)}
                  </div>
                )}
                {!running&&done>0&&workIds&&workIds.length>0&&(
                  <button className="btn-ghost" style={{width:'100%',justifyContent:'center',padding:'8px',opacity:zipLoading?0.6:1}} disabled={zipLoading} onClick={()=>onDownloadZip(workIds)}>
                    {zipLoading?<>{Ic.Spin} 打包中…</>:<>{Ic.Zip} 打包下载全部 ({workIds.length} 篇)</>}
                  </button>
                )}
                <button className="btn-primary" style={{width:'100%',justifyContent:'center',padding:'9px'}}
                  disabled={queue.length===0} onClick={()=>onRun(queue)}>
                  {Ic.Bolt} 开始批量生成 ({queue.length} 篇)
                </button>
                {queue.length===0&&<p style={{fontSize:11,color:'var(--text-4)',textAlign:'center',marginTop:6}}>请先在标题库中输入标题</p>}
              </div>
            )}
          </>
        )}
      </div>
    </Card>
  );
}

// ══════════════════════════════════════════════
//  Admin panel
// ══════════════════════════════════════════════
function AdminPanel({currentUser,onClose,onSelfThreadChange}){
  const [users,setUsers]=React.useState([]);
  const [loading,setLoading]=React.useState(true);
  const [resetUid,setResetUid]=React.useState(null);
  const [newPw,setNewPw]=React.useState('');
  const [pwErr,setPwErr]=React.useState('');
  const [saving,setSaving]=React.useState(false);
  // Create account state
  const [newUser,setNewUser]=React.useState('');
  const [newUserPw,setNewUserPw]=React.useState('');
  const [createErr,setCreateErr]=React.useState('');
  const [creating,setCreating]=React.useState(false);
  const [createOk,setCreateOk]=React.useState('');
  const load=async()=>{setLoading(true);try{setUsers(await apiFetch('/api/admin/users'));}catch{}finally{setLoading(false);}};
  const toggleMultiThread=async(id,current)=>{const n=current>1?1:10;try{await apiFetch(`/api/admin/users/${id}/maxthreads`,{method:'PUT',body:{maxThreads:n}});load();if(id===currentUser.id){try{const d=await apiFetch('/api/user/config');onSelfThreadChange&&onSelfThreadChange(d.maxThreads||1);}catch{}}}catch(e){alert(e.message);}};
  const [tab,setTab]=React.useState('users');
  const [plat,setPlat]=React.useState({baseUrl:'',apiKey:'',model:'',threadKeys:Array(10).fill('')});
  const [platSaving,setPlatSaving]=React.useState(false);
  const [platMsg,setPlatMsg]=React.useState(null);
  const [showPlatKey,setShowPlatKey]=React.useState(false);
  const loadPlat=async()=>{try{const d=await apiFetch('/api/admin/platform');setPlat({...d,threadKeys:d.threadKeys||Array(10).fill('')});}catch{}};
  const [imgApi,setImgApi]=React.useState({baseUrl:'',apiKey:'',searchPath:'/search'});
  const [imgSaving,setImgSaving]=React.useState(false);
  const [imgMsg,setImgMsg]=React.useState(null);
  const [showImgKey,setShowImgKey]=React.useState(false);
  const loadImgApi=async()=>{try{setImgApi(await apiFetch('/api/admin/imageapi'));}catch{}};
  React.useEffect(()=>{load();loadPlat();loadImgApi();},[]);
  const del=async(id,name)=>{if(!confirm(`删除「${name}」？`))return;try{await apiFetch(`/api/admin/users/${id}`,{method:'DELETE'});load();}catch(e){alert(e.message);}};
  const toggleAdmin=async(id)=>{try{await apiFetch(`/api/admin/users/${id}/admin`,{method:'PUT'});load();}catch(e){alert(e.message);}};
  const resetPw=async()=>{if(!newPw||newPw.length<6){setPwErr('密码至少 6 位');return;}setSaving(true);setPwErr('');try{await apiFetch(`/api/admin/users/${resetUid}/password`,{method:'PUT',body:{password:newPw}});setResetUid(null);setNewPw('');}catch(e){setPwErr(e.message);}finally{setSaving(false);}};
  const fmt=d=>new Date(d).toLocaleDateString('zh-CN',{month:'short',day:'numeric',year:'numeric'});
  return(
    <div className="overlay" onClick={onClose}>
      <div className="modal anim-modal" style={{maxWidth:580}} onClick={e=>e.stopPropagation()}>
        {/* Header */}
        <div style={{display:'flex',alignItems:'center',gap:10,marginBottom:14}}>
          <span style={{color:'var(--accent)'}}>{Ic.Shield}</span>
          <span style={{flex:1,fontSize:15,fontWeight:600,color:'var(--text-1)'}}>后台管理</span>
          <button className="btn-ghost" style={{padding:'4px 8px'}} onClick={onClose}>{Ic.X}</button>
        </div>
        {/* Tabs */}
        <div style={{display:'flex',gap:4,marginBottom:14,borderBottom:'1px solid var(--border-soft)',paddingBottom:10}}>
          {[['users',`账号管理 (${users.length})`],['platform','平台 API'],['imageapi','图片 API']].map(([id,label])=>(
            <button key={id} onClick={()=>setTab(id)} style={{padding:'5px 12px',borderRadius:7,border:'none',fontSize:12,fontWeight:tab===id?600:400,cursor:'pointer',background:tab===id?'var(--accent-dim)':'transparent',color:tab===id?'var(--accent)':'var(--text-2)',transition:'all .12s'}}>
              {label}
            </button>
          ))}
        </div>

        {/* Users tab */}
        {tab==='users'&&<>
          {loading?<div style={{textAlign:'center',padding:32,color:'var(--text-3)'}}>{Ic.Spin}</div>:(
            <div style={{overflowX:'auto',borderRadius:10,border:'1px solid var(--border)'}}>
              <table className="admin-table">
                <thead><tr><th>用户名</th><th>权限</th><th>注册时间</th><th>多线程</th><th style={{textAlign:'right'}}>操作</th></tr></thead>
                <tbody>
                  {users.map(u=>(
                    <tr key={u.id}>
                      <td><div style={{display:'flex',alignItems:'center',gap:8}}>
                        <div style={{width:26,height:26,borderRadius:99,background:'var(--accent-dim)',border:'1px solid var(--accent)',display:'flex',alignItems:'center',justifyContent:'center',fontSize:11,fontWeight:600,color:'var(--accent)',flexShrink:0}}>{u.username[0].toUpperCase()}</div>
                        <span style={{fontWeight:500}}>{u.username}</span>
                        {u.id===currentUser.id&&<span className="tag">你</span>}
                      </div></td>
                      <td>{u.isAdmin?<span className="tag-accent">管理员</span>:<span className="tag">普通用户</span>}</td>
                      <td style={{color:'var(--text-3)',fontSize:12}}>{fmt(u.createdAt)}</td>
                      <td>
                        <button onClick={()=>toggleMultiThread(u.id,u.maxThreads||1)}
                          style={{padding:'3px 10px',borderRadius:9999,border:'1px solid',fontSize:11,fontWeight:600,cursor:'pointer',fontFamily:'inherit',transition:'all .15s',
                            background:(u.maxThreads||1)>1?'rgba(52,211,153,0.12)':'var(--bg-sunken)',
                            color:(u.maxThreads||1)>1?'#34d399':'var(--text-4)',
                            borderColor:(u.maxThreads||1)>1?'rgba(52,211,153,0.4)':'var(--border)'}}>
                          {(u.maxThreads||1)>1?'⚡ 已开启':'— 未开启'}
                        </button>
                      </td>
                      <td><div style={{display:'flex',gap:4,justifyContent:'flex-end'}}>
                        {u.id!==currentUser.id&&<>
                          <button className="btn-ghost" style={{padding:'3px 8px',fontSize:11}} onClick={()=>toggleAdmin(u.id)}>{Ic.Shield} {u.isAdmin?'撤销':'设管理'}</button>
                          <button className="btn-ghost" style={{padding:'3px 8px',fontSize:11}} onClick={()=>{setResetUid(u.id);setNewPw('');setPwErr('');}}>{Ic.Lock}</button>
                          <button className="btn-ghost" style={{padding:'3px 8px',fontSize:11,color:'var(--error)'}} onClick={()=>del(u.id,u.username)}>{Ic.Trash}</button>
                        </>}
                      </div></td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
          {resetUid&&(
            <div style={{marginTop:14,padding:'14px',borderRadius:10,background:'var(--bg-sunken)',border:'1px solid var(--border)'}}>
              <p style={{fontSize:12,color:'var(--text-2)',marginBottom:8}}>重置密码 — {users.find(u=>u.id===resetUid)?.username}</p>
              <div style={{display:'flex',gap:8}}>
                <input className="input" style={{flex:1}} type="password" value={newPw} onChange={e=>{setNewPw(e.target.value);setPwErr('');}} placeholder="新密码（至少 6 位）"/>
                <button className="btn-primary" onClick={resetPw} disabled={saving} style={{whiteSpace:'nowrap',padding:'7px 12px'}}>{saving?Ic.Spin:Ic.Check} 确认</button>
                <button className="btn-ghost" onClick={()=>setResetUid(null)} style={{padding:'7px 10px'}}>{Ic.X}</button>
              </div>
              {pwErr&&<p style={{fontSize:11,color:'var(--error)',marginTop:6}}>{pwErr}</p>}
            </div>
          )}

          {/* Create account */}
          <div style={{marginTop:16,padding:'14px',borderRadius:10,border:'1px solid var(--border)',background:'var(--bg-sunken)'}}>
            <p style={{fontSize:12,fontWeight:600,color:'var(--text-2)',marginBottom:10,display:'flex',alignItems:'center',gap:6}}>{Ic.Plus} 创建账号</p>
            <div style={{display:'flex',gap:8,marginBottom:8}}>
              <input className="input" value={newUser} onChange={e=>{setNewUser(e.target.value);setCreateErr('');setCreateOk('');}} placeholder="用户名（至少 2 位）" style={{flex:1}}/>
              <input className="input" value={newUserPw} onChange={e=>{setNewUserPw(e.target.value);setCreateErr('');setCreateOk('');}} type="password" placeholder="密码（至少 6 位）" style={{flex:1}}/>
              <button className="btn-primary" disabled={creating} style={{whiteSpace:'nowrap',padding:'7px 12px'}}
                onClick={async()=>{
                  if(!newUser.trim()||!newUserPw){setCreateErr('请填写用户名和密码');return;}
                  setCreating(true);setCreateErr('');setCreateOk('');
                  try{
                    await apiFetch('/api/register',{method:'POST',body:{username:newUser,password:newUserPw}});
                    setCreateOk(`账号「${newUser.trim()}」已创建`);
                    setNewUser('');setNewUserPw('');
                    load();
                  }catch(e){setCreateErr(e.message);}
                  finally{setCreating(false);}
                }}>
                {creating?Ic.Spin:Ic.Plus} 创建
              </button>
            </div>
            {createErr&&<p style={{fontSize:11,color:'var(--error)'}}>{createErr}</p>}
            {createOk&&<p style={{fontSize:11,color:'var(--success)',display:'flex',alignItems:'center',gap:4}}>{Ic.Check} {createOk}</p>}
          </div>
        </>}

        {/* Platform API tab */}
        {tab==='platform'&&(
          <div style={{display:'flex',flexDirection:'column',gap:12}}>
            <div style={{padding:'10px 12px',borderRadius:9,background:'var(--accent-dim)',border:'1px solid color-mix(in srgb,var(--accent) 30%,transparent)'}}>
              <p style={{fontSize:12,color:'var(--text-2)',lineHeight:1.6}}>配置后，普通用户可在「API 配置」中选择「使用平台配置」，无需填写自己的 Key 即可使用。</p>
            </div>
            <FL label="Base URL">
              <input className="input" value={plat.baseUrl} onChange={e=>setPlat(p=>({...p,baseUrl:e.target.value}))} placeholder="https://api.openai.com/v1"/>
            </FL>
            <FL label="线程 API Key" hint={`已配置 ${(plat.threadKeys||[]).filter(k=>k).length} / 10 个`}>
              <div style={{display:'flex',flexDirection:'column',gap:5}}>
                {Array.from({length:10},(_,i)=>(
                  <div key={i} style={{display:'flex',alignItems:'center',gap:6}}>
                    <span style={{fontSize:10,fontWeight:700,color:(plat.threadKeys||[])[i]?'var(--accent)':'var(--text-4)',fontFamily:'ui-monospace,monospace',width:36,flexShrink:0,textAlign:'right'}}>T{i+1}</span>
                    <input className="input" style={{flex:1,fontSize:12,fontFamily:'ui-monospace,monospace',padding:'5px 10px',
                      filter:showPlatKey?'none':'blur(3px)',transition:'filter .15s',
                      borderColor:(plat.threadKeys||[])[i]?'rgba(52,211,153,0.4)':'var(--border)'}}
                      type="text"
                      value={(plat.threadKeys||[])[i]||''}
                      onChange={e=>{const keys=[...(plat.threadKeys||Array(10).fill(''))];keys[i]=e.target.value;setPlat(p=>({...p,threadKeys:keys}));}}
                      placeholder={`线程${i+1} Key…`}/>
                  </div>
                ))}
                <button onClick={()=>setShowPlatKey(v=>!v)} style={{alignSelf:'flex-start',background:'none',border:'none',cursor:'pointer',color:'var(--text-3)',fontSize:11,fontFamily:'inherit',display:'flex',alignItems:'center',gap:4,marginTop:2}}>
                  {showPlatKey?Ic.EyeOff:Ic.Eye} {showPlatKey?'隐藏 Key':'显示 Key'}
                </button>
              </div>
            </FL>
            <FL label="Model">
              <input className="input" value={plat.model} onChange={e=>setPlat(p=>({...p,model:e.target.value}))} placeholder="gpt-4o · deepseek-chat · …"/>
            </FL>
            {platMsg&&<p style={{fontSize:12,color:platMsg.ok?'var(--success)':'var(--error)',display:'flex',alignItems:'center',gap:5}}>{platMsg.ok?Ic.Check:Ic.X} {platMsg.text}</p>}
            <div style={{display:'flex',gap:8}}>
              <button className="btn-primary" style={{flex:1,justifyContent:'center'}} disabled={platSaving}
                onClick={async()=>{
                  setPlatSaving(true);setPlatMsg(null);
                  try{
                    const r=await apiFetch('/api/admin/platform',{method:'PUT',body:plat});
                    setPlatMsg({ok:true,text:r.available?`已保存，平台 API 可用（${r.threadCount||0} 个线程 Key）`:'已保存（配置不完整，暂不可用）'});
                  }catch(e){setPlatMsg({ok:false,text:e.message});}
                  finally{setPlatSaving(false);}
                }}>
                {platSaving?Ic.Spin:Ic.Check} 保存平台配置
              </button>
              <button className="btn-ghost" onClick={()=>setPlat({baseUrl:'',apiKey:'',model:'',threadKeys:Array(10).fill('')})}>清空</button>
            </div>
            <p style={{fontSize:11,color:'var(--text-4)'}}>API Key 仅存于服务端，不会暴露给普通用户。</p>
          </div>
        )}

        {/* Image API tab — nanobanana */}
        {tab==='imageapi'&&(
          <div style={{display:'flex',flexDirection:'column',gap:12}}>
            <div style={{padding:'10px 12px',borderRadius:9,background:'var(--accent-dim)',border:'1px solid color-mix(in srgb,var(--accent) 30%,transparent)'}}>
              <p style={{fontSize:12,color:'var(--text-2)',lineHeight:1.6}}>配置图片 API（nanobanana）后，用户可开启「图文并茂」功能，生成文章时自动插配图。</p>
            </div>
            <FL label="Base URL">
              <input className="input" value={imgApi.baseUrl} onChange={e=>setImgApi(p=>({...p,baseUrl:e.target.value}))} placeholder="https://api.nanobanana.com/v1"/>
            </FL>
            <FL label="API Key">
              <div style={{position:'relative'}}>
                <input className="input" style={{paddingRight:36}} type={showImgKey?'text':'password'} value={imgApi.apiKey} onChange={e=>setImgApi(p=>({...p,apiKey:e.target.value}))} placeholder="API Key…"/>
                <button onClick={()=>setShowImgKey(v=>!v)} style={{position:'absolute',right:10,top:'50%',transform:'translateY(-50%)',background:'none',border:'none',cursor:'pointer',color:'var(--text-3)',display:'flex'}}>{showImgKey?Ic.EyeOff:Ic.Eye}</button>
              </div>
            </FL>
            <FL label="搜索路径" hint="相对路径，如 /search">
              <input className="input" value={imgApi.searchPath} onChange={e=>setImgApi(p=>({...p,searchPath:e.target.value}))} placeholder="/search"/>
            </FL>
            {imgMsg&&<p style={{fontSize:12,color:imgMsg.ok?'var(--success)':'var(--error)',display:'flex',alignItems:'center',gap:5}}>{imgMsg.ok?Ic.Check:Ic.X} {imgMsg.text}</p>}
            <div style={{display:'flex',gap:8}}>
              <button className="btn-primary" style={{flex:1,justifyContent:'center'}} disabled={imgSaving}
                onClick={async()=>{
                  setImgSaving(true);setImgMsg(null);
                  try{
                    const r=await apiFetch('/api/admin/imageapi',{method:'PUT',body:imgApi});
                    setImgMsg({ok:true,text:r.available?'已保存，图片 API 可用':'已保存（Base URL 或 Key 不完整）'});
                  }catch(e){setImgMsg({ok:false,text:e.message});}
                  finally{setImgSaving(false);}
                }}>
                {imgSaving?Ic.Spin:Ic.Check} 保存图片 API 配置
              </button>
              <button className="btn-ghost" onClick={()=>setImgApi({baseUrl:'',apiKey:'',searchPath:'/search'})}>清空</button>
            </div>
            <p style={{fontSize:11,color:'var(--text-4)'}}>API Key 仅存于服务端。搜索接口需支持 ?q= 参数，响应兼容 Unsplash / Pixabay 格式或 nanobanana 原生格式。</p>
          </div>
        )}
      </div>
    </div>
  );
}

// ══════════════════════════════════════════════
//  User menu
// ══════════════════════════════════════════════
function UserMenu({user,onLogout}){
  const [open,setOpen]=React.useState(false);
  const [pwMode,setPwMode]=React.useState(false);
  const [oldPw,setOldPw]=React.useState('');
  const [newPw,setNewPw]=React.useState('');
  const [msg,setMsg]=React.useState(null);
  const [saving,setSaving]=React.useState(false);
  const ref=React.useRef(null);
  React.useEffect(()=>{
    const h=e=>{if(ref.current&&!ref.current.contains(e.target))setOpen(false);};
    document.addEventListener('mousedown',h);return()=>document.removeEventListener('mousedown',h);
  },[]);
  const changePw=async()=>{
    if(!newPw||newPw.length<6){setMsg({ok:false,text:'新密码至少 6 位'});return;}
    setSaving(true);setMsg(null);
    try{await apiFetch('/api/user/password',{method:'PUT',body:{oldPassword:oldPw,newPassword:newPw}});setMsg({ok:true,text:'密码已更新'});setOldPw('');setNewPw('');setTimeout(()=>{setPwMode(false);setMsg(null);},1500);}
    catch(e){setMsg({ok:false,text:e.message});}
    finally{setSaving(false);}
  };
  return(
    <div ref={ref} style={{position:'relative'}}>
      <div className="avatar" onClick={()=>{setOpen(v=>!v);setPwMode(false);setMsg(null);}}>{user.username[0].toUpperCase()}</div>
      {open&&(
        <div className="anim-fade" style={{position:'absolute',top:38,right:0,zIndex:100,background:'var(--bg-card)',border:'1px solid var(--border)',borderRadius:12,padding:6,minWidth:200,boxShadow:'var(--shadow-lg)'}}>
          <div style={{padding:'8px 10px 8px',marginBottom:4}}>
            <p style={{fontSize:13,fontWeight:600,color:'var(--text-1)'}}>{user.username}</p>
            {user.isAdmin&&<span className="tag-accent" style={{marginTop:3,display:'inline-block'}}>管理员</span>}
          </div>
          <div style={{height:1,background:'var(--border-soft)',margin:'0 4px 4px'}}/>
          {!pwMode?(
            <>
              <button className="btn-ghost" style={{width:'100%',justifyContent:'flex-start',border:'none',borderRadius:8,padding:'7px 10px'}} onClick={()=>setPwMode(true)}>{Ic.Lock} 修改密码</button>
              <button className="btn-ghost" style={{width:'100%',justifyContent:'flex-start',border:'none',borderRadius:8,padding:'7px 10px',color:'var(--error)'}} onClick={onLogout}>{Ic.Logout} 退出登录</button>
            </>
          ):(
            <div style={{padding:'4px 6px 6px',display:'flex',flexDirection:'column',gap:7}}>
              <input className="input" type="password" value={oldPw} onChange={e=>{setOldPw(e.target.value);setMsg(null);}} placeholder="原密码"/>
              <input className="input" type="password" value={newPw} onChange={e=>{setNewPw(e.target.value);setMsg(null);}} placeholder="新密码（至少 6 位）"/>
              {msg&&<p style={{fontSize:11,color:msg.ok?'var(--success)':'var(--error)'}}>{msg.text}</p>}
              <div style={{display:'flex',gap:6}}>
                <button className="btn-primary" onClick={changePw} disabled={saving} style={{flex:1,justifyContent:'center',padding:'7px',fontSize:12}}>{saving?Ic.Spin:Ic.Check} 确认</button>
                <button className="btn-ghost" onClick={()=>{setPwMode(false);setMsg(null);}} style={{padding:'7px 10px',fontSize:12}}>取消</button>
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ══════════════════════════════════════════════
//  API Config
// ══════════════════════════════════════════════
function ApiConfig({config,onChange,usePlatform,onUsePlatformChange,platformAvailable}){
  const [showKey,setShowKey]=React.useState(false);
  const [testing,setTesting]=React.useState(false);
  const [testResult,setTestResult]=React.useState(null);
  const [testMsg,setTestMsg]=React.useState('');
  const set=f=>e=>onChange({...config,[f]:e.target.value});

  const test=async()=>{
    if(usePlatform){
      setTesting(true);setTestResult(null);
      try{await streamProxy({messages:[{role:'user',content:'hi'}],onChunk:()=>{}});setTestResult('ok');setTestMsg('平台连接成功');}
      catch(e){setTestResult('err');setTestMsg(e.message||'失败');}
      finally{setTesting(false);}
    } else {
      if(!config.baseUrl||!config.apiKey||!config.model){setTestResult('err');setTestMsg('请填写完整配置');return;}
      setTesting(true);setTestResult(null);
      try{await streamContent({apiConfig:config,messages:[{role:'user',content:'hi'}],onChunk:()=>{}});setTestResult('ok');setTestMsg('连接成功');}
      catch(e){setTestResult('err');setTestMsg(e.message||'失败');}
      finally{setTesting(false);}
    }
  };

  return(
    <Card icon={Ic.Cog} title="API 配置" defaultOpen={false}>
      <div style={{display:'flex',flexDirection:'column',gap:10}}>

        {/* Mode selector */}
        <FL label="接入方式">
          <div style={{display:'flex',flexDirection:'column',gap:6}}>
            {/* Platform option — only shown if admin has configured it */}
            <div
              className={`option-row${usePlatform?' selected':''}${!platformAvailable?' disabled':''}`}
              onClick={()=>platformAvailable&&onUsePlatformChange(true)}
              style={{opacity:platformAvailable?1:0.4,cursor:platformAvailable?'pointer':'not-allowed'}}>
              <div className="radio-dot"/>
              <div style={{flex:1}}>
                <div style={{fontSize:13,fontWeight:500,color:'var(--text-1)',display:'flex',alignItems:'center',gap:6}}>
                  使用平台配置
                  {platformAvailable
                    ? <span className="tag-accent" style={{fontSize:9}}>可用</span>
                    : <span className="tag" style={{fontSize:9}}>未配置</span>}
                </div>
                <div style={{fontSize:11,color:'var(--text-3)',marginTop:1}}>
                  {platformAvailable?'由管理员提供，无需填写 Key':'管理员尚未配置平台 API'}
                </div>
              </div>
            </div>
            <div className={`option-row${!usePlatform?' selected':''}`} onClick={()=>onUsePlatformChange(false)}>
              <div className="radio-dot"/>
              <div>
                <div style={{fontSize:13,fontWeight:500,color:'var(--text-1)'}}>自行配置</div>
                <div style={{fontSize:11,color:'var(--text-3)',marginTop:1}}>使用自己的 API Key</div>
              </div>
            </div>
          </div>
        </FL>

        {/* Self-config fields — only shown in self-config mode */}
        {!usePlatform&&(
          <>
            <div style={{height:1,background:'var(--border-soft)'}}/>
            <FL label="Base URL"><input className="input" value={config.baseUrl} onChange={set('baseUrl')} placeholder="https://api.openai.com/v1"/></FL>
            <FL label="API Key">
              <div style={{position:'relative'}}>
                <input className="input" style={{paddingRight:36}} type={showKey?'text':'password'} value={config.apiKey} onChange={set('apiKey')} placeholder="sk-…"/>
                <button onClick={()=>setShowKey(v=>!v)} style={{position:'absolute',right:10,top:'50%',transform:'translateY(-50%)',background:'none',border:'none',cursor:'pointer',color:'var(--text-3)',display:'flex'}}>{showKey?Ic.EyeOff:Ic.Eye}</button>
              </div>
            </FL>
            <FL label="Model"><input className="input" value={config.model} onChange={set('model')} placeholder="gpt-4o · deepseek-chat · …"/></FL>
          </>
        )}

        {/* Test button */}
        <div style={{display:'flex',alignItems:'center',gap:10,paddingTop:2}}>
          <button className="btn-ghost" onClick={test} disabled={testing||(!usePlatform&&(!config.baseUrl||!config.apiKey||!config.model))||(usePlatform&&!platformAvailable)}>
            {testing?Ic.Spin:Ic.Cog} {testing?'测试中…':'连接测试'}
          </button>
          {testResult==='ok'&&<span style={{fontSize:12,color:'var(--success)',display:'flex',alignItems:'center',gap:4}}>{Ic.Check} {testMsg}</span>}
          {testResult==='err'&&<span style={{fontSize:12,color:'var(--error)',maxWidth:200,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}} title={testMsg}>{testMsg}</span>}
        </div>
      </div>
    </Card>
  );
}

// ══════════════════════════════════════════════
//  Topic input
// ══════════════════════════════════════════════
function TopicInput({topic,note,onTopicChange,onNoteChange,apiConfig,onAddToLibrary,usePlatform,platformAvailable}){
  const [kw,setKw]=React.useState('');
  const [sugs,setSugs]=React.useState([]);
  const [genMode,setGenMode]=React.useState('general'); // 'general' | 'zhihu'
  const [gen,setGen]=React.useState(false);
  const [buf,setBuf]=React.useState('');
  const [err,setErr]=React.useState('');
  const strip=l=>l.replace(/^[\s]*[\d]+[\.、。\)）\-\–:：]\s*/,'').replace(/^[-*•]\s*/,'').trim();
  const generate=async()=>{
    if(genMode==='general'&&!kw.trim()){setErr('请输入关键词');return;}
    const apiReady=usePlatform?platformAvailable:(apiConfig.baseUrl&&apiConfig.apiKey&&apiConfig.model);
    if(!apiReady){setErr(usePlatform?'平台 API 未配置，请联系管理员':'请先完成 API 配置');return;}
    setGen(true);setErr('');setSugs([]);setBuf('');
    let full='';
    const msgs=genMode==='zhihu'
      ?[{role:'system',content:'你是一位知乎内容策划，专注于ChatGPT Plus相关话题，熟悉国内用户痛点。禁止使用"梯子"，统一用"魔法"替代。'},{role:'user',content:'请生成8个适合在知乎发布的文章选题标题，主题围绕"国内普通用户如何升级使用ChatGPT Plus"。要求：\n1. 角度多样：省钱技巧、避坑经验、横向评测、亲测分享、新手入门等\n2. 标题要有代入感，体现真实用户视角（如"我踩过的坑""亲测有效"等口吻）\n3. 禁止出现"梯子"，如需表达相关概念用"魔法"替代\n4. 每行一个标题，只输出标题，不要编号，不要多余说明'}]
      :[{role:'system',content:'你是一位内容策划专家，擅长为自媒体创作者提供选题建议。'},{role:'user',content:`请围绕「${kw.trim()}」生成 8 个有吸引力的文章选题标题。每行一个，只输出标题文字，不要编号，不要多余说明。`}];
    try{
      if(usePlatform){
        await streamProxy({messages:msgs,onChunk:c=>{full+=c;setBuf(full);}});
      } else {
        await streamContent({apiConfig,messages:msgs,onChunk:c=>{full+=c;setBuf(full);}});
      }
      setBuf('');
      const lines=full.split('\n').map(strip).filter(l=>l.length>2).slice(0,8);
      if(!lines.length) throw new Error('未能解析选题，请重试');
      setSugs(lines);
    }catch(e){setBuf('');setErr(e.message||'生成失败');}
    finally{setGen(false);}
  };
  return(
    <Card icon={Ic.Pen} title="选题" defaultOpen={false}>
      <div style={{display:'flex',flexDirection:'column',gap:12}}>
        <FL label="自动生成">
          {/* Mode toggle */}
          <div style={{display:'flex',gap:4,marginBottom:8}}>
            {[['general','通用'],['zhihu','知乎推广']].map(([m,label])=>(
              <button key={m} onClick={()=>{setGenMode(m);setSugs([]);setErr('');}}
                style={{padding:'4px 12px',borderRadius:9999,fontSize:12,fontWeight:500,cursor:'pointer',border:'1px solid',transition:'all .15s',fontFamily:'inherit',
                  background:genMode===m?'var(--accent)':'var(--bg-card-2)',
                  color:genMode===m?'#fff':'var(--text-3)',
                  borderColor:genMode===m?'var(--accent)':'var(--border)'}}>
                {label}
              </button>
            ))}
          </div>
          <div style={{display:'flex',gap:6}}>
            {genMode==='general'&&<input className="input" style={{flex:1}} value={kw} onChange={e=>{setKw(e.target.value);setErr('');}} onKeyDown={e=>e.key==='Enter'&&!gen&&generate()} placeholder="职场成长、AI工具、健身饮食…"/>}
            {genMode==='zhihu'&&<div style={{flex:1,padding:'7px 12px',borderRadius:8,fontSize:12,color:'var(--text-4)',background:'var(--bg-sunken)',border:'1px solid var(--border)'}}>AI 实时生成知乎推广选题</div>}
            <button className="btn-primary" onClick={generate} disabled={gen} style={{whiteSpace:'nowrap',padding:'7px 12px',fontSize:12}}>{gen?Ic.Spin:Ic.Bolt} {gen?'生成中':'生成'}</button>
          </div>
          {err&&<p style={{fontSize:12,color:'var(--error)',marginTop:4}}>{err}</p>}
        </FL>
        {gen&&buf&&(
          <div className="anim-fade" style={{background:'var(--bg-sunken)',border:'1px solid var(--border)',borderRadius:8,padding:'8px 12px',fontSize:12,color:'var(--text-3)',whiteSpace:'pre-wrap',lineHeight:1.7,fontFamily:'ui-monospace,monospace'}}>
            {buf}<span style={{display:'inline-block',width:6,height:'1em',background:'var(--accent)',borderRadius:2,marginLeft:2,verticalAlign:'middle',animation:'pulse-dot 1s infinite'}}/>
          </div>
        )}
        {sugs.length>0&&(
          <div className="anim-fade" style={{display:'flex',flexDirection:'column',gap:4}}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:2}}>
              <p style={{fontSize:10,color:'var(--text-4)',textTransform:'uppercase',letterSpacing:'.06em'}}>点击填入选题</p>
              {onAddToLibrary&&<button className="btn-ghost" style={{padding:'2px 8px',fontSize:11}} onClick={()=>onAddToLibrary(sugs)} title="全部加入批量生成标题库">{Ic.Plus} 加入标题库</button>}
            </div>
            {sugs.map((s,i)=>(
              <div key={i} style={{display:'flex',alignItems:'center',gap:4}}>
                <button className="suggestion" style={{flex:1}} onClick={()=>onTopicChange(s)}>
                  {genMode==='zhihu'
                    ?<span style={{fontSize:10,color:'var(--accent)',fontFamily:'ui-monospace,monospace',flexShrink:0}}>知</span>
                    :<span style={{fontSize:10,color:'var(--text-4)',fontFamily:'ui-monospace,monospace',flexShrink:0}}>{String(i+1).padStart(2,'0')}</span>}
                  <span>{s}</span>
                </button>
                {onAddToLibrary&&(
                  <button onClick={()=>onAddToLibrary([s])} title="加入批量标题库"
                    style={{flexShrink:0,width:26,height:26,borderRadius:6,border:'1px solid var(--border)',background:'var(--bg-card-2)',color:'var(--text-3)',cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',transition:'all .15s',fontFamily:'inherit'}}
                    onMouseEnter={e=>{e.currentTarget.style.background='rgba(0,113,227,0.12)';e.currentTarget.style.color='var(--accent)';e.currentTarget.style.borderColor='rgba(0,113,227,0.4)';}}
                    onMouseLeave={e=>{e.currentTarget.style.background='var(--bg-card-2)';e.currentTarget.style.color='var(--text-3)';e.currentTarget.style.borderColor='var(--border)';}}>
                    {Ic.Plus}
                  </button>
                )}
              </div>
            ))}
          </div>
        )}

        <div className="divider"><span>确定选题</span></div>
        <FL label="选题标题" req><input className="input" value={topic} onChange={e=>onTopicChange(e.target.value)} placeholder="直接输入，或点击上方选题填入"/></FL>
        <FL label="补充说明" hint="可选"><textarea className="input" value={note} onChange={e=>onNoteChange(e.target.value)} placeholder="写作角度、目标读者、特殊要求…" rows={2} style={{resize:'none'}}/></FL>
      </div>
    </Card>
  );
}

// ══════════════════════════════════════════════
//  Template selector
// ══════════════════════════════════════════════
function TplSelector({selectedId,onSelect,custom,onCustomChange}){
  const all=[...DEFAULT_TEMPLATES,...custom];
  const [editing,setEditing]=React.useState(null);
  const [viewing,setViewing]=React.useState(null);
  const save=()=>{if(!editing.name.trim())return;if(!editing.id){const t={...editing,id:'cus_'+Date.now()};onCustomChange(p=>[...p,t]);onSelect(t.id);}else{onCustomChange(p=>p.map(t=>t.id===editing.id?{...editing}:t));}setEditing(null);};
  const del=id=>{onCustomChange(p=>p.filter(t=>t.id!==id));if(selectedId===id)onSelect(DEFAULT_TEMPLATES[0].id);};
  return(
    <Card icon={Ic.Doc} title="提示词模板" defaultOpen={false} badge={`${all.length} 个`}>
      <div style={{display:'flex',flexDirection:'column',gap:6}}>
        {all.map(t=>(
          <div key={t.id} className={`option-row${selectedId===t.id?' selected':''}`} onClick={()=>onSelect(t.id)}>
            <div className="radio-dot"/>
            <span style={{flex:1,fontSize:13,color:'var(--text-1)'}}>{t.name}</span>
            {t.builtIn&&<span className="tag">内置</span>}
            <div style={{display:'flex',gap:2}} onClick={e=>e.stopPropagation()}>
              <button className="btn-ghost" style={{padding:'3px 6px'}} onClick={()=>setViewing(t)}>{Ic.Eye}</button>
              {!t.builtIn&&<><button className="btn-ghost" style={{padding:'3px 6px'}} onClick={()=>setEditing({...t})}>{Ic.Edit}</button><button className="btn-ghost" style={{padding:'3px 6px',color:'var(--error)'}} onClick={()=>del(t.id)}>{Ic.Trash}</button></>}
            </div>
          </div>
        ))}
        <button className="btn-ghost" style={{justifyContent:'center',borderStyle:'dashed',marginTop:2}} onClick={()=>setEditing({id:'',name:'',system:'',userTpl:'{topic}\n补充说明：{note}',builtIn:false})}>{Ic.Plus} 新增自定义预设</button>
      </div>
      {viewing&&(
        <div className="overlay" onClick={()=>setViewing(null)}>
          <div className="modal anim-modal" onClick={e=>e.stopPropagation()}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14}}><span style={{fontWeight:600,color:'var(--text-1)'}}>{viewing.name}</span><button className="btn-ghost" style={{padding:'4px 8px'}} onClick={()=>setViewing(null)}>{Ic.X}</button></div>
            {[['System Prompt',viewing.system],['User Prompt 模板',viewing.userTpl]].map(([lbl,val])=>(
              <div key={lbl} style={{marginBottom:12}}><p style={{fontSize:10,textTransform:'uppercase',letterSpacing:'.06em',color:'var(--text-4)',marginBottom:6}}>{lbl}</p><pre style={{background:'var(--bg-sunken)',border:'1px solid var(--border)',borderRadius:8,padding:'10px 12px',fontSize:12,color:'var(--text-2)',whiteSpace:'pre-wrap',lineHeight:1.6,fontFamily:'ui-monospace,monospace'}}>{val}</pre></div>
            ))}
          </div>
        </div>
      )}
      {editing&&(
        <div className="overlay" onClick={()=>setEditing(null)}>
          <div className="modal anim-modal" onClick={e=>e.stopPropagation()}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:16}}><span style={{fontWeight:600,color:'var(--text-1)'}}>{editing.id?'编辑预设':'新增预设'}</span><button className="btn-ghost" style={{padding:'4px 8px'}} onClick={()=>setEditing(null)}>{Ic.X}</button></div>
            <div style={{display:'flex',flexDirection:'column',gap:10}}>
              <FL label="预设名称" req><input className="input" value={editing.name} onChange={e=>setEditing(p=>({...p,name:e.target.value}))} placeholder="如：知乎风格、学术摘要…"/></FL>
              <FL label="System Prompt"><textarea className="input" rows={3} style={{resize:'none'}} value={editing.system} onChange={e=>setEditing(p=>({...p,system:e.target.value}))} placeholder="你是…"/></FL>
              <FL label="User Prompt 模板" hint="{topic} {note} 自动替换"><textarea className="input" rows={3} style={{resize:'none'}} value={editing.userTpl} onChange={e=>setEditing(p=>({...p,userTpl:e.target.value}))}/></FL>
              <div style={{display:'flex',justifyContent:'flex-end',gap:8,paddingTop:4}}>
                <button className="btn-ghost" onClick={()=>setEditing(null)}>取消</button>
                <button className="btn-primary" onClick={save} style={{padding:'7px 16px'}}>保存预设</button>
              </div>
            </div>
          </div>
        </div>
      )}
    </Card>
  );
}

// ══════════════════════════════════════════════
//  Format selector
// ══════════════════════════════════════════════
function FmtSelector({value,onChange}){
  return(
    <Card icon={Ic.Fmt} title="输出格式" defaultOpen={false}>
      <div style={{display:'flex',flexDirection:'column',gap:6}}>
        {[{id:'md',label:'Markdown',sub:'.md 文件，渲染预览'},{id:'txt',label:'纯文本',sub:'.txt，无任何标记'},{id:'html',label:'HTML',sub:'.txt，含完整 HTML 源码'}].map(f=>(
          <div key={f.id} className={`option-row${value===f.id?' selected':''}`} onClick={()=>onChange(f.id)}>
            <div className="radio-dot"/>
            <div><div style={{fontSize:13,color:'var(--text-1)',fontWeight:500}}>{f.label}</div><div style={{fontSize:11,color:'var(--text-3)',marginTop:1}}>{f.sub}</div></div>
          </div>
        ))}
      </div>
    </Card>
  );
}

// ══════════════════════════════════════════════
//  Enhance options card
// ══════════════════════════════════════════════
function EnhanceCard({withImages,onWithImagesChange,imageApiAvailable}){
  return(
    <Card icon={Ic.Img} title="增强功能" defaultOpen={false}>
      <div
        className={`option-row${withImages?' selected':''}${!imageApiAvailable?' disabled':''}`}
        onClick={()=>imageApiAvailable&&onWithImagesChange(!withImages)}
        style={{opacity:imageApiAvailable?1:0.4,cursor:imageApiAvailable?'pointer':'not-allowed'}}>
        <div style={{width:14,height:14,borderRadius:3,border:`1.5px solid ${withImages&&imageApiAvailable?'#0071e3':'rgba(255,255,255,0.25)'}`,background:withImages&&imageApiAvailable?"#0071e3":"transparent",flexShrink:0,display:'flex',alignItems:'center',justifyContent:'center',transition:'all .1s'}}>
          {withImages&&imageApiAvailable&&<svg width="9" height="9" fill="none" viewBox="0 0 10 10" stroke="#fff" strokeWidth={2.2}><path d="M2 5l2.5 2.5L8 2.5"/></svg>}
        </div>
        <div style={{flex:1}}>
          <div style={{fontSize:13,fontWeight:500,color:'var(--text-1)',display:'flex',alignItems:'center',gap:6}}>
            图文并茂
            {imageApiAvailable?<span className="tag-accent" style={{fontSize:9}}>可用</span>:<span className="tag" style={{fontSize:9}}>待配置</span>}
          </div>
          <div style={{fontSize:11,color:'var(--text-3)',marginTop:1}}>
            {imageApiAvailable?'自动为文章各段落标题配图':'管理员需在后台配置图片 API（nanobanana）'}
          </div>
        </div>
      </div>
    </Card>
  );
}



// ══════════════════════════════════════════════
//  Result panel
// ══════════════════════════════════════════════
function ResultPanel({text,generating,format,batchRunning,batchState}){
  const mdHtml=React.useMemo(()=>(!text||format!=='md'?'':marked.parse(text)),[text,format]);
  const htmlPrev=React.useMemo(()=>(!text||format!=='html'?'':marked.parse(text)),[text,format]);
  const plain=React.useMemo(()=>(!text||format!=='txt'?'':stripMd(text)),[text,format]);
  const Cursor=()=>(generating||batchRunning)?<span style={{display:'inline-block',width:6,height:'1.1em',background:'var(--accent)',borderRadius:2,marginLeft:3,verticalAlign:'middle',opacity:.8,animation:'pulse-dot 1s infinite'}}/>:null;
  if(!text&&!generating&&!batchRunning) return(
    <div style={{flex:1,display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',gap:12,padding:32,textAlign:'center',background:'var(--bg)'}}>
      <div style={{width:52,height:52,borderRadius:12,background:'rgba(0,113,227,0.10)',display:'flex',alignItems:'center',justifyContent:'center'}}>
        <svg width="22" height="22" fill="none" viewBox="0 0 24 24" stroke="var(--text-4)" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
      </div>
      <div><p style={{fontSize:13,color:'var(--text-3)',fontWeight:500}}>等待内容</p><p style={{fontSize:12,color:'var(--text-4)',marginTop:3}}>填写选题后点击「生成文章」</p></div>
    </div>
  );
  if(format==='md') return(<div style={{flex:1,overflowY:'auto',padding:'28px 36px',background:'var(--bg)'}}><div className="prose" dangerouslySetInnerHTML={{__html:mdHtml}}/><Cursor/></div>);
  if(format==='html') return(
    <div style={{flex:1,overflowY:'auto',padding:'20px 24px',background:'var(--bg)'}}>
      <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:14}}><span className="tag-accent">HTML 预览</span><span style={{fontSize:11,color:'var(--text-4)'}}>下载文件为 .txt，内容是完整 HTML 源码</span></div>
      <div style={{background:'#fff',borderRadius:8,padding:'28px 32px',boxShadow:'var(--shadow)'}}><div dangerouslySetInnerHTML={{__html:htmlPrev}} style={{fontFamily:'system-ui,sans-serif',lineHeight:1.8,color:'#1a1816',fontSize:14}}/></div>
      <Cursor/>
    </div>
  );
  return(<div style={{flex:1,overflowY:'auto',padding:'28px 36px',background:'var(--bg)'}}><pre style={{fontSize:13,color:'var(--text-2)',lineHeight:1.8,whiteSpace:'pre-wrap',fontFamily:'inherit'}}>{plain}<Cursor/></pre></div>);
}

// ══════════════════════════════════════════════
//  Main App
// ══════════════════════════════════════════════
function App(){
  const [authed,setAuthed]=React.useState(false);
  const [authLoading,setAuthLoading]=React.useState(true);
  const [authData,setAuthData]=React.useState(null);
  const [isDark,setIsDark]=React.useState(()=>localStorage.getItem(THEME_KEY)!=='light');
  const [showAdmin,setShowAdmin]=React.useState(false);
  const [showWorks,setShowWorks]=React.useState(false);

  // User config
  const [apiConfig,setApiConfig]=React.useState({baseUrl:'',apiKey:'',model:''});
  const [custom,setCustom]=React.useState([]);
  const [titleLibrary,setTitleLibrary]=React.useState([]);
  const [selTplId,setSelTplId]=React.useState(DEFAULT_TEMPLATES[0].id);
  const [fmt,setFmt]=React.useState('md');
  const [usePlatform,setUsePlatform]=React.useState(false);
  const [platformAvailable,setPlatformAvailable]=React.useState(false);
  const [maxThreads,setMaxThreads]=React.useState(1);
  const [platformThreadCount,setPlatformThreadCount]=React.useState(0);
  const [threadCount,setThreadCount]=React.useState(1);
  const [withImages,setWithImages]=React.useState(false);
  const [imageApiAvailable,setImageApiAvailable]=React.useState(false);

  // Single generation
  const [topic,setTopic]=React.useState('');
  const [note,setNote]=React.useState('');
  const [text,setText]=React.useState('');
  const [gen,setGen]=React.useState(false);
  const [err,setErr]=React.useState('');
  const [copied,setCopied]=React.useState(false);
  const abortRef=React.useRef(null);

  // Works
  const [works,setWorks]=React.useState([]);
  const [worksLoading,setWorksLoading]=React.useState(false);

  // Batch
  const [batchRunning,setBatchRunning]=React.useState(false);
  const [batchDone,setBatchDone]=React.useState(0);
  const [batchTotal,setBatchTotal]=React.useState(0);
  const [batchCurrent,setBatchCurrent]=React.useState('');
  const [batchErrors,setBatchErrors]=React.useState([]);
  const [batchWorkIds,setBatchWorkIds]=React.useState([]);
  const batchAbortRef=React.useRef(null);
  const [threadProgress,setThreadProgress]=React.useState([]);
  const [threadTexts,setThreadTexts]=React.useState([]);
  const [selectedThread,setSelectedThread]=React.useState(0);

  const saveTimer=React.useRef(null);

  // Theme sync
  React.useEffect(()=>{
    document.documentElement.setAttribute('data-theme',isDark?'dark':'light');
    localStorage.setItem(THEME_KEY,isDark?'dark':'light');
  },[isDark]);

  // Session restore
  React.useEffect(()=>{
    const token=localStorage.getItem(TOKEN_KEY);
    if(!token){setAuthLoading(false);return;}
    apiFetch('/api/user/config').then(data=>{
      const stored=JSON.parse(localStorage.getItem('ag_userinfo')||'null');
      setAuthData({user:stored});
      setApiConfig(data.apiConfig||{baseUrl:'',apiKey:'',model:''});
      setCustom(data.customTemplates||[]);
      setTitleLibrary(data.titleLibrary||[]);
      setSelTplId(data.selectedTpl||DEFAULT_TEMPLATES[0].id);
      setFmt(data.outputFormat||'md');
      setUsePlatform(!!data.usePlatform);
      setPlatformAvailable(!!data.platformAvailable);
      setImageApiAvailable(!!data.imageApiAvailable);
      setMaxThreads(data.maxThreads||1);
      setPlatformThreadCount(data.platformThreadCount||0);
      setThreadCount(1);
      setAuthed(true);
      setAuthLoading(false);
    }).catch(()=>{
      localStorage.removeItem(TOKEN_KEY);
      setAuthed(false);setAuthLoading(false);
    });
  },[]);

  // Load works when works panel opens
  const loadWorks=React.useCallback(async()=>{
    setWorksLoading(true);
    try{setWorks(await apiFetch('/api/works'));}
    catch{}
    finally{setWorksLoading(false);}
  },[]);
  React.useEffect(()=>{if(showWorks&&authed) loadWorks();},[showWorks]);

  const handleAuth=(data)=>{
    localStorage.setItem('ag_userinfo',JSON.stringify(data.user));
    setAuthData(data);
    setApiConfig(data.apiConfig||{baseUrl:'',apiKey:'',model:''});
    setCustom(data.customTemplates||[]);
    setTitleLibrary(data.titleLibrary||[]);
    setSelTplId(data.selectedTpl||DEFAULT_TEMPLATES[0].id);
    setFmt(data.outputFormat||'md');
    setUsePlatform(!!data.usePlatform);
    setPlatformAvailable(!!data.platformAvailable);
    setMaxThreads(data.maxThreads||1);
    setPlatformThreadCount(data.platformThreadCount||0);
    setThreadCount(Math.min(1, data.maxThreads||1));
    setAuthed(true);
  };
  const handleLogout=()=>{
    localStorage.removeItem(TOKEN_KEY);localStorage.removeItem('ag_userinfo');
    setAuthed(false);setAuthData(null);setText('');setTopic('');setNote('');
  };

  // Debounced server save
  const saveConfig=React.useCallback((patch)=>{
    clearTimeout(saveTimer.current);
    saveTimer.current=setTimeout(()=>apiFetch('/api/user/config',{method:'PUT',body:patch}).catch(()=>{}),800);
  },[]);
  const handleApiChange=(cfg)=>{setApiConfig(cfg);saveConfig({apiConfig:cfg});};
  const handleCustomChange=(fn)=>{setCustom(prev=>{const next=typeof fn==='function'?fn(prev):fn;saveConfig({customTemplates:next});return next;});};
  const handleLibraryChange=(fn)=>{setTitleLibrary(prev=>{const next=typeof fn==='function'?fn(prev):fn;saveConfig({titleLibrary:next});return next;});};
  const handleSelTpl=(id)=>{setSelTplId(id);saveConfig({selectedTpl:id});};
  const handleFmt=(f)=>{setFmt(f);saveConfig({outputFormat:f});};
  const handleUsePlatformChange=(v)=>{setUsePlatform(v);saveConfig({usePlatform:v});};

  // Save work helper
  const saveWork=async(title,content)=>{
    if(!content) return;
    try{
      await apiFetch('/api/works',{method:'POST',body:{title,content,format:fmt,templateName:activeTpl.name}});
      if(showWorks) loadWorks();
    }catch{}
  };

  const allTpls=[...DEFAULT_TEMPLATES,...custom];
  const activeTpl=allTpls.find(t=>t.id===selTplId)||DEFAULT_TEMPLATES[0];
  const apiReady=usePlatform?platformAvailable:(apiConfig.baseUrl&&apiConfig.apiKey&&apiConfig.model);
  const canGen=!gen&&!batchRunning&&topic.trim()&&apiReady;

  // Single generation
  const generate=async()=>{
    if(!topic.trim()) return;
    setText('');setErr('');setGen(true);
    const ctrl=new AbortController();abortRef.current=ctrl;
    const userMsg=activeTpl.userTpl.replace('{topic}',topic.trim()).replace('{note}',note.trim()||'无');
    let full='';
    const msgs=[{role:'system',content:activeTpl.system},{role:'user',content:userMsg}];
    try{
      if(usePlatform){
        await streamProxy({messages:msgs,onChunk:c=>{full+=c;setText(p=>p+c);},signal:ctrl.signal});
      } else {
        await streamContent({apiConfig,messages:msgs,onChunk:c=>{full+=c;setText(p=>p+c);},signal:ctrl.signal});
      }
      let finalText=full;
      if(withImages&&imageApiAvailable){
        setText(p=>p+'\n\n_正在配图…_');
        finalText=await insertImages(full);
        setText(finalText);
      }
      saveWork(topic.trim(), finalText);
    }catch(e){if(e.name!=='AbortError') setErr(e.message||'生成失败');}
    finally{setGen(false);abortRef.current=null;}
  };

  // Batch generation
  const runBatch=async(queue)=>{
    if(!queue.length||!apiReady){setErr(usePlatform?'平台 API 未配置，请联系管理员':'请先完成 API 配置');return;}
    setBatchRunning(true);setBatchDone(0);setBatchTotal(queue.length);setBatchCurrent('');setBatchErrors([]);setBatchWorkIds([]);
    const ctrl=new AbortController();batchAbortRef.current=ctrl;

    const actualThreads = Math.min(threadCount, queue.length);

    if(actualThreads<=1){
      // Sequential (existing logic)
      for(let i=0;i<queue.length;i++){
        if(ctrl.signal.aborted) break;
        const title=queue[i];
        setBatchCurrent(title);
        setText('');
        let content='';
        try{
          const userMsg=activeTpl.userTpl.replace('{topic}',title).replace('{note}','无');
          const msgs=[{role:'system',content:activeTpl.system},{role:'user',content:userMsg}];
          if(usePlatform){
            await streamProxy({messages:msgs,onChunk:c=>{content+=c;setText(p=>p+c);},signal:ctrl.signal,threadIdx:0});
          } else {
            await streamContent({apiConfig,messages:msgs,onChunk:c=>{content+=c;setText(p=>p+c);},signal:ctrl.signal});
          }
          if(withImages&&imageApiAvailable&&content){ content=await insertImages(content); setText(content); }
          if(content){
            const saved=await apiFetch('/api/works',{method:'POST',body:{title,content,format:fmt,templateName:activeTpl.name}});
            setBatchWorkIds(p=>[...p,saved.id]);
          }
          if(showWorks) loadWorks();
        }catch(e){
          if(e.name==='AbortError') break;
          setBatchErrors(p=>[...p,`「${title}」: ${e.message}`]);
        }
        setBatchDone(i+1);
      }
    } else {
      // Concurrent worker pool
      const taskQueue=[...queue];
      let globalDone=0;
      setThreadProgress(Array.from({length:actualThreads},(_,i)=>({idx:i,status:'idle',title:'',chars:0})));
      setThreadTexts(Array(actualThreads).fill(''));
      setSelectedThread(0);

      const worker=async(tIdx)=>{
        while(true){
          if(ctrl.signal.aborted) break;
          const title=taskQueue.shift();
          if(title===undefined) break;
          setBatchCurrent(title);
          setThreadProgress(p=>p.map((t,i)=>i===tIdx?{...t,status:'generating',title,chars:0}:t));
          setThreadTexts(p=>{const n=[...p];n[tIdx]='';return n;});
          let content='';
          try{
            const userMsg=activeTpl.userTpl.replace('{topic}',title).replace('{note}','无');
            const msgs=[{role:'system',content:activeTpl.system},{role:'user',content:userMsg}];
            const onChunk=c=>{
              content+=c;
              setThreadProgress(p=>p.map((t,i)=>i===tIdx?{...t,chars:content.length}:t));
              setThreadTexts(p=>{const n=[...p];n[tIdx]=content;return n;});
            };
            if(usePlatform){
              await streamProxy({messages:msgs,onChunk,signal:ctrl.signal,threadIdx:tIdx});
            } else {
              await streamContent({apiConfig,messages:msgs,onChunk,signal:ctrl.signal});
            }
            if(withImages&&imageApiAvailable&&content){ content=await insertImages(content);setThreadTexts(p=>{const n=[...p];n[tIdx]=content;return n;}); }
            setThreadProgress(p=>p.map((t,i)=>i===tIdx?{...t,status:'done',chars:content.length}:t));
            if(content){
              const saved=await apiFetch('/api/works',{method:'POST',body:{title,content,format:fmt,templateName:activeTpl.name}});
              setBatchWorkIds(p=>[...p,saved.id]);
              if(showWorks) loadWorks();
            }
          }catch(e){
            if(e.name==='AbortError') break;
            setThreadProgress(p=>p.map((t,i)=>i===tIdx?{...t,status:'error',chars:0}:t));
            setBatchErrors(p=>[...p,`「${title}」: ${e.message}`]);
          }
          globalDone++;
          setBatchDone(globalDone);
          await new Promise(r=>setTimeout(r,300));
          setThreadProgress(p=>p.map((t,i)=>i===tIdx?{...t,status:'idle',title:'',chars:0}:t));
        }
      };

      await Promise.all(Array.from({length:actualThreads},(_,i)=>worker(i)));

      if(!ctrl.signal.aborted) setText('');
    }

    setBatchRunning(false);batchAbortRef.current=null;setBatchCurrent('');
    loadWorks();
  };

  const [zipLoading,setZipLoading]=React.useState(false);
  const downloadBatchZip=async(workIds)=>{
    if(!workIds?.length||zipLoading) return;
    setZipLoading(true);
    try{
      const zip=new JSZip();
      const usedNames={};
      for(const id of workIds){
        try{
          const work=await apiFetch(`/api/works/${id}`);
          let name=workFilename(work);
          // Deduplicate: if same filename already exists, append _2, _3, ...
          if(usedNames[name]){
            usedNames[name]++;
            const dot=name.lastIndexOf('.');
            name=dot>=0?name.slice(0,dot)+'_'+usedNames[name]+name.slice(dot):name+'_'+usedNames[name];
          } else {
            usedNames[name]=1;
          }
          zip.file(name, workContent(work));
        }catch{}
      }
      const blob=await zip.generateAsync({type:'blob'});
      const a=Object.assign(document.createElement('a'),{href:URL.createObjectURL(blob),download:`批量生成_${new Date().toLocaleDateString('zh-CN').replace(/\//g,'-')}.zip`});
      document.body.appendChild(a);a.click();document.body.removeChild(a);
    }finally{
      setZipLoading(false);
    }
  };

  const activeText=()=>(batchRunning&&threadProgress.length>1)?(threadTexts[selectedThread]||''):text;
  const activeTopic=()=>(batchRunning&&threadProgress.length>1)?(threadProgress[selectedThread]?.title||topic):topic;
  const copy=async()=>{
    const src=activeText();
    let out=src;
    if(fmt==='txt') out=stripMd(src);
    else if(fmt==='html') out=buildHTML(src,activeTopic());
    await navigator.clipboard?.writeText(out);
    setCopied(true);setTimeout(()=>setCopied(false),2000);
  };
  const dlSingle=()=>{
    const src=activeText();const tp=activeTopic();
    let out=src,fn=safeName(tp);
    if(fmt==='txt'){out=stripMd(src);fn+='.txt';}
    else if(fmt==='html'){out=buildHTML(src,tp);fn+='.txt';}
    else fn+='.md';
    download(out,fn);
  };

  const handleAddToLibrary=(titles)=>{
    handleLibraryChange(prev=>{
      const exist=new Set(prev.map(t=>t.title));
      const news=titles.filter(t=>!exist.has(t)).map(t=>({id:Date.now()+Math.random().toString(36).slice(2),title:t,sel:true}));
      return [...prev,...news];
    });
  };

  const deleteWork=async(id)=>{try{await apiFetch(`/api/works/${id}`,{method:'DELETE'});setWorks(p=>p.filter(w=>w.id!==id));}catch(e){alert(e.message);}};
  const deleteAllWorks=async()=>{try{await apiFetch('/api/works',{method:'DELETE'});setWorks([]);}catch(e){alert(e.message);}};

  const ext=(fmt==='md'?'MD':'TXT');
  const user=authData?.user||JSON.parse(localStorage.getItem('ag_userinfo')||'{}');

  if(authLoading) return(
    <div style={{height:'100vh',display:'flex',alignItems:'center',justifyContent:'center',background:'transparent'}}>
      <span style={{color:'var(--text-3)',display:'flex',gap:8}}>{Ic.Spin} 加载中…</span>
    </div>
  );
  if(!authed) return <AuthScreen onAuth={handleAuth}/>;

  const batchState={running:batchRunning,done:batchDone,total:batchTotal,current:batchCurrent,errors:batchErrors,workIds:batchWorkIds};

  return(
    <div style={{height:'100vh',display:'flex',flexDirection:'column',background:'var(--bg)',overflow:'hidden'}}>

      {/* ── Topbar ── */}
      <div className="topbar">
        <div style={{display:'flex',alignItems:'center',gap:9}}>
          <div style={{width:30,height:30,borderRadius:8,background:'#0071e3',display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0}}>
            <svg width="14" height="14" fill="#fff" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
          </div>
          <span style={{fontSize:14,fontWeight:700,color:'var(--text-1)',letterSpacing:'-.01em'}}>南阳市择宸科技内容流</span>
          <span style={{fontSize:10,color:'var(--text-4)',background:'var(--bg-card-2)',borderRadius:9999,padding:'1px 7px',fontFamily:'ui-monospace,monospace',letterSpacing:'.02em'}}>v4.13.0</span>
        </div>

        <div style={{flex:1,display:'flex',justifyContent:'center'}}>
          {(batchRunning?batchCurrent:topic)&&(
            <span style={{fontSize:12,color:'var(--text-3)',maxWidth:300,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',background:'var(--bg-card-2)',borderRadius:980,padding:'3px 14px'}}>
              {batchRunning?`批量 ${batchDone}/${batchTotal} · ${batchCurrent}`:topic}
            </span>
          )}
        </div>

        <div style={{display:'flex',alignItems:'center',gap:8}}>
          <button className="btn-ghost" style={{padding:'5px 10px',fontSize:12,position:'relative'}} onClick={()=>setShowWorks(true)}>
            {Ic.Books} 已生成作品
            {works.length>0&&<span style={{position:'absolute',top:-4,right:-4,background:'var(--accent)',color:'#fff',borderRadius:99,fontSize:9,padding:'1px 5px',fontWeight:700}}>{works.length}</span>}
          </button>
          {user.isAdmin&&<button className="btn-ghost" style={{padding:'5px 10px',fontSize:12}} onClick={()=>setShowAdmin(true)}>{Ic.Shield} 管理</button>}
          <button className="btn-theme" title={isDark?'切换白天模式':'切换夜间模式'} onClick={()=>setIsDark(v=>!v)}>
            {isDark?Ic.Sun:Ic.Moon}
          </button>
          <UserMenu user={user} onLogout={handleLogout}/>
        </div>
      </div>

      {/* ── Body ── */}
      <div style={{flex:1,display:'flex',overflow:'hidden'}}>

        {/* Left column */}
        <aside style={{width:'42%',minWidth:300,maxWidth:460,display:'flex',flexDirection:'column',borderRight:'1px solid var(--border)',overflow:'hidden'}}>
          <div style={{flex:1,overflowY:'auto',padding:'12px 12px 0',display:'flex',flexDirection:'column',gap:10}}>
            <ApiConfig config={apiConfig} onChange={handleApiChange} usePlatform={usePlatform} onUsePlatformChange={handleUsePlatformChange} platformAvailable={platformAvailable}/>
            <TopicInput topic={topic} note={note} onTopicChange={setTopic} onNoteChange={setNote} apiConfig={apiConfig} onAddToLibrary={handleAddToLibrary} usePlatform={usePlatform} platformAvailable={platformAvailable}/>
            <TplSelector selectedId={selTplId} onSelect={handleSelTpl} custom={custom} onCustomChange={handleCustomChange}/>
            <FmtSelector value={fmt} onChange={handleFmt}/>
            <BatchCard titleLibrary={titleLibrary} onLibraryChange={handleLibraryChange} batchState={batchState} onRun={runBatch} onStop={()=>batchAbortRef.current?.abort()} onDownloadZip={downloadBatchZip} zipLoading={zipLoading} maxThreads={maxThreads} platformThreadCount={platformThreadCount} threadCount={threadCount} setThreadCount={setThreadCount} usePlatform={usePlatform} threadProgress={threadProgress}/>
            <div style={{height:12}}/>
          </div>

          <div className="action-footer">
            {err&&(
              <div style={{display:'flex',gap:8,padding:'8px 10px',borderRadius:8,background:'var(--error-bg)',border:'1px solid color-mix(in srgb,var(--error) 30%,transparent)',marginBottom:10}}>
                <span style={{color:'var(--error)',flexShrink:0,marginTop:1}}>{Ic.X}</span>
                <p style={{fontSize:12,color:'var(--error)',lineHeight:1.5}}>{err}</p>
              </div>
            )}
            <div
              onClick={()=>imageApiAvailable&&setWithImages(v=>!v)}
              style={{display:'flex',alignItems:'center',gap:8,marginBottom:8,padding:'7px 10px',borderRadius:8,border:`1px solid ${withImages&&imageApiAvailable?'rgba(0,113,227,0.40)':'var(--border-soft)'}`,background:withImages&&imageApiAvailable?"rgba(0,113,227,0.12)":"var(--bg-card-2)",cursor:imageApiAvailable?'pointer':'not-allowed',opacity:imageApiAvailable?1:0.45,transition:'all .15s',userSelect:'none'}}>
              <div style={{width:14,height:14,borderRadius:3,border:`1.5px solid ${withImages&&imageApiAvailable?'#0071e3':'rgba(255,255,255,0.25)'}`,background:withImages&&imageApiAvailable?'#0071e3':'transparent',flexShrink:0,display:'flex',alignItems:'center',justifyContent:'center',transition:'all .12s'}}>
                {withImages&&imageApiAvailable&&<svg width="9" height="9" fill="none" viewBox="0 0 10 10" stroke="#fff" strokeWidth={2.2}><path d="M2 5l2.5 2.5L8 2.5"/></svg>}
              </div>
              <span style={{fontSize:12,color:'var(--text-2)',flex:1}}>启用图文并茂功能</span>
              {!imageApiAvailable&&<span className="tag" style={{fontSize:9}}>待配置</span>}
              {withImages&&imageApiAvailable&&<span className="tag-accent" style={{fontSize:9}}>已启用</span>}
            </div>
            {gen?(
              <button className="btn-danger" style={{width:'100%',justifyContent:'center',padding:'10px 16px'}} onClick={()=>abortRef.current?.abort()}>{Ic.Stop} 停止生成</button>
            ):(
              <button className="btn-primary" style={{width:'100%',justifyContent:'center',padding:'10px 16px'}} onClick={generate} disabled={!canGen}>{Ic.Bolt} 生成文章</button>
            )}
            {!canGen&&!gen&&!batchRunning&&<p style={{textAlign:'center',fontSize:11,color:'var(--text-4)',marginTop:6}}>{!topic.trim()?'请先填写选题标题':usePlatform?'平台 API 未配置':'请先完成 API 配置'}</p>}
          </div>
        </aside>

        {/* Right column */}
        <main style={{flex:1,display:'flex',flexDirection:'column',overflow:'hidden'}}>
          {/* Header bar */}
          <div style={{flexShrink:0,height:44,display:'flex',alignItems:'center',gap:8,padding:'0 16px',borderBottom:'1px solid var(--border-soft)',background:'var(--bg-card)'}}>
            <span style={{fontSize:11,color:'var(--text-3)',textTransform:'uppercase',letterSpacing:'.04em'}}>
              {batchRunning&&threadProgress.length>1
                ?`T${selectedThread+1} · ${threadProgress[selectedThread]?.title||'待机中'}`
                :batchRunning?`批量生成中 ${batchDone}/${batchTotal}`:gen?'生成中…':text?`${text.length} 字符`:'等待内容'}
            </span>
            {(gen||batchRunning)&&<span style={{color:'var(--accent)',display:'flex'}}>{Ic.Spin}</span>}
            <div style={{flex:1}}/>
            <button className="btn-ghost" disabled={!(batchRunning&&threadProgress.length>1?threadTexts[selectedThread]:text)} onClick={copy}>{copied?Ic.Check:Ic.Copy} {copied?'已复制':'复制'}</button>
            <button className="btn-ghost" disabled={!(batchRunning&&threadProgress.length>1?threadTexts[selectedThread]:text)} onClick={dlSingle}>{Ic.Dl} 下载 {ext}</button>
          </div>

          {/* Multi-thread window tabs */}
          {batchRunning&&threadProgress.length>1&&(
            <div style={{flexShrink:0,padding:'10px 16px 0',borderBottom:'1px solid var(--border-soft)',background:'var(--bg-card)',overflowX:'auto'}}>
              <div style={{display:'flex',gap:6,paddingBottom:10,minWidth:'max-content'}}>
                {threadProgress.map(t=>{
                  const isGen=t.status==='generating';
                  const isDone=t.status==='done';
                  const isErr=t.status==='error';
                  const isSel=selectedThread===t.idx;
                  return(
                    <div key={t.idx} onClick={()=>setSelectedThread(t.idx)}
                      style={{minWidth:150,maxWidth:200,borderRadius:10,padding:'8px 11px',border:'1px solid',transition:'all .15s',cursor:'pointer',userSelect:'none',
                        background:isSel?'rgba(79,95,204,0.18)':isGen?'rgba(79,95,204,0.07)':isDone?'rgba(52,211,153,0.06)':isErr?'rgba(248,113,113,0.06)':'var(--bg-sunken)',
                        borderColor:isSel?'rgba(79,95,204,0.7)':isGen?'rgba(79,95,204,0.35)':isDone?'rgba(52,211,153,0.25)':isErr?'rgba(248,113,113,0.25)':'var(--border)',
                        boxShadow:isSel?'0 0 0 1px rgba(79,95,204,0.25)':'none'}}>
                      <div style={{display:'flex',alignItems:'center',gap:5,marginBottom:4}}>
                        <span style={{fontSize:11,fontWeight:700,fontFamily:'ui-monospace,monospace',color:isSel?'#818cf8':isGen?'var(--accent)':isDone?'#34d399':isErr?'#f87171':'var(--text-4)'}}>T{t.idx+1}</span>
                        <span style={{fontSize:11,color:isGen?'var(--accent)':isDone?'#34d399':isErr?'#f87171':'var(--text-4)'}}>
                          {isGen?Ic.Spin:isDone?'✓':isErr?'✗':'—'}
                        </span>
                        {t.chars>0&&<span style={{fontSize:9,color:'var(--text-4)',marginLeft:'auto',fontFamily:'ui-monospace,monospace'}}>{t.chars}字</span>}
                      </div>
                      <p style={{fontSize:10,color:isSel?'var(--text-2)':isGen?'var(--text-3)':isDone?'var(--text-3)':'var(--text-4)',lineHeight:1.4,overflow:'hidden',display:'-webkit-box',WebkitLineClamp:2,WebkitBoxOrient:'vertical',margin:0}}>
                        {t.title||'待机中…'}
                      </p>
                      {isGen&&(
                        <div style={{marginTop:5,height:2,borderRadius:99,background:'var(--border)',overflow:'hidden'}}>
                          <div style={{height:'100%',borderRadius:99,background:isSel?'#818cf8':'var(--accent)',width:'100%',animation:'indeterminate 1.5s ease infinite',transformOrigin:'left'}}/>
                        </div>
                      )}
                    </div>
                  );
                })}
              </div>
            </div>
          )}

          {/* Preview area — shows selected thread text or single-gen text */}
          <ResultPanel
            text={batchRunning&&threadProgress.length>1?(threadTexts[selectedThread]||''):text}
            generating={gen||(batchRunning&&threadProgress[selectedThread]?.status==='generating')}
            format={fmt}
            batchRunning={batchRunning&&threadProgress.length<=1}
            batchState={batchState}
          />
        </main>
      </div>

      {showWorks&&<WorksPanel works={works} loadingWorks={worksLoading} onRefresh={loadWorks} onDelete={deleteWork} onDeleteAll={deleteAllWorks} onClose={()=>setShowWorks(false)}/>}
      {showAdmin&&<AdminPanel currentUser={user} onClose={()=>setShowAdmin(false)} onSelfThreadChange={n=>{setMaxThreads(n);setThreadCount(1);}}/>}
    </div>
  );
}

const root=ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);
