Add AI-check details modal on worker page and raise backup upload limit
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=сложный-пароль-админки
|
||||
DB_PASSWORD=случайная-длинная-строка
|
||||
# Лимит размера загружаемого на восстановление бэкапа, МБ (по умолчанию 500)
|
||||
BACKUP_UPLOAD_LIMIT_MB=500
|
||||
|
||||
# Hugging Face token (нужен для закрытых/приватных репозиториев моделей)
|
||||
HUGGINGFACE_TOKEN=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user