v1.0.76: фикс uppercase ключей Lucee, заголовки столбцов, красная корзина, Инфо+, столбец Текст с 2-панельной модалкой
This commit is contained in:
+74
-48
@@ -1,6 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""HTTP-сервер: конвертация .doc → .docx + LLM-анализ ДС (/llm-ops) + SSE v2 (/process-v2)"""
|
"""HTTP-сервер: конвертация .doc → .docx + LLM-анализ ДС (/llm-ops) + SSE v2 (/process-v2)"""
|
||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
from socketserver import ThreadingMixIn
|
||||||
|
|
||||||
|
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
||||||
|
"""Многопоточный HTTP-сервер."""
|
||||||
|
daemon_threads = True
|
||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
import subprocess, tempfile, os, sys, json, time
|
import subprocess, tempfile, os, sys, json, time
|
||||||
|
|
||||||
@@ -13,6 +18,30 @@ LLM_MODEL = "gpt-oss-120b"
|
|||||||
LUCEE_URL = "https://contractor.luceek8s.dev.nubes.ru"
|
LUCEE_URL = "https://contractor.luceek8s.dev.nubes.ru"
|
||||||
|
|
||||||
|
|
||||||
|
def call_llm(current_spec, doc_text):
|
||||||
|
"""Вызов LLM API. Возвращает {mode, ops} или кидает исключение."""
|
||||||
|
prompt = build_prompt(current_spec, doc_text)
|
||||||
|
payload = {
|
||||||
|
"model": LLM_MODEL,
|
||||||
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
|
"max_tokens": 8000,
|
||||||
|
"temperature": 0.1,
|
||||||
|
}
|
||||||
|
with httpx.Client(http2=True, timeout=120, verify=False) as client:
|
||||||
|
resp = client.post(LLM_URL, json=payload,
|
||||||
|
headers={"Authorization": f"Bearer {LLM_KEY}", "Content-Type": "application/json"})
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
raw_text = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||||
|
json_text = raw_text
|
||||||
|
if "```json" in json_text:
|
||||||
|
json_text = json_text.split("```json")[1].split("```")[0]
|
||||||
|
elif "```" in json_text:
|
||||||
|
json_text = json_text.split("```")[1].split("```")[0]
|
||||||
|
return json.loads(json_text.strip())
|
||||||
|
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
parsed = urlparse(self.path)
|
parsed = urlparse(self.path)
|
||||||
@@ -21,6 +50,13 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
else:
|
else:
|
||||||
self.send_error(404)
|
self.send_error(404)
|
||||||
|
|
||||||
|
def do_OPTIONS(self):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||||
|
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
if self.path == "/llm-ops":
|
if self.path == "/llm-ops":
|
||||||
self._handle_llm_ops()
|
self._handle_llm_ops()
|
||||||
@@ -57,11 +93,12 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", "text/event-stream")
|
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
||||||
self.send_header("Cache-Control", "no-cache")
|
self.send_header("Cache-Control", "no-cache")
|
||||||
self.send_header("Connection", "keep-alive")
|
self.send_header("Connection", "keep-alive")
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
self.wfile.write(b": ok\n\n")
|
||||||
|
self.wfile.flush()
|
||||||
|
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
|
|
||||||
@@ -102,7 +139,12 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
)
|
)
|
||||||
if not docs:
|
if not docs:
|
||||||
continue
|
continue
|
||||||
elements = json.loads(docs[0]["elements_json"])
|
ej = docs[0]["elements_json"]
|
||||||
|
if isinstance(ej, dict) and "Value" in ej:
|
||||||
|
ej = ej["Value"]
|
||||||
|
elements = json.loads(ej)
|
||||||
|
# Lucee serializeJSON → UPPERCASE keys, normalize to lowercase
|
||||||
|
elements = [{k.lower(): v for k, v in el.items()} for el in elements]
|
||||||
lines = []
|
lines = []
|
||||||
for el in elements:
|
for el in elements:
|
||||||
if el.get("type") == "paragraph":
|
if el.get("type") == "paragraph":
|
||||||
@@ -121,25 +163,37 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
self._sse({"type": "extract_start", "supplement_id": sid, "filename": s["filename"]})
|
self._sse({"type": "extract_start", "supplement_id": sid, "filename": s["filename"]})
|
||||||
|
|
||||||
# LLM
|
# LLM (прямой вызов, не HTTP self-call)
|
||||||
|
import threading
|
||||||
t1 = time.time()
|
t1 = time.time()
|
||||||
resp = httpx.post(f"http://127.0.0.1:8766/llm-ops", json={
|
done = threading.Event()
|
||||||
"contract_id": cid, "supplement_id": sid,
|
result = [None]
|
||||||
"current_spec": current_spec, "doc_text": doc_text
|
error = [None]
|
||||||
}, timeout=130)
|
|
||||||
|
def do_llm():
|
||||||
|
try:
|
||||||
|
result[0] = call_llm(current_spec, doc_text)
|
||||||
|
except Exception as e:
|
||||||
|
error[0] = str(e)
|
||||||
|
finally:
|
||||||
|
done.set()
|
||||||
|
|
||||||
|
t = threading.Thread(target=do_llm)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
# Keepalive каждые 15с пока LLM думает
|
||||||
|
while not done.wait(15):
|
||||||
|
self._sse({"type": "keepalive"})
|
||||||
|
|
||||||
|
t.join()
|
||||||
elapsed = round(time.time() - t1, 1)
|
elapsed = round(time.time() - t1, 1)
|
||||||
|
|
||||||
if resp.status_code != 200:
|
if error[0]:
|
||||||
self._sse({"type": "extract_error", "supplement_id": sid, "filename": s["filename"],
|
self._sse({"type": "extract_error", "supplement_id": sid, "filename": s["filename"],
|
||||||
"error": f"LLM HTTP {resp.status_code}", "time_s": elapsed})
|
"error": error[0], "time_s": elapsed})
|
||||||
continue
|
|
||||||
|
|
||||||
llm_result = resp.json()
|
|
||||||
if "error" in llm_result:
|
|
||||||
self._sse({"type": "extract_error", "supplement_id": sid, "filename": s["filename"],
|
|
||||||
"error": llm_result["error"], "time_s": elapsed})
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
llm_result = result[0]
|
||||||
ops = llm_result.get("ops", [])
|
ops = llm_result.get("ops", [])
|
||||||
mode = llm_result.get("mode", "partial")
|
mode = llm_result.get("mode", "partial")
|
||||||
self._sse({"type": "llm_done", "supplement_id": sid, "filename": s["filename"],
|
self._sse({"type": "llm_done", "supplement_id": sid, "filename": s["filename"],
|
||||||
@@ -152,7 +206,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
)
|
)
|
||||||
if apply_resp.status_code == 200:
|
if apply_resp.status_code == 200:
|
||||||
ar = apply_resp.json()
|
ar = apply_resp.json()
|
||||||
summary = ar.get("summary", {})
|
# Lucee serializeJSON → UPPERCASE keys
|
||||||
|
summary = {k.lower(): v for k, v in ar.get("SUMMARY", {}).items()}
|
||||||
self._sse({"type": "applied", "supplement_id": sid, "summary": summary})
|
self._sse({"type": "applied", "supplement_id": sid, "summary": summary})
|
||||||
else:
|
else:
|
||||||
self._sse({"type": "apply_error", "supplement_id": sid,
|
self._sse({"type": "apply_error", "supplement_id": sid,
|
||||||
@@ -167,41 +222,12 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
def _handle_llm_ops(self):
|
def _handle_llm_ops(self):
|
||||||
length = int(self.headers.get("Content-Length", 0))
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
body = json.loads(self.rfile.read(length))
|
body = json.loads(self.rfile.read(length))
|
||||||
|
|
||||||
prompt = build_prompt(
|
|
||||||
body.get("current_spec", []),
|
|
||||||
body.get("doc_text", ""),
|
|
||||||
)
|
|
||||||
payload = {
|
|
||||||
"model": LLM_MODEL,
|
|
||||||
"messages": [{"role": "user", "content": prompt}],
|
|
||||||
"max_tokens": 8000,
|
|
||||||
"temperature": 0.1,
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with httpx.Client(http2=True, timeout=120, verify=False) as client:
|
result = call_llm(body.get("current_spec", []), body.get("doc_text", ""))
|
||||||
resp = client.post(LLM_URL, json=payload,
|
|
||||||
headers={"Authorization": f"Bearer {LLM_KEY}", "Content-Type": "application/json"})
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = resp.json()
|
|
||||||
|
|
||||||
raw_text = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
||||||
|
|
||||||
# Вытащить JSON из ответа
|
|
||||||
json_text = raw_text
|
|
||||||
if "```json" in json_text:
|
|
||||||
json_text = json_text.split("```json")[1].split("```")[0]
|
|
||||||
elif "```" in json_text:
|
|
||||||
json_text = json_text.split("```")[1].split("```")[0]
|
|
||||||
|
|
||||||
result = json.loads(json_text.strip())
|
|
||||||
|
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", "application/json")
|
self.send_header("Content-Type", "application/json")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(json.dumps(result, ensure_ascii=False).encode())
|
self.wfile.write(json.dumps(result, ensure_ascii=False).encode())
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.send_response(502)
|
self.send_response(502)
|
||||||
self.send_header("Content-Type", "application/json")
|
self.send_header("Content-Type", "application/json")
|
||||||
@@ -234,4 +260,4 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
os.rmdir(tmpdir)
|
os.rmdir(tmpdir)
|
||||||
def log_message(self, *a): pass
|
def log_message(self, *a): pass
|
||||||
|
|
||||||
HTTPServer(("127.0.0.1", 8766), Handler).serve_forever()
|
ThreadingHTTPServer(("127.0.0.1", 8766), Handler).serve_forever()
|
||||||
|
|||||||
@@ -34,14 +34,15 @@
|
|||||||
.status-err { color: var(--destructive); }
|
.status-err { color: var(--destructive); }
|
||||||
.summary-row td { background: rgba(34,197,94,.05); font-weight: 600; }
|
.summary-row td { background: rgba(34,197,94,.05); font-weight: 600; }
|
||||||
.empty-row td { color: var(--muted); text-align: center; padding: 24px; }
|
.empty-row td { color: var(--muted); text-align: center; padding: 24px; }
|
||||||
.remove-btn { cursor: pointer; color: var(--muted); background: none; border: none; padding: 2px 4px; font-size: 16px; line-height: 1; }
|
.remove-btn { cursor: pointer; color: #f87171; background: none; border: none; padding: 2px 4px; font-size: 16px; line-height: 1; }
|
||||||
.remove-btn:hover { color: var(--destructive); }
|
.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 { 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:hover { color: var(--brand-primary); }
|
||||||
.info-btn.visible { display: inline; }
|
.info-btn.visible { display: inline; }
|
||||||
.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 { 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-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 { 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-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 { padding: 16px; overflow-y: auto; }
|
||||||
.modal-body .kv { display: flex; margin-bottom: 6px; font-size: 13px; }
|
.modal-body .kv { display: flex; margin-bottom: 6px; font-size: 13px; }
|
||||||
@@ -56,12 +57,21 @@
|
|||||||
.diff-field-new { color: var(--green); font-weight: 600; }
|
.diff-field-new { color: var(--green); font-weight: 600; }
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@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; }
|
.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; margin: 0; }
|
||||||
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<img src="/nubes-logo.svg" alt="Nubes">
|
<img src="/nubes-logo.svg" alt="Nubes">
|
||||||
<span class="title">Сверка договоров — LLM <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.75 — Lucee</span></span>
|
<span class="title">Сверка договоров — LLM <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.76 — Lucee</span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@@ -76,7 +86,7 @@
|
|||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<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;">View</th><th style="width:30px;"></th><th style="width:30px;"></th></tr>
|
<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;">View</th><th style="width:30px;">Текст</th><th style="width:30px;">Инфо</th><th style="width:30px;">✕</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="fileTable"></tbody>
|
<tbody id="fileTable"></tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -183,6 +193,7 @@ function renderTable() {
|
|||||||
'<td class="status-cell">' + (f.status || '') + '</td>' +
|
'<td class="status-cell">' + (f.status || '') + '</td>' +
|
||||||
'<td style="text-align:center;">' + radio + '</td>' +
|
'<td style="text-align:center;">' + radio + '</td>' +
|
||||||
'<td><button class="info-btn' + (f.doc_id ? ' visible' : '') + '" onclick="showView(\'' + (f.doc_id||'') + '\')" title="Просмотр"><i data-lucide="eye" style="width:16px;height:16px;"></i></button></td>' +
|
'<td><button class="info-btn' + (f.doc_id ? ' visible' : '') + '" onclick="showView(\'' + (f.doc_id||'') + '\')" title="Просмотр"><i data-lucide="eye" style="width:16px;height:16px;"></i></button></td>' +
|
||||||
|
'<td><button class="info-btn' + (f.doc_id ? ' visible' : '') + '" onclick="showText(\'' + (f.doc_id||'') + '\')" title="Текст / Распарсено"><i data-lucide="file-text" style="width:16px;height:16px;"></i></button></td>' +
|
||||||
'<td><button class="info-btn' + (f.doc_id ? ' visible' : '') + '" id="info_' + i + '" onclick="showInfo(' + i + ')" title="Инфо"><i data-lucide="info" style="width:16px;height:16px;"></i></button></td>' +
|
'<td><button class="info-btn' + (f.doc_id ? ' visible' : '') + '" id="info_' + i + '" onclick="showInfo(' + i + ')" title="Инфо"><i data-lucide="info" style="width:16px;height:16px;"></i></button></td>' +
|
||||||
'<td><button class="remove-btn" onclick="removeFile(' + i + ')" title="Удалить"><i data-lucide="trash-2" style="width:16px;height:16px;"></i></button></td>' +
|
'<td><button class="remove-btn" onclick="removeFile(' + i + ')" title="Удалить"><i data-lucide="trash-2" style="width:16px;height:16px;"></i></button></td>' +
|
||||||
'</tr>';
|
'</tr>';
|
||||||
@@ -568,29 +579,48 @@ function showInfo(i) {
|
|||||||
document.getElementById('modalTitle').textContent = f.name;
|
document.getElementById('modalTitle').textContent = f.name;
|
||||||
var html = '<div class="kv"><span class="k">Размер</span><span class="v">' + formatSize(f.size) + '</span></div>';
|
var html = '<div class="kv"><span class="k">Размер</span><span class="v">' + formatSize(f.size) + '</span></div>';
|
||||||
html += '<div class="kv"><span class="k">Изменён</span><span class="v">' + formatDate(f.lastModified) + '</span></div>';
|
html += '<div class="kv"><span class="k">Изменён</span><span class="v">' + formatDate(f.lastModified) + '</span></div>';
|
||||||
|
html += '<div class="kv"><span class="k">Тип</span><span class="v">' + (f.type || '—') + '</span></div>';
|
||||||
if (f.parseInfo) {
|
if (f.parseInfo) {
|
||||||
var d = f.parseInfo;
|
var d = f.parseInfo;
|
||||||
html += '<div class="kv"><span class="k">Элементов</span><span class="v">' + (d.ELEMENT_COUNT||0) + '</span></div>';
|
html += '<div class="kv"><span class="k">Элементов</span><span class="v">' + (d.ELEMENT_COUNT||0) + '</span></div>';
|
||||||
html += '<div class="kv"><span class="k">Параграфов</span><span class="v">' + (d.PARAGRAPHS||0) + '</span></div>';
|
html += '<div class="kv"><span class="k">Параграфов</span><span class="v">' + (d.PARAGRAPHS||0) + '</span></div>';
|
||||||
html += '<div class="kv"><span class="k">Таблиц</span><span class="v">' + (d.TABLES||0) + '</span></div>';
|
html += '<div class="kv"><span class="k">Таблиц</span><span class="v">' + (d.TABLES||0) + '</span></div>';
|
||||||
html += '<div class="kv"><span class="k">Строк таблиц</span><span class="v">' + (d.TABLE_ROWS||0) + '</span></div>';
|
html += '<div class="kv"><span class="k">Строк таблиц</span><span class="v">' + (d.TABLE_ROWS||0) + '</span></div>';
|
||||||
|
html += '<div class="kv"><span class="k">Страниц</span><span class="v">' + (d.PAGES||'—') + '</span></div>';
|
||||||
|
if (d.PARSE_TIME_MS) {
|
||||||
|
html += '<div class="kv"><span class="k">Время парсинга</span><span class="v">' + (d.PARSE_TIME_MS > 1000 ? (d.PARSE_TIME_MS/1000).toFixed(1) + ' с' : d.PARSE_TIME_MS + ' мс') + '</span></div>';
|
||||||
|
}
|
||||||
|
if (d.TEXT_LENGTH) {
|
||||||
|
html += '<div class="kv"><span class="k">Символов текста</span><span class="v">' + d.TEXT_LENGTH.toLocaleString() + '</span></div>';
|
||||||
|
}
|
||||||
if (d.TEXT_PREVIEW) {
|
if (d.TEXT_PREVIEW) {
|
||||||
html += '<div style="margin-top:10px;font-weight:600;font-size:13px;">Превью текста:</div>';
|
html += '<div style="margin-top:10px;font-weight:600;font-size:13px;">Превью текста (первые 3000 симв.):</div>';
|
||||||
html += '<pre>' + d.TEXT_PREVIEW + '</pre>';
|
html += '<pre style="max-height:200px;overflow-y:auto;">' + d.TEXT_PREVIEW.substring(0, 3000) + '</pre>';
|
||||||
}
|
}
|
||||||
if (d.TABLE_HEADERS && d.TABLE_HEADERS.length > 0) {
|
if (d.TABLE_HEADERS && d.TABLE_HEADERS.length > 0) {
|
||||||
html += '<div style="margin-top:10px;font-weight:600;font-size:13px;">Первые строки таблиц:</div>';
|
html += '<div style="margin-top:10px;font-weight:600;font-size:13px;">Заголовки таблиц:</div>';
|
||||||
d.TABLE_HEADERS.forEach(function(hdr, ti) {
|
d.TABLE_HEADERS.forEach(function(hdr, ti) {
|
||||||
html += '<div style="margin-top:4px;font-size:11px;color:var(--muted);">Таблица ' + (ti+1) + ':</div>';
|
html += '<div style="margin-top:4px;font-size:11px;color:var(--muted);">Таблица ' + (ti+1) + ' (' + (hdr||[]).length + ' колонок):</div>';
|
||||||
html += '<pre>' + (hdr||[]).map(function(c){return c||'(пусто)';}).join(' | ') + '</pre>';
|
html += '<pre>' + (hdr||[]).map(function(c){return c||'(пусто)';}).join(' | ') + '</pre>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (d.TABLE_SAMPLES && d.TABLE_SAMPLES.length > 0) {
|
||||||
|
html += '<div style="margin-top:10px;font-weight:600;font-size:13px;">Первые строки таблиц:</div>';
|
||||||
|
d.TABLE_SAMPLES.forEach(function(sample, ti) {
|
||||||
|
html += '<div style="margin-top:4px;font-size:11px;color:var(--muted);">Таблица ' + (ti+1) + ':</div>';
|
||||||
|
html += '<pre style="max-height:120px;overflow-y:auto;">' + sample.map(function(row){ return (row||[]).map(function(c){return c||'(пусто)';}).join(' | '); }).join('\n') + '</pre>';
|
||||||
|
});
|
||||||
|
}
|
||||||
if (d.ERRORS && d.ERRORS.length > 0) {
|
if (d.ERRORS && d.ERRORS.length > 0) {
|
||||||
html += '<div style="margin-top:8px;color:var(--destructive);font-weight:600;">Ошибки:</div>';
|
html += '<div style="margin-top:8px;color:var(--destructive);font-weight:600;">Ошибки:</div>';
|
||||||
html += '<pre>' + d.ERRORS.join('\n') + '</pre>';
|
html += '<pre>' + d.ERRORS.join('\n') + '</pre>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
html += '<div class="kv"><span class="k">doc_id</span><span class="v">' + (f.doc_id || '—') + '</span></div>';
|
html += '<div class="kv"><span class="k">doc_id</span><span class="v">' + (f.doc_id || '—') + '</span></div>';
|
||||||
|
if (f.supp_id) {
|
||||||
|
html += '<div class="kv"><span class="k">supp_id</span><span class="v">' + f.supp_id + '</span></div>';
|
||||||
|
html += '<div class="kv"><span class="k">Тип ДС</span><span class="v">' + (f.supp_type || '—') + '</span></div>';
|
||||||
|
}
|
||||||
document.getElementById('modalBody').innerHTML = html;
|
document.getElementById('modalBody').innerHTML = html;
|
||||||
document.getElementById('modalOverlay').classList.add('open');
|
document.getElementById('modalOverlay').classList.add('open');
|
||||||
}
|
}
|
||||||
@@ -612,9 +642,64 @@ async function showView(docId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function showText(docId) {
|
||||||
|
document.getElementById('modalTitle').textContent = 'Текст / Распарсено';
|
||||||
|
document.getElementById('modalBody').innerHTML = '<span style="color:var(--muted);">Загрузка...</span>';
|
||||||
|
var modal = document.querySelector('.modal');
|
||||||
|
modal.classList.add('wide');
|
||||||
|
document.getElementById('modalOverlay').classList.add('open');
|
||||||
|
try {
|
||||||
|
var resp = await fetch('/view.cfm?doc_id=' + docId);
|
||||||
|
var rawText = await resp.text();
|
||||||
|
// Clean leading whitespace from CFML
|
||||||
|
rawText = rawText.replace(/^\s+/, '');
|
||||||
|
// Fetch parsed elements via API
|
||||||
|
var qResp = await fetch('/api.cfm?action=query&sql=SELECT%20elements_json%20FROM%20documents%20WHERE%20id%3D\'' + docId + '\'');
|
||||||
|
var qData = await qResp.json();
|
||||||
|
var parsedHtml = '<span style="color:var(--muted);">Нет данных парсинга</span>';
|
||||||
|
if (qData.OK && qData.ROWS && qData.ROWS.length > 0) {
|
||||||
|
var ej = qData.ROWS[0].ELEMENTS_JSON;
|
||||||
|
if (typeof ej === 'object' && ej.Value) ej = ej.Value;
|
||||||
|
var elements = JSON.parse(ej);
|
||||||
|
if (elements.length > 0) {
|
||||||
|
var rows = '';
|
||||||
|
elements.forEach(function(el, idx) {
|
||||||
|
var etype = el.type || el.TYPE || '?';
|
||||||
|
var style = el.style || el.STYLE || '';
|
||||||
|
var content = '';
|
||||||
|
if (etype === 'paragraph') {
|
||||||
|
content = (el.text || el.TEXT || '').substring(0, 200);
|
||||||
|
} else if (etype === 'table') {
|
||||||
|
var tblRows = el.rows || el.ROWS || [];
|
||||||
|
content = tblRows.length + '×' + (tblRows[0] ? tblRows[0].length : 0) + ' ячеек';
|
||||||
|
}
|
||||||
|
rows += '<tr><td>' + (idx+1) + '</td><td>' + etype + '</td><td>' + style + '</td><td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + (content||'').replace(/</g,'<').replace(/>/g,'>') + '</td></tr>';
|
||||||
|
});
|
||||||
|
parsedHtml = '<div class="table-wrap"><table><thead><tr><th>№</th><th>Тип</th><th>Стиль</th><th>Содержание</th></tr></thead><tbody>' + rows + '</tbody></table></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var html = '<div class="text-panes">' +
|
||||||
|
'<div class="text-pane"><div class="pane-title">Сырой текст</div><pre>' + rawText.replace(/</g,'<').replace(/>/g,'>') + '</pre></div>' +
|
||||||
|
'<div class="text-pane"><div class="pane-title">Распарсено</div>' + parsedHtml + '</div>' +
|
||||||
|
'</div>';
|
||||||
|
document.getElementById('modalBody').innerHTML = html;
|
||||||
|
} catch(e) {
|
||||||
|
document.getElementById('modalBody').innerHTML = '<span style="color:var(--destructive);">Ошибка загрузки</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeModal — сбросить wide при закрытии
|
||||||
|
var origCloseModal = closeModal;
|
||||||
|
closeModal = function(e) {
|
||||||
|
if (e && e.target !== document.getElementById('modalOverlay')) return;
|
||||||
|
document.querySelector('.modal').classList.remove('wide');
|
||||||
|
document.getElementById('modalOverlay').classList.remove('open');
|
||||||
|
};
|
||||||
|
|
||||||
window.showInfo = showInfo;
|
window.showInfo = showInfo;
|
||||||
window.closeModal = closeModal;
|
window.closeModal = closeModal;
|
||||||
window.showView = showView;
|
window.showView = showView;
|
||||||
|
window.showText = showText;
|
||||||
|
|
||||||
// ── Промпт ───────────────────────────────────────────────────
|
// ── Промпт ───────────────────────────────────────────────────
|
||||||
var promptEditor = document.getElementById('promptEditor');
|
var promptEditor = document.getElementById('promptEditor');
|
||||||
|
|||||||
Reference in New Issue
Block a user