Add AI-check details modal on worker page and raise backup upload limit

This commit is contained in:
dev
2026-09-11 16:26:30 +03:00
parent ee19da7fa0
commit 4db02d75bc
6 changed files with 51 additions and 3 deletions
+2
View File
@@ -1,6 +1,8 @@
ADMIN_USERNAME=admin
ADMIN_PASSWORD=сложный-пароль-админки
DB_PASSWORD=случайная-длинная-строка
# Лимит размера загружаемого на восстановление бэкапа, МБ (по умолчанию 500)
BACKUP_UPLOAD_LIMIT_MB=500
# Hugging Face token (нужен для закрытых/приватных репозиториев моделей)
HUGGINGFACE_TOKEN=
+1
View File
@@ -29,6 +29,7 @@ services:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/whereldo
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
BACKUP_UPLOAD_LIMIT_MB: ${BACKUP_UPLOAD_LIMIT_MB:-500}
AI_MODEL: ${AI_MODEL:-qwen2.5-1.5b-instruct-q4_k_m.gguf}
AI_PROMPT: ${AI_PROMPT:-}
NODE_ENV: production
+6
View File
@@ -373,6 +373,12 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
.ai-badge-skipped,.ai-badge-reverted{background:rgba(148,163,184,.18);color:var(--muted)}
.ai-badge-ic.ai-badge-processing .lucide-loader{animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.ai-details{background:none;border:none;padding:0;margin:0;color:var(--accent);font-size:.8rem;cursor:pointer;white-space:nowrap}
.ai-details:hover{text-decoration:underline}
.ai-detail-block{margin-top:4px}
.ai-detail-block + .ai-detail-block{margin-top:14px}
.ai-detail-label{font-size:.75rem;color:var(--muted);margin-bottom:6px;font-weight:600}
.ai-detail-block pre{margin:0;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.78rem;line-height:1.45;color:var(--text);max-height:34vh;overflow:auto}
.ai-status{margin-top:6px;font-size:.78rem;color:var(--muted)}
.ai-queue{margin-top:8px;font-size:.78rem;color:var(--muted)}
+21 -1
View File
@@ -17,6 +17,7 @@ const STATUS_BADGE = {
let state = null;
let refreshTimer = null;
let busy = false;
let recentRows = [];
function fmtDur(ms) {
if (ms === null || ms === undefined || ms < 0) return '—';
@@ -141,13 +142,15 @@ function renderRecent() {
const empty = document.getElementById('recentEmpty');
if (!rows.length) { tbody.innerHTML = ''; empty.style.display = 'block'; return; }
empty.style.display = 'none';
tbody.innerHTML = rows.map(r => `
recentRows = rows;
tbody.innerHTML = rows.map((r, i) => `
<tr>
<td style="white-space:nowrap">${fmtTime(r.ai_checked_at)}</td>
<td>${esc(r.student_name)}</td>
<td style="color:var(--muted)">${esc(r.group_name || '')}</td>
<td>${statusBadge(r.ai_status)}</td>
<td>${r.changed ? '✏️ исправлен' : '—'}</td>
<td><button type="button" class="ai-details" data-ai-idx="${i}">Подробнее</button></td>
</tr>`).join('');
}
@@ -225,6 +228,23 @@ document.getElementById('requeueBtn').addEventListener('click', requeueFailed);
document.getElementById('recentFilter').addEventListener('change', renderRecent);
document.getElementById('autoRefresh').addEventListener('change', e => setAutoRefresh(e.target.checked));
document.getElementById('recentTable').addEventListener('click', e => {
const btn = e.target.closest('.ai-details');
if (!btn) return;
const r = recentRows[parseInt(btn.dataset.aiIdx, 10)];
if (!r) return;
document.getElementById('aiDetailTitle').textContent = `ИИ-проверка #${r.id} — ${r.student_name}`;
document.getElementById('aiDetailInput').textContent = r.description_original || '—';
document.getElementById('aiDetailOutput').textContent = r.description_ai || '—';
document.getElementById('aiDetailModal').classList.add('open');
});
document.getElementById('aiDetailCloseBtn').addEventListener('click', () => {
document.getElementById('aiDetailModal').classList.remove('open');
});
document.getElementById('aiDetailModal').addEventListener('click', e => {
if (e.target === e.currentTarget) e.currentTarget.classList.remove('open');
});
(async () => {
if (await requireAdminPage()) {
buildSidebar(document.body.dataset.page);
+17 -1
View File
@@ -69,7 +69,7 @@
<div class="audit-wrap">
<table class="audit-table" id="recentTable">
<thead>
<tr><th>Проверено</th><th>Ученик</th><th>Группа</th><th>Статус</th><th>Изменения</th></tr>
<tr><th>Проверено</th><th>Ученик</th><th>Группа</th><th>Статус</th><th>Изменения</th><th>Детали</th></tr>
</thead>
<tbody></tbody>
</table>
@@ -95,6 +95,22 @@
</div>
</div>
</div>
<div class="modal-overlay" id="aiDetailModal">
<div class="edit-modal" style="max-width:680px">
<h3 id="aiDetailTitle">Детали ИИ-проверки</h3>
<div class="ai-detail-block">
<div class="ai-detail-label">Отправлено в модель</div>
<pre id="aiDetailInput"></pre>
</div>
<div class="ai-detail-block">
<div class="ai-detail-label">Ответ модели</div>
<pre id="aiDetailOutput"></pre>
</div>
<div class="card-foot" style="justify-content:flex-end;gap:8px">
<button type="button" class="cancel" id="aiDetailCloseBtn">Закрыть</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script src="vendor/lucide.min.js"></script>
<script src="admin.js"></script>
+4 -1
View File
@@ -749,6 +749,8 @@ async function aiCorrectText(text) {
}
}
const BACKUP_UPLOAD_LIMIT_MB = parseInt(process.env.BACKUP_UPLOAD_LIMIT_MB || '500', 10);
const uploadBackup = multer({
storage: multer.diskStorage({
destination: (_, __, cb) => {
@@ -760,7 +762,7 @@ const uploadBackup = multer({
cb(null, `backup-${Date.now()}${ext}`);
},
}),
limits: { fileSize: 50 * 1024 * 1024 },
limits: { fileSize: BACKUP_UPLOAD_LIMIT_MB * 1024 * 1024 },
});
const SAFE_NAME = /^[\w,.()-]+$/;
@@ -2633,6 +2635,7 @@ app.get('/api/ai/status', requireAdmin, async (_, res) => {
),
pool.query(
`SELECT e.id, e.student_name, e.ai_status, e.ai_checked_at, e.ai_error, g.name AS group_name,
e.description_original, e.description_ai,
(e.description_ai IS NOT NULL AND e.description_original IS NOT NULL AND e.description_ai <> e.description_original) AS changed
FROM entries e JOIN groups g ON g.id = e.group_id
WHERE e.ai_checked_at IS NOT NULL AND e.deleted_at IS NULL