/* global React, ReactDOM, Shell, ChatHead, Msg, Bubble, Mention, AttachedFiles, API */

// DataDoc AI-чат — живой прототип.
// Один Shell (без figma-холста): экран подключения → чат с роутингом в 6 сценариев,
// каждый ходит в реальные API (DataDoc / Dadata / Claude), паспорт — заглушка.

const { useState, useRef, useEffect, useCallback } = React;

// ── мелкие помощники ─────────────────────────────────────────────
const now = () => new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
window.now = now;
window.sleep = sleep;

const EmptyDoc = ({ lines = ['⌁', 'здесь появится документ'] }) => (
  <div className="docp empty">
    <div className="ph"><div className="picon">{lines[0]}</div><div className="pt">{lines[1]}</div></div>
  </div>
);
window.EmptyDoc = EmptyDoc;

// стандартная рамка документа справа (head + body)
const DocFrame = ({ title = 'окно документа', sub = 'здесь появится превью', toolbar = null, children }) => (
  <>
    <div className="head">
      <div><div className="ttl">{title}</div><div className="sub">{sub}</div></div>
      {toolbar ? <div className="toolbar">{toolbar}</div> : null}
    </div>
    <div className="body">{children}</div>
  </>
);
window.DocFrame = DocFrame;

const DefaultDoc = () => (
  <DocFrame>
    <EmptyDoc lines={['▤', 'что бы вы ни создали или нашли —\nпокажу и дам скачать здесь']} />
  </DocFrame>
);

// превью справа при открытом «Поделиться»
const ShareDocPreview = () => (
  <DocFrame title="доступ к чату" sub="только рабочее пространство">
    <EmptyDoc lines={['👥', 'коллеги видят переписку и документы этого чата —\nдоступ можно отозвать в любой момент']} />
  </DocFrame>
);

// прогресс-строка «AI работает»
// анимация «думаю» — искра + прыгающие точки (инлайн из DataDoc Thinking)
const SPARK = 'M12 0C12.7 5.8 14.6 7.9 20 8.5C14.6 9.1 12.7 11.2 12 17C11.3 11.2 9.4 9.1 4 8.5C9.4 7.9 11.3 5.8 12 0Z';
const ThinkAnim = () => (
  <div style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
    <div style={{ position: 'relative', width: 24, height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <svg viewBox="0 0 24 24" style={{ width: 22, height: 22, fill: '#006EDD', transformOrigin: 'center', animation: 'dt-spark-spin 1.8s ease-in-out infinite' }}><path d={SPARK} /></svg>
      <svg viewBox="0 0 24 24" style={{ position: 'absolute', top: -2, right: -3, width: 9, height: 9, fill: '#4c9dff', transformOrigin: 'center', animation: 'dt-spark-mini 1.8s ease-in-out 0.35s infinite' }}><path d={SPARK} /></svg>
    </div>
    <div style={{ display: 'flex', alignItems: 'flex-end', gap: 5 }}>
      {[0, 0.18, 0.36].map((d, i) => <span key={i} style={{ width: 7, height: 7, borderRadius: '50%', background: '#006EDD', animation: `dt-dot 1.3s ease-in-out ${d}s infinite` }} />)}
    </div>
  </div>
);
window.ThinkAnim = ThinkAnim;

// прогресс-строка «AI думает» — анимация вместо статичной иконки, без надписи «Claude»
const AiProg = ({ label = 'думаю' }) => (
  <div className="aiprog"><ThinkAnim /><span>{label}…</span></div>
);
window.AiProg = AiProg;

// редактируемый документ в превью: скролл + встроенный редактор (contentEditable) + скачать Word с правками
const escH = (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
function EditableDoc({ title, text, html, fileName }) {
  const ref = useRef(null);
  const initial = React.useMemo(() => (html != null ? html : '<h1>' + escH(title || '') + '</h1>' + (window.mdToHtml ? window.mdToHtml(text || '') : escH(text || ''))), []);
  const focusEd = () => { if (ref.current) ref.current.focus(); };
  const cmd = (c) => { try { document.execCommand(c, false, null); } catch (e) { } focusEd(); };
  const setSize = (pt) => {
    try {
      document.execCommand('styleWithCSS', false, true);
      document.execCommand('fontSize', false, '7');
      if (ref.current) ref.current.querySelectorAll('font[size="7"]').forEach((f) => { const s = document.createElement('span'); s.style.fontSize = pt + 'pt'; while (f.firstChild) s.appendChild(f.firstChild); f.replaceWith(s); });
    } catch (e) { }
    focusEd();
  };
  const setLH = (lh) => { if (ref.current) ref.current.style.lineHeight = lh; focusEd(); };
  const align = (v) => cmd({ left: 'justifyLeft', center: 'justifyCenter', right: 'justifyRight', justify: 'justifyFull' }[v]);
  const dl = () => {
    const inner = ref.current ? ref.current.innerHTML : initial;
    const url = URL.createObjectURL(window.makeWordBlob(title || 'Документ', inner));
    const a = document.createElement('a'); a.href = url; a.download = (fileName || title || 'Документ').replace(/[\\/:*?"<>|]/g, ' ').slice(0, 80) + '.doc'; a.click();
  };
  const tb = { cursor: 'pointer', border: '1px solid var(--line-2)', background: 'var(--paper)', borderRadius: 7, padding: '5px 10px', fontSize: 13, fontWeight: 600, color: 'var(--ink)', lineHeight: 1, minWidth: 30 };
  const sel = { border: '1px solid var(--line-2)', background: 'var(--paper)', borderRadius: 7, padding: '5px 6px', fontSize: 12.5, color: 'var(--ink)', cursor: 'pointer', fontFamily: 'var(--sans)' };
  return (
    <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
      <div style={{ display: 'flex', gap: 6, marginBottom: 8, alignItems: 'center', flexWrap: 'wrap' }}>
        <select style={sel} title="Размер шрифта" defaultValue="12" onChange={(e) => setSize(e.target.value)}>
          {[10, 11, 12, 14, 16, 18, 22].map((s) => <option key={s} value={s}>{s}pt</option>)}
        </select>
        <select style={sel} title="Межстрочный интервал" defaultValue="1.55" onChange={(e) => setLH(e.target.value)}>
          <option value="1.15">↕ 1.0</option><option value="1.35">↕ 1.15</option><option value="1.55">↕ 1.5</option><option value="2">↕ 2.0</option>
        </select>
        <select style={sel} title="Выравнивание" defaultValue="left" onChange={(e) => align(e.target.value)}>
          <option value="left">≡ слева</option><option value="center">≡ центр</option><option value="right">≡ справа</option><option value="justify">≡ ширина</option>
        </select>
        <button style={{ ...tb, fontWeight: 800 }} title="Жирный" onMouseDown={(e) => { e.preventDefault(); cmd('bold'); }}>B</button>
        <button style={{ ...tb, fontStyle: 'italic', fontFamily: 'Georgia, serif' }} title="Курсив" onMouseDown={(e) => { e.preventDefault(); cmd('italic'); }}>I</button>
        <button style={{ ...tb, textDecoration: 'underline' }} title="Подчёркнутый" onMouseDown={(e) => { e.preventDefault(); cmd('underline'); }}>U</button>
        <button style={{ ...tb, background: 'var(--blue)', color: '#fff', border: 0, marginLeft: 'auto', fontWeight: 500 }} onClick={dl}>Скачать Word</button>
      </div>
      <div ref={ref} contentEditable suppressContentEditableWarning spellCheck={false}
        style={{ flex: 1, minHeight: 0, overflowY: 'auto', background: '#fff', border: '1px solid var(--line)', borderRadius: 10, padding: '20px 24px', fontFamily: 'Georgia, "Times New Roman", serif', fontSize: 13.5, lineHeight: 1.55, color: '#0e1621', outline: 'none' }}
        dangerouslySetInnerHTML={{ __html: initial }} />
    </div>
  );
}
window.EditableDoc = EditableDoc;

// демо-письмо: оформление как в почте (кому/тема/текст/вложение) — реально ничего не отправляется
function EmailCompose({ ctx, fileName, subject }) {
  const [to, setTo] = useState('');
  const [subj, setSubj] = useState(subject || 'Документ DataDoc');
  const [body, setBody] = useState(`Здравствуйте!\n\nНаправляю ${subject || 'документ'} во вложении. Прошу ознакомиться.\n\nС уважением.`);
  const [state, setState] = useState('compose'); // compose | sent | cancel
  const inp = { width: '100%', padding: '8px 10px', border: '1px solid var(--line-2)', borderRadius: 8, fontSize: 13, fontFamily: 'var(--sans)', boxSizing: 'border-box' };
  if (state === 'sent') return <Bubble><span style={{ display: 'flex', gap: 8, alignItems: 'center' }}><span style={{ fontSize: 22, color: '#1f6b3a' }}>✓</span><span>Письмо отправлено на <b>{to || 'получателя'}</b> (демо). Вложение «{fileName}». Реально ничего не отправлено.</span></span></Bubble>;
  if (state === 'cancel') return <Bubble>Отправка отменена.</Bubble>;
  return (
    <div className="cpcard" style={{ maxWidth: 500 }}>
      <div className="h"><div className="nm">📧 Новое письмо</div><div className="ok">демо</div></div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 8 }}>
        <div><div className="k" style={{ marginBottom: 3 }}>Кому</div><input style={inp} value={to} onChange={(e) => setTo(e.target.value)} placeholder="email получателя, напр. buh@kadr.ru" /></div>
        <div><div className="k" style={{ marginBottom: 3 }}>Тема</div><input style={inp} value={subj} onChange={(e) => setSubj(e.target.value)} /></div>
        <div><div className="k" style={{ marginBottom: 3 }}>Сообщение</div><textarea style={{ ...inp, resize: 'vertical', minHeight: 92, lineHeight: 1.5 }} value={body} onChange={(e) => setBody(e.target.value)} /></div>
        <div><div className="k" style={{ marginBottom: 3 }}>Вложение</div><div className="afiles"><div className="afile" title={fileName}><div className="thumb" /><span>{fileName}</span></div></div></div>
        <div className="acta">
          <Btn kind="primary" onClick={() => setState('sent')}>Отправить →</Btn>
          <Btn kind="dashed" onClick={() => setState('cancel')}>Отмена</Btn>
        </div>
      </div>
    </div>
  );
}
window.EmailCompose = EmailCompose;

// кнопка действия из дизайна
const Btn = ({ kind = '', hint = null, onClick, children }) => (
  <div className={`btn-aip ${kind}`} onClick={onClick} role="button">{children}{hint ? <span className="hint">{hint}</span> : null}</div>
);
window.Btn = Btn;

// ── живой ввод (реюз .aiinput, рабочий + @-поповер шаблонов/контрагентов) ──
function LiveInput({ onSend, busy, quick = null, onQuick, apiRef, placeholder = 'Опишите задачу · или начните с @ для шаблона/контрагента', accept,
                     templates = [], contractors = [], catalogLoading = false, onCatalogNeeded, onPickMention }) {
  const [text, setText] = useState('');
  const [files, setFiles] = useState([]);
  const [menu, setMenu] = useState(false);
  const [filter, setFilter] = useState('');
  const [dadataCps, setDadataCps] = useState([]);
  const taRef = useRef(null);
  const blurTimer = useRef(null);

  // живой поиск в ЕГРЮЛ (Dadata) для @-контрагентов после 3 символов
  useEffect(() => {
    if (!menu || filter.trim().length < 3) { setDadataCps([]); return; }
    const id = setTimeout(async () => {
      try {
        const sugg = window.ddPickSuggestions ? window.ddPickSuggestions(await API.suggestParty(filter.trim(), 5)) : [];
        setDadataCps(sugg.map(window.ddPartyFields || ((x) => x)));
      } catch (e) { setDadataCps([]); }
    }, 350);
    return () => clearTimeout(id);
  }, [filter, menu]);

  const grow = () => { const t = taRef.current; if (t) { t.style.height = 'auto'; t.style.height = Math.min(t.scrollHeight, 160) + 'px'; } };
  useEffect(grow, [text]);

  const send = () => {
    if (busy) return;
    if (!text.trim() && !files.length) return;
    onSend(text.trim(), files);
    setText(''); setFiles([]); setMenu(false);
  };
  const addFiles = (list) => setFiles((f) => [...f, ...Array.from(list)]);
  if (apiRef) apiRef.current = { addFiles }; // даём внешнему dropzone добавлять файлы во ввод

  // @-поповер: реагируем на «@…» в конце ввода
  const onChange = (e) => {
    const v = e.target.value; setText(v);
    const m = v.match(/@([^\s@]*)$/);
    if (m) { setMenu(true); setFilter(m[1] || ''); onCatalogNeeded && onCatalogNeeded(); }
    else setMenu(false);
  };

  const f = filter.toLowerCase().trim();
  const tpl = (templates || []).filter((t) => !f || t.name.toLowerCase().includes(f) || (t.folders || []).join(' ').toLowerCase().includes(f)).slice(0, 8);
  const cps = (contractors || []).filter((c) => !f || c.name.toLowerCase().includes(f) || (c.inn || '').includes(f)).slice(0, 6);
  const localInn = new Set(cps.map((c) => c.inn).filter(Boolean));
  const egrul = dadataCps.filter((p) => p.inn && !localInn.has(p.inn)).slice(0, 5);
  const firstPick = tpl[0] ? { kind: 'tpl', ...tpl[0] }
    : cps[0] ? { kind: 'cp', ...cps[0] }
    : egrul[0] ? { kind: 'cp', name: egrul[0].value, inn: egrul[0].inn, source: 'egrul' } : null;

  const pick = (item) => {
    setText(''); setMenu(false); setFilter('');
    onPickMention && onPickMention(item);
  };

  const onKey = (e) => {
    if ((e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey) {
      e.preventDefault();
      if (menu && firstPick) { pick(firstPick); return; }
      send();
    } else if (e.key === 'Escape') { setMenu(false); }
  };

  return (
    <div className="aiinput">
      {quick && quick.length ? (
        <div className="quick">
          {quick.map((q, i) => <div className="qc" key={i} title="нажмите, чтобы выполнить" onClick={() => { if (busy) return; onQuick ? onQuick(q) : setText(q); }}>⚡ {q}</div>)}
        </div>
      ) : null}
      {files.length ? (
        <div className="afiles" style={{ marginBottom: 8 }}>
          {files.map((file, i) => (
            <div className="afile" key={i} title={file.name}>
              <div className="thumb" />
              <span>{file.name.length > 22 ? file.name.slice(0, 20) + '…' : file.name}</span>
              <span style={{ cursor: 'pointer', color: 'var(--ink-3)', marginLeft: 2 }}
                onClick={() => setFiles((fs) => fs.filter((_, j) => j !== i))}>✕</span>
            </div>
          ))}
        </div>
      ) : null}
      <div className="field">
        <div className="row">
          <textarea
            ref={taRef}
            className="text"
            value={text}
            rows={1}
            placeholder={placeholder}
            spellCheck={false}
            onChange={onChange}
            onKeyDown={onKey}
            onFocus={() => { if (blurTimer.current) clearTimeout(blurTimer.current); }}
            onBlur={() => { blurTimer.current = setTimeout(() => setMenu(false), 150); }}
            style={{ flex: 1, border: 0, outline: 'none', resize: 'none', background: 'transparent',
                     font: 'inherit', color: 'inherit', lineHeight: 1.5, padding: '2px 0', maxHeight: 160, overflowY: 'auto' }}
          />
          <div className="actions">
            <label className="ic" title="прикрепить файл" style={{ cursor: 'pointer' }}>
              📎
              <input type="file" hidden multiple accept={accept} onChange={(e) => { addFiles(e.target.files); e.target.value = ''; }} />
            </label>
            <div className="ic send" title="отправить" onClick={send}
                 style={{ opacity: busy ? 0.4 : 1, pointerEvents: busy ? 'none' : 'auto' }}>
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 13V3.5M8 3.5L3.5 8M8 3.5L12.5 8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /></svg>
            </div>
          </div>
        </div>

        {menu ? (
          <div className="amenu" onMouseDown={(e) => e.preventDefault()}>
            {catalogLoading && !tpl.length && !cps.length ? (
              <div style={{ padding: 14, color: 'var(--ink-3)', fontSize: 12 }}>загружаю шаблоны и контрагентов…</div>
            ) : null}
            {tpl.length ? (
              <div>
                <div className="cat">Шаблоны</div>
                {tpl.map((t, i) => (
                  <div className={`it ${i === 0 ? 'on' : ''}`} key={t.id} onClick={() => pick({ kind: 'tpl', ...t })}>
                    <div className="ic">▤</div>
                    <div className="body"><div className="name">{t.name}</div><div className="sub">{(t.folders || []).join(' / ') || 'шаблон'}</div></div>
                    <div className="kb">{i === 0 ? '↵' : ''}</div>
                  </div>
                ))}
              </div>
            ) : null}
            {cps.length ? (
              <div>
                <div className="cat">Контрагенты · в базе</div>
                {cps.map((c) => (
                  <div className="it" key={c.id} onClick={() => pick({ kind: 'cp', ...c })}>
                    <div className="ic">👤</div>
                    <div className="body"><div className="name">{c.name}</div><div className="sub">{c.inn ? 'ИНН ' + c.inn : 'контрагент'}</div></div>
                  </div>
                ))}
              </div>
            ) : null}
            {egrul.length ? (
              <div>
                <div className="cat">Контрагенты · ЕГРЮЛ (Dadata)</div>
                {egrul.map((p, i) => (
                  <div className="it" key={'e' + i} onClick={() => pick({ kind: 'cp', name: p.value, inn: p.inn, source: 'egrul' })}>
                    <div className="ic">🏢</div>
                    <div className="body"><div className="name">{p.value}</div><div className="sub">{p.inn ? 'ИНН ' + p.inn : 'из ЕГРЮЛ'}</div></div>
                  </div>
                ))}
              </div>
            ) : null}
            {!catalogLoading && !tpl.length && !cps.length && !egrul.length ? (
              <div style={{ padding: 14, color: 'var(--ink-3)', fontSize: 12 }}>{filter.trim().length < 3 ? 'введите 3+ символа для поиска' : 'ничего не нашли по «' + filter + '»'}</div>
            ) : null}
          </div>
        ) : null}
      </div>
    </div>
  );
}

// ── экран приветствия ────────────────────────────────────────────
const WELCOME_CARDS = [
  ['🗂', 'Массовое создание документов', 'любой шаблон · AI подскажет что приложить', 'Массовое создание документов по шаблону'],
  ['❓', 'Собрать по вопросам', 'AI пишет текст и подводит нормы', 'Собери договор по вопросам'],
  ['🔎', 'Найти документы', 'словами: «договоры с Восходом»', 'Найди мои документы'],
  ['👥', 'Добавить контрагента', 'по ИНН из ЕГРЮЛ или паспорту', 'Добавь контрагента'],
  ['📎', 'Заполнить из файлов', 'киньте сканы — разложу по полям', 'Заполнить документ из файлов'],
  ['⚖', 'Сверить на тождественность', 'два документа → где расхождения', 'Сверь два документа на тождественность'],
  ['📄', 'Сделать акт к договору', 'стороны и сумму — из договора', 'Сделай акт к договору'],
  ['🛡', 'Проверить на риски', 'найду слабые места и правки', 'Проверь договор на риски'],
];

const Welcome = ({ onPick }) => (
  <div className="wlc">
    <WelcomeLogo />
    <div className="wlc-sub">AI-ассистент DataDoc. Создаёт, распознаёт, проверяет и сверяет договоры, акты и счета прямо в чате, как ваш личный юрист.</div>
    <div className="wlc-grid">
      {WELCOME_CARDS.map((c, i) => (
        <div className="wlc-card" key={i} onClick={() => onPick(c[3])}>
          <div className="ic">{c[0]}</div>
          <div><div className="t">{c[1]}</div><div className="d">{c[2]}</div></div>
        </div>
      ))}
    </div>
    <div className="wlc-hint">Начните с <b>@</b> — выберете шаблон или контрагента · или просто опишите задачу словами</div>
  </div>
);

// ── экран подключения к DataDoc ──────────────────────────────────
function ConnectScreen({ onConnected }) {
  const [key, setKey] = useState(API.savedApiKey());
  const [err, setErr] = useState('');
  const [busy, setBusy] = useState(false);
  const connect = async () => {
    if (!key.trim()) { setErr('Вставьте API-ключ DataDoc.'); return; }
    setBusy(true); setErr('');
    try { await API.auth(key.trim()); onConnected(); }
    catch (e) { setErr('Не удалось подключиться: ' + e.message + '\nКлюч — на странице lk.datadoc.ru/profile'); }
    finally { setBusy(false); }
  };
  return (
    <div style={{ position: 'fixed', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
                  background: 'linear-gradient(135deg,#006EDD,#0a4fa0)', fontFamily: 'var(--sans)' }}>
      <div style={{ width: 420, background: '#fff', borderRadius: 16, padding: '28px 28px 24px', boxShadow: '0 20px 60px rgba(0,0,0,.25)' }}>
        <div style={{ fontWeight: 800, fontSize: 22, color: '#006EDD' }}>DataDoc<span style={{ color: '#1b2733' }}> AI</span></div>
        <input
          type="password" value={key} placeholder="API-ключ DataDoc"
          onChange={(e) => setKey(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && connect()}
          style={{ width: '100%', marginTop: 16, padding: '11px 13px', border: '1.5px solid #d6dde3', borderRadius: 10, fontSize: 14, fontFamily: 'var(--mono, monospace)' }}
        />
        {err ? <div style={{ marginTop: 10, color: '#c33', fontSize: 12, whiteSpace: 'pre-line' }}>{err}</div> : null}
        <button onClick={connect} disabled={busy}
          style={{ width: '100%', marginTop: 14, padding: '11px', border: 0, borderRadius: 10, background: '#006EDD', color: '#fff', fontSize: 14, fontWeight: 600, cursor: 'pointer', opacity: busy ? 0.6 : 1 }}>
          {busy ? 'Подключаюсь…' : 'Подключиться'}
        </button>
      </div>
    </div>
  );
}

// ── роутер намерений ─────────────────────────────────────────────
function route(text, files) {
  const t = (text || '').toLowerCase();
  const inn = (text.match(/\b\d{10}(?:\d{2})?\b/) || [])[0];
  if (/сверь|сверк|тождествен|идентичн|сравни|расхожд|две редакции|две версии|протокол разноглас/.test(t)) return 'identity';
  if (/риск|слаб(ое|ых|ые) мест|провер.*(договор|документ)|в нашу пользу|чего не хватает|ревиз|асиммет|подводн/.test(t)) return 'risk';
  if (/массов|пачк(ой|у|и)?|сканам паспорт|договор[а-я]* по (паспорт|скан)|5 договор/.test(t)) return 'mass';
  if (/\bакт\b|акт к|акт по|закрыть месяц|сдела.*акт/.test(t)) return 'akt';
  if (/найди|найти|поиск|покажи (все |мои |договор|документ)|ищу.*документ|мои документы/.test(t)) return 'find';
  if (/собер.*вопрос|по вопросам|состав.*(договор|текст)|с нуля|нет шаблона|напиши договор|консультант/.test(t)) return 'qna';
  if (/паспорт/.test(t)) return 'passport';
  if (/контрагент|егрюл|егрип|поставщик|по инн|добав.*(компан|контрагент|юрлиц|фирм)/.test(t) || (inn && !/договор|заполни|шаблон/.test(t))) return 'counteragent';
  if (/распозна|скан|извлеки|вытащи данн|что (в|на) (файл|скан|фото)|прочитай (файл|скан)/.test(t)) return 'scan';
  if (/заполни|запол|из файл|создай|сдела.*(договор|счёт|счет|документ)|договор|счёт|счет|шаблон|документ/.test(t)) return 'fill';
  if (files && files.length) return 'scan';
  return 'help';
}

const HEADS = {
  mass: ['Массовое создание документов', 'любой шаблон · данные из сканов и Дадаты'],
  fill: ['Заполнение документа', 'по шаблону DataDoc'],
  qna: ['Составление документа', 'по вопросам · Claude'],
  find: ['Поиск документов', 'по запросу'],
  akt: ['Акт к договору', 'данные из договора'],
  passport: ['Распознавание паспорта', 'карточка физлица'],
  counteragent: ['Добавление контрагента', 'из ЕГРЮЛ · Дадата'],
  risk: ['Проверка на риски', 'слабые места договора'],
  identity: ['Сверка документов', 'тождественность'],
  scan: ['Распознавание сканов', 'извлечение данных · Claude'],
  help: ['Новый чат с DataDoc AI', 'ассистент по документам'],
};

// пресеты быстрых действий над полем ввода — свои для каждого сценария
const PRESETS = {
  help: ['Массовое создание документов', 'Найди неподписанные за месяц', 'Проверь договор на риски'],
  mass: ['Договор оказания услуг', 'Договор поставки', 'Доверенность'],
  fill: ['заполнить из паспорта', 'вставить реквизиты текстом', 'другой шаблон'],
  qna: ['договор оказания услуг', 'NDA с подрядчиком', 'договор аренды'],
  find: ['неподписанные за месяц', 'акты за май', 'счета без НДС', 'договоры дороже 100 000 ₽'],
  counteragent: ['по фото визитки', 'вставить реквизиты текстом', 'найти по названию'],
  risk: ['насколько он в нашу пользу?', 'чего не хватает?', 'сравнить с типовым'],
  identity: ['наш черновик vs подписанный', 'две редакции договора'],
  akt: ['акт за май', 'закрыть месяц актами'],
  passport: ['создать контрагента', 'подставить в документ'],
  scan: ['извлечь реквизиты', 'создать контрагента из скана'],
  topup: ['сколько доксов на массовую отправку?'],
};

// демо-история чатов для бокового меню
const DEMO_CHATS = [
  ['Сегодня', ['Массовое создание договоров', 'Акт № 1 к договору №47']],
  ['Вчера', ['Проверка договора подряда', 'Поиск: Восход, 2 квартал', 'Доп.соглашение ООО Ромашка']],
  ['Ранее', ['Договор аренды по паспорту', 'Сверка двух редакций', 'Добавление ООО «Восход»', 'Составление договора услуг']],
];
function initialChats() {
  const out = [{ id: 'c-0', title: 'Новый чат', group: 'Сегодня' }];
  DEMO_CHATS.forEach(([group, items], gi) => items.forEach((title, ii) => out.push({ id: `demo-${gi}-${ii}`, title, group })));
  return out;
}
const CHAT_GROUPS = ['Сегодня', 'Вчера', 'Ранее'];

// ── сохранение чатов между перезагрузками (снимок HTML переписки + документа) ──
const PERSIST_KEY = 'dd_chat_state_v2';
function loadPersisted() { try { return JSON.parse(localStorage.getItem(PERSIST_KEY) || 'null'); } catch (e) { return null; } }
function restoreMsgsNode(body) {
  return body ? [{ key: 'restore', node: <div className="restored-chat" style={{ display: 'flex', flexDirection: 'column', gap: 15 }} dangerouslySetInnerHTML={{ __html: body }} /> }] : [];
}
function restoreDocNode(docHtml) {
  return docHtml ? <div style={{ display: 'contents' }} dangerouslySetInnerHTML={{ __html: docHtml }} /> : null;
}

// демо-поповер «Поделиться» (без реального доступа)
function SharePopover({ onClose }) {
  return (
    <>
      <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 20 }} />
      <div className="sharemenu">
        <div className="sh-h">Поделиться чатом<span className="sh-x" onClick={onClose} style={{ cursor: 'pointer' }}>✕</span></div>
        <div className="sh-sub">Демо: доступ к чату для коллег из рабочего пространства <b>«Студия Кадр»</b>. Реально ничего не отправляется.</div>
        <div className="sh-add"><span className="sh-search">🔎 добавить коллегу по имени или e-mail</span></div>
        <div className="sh-list">
          <div className="sh-row"><div className="sh-ava you">НК</div><div className="sh-who"><div className="nm">Никита Колосов <span className="tag">вы</span></div><div className="em">n.kolosov@kadr.ru</div></div><div className="sh-role">Владелец</div></div>
          <div className="sh-row"><div className="sh-ava">ММ</div><div className="sh-who"><div className="nm">Макаров Михаил</div><div className="em">m.makarov@kadr.ru</div></div><div className="sh-role sel">Редактор ▾</div><div className="sh-revoke">Отозвать</div></div>
          <div className="sh-row"><div className="sh-ava">ВМ</div><div className="sh-who"><div className="nm">Винников Максим</div><div className="em">m.vinnikov@kadr.ru</div></div><div className="sh-role sel">Просмотр ▾</div><div className="sh-revoke">Отозвать</div></div>
        </div>
        <div className="sh-foot">
          <span className="sh-link" onClick={onClose}>🔗 Скопировать ссылку · демо</span>
          <span className="sh-done" onClick={onClose} style={{ cursor: 'pointer' }}>Готово</span>
        </div>
      </div>
    </>
  );
}

// ── корневое приложение ──────────────────────────────────────────
function App() {
  const persistedRef = useRef(loadPersisted());
  const P = persistedRef.current;
  const embedded = (() => { try { return window.self !== window.top; } catch (e) { return true; } })(); // внутри iframe продукта → без своего сайдбара
  const activeSnap = (P && P.snaps && P.active) ? P.snaps[P.active] : null;

  const [connected, setConnected] = useState(!!API.getToken());
  const [started, setStarted] = useState(() => !!(activeSnap && activeSnap.started));
  const [msgs, setMsgs] = useState(() => activeSnap ? restoreMsgsNode(activeSnap.body) : []);
  const [doc, setDocState] = useState(() => restoreDocNode(activeSnap && activeSnap.docHtml));
  const [head, setHead] = useState(() => (activeSnap && activeSnap.head) ? activeSnap.head : { title: HEADS.help[0], sub: HEADS.help[1] });
  const [busy, setBusy] = useState(false);
  const [scenario, setScenario] = useState('help');
  const [balance, setBalance] = useState(() => (P && typeof P.balance === 'number') ? P.balance : 1240);
  const [chatList, setChatList] = useState(() => (P && Array.isArray(P.list) && P.list.length) ? P.list : initialChats());
  const [chatId, setChatId] = useState(() => (P && P.active) ? P.active : 'c-0');
  const [search, setSearch] = useState('');
  const [collapsed, setCollapsed] = useState(embedded); // в iframe меню чатов стартует свёрнутым — экономим ширину
  const [docOpen, setDocOpen] = useState(false);        // панель документа открывается кнопкой / при появлении документа
  const [docW, setDocW] = useState(520);
  const [shareOpen, setShareOpen] = useState(false);
  const [docFull, setDocFull] = useState(false);
  const idRef = useRef(0);
  const scrollRef = useRef(null);
  const docContentRef = useRef(null);
  const storeRef = useRef(null);
  if (storeRef.current === null) {
    storeRef.current = {};
    if (P && P.snaps) Object.keys(P.snaps).forEach((id) => {
      const snap = P.snaps[id];
      storeRef.current[id] = { started: !!snap.started, head: snap.head || { title: HEADS.help[0], sub: HEADS.help[1] }, messages: restoreMsgsNode(snap.body), doc: restoreDocNode(snap.docHtml), snap };
    });
  }
  const liveApi = useRef(null);            // доступ к addFiles поля ввода
  const [dragging, setDragging] = useState(false);
  const dragDepth = useRef(0);

  // каталог для @-поповера (реальные шаблоны и контрагенты из DataDoc)
  const catalogRef = useRef({ templates: [], contractors: [], loaded: false, loading: false });
  const [, setCatalogTick] = useState(0);
  const loadCatalog = useCallback(async () => {
    const c = catalogRef.current;
    if (c.loaded || c.loading) return;
    c.loading = true; setCatalogTick((t) => t + 1);
    try {
      const tree = await API.datadoc('GET', '/folders/allcontent');
      const roots = Array.isArray(tree) ? tree : (tree ? [tree] : []);
      const out = []; (window.collectTemplateTree || (() => {}))(roots, [], out);
      c.templates = out;
    } catch (e) { c.templates = []; }
    try {
      const ca = await API.datadoc('GET', '/counteragents?from=0&to=50');
      const items = (ca && ca.items) || [];
      c.contractors = items.map((x) => ({ id: x.id, name: x.legalName || x.name || x.fullName || '—', inn: x.inn || '' }));
    } catch (e) { c.contractors = []; }
    c.loaded = true; c.loading = false; setCatalogTick((t) => t + 1);
  }, []);

  const scrollDown = useCallback(() => {
    requestAnimationFrame(() => { const s = scrollRef.current; if (s) s.scrollTop = s.scrollHeight; });
  }, []);
  useEffect(scrollDown, [msgs, scrollDown]);
  // реальное списание доксов: стоимость Claude-операций (по токенам) вычитается из баланса
  useEffect(() => { API.setChargeHook((n) => setBalance((b) => Math.max(0, b - n))); }, []);

  // стабильный ctx для сценариев
  const ctxRef = useRef(null);
  if (!ctxRef.current) {
    const push = (node) => { const key = 'm' + (++idRef.current); setMsgs((m) => [...m, { key, node }]); return key; };
    const remove = (key) => setMsgs((m) => m.filter((x) => x.key !== key));
    const clearTyping = () => { const k = ctxRef.current && ctxRef.current._typing; if (k) { remove(k); ctxRef.current._typing = null; } };
    ctxRef.current = {
      API,
      _typing: null,
      // индикатор «печатает» (3 точки) — показывается перед ответом, снимается первым сообщением
      think: () => { const k = push(<Msg who="ai" ts={now()}><Bubble><ThinkAnim /></Bubble></Msg>); ctxRef.current._typing = k; return k; },
      stopThink: clearTyping,
      ai: (text) => { clearTyping(); return push(<Msg who="ai" ts={now()}><Bubble>{text}</Bubble></Msg>); },
      aiNode: (node) => { clearTyping(); return push(<Msg who="ai" ts={now()}>{node}</Msg>); },
      you: (text, files) => push(
        <Msg who="you" ts={now()}>
          <Bubble>{text}{files && files.length ? <AttachedFiles files={files.map((f) => f.name)} /> : null}</Bubble>
        </Msg>),
      node: (node) => { clearTyping(); return push(node); },
      remove,
      setDoc: (node) => { setDocState(node); if (node) setDocOpen(true); },
      setHead: (title, sub) => setHead({ title, sub }),
      busy: (b) => setBusy(b),
      addBalance: (n) => setBalance((b) => b + n),
      charge: (n) => setBalance((b) => Math.max(0, b - Math.max(0, Math.ceil(n || 0)))),
      // сценарий, ждущий вложение: следующее сообщение-только-с-файлами продолжит его
      _expect: null,
      expect: (name) => { ctxRef.current._expect = name; },
      // режим «захвата»: пока активен, все сообщения идут в handler (напр. заполнение шаблона из чата)
      _capture: null,
      capture: (fn) => { ctxRef.current._capture = fn; },
      uncapture: () => { ctxRef.current._capture = null; },
      // добавить контрагента в живой каталог @-поповера (для сессии)
      addContractor: (c) => { const cc = catalogRef.current; if (!cc.contractors.some((x) => x.inn && x.inn === c.inn)) { cc.contractors = [c, ...cc.contractors]; setCatalogTick((t) => t + 1); } },
      scrollDown,
      token: () => API.getToken(),
    };
  }
  const ctx = ctxRef.current;

  const updateTitle = (id, text) => setChatList((l) => l.map((x) => x.id === id ? { ...x, title: (text || '').slice(0, 42) || x.title } : x));

  // снимок текущего чата (HTML переписки + документа) для сохранения между перезагрузками
  const captureSnap = () => ({
    title: (chatList.find((c2) => c2.id === chatId) || {}).title,
    head, started: true,
    body: scrollRef.current ? scrollRef.current.innerHTML : '',
    docHtml: (doc && !shareOpen && docContentRef.current) ? docContentRef.current.innerHTML : '',
  });
  const persistAll = () => {
    try {
      const snaps = {};
      Object.keys(storeRef.current).forEach((id) => { const s = storeRef.current[id]; if (s && s.snap) snaps[id] = s.snap; });
      localStorage.setItem(PERSIST_KEY, JSON.stringify({ list: chatList, active: chatId, snaps, balance }));
    } catch (e) { /* хранилище недоступно */ }
  };
  const saveCurrent = () => {
    const snap = started ? captureSnap() : null;
    storeRef.current[chatId] = { messages: msgs, doc, head, started, snap };
  };
  // авто-сохранение при любом изменении переписки/документа/баланса (с дебаунсом)
  useEffect(() => {
    const t = setTimeout(() => {
      if (started) storeRef.current[chatId] = { messages: msgs, doc, head, started, snap: captureSnap() };
      persistAll();
    }, 500);
    return () => clearTimeout(t);
  }, [msgs, doc, head, started, chatId, chatList, balance]);

  const handleSend = async (text, files) => {
    if (!started) { updateTitle(chatId, text); setStarted(true); }
    ctx.you(text, files);
    if (balance < 1) { ctx.aiNode(<Bubble><span style={{ color: '#c33' }}>Недостаточно доксов для операции. Пополните баланс — нажмите «пополнить» вверху.</span></Bubble>); return; }
    // режим захвата (напр. заполнение шаблона из чата) — перехватывает сообщение
    if (ctx._capture) {
      const cap = ctx._capture;
      ctx.think(); await sleep(300); ctx.busy(true);
      try { await cap(ctx, { text, files }); }
      catch (e) { console.error(e); ctx.aiNode(<Bubble><span style={{ color: '#c33' }}>⚠ {e.message}</span></Bubble>); }
      finally { ctx.busy(false); ctx.stopThink(); }
      return;
    }
    // если сценарий ждал файл и пришло сообщение только с файлами — продолжаем его
    const pending = ctx._expect;
    ctx._expect = null;
    const scenario = (!text.trim() && files && files.length && pending) ? pending : route(text, files);
    setScenario(scenario);
    const h = HEADS[scenario] || HEADS.help;
    ctx.setHead(h[0], h[1]);
    ctx.think();
    await sleep(450);
    const fn = (window.SCENARIOS || {})[scenario] || (window.SCENARIOS || {}).help;
    ctx.busy(true);
    try { await fn(ctx, { text, files }); }
    catch (e) { console.error(e); ctx.aiNode(<Bubble><span style={{ color: '#c33' }}>⚠ Ошибка: {e.message}</span></Bubble>); }
    finally { ctx.busy(false); ctx.stopThink(); }
  };

  const onPickMention = (item) => {
    const first = !started;
    if (first) setStarted(true);
    const run = async (p) => { ctx.think(); await sleep(300); ctx.busy(true); try { await p; } catch (e) { ctx.aiNode(<Bubble><span style={{ color: '#c33' }}>⚠ {e.message}</span></Bubble>); } finally { ctx.busy(false); ctx.stopThink(); } };
    if (item.kind === 'tpl') {
      setScenario('fill');
      ctx.you(<>Заполнить <Mention kind="tpl" name={item.name} /></>);
      if (first) updateTitle(chatId, 'Заполнить: ' + item.name);
      run(window.SCENARIOS.fillTemplate(ctx, { id: item.id, name: item.name }));
    } else if (item.kind === 'cp') {
      setScenario('counteragent');
      if (first) updateTitle(chatId, 'Контрагент: ' + item.name);
      window.ddShowParty(ctx, { value: item.name, inn: item.inn || '', source: item.source || 'base', id: item.id });
    }
  };

  const newChat = () => {
    saveCurrent();
    ctx._capture = null; ctx._expect = null; window.__activeFill = null;
    const id = 'c' + (++idRef.current);
    setChatList((l) => [{ id, title: 'Новый чат', group: 'Сегодня' }, ...l]);
    setChatId(id); setMsgs([]); setDocState(null); setHead({ title: HEADS.help[0], sub: HEADS.help[1] }); setStarted(false); setShareOpen(false); setSearch(''); setScenario('help');
  };

  const switchChat = (id) => {
    if (id === chatId) return;
    saveCurrent();
    ctx._capture = null; ctx._expect = null; window.__activeFill = null;
    const s = storeRef.current[id];
    if (s) { setMsgs(s.messages); setDocState(s.doc); setHead(s.head); setStarted(s.started); }
    else {
      const chat = chatList.find((x) => x.id === id);
      const t = chat ? chat.title : 'Чат';
      setMsgs([{ key: 'seed-' + id, node: <Msg who="ai" ts={now()}><Bubble>Это сохранённый чат «{t}» (демо из истории). Продолжите его или начните новый.</Bubble></Msg> }]);
      setDocState(null); setHead({ title: t, sub: 'демо-чат из истории' }); setStarted(true);
    }
    setChatId(id); setShareOpen(false);
  };

  const startResize = (e) => {
    e.preventDefault();
    const move = (ev) => setDocW(Math.max(360, Math.min(1000, window.innerWidth - ev.clientX)));
    const up = () => { window.removeEventListener('mousemove', move); window.removeEventListener('mouseup', up); document.body.style.userSelect = ''; };
    window.addEventListener('mousemove', move); window.addEventListener('mouseup', up); document.body.style.userSelect = 'none';
  };

  const openTopup = async () => {
    if (!started) setStarted(true);
    setScenario('topup');
    ctx.setHead('Пополнение баланса', 'доксы для операций (демо)');
    ctx.think(); await sleep(350);
    window.SCENARIOS.topup(ctx, { balance });
  };

  // dropzone: перетаскивание файлов в область чата → загрузка в поле ввода
  const dzHasFiles = (e) => e.dataTransfer && Array.from(e.dataTransfer.types || []).indexOf('Files') >= 0;
  const onDragEnter = (e) => { if (!dzHasFiles(e)) return; e.preventDefault(); dragDepth.current++; setDragging(true); };
  const onDragOver = (e) => { if (!dzHasFiles(e)) return; e.preventDefault(); try { e.dataTransfer.dropEffect = 'copy'; } catch (x) { } };
  const onDragLeave = (e) => { if (!dzHasFiles(e)) return; dragDepth.current = Math.max(0, dragDepth.current - 1); if (!dragDepth.current) setDragging(false); };
  const onDrop = (e) => { e.preventDefault(); dragDepth.current = 0; setDragging(false); const files = Array.from((e.dataTransfer && e.dataTransfer.files) || []); if (files.length && liveApi.current) liveApi.current.addFiles(files); };

  const cat = catalogRef.current;
  const inputProps = {
    onSend: handleSend, busy, apiRef: liveApi,
    templates: cat.templates, contractors: cat.contractors, catalogLoading: cat.loading,
    onCatalogNeeded: loadCatalog, onPickMention,
    quick: PRESETS[scenario] || PRESETS.help, onQuick: (q) => handleSend(q, []),
  };

  if (!connected) return <ConnectScreen onConnected={() => setConnected(true)} />;

  const q = search.trim().toLowerCase();
  const visibleChats = chatList.filter((c2) => !q || c2.title.toLowerCase().includes(q));
  const paneVisible = docOpen || shareOpen; // панель документа занимает место только когда открыта

  return (
    <div className={`ai-shell has-hist ${collapsed ? 'hist-min' : ''}`} style={{ gridTemplateColumns: `${embedded ? '' : '88px '}${collapsed ? 56 : 250}px 1fr${paneVisible ? ` ${docW}px` : ''}` }}>
      {embedded ? null : <Sidebar active="chat" />}

      {collapsed ? (
        <div className="chist collapsed">
          <div className="chist-exp" title="Развернуть чаты" onClick={() => setCollapsed(false)}>»</div>
          <div className="chist-new mini" title="Новый чат" onClick={newChat}>＋</div>
          <div className="chist-mini-list">
            {visibleChats.slice(0, 8).map((c2) => <div key={c2.id} className={`cm ${c2.id === chatId ? 'on' : ''}`} title={c2.title} onClick={() => switchChat(c2.id)}>✶</div>)}
          </div>
        </div>
      ) : (
        <div className="chist">
          <div className="chist-top"><span className="ct">Чаты</span><span className="chist-collapse" title="Свернуть" style={{ cursor: 'pointer' }} onClick={() => setCollapsed(true)}>«</span></div>
          <div className="chist-new" onClick={newChat}>＋ Новый чат</div>
          <div className="chist-search"><span>🔎</span><input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="поиск по диалогам" style={{ border: 0, outline: 'none', background: 'transparent', font: 'inherit', color: 'var(--ink)', width: '100%' }} /></div>
          <div className="chist-list">
            {CHAT_GROUPS.map((g) => {
              const items = visibleChats.filter((c2) => c2.group === g);
              if (!items.length) return null;
              return (
                <div className="chist-grp" key={g}>
                  <div className="gl">{g}</div>
                  {items.map((c2) => <div key={c2.id} className={`ci ${c2.id === chatId ? 'on' : ''}`} onClick={() => switchChat(c2.id)}><span className="d">✶</span><span className="nm">{c2.title}</span></div>)}
                </div>
              );
            })}
            {!visibleChats.length ? <div style={{ padding: 10, color: 'var(--ink-3)', fontSize: 12 }}>ничего не найдено</div> : null}
          </div>
        </div>
      )}

      <div className="chat-col" style={{ position: 'relative' }}
           onDragEnter={onDragEnter} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}>
        {dragging ? (
          <div className="dropzone-ov"><div className="dz-inner"><div style={{ fontSize: 30, marginBottom: 8 }}>📎</div>Отпустите файлы — загружу и распознаю</div></div>
        ) : null}
        <div className="chat-head">
          <div><div className="title">{head.title}</div><div className="sub">{head.sub}</div></div>
          <div className="chat-head-actions">
            <div className="share-btn" onClick={() => setDocOpen((o) => !o)} title={docOpen ? 'Скрыть окно документа' : 'Показать окно документа'}
              style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12.5, color: docOpen ? 'var(--blue)' : 'var(--ink-2)' }}>
              <span className="ico">▤</span>{docOpen ? 'Скрыть документ' : 'Документ'}
            </div>
            <div className="share-btn" onClick={() => setShareOpen((o) => !o)} style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12.5, color: 'var(--ink-2)' }}>
              <span className="ico">🔗</span>Поделиться
            </div>
            <div className={`doxes ${balance < 80 ? 'low' : ''}`} onClick={openTopup} title="пополнить баланс" style={{ cursor: 'pointer' }}>
              <span className="di">⬨</span><span>{balance.toLocaleString('ru-RU')} доксов</span>
              <span style={{ color: 'var(--ink-3)', borderLeft: '1px solid var(--line-2)', paddingLeft: 6, marginLeft: 4, fontSize: 11 }}>пополнить</span>
            </div>
          </div>
          {shareOpen ? <SharePopover onClose={() => setShareOpen(false)} /> : null}
        </div>
        <div className="chat-body">
          {started ? (
            <div ref={scrollRef} style={{ flex: 1, minHeight: 0, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 15 }}>
              {msgs.map((m) => <React.Fragment key={m.key}>{m.node}</React.Fragment>)}
            </div>
          ) : (
            <Welcome onPick={(prompt) => handleSend(prompt, [])} />
          )}
          <LiveInput {...inputProps} />
        </div>
      </div>

      {paneVisible ? (
        <div className={`docpane ${docFull ? 'fullscreen' : ''}`}>
          {!docFull ? <div className="doc-resize" onMouseDown={startResize} title="потянуть, чтобы изменить ширину" /> : null}
          <div className="docfull-btn" onClick={() => (docFull ? setDocFull(false) : setDocOpen(false))} title="Закрыть окно документа"
            style={{ position: 'absolute', top: 12, right: 44, zIndex: 130 }}>✕</div>
          {doc && !shareOpen ? (
            <div className="docfull-btn" onClick={() => setDocFull((f) => !f)} title={docFull ? 'Свернуть' : 'Открыть на полный экран'}
              style={{ position: 'absolute', top: 12, right: 16, zIndex: 130, padding: '4px 7px' }}>
              {docFull ? '⤢' : '⛶'}
            </div>
          ) : null}
          <div ref={docContentRef} style={{ display: 'contents' }}>
            {shareOpen ? <ShareDocPreview /> : (doc || <DefaultDoc />)}
          </div>
        </div>
      ) : null}
    </div>
  );
}

// уберём загрузочный экран и смонтируемся
const boot = document.getElementById('boot');
if (boot) boot.remove();
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
