diff --git a/site/app.py b/site/app.py
index 2a6558a..1256a79 100644
--- a/site/app.py
+++ b/site/app.py
@@ -21,7 +21,7 @@ if _sys_path_root not in sys.path:
sys.path.insert(0, _sys_path_root)
# Версия приложения (меняется при изменениях)
-VERSION = "0.0.63"
+VERSION = "0.0.64"
def setup_logging():
diff --git a/site/templates/index.html b/site/templates/index.html
index 88ddedf..60570df 100644
--- a/site/templates/index.html
+++ b/site/templates/index.html
@@ -210,6 +210,7 @@ let procPhase = 'idle'; // 'idle' | 'processing'
let procState = {}; // sfIdx -> {st, elapsed, eta, est, chars, t0}
let procNameIdx = {}; // имя (с бэка) -> sfIdx
let procExtractDone = false; // true после extract_done (далее done = реальная готовность)
+let procExtractRate = null; // измеренная скорость извлечения (сек/МБ) для оценок ожидающих
let procRefresh = null; // setInterval перерисовки таблицы
let ptimer = null, liveRefresh = null; // таймеры статуса и live-блока
const MAX_FILE_BYTES = 50 * 1024 * 1024; // 50 МБ на один файл
@@ -235,6 +236,7 @@ function resetAll() {
procState = {};
procNameIdx = {};
procExtractDone = false;
+ procExtractRate = null;
document.getElementById('cancelBtn').style.display = 'none';
fi.value = '';
rr();
@@ -323,10 +325,17 @@ function renderProcTable() {
rows.push('
| ○ Ожидают обработки (' + groups.pending.length + ') |
');
for (const i of groups.pending) {
const p = procState[i] || {};
+ // Оценка: LLM (chars x rate) если известна; иначе грубая по размеру/скорости извлечения
if (rate && !p.est && p.chars > 0) p.est = p.chars * rate;
+ if (!p.est) {
+ const szMB = (sf[i] ? sf[i].size : 0) / 1048576;
+ const k = procExtractRate || 3; // сек/МБ: замеренная или запасная
+ p.est = Math.max(1, Math.round(szMB * k));
+ }
let txt;
if (overNames.has(sf[i].name)) txt = '🔥 не учитывается';
else if (p.st === 'analyzed') txt = 'анализ ✓';
+ else if (p.st === 'current') txt = '⏳';
else if (p.est != null) txt = '~' + fmtSec(p.est) + '';
else txt = '—';
rows.push(procRow(i, txt));
@@ -644,6 +653,7 @@ async function uploadFiles() {
procState = {};
procNameIdx = {};
procExtractDone = false;
+ procExtractRate = null;
for (let k = 0; k < total; k++) {
procState[sendIdx[k]] = { st: 'pending', elapsed: 0, eta: null, est: null, chars: 0, t0: 0 };
}
@@ -688,6 +698,20 @@ async function uploadFiles() {
procNameIdx[d.name] = idx;
const p = procState[idx];
if (p) { p.st = 'pending'; p.elapsed = 0; }
+ // Этап извлечения: текущий файл = обрабатываемый сейчас (тикер в таблице).
+ // Предыдущий «текущий» демотируем и замеряем его скорость (сек/МБ) для оценок.
+ if (!procExtractDone) {
+ let prevIdx = null;
+ for (const i in procState) {
+ if (procState[i].st === 'current') { prevIdx = Number(i); procState[i].st = 'pending'; }
+ }
+ if (prevIdx != null && sf[prevIdx] && procState[prevIdx].t0) {
+ const el = (performance.now() - procState[prevIdx].t0) / 1000;
+ const szMB = sf[prevIdx].size / 1048576;
+ if (el > 0 && szMB > 0) procExtractRate = el / szMB;
+ }
+ if (p) { p.st = 'current'; p.t0 = performance.now(); p.eta = null; }
+ }
const f = sf[idx];
const sz = f ? fs(f.size) : '';
currentLiveFile = 'Файл ' + (d.idx + 1) + '/' + total + ': ' + d.name + (sz ? ' (' + sz + ')' : '');
@@ -696,6 +720,10 @@ async function uploadFiles() {
activeES.addEventListener('extract_done', function(e) {
const d = JSON.parse(e.data);
procExtractDone = true;
+ // Этап извлечения завершён — снимаем подсветку «текущего» извлечения
+ for (const i in procState) {
+ if (procState[i].st === 'current') procState[i].st = 'pending';
+ }
// per-file символы -> оценка времени для ожидающих файлов
if (d.per_file) {
for (const nm in d.per_file) {