Только фронтенд: server.js, worker.js, photo-ai/, compose и схема БД не тронуты.
Контракт /enhance-ai, /api/photo-ai/health и service в /api/photo-jobs/status
взят из Stage 4 как есть (I6 — новых колонок нет, метаданные из photo_jobs.params
и audit_log.target).
journal.html + journal.js:
- селект режима рядом с «🤖 ИИ»: Универсально (x2) / Быстро (x4) / Лица /
Лица + фон; PHOTO_AI_MODES — единственная таблица режим→{model, face}
- значение уходит в POST .../enhance-ai телом {model, face, face_model};
face_model подставляется только когда face !== 'off'
- дефолт селекта из photo_ai_face_mode (face=all → facesbg, face=face → faces)
- подсказка про медленный CPU: loadPhotoAiDevice() читает
GET /api/photo-ai/health (только для админа)
- текст подтверждения и надпись «ИИ обрабатывает…» различают апскейл и face-режим
- кнопка и селект скрываются вместе при photo_ai_enabled === 'false' (I3)
settings.html + settings.js:
- новый раздел «Фото-ИИ»: режим по умолчанию, модель лиц, желаемое устройство
- read-only статус из /api/photo-jobs/status: фактическое устройство,
device_name, VRAM, модели/модели лиц/загруженные; жёлтым — расхождение с
желаемым устройством, красным — недоступность и незаданный PHOTO_AI_URL
- renderStackInfo() рисует карточку «Фото-ИИ» из stack.photo_ai
- новые id в DIRTY_FIELDS и в payload PUT /api/settings
- photo_ai_device_pref документирующий: фактическое устройство задаёт
PHOTO_AI_DEVICE в контейнере
worker.html + worker.js:
- PHOTO_ACTION_LABELS: ai_face «ИИ + лица», ai_upscale «ИИ-апскейл»
- в модалке сравнения — чипы с моделью, режимом лиц, найденными лицами,
устройством, временем обработки и предупреждениями
- строка состояния учитывает service.reachable === false («задания ждут»)
и дописывает «· расчёт на <device>»
audit.js: 10 меток для кодов, которые раньше показывались сырыми (профиль и
фото ученика, главное фото, фото модуля, удаление/очистка уведомлений, логотип).
Проверено: node --check для всех четырёх js. Приёмка на живом стеке в этом
изменении не гонялась — чекбокс приёмки Stage 5 в TODO_PHOTO_FACE_AI.md
оставлен пустым, там же журнал раздела 5 с замечаниями для Stage 6.
399 lines
26 KiB
HTML
399 lines
26 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Настройки — Админ-панель</title>
|
||
<link rel="stylesheet" href="admin.css">
|
||
</head>
|
||
<body data-page="settings">
|
||
<div class="layout">
|
||
<div class="sidebar" id="sidebar"></div>
|
||
<div class="main">
|
||
<div class="page-head">
|
||
<h2>Настройки</h2>
|
||
<p class="page-sub">Параметры формы, публичных страниц и данные системы</p>
|
||
<span class="page-status" id="authStatus"><span class="spinner dark"></span> Загрузка…</span>
|
||
<button class="head-btn" id="reloadBtn" type="button" title="Перезагрузить настройки"><i data-lucide="rotate-ccw"></i> Сбросить</button>
|
||
</div>
|
||
|
||
<div class="settings-layout">
|
||
<aside class="settings-nav" id="settingsNav">
|
||
<a href="#sec-system" data-nav="sec-system" class="active"><i data-lucide="bar-chart-3"></i> Система</a>
|
||
<a href="#sec-stack" data-nav="sec-stack"><i data-lucide="server"></i> Стек</a>
|
||
<a href="#sec-brand" data-nav="sec-brand"><i data-lucide="palette"></i> Брендинг</a>
|
||
<a href="#sec-antispam" data-nav="sec-antispam"><i data-lucide="shield"></i> Антиспам</a>
|
||
<a href="#sec-share" data-nav="sec-share"><i data-lucide="link"></i> Публичные ссылки</a>
|
||
<a href="#sec-form" data-nav="sec-form"><i data-lucide="file-text"></i> Форма ответа</a>
|
||
<a href="#sec-photo" data-nav="sec-photo"><i data-lucide="camera"></i> Фото</a>
|
||
<a href="#sec-photo-ai" data-nav="sec-photo-ai"><i data-lucide="sparkles"></i> Фото-ИИ</a>
|
||
<a href="#sec-notify" data-nav="sec-notify"><i data-lucide="bell"></i> Уведомления</a>
|
||
<a href="#sec-ai" data-nav="sec-ai"><i data-lucide="bot"></i> ИИ</a>
|
||
<a href="#sec-backup" data-nav="sec-backup"><i data-lucide="database"></i> Бэкапы</a>
|
||
<a href="#sec-trash" data-nav="sec-trash"><i data-lucide="trash-2"></i> Корзина</a>
|
||
<a href="#sec-bans" data-nav="sec-bans"><i data-lucide="shield-off"></i> Блокировки</a>
|
||
</aside>
|
||
|
||
<div class="settings-stack settings-stack-grid">
|
||
<div class="settings-card wide" id="sec-system">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="bar-chart-3"></i></div>
|
||
<h3>Системная информация</h3>
|
||
<button class="icon-btn" id="systemRefreshBtn" type="button" title="Обновить" aria-label="Обновить"><i data-lucide="refresh-cw"></i></button>
|
||
<span class="head-live" id="systemLive" title="Данные кэшируются на сервере"></span>
|
||
</div>
|
||
<div id="systemInfo" class="sys-grid">
|
||
<div class="sys-card loading">Загрузка…</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-card wide" id="sec-stack">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="server"></i></div>
|
||
<h3>Статус стека</h3>
|
||
<button class="icon-btn" id="stackRefreshBtn" type="button" title="Обновить" aria-label="Обновить стек"><i data-lucide="refresh-cw"></i></button>
|
||
<span class="head-live" id="stackLive" title="Версии и состояние сервисов"></span>
|
||
</div>
|
||
<p class="hint">На чём работает приложение прямо сейчас: версии компонентов, состояние сервисов и нагрузка. Обновление перезагружает данные и этого блока, и системной информации выше.</p>
|
||
<div id="stackInfo" class="sys-grid">
|
||
<div class="sys-card loading">Загрузка…</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-card" id="sec-brand">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="palette"></i></div>
|
||
<h3>Брендинг</h3>
|
||
</div>
|
||
<p class="hint">Название и логотип отображаются в админ-панели, на форме ответа ученика, странице входа и в отчёте.</p>
|
||
<div class="settings-field">
|
||
<label for="systemName">Название системы</label>
|
||
<input type="text" id="systemName" class="settings-input" maxlength="60" placeholder="WhatIDo">
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Логотип</label>
|
||
<div class="logo-picker">
|
||
<div class="logo-preview" id="logoPreview">
|
||
<span id="logoPreviewFallback">—</span>
|
||
<img id="logoPreviewImg" alt="Логотип">
|
||
</div>
|
||
<div class="logo-picker-actions">
|
||
<div class="logo-picker-btns">
|
||
<button class="btn-primary ghost" id="logoUploadBtn" type="button"><i data-lucide="upload"></i> Загрузить логотип</button>
|
||
<button class="btn-primary danger-ghost" id="logoRemoveBtn" type="button" hidden><i data-lucide="trash-2"></i> Удалить</button>
|
||
</div>
|
||
<input type="file" id="logoInput" accept="image/*" style="display:none">
|
||
<div class="logo-upload-status" id="logoUploadStatus"></div>
|
||
<span class="hint">Рекомендуется широкое изображение с прозрачным фоном (PNG/WebP), не более 1 МБ</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-card" id="sec-antispam">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="shield"></i></div>
|
||
<h3>Защита от повторов</h3>
|
||
</div>
|
||
<p class="hint">Если ученик уже отправил ответ, следующая отправка от него будет отклонена в течение заданного времени. Минимальное значение — 1 минута.</p>
|
||
<div class="settings-field">
|
||
<label for="spamInterval">Интервал ожидания</label>
|
||
<div class="unit-row">
|
||
<input type="number" id="spamInterval" class="settings-input" min="1" max="1440" step="1">
|
||
<span class="unit">минут</span>
|
||
</div>
|
||
<span class="field-error" id="spamIntervalError"></span>
|
||
<div class="preset-btns" id="spamPresets">
|
||
<button class="preset-btn" type="button" data-min="1">1 мин</button>
|
||
<button class="preset-btn" type="button" data-min="15">15 мин</button>
|
||
<button class="preset-btn" type="button" data-min="30">30 мин</button>
|
||
<button class="preset-btn" type="button" data-min="60">1 час</button>
|
||
<button class="preset-btn" type="button" data-min="1440">Сутки</button>
|
||
</div>
|
||
<span class="hint" id="spamIntervalHint"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-card" id="sec-share">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="link"></i></div>
|
||
<h3>Страница публичной ссылки /s/<hash></h3>
|
||
</div>
|
||
<p class="hint">Управление видимостью элементов на публичных страницах записей.</p>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="shareShowMessage">
|
||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||
<span class="toggle-text"><b>Сообщение ученика</b><small>Текст «Что делала(а)» на странице по ссылке</small></span>
|
||
</label>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="shareShowDate">
|
||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||
<span class="toggle-text"><b>Дата записи</b><small>Дата в карточке записи</small></span>
|
||
</label>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="shareShowNames">
|
||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||
<span class="toggle-text"><b>Фамилии и имена</b><small>Имена учеников в галерее работ</small></span>
|
||
</label>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="shareShowPhotos">
|
||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||
<span class="toggle-text"><b>Фото группы</b><small>Групповое фото в шапке страницы</small></span>
|
||
</label>
|
||
<div class="settings-field">
|
||
<label for="cookieNoticeText">Уведомление о cookie</label>
|
||
<textarea id="cookieNoticeText" class="settings-input" rows="3" style="min-height:80px"></textarea>
|
||
<span class="hint">Пусто — баннер не показывается</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-card" id="sec-ai">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="bot"></i></div>
|
||
<h3>Промпт ИИ для исправления текста</h3>
|
||
</div>
|
||
<p class="hint">Инструкция для нейросети при исправлении ошибок в записи «Что делала(а)».</p>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="aiAutoCheck">
|
||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||
<span class="toggle-text"><b>Автопроверка новых записей</b><small>Без участия человека — воркер сам обрабатывает очередь</small></span>
|
||
</label>
|
||
<div class="settings-field">
|
||
<label for="aiPrompt">Промпт</label>
|
||
<textarea id="aiPrompt" class="settings-input mono" rows="5" spellcheck="false"></textarea>
|
||
</div>
|
||
<div class="ai-queue" id="aiQueueInfo"></div>
|
||
</div>
|
||
|
||
<div class="settings-card">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="boxes"></i></div>
|
||
<h3>Профили моделей ИИ</h3>
|
||
</div>
|
||
<p class="hint">Модель, выбранная по умолчанию, обрабатывает записи учеников. «Нативная» — llama.cpp в Docker. Можно добавить любого провайдера с OpenAI-совместимым API (OpenRouter, OpenAI, Ollama и др.).</p>
|
||
<div class="ai-queue" id="aiProfileActive"></div>
|
||
<div class="card-foot" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||
<label style="display:flex;align-items:center;gap:8px;font-size:.88rem;min-width:0;max-width:100%">
|
||
Модель по умолчанию:
|
||
<select id="aiActiveProfile" class="settings-input" style="width:auto;min-width:0;max-width:100%;flex:1 1 200px">
|
||
<option value="native">Нативная (llama.cpp)</option>
|
||
</select>
|
||
</label>
|
||
<button class="btn-primary ghost" id="aiProfileAddBtn" type="button"><i data-lucide="plus"></i> Добавить профиль</button>
|
||
</div>
|
||
<div id="aiProfilesList" class="ai-profiles-list"></div>
|
||
</div>
|
||
|
||
<div class="settings-card" id="sec-backup">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="database"></i></div>
|
||
<h3>Резервное копирование</h3>
|
||
<button class="icon-btn" id="statsRefreshBtn" type="button" title="Обновить статистику" aria-label="Обновить статистику"><i data-lucide="refresh-cw"></i></button>
|
||
</div>
|
||
<p class="hint">Скачайте архив со всеми данными (ученики, группы, журнал, настройки) и фотографиями. Архив формируется на сервере, скачивание выполняет браузер — при обрыве связи загрузку можно продолжить. Восстановление полностью заменит текущие данные.</p>
|
||
<div class="data-stats" id="dataStats"><span class="chip">Загрузка статистики…</span></div>
|
||
<div class="card-foot">
|
||
<button class="btn-primary ghost" id="backupBtn" type="button"><i data-lucide="download"></i> Скачать бэкап</button>
|
||
<button class="btn-primary ghost danger" id="restoreBtn" type="button"><i data-lucide="upload"></i> Восстановить из файла</button>
|
||
<input type="file" id="restoreFile" accept=".tar.gz,.gz,application/gzip" style="display:none">
|
||
</div>
|
||
<div class="restore-status" id="restoreStatus"></div>
|
||
<div class="restore-status" id="backupStatus"></div>
|
||
</div>
|
||
|
||
<div class="settings-card" id="sec-form">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="file-text"></i></div>
|
||
<h3>Футер на форме</h3>
|
||
</div>
|
||
<p class="hint">Тексты показываются внизу формы ответа ученика.</p>
|
||
<div class="settings-row">
|
||
<div class="settings-field">
|
||
<label for="footerLeft">Текст слева</label>
|
||
<input type="text" id="footerLeft" class="settings-input" placeholder="Например: Поле, руководитель">
|
||
</div>
|
||
<div class="settings-field">
|
||
<label for="footerRight">Текст справа</label>
|
||
<input type="text" id="footerRight" class="settings-input" placeholder="Например: Год, курс">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-card" id="sec-photo">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="camera"></i></div>
|
||
<h3>Фотография с веб-камеры</h3>
|
||
</div>
|
||
<p class="hint">Разрешение и качество снимка на форме ответа ученика. Камера может выдать разрешение, ближайшее к выбранному.</p>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="camEnabled">
|
||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||
<span class="toggle-text"><b>Кнопка «Камера»</b><small>Показывать кнопку «Камера» на форме ответа. Если выключено, фото делается кликом по превью «Нажми чтобы сделать фото»</small></span>
|
||
</label>
|
||
<div class="settings-row">
|
||
<div class="settings-field">
|
||
<label for="photoResolution">Разрешение</label>
|
||
<select id="photoResolution" class="settings-input">
|
||
<option value="640x480">640 × 480 (по умолчанию)</option>
|
||
<option value="1280x720">1280 × 720 (HD)</option>
|
||
<option value="1920x1080">1920 × 1080 (Full HD)</option>
|
||
<option value="2560x1440">2560 × 1440 (QHD)</option>
|
||
</select>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label for="photoQuality">Качество JPEG</label>
|
||
<div class="unit-row">
|
||
<input type="number" id="photoQuality" class="settings-input" min="0.5" max="1" step="0.01" value="0.92">
|
||
<span class="unit">0.5–1.0</span>
|
||
</div>
|
||
<span class="hint">Больше значение — лучше качество и крупнее файл</span>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label for="photoEngine">Обработка улучшения фото</label>
|
||
<select id="photoEngine" class="settings-input">
|
||
<option value="auto">Авто (сервер, иначе клиент)</option>
|
||
<option value="server">Сервер (sharp)</option>
|
||
<option value="client">Клиент (браузер)</option>
|
||
</select>
|
||
<span class="hint">Серверный режим обрабатывает оригинал без пережатия и быстрее на больших фото</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-card" id="sec-photo-ai">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="sparkles"></i></div>
|
||
<h3>Фото-ИИ</h3>
|
||
<button class="icon-btn" id="photoAiRefreshBtn" type="button" title="Обновить статус фото-ИИ" aria-label="Обновить статус фото-ИИ"><i data-lucide="refresh-cw"></i></button>
|
||
</div>
|
||
<p class="hint">Нейросетевая обработка фотографий: Real-ESRGAN увеличивает разрешение, GFPGAN восстанавливает лица. Лицевой режим включается только явно — модель дорисовывает лицо по приору и может изменить черты. Без `PHOTO_AI_URL` кнопка «🤖 ИИ» в журнале скрыта.</p>
|
||
<div class="settings-row">
|
||
<div class="settings-field">
|
||
<label for="photoAiFaceMode">Режим по умолчанию</label>
|
||
<select id="photoAiFaceMode" class="settings-input">
|
||
<option value="off">Без лиц — только увеличение</option>
|
||
<option value="face">Лица — восстановить лица, фон увеличить</option>
|
||
<option value="all">Лица + фон — восстановить лица и снять шум фона</option>
|
||
</select>
|
||
<span class="hint">Предлагается в окне улучшения фото, режим можно переключить там же</span>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label for="photoAiFaceModel">Модель лиц</label>
|
||
<select id="photoAiFaceModel" class="settings-input">
|
||
<option value="gfpgan">GFPGAN v1.4</option>
|
||
<option value="codeformer">CodeFormer</option>
|
||
</select>
|
||
<span class="hint">CodeFormer работает, только если модуль установлен в образ photo-ai</span>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label for="photoAiDevicePref">Желаемое устройство</label>
|
||
<select id="photoAiDevicePref" class="settings-input">
|
||
<option value="auto">Авто (GPU, если доступен)</option>
|
||
<option value="cuda">GPU (CUDA)</option>
|
||
<option value="cpu">CPU</option>
|
||
</select>
|
||
<span class="hint">Документирующее значение: фактическое устройство задаёт переменная PHOTO_AI_DEVICE в контейнере</span>
|
||
</div>
|
||
</div>
|
||
<div class="data-stats" id="photoAiStatus"><span class="hint">Загрузка статуса фото-ИИ…</span></div>
|
||
</div>
|
||
|
||
<div class="settings-card" id="sec-trash">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="trash-2"></i></div>
|
||
<h3>Корзина</h3>
|
||
</div>
|
||
<p class="hint">При очистке корзины или удалении пункта данные не удаляются сразу, а помечаются на удаление и исчезают из корзины. Через указанное количество дней они удаляются автоматически и безвозвратно вместе с файлами. До этого помеченные данные можно вернуть в корзину и восстановить (страница «Корзина»).</p>
|
||
<div class="settings-field">
|
||
<label for="trashPurgeDays">Время до безвозвратного удаления</label>
|
||
<div class="unit-row">
|
||
<input type="number" id="trashPurgeDays" class="settings-input" min="1" max="365" step="1">
|
||
<span class="unit">дней</span>
|
||
</div>
|
||
<span class="hint">От 1 до 365 дней</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-card wide" id="sec-notify">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="bell"></i></div>
|
||
<h3>Уведомления</h3>
|
||
<button class="icon-btn" id="notifyTestBtn" type="button" title="Отправить тестовое уведомление" aria-label="Тестовое уведомление"><i data-lucide="send"></i></button>
|
||
</div>
|
||
<p class="hint">Уведомления собираются в «колокольчике» админ-панели и на странице «Уведомления». Здесь выбирается, о каких событиях сообщать: новые записи журнала, обработка фото нейросетью, блокировки IP, бэкапы и другое.</p>
|
||
<label class="toggle">
|
||
<input type="checkbox" id="notifyEnabled">
|
||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||
<span class="toggle-text"><b>Уведомления включены</b><small>Общий выключатель: при снятии галочки не создаётся ни одно уведомление</small></span>
|
||
</label>
|
||
<div class="settings-field">
|
||
<label for="notifyRetentionDays">Хранение уведомлений</label>
|
||
<div class="unit-row">
|
||
<input type="number" id="notifyRetentionDays" class="settings-input" min="1" max="365" step="1">
|
||
<span class="unit">дней</span>
|
||
</div>
|
||
<span class="hint">От 1 до 365 дней, старые уведомления удаляются автоматически</span>
|
||
</div>
|
||
<div class="notify-types" id="notifyTypes"><span class="hint">Загрузка…</span></div>
|
||
</div>
|
||
|
||
<div class="settings-card" id="sec-bans">
|
||
<div class="card-head">
|
||
<div class="ic"><i data-lucide="shield-off"></i></div>
|
||
<h3>Блокировки IP</h3>
|
||
</div>
|
||
<p class="hint">IP автоматически блокируется на 24 часа: за заполнение скрытого антиспам-поля, за 10 неудачных попыток входа или 10 неверных паролей на защищённой ссылке.</p>
|
||
<div class="data-stats" id="bansList"><span class="chip">Загрузка…</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="save-bar" id="saveBar">
|
||
<span class="hint" id="saveHint"><i data-lucide="triangle-alert"></i> Есть несохранённые изменения</span>
|
||
<span class="save-bar-actions">
|
||
<button class="btn-primary ghost" id="resetBtn" type="button"><i data-lucide="rotate-ccw"></i> Отменить</button>
|
||
<button class="btn-primary" id="saveBtn" type="button"><i data-lucide="check"></i> Сохранить</button>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="aiProfileModal">
|
||
<div class="edit-modal" style="max-width:560px">
|
||
<h3 id="aiProfileModalTitle">Добавить профиль ИИ</h3>
|
||
<div class="settings-field">
|
||
<label for="aiProfileName">Название профиля <span style="color:#ef4444">*</span></label>
|
||
<input type="text" id="aiProfileName" class="settings-input" placeholder="Например: OpenRouter GPT-4o-mini">
|
||
</div>
|
||
<div class="settings-field">
|
||
<label for="aiProfileModel">Модель <span style="color:#ef4444">*</span></label>
|
||
<input type="text" id="aiProfileModel" class="settings-input" placeholder="openai/gpt-4o-mini">
|
||
</div>
|
||
<div class="settings-field">
|
||
<label for="aiProfileBaseUrl">Base URL (OpenAI-совместимый API) <span style="color:#ef4444">*</span></label>
|
||
<input type="text" id="aiProfileBaseUrl" class="settings-input" placeholder="https://openrouter.ai/api/v1">
|
||
</div>
|
||
<div class="settings-field">
|
||
<label for="aiProfileApiKey">API-ключ (если требуется)</label>
|
||
<input type="password" id="aiProfileApiKey" class="settings-input" placeholder="sk-...">
|
||
</div>
|
||
<div class="settings-field">
|
||
<label for="aiProfileMaxTokens">Max tokens (лимит ответа модели)</label>
|
||
<input type="number" id="aiProfileMaxTokens" class="settings-input" min="16" max="32768" placeholder="Авто (1024–4096)">
|
||
<span class="hint">Оставьте пустым для авто. Для reasoning-моделей (gpt-oss, deepseek-r1) нужно 1024+ — иначе рассуждения не уместятся и ответ придёт пустым.</span>
|
||
</div>
|
||
<div class="ai-test-status" id="aiProfileTestStatus"></div>
|
||
<div class="actions" style="flex-wrap:wrap">
|
||
<button class="btn-primary" id="aiProfileSaveBtn" type="button">Сохранить</button>
|
||
<button class="btn-primary ghost" id="aiProfileTestBtn" type="button"><i data-lucide="plug-zap"></i> Проверить</button>
|
||
<button class="btn-primary ghost" id="aiProfileCancelBtn" type="button">Отмена</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
<script src="vendor/lucide.min.js"></script>
|
||
<script src="admin.js"></script>
|
||
<script src="js/settings.js"></script>
|
||
</body>
|
||
</html> |