diff --git a/src/static/js/components.js b/src/static/js/components.js index f6b2efe..f2d9263 100644 --- a/src/static/js/components.js +++ b/src/static/js/components.js @@ -228,6 +228,8 @@ const UI = { _progressStartTime: null, _progressTimer: null, + _progressCreepTimer: null, + _progressCurrentPercent: 0, /** * Fortschrittsanzeige einblenden und Status setzen. @@ -279,11 +281,25 @@ const UI = { else if (i + 1 === step.active) el.classList.add('active'); }); + // Phasen-Bereiche für stetigen Fortschritt + const ranges = { + queued: { from: 2, to: 10 }, + researching: { from: 12, to: 30 }, + deep_researching: { from: 12, to: 30 }, + analyzing: { from: 33, to: 60 }, + factchecking: { from: 65, to: 92 }, + cancelling: { from: 0, to: 5 }, + }; + + const range = ranges[status] || ranges.queued; const fill = document.getElementById('progress-fill'); - const percent = step.active === 0 ? 5 : Math.round((step.active / 3) * 100); if (fill) { - fill.style.width = percent + '%'; + // Nicht rückwärts gehen, mindestens auf Phasen-Start springen + this._progressCurrentPercent = Math.max(this._progressCurrentPercent, range.from); + fill.style.width = this._progressCurrentPercent + '%'; + this._startProgressCreep(range.to); } + const percent = Math.round(this._progressCurrentPercent); // ARIA-Werte auf der Progressbar aktualisieren bar.setAttribute('aria-valuenow', String(percent)); @@ -314,6 +330,33 @@ const UI = { }, 1000); }, + /** + * Creep-Animation starten: Balken kriecht asymptotisch zum Ziel. + */ + _startProgressCreep(target) { + this._stopProgressCreep(); + this._progressCreepTimer = setInterval(() => { + const remaining = target - this._progressCurrentPercent; + if (remaining <= 0.1) return; + // 3% des Restwegs pro Tick → asymptotisch, nie überschießend + this._progressCurrentPercent += remaining * 0.03; + const fill = document.getElementById('progress-fill'); + if (fill) fill.style.width = this._progressCurrentPercent + '%'; + const bar = document.getElementById('progress-bar'); + if (bar) bar.setAttribute('aria-valuenow', String(Math.round(this._progressCurrentPercent))); + }, 500); + }, + + /** + * Creep-Animation stoppen. + */ + _stopProgressCreep() { + if (this._progressCreepTimer) { + clearInterval(this._progressCreepTimer); + this._progressCreepTimer = null; + } + }, + /** * Abschluss-Animation: Grüner Balken mit Summary-Text. */ @@ -321,8 +364,10 @@ const UI = { const bar = document.getElementById('progress-bar'); if (!bar) return; - // Timer stoppen + // Timer und Creep stoppen this._stopProgressTimer(); + this._stopProgressCreep(); + this._progressCurrentPercent = 100; // Alle Steps auf done ['step-researching', 'step-analyzing', 'step-factchecking'].forEach(id => { @@ -369,8 +414,9 @@ const UI = { if (!bar) return; bar.style.display = 'block'; - // Timer stoppen + // Timer und Creep stoppen this._stopProgressTimer(); + this._stopProgressCreep(); // Error-Klasse bar.classList.remove('progress-bar--complete'); @@ -416,6 +462,8 @@ const UI = { bar.classList.remove('progress-bar--complete', 'progress-bar--error'); } this._stopProgressTimer(); + this._stopProgressCreep(); + this._progressCurrentPercent = 0; }, /**