Files
contractor/index.cfm
T

237 lines
18 KiB
Plaintext
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.
<cfsetting showdebugoutput="no" enablecfoutputonly="no">
<cfheader name="Content-Type" value="text/html; charset=utf-8">
<!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/png" href="/favicon.png">
<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; }
* { 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(--background); border-bottom: 1px solid var(--brand-gray); padding: 0 24px; height: 56px; display: flex; align-items: center; gap: 16px; position: sticky; top: 0; z-index: 50; }
.topbar img { height: 28px; }
.topbar .title { font-weight: 600; font-size: 16px; }
.content { max-width: 900px; margin: 32px auto; padding: 0 16px; }
.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: hidden; 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; }
</style>
</head>
<body>
<div class="topbar">
<img src="/nubes-logo.svg" alt="Nubes">
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.170 — Lucee</span></span>
</div>
<div class="content">
<div class="card">
<div class="card-header">
<i data-lucide="folder-open" style="width:18px;height:18px;"></i>
Загрузка договоров/приложений/спецификаций
<span style="margin-left:auto;font-size:11px;color:var(--muted);cursor:pointer;" onclick="openAbout()">О сервисе</span>
</div>
<div class="card-body">
<input type="file" id="fileInput" accept=".docx,.doc,.pdf" multiple style="margin-bottom:6px;width:100%;">
<div style="text-align:right;font-size:11px;color:var(--muted);margin-bottom:6px;">Порядок определяется автоматически при классификации</div>
<div class="table-wrap">
<table>
<thead>
<tr><th>Имя</th><th style="width:130px;">Изменён</th><th style="width:90px;">Размер</th><th style="width:115px;">Парсинг</th><th style="width:30px;">Текст</th><th style="width:30px;">✕</th></tr>
</thead>
<tbody id="fileTable"></tbody>
</table>
</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>
</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="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:10px;">
<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>
</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()" 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>Автоматический разбор и сравнение договоров облачного провайдера (colocation, ЦОД, аренда стоек, каналы связи, облачные ресурсы). Юрист загружает файлы — система сама определяет что есть что, группирует по контрактам и через LLM находит изменения между версиями.</p>
<h3>Архитектура</h3>
<p><b>Два сервера:</b></p>
<ul>
<li><b>Lucee CFML (k8s)</b> — только HTML-оболочка (index.cfm, 191 строка). Домен: contractor.luceek8s.dev.nubes.ru</li>
<li><b>Python 3.12 (VM, 5.172.178.213)</b> — вся бизнес-логика: загрузка, парсинг, LLM-классификация, сравнение, Event Sourcing. Домен: contracts.kube5s.ru</li>
</ul>
<p><b>База данных:</b> PostgreSQL 16 на VM (127.0.0.1:5432/baza). Конфиденциальные данные не покидают ВМ.</p>
<p><b>LLM:</b> gpt-oss-120b через api.aillm.ru (бесплатный доступ, HTTP/2).</p>
<h3>Пайплайн обработки</h3>
<ol>
<li><b>Загрузка</b> — .docx, .doc, .pdf. DOC→DOCX через LibreOffice на VM.</li>
<li><b>Парсинг</b> — python-docx (Word) и PyPDF2 (PDF). Результат: структурированный JSON (параграфы + таблицы).</li>
<li><b>LLM-классификация</b> — каждый файл анализируется LLM (gpt-oss-120b): определяется тип (договор/допсоглашение/спецификация), номер, дата, контрагент. Умная выжимка текста: ~3000 символов (header + маркерные строки) для экономии токенов.</li>
<li><b>Авто-группировка</b> — Python-код нормализует номера договоров и группирует документы по контрактам. Хронологический порядок по датам.</li>
<li><b>LLM-сравнение</b> — для каждой группы: первый документ → извлечение спецификации (extract). Каждый следующий → сравнение с накопленной спецификацией (diff). Операции: ADD (новая услуга), UPDATE (изменение), DELETE (исключена), UNRESOLVED (не удалось сопоставить).</li>
</ol>
<h3>Event Sourcing</h3>
<p>Изменения не перезаписывают спецификацию. Каждая операция — событие в таблице <code>spec_events</code>. Текущее состояние — <code>spec_current</code>. Это даёт:</p>
<ul>
<li><b>Аудит</b> — полная история изменений: кто, когда, на основании какого документа</li>
<li><b>Provenance</b> — для каждого события хранится: ID документа-источника, версия промпта LLM, полный сырой ответ LLM</li>
<li><b>Откат</b> — можно вернуться к любому предыдущему состоянию спецификации</li>
</ul>
<h3>Промпты LLM</h3>
<p>Хранятся в БД, версионируются. Три вида:</p>
<ul>
<li><b>extract</b> — извлечение спецификации из первого документа (базовый договор)</li>
<li><b>diff</b> — сравнение допсоглашения с текущей спецификацией</li>
<li><b>classify</b> — авто-классификация: определение типа/номера/даты/контрагента документа</li>
</ul>
<h3>Схема БД</h3>
<p>7 таблиц: <code>documents</code> (файлы + elements_json + поля классификации), <code>contracts</code>, <code>supplements</code> (связь документ→договор), <code>spec_events</code> (события), <code>spec_current</code> (текущая спецификация), <code>prompts</code> (версионные промпты), <code>upload_chunks</code> (временные чанки).</p>
<h3>Безопасность</h3>
<p>Все файлы и тексты договоров хранятся ТОЛЬКО на ВМ (5.172.178.213). При перезагрузке страницы все данные автоматически удаляются из БД. Промпты (код системы) сохраняются.</p>
<h3>Стек</h3>
<p>Lucee 6.0 (CFML, k8s) + Python 3.12 (http.server, psycopg2, httpx, python-docx, PyPDF2) + PostgreSQL 16 + gpt-oss-120b + nginx + systemd.</p>
</div>
</div>
</div>
<script src="https://contracts.kube5s.ru/static/app_utils.js"></script>
<script src="https://contracts.kube5s.ru/static/app.js"></script>
</body>
</html>