/* 会议速记 · 手机版(2026-07-02)· chrome-free 独立移动页
 * 流:选项目 → 传录音(签名直传 Storage)→ 听悟转写轮询 → AI 拆条草稿逐条采纳 → 确认入库 + 邮件分发
 * 状态存 localStorage('hx_ma_job'):锁屏/关页可恢复;收件人按项目记忆('hx_ma_recipients:<pid>')。
 * 接 /api/m03/meeting-audio/*(upload-url / submit / status / confirm)+ /api/m03/projects。
 * 红线:AI 拆条只是初稿(✨ 标注),人逐条采纳才入 handover_minutes;fetch 失败一律红字可重试,不静默。 */
const { useState, useEffect, useRef } = React;

const JOB_KEY = 'hx_ma_job';
const CATEGORIES = ['采购', '设计', '施工', '商务', '进度', '安全质量', '其他'];
const KIND_META = {
  decision: { label: '决议', style: { color: 'var(--danger)', border: '1.5px solid var(--danger)', background: 'transparent' } },
  todo:     { label: '待办', style: { color: '#fff', border: '1.5px solid var(--accent)', background: 'var(--accent)' } },
  risk:     { label: '风险', style: { color: 'var(--warn)', border: '1.5px solid var(--warn)', background: 'var(--warn-soft)' } },
  info:     { label: '信息', style: { color: 'var(--ink-3)', border: '1.5px solid var(--line)', background: 'var(--chip)' } },
};

function authHeaders(json) {
  const h = json ? { 'Content-Type': 'application/json' } : {};
  return window.HX_authHeaders(h);
}
function fetchJson(url, opts) {
  return fetch(url, opts).then(r => r.json().catch(() => ({})).then(d => ({ ok: r.ok, status: r.status, d })));
}
function apiMsg(res, fallback) {
  if (res.status === 401) return '登录已过期,请重新在平台登录后回来';
  return (res.d && (res.d.message || res.d.error)) || (fallback + ' · HTTP ' + res.status);
}
function today() {
  const d = new Date();
  return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
}
function loadJob() {
  try { const raw = localStorage.getItem(JOB_KEY); return raw ? JSON.parse(raw) : null; } catch (e) { return null; }
}
function recipKey(pid) { return 'hx_ma_recipients:' + pid; }
function parseEmails(text) {
  return String(text || '').split(/\n/).map(s => s.trim()).filter(s => /^\S+@\S+\.\S+$/.test(s));
}

const KindBadge = ({ kind }) => {
  const m = KIND_META[kind] || KIND_META.info;
  return <span className="ma-kind" style={m.style}>{m.label}</span>;
};

const Toggle = ({ on, onChange }) => (
  <button type="button" className={'ma-switch' + (on ? ' on' : '')} onClick={onChange} aria-label={on ? '已采纳,点击排除' : '已排除,点击采纳'}>
    <span className="track"><span className="knob" /></span>
  </button>
);

const ErrBar = ({ msg, onRetry, retryLabel }) => (
  <div className="ma-err">
    <span className="msg">{msg}</span>
    {onRetry ? <button className="ma-btn sm" style={{ borderColor: 'var(--danger)', color: 'var(--danger)', background: 'transparent' }} onClick={onRetry}>{retryLabel || '重试'}</button> : null}
  </div>
);

/* ========== ① 登录门 ========== */
const LoginGate = () => (
  <div className="ma-card" style={{ textAlign: 'center', padding: '42px 20px' }}>
    <div style={{ fontSize: 30, marginBottom: 12 }}>🔒</div>
    <div style={{ fontSize: 16, fontWeight: 700, marginBottom: 8 }}>请先在平台登录</div>
    <div className="ma-sub" style={{ marginBottom: 18 }}>会议速记复用平台登录态,登录后会自动回到本页。</div>
    <a className="ma-btn primary" style={{ display: 'inline-block', width: 'auto', padding: '13px 34px', textDecoration: 'none', lineHeight: '22px' }} href={'/login?return=' + encodeURIComponent(location.pathname + location.search)}>去登录</a>
    <div style={{ marginTop: 14 }}>
      <button className="ma-btn sm" onClick={() => location.reload()}>我已登录 · 刷新本页</button>
    </div>
  </div>
);

/* ========== ② 选项目 ========== */
function ProjectPick({ onPick }) {
  const [items, setItems] = useState(null); // null=加载中
  const [err, setErr] = useState(null);
  const [tick, setTick] = useState(0);
  const [rawCount, setRawCount] = useState(0); // 过滤前的可见项目数(区分「真无项目」vs「有但全归档」)

  useEffect(() => {
    let live = true;
    setErr(null); setItems(null);
    fetchJson('/api/m03/projects', { headers: authHeaders() })
      .then(res => {
        if (!live) return;
        if (!res.ok) { setErr(apiMsg(res, '读项目列表失败')); setItems([]); return; }
        // 只显示进行中的项目:已归档阶段(status='closed')不记新会议 → 一楼栋只剩当前阶段
        // (波克只剩施工 COM-C · 图书馆只剩售后 COM-W · 售前/完工阶段不再冒出来)
        const raw = res.d.items || [];
        setRawCount(raw.length);
        const list = raw.filter(p => p.status !== 'closed').slice().sort((a, b) => {
          const ac = String(a.code || ''), bc = String(b.code || '');
          const aw = ac.startsWith('COM-C') ? 0 : 1, bw = bc.startsWith('COM-C') ? 0 : 1;
          return aw - bw || bc.localeCompare(ac);
        });
        setItems(list);
      })
      .catch(e => { if (live) { setErr('网络错误:' + String(e)); setItems([]); } });
    return () => { live = false; };
  }, [tick]);

  return (
    <div>
      <div className="ma-sub" style={{ margin: '2px 2px 10px' }}>这场会记到哪个项目?</div>
      {err ? <ErrBar msg={err} onRetry={() => setTick(t => t + 1)} /> : null}
      {items === null ? <div className="ma-card" style={{ color: 'var(--ink-3)' }}><span className="ma-pulse" style={{ marginRight: 8 }} />读取项目列表…</div> : null}
      {items && items.length === 0 && !err ? <div className="ma-card" style={{ color: 'var(--ink-3)' }}>{rawCount > 0 ? '你名下项目均已归档(售前已结束 / 已完工),暂不能新建会议纪要。' : '你名下暂无可见项目。'}</div> : null}
      {(items || []).map(p => (
        <button key={p.id} className="ma-row" onClick={() => onPick({ id: p.id, code: p.code, name: p.name })}>
          <div className="code">{p.code}</div>
          <div className="name">{p.name}</div>
        </button>
      ))}
    </div>
  );
}

/* ========== ③ 传录音 ========== */
function UploadStep({ project, jobTitle, setJobTitle, onSubmitted, onChangeProject }) {
  const [phase, setPhase] = useState(null); // null | signing | uploading | submitting
  const [err, setErr] = useState(null);
  const fileRef = useRef(null);
  const inputRef = useRef(null);

  async function doUpload(file) {
    fileRef.current = file;
    setErr(null);
    try {
      setPhase('signing');
      const r1 = await fetchJson('/api/m03/meeting-audio/upload-url', {
        method: 'POST', headers: authHeaders(true),
        body: JSON.stringify({ project_id: project.id, filename: file.name }),
      });
      if (!r1.ok) throw new Error(apiMsg(r1, '获取上传通道失败'));

      setPhase('uploading');
      // Supabase signed upload URL 直接 PUT(token 已在 URL query;raw body + content-type,同 storage-js uploadToSignedUrl)
      const put = await fetch(r1.d.signedUrl, {
        method: 'PUT',
        headers: { 'content-type': file.type || 'application/octet-stream', 'x-upsert': 'false' },
        body: file,
      });
      if (!put.ok) {
        const t = await put.text().catch(() => '');
        throw new Error('存储上传失败 · HTTP ' + put.status + (t ? ' · ' + t.slice(0, 200) : ''));
      }

      setPhase('submitting');
      // 兜底标题不再用录音文件名(那只是文件名不是会议名);AI 拆条会按内容重起,人在审核步可改
      const title = (jobTitle || '').trim() || (project.name + ' 会议纪要 ' + today());
      const r2 = await fetchJson('/api/m03/meeting-audio/submit', {
        method: 'POST', headers: authHeaders(true),
        body: JSON.stringify({ project_id: project.id, path: r1.d.path, title }),
      });
      if (!r2.ok) throw new Error(apiMsg(r2, '提交转写失败'));
      onSubmitted(r2.d.job_id, title);
    } catch (e) {
      setErr(String((e && e.message) || e));
      setPhase(null);
    }
  }

  const busy = phase !== null;
  const file = fileRef.current;
  const PHASE_CN = { signing: '获取上传通道…', uploading: '上传中…请勿关闭页面', submitting: '提交听悟转写任务…' };

  return (
    <div>
      <div className="ma-card">
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
          <div>
            <div className="ma-mono">{project.code}</div>
            <div style={{ fontSize: 15, fontWeight: 700, marginTop: 2 }}>{project.name}</div>
          </div>
          <button className="ma-btn sm" onClick={onChangeProject} disabled={busy}>换项目</button>
        </div>
        <div className="ma-sub" style={{ marginTop: 10 }}>标题不用先起 —— AI 会按会议内容自动命名,你在下一步逐条核对时可微调。</div>
      </div>

      {err ? <ErrBar msg={err} onRetry={file ? () => doUpload(file) : null} retryLabel="重传" /> : null}

      {busy ? (
        <div className="ma-card" style={{ textAlign: 'center', padding: '30px 16px' }}>
          <span className="ma-pulse" style={{ marginRight: 8 }} />
          <span style={{ fontWeight: 600 }}>{PHASE_CN[phase]}</span>
          {file ? <div className="ma-sub" style={{ marginTop: 8 }}>{file.name} · {(file.size / 1024 / 1024).toFixed(1)} MB</div> : null}
        </div>
      ) : (
        <div>
          <input ref={inputRef} type="file" accept="audio/*" style={{ display: 'none' }}
            onChange={e => { const f = e.target.files && e.target.files[0]; e.target.value = ''; if (f) doUpload(f); }} />
          <button className="ma-btn primary big" onClick={() => inputRef.current && inputRef.current.click()}>🎙 选择会议录音上传</button>
          <div className="ma-sub" style={{ margin: '10px 4px' }}>选中即开始上传 → 听悟转写 → AI 拆条初稿 → 你逐条核对后入库并邮件分发。</div>
        </div>
      )}
    </div>
  );
}

/* ========== ④ 转写轮询 ========== */
function PollStep({ jobId, onDrafted, onConfirmedAlready, onRestart }) {
  const [state, setState] = useState({ status: 'transcribing' }); // {status, transcript, note, failMsg}
  const [pollErr, setPollErr] = useState(null);
  const [manual, setManual] = useState(false); // 跳过 AI 手动整理

  useEffect(() => {
    let stop = false;
    const tick = () => {
      fetchJson('/api/m03/meeting-audio/status?job_id=' + encodeURIComponent(jobId), { headers: authHeaders() })
        .then(res => {
          if (stop) return;
          if (!res.ok) { setPollErr(apiMsg(res, '查转写状态失败') + ' · 5 秒后自动重试'); return; }
          setPollErr(null);
          const d = res.d;
          if (d.status === 'failed') { setState({ status: 'failed', failMsg: d.error || '听悟转写失败' }); return; }
          if (d.status === 'drafted') { onDrafted(d); return; }
          if (d.status === 'confirmed') { onConfirmedAlready(); return; }
          setState({ status: d.status, transcript: d.transcript_text, note: d.note });
        })
        .catch(e => { if (!stop) setPollErr('网络错误:' + String(e) + ' · 5 秒后自动重试'); });
    };
    tick();
    const iv = setInterval(tick, 5000);
    return () => { stop = true; clearInterval(iv); };
  }, [jobId]);

  if (manual) return null; // App 层已切 review(见 onManual 调用处)

  if (state.status === 'failed') {
    return (
      <div>
        <ErrBar msg={'转写失败:' + state.failMsg} />
        <button className="ma-btn" onClick={onRestart}>再记一场</button>
      </div>
    );
  }

  if (state.status === 'transcribed') {
    const note = state.note || '';
    return (
      <div>
        <div className="ma-card">
          <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 6 }}>转写完成 · 等 AI 拆条</div>
          {note === 'llm_not_configured'
            ? <div className="ma-sub" style={{ marginBottom: 10 }}>AI 拆条在服务机环境可用(本环境未配 LLM)。可先看转写全文,或跳过 AI 手动整理入库。</div>
            : <div className="ma-sub" style={{ marginBottom: 10 }}>{note ? 'AI 拆条暂时失败,每 5 秒自动重试(' + note + ')' : 'AI 拆条中…每 5 秒自动查一次'}</div>}
          <div className="ma-transcript">{state.transcript || '(转写文本为空)'}</div>
        </div>
        {pollErr ? <ErrBar msg={pollErr} /> : null}
        <button className="ma-btn" onClick={() => { setManual(true); onDrafted({ draft: null, transcript_text: state.transcript }, true); }}>不等 AI · 手动整理入库</button>
      </div>
    );
  }

  return (
    <div>
      <div className="ma-card" style={{ textAlign: 'center', padding: '38px 18px' }}>
        <span className="ma-pulse" style={{ marginRight: 9 }} />
        <span style={{ fontSize: 16, fontWeight: 700 }}>听悟转写中…</span>
        <div className="ma-sub" style={{ marginTop: 10 }}>可锁屏稍后回来,进度已保存,回来自动续上。</div>
        <div className="ma-mono" style={{ marginTop: 12 }}>job {jobId}</div>
      </div>
      {pollErr ? <ErrBar msg={pollErr} /> : null}
    </div>
  );
}

/* ========== ⑤+⑥ 草稿审核 + 收件人 + 确认 ========== */
function ReviewStep({ project, jobId, aiDraft, title, setTitle, date, setDate, items, setItems, onDone, onRestart }) {
  const [pickerIdx, setPickerIdx] = useState(null);
  const [kindPickerIdx, setKindPickerIdx] = useState(null); // 类型(kind)选择器:决议/待办/风险/信息 · 驱动下游分拣
  const [editIdx, setEditIdx] = useState(null);
  const [recipText, setRecipText] = useState(() => {
    try { return localStorage.getItem(recipKey(project.id)) || ''; } catch (e) { return ''; }
  });
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  useEffect(() => {
    try { localStorage.setItem(recipKey(project.id), recipText); } catch (e) {}
  }, [recipText, project.id]);

  const patch = (i, p) => setItems(list => list.map((it, k) => (k === i ? Object.assign({}, it, p) : it)));
  const adopted = items.filter(it => it.adopted && it.text.trim());
  const emails = parseEmails(recipText);

  function confirm() {
    setBusy(true); setErr(null);
    fetchJson('/api/m03/meeting-audio/confirm', {
      method: 'POST', headers: authHeaders(true),
      body: JSON.stringify({
        job_id: jobId,
        title: title.trim(),
        meeting_date: date || undefined,
        items: adopted.map(it => ({ text: it.text.trim(), category: it.category, owner: it.owner.trim(), kind: it.kind })),
        recipients: emails,
      }),
    })
      .then(res => {
        setBusy(false);
        if (!res.ok) { setErr(apiMsg(res, '确认入库失败')); return; }
        onDone(res.d);
      })
      .catch(e => { setBusy(false); setErr('网络错误:' + String(e)); });
  }

  return (
    <div>
      <div className="ma-card">
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
          <div className="ma-mono">{project.code} · {project.name}</div>
          {aiDraft ? <span className="ma-ai-chip">✨ AI 初稿 · 逐条核对</span> : <span className="ma-sub">手动整理</span>}
        </div>
        <label className="ma-label">纪要标题</label>
        <input className="ma-input" value={title} onChange={e => setTitle(e.target.value)} placeholder="会议标题" />
        <label className="ma-label">会议日期</label>
        <input className="ma-input" type="date" value={date} onChange={e => setDate(e.target.value)} />
      </div>

      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '4px 2px 8px' }}>
        <div style={{ fontSize: 13.5, fontWeight: 700 }}>条目 · 采纳 {adopted.length}/{items.length}</div>
        <button className="ma-btn sm" onClick={() => { setItems(list => list.concat([{ text: '', category: '其他', owner: '', kind: 'todo', adopted: true, ai: false }])); setEditIdx(items.length); }}>+ 手动加一条</button>
      </div>
      <div className="ma-sub" style={{ margin: '0 2px 8px', fontSize: 11 }}>点每条左侧类型徽标可改「决议 / 待办 / 风险 / 信息」—— 待办自动进项目待办、风险自动进暴露问题墙。</div>
      {items.length === 0 ? <div className="ma-card ma-sub">暂无条目 · 点「+ 手动加一条」开始。</div> : null}

      {items.map((it, i) => (
        <div key={i} className={'ma-item' + (it.adopted ? '' : ' off')}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
            <button onClick={() => { setKindPickerIdx(kindPickerIdx === i ? null : i); setPickerIdx(null); }} style={{ border: 'none', background: 'none', padding: 0, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 3 }} title="点选类型:待办→项目待办 · 风险→暴露墙">
              <KindBadge kind={it.kind} /><span style={{ fontSize: 10, color: 'var(--ink-4)' }}>▾</span>
            </button>
            <button className="ma-chip" style={{ minHeight: 40 }} onClick={() => { setPickerIdx(pickerIdx === i ? null : i); setKindPickerIdx(null); }}>{it.category} ▾</button>
            {it.ai ? <span title="AI 生成" style={{ fontSize: 12, color: 'var(--ai)' }}>✨</span> : null}
            <div style={{ flex: 1 }} />
            <Toggle on={it.adopted} onChange={() => patch(i, { adopted: !it.adopted })} />
          </div>
          {kindPickerIdx === i ? (
            <div className="ma-cats">
              {['decision', 'todo', 'risk', 'info'].map(k => (
                <button key={k} className={'ma-chip' + (k === it.kind ? ' sel' : '')} onClick={() => { patch(i, { kind: k }); setKindPickerIdx(null); }}>{KIND_META[k].label}</button>
              ))}
            </div>
          ) : null}
          {pickerIdx === i ? (
            <div className="ma-cats">
              {CATEGORIES.map(c => (
                <button key={c} className={'ma-chip' + (c === it.category ? ' sel' : '')} onClick={() => { patch(i, { category: c }); setPickerIdx(null); }}>{c}</button>
              ))}
            </div>
          ) : null}
          {editIdx === i ? (
            <textarea className="ma-input" autoFocus style={{ marginTop: 8 }} value={it.text}
              onChange={e => patch(i, { text: e.target.value })} onBlur={() => setEditIdx(null)} placeholder="条目内容…" />
          ) : (
            <div className="ma-item-text" onClick={() => setEditIdx(i)}>{it.text || <span style={{ color: 'var(--ink-4)' }}>(空 · 点击填写)</span>}</div>
          )}
          <input className="ma-input" style={{ minHeight: 40, fontSize: 13.5, maxWidth: 220 }} value={it.owner}
            onChange={e => patch(i, { owner: e.target.value })} placeholder="负责人(可空)" />
        </div>
      ))}

      <div className="ma-card">
        <label className="ma-label" style={{ marginTop: 0 }}>收件人(一行一个邮箱 · 按项目记忆)</label>
        <textarea className="ma-input" value={recipText} onChange={e => setRecipText(e.target.value)} placeholder={'zhang@hecian.com\nli@hecian.com'} />
        <div className="ma-sub" style={{ marginTop: 6 }}>{emails.length ? '将发给 ' + emails.length + ' 人' : '不填则只入库、不发邮件'}</div>
      </div>

      {err ? <ErrBar msg={err} onRetry={confirm} /> : null}
      <button className="ma-btn primary big" disabled={busy || !title.trim() || adopted.length === 0} onClick={confirm}>
        {busy ? '入库中…' : '确认并发送(' + adopted.length + ' 条)'}
      </button>
      {!busy && adopted.length === 0 && items.length > 0 ? <div className="ma-sub" style={{ margin: '8px 4px', color: 'var(--warn)' }}>至少采纳一条才能入库。</div> : null}
      <div style={{ textAlign: 'center', marginTop: 14 }}>
        <button className="ma-btn sm" onClick={() => { if (window.confirm('放弃本次纪要草稿?')) onRestart(); }} style={{ color: 'var(--ink-4)' }}>放弃本次</button>
      </div>
    </div>
  );
}

/* ========== 成功页 ========== */
function DoneStep({ result, onAgain }) {
  const email = result && result.email;
  let mailLine;
  if (!email) mailLine = '该任务此前已确认入库'; // 恢复到已 confirmed 的旧 job
  else if (email.sent) mailLine = '邮件已发给 ' + ((email.to || []).length) + ' 人';
  else if (email.reason === 'no_recipients') mailLine = '未填收件人 · 未发邮件';
  else if (email.reason === 'smtp_not_configured') mailLine = '邮件未配置(SMTP)· 仅入库';
  else mailLine = '邮件发送失败:' + (email.reason || '未知原因');
  return (
    <div className="ma-card" style={{ textAlign: 'center', padding: '42px 20px' }}>
      <div style={{ fontSize: 34, color: 'var(--ok)', marginBottom: 10 }}>✓</div>
      <div style={{ fontSize: 17, fontWeight: 700, marginBottom: 8 }}>已入库</div>
      <div className="ma-sub" style={{ marginBottom: 6 }}>{mailLine}</div>
      {result && result.minute_id ? <div className="ma-mono" style={{ marginBottom: 18 }}>minute {result.minute_id}</div> : null}
      <button className="ma-btn primary big" onClick={onAgain}>再记一场</button>
    </div>
  );
}

/* ========== 主 App · 状态机 ========== */
const STEP_SUB = {
  project: '① 选项目',
  upload: '② 传录音',
  poll: '③ 听悟转写 + AI 拆条',
  review: '④ 逐条核对 · 确认入库',
  done: '完成',
};

function App() {
  const [step, setStep] = useState('project');
  const [project, setProject] = useState(null);
  const [jobId, setJobId] = useState(null);
  const [jobTitle, setJobTitle] = useState('');
  const [aiDraft, setAiDraft] = useState(false);
  const [title, setTitle] = useState('');
  const [date, setDate] = useState(today());
  const [items, setItems] = useState([]);
  const [doneResult, setDoneResult] = useState(null);
  const [bootErr, setBootErr] = useState(null); // 引导态错误(如 mode=manual 起 job 失败)· 不静默
  const hasToken = !!window.HX_getAuthToken();

  // 恢复(锁屏/关页回来续上)
  useEffect(() => {
    const saved = loadJob();
    if (!saved || !saved.project) {
      // C(2026-07-04):从项目会议纪要页带 ?project_id 进来 → 预选该项目、直接进上传步(项目感知·成果回落同一项目)· 有在途 job 则走恢复不覆盖
      // mode=manual(2026-07-05):桌面「+ 新建纪要」进来 → 起一个空 job → 直接进空审核表单(手动录入·不录音)
      var pid = '', mode = '';
      try { var sp = new URLSearchParams(location.search); pid = sp.get('project_id') || ''; mode = sp.get('mode') || ''; } catch (e) {}
      if (pid && window.HX_getAuthToken()) {
        fetchJson('/api/m03/projects', { headers: authHeaders() })
          .then(function(res){
            if (!res.ok || !res.d || !Array.isArray(res.d.items)) return;
            var p = res.d.items.find(function(x){ return x.id === pid; });
            if (!p) return;
            var proj = { id: p.id, code: p.code, name: p.name };
            if (mode === 'manual') {
              var defTitle = p.name + ' 会议纪要 ' + today();
              setProject(proj); setJobTitle(defTitle);
              fetchJson('/api/m03/meeting-audio/manual-start', { method: 'POST', headers: authHeaders(true), body: JSON.stringify({ project_id: p.id }) })
                .then(function(r){
                  // 起 job 失败 → 报可见错误 + 退回上传步(不静默:用户明明点了「手动新建」)
                  if (!r.ok || !r.d || !r.d.job_id) { setBootErr(apiMsg(r, '手动新建纪要失败') + ' · 已切到录音上传,可返回重试'); setStep('upload'); return; }
                  setJobId(r.d.job_id); setAiDraft(false); setTitle(defTitle); setDate(today()); setItems([]); setStep('review');
                })
                .catch(function(e){ setBootErr('网络错误:' + String(e) + ' · 已切到录音上传,可返回重试'); setStep('upload'); });
            } else {
              setProject(proj); setStep('upload');
            }
          })
          .catch(function(){});
      }
      return;
    }
    setProject(saved.project);
    setJobTitle(saved.jobTitle || '');
    if (!saved.job_id) { setStep('upload'); return; }
    setJobId(saved.job_id);
    if (saved.review && Array.isArray(saved.review.items)) {
      setAiDraft(!!saved.review.aiDraft);
      setTitle(saved.review.title || '');
      setDate(saved.review.date || today());
      setItems(saved.review.items);
      setStep('review');
    } else {
      setStep('poll');
    }
  }, []);

  // 持久化
  useEffect(() => {
    try {
      if (step === 'project' || step === 'done') { localStorage.removeItem(JOB_KEY); return; }
      const o = { step, project, job_id: jobId, jobTitle };
      if (step === 'review') o.review = { aiDraft, title, date, items };
      localStorage.setItem(JOB_KEY, JSON.stringify(o));
    } catch (e) {}
  }, [step, project, jobId, jobTitle, aiDraft, title, date, items]);

  function reset() {
    setProject(null); setJobId(null); setJobTitle(''); setAiDraft(false);
    setTitle(''); setDate(today()); setItems([]); setDoneResult(null); setBootErr(null);
    setStep('project');
  }

  // 拆条草稿 → 审核态(manual=true 为跳过 AI 手动整理)
  function enterReview(d, manual) {
    const draft = (d && d.draft) || null;
    const list = draft && Array.isArray(draft.items)
      ? draft.items.map(it => ({
          text: it.text || '', category: CATEGORIES.includes(it.category) ? it.category : '其他',
          owner: it.owner || '', kind: KIND_META[it.kind] ? it.kind : 'info', adopted: true, ai: true,
        }))
      : [];
    setAiDraft(!!draft && !manual);
    setTitle((draft && draft.title) || jobTitle || '会议纪要');
    setDate(today());
    setItems(list);
    setStep('review');
  }

  return (
    <div className="ma-wrap">
      <div className="ma-head">
        <div className="ma-kicker">HECIAN · MEETING NOTES</div>
        <div className="ma-h1">会议速记</div>
        <div className="ma-sub">录音 → 听悟转写 → ✨AI 拆条 → 人核对入库 · {STEP_SUB[step]}</div>
      </div>

      {!hasToken ? <LoginGate /> : (
        <React.Fragment>
          {bootErr ? <ErrBar msg={bootErr} onRetry={() => { setBootErr(null); setProject(null); setStep('project'); }} retryLabel="重选项目" /> : null}
          {step === 'project' && <ProjectPick onPick={p => { setBootErr(null); setProject(p); setStep('upload'); }} />}
          {step === 'upload' && project && (
            <UploadStep project={project} jobTitle={jobTitle} setJobTitle={setJobTitle}
              onSubmitted={(jid, t) => { setJobId(jid); setJobTitle(t); setStep('poll'); }}
              onChangeProject={() => { setProject(null); setStep('project'); }} />
          )}
          {step === 'poll' && jobId && (
            <PollStep jobId={jobId} onDrafted={enterReview}
              onConfirmedAlready={() => { setDoneResult(null); setStep('done'); }}
              onRestart={reset} />
          )}
          {step === 'review' && project && jobId && (
            <ReviewStep project={project} jobId={jobId} aiDraft={aiDraft}
              title={title} setTitle={setTitle} date={date} setDate={setDate}
              items={items} setItems={setItems}
              onDone={r => { setDoneResult(r); setStep('done'); }} onRestart={reset} />
          )}
          {step === 'done' && <DoneStep result={doneResult} onAgain={reset} />}
        </React.Fragment>
      )}
    </div>
  );
}

function _MA_BOOT() {
  if (typeof ReactDOM === 'undefined' || typeof window.HX_getAuthToken === 'undefined') {
    return setTimeout(_MA_BOOT, 80);
  }
  const rootEl = document.getElementById('root');
  if (!rootEl) return setTimeout(_MA_BOOT, 80);
  if (rootEl.children.length > 0) return;
  try {
    ReactDOM.createRoot(rootEl).render(<App />);
  } catch (e) {
    console.error('[会议速记] render 失败:', e);
    rootEl.innerHTML = '<pre style="padding:24px;color:#a52a2a;font-family:monospace;">会议速记 render 失败 · 见 console</pre>';
  }
}
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', _MA_BOOT);
} else {
  _MA_BOOT();
}
