Files
contracts-flask/site/templates/index.html
T
RepinoidandCopilot 961d60a79e
Deploy contracts-flask / validate (push) Canceled after 0s
Match Contracts UI to Obfuscator and add Help
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-30 10:40:44 +03:00

275 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Сверка договоров — LLM</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
:root { --brand-primary: #2563eb; --brand-primary-dark: #1d4ed8; --brand-gray: #d1d5db; --brand-grey-light: #f3f4f6; --background: #ffffff; --foreground: #1a1a1a; --muted: #6b7280; --destructive: #ef4444; --green: #22c55e; --navy: #001c34; --topbar-muted: #a4acb3; --topbar-active: #8ec5ff; --topbar-done: #4ade80; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: var(--foreground); background: var(--brand-grey-light); min-height: 100vh; }
.topbar { background: var(--navy); color: #fff; padding: 0 24px; height: 56px; display: flex; align-items: center; gap: 12px; position: sticky; top: 0; z-index: 50; }
.topbar img { height: 20px; filter: brightness(0) invert(1); }
.topbar .title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; font-size: 15px; }
.topbar .ver { flex-shrink: 0; color: var(--topbar-muted); font-size: 12px; }
.pipeline-stepper { display: flex; align-items: center; gap: 8px; margin-left: auto; color: var(--topbar-muted); font-size: 11px; white-space: nowrap; }
.pipeline-stepper > span:nth-child(even) { color: rgba(255,255,255,.4); }
.topbar .help { flex-shrink: 0; cursor: pointer; user-select: none; padding: 6px 16px; border-radius: 8px; border: 1px solid rgba(255,255,255,.4); color: #fff; background: transparent; font: 500 13px system-ui, sans-serif; }
.topbar .help:hover { background: rgba(255,255,255,.1); }
.content { max-width: 960px; margin: 40px auto; padding: 0 24px; }
.page-title { color: var(--navy); font-size: 28px; font-weight: 500; line-height: 1.2; }
.page-sub { margin: 8px 0 24px; color: var(--muted); }
.card { background: var(--background); border-radius: 12px; border: 1px solid var(--brand-gray); box-shadow: 0 1px 2px rgba(0,0,0,.05); overflow: hidden; margin-bottom: 16px; }
.card-header { background: var(--brand-grey-light); padding: 12px 16px; font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px; }
.card-body { padding: 16px; }
.btn { height: 36px; border-radius: 6px; gap: 6px; padding: 0 14px; font-size: 14px; font-family: inherit; cursor: pointer; display: inline-flex; align-items: center; border: 1px solid var(--brand-gray); background: var(--background); }
.btn-primary { background: var(--brand-primary); border-color: var(--brand-primary); color: #fff; }
.btn-primary:hover { background: var(--brand-primary-dark); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.table-wrap { border-radius: 6px; border: 1px solid var(--brand-gray); overflow-y: auto; max-height: 50vh; margin-top: 12px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { background: var(--brand-grey-light); text-transform: uppercase; padding: 6px 10px; border-right: 1px solid var(--brand-gray); text-align: left; font-weight: 600; font-size: 11px; color: var(--muted); }
td { padding: 6px 10px; border-right: 1px solid var(--brand-gray); border-bottom: 1px solid var(--brand-gray); }
tr:hover td { background: rgba(243,244,246,.5); }
.name-cell { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.num-cell { text-align: right; white-space: nowrap; }
.status-ok { color: var(--green); }
.status-err { color: var(--destructive); }
.summary-row td { background: rgba(34,197,94,.05); font-weight: 600; }
.empty-row td { color: var(--muted); text-align: center; padding: 24px; }
.remove-btn { cursor: pointer; color: #f87171; background: none; border: none; padding: 2px 4px; font-size: 16px; line-height: 1; }
.remove-btn:hover { color: #ef4444; }
.info-btn { cursor: pointer; color: var(--muted); background: none; border: none; padding: 2px 4px; font-size: 14px; display: none; }
.info-btn:hover { color: var(--brand-primary); }
.info-btn.visible { display: inline; }
.first-row td { background: rgba(37,99,235,.06); font-weight: 600; }
.arrow-btn { cursor: pointer; color: var(--muted); background: none; border: none; padding: 2px; font-size: 12px; line-height: 1; }
.arrow-btn:hover { color: var(--brand-primary); }
.arrow-btn:disabled { opacity: .3; cursor: default; color: var(--muted); }
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,.4); z-index: 100; justify-content: center; align-items: center; }
.modal-overlay.open { display: flex; }
.modal { background: var(--background); border-radius: 12px; border: 1px solid var(--brand-gray); box-shadow: 0 4px 24px rgba(0,0,0,.15); max-width: 520px; width: 90%; max-height: 80vh; display: flex; flex-direction: column; }
.modal.wide { max-width: 900px; }
.modal-header { padding: 14px 16px; border-bottom: 1px solid var(--brand-gray); display: flex; align-items: center; gap: 8px; font-weight: 600; flex-shrink: 0; }
.modal-body { padding: 16px; overflow-y: auto; }
.modal-body .kv { display: flex; margin-bottom: 6px; font-size: 13px; }
.modal-body .kv .k { color: var(--muted); width: 110px; flex-shrink: 0; }
.modal-body .kv .v { word-break: break-all; }
.diff-added { background: rgba(34,197,94,.1); }
.diff-deleted { background: rgba(239,68,68,.1); }
.diff-changed { background: rgba(234,179,8,.1); }
.diff-added td { color: var(--green); }
.diff-deleted td { color: var(--destructive); text-decoration: line-through; }
.diff-field-old { color: var(--destructive); text-decoration: line-through; font-size: 11px; }
.diff-field-new { color: var(--green); font-weight: 600; }
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: spin .6s linear infinite; }
.text-panes { display: flex; gap: 0; }
.text-pane { flex: 1; min-width: 0; overflow-y: auto; max-height: 65vh; }
.text-pane:first-child { border-right: 1px solid var(--brand-gray); padding-right: 12px; }
.text-pane:last-child { padding-left: 12px; }
.text-pane pre { background: var(--brand-grey-light); padding: 10px; border-radius: 6px; font-size: 11px; white-space: pre-wrap; word-break: break-word; overflow-x: auto; margin: 0; max-height: 60vh; overflow-y: auto; }
.text-pane table { font-size: 11px; width: 100%; }
.text-pane th { font-size: 10px; padding: 4px 6px; }
.text-pane td { padding: 3px 6px; font-size: 11px; }
.text-pane .pane-title { font-weight: 600; font-size: 12px; margin-bottom: 8px; color: var(--muted); text-transform: uppercase; }
#filePicker .file-picker { color: var(--foreground); font-family: system-ui, sans-serif; }
#filePicker .fp-toolbar { margin: 0 0 8px; }
#filePicker .fp-toolbar button { min-height: 36px; border: 1px solid var(--brand-gray); border-radius: 6px; padding: 0 14px; color: var(--foreground); background: var(--background); font: inherit; }
#filePicker .fp-toolbar button:first-of-type { color: #fff; background: var(--brand-primary); border-color: var(--brand-primary); }
#filePicker .fp-toolbar button.secondary { color: var(--foreground); background: var(--background); }
#filePicker .fp-toolbar button.quiet { color: var(--muted); }
#filePicker .fp-table-wrap { max-height: 50vh; overflow: auto; border: 1px solid var(--brand-gray); border-radius: 6px; }
#filePicker .fp-table { font: 13px system-ui, sans-serif; }
#filePicker .fp-table th, #filePicker .fp-table td { padding: 6px 10px; border-color: var(--brand-gray); }
#filePicker .fp-table th { color: var(--muted); letter-spacing: 0; }
#filePicker .tree-name { min-width: 0; padding-top: 0; padding-bottom: 0; color: var(--foreground); font-family: inherit; }
#filePicker .tree-file .tree-name { overflow: hidden; text-overflow: ellipsis; }
#filePicker .fp-actions { white-space: nowrap; }
#filePicker .fp-action { margin-right: 4px; padding: 3px 6px; border: 0; border-radius: 4px; color: var(--brand-primary); background: transparent; cursor: pointer; font: 11px system-ui, sans-serif; }
#filePicker .fp-status-uploading { color: var(--brand-primary); }
#filePicker .fp-status-parsing { color: var(--brand-primary); }
#filePicker .fp-status-parsed { color: var(--green); }
#filePicker .fp-status-duplicate { color: var(--muted); }
#filePicker .fp-status-error { color: var(--destructive); }
#filePicker .fp-status, #filePicker .fp-count { color: var(--muted); font: 12px system-ui, sans-serif; }
@media (max-width: 900px) { .pipeline-stepper { display: none; } }
@media (max-width: 600px) {
.topbar { padding: 0 12px; gap: 8px; }
.topbar .title { font-size: 13px; }
.topbar .help { padding: 6px 10px; }
.content { margin: 24px auto; padding: 0 12px; }
.page-title { font-size: 24px; }
}
</style>
</head>
<body>
<div class="topbar">
<img src="/static/logo.svg" alt="Nubes">
<span class="title">Контрактор</span>
<span class="ver">v{{ config["VERSION"] }}</span>
<div id="pipelineStepper" class="pipeline-stepper">
<span id="stepUpload">○ Загрузка</span><span>→</span>
<span id="stepClassify">○ Классификация</span><span>→</span>
<span id="stepGroups">○ Группировка</span><span>→</span>
<span id="stepCompare">○ Сравнение</span>
</div>
<button class="help" type="button" onclick="openAbout()" aria-controls="aboutModalOverlay">HELP</button>
</div>
<div class="content">
<h1 class="page-title">Сверка договоров</h1>
<p class="page-sub">Загрузите договоры, дополнительные соглашения и спецификации — система классифицирует их, сгруппирует и покажет изменения.</p>
<div class="card">
<div class="card-header">
<i data-lucide="folder-open" style="width:18px;height:18px;"></i>
Загрузка договоров/приложений/спецификаций
</div>
<div class="card-body">
<div style="text-align:right;font-size:11px;color:var(--muted);margin-bottom:6px;">Порядок определяется автоматически при классификации</div>
<div style="font-size:11px;color:var(--muted);margin-bottom:6px;">PDF, DOC, DOCX и ZIP (архивы раскрываются рекурсивно). Лимиты: 50 МБ на файл, 500 МБ суммарно. При выборе папки можно фильтровать состав через корневой <code>obscure.lst</code>.</div>
<div id="filePicker"></div>
<button class="btn btn-primary" id="llmBtn" style="width:100%;justify-content:center;margin-top:8px;display:none;background:var(--green);border-color:var(--green);">
<i data-lucide="scale" style="width:16px;height:16px;"></i> Общее сравнение
</button>
<details style="margin-top:8px;font-size:12px;">
<summary style="cursor:pointer;color:var(--muted);">⚙ Промпты LLM</summary>
<div style="display:flex;gap:8px;margin-top:6px;">
<button class="btn" id="promptBtnExtract" style="font-size:11px;height:28px;flex:1;">📄 Извлечение</button>
<button class="btn" id="promptBtnDiff" style="font-size:11px;height:28px;flex:1;">🔄 Сравнение</button>
<button class="btn" id="promptBtnClassify" style="font-size:11px;height:28px;flex:1;">🏷️ Классификация</button>
</div>
<div style="display:flex;gap:8px;margin-top:3px;font-size:10px;color:var(--muted);">
<span style="flex:1;">для базового договора</span>
<span style="flex:1;">для допсоглашений</span>
<span style="flex:1;">автоопределение</span>
</div>
</details>
</div>
</div>
<div class="card" id="diffCard" style="display:none;">
<div class="card-header">
<i data-lucide="file-diff" style="width:18px;height:18px;"></i>
Классификация и группы <span id="diffStatus" style="font-weight:400;font-size:12px;margin-left:8px;"></span>
</div>
<div class="card-body" id="diffBody"></div>
</div>
<div class="card" id="compareCard" style="display:none;margin-top:16px;">
<div class="card-header">
<i data-lucide="scale" style="width:18px;height:18px;"></i>
Результаты сравнения <span id="compareStatus" style="font-weight:400;font-size:12px;margin-left:8px;"></span>
</div>
<div class="card-body" id="compareBody"></div>
</div>
<div class="card" id="chatCard" style="display:none;margin-top:16px;">
<div class="card-header">
<i data-lucide="message-circle" style="width:18px;height:18px;"></i>
Задать вопрос по договору <span style="font-weight:400;font-size:11px;color:var(--muted);">— используются все загруженные документы</span>
</div>
<div class="card-body">
<div id="chatMessages" style="margin-bottom:8px;font-size:13px;"></div>
<div style="display:flex;gap:8px;">
<input type="text" id="chatInput" placeholder="Какая общая сумма? Что изменилось? ..." style="flex:1;height:32px;border:1px solid var(--brand-gray);border-radius:6px;padding:0 8px;font-size:13px;">
<button class="btn btn-primary" id="chatSend" style="height:32px;font-size:13px;">→</button>
</div>
</div>
</div>
</div>
<div class="modal-overlay" id="modalOverlay" onclick="closeModal(event)">
<div class="modal" onclick="event.stopPropagation()">
<div class="modal-header">
<i data-lucide="file-text" style="width:18px;height:18px;"></i>
<span id="modalTitle">Информация о файле</span>
<button class="remove-btn" onclick="closeModal()" style="margin-left:auto;font-size:18px;">&#xd7;</button>
</div>
<div class="modal-body" id="modalBody"></div>
</div>
</div>
<div class="modal-overlay" id="promptModalOverlay" onclick="closePromptModal(event)">
<div class="modal" style="max-width:680px;" onclick="event.stopPropagation()">
<div class="modal-header">
<i data-lucide="settings" style="width:18px;height:18px;"></i>
<span id="promptModalTitle">Промпт: Сравнение</span>
<button class="remove-btn" onclick="closePromptModal()" style="margin-left:auto;font-size:18px;">&#xd7;</button>
</div>
<div class="modal-body" style="padding:16px;">
<div style="display:flex;gap:6px;margin-bottom:4px;">
<button class="btn" id="promptTabExtract" style="font-size:11px;height:26px;">📄 Извлечение</button>
<button class="btn" id="promptTabDiff" style="font-size:11px;height:26px;background:var(--brand-primary);color:#fff;">🔄 Сравнение</button>
<button class="btn" id="promptTabClassify" style="font-size:11px;height:26px;">🏷️ Классификация</button>
</div>
<div style="font-size:10px;color:var(--muted);margin-bottom:10px;">
Извлечение — для базового договора &nbsp;|&nbsp; Сравнение — для допсоглашений &nbsp;|&nbsp; Классификация — автоопределение типа/даты/контрагента
</div>
<textarea id="promptEditor" style="width:100%;height:220px;font-family:monospace;font-size:11px;border:1px solid var(--brand-gray);border-radius:6px;padding:8px;box-sizing:border-box;"></textarea>
<div style="display:flex;gap:8px;margin-top:8px;align-items:center;">
<input id="promptName" placeholder="Имя версии" style="width:140px;font-size:11px;border:1px solid var(--brand-gray);border-radius:4px;padding:4px 6px;">
<input id="promptNotes" placeholder="Что изменили" style="flex:1;font-size:11px;border:1px solid var(--brand-gray);border-radius:4px;padding:4px 6px;">
<button class="btn" id="promptSave" style="font-size:11px;height:28px;background:var(--green);color:#fff;border-color:var(--green);">Сохранить</button>
</div>
<span id="promptStatus" style="font-size:11px;color:var(--muted);display:block;margin-top:4px;"></span>
<div style="margin-top:12px;font-size:11px;color:var(--muted);font-weight:600;">История версий</div>
<div id="promptHistory" style="max-height:180px;overflow-y:auto;margin-top:4px;"></div>
</div>
</div>
</div>
<div class="modal-overlay" id="aboutModalOverlay" onclick="closeAbout(event)">
<div class="modal" style="max-width:720px;" onclick="event.stopPropagation()">
<div class="modal-header">
<i data-lucide="info" style="width:18px;height:18px;"></i>
<span>Контрактор — справка</span>
<button class="remove-btn" onclick="closeAbout()" aria-label="Закрыть справку" style="margin-left:auto;font-size:18px;">&#xd7;</button>
</div>
<div class="modal-body" style="padding:16px;font-size:13px;line-height:1.6;max-height:65vh;overflow-y:auto;" id="aboutBody">
<h3 style="margin-top:0;">Обработка договоров</h3>
<p>Загрузите договоры, дополнительные соглашения и спецификации. Система разберёт документы, определит их тип и сгруппирует связанные документы для сравнения.</p>
<h3>Выбор файлов</h3>
<ul style="padding-left:20px;margin:6px 0 12px;">
<li>Поддерживаются документы PDF, DOC и DOCX; ZIP-архивы раскрываются рекурсивно.</li>
<li>Можно выбрать отдельные файлы или папку. Порядок выбора не влияет на классификацию.</li>
<li>Лимит — 50 МБ на файл и 500 МБ суммарно.</li>
</ul>
<h3>Фильтр папки через obscure.lst</h3>
<p>Чтобы выбрать из папки не все документы, положите файл <code>obscure.lst</code> в её корень. Файл фильтра не добавляется в список документов; если его нет, выбираются все поддерживаемые документы.</p>
<ul style="padding-left:20px;margin:6px 0 12px;">
<li>Одна маска на строку; пустые строки игнорируются, строки с <code>#</code> — комментарии.</li>
<li><code>!</code> в начале маски задаёт исключение; исключения применяются после включающих правил.</li>
<li><code>*</code> соответствует символам внутри одного сегмента пути, <code>?</code> — одному символу, <code>**</code> — подпапкам.</li>
<li>Пути задаются относительно выбранной папки; регистр не учитывается. Вложенный <code>subdir/obscure.lst</code> не используется.</li>
</ul>
<pre style="font-size:12px;background:var(--brand-grey-light);border:1px solid var(--brand-gray);border-radius:8px;padding:10px 12px;overflow-x:auto;margin:8px 0;"># PDF в корне и подпапках
**/*.pdf
# Исключить черновики
!**/*draft*.*</pre>
<h3>После загрузки</h3>
<p><b>1. Классификация</b> — нажмите «Классифицировать»: система определит тип документа и связанные реквизиты.</p>
<p><b>2. Группы</b> — проверьте автоматически сформированные группы документов.</p>
<p><b>3. Сравнение</b> — нажмите «Сравнить» у нужной группы или запустите «Общее сравнение».</p>
</div>
</div>
</div>
<script src="/file-picker/file-picker.iife.js?v={{ config['VERSION'] }}"></script>
<script src="/static/state.js?v={{ config['VERSION'] }}"></script>
<script src="/static/app_utils.js?v={{ config['VERSION'] }}"></script>
<script src="/static/files.js?v={{ config['VERSION'] }}"></script>
<script src="/static/groups.js?v={{ config['VERSION'] }}"></script>
<script src="/static/compare.js?v={{ config['VERSION'] }}"></script>
<script src="/static/app.js?v={{ config['VERSION'] }}"></script>
</body>
</html>