/
dmukhin
/
SpherePlatform
Обзор
Документация
Войти
/
dmukhin
/
SpherePlatform
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
style_reference.html
721 строка
23 KB
OIS\sabogdan
first commit
24 мар 2026, 07:50
24 мар 2026, 07:50
b0e18c7
Код
Авторство
О чём код?
<!doctype html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>OIS AI Проверка ТЗ</title> <style> :root { --bg: #0b1020; --card: #121a2e; --card-2: #0f172a; --text: #e5e7eb; --muted: #9ca3af; --accent: #3b82f6; --accent-2: #2563eb; --border: #2a3655; --error: #f87171; --ok: #34d399; } body { margin: 0; font-family: Inter, Segoe UI, Arial, sans-serif; background: radial-gradient(circle at top, #111a33 0%, var(--bg) 65%); color: var(--text); min-height: 100vh; } .container { width: 70%; max-width: 1400px; margin: 32px auto; padding: 0 16px; } h1 { margin-bottom: 8px; } .hint { color: var(--muted); margin-bottom: 16px; } .dropzone { border: 2px dashed var(--border); border-radius: 14px; background: linear-gradient(180deg, var(--card) 0%, var(--card-2) 100%); padding: 36px 16px; text-align: center; transition: 0.2s ease; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2); } .dropzone.dragover { border-color: var(--accent); transform: translateY(-1px); box-shadow: 0 12px 30px rgba(37, 99, 235, 0.2); } input[type="file"] { display: none; } .actions { margin-top: 12px; display: flex; justify-content: center; } .btn { border: 1px solid var(--accent); background: var(--accent); color: #fff; padding: 10px 16px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.2s, border-color 0.2s; } .btn:hover { background: var(--accent-2); border-color: var(--accent-2); } .btn.stop { background: transparent; border-color: #f87171; color: #fca5a5; margin-left: 8px; } .btn.stop:hover { background: rgba(248, 113, 113, 0.12); border-color: #f87171; } .report-section { display: none; margin-top: 16px; border: 1px solid var(--border); border-radius: 8px; background: #0a0f1f; overflow: hidden; } .report-section.visible { display: block; } .report-header { padding: 10px 12px; border-bottom: 1px solid var(--border); font-weight: 600; } .report-subheader { padding: 8px 12px; color: var(--muted); border-bottom: 1px solid var(--border); font-size: 13px; } .summary-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; padding: 12px; } .summary-grid.llm-summary { grid-template-columns: repeat(4, minmax(0, 1fr)); } .summary-card { background: #0f172a; border: 1px solid var(--border); border-radius: 8px; padding: 10px; text-align: center; } .summary-card .label { color: var(--muted); font-size: 12px; margin-bottom: 6px; } .summary-card .value { font-size: 22px; font-weight: 700; } .summary-card.pass .value { color: #34d399; } .summary-card.fail .value { color: #f87171; } .table-wrap { padding: 0 12px 12px; overflow-x: auto; } table { width: 100%; border-collapse: collapse; min-width: 640px; } th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top; font-size: 13px; } th { color: var(--muted); font-weight: 600; background: #0f172a; } .badge { display: inline-block; padding: 3px 8px; border-radius: 999px; font-size: 12px; font-weight: 700; line-height: 1.2; } .badge.pass { background: rgba(52, 211, 153, 0.15); color: #34d399; border: 1px solid rgba(52, 211, 153, 0.35); } .badge.fail { background: rgba(248, 113, 113, 0.15); color: #f87171; border: 1px solid rgba(248, 113, 113, 0.35); } .badge.in-progress { background: rgba(96, 165, 250, 0.15); color: #60a5fa; border: 1px solid rgba(96, 165, 250, 0.35); } .badge.waiting { background: rgba(148, 163, 184, 0.15); color: #cbd5e1; border: 1px solid rgba(148, 163, 184, 0.35); } .badge.stopped { background: rgba(251, 191, 36, 0.15); color: #fbbf24; border: 1px solid rgba(251, 191, 36, 0.35); } .llm-comment { max-width: 560px; white-space: normal; color: #c7d2fe; } .debug-toggle-wrap { margin-top: 10px; text-align: center; } .debug-toggle { background: transparent; border: 1px solid rgba(156, 163, 175, 0.25); color: rgba(156, 163, 175, 0.55); border-radius: 999px; padding: 2px 10px; font-size: 11px; cursor: pointer; transition: all 0.2s ease; } .debug-toggle:hover { color: rgba(156, 163, 175, 0.8); border-color: rgba(156, 163, 175, 0.4); background: rgba(156, 163, 175, 0.05); } .debug-panel { display: none; margin-top: 8px; border: 1px solid var(--border); border-radius: 8px; background: #0a0f1f; overflow: hidden; } .debug-panel.visible { display: block; } .debug-panel-header { padding: 8px 12px; border-bottom: 1px solid var(--border); color: var(--muted); font-size: 12px; } .debug-json { margin: 0; padding: 12px; white-space: pre-wrap; overflow-x: auto; font-family: Consolas, monospace; font-size: 12px; color: #9fb5ff; min-height: 48px; } .result-panel { margin-top: 16px; border: 1px solid var(--border); border-radius: 8px; background: #0a0f1f; overflow: hidden; } .result-toggle { width: 100%; border: 0; border-bottom: 1px solid var(--border); background: transparent; color: var(--text); text-align: left; padding: 10px 12px; cursor: pointer; font-weight: 600; } .result-toggle:hover { background: rgba(59, 130, 246, 0.08); } .result { margin-top: 0; border: 0; border-radius: 0 0 8px 8px; background: #0a0f1f; color: #dbe3f0; padding: 12px; min-height: 160px; white-space: pre-wrap; overflow-x: auto; font-family: Consolas, monospace; font-size: 13px; } .result.collapsed { display: none; } .error { color: var(--error); margin-top: 10px; min-height: 20px; } .drop-icon { font-size: 34px; line-height: 1; margin-bottom: 10px; } .status { margin-top: 10px; color: var(--muted); min-height: 20px; display: flex; align-items: center; justify-content: center; gap: 8px; } .spinner { width: 14px; height: 14px; border-radius: 50%; border: 2px solid #3b4a73; border-top-color: var(--ok); animation: spin 0.8s linear infinite; display: none; } .status.loading .spinner { display: inline-block; } @keyframes spin { to { transform: rotate(360deg); } } </style> </head> <body> <div class="container"> <h1>OIS AI Проверка ТЗ</h1> <div class="hint">Перетащите Техническое задание в формате DOCX. После конвертации проверка запускается автоматически.</div> <div class="dropzone" id="dropzone"> <div class="drop-icon">📄</div> <div id="dropText">Перетащите .docx сюда</div> <div class="actions"> <button id="pickBtn" class="btn" type="button">Выбрать DOCX</button> <button id="stopBtn" class="btn stop" type="button" style="display:none;">Остановить проверку</button> </div> <input id="fileInput" type="file" accept=".docx"> </div> <div id="status" class="status"> <span class="spinner" id="spinner"></span> <span id="statusText">Готово к конвертации и проверке</span> </div> <div id="error" class="error"></div> <div id="reportSection" class="report-section"> <div class="report-header">Шаг 1: Проверка структуры документа</div> <div class="report-subheader">Следующий этап: LLM-проверка содержания и качества формулировок.</div> <div class="summary-grid"> <div class="summary-card"> <div class="label">Всего</div> <div id="sumTotal" class="value">0</div> </div> <div class="summary-card pass"> <div class="label">PASS</div> <div id="sumPass" class="value">0</div> </div> <div class="summary-card fail"> <div class="label">FAIL</div> <div id="sumFail" class="value">0</div> </div> </div> <div class="table-wrap"> <table> <thead> <tr> <th>Check ID</th> <th>Раздел</th> <th>Статус</th> </tr> </thead> <tbody id="reportBody"></tbody> </table> </div> </div> <div class="debug-toggle-wrap"> <button id="debugToggleBtn" class="debug-toggle" type="button">debug</button> </div> <div id="debugPanel" class="debug-panel"> <div class="debug-panel-header">Сконвертированный JSON</div> <pre id="debugJson" class="debug-json"></pre> </div> <div id="llmReportSection" class="report-section"> <div class="report-header">Шаг 2: LLM-проверка соответствия описанию требований</div> <div class="report-subheader">Проверка содержания разделов по каждому требованию</div> <div class="summary-grid llm-summary"> <div class="summary-card"> <div class="label">Всего</div> <div id="llmSumTotal" class="value">0</div> </div> <div class="summary-card pass"> <div class="label">PASS</div> <div id="llmSumPass" class="value">0</div> </div> <div class="summary-card fail"> <div class="label">FAIL</div> <div id="llmSumFail" class="value">0</div> </div> <div class="summary-card"> <div class="label">Общее время проверки</div> <div id="llmSumDuration" class="value"></div> </div> </div> <div class="table-wrap"> <table> <thead> <tr> <th>Требование</th> <th>Раздел</th> <th>Статус</th> <th>Время</th> <th>Комментарий LLM</th> </tr> </thead> <tbody id="llmReportBody"></tbody> </table> </div> </div> <pre id="result" class="result" style="display:none;"></pre> </div> <script> const dropzone = document.getElementById("dropzone"); const fileInput = document.getElementById("fileInput"); const result = document.getElementById("result"); const error = document.getElementById("error"); const dropText = document.getElementById("dropText"); const pickBtn = document.getElementById("pickBtn"); const status = document.getElementById("status"); const statusText = document.getElementById("statusText"); const stopBtn = document.getElementById("stopBtn"); const reportSection = document.getElementById("reportSection"); const reportBody = document.getElementById("reportBody"); const sumTotal = document.getElementById("sumTotal"); const sumPass = document.getElementById("sumPass"); const sumFail = document.getElementById("sumFail"); const llmReportSection = document.getElementById("llmReportSection"); const llmReportBody = document.getElementById("llmReportBody"); const llmSumTotal = document.getElementById("llmSumTotal"); const llmSumPass = document.getElementById("llmSumPass"); const llmSumFail = document.getElementById("llmSumFail"); const llmSumDuration = document.getElementById("llmSumDuration"); const debugToggleBtn = document.getElementById("debugToggleBtn"); const debugPanel = document.getElementById("debugPanel"); const debugJson = document.getElementById("debugJson"); let activePollTimer = null; let activeJobId = null; pickBtn.addEventListener("click", () => fileInput.click()); debugToggleBtn.addEventListener("click", () => { debugPanel.classList.toggle("visible"); }); stopBtn.addEventListener("click", async () => { if (!activeJobId) return; stopBtn.disabled = true; try { await fetch(`/llm-stop/${activeJobId}`, { method: "POST" }); const progressRes = await fetch(`/llm-progress/${activeJobId}`); const progressData = await progressRes.json(); if (progressRes.ok) { const llmReport = progressData.llm_report || {}; renderLlmReport(llmReport); } statusText.textContent = "Шаг 2 остановлен пользователем"; } catch (e) { error.textContent = "Не удалось остановить проверку: " + e.message; } finally { stopLlmPolling(); status.classList.remove("loading"); activeJobId = null; stopBtn.style.display = "none"; stopBtn.disabled = false; setPickButtonEnabled(true); } }); function renderReport(report) { const summary = report.summary || { total: 0, passed: 0, failed: 0 }; sumTotal.textContent = String(summary.total || 0); sumPass.textContent = String(summary.passed || 0); sumFail.textContent = String(summary.failed || 0); const rows = (report.results || []).map((item) => { const statusClass = (item.status || "").toLowerCase() === "pass" ? "pass" : "fail"; const safeId = item.requirement_id || "-"; const safeTitle = item.section_title || "-"; const safeStatus = item.status || "FAIL"; return ` <tr> <td>${safeId}</td> <td>${safeTitle}</td> <td><span class="badge ${statusClass}">${safeStatus}</span></td> </tr> `; }).join(""); reportBody.innerHTML = rows || "<tr><td colspan='3'>Нет данных</td></tr>"; reportSection.classList.add("visible"); } function renderLlmReport(report) { const summary = report.summary || { total: 0, passed: 0, failed: 0 }; llmSumTotal.textContent = String(summary.total || 0); llmSumPass.textContent = String(summary.passed || 0); llmSumFail.textContent = String(summary.failed || 0); function formatElapsed(secondsRaw) { if (secondsRaw === null || secondsRaw === undefined || secondsRaw === "") return ""; const seconds = Number(secondsRaw); if (!Number.isFinite(seconds) || seconds < 0) return ""; const totalSeconds = Math.floor(seconds); const minutes = Math.floor(totalSeconds / 60); const secs = totalSeconds % 60; return `${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; } llmSumDuration.textContent = formatElapsed(summary.total_elapsed_seconds); const rows = (report.results || []).map((item) => { const normalizedStatus = (item.status || "").toLowerCase(); let statusClass = "waiting"; if (normalizedStatus === "in_progress") statusClass = "in-progress"; if (normalizedStatus === "in-progress") statusClass = "in-progress"; if (normalizedStatus === "eval") statusClass = "in-progress"; if (normalizedStatus === "wait") statusClass = "waiting"; if (normalizedStatus === "pass") statusClass = "pass"; if (normalizedStatus === "fail") statusClass = "fail"; if (normalizedStatus === "stop") statusClass = "stopped"; if (normalizedStatus === "stopped") statusClass = "stopped"; const safeId = item.requirement_id || "-"; const safeTitle = item.section_title || "-"; const safeStatus = normalizedStatus === "in_progress" ? "EVAL" : normalizedStatus === "stopped" ? "STOP" : normalizedStatus === "waiting" ? "WAIT" : (item.status || "WAIT"); const safeElapsed = (normalizedStatus === "waiting" || normalizedStatus === "wait") ? "" : formatElapsed(item.elapsed_seconds); const safeComment = item.comment || "-"; return ` <tr> <td>${safeId}</td> <td>${safeTitle}</td> <td><span class="badge ${statusClass}">${safeStatus}</span></td> <td>${safeElapsed}</td> <td class="llm-comment">${safeComment}</td> </tr> `; }).join(""); llmReportBody.innerHTML = rows || "<tr><td colspan='5'>Нет данных</td></tr>"; llmReportSection.classList.add("visible"); } function setPickButtonEnabled(enabled) { pickBtn.disabled = !enabled; pickBtn.style.opacity = enabled ? "1" : "0.6"; pickBtn.style.cursor = enabled ? "pointer" : "not-allowed"; } function stopLlmPolling() { if (activePollTimer) { clearTimeout(activePollTimer); activePollTimer = null; } } async function pollLlmProgress(jobId) { try { const response = await fetch(`/llm-progress/${jobId}`); const data = await response.json(); if (!response.ok) { error.textContent = data.detail || "Ошибка получения прогресса LLM"; statusText.textContent = "Ошибка шага 2"; setPickButtonEnabled(true); return; } const llmReport = data.llm_report || {}; renderLlmReport(llmReport); result.textContent = JSON.stringify( { step1_structure: JSON.parse(result.dataset.step1 || "{}"), step2_llm: llmReport }, null, 2 ); if (data.status === "completed") { statusText.textContent = "Готово: шаги 1 и 2 завершены"; status.classList.remove("loading"); stopBtn.style.display = "none"; activeJobId = null; setPickButtonEnabled(true); return; } if (data.status === "stopped") { statusText.textContent = "Шаг 2 остановлен пользователем"; status.classList.remove("loading"); stopBtn.style.display = "none"; activeJobId = null; setPickButtonEnabled(true); return; } statusText.textContent = "Шаг 2/2: LLM-проверка выполняется..."; activePollTimer = setTimeout(() => pollLlmProgress(jobId), 1500); } catch (e) { error.textContent = "Ошибка сети при получении прогресса LLM: " + e.message; statusText.textContent = "Ошибка шага 2"; status.classList.remove("loading"); setPickButtonEnabled(true); } } dropzone.addEventListener("dragover", (e) => { e.preventDefault(); dropzone.classList.add("dragover"); }); dropzone.addEventListener("dragleave", () => { dropzone.classList.remove("dragover"); }); dropzone.addEventListener("drop", (e) => { e.preventDefault(); dropzone.classList.remove("dragover"); const file = e.dataTransfer.files[0]; if (!file) return; sendFile(file); }); fileInput.addEventListener("change", () => { if (!fileInput.files.length) return; sendFile(fileInput.files[0]); }); async function sendFile(file) { error.textContent = ""; if (!file.name.toLowerCase().endsWith(".docx")) { error.textContent = "Выберите файл с расширением .docx"; status.classList.remove("loading"); statusText.textContent = "Ожидание корректного файла"; return; } dropText.textContent = "Конвертация и проверка..."; result.textContent = "Подождите, идет обработка документа..."; result.dataset.step1 = "{}"; debugJson.textContent = ""; debugPanel.classList.remove("visible"); reportSection.classList.remove("visible"); llmReportSection.classList.remove("visible"); status.classList.add("loading"); statusText.textContent = "Шаг 1/2: конвертация и структурная проверка..."; stopLlmPolling(); activeJobId = null; stopBtn.style.display = "none"; setPickButtonEnabled(false); const formData = new FormData(); formData.append("file", file); try { const controller = new AbortController(); const timeoutMs = 120000; const timeoutId = setTimeout(() => controller.abort(), timeoutMs); const response = await fetch("/convert-and-check", { method: "POST", body: formData, signal: controller.signal }); clearTimeout(timeoutId); const data = await response.json(); if (!response.ok) { error.textContent = data.detail || "Ошибка проверки"; result.textContent = ""; statusText.textContent = "Ошибка обработки"; } else { const report = data.report || {}; const llmReport = data.llm_report || {}; const llmJobId = data.llm_job_id; const converted = data.converted || {}; renderReport(report); result.dataset.step1 = JSON.stringify(report); debugJson.textContent = JSON.stringify(converted, null, 2); statusText.textContent = "Шаг 1 завершен. Шаг 2/2: запускаем LLM-проверку..."; renderLlmReport(llmReport); result.textContent = JSON.stringify({ step1_structure: report, step2_llm: llmReport }, null, 2); if (llmJobId) { activeJobId = llmJobId; stopBtn.style.display = "inline-block"; pollLlmProgress(llmJobId); } else { statusText.textContent = "Не удалось запустить шаг 2"; status.classList.remove("loading"); stopBtn.style.display = "none"; setPickButtonEnabled(true); } } } catch (e) { if (e.name === "AbortError") { error.textContent = "Превышено время ожидания ответа шага 1 (120 сек). Попробуйте еще раз."; statusText.textContent = "Таймаут проверки"; result.textContent = "Сервер не успел ответить за 120 секунд."; } else { error.textContent = "Ошибка сети: " + e.message; statusText.textContent = "Ошибка сети"; } status.classList.remove("loading"); stopBtn.style.display = "none"; activeJobId = null; setPickButtonEnabled(true); } finally { dropText.textContent = "Перетащите .docx сюда"; } } </script> </body> </html>