// web/screens/settings.jsx — экран Настройки + device-flow подключения Я.Диска.

function SettingsScreen() {
  const [status, reload] = useAsync(() => api.yandex.Status(), []);
  const [step, setStep] = React.useState(null); // null | "starting" | "polling"
  const [code, setCode] = React.useState(null);  // { userCode, deviceCode, verificationUrl }
  const [_, toast] = useToasts();

  const start = async () => {
    setStep("starting");
    try {
      const dc = await api.yandex.StartConnect();
      setCode(dc);
      setStep("polling");
      // Стартуем поллинг сами.
      pollLoop(dc.deviceCode);
    } catch (e) {
      setStep(null);
      toast("Не удалось начать подключение: " + e.message, "error");
    }
  };

  const pollLoop = async (deviceCode) => {
    const startedAt = Date.now();
    const timeout = 10 * 60 * 1000; // 10 минут на ввод кода
    while (Date.now() - startedAt < timeout) {
      try {
        const r = await api.yandex.PollConnect(deviceCode);
        if (r.status === "connected") {
          toast("Я.Диск подключён", "info");
          setStep(null);
          setCode(null);
          reload();
          return;
        }
        if (r.status === "expired") {
          toast("Код истёк — начни заново", "error");
          setStep(null);
          setCode(null);
          return;
        }
        if (r.status === "denied") {
          toast("Доступ отклонён", "error");
          setStep(null);
          setCode(null);
          return;
        }
        // status === "pending" — ждём
      } catch (e) {
        toast("Ошибка при опросе: " + e.message, "error");
        setStep(null);
        return;
      }
      await new Promise((r) => setTimeout(r, 5000));
    }
    toast("Время на подключение истекло", "error");
    setStep(null);
  };

  return (
    <div className="page" style={{ maxWidth: 880 }}>
      <div className="page-header">
        <div>
          <h1 className="page-title">Настройки</h1>
          <div className="page-sub">Подключения и параметры конвейера</div>
        </div>
      </div>

      <div className="set-section">
        <h3>Яндекс Диск</h3>
        <div className="h-sub">
          Один токен, подключается через device-код один раз. Дальше живёт в БД, обновляется автоматически.
        </div>

        <div className="set-row">
          <div>
            <div className="k">Статус</div>
            <div className="k-sub">Реальный ответ <span className="mono">yandex.Status</span></div>
          </div>
          <div className="v">
            {status.loading && <Spinner />}
            {status.error && <span className="pill warn"><Icon name="alert" size={11}/> {status.error.message}</span>}
            {status.data && status.data.connected
              ? <span className="pill ok"><span className="pdot"/>Подключено{status.data.account ? ` · ${status.data.account}` : ""}</span>
              : status.data && <span className="pill warn"><span className="pdot"/>Не подключено</span>}
          </div>
          <div>
            <button className="btn btn-sm" onClick={reload} disabled={status.loading}>
              <Icon name="refresh" size={11}/> Обновить
            </button>
          </div>
        </div>

        <hr className="divider" style={{ margin: "18px 0" }} />

        {!step && (!status.data || !status.data.connected) && (
          <button className="btn btn-primary" onClick={start}>
            <Icon name="link" size={13} stroke={2} /> Подключить Я.Диск (device-код)
          </button>
        )}

        {step === "starting" && (
          <div className="device-code">
            <Spinner /> запрашиваем код у Яндекса…
          </div>
        )}

        {step === "polling" && code && (
          <div>
            <div className="device-code">
              <div>
                <div className="muted" style={{ fontSize: 12 }}>Введи этот код на</div>
                <a className="mono" style={{ fontSize: 14 }} href={code.verificationUrl} target="_blank" rel="noreferrer">
                  {code.verificationUrl} ↗
                </a>
              </div>
              <div className="code">{code.userCode}</div>
              <div className="spacer" />
              <span className="pill"><Spinner /> ждём подтверждения…</span>
              <button className="btn btn-sm" onClick={() => { setStep(null); setCode(null); }}>Отмена</button>
            </div>
            <div className="muted" style={{ fontSize: 12, marginTop: 8 }}>
              Открой ссылку, введи код, разреши приложение pikasrv — страница сама обновится.
            </div>
          </div>
        )}

        {status.data && status.data.connected && !step && (
          <div className="muted" style={{ fontSize: 13 }}>
            Токен живёт в БД. Чтобы переподключить — сначала удали запись через VT-админку.
          </div>
        )}
      </div>

      <OpenClawSection/>
      <PromptsSection/>
      <LogsSection/>

      {/* Параметры конвейера */}
      <div className="set-section">
        <h3>Параметры конвейера</h3>
        <div className="h-sub">Технические дефолты — в обычной работе не трогаются.</div>

        <div className="set-row">
          <div>
            <div className="k">Ресайз для распознавания</div>
            <div className="k-sub">по большей стороне</div>
          </div>
          <div className="v mono">512 px · q80 JPEG</div>
          <div/>
        </div>

        <div className="set-row">
          <div>
            <div className="k">Группировка серий</div>
            <div className="k-sub">основной ключ — префикс ГГММДД в имени</div>
          </div>
          <div className="v mono">по дате в имени</div>
          <div/>
        </div>

        <div className="set-row">
          <div>
            <div className="k">Порог разрыва времени</div>
            <div className="k-sub">fallback, если префикса нет</div>
          </div>
          <div className="v mono">1 ч 30 мин</div>
          <div><button className="btn btn-sm" disabled>Изменить</button></div>
        </div>

        <div className="set-row">
          <div>
            <div className="k">Соотношение баннера</div>
            <div className="k-sub">шире — баннер 21:9 / выше — 9:16</div>
          </div>
          <div className="v mono">16:9</div>
          <div/>
        </div>

        <div className="set-row">
          <div>
            <div className="k">Чистка 512-кэша</div>
            <div className="k-sub">после ухода серии в архив</div>
          </div>
          <div className="v mono">через 30 дней</div>
          <div/>
        </div>
      </div>
    </div>
  );
}

// OpenClawSection — статус подключения к OpenClaw + self-enroll по кнопке.
// Поток: POST /v1/openclaw/connect → "paired" / "pending" / "error". При
// pending показываем deviceId, который Лена передаёт админу gpu1 для
// `openclaw devices approve <requestId>`; после approve следующий клик уже
// вернёт "paired".
function OpenClawSection() {
  const [status, reload] = useAsync(() => api.openclaw.status(), []);
  const [busy, setBusy] = React.useState(false);
  const [lastResp, setLastResp] = React.useState(null);
  const [_, toast] = useToasts();

  const connect = async () => {
    setBusy(true);
    try {
      const r = await api.openclaw.connect();
      setLastResp(r);
      if (r.status === "paired") {
        toast("OpenClaw подключён", "info");
        reload();
      } else if (r.status === "pending") {
        toast("Pairing запрошен — попроси админа approve", "info");
        reload();
      } else {
        toast("OpenClaw: " + (r.error || r.status), "error");
      }
    } catch (e) {
      toast("Запрос не прошёл: " + e.message, "error");
    } finally {
      setBusy(false);
    }
  };

  const data = status.data || {};
  const paired = data.paired;
  const configured = data.configured;
  const deviceId = data.deviceId || "";

  return (
    <div className="set-section">
      <h3>OpenClaw — провайдер моделей</h3>
      <div className="h-sub">
        Внешние сервисы (Vision + Text-модель) ходят через OpenClaw. Подключение —
        self-enroll: сервис формирует Ed25519-identity и отправляет device-запрос;
        админ gateway подтверждает pairing.
      </div>

      <div className="set-row">
        <div>
          <div className="k">Gateway URL</div>
          <div className="k-sub">из переменной <span className="mono">OPENCLAW_URL</span></div>
        </div>
        <div className="v mono">{data.url || "не задано"}</div>
        <div>
          {!configured
            ? <span className="pill warn"><span className="pdot"/>не настроен</span>
            : paired
              ? <span className="pill ok"><span className="pdot"/>paired</span>
              : <span className="pill warn"><span className="pdot"/>требует pairing</span>}
        </div>
      </div>

      <div className="set-row">
        <div>
          <div className="k">Агент</div>
          <div className="k-sub">по умолчанию <span className="mono">pika</span> (glm-5.1)</div>
        </div>
        <div className="v mono">{data.agent || "pika"}</div>
        <div/>
      </div>

      {configured && deviceId && (
        <div className="set-row">
          <div>
            <div className="k">Device ID</div>
            <div className="k-sub">hex(sha256(public-key)) — стабильный, переживает рестарт</div>
          </div>
          <div className="v mono" style={{ fontSize: 11, wordBreak: "break-all" }}>{deviceId}</div>
          <div>
            <button className="btn btn-sm" onClick={() => navigator.clipboard?.writeText(deviceId)}
                    title="Скопировать deviceId">
              <Icon name="link" size={11} stroke={2}/>
            </button>
          </div>
        </div>
      )}

      <hr className="divider" style={{ margin: "18px 0" }}/>

      {!configured && (
        <div className="muted" style={{ fontSize: 13 }}>
          Чтобы появилось подключение, задай в окружении{" "}
          <span className="mono">OPENCLAW_URL=ws://192.168.31.101:18789</span>{" "}
          и перезапусти pikasrv.
        </div>
      )}

      {configured && !paired && (
        <div>
          <div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 12 }}>
            <button className="btn btn-primary" onClick={connect} disabled={busy}>
              {busy ? <><Spinner/> запрашиваем…</> : <><Icon name="link" size={13} stroke={2}/> Попробовать подключиться</>}
            </button>
            <span className="muted" style={{ fontSize: 12 }}>— проверяет, есть ли уже работающий токен</span>
          </div>

          <TokenForm onSaved={() => { setLastResp(null); reload(); }} />


          {lastResp?.status === "pending" && (
            <div style={{
              marginTop: 14, padding: "12px 14px",
              background: "var(--accent-soft)", border: "1px solid var(--accent)",
              borderRadius: "var(--r-lg)",
              fontSize: 13, color: "var(--accent-2)", lineHeight: 1.55,
            }}>
              <b>Запрос отправлен.</b> Передай админу gpu1 device-id выше — он
              выполнит approve, и при следующем клике статус станет paired.
              Команда админа:
              <pre className="mono" style={{ marginTop: 6, padding: "8px 10px", background: "var(--bg)", borderRadius: 4, fontSize: 11.5, overflowX: "auto" }}>
{`ssh gpu1 'docker exec bot node openclaw.mjs devices list'
ssh gpu1 'docker exec bot node openclaw.mjs devices approve <requestId>'`}
              </pre>
              После approve нажми кнопку ещё раз — увидишь paired.
            </div>
          )}
          {lastResp?.status === "error" && (
            <div style={{
              marginTop: 14, padding: "10px 12px",
              background: "var(--warn-soft)", border: "1px solid var(--warn)",
              borderRadius: "var(--r-lg)", fontSize: 12, color: "var(--warn)",
            }}>
              <Icon name="alert" size={11} stroke={2} style={{ marginRight: 4 }}/>
              {lastResp.error}
            </div>
          )}
        </div>
      )}

      {paired && (
        <div className="muted" style={{ fontSize: 13 }}>
          Device-token живёт в <span className="mono">openclaw-token.json</span>{" "}
          рядом с бинарём. Если файл удалить — pairing нужно пройти заново.
          {" "}<button className="btn btn-sm" onClick={connect} disabled={busy}>
            {busy ? <><Spinner/> проверяем…</> : "Проверить подключение"}
          </button>
        </div>
      )}
    </div>
  );
}

// TokenForm — ввод одноразового bootstrap-токена от админа gpu1. Bootstrap
// подписывает identity-блок, gateway создаёт pending; после `devices approve`
// у админа следующий connect автоматически вернёт постоянный device-token.
function TokenForm({ onSaved }) {
  const [token, setToken] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [resp, setResp] = React.useState(null);
  const [_, toast] = useToasts();

  const submit = async () => {
    if (!token.trim()) return;
    setBusy(true);
    try {
      const r = await api.openclaw.bootstrap(token.trim());
      setResp(r);
      if (r.status === "paired") {
        toast("Подключение установлено — device-token получен", "info");
        setToken("");
        onSaved && onSaved();
      } else if (r.status === "pending") {
        toast("Устройство ушло в pending — попроси админа approve", "info");
        onSaved && onSaved();
      } else {
        toast("Ошибка: " + (r.error || r.status), "error");
      }
    } catch (e) {
      toast("Запрос не прошёл: " + e.message, "error");
    } finally {
      setBusy(false);
    }
  };

  return (
    <div style={{
      marginTop: 8, padding: "14px 14px 12px",
      background: "var(--surface-2)", border: "1px solid var(--line)",
      borderRadius: "var(--r-lg)",
    }}>
      <div style={{ fontSize: 13, color: "var(--ink-2)", marginBottom: 8, lineHeight: 1.5 }}>
        <b>Bootstrap-token от админа.</b> Одноразовый ключ для первичного enroll —
        admin gpu1 выдаёт его по нашему device-id. После успешного pairing
        gateway вернёт постоянный device-token, он ляжет в{" "}
        <span className="mono">openclaw-token.json</span>, и bootstrap больше не нужен.
      </div>
      <div style={{ display: "flex", gap: 8 }}>
        <input className="input" type="password"
               placeholder="bootstrap-token (одноразовый)"
               value={token}
               onChange={(e) => setToken(e.target.value)}
               onKeyDown={(e) => e.key === "Enter" && submit()}
               style={{ flex: 1, padding: "6px 10px", fontSize: 13, fontFamily: "var(--font-mono)" }}/>
        <button className="btn btn-primary" onClick={submit} disabled={busy || !token.trim()}>
          {busy ? <><Spinner/> отправляю…</> : "Запросить pairing"}
        </button>
      </div>

      {resp?.status === "pending" && (
        <div style={{
          marginTop: 10, padding: "10px 12px",
          background: "var(--accent-soft)", border: "1px solid var(--accent)",
          borderRadius: "var(--r)", fontSize: 12.5, color: "var(--accent-2)", lineHeight: 1.5,
        }}>
          Запрос принят, device в pending у gateway. Попроси админа выполнить
          {" "}<span className="mono">openclaw devices approve {"<requestId>"}</span>.
          После approve нажми «Попробовать подключиться» сверху —
          gateway отдаст device-token, и статус станет paired.
        </div>
      )}
      {resp?.status === "error" && (
        <div style={{
          marginTop: 10, padding: "10px 12px",
          background: "var(--warn-soft)", border: "1px solid var(--warn)",
          borderRadius: "var(--r)", fontSize: 12, color: "var(--warn)",
        }}>
          <Icon name="alert" size={11} stroke={2} style={{ marginRight: 4 }}/>
          {resp.error}
        </div>
      )}
    </div>
  );
}

window.OpenClawSection = OpenClawSection;
window.TokenForm = TokenForm;

// PromptsSection — список 5 шаблонов с метаданными + раскрываемый полный текст.
// Тексты подсасываются прямо из живого кода pikasrv (`/v1/prompts`), чтобы
// Лена видела что именно сервис сейчас шлёт моделям — без рассинхрона с
// архитектурой или этим UI.
function PromptsSection() {
  const [list, reload] = useAsync(() => api.prompts(), []);
  const [open, setOpen] = React.useState({}); // {id: bool}
  const items = list.data || [];

  const toggle = (id) => setOpen((s) => ({ ...s, [id]: !s[id] }));

  return (
    <div className="set-section">
      <h3>Промпты к моделям</h3>
      <div className="h-sub">
        Шаблоны запросов, которые pikasrv шлёт в OpenClaw. Vision — только английский
        (требование llama3.2-vision). Текстовые — на русском, GLM-5.1 их прекрасно понимает.
      </div>

      {list.loading && <div className="muted" style={{ padding: 12 }}><Spinner/> загружаем…</div>}
      {list.error && (
        <div style={{ padding: 12, color: "var(--warn)", fontSize: 12 }}>
          Не удалось получить промпты: {list.error.message}
        </div>
      )}

      {items.map((p) => (
        <PromptCard key={p.id} p={p}
                    isOpen={!!open[p.id]}
                    toggle={() => toggle(p.id)}
                    onSaved={reload}/>
      ))}
    </div>
  );
}

// PromptCard — раскрываемая карточка промпта с режимом редактирования.
// View-mode: показывает текст. Edit-mode: textarea + Сохранить / Отменить /
// Сбросить к дефолту. После сохранения дёргаем onSaved чтобы родитель
// перезагрузил список (для актуального флага overridden).
function PromptCard({ p, isOpen, toggle, onSaved }) {
  const [edit, setEdit] = React.useState(false);
  const [text, setText] = React.useState(p.text || "");
  const [busy, setBusy] = React.useState(false);
  const [preview, setPreview] = React.useState(null);   // {open, text}
  const [history, setHistory] = React.useState(null);   // {open, list}
  const [_, toast] = useToasts();
  React.useEffect(() => { setText(p.text || ""); setEdit(false); }, [p.text]);

  // Подгрузка серий + фоток для дропдаунов в Preview.
  const [feed] = useAsync(async () => {
    const shoots = await api.feed.shoots();
    return shoots || [];
  }, []);
  const [shootID, setShootID] = React.useState(0);
  const [photoID, setPhotoID] = React.useState(0);
  const [photos] = useAsync(async () => {
    if (!shootID) return [];
    return api.feed.photos(shootID);
  }, [shootID]);
  React.useEffect(() => { if (photoID === 0 && (photos.data || []).length) setPhotoID(photos.data[0].ID); }, [photos.data]);

  // miniplan тоже требует фотку — он работает по конкретному gist'у.
  // recognition и plan — по всей серии или вообще без подстановок.
  const needsPhoto = p.id === "analyse" || p.id === "generate" || p.id === "miniplan";

  const doPreview = async () => {
    try {
      const r = await api.previewPrompt(p.id, shootID, needsPhoto ? photoID : 0);
      setPreview({ open: true, text: r.text || "" });
    } catch (e) { toast("Не удалось: " + e.message, "error"); }
  };
  const doHistory = async () => {
    try {
      const list = await api.promptHistory(p.id, 20);
      setHistory({ open: true, list: list || [] });
    } catch (e) { toast("Не удалось: " + e.message, "error"); }
  };

  const save = async () => {
    setBusy(true);
    try {
      await api.setPrompt(p.id, text);
      toast("Промпт сохранён, действует с следующего хода модели", "info");
      setEdit(false);
      onSaved && onSaved();
    } catch (e) {
      toast("Не удалось сохранить: " + e.message, "error");
    } finally { setBusy(false); }
  };
  const reset = async () => {
    if (!confirm("Сбросить «" + p.title + "» к зашитому тексту?\nТекущие правки будут потеряны.")) return;
    setBusy(true);
    try {
      await api.resetPrompt(p.id);
      toast("Сброшено к дефолту", "info");
      setEdit(false);
      onSaved && onSaved();
    } catch (e) {
      toast("Не удалось: " + e.message, "error");
    } finally { setBusy(false); }
  };
  const cancel = () => { setText(p.text || ""); setEdit(false); };

  return (
    <div style={{
      marginTop: 12, border: "1px solid var(--line)",
      borderRadius: "var(--r-lg)", background: "var(--surface)",
    }}>
      <button onClick={toggle}
              style={{
                width: "100%", textAlign: "left", padding: "12px 14px",
                background: "transparent", border: 0, cursor: "pointer",
                fontFamily: "inherit",
              }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
          <Icon name={isOpen ? "chevronDown" : "chevron"} size={12}/>
          <div style={{ fontWeight: 500, fontSize: 13.5 }}>{p.title}</div>
          <span className="kind-pill">{p.agent}</span>
          <span className="kind-pill" style={{ background: "var(--surface-2)" }}>{p.language}</span>
          {p.overridden && (
            <span className="kind-pill" style={{
              background: "var(--accent-soft)", color: "var(--accent-2)",
              border: "1px solid var(--accent)",
            }}>изменён</span>
          )}
          <div style={{ flex: 1 }}/>
          <span className="muted" style={{ fontSize: 11.5 }}>{p.step}</span>
        </div>
        <div className="muted" style={{ fontSize: 12, marginTop: 6, lineHeight: 1.5 }}>
          {p.description}
        </div>
        {/* Плейсхолдеры — фигурные переменные которые пайплайн подставляет
            перед отправкой модели. Лена просила видеть их и расшифровку:
            при редактировании промпта эти конструкции трогать нельзя. */}
        {p.placeholders && p.placeholders.length > 0 && (
          <div style={{ marginTop: 8, fontSize: 11, lineHeight: 1.55 }}>
            <div className="muted" style={{ marginBottom: 3, fontSize: 10.5 }}>
              переменные в промпте (не редактируй фигурные скобки):
            </div>
            {p.placeholders.map((ph) => (
              <div key={ph.name} style={{ display: "flex", gap: 8, marginTop: 1 }}>
                <span className="mono" style={{
                  color: "var(--accent-2)", fontSize: 10.5,
                  flex: "0 0 145px", textAlign: "left",
                }}>
                  {ph.name}
                </span>
                <span className="muted" style={{ fontSize: 11 }}>{ph.desc}</span>
              </div>
            ))}
          </div>
        )}
      </button>

      {isOpen && (
        <div style={{ borderTop: "1px solid var(--line)", padding: "10px 14px", background: "var(--bg)" }}>
          {/* Toolbar */}
          <div style={{ display: "flex", gap: 6, marginBottom: 8 }}>
            {!edit ? (
              <button className="btn btn-sm" onClick={(e) => { e.stopPropagation(); setEdit(true); }}>
                <Icon name="edit" size={11}/> Изменить
              </button>
            ) : (
              <>
                <button className="btn btn-sm btn-accent" disabled={busy}
                        onClick={(e) => { e.stopPropagation(); save(); }}>
                  {busy ? <Spinner/> : "Сохранить"}
                </button>
                <button className="btn btn-sm" disabled={busy}
                        onClick={(e) => { e.stopPropagation(); cancel(); }}>
                  Отмена
                </button>
              </>
            )}
            {p.overridden && !edit && (
              <button className="btn btn-sm" disabled={busy}
                      onClick={(e) => { e.stopPropagation(); reset(); }}
                      style={{ color: "var(--warn)", borderColor: "var(--warn)" }}>
                <Icon name="refresh" size={11}/> Сбросить к дефолту
              </button>
            )}
            {!edit && (
              <button className="btn btn-sm" onClick={(e) => { e.stopPropagation(); doHistory(); }}
                      title="История правок">
                <Icon name="archive" size={11}/> История
              </button>
            )}
            <div style={{ flex: 1 }}/>
            <span className="muted" style={{ fontSize: 11, alignSelf: "center" }}>
              {text.length} символов
            </span>
          </div>

          {/* Предпросмотр итогового промпта */}
          {!edit && (
            <div style={{
              marginBottom: 10, padding: "8px 10px",
              background: "var(--surface-2)", borderRadius: "var(--r)",
              fontSize: 11.5, display: "flex", gap: 6, alignItems: "center",
              flexWrap: "wrap",
            }}>
              <span className="muted">Предпросмотр для:</span>
              <select value={shootID} onChange={(e) => { setShootID(+e.target.value); setPhotoID(0); }}
                      className="input" style={{ padding: "3px 8px", fontSize: 11 }}>
                <option value={0}>—</option>
                {(feed.data || []).map((s) => (
                  <option key={s.ID} value={s.ID}>{s.Title || `Серия ${s.ID}`}</option>
                ))}
              </select>
              {needsPhoto && shootID > 0 && (
                <select value={photoID} onChange={(e) => setPhotoID(+e.target.value)}
                        className="input" style={{ padding: "3px 8px", fontSize: 11 }}>
                  {(photos.data || []).map((p) => (
                    <option key={p.ID} value={p.ID}>{p.Filename}</option>
                  ))}
                </select>
              )}
              <button className="btn btn-sm"
                      disabled={!shootID || (needsPhoto && !photoID)}
                      onClick={(e) => { e.stopPropagation(); doPreview(); }}>
                <Icon name="play" size={11}/> Показать как уйдёт
              </button>
            </div>
          )}

          {edit ? (
            <textarea value={text} onChange={(e) => setText(e.target.value)}
                      style={{
                        width: "100%", minHeight: 280, maxHeight: 500,
                        padding: 10, fontFamily: "var(--font-mono)",
                        fontSize: 11.5, lineHeight: 1.5, color: "var(--ink-2)",
                        background: "var(--surface)", border: "1px solid var(--line-2)",
                        borderRadius: "var(--r)", resize: "vertical",
                      }}/>
          ) : (
            <div style={{
              fontFamily: "var(--font-mono)", fontSize: 11.5, lineHeight: 1.5,
              color: "var(--ink-2)", whiteSpace: "pre-wrap",
              maxHeight: 400, overflow: "auto",
            }}>{text}</div>
          )}

          {/* Модалка Preview — итоговый текст с подставленными плейсхолдерами */}
          {preview?.open && (
            <PromptModal title={"Предпросмотр промпта «" + p.title + "»"}
                         onClose={() => setPreview(null)}>
              <div className="muted" style={{ fontSize: 11.5, marginBottom: 8 }}>
                Это финальный текст, который уйдёт в модель — со всеми подстановками
                (recognition, акцент, комментарии, content-type). Сам промпт-шаблон
                остался прежним; здесь видно, как он раскрывается на конкретной фотке.
              </div>
              <pre style={{
                whiteSpace: "pre-wrap", wordBreak: "break-word",
                fontFamily: "var(--font-mono)", fontSize: 11.5, lineHeight: 1.5,
                background: "var(--bg)", border: "1px solid var(--line)",
                borderRadius: "var(--r)", padding: 10, maxHeight: "65vh",
                overflow: "auto", color: "var(--ink-2)", margin: 0,
              }}>{preview.text}</pre>
            </PromptModal>
          )}

          {/* Модалка History — последние 20 правок именно этого промпта */}
          {history?.open && (
            <PromptModal title={"История правок «" + p.title + "»"}
                         onClose={() => setHistory(null)}>
              {history.list.length === 0 ? (
                <div className="muted" style={{ padding: 12, fontSize: 12.5 }}>
                  Истории нет — этот промпт ни разу не редактировали через UI.
                </div>
              ) : (
                <div style={{ display: "flex", flexDirection: "column", gap: 10, maxHeight: "65vh", overflow: "auto" }}>
                  {history.list.map((h, i) => (
                    <HistoryEntry key={i} e={h}/>
                  ))}
                </div>
              )}
            </PromptModal>
          )}
        </div>
      )}
    </div>
  );
}

// PromptModal — простая overlay-модалка для Preview и History.
// Закрытие по Esc / клику на тёмный фон / крестик.
function PromptModal({ title, onClose, children }) {
  React.useEffect(() => {
    const h = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", h);
    return () => window.removeEventListener("keydown", h);
  }, [onClose]);
  return (
    <div onClick={onClose}
         style={{
           position: "fixed", inset: 0, zIndex: 1000,
           background: "rgba(0,0,0,0.5)",
           display: "flex", alignItems: "center", justifyContent: "center",
           padding: 20,
         }}>
      <div onClick={(e) => e.stopPropagation()}
           style={{
             background: "var(--surface)", border: "1px solid var(--line)",
             borderRadius: "var(--r-lg)", padding: 16,
             width: "min(900px, 100%)", maxHeight: "90vh", overflow: "auto",
             boxShadow: "0 10px 40px rgba(0,0,0,0.3)",
           }}>
        <div style={{ display: "flex", alignItems: "center", marginBottom: 10 }}>
          <div style={{ fontWeight: 500, fontSize: 14 }}>{title}</div>
          <div style={{ flex: 1 }}/>
          <button className="btn btn-sm" onClick={onClose}>
            <Icon name="close" size={11}/> Закрыть
          </button>
        </div>
        {children}
      </div>
    </div>
  );
}

// HistoryEntry — одна запись из prompts.history.jsonl с разворачивающимся
// diff'ом (просто двумя блоками для краткости — Лена не разработчик).
function HistoryEntry({ e }) {
  const [open, setOpen] = React.useState(false);
  const dt = new Date(e.time);
  const time = dt.toLocaleString("ru-RU", { hour12: false });
  const oldLen = (e.oldText || "").length;
  const newLen = (e.newText || "").length;
  const delta = newLen - oldLen;
  return (
    <div style={{ border: "1px solid var(--line)", borderRadius: "var(--r)", background: "var(--surface-2)" }}>
      <button onClick={() => setOpen((s) => !s)}
              style={{
                width: "100%", textAlign: "left", padding: "8px 10px",
                background: "transparent", border: 0, cursor: "pointer",
                fontFamily: "inherit", display: "flex", gap: 8, alignItems: "center",
              }}>
        <Icon name={open ? "chevronDown" : "chevron"} size={11}/>
        <span style={{ fontSize: 12.5 }}>{time}</span>
        <span className="muted" style={{ fontSize: 11.5 }}>
          было {oldLen} → стало {newLen} симв.
          {delta !== 0 && (
            <span style={{ color: delta > 0 ? "var(--accent-2)" : "var(--warn)", marginLeft: 4 }}>
              ({delta > 0 ? "+" : ""}{delta})
            </span>
          )}
        </span>
      </button>
      {open && (
        <div style={{ borderTop: "1px solid var(--line)", padding: 10, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <div>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>Было:</div>
            <pre style={{
              whiteSpace: "pre-wrap", wordBreak: "break-word",
              fontFamily: "var(--font-mono)", fontSize: 11, lineHeight: 1.5,
              background: "var(--bg)", border: "1px solid var(--line)",
              borderRadius: "var(--r)", padding: 8, margin: 0,
              color: "var(--ink-3)", maxHeight: 320, overflow: "auto",
            }}>{e.oldText || "(пусто)"}</pre>
          </div>
          <div>
            <div className="muted" style={{ fontSize: 11, marginBottom: 4 }}>Стало:</div>
            <pre style={{
              whiteSpace: "pre-wrap", wordBreak: "break-word",
              fontFamily: "var(--font-mono)", fontSize: 11, lineHeight: 1.5,
              background: "var(--bg)", border: "1px solid var(--line)",
              borderRadius: "var(--r)", padding: 8, margin: 0,
              color: "var(--ink-2)", maxHeight: 320, overflow: "auto",
            }}>{e.newText || "(пусто)"}</pre>
          </div>
        </div>
      )}
    </div>
  );
}

window.PromptCard = PromptCard;
window.PromptsSection = PromptsSection;

// LogsSection — лента последних записей slog. Auto-refresh каждые 3 секунды,
// фильтр по уровню. Удобно смотреть пока идёт распознавание/анализ.
function LogsSection() {
  const [entries, setEntries] = React.useState([]);
  const [level, setLevel] = React.useState("INFO");
  const [paused, setPaused] = React.useState(false);

  const load = React.useCallback(() => {
    api.logs(level, 100).then((r) => setEntries(Array.isArray(r) ? r : [])).catch(() => {});
  }, [level]);

  React.useEffect(() => {
    load();
    if (paused) return;
    const t = setInterval(load, 3000);
    return () => clearInterval(t);
  }, [load, paused]);

  const colorFor = (lvl) => ({
    DEBUG: "var(--ink-4)",
    INFO:  "var(--ink-2)",
    WARN:  "var(--accent-2)",
    ERROR: "var(--warn)",
  }[lvl] || "var(--ink-2)");

  return (
    <div className="set-section">
      <h3>Логи сервиса</h3>
      <div className="h-sub">
        Последние записи pikasrv. Auto-refresh каждые 3 сек. Видно ошибки распознавания,
        обращения к OpenClaw, Yandex и т.п.
      </div>

      <div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 10 }}>
        <label className="muted" style={{ fontSize: 12 }}>Уровень:</label>
        <select className="input" value={level} onChange={(e) => setLevel(e.target.value)}
                style={{ padding: "4px 22px 4px 8px", fontSize: 12.5 }}>
          <option value="">все</option>
          <option value="INFO">INFO+</option>
          <option value="WARN">WARN+</option>
          <option value="ERROR">только ERROR</option>
        </select>
        <button className="btn btn-sm" onClick={() => setPaused((p) => !p)}>
          {paused ? "▶ возобновить" : "⏸ пауза"}
        </button>
        <button className="btn btn-sm" onClick={load}>обновить</button>
        <div style={{ flex: 1 }}/>
        <span className="muted" style={{ fontSize: 12 }}>{entries.length} записей</span>
      </div>

      <div style={{
        height: 320, overflow: "auto",
        background: "var(--bg)", border: "1px solid var(--line)",
        borderRadius: "var(--r)",
        padding: 8, fontFamily: "var(--font-mono)", fontSize: 11.5, lineHeight: 1.5,
      }}>
        {entries.length === 0 && (
          <div className="muted-2" style={{ padding: 12, fontFamily: "inherit", textAlign: "center" }}>
            нет записей такого уровня
          </div>
        )}
        {entries.map((e, i) => (
          <div key={i} style={{ marginBottom: 4, color: colorFor(e.level), display: "flex", gap: 8 }}>
            <span style={{ color: "var(--ink-4)", flex: "0 0 130px" }}>
              {new Date(e.time).toLocaleTimeString("ru-RU", { hour12: false }) + "." +
               String(new Date(e.time).getMilliseconds()).padStart(3, "0")}
            </span>
            <span style={{ flex: "0 0 55px", fontWeight: 600 }}>{e.level}</span>
            <span style={{ flex: 1, wordBreak: "break-word" }}>
              {e.msg}
              {e.attrs && Object.keys(e.attrs).length > 0 && (
                <span style={{ color: "var(--ink-3)", marginLeft: 8 }}>
                  {Object.entries(e.attrs).map(([k, v]) => `${k}=${v}`).join(" ")}
                </span>
              )}
              {e.source && (
                <span style={{ color: "var(--ink-4)", marginLeft: 8, fontSize: 10.5 }}>
                  @{e.source}
                </span>
              )}
            </span>
          </div>
        ))}
      </div>
    </div>
  );
}

window.LogsSection = LogsSection;
window.SettingsScreen = SettingsScreen;
