/* global React, useI18n, ToolHead, StatusBar, Dropzone, OptCheckbox, OptRadio, OptDivider, EmptyOutput,
   TOOL_BY_ID, convertCliToMarkdown, convertChatToMarkdown, renderMarkdown, pdfToMarkdown, pdfToTables, tableToCsv,
   docxToMarkdown, excelToSheets, sheetsToXlsxBlob, ocrImage,
   downloadText, downloadBlob, readFileAsText, fmtBytes, addToHistory */
/* ============================================================
   tools.jsx — Tool view components (one per converter)
   ============================================================ */

const { useState: useS, useEffect: useE, useMemo: useM, useRef: useR, useCallback: useC } = React;

// Restore a text-input tool from a history entry id (?h=<id>). Sets the input
// when the matching entry carries a stored payload. No-op for file/binary tools.
function useRestoreInput(restoreId, setInput) {
  useE(() => {
    if (!restoreId) return;
    const e = window.getHistoryEntry(restoreId);
    if (e && e.payload && typeof e.payload.input === 'string') setInput(e.payload.input);
  }, [restoreId]);
}

const CLI_SAMPLE = `✅ gitnexus 重新索引完成

  📊 4 个 EnterNet 仓库已注册（新路径）

  ┌──────────────────────┬────────┬────────┬───────┐
  │         仓库         │ nodes  │ edges  │ flows │
  ├──────────────────────┼────────┼────────┼───────┤
  │ enternet-center      │  3,794 │  6,967 │   158 │
  ├──────────────────────┼────────┼────────┼───────┤
  │ enternet-agent-suite │  5,515 │ 12,329 │   300 │
  ├──────────────────────┼────────┼────────┼───────┤
  │ enternet-pc-api      │    536 │  1,216 │    46 │
  ├──────────────────────┼────────┼────────┼───────┤
  │ enternet-agent-qt    │    602 │  1,107 │    25 │
  ├──────────────────────┼────────┼────────┼───────┤
  │ 合计                 │ 10,447 │ 21,619 │   529 │
  └──────────────────────┴────────┴────────┴───────┘

  数字与旧索引完全一致（之前的 .gitnexus/ 数据本来就在仓库里，只是注册表路径过时；用 gitnexus index 重注册即可，无需重跑 analyze）。

  🗂 Group 修正

  - ✅ 新建 enternet group（4 个仓库）
  - ✅ gitnexus group sync enternet → 93 contracts 已抽取
  - ✅ 删掉旧 byrust group 目录

  🔍 现在可以直接用 MCP 查询

  mcp__gitnexus__route_map({ repo: "enternet-center" })
  mcp__gitnexus__group_query({ name: "enternet", query: "user login flow" })
  mcp__gitnexus__api_impact({ repo: "@enternet", route: "/api/v1/admin/..." })
`;

const MD_SAMPLE = `# Welcome to Convertly

A document conversion suite running **entirely in your browser**.

## Features

- 🚀 Fast and lightweight
- 🔒 Private (no uploads)
- 🌐 Bilingual interface

## Example table

| Format | Direction | Status |
| --- | --- | --- |
| PDF | → Markdown | ✅ |
| PDF | → Excel | ✅ |
| DOCX | → Markdown | ✅ |
| Image | → Text (OCR) | ✅ |

> Tip: paste any Markdown here and see it rendered live.

\`\`\`bash
# Install
npm i convertly
\`\`\`
`;

// Common copy button with feedback
function CopyButton({ text, label, className = 'btn btn-primary' }) {
  const { t } = useI18n();
  const [copied, setCopied] = useS(false);
  const doCopy = async () => {
    try {
      await navigator.clipboard.writeText(text);
    } catch (e) {
      const ta = document.createElement('textarea');
      ta.value = text; document.body.appendChild(ta);
      ta.select(); document.execCommand('copy');
      document.body.removeChild(ta);
    }
    setCopied(true);
    setTimeout(() => setCopied(false), 1400);
  };
  return (
    <button className={className} onClick={doCopy}>
      {copied ? t('common_copied') : (label || t('common_copy'))}
    </button>
  );
}

// =================================================================
// TOOL: CLI → Markdown
// =================================================================
function CliToMarkdown({ restoreId }) {
  const { t } = useI18n();
  const tool = TOOL_BY_ID['cli-to-markdown'];
  const [input, setInput] = useS(CLI_SAMPLE);
  const [opts, setOpts] = useS({
    headerStyle: 'auto', fenceCode: true, tightLists: true, stripEmojis: false,
  });
  const [view, setView] = useS('markdown');
  const tweaks = window.__cv_tweaks || {};
  const layout = tweaks.layout || 'horiz';
  const mdStyle = tweaks.mdStyle || 'gfm';
  const optsResolved = useM(() => {
    const o = { ...opts };
    if (mdStyle === 'plain') { o.headerStyle = 'bold'; o.fenceCode = false; }
    if (mdStyle === 'emoji') { o.stripEmojis = false; }
    if (mdStyle === 'plain') { o.stripEmojis = true; }
    return o;
  }, [opts, mdStyle]);

  const markdown = useM(() => convertCliToMarkdown(input, optsResolved), [input, optsResolved]);

  const tableCount = useM(() => {
    return (markdown.match(/^\|[\s\-:|]+\|\s*$/gm) || []).length;
  }, [markdown]);

  const recordHistory = useC(() => {
    if (!input.trim()) return;
    addToHistory({
      toolId: 'cli-to-markdown',
      title: input.split('\n')[0].slice(0, 60) || t('untitled'),
      size: new Blob([input]).size,
      payload: { input },
    });
  }, [input, t]);

  useRestoreInput(restoreId, setInput);

  return (
    <div className="tool" style={{ '--card-hue': `var(--hue-${tool.hue})` }}>
      <ToolHead tool={tool} actions={
        <>
          <button className="btn btn-ghost" onClick={() => setInput(CLI_SAMPLE)}>{t('common_load_sample')}</button>
          <button className="btn btn-ghost" onClick={() => setInput('')}>{t('common_clear')}</button>
          <div className="seg">
            <button className={`seg-btn ${view === 'markdown' ? 'active' : ''}`} onClick={() => setView('markdown')}>{t('common_markdown')}</button>
            <button className={`seg-btn ${view === 'preview' ? 'active' : ''}`} onClick={() => setView('preview')}>{t('common_preview')}</button>
          </div>
          <button className="btn btn-ghost" onClick={() => { recordHistory(); downloadText(markdown, 'cli-output.md', 'text/markdown'); }}>{t('common_download')}</button>
          <CopyButton text={markdown} />
        </>
      } />

      <div className="tool-opts">
        <OptRadio
          value={opts.headerStyle}
          onChange={v => setOpts(o => ({ ...o, headerStyle: v }))}
          label={t('opt_headers')}
          options={[
            { value: 'auto', label: t('opt_headers_auto') },
            { value: 'bold', label: t('opt_headers_bold') },
            { value: 'none', label: t('opt_headers_none') },
          ]}
        />
        <OptDivider />
        <OptCheckbox
          checked={opts.fenceCode}
          onChange={v => setOpts(o => ({ ...o, fenceCode: v }))}
          label={t('opt_fence_code')}
          hint={t('opt_fence_code_hint')}
        />
        <OptCheckbox
          checked={opts.tightLists}
          onChange={v => setOpts(o => ({ ...o, tightLists: v }))}
          label={t('opt_tight_lists')}
        />
        <OptCheckbox
          checked={opts.stripEmojis}
          onChange={v => setOpts(o => ({ ...o, stripEmojis: v }))}
          label={t('opt_strip_emojis')}
        />
      </div>

      <main className={`split ${layout === 'vert' ? 'vertical' : ''}`}>
        <section className="pane pane-input">
          <div className="pane-head">
            <span className="pane-label">{t('common_input')}</span>
            <span className="pane-meta">{input.length} {t('common_chars')}</span>
          </div>
          <textarea
            className="editor"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder={t('cli_input_placeholder')}
            onBlur={recordHistory}
            spellCheck={false}
          />
        </section>
        <div className="divider" />
        <section className="pane pane-output">
          <div className="pane-head">
            <span className="pane-label">{view === 'preview' ? t('common_rendered_preview') : t('common_markdown')}</span>
            <span className="pane-meta">{markdown.length} {t('common_chars')} · {tableCount} {t('common_tables')}</span>
          </div>
          {markdown ? (
            view === 'preview'
              ? <div className="preview" dangerouslySetInnerHTML={{ __html: renderMarkdown(markdown) }} />
              : <pre className="md-source">{markdown}</pre>
          ) : <EmptyOutput title={t('placeholder_paste_first')} sub="" />}
        </section>
      </main>

      <StatusBar
        status="ok"
        left={<>
          <span className="stat"><b>{input.split('\n').length}</b> {t('common_lines')}</span>
          <span className="stat-sep">·</span>
          <span className="stat">{markdown.split('\n').length} {t('common_lines')} out</span>
          <span className="stat-sep">·</span>
          <span className="stat">{tableCount} {t('common_tables')}</span>
        </>}
        right={null}
      />
    </div>
  );
}

// =================================================================
// TOOL: Chat → Markdown
// =================================================================

const CHAT_SAMPLE = `問題 2 先（純知識）

Volterra

F5 Networks 旗下產品線（原 Volterra Inc. 2021 被 F5 收購），現叫「F5 Distributed Cloud Services」(F5 XC)。

- 核心能力：分散式 edge cloud + Bot Defense（前身 Shape Security）+ WAF + DDoS 防護
- 工作原理：客戶端請求不直接打後端，先到 F5 XC 全球邊緣節點 → 行為分析 / fingerprint 比對
- response header 兩個指紋：
  - server: volt-adc → Volterra Application Delivery Controller
  - x-volterra-location → 揭示哪個邊緣節點處理了這次請求

sg3-sin

F5 XC 節點命名規則 <region><index>-<airport_iata>：
- sg = Singapore（國家碼）
- 3 = 該 region 第 3 個 PoP（Point of Presence）
- sin = Singapore Changi 樟宜機場 IATA 代碼
`;

function ChatToMarkdown({ restoreId }) {
  const { t } = useI18n();
  const tool = TOOL_BY_ID['chat-to-markdown'];
  const [input, setInput] = useS(CHAT_SAMPLE);
  const [opts, setOpts] = useS({
    h1FromFirstLine: true,
    h2FromBareLines: true,
    inlineCodePairs: true,
    fencePlaceholders: true,
    tightLists: true,
  });
  const [view, setView] = useS('markdown');
  const tweaks = window.__cv_tweaks || {};
  const layout = tweaks.layout || 'horiz';

  const markdown = useM(() => window.convertChatToMarkdown(input, opts), [input, opts]);

  const recordHistory = useC(() => {
    if (!input.trim()) return;
    addToHistory({
      toolId: 'chat-to-markdown',
      title: input.split('\n').find(l => l.trim())?.slice(0, 60) || t('untitled'),
      size: new Blob([input]).size,
      payload: { input },
    });
  }, [input, t]);

  useRestoreInput(restoreId, setInput);

  return (
    <div className="tool" style={{ '--card-hue': `var(--hue-${tool.hue})` }}>
      <ToolHead tool={tool} actions={
        <>
          <button className="btn btn-ghost" onClick={() => setInput(CHAT_SAMPLE)}>{t('common_load_sample')}</button>
          <button className="btn btn-ghost" onClick={() => setInput('')}>{t('common_clear')}</button>
          <div className="seg">
            <button className={`seg-btn ${view === 'markdown' ? 'active' : ''}`} onClick={() => setView('markdown')}>{t('common_markdown')}</button>
            <button className={`seg-btn ${view === 'preview' ? 'active' : ''}`} onClick={() => setView('preview')}>{t('common_preview')}</button>
          </div>
          <button className="btn btn-ghost" onClick={() => { recordHistory(); downloadText(markdown, 'chat.md', 'text/markdown'); }}>{t('common_download')}</button>
          <CopyButton text={markdown} />
        </>
      } />

      <div className="tool-opts">
        <OptCheckbox checked={opts.h1FromFirstLine} onChange={v => setOpts(o => ({ ...o, h1FromFirstLine: v }))} label={t('opt_chat_h1')} />
        <OptCheckbox checked={opts.h2FromBareLines} onChange={v => setOpts(o => ({ ...o, h2FromBareLines: v }))} label={t('opt_chat_h2')} />
        <OptDivider />
        <OptCheckbox checked={opts.inlineCodePairs} onChange={v => setOpts(o => ({ ...o, inlineCodePairs: v }))} label={t('opt_chat_inline_code')} />
        <OptCheckbox checked={opts.fencePlaceholders} onChange={v => setOpts(o => ({ ...o, fencePlaceholders: v }))} label={t('opt_chat_fence')} />
        <OptCheckbox checked={opts.tightLists} onChange={v => setOpts(o => ({ ...o, tightLists: v }))} label={t('opt_tight_lists')} />
      </div>

      <main className={`split ${layout === 'vert' ? 'vertical' : ''}`}>
        <section className="pane pane-input">
          <div className="pane-head">
            <span className="pane-label">{t('common_input')}</span>
            <span className="pane-meta">{input.length} {t('common_chars')}</span>
          </div>
          <textarea
            className="editor"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder={t('chat_input_placeholder')}
            onBlur={recordHistory}
            spellCheck={false}
          />
        </section>
        <div className="divider" />
        <section className="pane pane-output">
          <div className="pane-head">
            <span className="pane-label">{view === 'preview' ? t('common_rendered_preview') : t('common_markdown')}</span>
            <span className="pane-meta">{markdown.length} {t('common_chars')}</span>
          </div>
          {markdown ? (
            view === 'preview'
              ? <div className="preview" dangerouslySetInnerHTML={{ __html: renderMarkdown(markdown) }} />
              : <pre className="md-source">{markdown}</pre>
          ) : <EmptyOutput title={t('placeholder_paste_first')} sub="" />}
        </section>
      </main>

      <StatusBar
        status="ok"
        left={<>
          <span className="stat"><b>{input.split('\n').length}</b> {t('common_lines')}</span>
          <span className="stat-sep">·</span>
          <span className="stat">{markdown.split('\n').length} {t('common_lines')} out</span>
        </>}
        right={null}
      />
    </div>
  );
}

// =================================================================
// AI settings modal (shared by AI → Markdown and AI-tidy buttons)
// =================================================================
function AiSettings({ open, onClose }) {
  const { t } = useI18n();
  const [cfg, setCfg] = useS(() => window.getAiConfig());
  const [tab, setTab] = useS(() => window.getAiConfig().activeProvider);
  const [testing, setTesting] = useS(false);
  const [testResult, setTestResult] = useS(null);
  const [proxyAvail, setProxyAvail] = useS(false);
  const [models, setModels] = useS([]);
  const [loadingModels, setLoadingModels] = useS(false);

  useE(() => {
    if (!open) return;
    const c = window.getAiConfig(); setCfg(c); setTab(c.activeProvider); setTestResult(null);
    // Probe /healthz — Caddy answers "ok", a bare static server 404s.
    let alive = true;
    fetch('/healthz').then(r => (r.ok ? r.text() : '')).then(txt => { if (alive) setProxyAvail(txt.trim() === 'ok'); }).catch(() => { if (alive) setProxyAvail(false); });
    return () => { alive = false; };
  }, [open]);

  const refreshModels = async (persistFirst) => {
    if (persistFirst) window.setAiConfig(cfg);
    setLoadingModels(true);
    try { setModels(await window.listModels(tab)); }
    finally { setLoadingModels(false); }
  };

  // Auto-load models when the modal opens or the provider tab changes.
  // Ollama needs no key; for others only auto-fetch if a key is already saved
  // (avoids predictable CORS failures on key-less cloud endpoints).
  useE(() => {
    if (!open) return;
    setModels([]);
    const saved = window.getProviderCfg(tab);
    if (tab === 'ollama' || saved.apiKey) {
      let alive = true;
      setLoadingModels(true);
      window.listModels(tab).then(ms => { if (alive) setModels(ms); }).finally(() => { if (alive) setLoadingModels(false); });
      return () => { alive = false; };
    }
  }, [open, tab]);

  if (!open) return null;

  const provIds = ['ollama', 'openaiCompat', 'anthropic'];
  const provLabel = { ollama: t('ai_provider_ollama'), openaiCompat: t('ai_provider_openai'), anthropic: t('ai_provider_anthropic') };
  const p = cfg.providers[tab];
  const proxyAllowed = tab !== 'ollama' && proxyAvail;

  const setField = (k, v) => setCfg(c => ({ ...c, providers: { ...c.providers, [tab]: { ...c.providers[tab], [k]: v } } }));

  const doTest = async () => {
    window.setAiConfig(cfg);
    setTesting(true); setTestResult(null);
    try {
      const r = await window.testConnection(tab);
      if (r.ok) setTestResult({ ok: true, msg: t('ai_set_test_ok') });
      else setTestResult({ ok: false, msg: t('ai_set_test_fail') + (r.status ? ' ' + r.status : '') + (r.error ? ' · ' + String(r.error).slice(0, 120) : '') });
    } catch (e) {
      setTestResult({ ok: false, msg: t('ai_set_test_fail') });
    } finally { setTesting(false); }
  };

  const save = () => { window.setAiConfig(cfg); onClose(); };

  return (
    <div className="ai-modal-backdrop" onClick={onClose}>
      <div className="ai-modal" onClick={e => e.stopPropagation()}>
        <h3>{t('ai_set_title')}</h3>
        <div className="ai-modal-note">{t('ai_set_security')}</div>

        <div className="ai-tabs">
          {provIds.map(id => (
            <button key={id} className={`ai-tab ${tab === id ? 'active' : ''}`} onClick={() => { setTab(id); setTestResult(null); }}>
              {provLabel[id]}{cfg.activeProvider === id ? ' ·' : ''}
            </button>
          ))}
        </div>

        <div className="ai-field">
          <label>{t('ai_set_transport')}</label>
          <div className="radio-group">
            <button className={`radio-pill ${p.transport === 'direct' ? 'active' : ''}`} onClick={() => setField('transport', 'direct')}>{t('ai_set_transport_direct')}</button>
            <button className={`radio-pill ${p.transport === 'proxy' ? 'active' : ''}`} disabled={!proxyAllowed} onClick={() => proxyAllowed && setField('transport', 'proxy')}>{t('ai_set_transport_proxy')}</button>
          </div>
          {!proxyAllowed && <div className="ai-field-na">{t('ai_set_transport_proxy_na')}</div>}
        </div>

        {tab !== 'anthropic' && (
          <div className="ai-field">
            <label>{t('ai_set_baseurl')}</label>
            <input type="text" value={p.baseURL} onChange={e => setField('baseURL', e.target.value)} placeholder="https://api.openai.com/v1" spellCheck={false} />
          </div>
        )}

        {tab !== 'ollama' && (
          <div className="ai-field">
            <label>{t('ai_set_apikey')}</label>
            <input type="password" value={p.apiKey} onChange={e => setField('apiKey', e.target.value)} placeholder="sk-…" autoComplete="off" spellCheck={false} />
          </div>
        )}

        <div className="ai-field">
          <label>{t('ai_set_model')}{loadingModels && <span style={{ textTransform: 'none', letterSpacing: 0, color: 'var(--ink-4)' }}> · {t('ai_set_loading_models')}</span>}</label>
          <div style={{ display: 'flex', gap: 6 }}>
            <input
              list="ai-model-options"
              type="text"
              style={{ flex: 1, minWidth: 0 }}
              value={p.model}
              onChange={e => setField('model', e.target.value)}
              placeholder="gemma4:e4b"
              spellCheck={false}
              autoComplete="off"
            />
            <button className="btn btn-ghost" style={{ height: 32, padding: '0 10px' }} onClick={() => refreshModels(true)} title={t('ai_set_refresh_models')} disabled={loadingModels}>↻</button>
          </div>
          <datalist id="ai-model-options">
            {models.map(m => <option key={m} value={m} />)}
          </datalist>
        </div>

        <div className="ai-modal-actions">
          <button className="btn btn-ghost" onClick={doTest} disabled={testing}>{testing ? t('ai_set_testing') : t('ai_set_test')}</button>
          {testResult && <span className={`ai-test-result ${testResult.ok ? 'ok' : 'fail'}`}>{testResult.msg}</span>}
          <span className="ai-modal-spacer" />
          <button className="btn btn-ghost" onClick={onClose}>{t('ai_set_close')}</button>
          <button className="btn btn-primary" onClick={save}>{t('ai_set_save')}</button>
        </div>
      </div>
    </div>
  );
}

// =================================================================
// TOOL: AI → Markdown
// =================================================================
const AI_SAMPLE = `volterra 发布说明
上线了新的鉴权流程
关键变化
登录现在走 oauth
会话超时 30 分钟
待办 补文档`;

function AiToMarkdown({ restoreId }) {
  const { t } = useI18n();
  const tool = TOOL_BY_ID['ai-to-markdown'];
  const [input, setInput] = useS(AI_SAMPLE);
  const [output, setOutput] = useS('');
  const [status, setStatus] = useS('idle'); // idle | streaming | error
  const [err, setErr] = useS(null);
  const [usage, setUsage] = useS(null);
  const [view, setView] = useS('markdown');
  const [showSettings, setShowSettings] = useS(false);
  const [provider, setProvider] = useS(() => window.getAiConfig().activeProvider);
  const [extra, setExtra] = useS(() => window.getAiConfig().extraInstruction || '');
  const [convo, setConvo] = useS([]);          // [{role,content}…] for refine context
  const [continueText, setContinueText] = useS('');
  const abortRef = useR(null);
  const tweaks = window.__cv_tweaks || {};
  const layout = tweaks.layout || 'horiz';

  const { dragover, bind, inputProps } = window.useDropzone({
    accept: tool.accepts, multiple: false,
    onFiles: async (files) => { try { const txt = await readFileAsText(files[0]); setInput(txt); } catch (e) { /* ignore */ } },
  });
  const { onClick: openFile, ...dragBind } = bind;

  const changeProvider = (id) => {
    setProvider(id);
    const c = window.getAiConfig(); c.activeProvider = id; window.setAiConfig(c);
  };
  const changeExtra = (v) => {
    setExtra(v);
    const c = window.getAiConfig(); c.extraInstruction = v; window.setAiConfig(c);
  };

  const doStream = useC(async (userText, history) => {
    const cfg = window.getProviderCfg(provider);
    if (provider !== 'ollama' && !cfg.apiKey) {
      setStatus('error'); setErr({ messageKey: 'ai_err_nokey', detail: '' });
      return;
    }
    setStatus('streaming'); setErr(null); setOutput(''); setUsage(null);
    const ac = new AbortController(); abortRef.current = ac;
    try {
      const res = await window.streamMarkdown({
        provider, input: userText, history, extraInstruction: extra, signal: ac.signal,
        onToken: (piece, acc) => setOutput(acc),
        onUsage: (u) => setUsage(u),
      });
      setOutput(res.markdown);
      setStatus('idle');
      const newConvo = [...(history || []), { role: 'user', content: userText }, { role: 'assistant', content: res.markdown }];
      setConvo(newConvo);
      addToHistory({
        toolId: 'ai-to-markdown',
        title: input.split('\n').find(l => l.trim())?.slice(0, 60) || t('untitled'),
        size: new Blob([input]).size,
        payload: { input, output: res.markdown, convo: newConvo },
      });
    } catch (e) {
      const c = window.classifyError(e, cfg);
      if (c.kind === 'aborted') setStatus('idle'); // keep partial output
      else { setStatus('error'); setErr(c); }
    } finally { abortRef.current = null; }
  }, [input, extra, provider, t]);

  const run = () => { if (!input.trim() || status === 'streaming') return; setConvo([]); doStream(input, []); };
  const refine = () => {
    const txt = continueText.trim();
    if (!txt || status === 'streaming') return;
    setContinueText('');
    doStream(txt, convo);
  };

  const stop = () => { if (abortRef.current) abortRef.current.abort(); };

  useE(() => () => { if (abortRef.current) abortRef.current.abort(); }, []);

  // Restore a past conversion (input + result + refine history) from ?h=<id>.
  useE(() => {
    if (!restoreId) return;
    const e = window.getHistoryEntry(restoreId);
    if (!e || !e.payload) return;
    const p = e.payload;
    if (typeof p.input === 'string') setInput(p.input);
    if (typeof p.output === 'string') { setOutput(p.output); setStatus('idle'); setErr(null); }
    if (Array.isArray(p.convo)) setConvo(p.convo);
  }, [restoreId]);

  const errText = err ? (t(err.messageKey) + (err.detail ? ' ' + err.detail : '')) : '';
  const sbStatus = status === 'streaming' ? 'busy' : status === 'error' ? 'err' : 'ok';

  return (
    <div className="tool" style={{ '--card-hue': `var(--hue-${tool.hue})` }}>
      <ToolHead tool={tool} actions={
        <>
          <button className="btn btn-ghost" onClick={() => setInput(AI_SAMPLE)}>{t('common_load_sample')}</button>
          <button className="btn btn-ghost" onClick={() => { setInput(''); setOutput(''); setErr(null); setStatus('idle'); setConvo([]); setContinueText(''); }}>{t('common_clear')}</button>
          <div className="seg">
            <button className={`seg-btn ${view === 'markdown' ? 'active' : ''}`} onClick={() => setView('markdown')}>{t('common_markdown')}</button>
            <button className={`seg-btn ${view === 'preview' ? 'active' : ''}`} onClick={() => setView('preview')}>{t('common_preview')}</button>
          </div>
          {output && <button className="btn btn-ghost" onClick={() => downloadText(output, 'ai.md', 'text/markdown')}>{t('common_download')}</button>}
          {output && <CopyButton text={output} />}
        </>
      } />

      <div className="tool-opts">
        <button
          className="btn btn-primary"
          onClick={status === 'streaming' ? stop : run}
          disabled={status !== 'streaming' && !input.trim()}
        >
          {status === 'streaming' ? t('ai_stop') : t('ai_convert')}
        </button>
        <select className="ai-provider-select" value={provider} onChange={e => changeProvider(e.target.value)}>
          <option value="ollama">{t('ai_provider_ollama')}</option>
          <option value="openaiCompat">{t('ai_provider_openai')}</option>
          <option value="anthropic">{t('ai_provider_anthropic')}</option>
        </select>
        <input className="ai-extra-input" value={extra} onChange={e => changeExtra(e.target.value)} placeholder={t('ai_extra_ph')} spellCheck={false} />
        <button className="btn btn-ghost" onClick={() => setShowSettings(true)} title={t('ai_settings')}>⚙ {t('ai_settings')}</button>
      </div>

      <main className={`split ${layout === 'vert' ? 'vertical' : ''}`}>
        <section className={`pane pane-input ${dragover ? 'dragover' : ''}`} {...dragBind}>
          <div className="pane-head">
            <span className="pane-label">{t('common_input')}</span>
            <span className="pane-meta" style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
              <button className="btn btn-ghost" style={{ height: 24, padding: '0 8px', fontSize: 11 }} onClick={openFile}>{t('common_browse')}</button>
              <span>{input.length} {t('common_chars')}</span>
            </span>
          </div>
          <input {...inputProps} />
          <textarea
            className="editor"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder={t('ai_input_placeholder')}
            spellCheck={false}
          />
        </section>
        <div className="divider" />
        <section className="pane pane-output">
          <div className="pane-head">
            <span className="pane-label">{view === 'preview' ? t('common_rendered_preview') : t('common_markdown')}</span>
            <span className="pane-meta">{output.length} {t('common_chars')}</span>
          </div>
          {status === 'error' ? (
            <div className="ai-error">
              {errText}
              <div style={{ marginTop: 12 }}>
                <button className="btn btn-ghost" onClick={() => setShowSettings(true)}>{t('ai_open_settings')}</button>
              </div>
            </div>
          ) : output ? (
            view === 'preview'
              ? <div className="preview" dangerouslySetInnerHTML={{ __html: renderMarkdown(output) }} />
              : <pre className="md-source">{output}{status === 'streaming' && <span className="ai-cursor" />}</pre>
          ) : status === 'streaming' ? (
            <pre className="md-source"><span className="ai-cursor" /></pre>
          ) : <EmptyOutput title={t('placeholder_paste_first')} sub="" />}
        </section>
      </main>

      {output && status !== 'streaming' && (
        <div className="ai-continue-row">
          <input
            className="ai-continue-input"
            value={continueText}
            onChange={(e) => setContinueText(e.target.value)}
            onKeyDown={(e) => { if (e.key === 'Enter') refine(); }}
            placeholder={t('ai_continue_ph')}
            spellCheck={false}
          />
          <button className="btn btn-ghost" onClick={refine} disabled={!continueText.trim()}>{t('ai_continue_btn')}</button>
        </div>
      )}

      <StatusBar
        status={sbStatus}
        left={<>
          <span className="stat"><b>{input.split('\n').length}</b> {t('common_lines')}</span>
          <span className="stat-sep">·</span>
          <span className="stat">{output.split('\n').length} {t('common_lines')} out</span>
        </>}
        right={usage ? <span className="ai-usage">↑{usage.prompt} ↓{usage.completion} {t('ai_usage_tokens')}</span> : null}
      />

      <AiSettings open={showSettings} onClose={() => setShowSettings(false)} />
    </div>
  );
}

// =================================================================
// TOOL: PDF → Markdown
// =================================================================
function PdfToMarkdown() {
  const { t } = useI18n();
  const tool = TOOL_BY_ID['pdf-to-markdown'];
  const [files, setFiles] = useS([]);
  const [busy, setBusy] = useS(false);
  const [progress, setProgress] = useS(null);
  const [markdown, setMarkdown] = useS('');
  const [view, setView] = useS('markdown');
  const [error, setError] = useS(null);
  const [opts, setOpts] = useS({ pageBreaks: true, detectHeadings: true });
  const [aiBusy, setAiBusy] = useS(false);
  const [showAiSet, setShowAiSet] = useS(false);
  const tweaks = window.__cv_tweaks || {};
  const layout = tweaks.layout || 'horiz';
  const dropStyle = tweaks.dropzone || 'default';

  const run = useC(async (file) => {
    setBusy(true); setError(null); setMarkdown(''); setProgress(null);
    try {
      const md = await pdfToMarkdown(file, opts, (cur, total) => setProgress({ cur, total }));
      setMarkdown(md);
      addToHistory({
        toolId: 'pdf-to-markdown', title: file.name, size: file.size,
      });
    } catch (e) {
      console.error(e);
      setError(e.message || String(e));
    } finally {
      setBusy(false); setProgress(null);
    }
  }, [opts]);

  const aiTidy = useC(async () => {
    if (!markdown.trim() || aiBusy) return;
    const cfg = window.getProviderCfg();
    if (cfg.id !== 'ollama' && !cfg.apiKey) { setShowAiSet(true); return; }
    setAiBusy(true); setError(null);
    const src = markdown;
    try {
      const res = await window.streamMarkdown({
        input: src, systemPrompt: window.AI_TIDY_SYSTEM_PROMPT,
        onToken: (p, acc) => setMarkdown(acc),
      });
      setMarkdown(res.markdown);
    } catch (e) {
      const c = window.classifyError(e, cfg);
      setMarkdown(src);
      setError(t(c.messageKey) + (c.detail ? ' ' + c.detail : ''));
    } finally { setAiBusy(false); }
  }, [markdown, aiBusy, t]);

  useE(() => {
    if (files.length && !busy) run(files[0]);
  }, [files, opts]);

  const status = error ? 'err' : busy ? 'busy' : 'ok';

  return (
    <div className="tool" style={{ '--card-hue': `var(--hue-${tool.hue})` }}>
      <ToolHead tool={tool} actions={
        <>
          {files[0] && !busy && (
            <button className="btn btn-ghost" onClick={() => run(files[0])}>{t('common_run_again')}</button>
          )}
          {markdown && (
            <>
              <div className="seg">
                <button className={`seg-btn ${view === 'markdown' ? 'active' : ''}`} onClick={() => setView('markdown')}>{t('common_markdown')}</button>
                <button className={`seg-btn ${view === 'preview' ? 'active' : ''}`} onClick={() => setView('preview')}>{t('common_preview')}</button>
              </div>
              <button className="btn btn-ghost" onClick={aiTidy} disabled={aiBusy || busy} title={t('ai_tidy')}>{aiBusy ? t('ai_streaming') : t('ai_tidy')}</button>
              <button className="btn btn-ghost" onClick={() => downloadText(markdown, (files[0]?.name || 'output').replace(/\.pdf$/i, '') + '.md', 'text/markdown')}>{t('common_download')}</button>
              <CopyButton text={markdown} />
            </>
          )}
        </>
      } />

      <div className="tool-opts">
        <OptCheckbox checked={opts.pageBreaks} onChange={v => setOpts(o => ({ ...o, pageBreaks: v }))}
          label={t('pdf_md_opt_pagebreaks')} hint={t('pdf_md_opt_pagebreaks_hint')} />
        <OptCheckbox checked={opts.detectHeadings} onChange={v => setOpts(o => ({ ...o, detectHeadings: v }))}
          label={t('pdf_md_opt_detect_headings')} />
      </div>

      <main className={`split ${layout === 'vert' ? 'vertical' : ''}`}>
        <section className="pane pane-input">
          <div className="pane-head">
            <span className="pane-label">{t('common_input')}</span>
            <span className="pane-meta">{files[0] ? `${files[0].name} · ${fmtBytes(files[0].size)}` : 'PDF'}</span>
          </div>
          <Dropzone
            accept={tool.accepts}
            files={files}
            onFiles={setFiles}
            onRemove={(i) => setFiles(f => f.filter((_, idx) => idx !== i))}
            styleVariant={dropStyle}
            title={t('pdf_drop_title')}
            sub={t('pdf_drop_sub')}
            busy={busy}
            busyText={progress ? `${t('pdf_processing')} ${progress.cur}/${progress.total}` : t('pdf_processing')}
          />
        </section>
        <div className="divider" />
        <section className="pane pane-output">
          <div className="pane-head">
            <span className="pane-label">{view === 'preview' ? t('common_rendered_preview') : t('common_markdown')}</span>
            <span className="pane-meta">{markdown ? `${markdown.length} ${t('common_chars')}` : ''}</span>
          </div>
          {error ? <EmptyOutput title={t('error_parse')} sub={error} />
            : markdown
              ? (view === 'preview'
                ? <div className="preview" dangerouslySetInnerHTML={{ __html: renderMarkdown(markdown) }} />
                : <pre className="md-source">{markdown}</pre>)
              : <EmptyOutput title={t('placeholder_input_first')} sub="" />}
        </section>
      </main>

      <StatusBar status={status}
        left={files[0] && <>
          <span className="stat"><b>{files[0].name}</b></span>
          <span className="stat-sep">·</span>
          <span className="stat">{fmtBytes(files[0].size)}</span>
          {progress && <>
            <span className="stat-sep">·</span>
            <span className="stat">{progress.cur}/{progress.total} {t('common_pages')}</span>
          </>}
        </>}
        right={markdown && <span className="stat">{markdown.split('\n').length} {t('common_lines')}</span>}
      />

      <AiSettings open={showAiSet} onClose={() => setShowAiSet(false)} />
    </div>
  );
}

// =================================================================
// TOOL: PDF → Excel
// =================================================================
function PdfToExcel() {
  const { t } = useI18n();
  const tool = TOOL_BY_ID['pdf-to-excel'];
  const [files, setFiles] = useS([]);
  const [busy, setBusy] = useS(false);
  const [progress, setProgress] = useS(null);
  const [sheets, setSheets] = useS([]);
  const [activeSheet, setActiveSheet] = useS(0);
  const [error, setError] = useS(null);
  const [opts, setOpts] = useS({ joinLines: true });
  const tweaks = window.__cv_tweaks || {};
  const layout = tweaks.layout || 'horiz';
  const dropStyle = tweaks.dropzone || 'default';

  const run = useC(async (file) => {
    setBusy(true); setError(null); setSheets([]); setProgress(null);
    try {
      const result = await pdfToTables(file, opts, (cur, total) => setProgress({ cur, total }));
      setSheets(result);
      setActiveSheet(0);
      addToHistory({ toolId: 'pdf-to-excel', title: file.name, size: file.size });
    } catch (e) {
      console.error(e);
      setError(e.message || String(e));
    } finally {
      setBusy(false); setProgress(null);
    }
  }, [opts]);

  useE(() => { if (files.length && !busy) run(files[0]); }, [files, opts]);

  const downloadXlsx = useC(async () => {
    if (!sheets.length) return;
    const blob = await sheetsToXlsxBlob(sheets);
    downloadBlob(blob, (files[0]?.name || 'output').replace(/\.pdf$/i, '') + '.xlsx');
  }, [sheets, files]);

  const downloadCsv = useC(() => {
    if (!sheets.length) return;
    const csv = tableToCsv(sheets[activeSheet].rows);
    downloadText('\uFEFF' + csv, (sheets[activeSheet].name || 'sheet') + '.csv', 'text/csv');
  }, [sheets, activeSheet, files]);

  const status = error ? 'err' : busy ? 'busy' : 'ok';
  const current = sheets[activeSheet];

  return (
    <div className="tool" style={{ '--card-hue': `var(--hue-${tool.hue})` }}>
      <ToolHead tool={tool} actions={
        <>
          {files[0] && !busy && (
            <button className="btn btn-ghost" onClick={() => run(files[0])}>{t('common_run_again')}</button>
          )}
          {sheets.length > 0 && (
            <>
              <button className="btn btn-ghost" onClick={downloadCsv}>CSV</button>
              <button className="btn btn-primary" onClick={downloadXlsx}>XLSX</button>
            </>
          )}
        </>
      } />

      <div className="tool-opts">
        <OptCheckbox checked={opts.joinLines} onChange={v => setOpts(o => ({ ...o, joinLines: v }))} label={t('pdf_xlsx_opt_join_lines')} />
      </div>

      <main className={`split ${layout === 'vert' ? 'vertical' : ''}`}>
        <section className="pane pane-input">
          <div className="pane-head">
            <span className="pane-label">{t('common_input')}</span>
            <span className="pane-meta">{files[0] ? `${files[0].name} · ${fmtBytes(files[0].size)}` : 'PDF'}</span>
          </div>
          <Dropzone
            accept={tool.accepts}
            files={files}
            onFiles={setFiles}
            onRemove={(i) => setFiles(f => f.filter((_, idx) => idx !== i))}
            styleVariant={dropStyle}
            title={t('pdf_drop_title')}
            sub={t('pdf_drop_sub')}
            busy={busy}
            busyText={progress ? `${t('pdf_processing')} ${progress.cur}/${progress.total}` : t('pdf_processing')}
          />
        </section>
        <div className="divider" />
        <section className="pane pane-output">
          <div className="pane-head">
            <span className="pane-label">{t('common_view_table')}</span>
            <span className="pane-meta">{sheets.length} {t('xlsx_sheets')}</span>
          </div>
          {error ? <EmptyOutput title={t('error_parse')} sub={error} />
            : sheets.length === 0
              ? <EmptyOutput title={t('placeholder_input_first')} sub="" />
              : <SheetView sheets={sheets} active={activeSheet} setActive={setActiveSheet} />}
        </section>
      </main>

      <StatusBar status={status}
        left={files[0] && <>
          <span className="stat"><b>{files[0].name}</b></span>
          <span className="stat-sep">·</span>
          <span className="stat">{fmtBytes(files[0].size)}</span>
        </>}
        right={current && <>
          <span className="stat">{current.rows.length} {t('xlsx_rows')}</span>
          <span className="stat-sep">·</span>
          <span className="stat">{current.rows[0]?.length ?? 0} {t('xlsx_cols')}</span>
        </>}
      />
    </div>
  );
}

function SheetView({ sheets, active, setActive }) {
  const current = sheets[active];
  if (!current) return null;
  return (
    <>
      <div className="sheet-tabs">
        {sheets.map((s, i) => (
          <button key={i} className={`sheet-tab ${i === active ? 'active' : ''}`} onClick={() => setActive(i)}>
            {s.name}
          </button>
        ))}
      </div>
      <div className="sheet-table-wrap">
        <table className="sheet-table">
          <thead>
            <tr>
              {(current.rows[0] || []).map((h, i) => <th key={i}>{h || `Col ${i + 1}`}</th>)}
            </tr>
          </thead>
          <tbody>
            {current.rows.slice(1).map((row, i) => (
              <tr key={i}>{row.map((c, j) => <td key={j}>{c}</td>)}</tr>
            ))}
          </tbody>
        </table>
      </div>
    </>
  );
}

// =================================================================
// TOOL: DOCX → Markdown
// =================================================================
function DocxToMarkdown() {
  const { t } = useI18n();
  const tool = TOOL_BY_ID['docx-to-markdown'];
  const [files, setFiles] = useS([]);
  const [busy, setBusy] = useS(false);
  const [markdown, setMarkdown] = useS('');
  const [view, setView] = useS('markdown');
  const [error, setError] = useS(null);
  const tweaks = window.__cv_tweaks || {};
  const layout = tweaks.layout || 'horiz';
  const dropStyle = tweaks.dropzone || 'default';

  const run = useC(async (file) => {
    setBusy(true); setError(null); setMarkdown('');
    try {
      const md = await docxToMarkdown(file);
      setMarkdown(md);
      addToHistory({ toolId: 'docx-to-markdown', title: file.name, size: file.size });
    } catch (e) {
      console.error(e); setError(e.message || String(e));
    } finally { setBusy(false); }
  }, []);

  useE(() => { if (files.length && !busy) run(files[0]); }, [files]);

  const status = error ? 'err' : busy ? 'busy' : 'ok';

  return (
    <div className="tool" style={{ '--card-hue': `var(--hue-${tool.hue})` }}>
      <ToolHead tool={tool} actions={
        <>
          {markdown && (
            <>
              <div className="seg">
                <button className={`seg-btn ${view === 'markdown' ? 'active' : ''}`} onClick={() => setView('markdown')}>{t('common_markdown')}</button>
                <button className={`seg-btn ${view === 'preview' ? 'active' : ''}`} onClick={() => setView('preview')}>{t('common_preview')}</button>
              </div>
              <button className="btn btn-ghost" onClick={() => downloadText(markdown, (files[0]?.name || 'output').replace(/\.docx$/i, '') + '.md', 'text/markdown')}>{t('common_download')}</button>
              <CopyButton text={markdown} />
            </>
          )}
        </>
      } />

      <main className={`split ${layout === 'vert' ? 'vertical' : ''}`}>
        <section className="pane pane-input">
          <div className="pane-head">
            <span className="pane-label">{t('common_input')}</span>
            <span className="pane-meta">{files[0] ? `${files[0].name} · ${fmtBytes(files[0].size)}` : 'DOCX'}</span>
          </div>
          <Dropzone
            accept={tool.accepts}
            files={files}
            onFiles={setFiles}
            onRemove={(i) => setFiles(f => f.filter((_, idx) => idx !== i))}
            styleVariant={dropStyle}
            title={t('docx_drop_title')}
            sub={t('docx_drop_sub')}
            busy={busy}
            busyText={t('docx_processing')}
          />
        </section>
        <div className="divider" />
        <section className="pane pane-output">
          <div className="pane-head">
            <span className="pane-label">{view === 'preview' ? t('common_rendered_preview') : t('common_markdown')}</span>
            <span className="pane-meta">{markdown ? `${markdown.length} ${t('common_chars')}` : ''}</span>
          </div>
          {error ? <EmptyOutput title={t('error_parse')} sub={error} />
            : markdown
              ? (view === 'preview'
                ? <div className="preview" dangerouslySetInnerHTML={{ __html: renderMarkdown(markdown) }} />
                : <pre className="md-source">{markdown}</pre>)
              : <EmptyOutput title={t('placeholder_input_first')} sub="" />}
        </section>
      </main>

      <StatusBar status={status}
        left={files[0] && <>
          <span className="stat"><b>{files[0].name}</b></span>
          <span className="stat-sep">·</span>
          <span className="stat">{fmtBytes(files[0].size)}</span>
        </>}
        right={markdown && <span className="stat">{markdown.split('\n').length} {t('common_lines')}</span>}
      />
    </div>
  );
}

// =================================================================
// TOOL: Excel → PDF (render + print)
// =================================================================
function ExcelToPdf() {
  const { t } = useI18n();
  const tool = TOOL_BY_ID['excel-to-pdf'];
  const [files, setFiles] = useS([]);
  const [busy, setBusy] = useS(false);
  const [sheets, setSheets] = useS([]);
  const [activeSheet, setActiveSheet] = useS(0);
  const [error, setError] = useS(null);
  const tweaks = window.__cv_tweaks || {};
  const layout = tweaks.layout || 'horiz';
  const dropStyle = tweaks.dropzone || 'default';

  const run = useC(async (file) => {
    setBusy(true); setError(null); setSheets([]);
    try {
      const res = await excelToSheets(file);
      setSheets(res); setActiveSheet(0);
      addToHistory({ toolId: 'excel-to-pdf', title: file.name, size: file.size });
    } catch (e) { console.error(e); setError(e.message || String(e)); }
    finally { setBusy(false); }
  }, []);

  useE(() => { if (files.length && !busy) run(files[0]); }, [files]);

  const doPrint = useC(() => {
    if (!sheets.length) return;
    const win = window.open('', '_blank', 'width=900,height=700');
    if (!win) return;
    const styles = `
      <style>
        body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; padding: 24px; color: #1a1a18; }
        h2 { font-size: 18px; margin: 24px 0 10px; border-bottom: 1px solid #d4d1c8; padding-bottom: 6px; }
        table { border-collapse: collapse; width: 100%; font-size: 11px; }
        th, td { border: 1px solid #d4d1c8; padding: 5px 8px; text-align: left; }
        th { background: #f3f2ee; font-weight: 600; }
        tr:nth-child(even) td { background: #fafaf8; }
        @page { size: A4 landscape; margin: 14mm; }
      </style>`;
    let html = `<!doctype html><html><head><meta charset="utf-8"><title>${files[0]?.name || 'Export'}</title>${styles}</head><body>`;
    for (const s of sheets) {
      html += `<h2>${escapeHtmlPlain(s.name)}</h2><table>`;
      const [head, ...body] = s.rows;
      if (head) {
        html += '<thead><tr>';
        head.forEach(h => html += `<th>${escapeHtmlPlain(h)}</th>`);
        html += '</tr></thead>';
      }
      html += '<tbody>';
      for (const r of body) {
        html += '<tr>';
        r.forEach(c => html += `<td>${escapeHtmlPlain(c)}</td>`);
        html += '</tr>';
      }
      html += '</tbody></table>';
    }
    html += `<script>window.onload=()=>{setTimeout(()=>window.print(),200);};</` + `script></body></html>`;
    win.document.write(html);
    win.document.close();
  }, [sheets, files]);

  const status = error ? 'err' : busy ? 'busy' : 'ok';

  return (
    <div className="tool" style={{ '--card-hue': `var(--hue-${tool.hue})` }}>
      <ToolHead tool={tool} actions={
        <>
          {sheets.length > 0 && (
            <button className="btn btn-primary" onClick={doPrint}>{t('xlsx_pdf_print')}</button>
          )}
        </>
      } />

      <main className={`split ${layout === 'vert' ? 'vertical' : ''}`}>
        <section className="pane pane-input">
          <div className="pane-head">
            <span className="pane-label">{t('common_input')}</span>
            <span className="pane-meta">{files[0] ? `${files[0].name} · ${fmtBytes(files[0].size)}` : 'XLSX / CSV'}</span>
          </div>
          <Dropzone
            accept={tool.accepts}
            files={files}
            onFiles={setFiles}
            onRemove={(i) => setFiles(f => f.filter((_, idx) => idx !== i))}
            styleVariant={dropStyle}
            title={t('xlsx_drop_title')}
            sub={t('xlsx_drop_sub')}
            busy={busy}
            busyText={t('xlsx_processing')}
          />
        </section>
        <div className="divider" />
        <section className="pane pane-output">
          <div className="pane-head">
            <span className="pane-label">{t('common_preview')}</span>
            <span className="pane-meta">{sheets.length} {t('xlsx_sheets')}</span>
          </div>
          {error ? <EmptyOutput title={t('error_parse')} sub={error} />
            : sheets.length === 0
              ? <EmptyOutput title={t('placeholder_input_first')} sub="" />
              : <SheetView sheets={sheets} active={activeSheet} setActive={setActiveSheet} />}
        </section>
      </main>

      <StatusBar status={status}
        left={files[0] && <>
          <span className="stat"><b>{files[0].name}</b></span>
          <span className="stat-sep">·</span>
          <span className="stat">{fmtBytes(files[0].size)}</span>
          <span className="stat-sep">·</span>
          <span className="stat">{sheets.length} {t('xlsx_sheets')}</span>
        </>}
        right={null}
      />
    </div>
  );
}

function escapeHtmlPlain(s) {
  return String(s ?? '').replace(/[&<>"']/g, c => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
  }[c]));
}

// =================================================================
// TOOL: Markdown → HTML / DOCX
// =================================================================
function MarkdownToHtml({ restoreId }) {
  const { t } = useI18n();
  const tool = TOOL_BY_ID['markdown-to-html'];
  const [input, setInput] = useS(MD_SAMPLE);
  const [view, setView] = useS('preview');
  useRestoreInput(restoreId, setInput);
  const tweaks = window.__cv_tweaks || {};
  const layout = tweaks.layout || 'horiz';

  const html = useM(() => renderMarkdown(input), [input]);

  const downloadHtml = useC(() => {
    const full = `<!doctype html><html><head><meta charset="utf-8"><title>Markdown</title>
<style>
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;max-width:760px;margin:40px auto;padding:0 24px;color:#1a1a18;line-height:1.65;}
h1,h2,h3,h4{font-weight:600;letter-spacing:-0.01em;margin:1.4em 0 0.5em;}
h1{font-size:1.8em;}h2{font-size:1.4em;border-bottom:1px solid #e6e4dd;padding-bottom:.3em;}h3{font-size:1.15em;}
code{font-family:ui-monospace,Menlo,monospace;font-size:.88em;background:#f3f2ee;padding:1px 5px;border-radius:4px;border:1px solid #e6e4dd;}
pre{background:#f3f2ee;border:1px solid #e6e4dd;border-radius:8px;padding:12px 14px;overflow:auto;}
pre code{background:transparent;border:none;padding:0;}
table{border-collapse:collapse;margin:.8em 0;width:100%;font-size:14px;}
th,td{border:1px solid #e6e4dd;padding:6px 12px;text-align:left;}
th{background:#f3f2ee;font-weight:600;}
blockquote{border-left:3px solid #aaa;padding:.2em 1em;color:#555;background:#f3f2ee;border-radius:0 6px 6px 0;}
a{color:#0a6e3c;}
</style></head><body>${html}</body></html>`;
    downloadText(full, 'document.html', 'text/html');
    addToHistory({ toolId: 'markdown-to-html', title: 'HTML export', size: full.length, payload: { input } });
  }, [html, input]);

  const downloadDocx = useC(() => {
    // Minimal: use HTML wrapped in a Word-compatible document with MIME hint.
    // Word can open .doc files that are HTML. Save with .doc extension.
    const docHtml = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">
<head><meta charset="utf-8"><title>Document</title>
<style>body{font-family:Calibri,'Microsoft YaHei',sans-serif;font-size:11pt;}
h1{font-size:20pt;}h2{font-size:16pt;}h3{font-size:13pt;}
table{border-collapse:collapse;}td,th{border:1px solid #888;padding:4px 8px;}
code,pre{font-family:Consolas,monospace;background:#f3f3f3;}
</style></head><body>${html}</body></html>`;
    downloadText(docHtml, 'document.doc', 'application/msword');
    addToHistory({ toolId: 'markdown-to-html', title: 'DOC export', size: docHtml.length, payload: { input } });
  }, [html, input]);

  return (
    <div className="tool" style={{ '--card-hue': `var(--hue-${tool.hue})` }}>
      <ToolHead tool={tool} actions={
        <>
          <button className="btn btn-ghost" onClick={() => setInput(MD_SAMPLE)}>{t('common_load_sample')}</button>
          <button className="btn btn-ghost" onClick={() => setInput('')}>{t('common_clear')}</button>
          <div className="seg">
            <button className={`seg-btn ${view === 'preview' ? 'active' : ''}`} onClick={() => setView('preview')}>{t('common_preview')}</button>
            <button className={`seg-btn ${view === 'html' ? 'active' : ''}`} onClick={() => setView('html')}>{t('common_html')}</button>
          </div>
          <button className="btn btn-ghost" onClick={downloadDocx}>{t('md_export_docx')}</button>
          <button className="btn btn-primary" onClick={downloadHtml}>{t('md_export_html')}</button>
        </>
      } />

      <main className={`split ${layout === 'vert' ? 'vertical' : ''}`}>
        <section className="pane pane-input">
          <div className="pane-head">
            <span className="pane-label">{t('common_markdown')}</span>
            <span className="pane-meta">{input.length} {t('common_chars')}</span>
          </div>
          <textarea
            className="editor"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder={t('md_input_placeholder')}
            spellCheck={false}
          />
        </section>
        <div className="divider" />
        <section className="pane pane-output">
          <div className="pane-head">
            <span className="pane-label">{view === 'preview' ? t('common_rendered_preview') : t('common_html')}</span>
            <span className="pane-meta">{html.length} {t('common_chars')}</span>
          </div>
          {view === 'preview'
            ? <div className="preview" dangerouslySetInnerHTML={{ __html: html }} />
            : <pre className="md-source">{html}</pre>}
        </section>
      </main>

      <StatusBar status="ok"
        left={<>
          <span className="stat"><b>{input.split('\n').length}</b> {t('common_lines')}</span>
        </>}
        right={null}
      />
    </div>
  );
}

// =================================================================
// TOOL: OCR (Image → Text)
// =================================================================
function ImageToText() {
  const { t } = useI18n();
  const tool = TOOL_BY_ID['image-to-text'];
  const [files, setFiles] = useS([]);
  const [busy, setBusy] = useS(false);
  const [progress, setProgress] = useS(0);
  const [text, setText] = useS('');
  const [error, setError] = useS(null);
  const [stage, setStage] = useS(''); // 'loading' | 'recognizing'
  const [lang, setLang] = useS('eng');
  const [imageUrl, setImageUrl] = useS(null);
  const [aiBusy, setAiBusy] = useS(false);
  const [showAiSet, setShowAiSet] = useS(false);
  const tweaks = window.__cv_tweaks || {};
  const layout = tweaks.layout || 'horiz';
  const dropStyle = tweaks.dropzone || 'default';

  useE(() => {
    if (!files[0]) { setImageUrl(null); return; }
    const url = URL.createObjectURL(files[0]);
    setImageUrl(url);
    return () => URL.revokeObjectURL(url);
  }, [files]);

  const run = useC(async (file) => {
    setBusy(true); setError(null); setText(''); setProgress(0); setStage('loading');
    try {
      const result = await ocrImage(file, lang, (p) => { setStage('recognizing'); setProgress(p); });
      setText(result);
      addToHistory({ toolId: 'image-to-text', title: file.name, size: file.size });
    } catch (e) { console.error(e); setError(e.message || String(e)); }
    finally { setBusy(false); setStage(''); setProgress(0); }
  }, [lang]);

  const aiTidy = useC(async () => {
    if (!text.trim() || aiBusy || busy) return;
    const cfg = window.getProviderCfg();
    if (cfg.id !== 'ollama' && !cfg.apiKey) { setShowAiSet(true); return; }
    setAiBusy(true); setError(null);
    const src = text;
    try {
      const res = await window.streamMarkdown({
        input: src, systemPrompt: window.AI_TIDY_SYSTEM_PROMPT,
        onToken: (p, acc) => setText(acc),
      });
      setText(res.markdown);
    } catch (e) {
      const c = window.classifyError(e, cfg);
      setText(src);
      setError(t(c.messageKey) + (c.detail ? ' ' + c.detail : ''));
    } finally { setAiBusy(false); }
  }, [text, aiBusy, busy, t]);

  useE(() => { if (files.length && !busy) run(files[0]); }, [files, lang]);

  const status = error ? 'err' : busy ? 'busy' : 'ok';
  const busyText = stage === 'loading' ? t('ocr_loading_engine') : `${t('ocr_processing')} ${Math.round(progress * 100)}%`;

  return (
    <div className="tool" style={{ '--card-hue': `var(--hue-${tool.hue})` }}>
      <ToolHead tool={tool} actions={
        <>
          {files[0] && !busy && (
            <button className="btn btn-ghost" onClick={() => run(files[0])}>{t('common_run_again')}</button>
          )}
          {text && (
            <>
              <button className="btn btn-ghost" onClick={aiTidy} disabled={aiBusy || busy} title={t('ai_tidy')}>{aiBusy ? t('ai_streaming') : t('ai_tidy')}</button>
              <button className="btn btn-ghost" onClick={() => downloadText(text, (files[0]?.name || 'ocr') + '.txt')}>{t('common_download')}</button>
              <CopyButton text={text} />
            </>
          )}
        </>
      } />

      <div className="tool-opts">
        <OptRadio
          value={lang}
          onChange={setLang}
          label={t('ocr_lang')}
          options={[
            { value: 'eng', label: t('ocr_lang_eng') },
            { value: 'chi_sim', label: t('ocr_lang_chi') },
            { value: 'chi_sim+eng', label: t('ocr_lang_both') },
          ]}
        />
      </div>

      <main className={`split ${layout === 'vert' ? 'vertical' : ''}`}>
        <section className="pane pane-input">
          <div className="pane-head">
            <span className="pane-label">{t('common_input')}</span>
            <span className="pane-meta">{files[0] ? `${files[0].name} · ${fmtBytes(files[0].size)}` : 'PNG / JPG / WEBP'}</span>
          </div>
          {imageUrl && !busy ? (
            <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 18, background: 'var(--bg-sunken)', overflow: 'auto' }}>
              <img src={imageUrl} alt="" style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 8, boxShadow: 'var(--shadow-md)' }} />
            </div>
          ) : (
            <Dropzone
              accept={tool.accepts}
              files={files}
              onFiles={setFiles}
              onRemove={(i) => setFiles(f => f.filter((_, idx) => idx !== i))}
              styleVariant={dropStyle}
              title={t('ocr_drop_title')}
              sub={t('ocr_drop_sub')}
              busy={busy}
              busyText={busyText}
            />
          )}
          {files[0] && !busy && (
            <div style={{ padding: '10px 18px', borderTop: '1px solid var(--line)', display: 'flex', gap: 8, background: 'var(--bg-elev)' }}>
              <button className="btn btn-ghost" onClick={() => setFiles([])}>{t('common_clear')}</button>
            </div>
          )}
        </section>
        <div className="divider" />
        <section className="pane pane-output">
          <div className="pane-head">
            <span className="pane-label">{t('common_text')}</span>
            <span className="pane-meta">{text ? `${text.length} ${t('common_chars')}` : ''}</span>
          </div>
          {error ? <EmptyOutput title={t('error_parse')} sub={error} />
            : busy ? <EmptyOutput title={busyText} sub="" />
            : text
              ? <pre className="md-source">{text}</pre>
              : <EmptyOutput title={t('placeholder_input_first')} sub="" />}
        </section>
      </main>

      <StatusBar status={status}
        left={files[0] && <>
          <span className="stat"><b>{files[0].name}</b></span>
          <span className="stat-sep">·</span>
          <span className="stat">{fmtBytes(files[0].size)}</span>
        </>}
        right={busy && <span className="stat">{Math.round(progress * 100)}%</span>}
      />

      <AiSettings open={showAiSet} onClose={() => setShowAiSet(false)} />
    </div>
  );
}

// =================================================================
// EXPORT
// =================================================================

const TOOL_VIEWS = {
  'cli-to-markdown': CliToMarkdown,
  'chat-to-markdown': ChatToMarkdown,
  'ai-to-markdown': AiToMarkdown,
  'pdf-to-markdown': PdfToMarkdown,
  'pdf-to-excel': PdfToExcel,
  'docx-to-markdown': DocxToMarkdown,
  'excel-to-pdf': ExcelToPdf,
  'markdown-to-html': MarkdownToHtml,
  'image-to-text': ImageToText,
};

Object.assign(window, { TOOL_VIEWS });
