v2.0.13: этап 3 — выбор папок + рекурсивный клиентский ZIP
Deploy contracts-flask / validate (push) Canceled after 0s

- pages_bp: route /upload/<path> отдаёт ES-модули upload/frontend
- index.html: инпут папки (webkitdirectory) + мост listZipFiles
- files.js: expandZipClient (рекурсивный ZIP через drhider listZipFiles)
- app.js: обработчик выбора папки (фильтр .pdf/.doc/.docx/.zip)
This commit is contained in:
“Naeel”
2026-08-26 08:40:19 +03:00
parent c2344f9738
commit d2894ad7c5
5 changed files with 66 additions and 6 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
"""Конфигурация приложения — все настройки в одном месте.""" """Конфигурация приложения — все настройки в одном месте."""
import os import os
VERSION = "2.0.12" VERSION = "2.0.13"
LLM_URL = os.getenv("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions") LLM_URL = os.getenv("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions")
LLM_KEY = os.getenv("LLM_API_KEY", "") LLM_KEY = os.getenv("LLM_API_KEY", "")
+14 -1
View File
@@ -1,8 +1,15 @@
"""HTML-страницы.""" """HTML-страницы."""
from flask import Blueprint, render_template import os
from flask import Blueprint, render_template, send_from_directory
pages_bp = Blueprint("pages", __name__) pages_bp = Blueprint("pages", __name__)
# Переиспользуемый модуль upload: отдаём ES-модули фронтенда браузеру (как drhider)
_UPLOAD_FRONTEND = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
"upload", "frontend",
)
@pages_bp.route("/") @pages_bp.route("/")
def index(): def index():
@@ -12,3 +19,9 @@ def index():
@pages_bp.route("/architect") @pages_bp.route("/architect")
def architect(): def architect():
return render_template("architect.html") return render_template("architect.html")
@pages_bp.route("/upload/<path:filename>")
def upload_frontend(filename):
"""Отдать статику ES-модулей upload/frontend (для клиентского ZIP)."""
return send_from_directory(_UPLOAD_FRONTEND, filename)
+14
View File
@@ -116,6 +116,20 @@ fileInput.addEventListener('change', async function() {
onFilesSelected(newFiles); onFilesSelected(newFiles);
}); });
// Выбор папки (webkitdirectory) — та же обработка, что и файлы
var folderInput = document.getElementById('folderInput');
var folderBtn = document.getElementById('folderBtn');
if (folderBtn && folderInput) {
folderBtn.addEventListener('click', function() { folderInput.click(); });
folderInput.addEventListener('change', function() {
var files = Array.from(folderInput.files).filter(function(f) {
var n = f.name.toLowerCase();
return n.endsWith('.pdf') || n.endsWith('.doc') || n.endsWith('.docx') || n.endsWith('.zip');
});
if (files.length) onFilesSelected(files);
});
}
// ── Классификация ────────────────────────────────────────── // ── Классификация ──────────────────────────────────────────
function showClassifyBtn() { function showClassifyBtn() {
var btn = document.getElementById('classifyBtn'); var btn = document.getElementById('classifyBtn');
+26 -1
View File
@@ -558,6 +558,31 @@ async function finalizeUpload() {
lucide.createIcons(); lucide.createIcons();
} }
/**
* expandZipClient(f) — клиентское рекурсивное раскрытие ZIP (drhider listZipFiles).
* Каждый документ из архива (включая вложенные zip) добавляется в state.files
* с zip_source = имя архива. Fallback: если раскрыть не удалось — архив как есть.
*/
async function expandZipClient(f) {
var nested = [];
try {
if (window.listZipFiles) {
nested = await window.listZipFiles(f, ['.pdf', '.doc', '.docx']);
}
} catch (e) {
nested = [];
}
if (!nested || nested.length === 0) {
state.files.push({ name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: { kind: 'connecting', elapsed: 0 }, zip_source: null });
state.files[state.files.length - 1]._pendingFile = f;
return;
}
for (var z = 0; z < nested.length; z++) {
state.files.push({ name: nested[z].name, lastModified: nested[z].lastModified, size: nested[z].size, file: nested[z], status: { kind: 'connecting', elapsed: 0 }, zip_source: f.name });
state.files[state.files.length - 1]._pendingFile = nested[z];
}
}
/** /**
* ⛔ НЕ МЕНЯТЬ ⛔ onFilesSelected — все строки сразу, потом загрузка. * ⛔ НЕ МЕНЯТЬ ⛔ onFilesSelected — все строки сразу, потом загрузка.
* *
@@ -574,7 +599,7 @@ async function onFilesSelected(newFiles) {
for (var i = 0; i < newFiles.length; i++) { for (var i = 0; i < newFiles.length; i++) {
var f = newFiles[i]; var f = newFiles[i];
if (f.name.toLowerCase().endsWith('.zip')) { if (f.name.toLowerCase().endsWith('.zip')) {
await addZipFile(f); await expandZipClient(f);
continue; continue;
} }
state.files.push({ name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: { kind: 'connecting', elapsed: 0 }, zip_source: null }); state.files.push({ name: f.name, lastModified: f.lastModified, size: f.size, file: f, status: { kind: 'connecting', elapsed: 0 }, zip_source: null });
+11 -3
View File
@@ -73,7 +73,7 @@
<body> <body>
<div class="topbar"> <div class="topbar">
<img src="/static/logo.svg" alt="Nubes"> <img src="/static/logo.svg" alt="Nubes">
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v2.0.12</span></span> <span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v2.0.13</span></span>
<div id="pipelineStepper" style="display:flex;gap:8px;font-size:11px;align-items:center;color:var(--muted);"> <div id="pipelineStepper" style="display:flex;gap:8px;font-size:11px;align-items:center;color:var(--muted);">
<span id="stepUpload">○ Загрузка</span><span></span> <span id="stepUpload">○ Загрузка</span><span></span>
<span id="stepClassify">○ Классификация</span><span></span> <span id="stepClassify">○ Классификация</span><span></span>
@@ -91,6 +91,10 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<input type="file" id="fileInput" accept=".doc,.docx,.pdf,.zip" multiple style="margin-bottom:6px;width:100%;"> <input type="file" id="fileInput" accept=".doc,.docx,.pdf,.zip" multiple style="margin-bottom:6px;width:100%;">
<input type="file" id="folderInput" webkitdirectory multiple style="display:none;">
<div style="margin-bottom:6px;">
<button type="button" class="btn" id="folderBtn" style="font-size:12px;height:30px;">📁 Выбрать папку</button>
</div>
<div style="text-align:right;font-size:11px;color:var(--muted);margin-bottom:6px;">Порядок определяется автоматически при классификации</div> <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;">⚠ При совпадении имён — запрос на перезапись (OK / Отмена). Файлы из ZIP-архивов загружаются через тот же поток.</div> <div style="font-size:11px;color:var(--muted);margin-bottom:6px;">⚠ При совпадении имён — запрос на перезапись (OK / Отмена). Файлы из ZIP-архивов загружаются через тот же поток.</div>
@@ -216,11 +220,15 @@
</div> </div>
</div> </div>
<script type="module">
import { listZipFiles } from '/upload/zip/list_zip_files.js';
window.listZipFiles = listZipFiles;
</script>
<script src="/static/state.js?v=2.0.8"></script> <script src="/static/state.js?v=2.0.8"></script>
<script src="/static/app_utils.js?v=2.0.8"></script> <script src="/static/app_utils.js?v=2.0.8"></script>
<script src="/static/files.js?v=2.0.11"></script> <script src="/static/files.js?v=2.0.13"></script>
<script src="/static/groups.js?v=2.0.8"></script> <script src="/static/groups.js?v=2.0.8"></script>
<script src="/static/compare.js?v=2.0.8"></script> <script src="/static/compare.js?v=2.0.8"></script>
<script src="/static/app.js?v=2.0.10"></script> <script src="/static/app.js?v=2.0.13"></script>
</body> </body>
</html> </html>