v0.0.38: живой таймер обработки + индикация ИИ (live-блок, heartbeat llm в SSE)
Deploy drhider / validate (push) Canceled after 0s

This commit is contained in:
“Naeel”
2026-08-19 09:25:01 +04:00
parent defbc10004
commit e548c89880
5 changed files with 165 additions and 26 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ if _sys_path_root not in sys.path:
sys.path.insert(0, _sys_path_root)
# Версия приложения (меняется при изменениях)
VERSION = "0.0.37"
VERSION = "0.0.38"
def create_app():
+9
View File
@@ -103,6 +103,15 @@ def process_stream(sid):
except queue.Empty:
if cancel.is_set():
return
# Heartbeat: живая статистика LLM (для таймера в UI)
try:
yield (
f"event: llm\n"
f"data: {json.dumps({'active': llm.llm_active, 'elapsed': round(llm.llm_elapsed_now(), 1), 'tokens': llm.tokens_total})}\n\n"
)
except _disconnect_exceptions():
cancel.set()
return
continue
kind = evt[0]
+58 -2
View File
@@ -97,6 +97,21 @@
}
.stat-label { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin-bottom: 4px; }
.stat-value { font-size: 24px; font-weight: 800; color: #166534; }
.live-block {
display: none; margin-top: 12px; padding: 16px;
background: linear-gradient(135deg, #eff6ff, #dbeafe);
border: 2px solid var(--brand-primary); border-radius: 12px;
}
.live-block.show { display: block; }
.live-title { font-size: 18px; font-weight: 800; color: #1e40af; margin-bottom: 6px; }
.live-file { font-size: 15px; color: #1e3a8a; margin-bottom: 10px; word-break: break-word; }
.live-timer { font-size: 40px; font-weight: 800; color: #1d4ed8; line-height: 1.1; }
.live-llm {
display: none; margin-top: 8px; padding: 8px 12px; border-radius: 8px;
background: #fef3c7; border: 1px solid #f59e0b; color: #92400e;
font-size: 18px; font-weight: 700; width: fit-content;
}
.live-llm.show { display: inline-block; }
</style>
</head>
<body>
@@ -141,6 +156,12 @@
<button class="btn btn-primary" id="dlZip" onclick="downloadZip()">📦 Скачать ZIP</button>
<button class="btn" id="dlCsv" onclick="downloadCsv()">📋 Скачать CSV</button>
</div>
<div class="live-block" id="liveBlock">
<div class="live-title">⏱ Обработка…</div>
<div class="live-file" id="liveFile">—</div>
<div class="live-timer" id="liveTimer">0.0 с</div>
<div class="live-llm" id="liveLlm">🤖 ИИ обрабатывает… <span id="liveLlmTime">0.0 с</span></div>
</div>
<div class="stats-block" id="statsBlock">
<div class="stats-title">📊 Итоги обработки</div>
<div class="stats-row">
@@ -149,7 +170,7 @@
<div class="stat-value" id="stTotalTime">—</div>
</div>
<div class="stat-item">
<div class="stat-label">Время ИИ</div>
<div class="stat-label">Из них ИИ</div>
<div class="stat-value" id="stLlmTime">—</div>
</div>
<div class="stat-item">
@@ -186,6 +207,7 @@ function resetAll() {
st.textContent = '';
db.classList.remove('show');
document.getElementById('statsBlock').classList.remove('show');
document.getElementById('liveBlock').classList.remove('show');
ub.disabled = false;
}
@@ -418,6 +440,21 @@ async function uploadFiles() {
st.textContent = 'Обработка... ' + sec + 'с';
}, 1000);
// Крупный live-блок «Идёт обработка» с тикающим таймером
const liveBlock = document.getElementById('liveBlock');
const liveTimerEl = document.getElementById('liveTimer');
const liveFileEl = document.getElementById('liveFile');
const liveLlm = document.getElementById('liveLlm');
const liveLlmTime = document.getElementById('liveLlmTime');
liveLlm.classList.remove('show');
liveTimerEl.textContent = '0.0 с';
liveFileEl.textContent = 'Подготовка…';
liveBlock.classList.add('show');
const liveRefresh = setInterval(() => {
const sec = ((performance.now() - t0) / 1000).toFixed(1);
liveTimerEl.textContent = sec + ' с';
}, 200);
try {
await new Promise((resolve, reject) => {
activeES = new EventSource('/api/process_stream/' + currentSid);
@@ -428,6 +465,19 @@ async function uploadFiles() {
const elapsed = ((performance.now() - fileTimers[d.idx]) / 1000).toFixed(1);
ss(d.idx, '<span style="color:#2563eb">⏳ ' + elapsed + 'с</span>');
}, 200);
// Показываем текущий файл и его размер в live-блоке
const f = sf[d.idx];
const sz = f ? fs(f.size) : '';
liveFileEl.textContent = 'Файл ' + (d.idx + 1) + '/' + total + ': ' + d.name + (sz ? ' (' + sz + ')' : '');
});
activeES.addEventListener('llm', function(e) {
const d = JSON.parse(e.data);
if (d.active) {
liveLlmTime.textContent = d.elapsed + ' с';
liveLlm.classList.add('show');
} else {
liveLlm.classList.remove('show');
}
});
activeES.addEventListener('done', function(e) {
const d = JSON.parse(e.data);
@@ -439,10 +489,12 @@ async function uploadFiles() {
activeES.close();
activeES = null;
clearInterval(ptimer);
clearInterval(liveRefresh);
liveBlock.classList.remove('show');
const d = JSON.parse(e.data);
const totalSec = ((performance.now() - t0) / 1000).toFixed(1);
st.className = 'status done';
st.textContent = '✅ Обработано ' + d.total + ' файлов за ' + totalSec + 'с';
st.textContent = '✅ Обработано ' + d.total + ' файлов: общее ' + totalSec + 'с, из них ИИ ' + (d.llm_sec > 0 ? d.llm_sec + 'с' : '0с');
// Крупный блок статистики
const sb = document.getElementById('statsBlock');
document.getElementById('stTotalTime').textContent = totalSec + ' с';
@@ -461,11 +513,15 @@ async function uploadFiles() {
activeES.close();
activeES = null;
clearInterval(ptimer);
clearInterval(liveRefresh);
liveBlock.classList.remove('show');
reject(new Error('SSE connection failed'));
};
});
} catch (err) {
clearInterval(ptimer);
clearInterval(liveRefresh);
liveBlock.classList.remove('show');
st.className = 'status error';
st.textContent = 'Ошибка: ' + err.message;
}