diff --git a/ui/deckgenerator.css b/ui/deckgenerator.css
index 6266265..76cb7ec 100644
--- a/ui/deckgenerator.css
+++ b/ui/deckgenerator.css
@@ -1297,4 +1297,42 @@
.dg-log-error {
color: #f59e0b;
+}
+
+/* ── Progress Bar (Deck Generation) ──────────────────── */
+.dg-progress-wrap {
+ width: 100%;
+ max-width: 400px;
+ margin-top: 16px;
+}
+
+.dg-progress-bar {
+ height: 6px;
+ background: var(--surface-2, #2a2a3a);
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.dg-progress-fill {
+ height: 100%;
+ width: 0%;
+ background: linear-gradient(90deg, var(--accent, #7c6ff7), #a78bfa);
+ border-radius: 3px;
+ transition: width 1s ease;
+}
+
+.dg-progress-meta {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 6px;
+ font-size: 0.78rem;
+}
+
+.dg-progress-phase {
+ color: var(--text-muted, #999);
+}
+
+.dg-progress-elapsed {
+ color: var(--text-muted, #999);
+ font-variant-numeric: tabular-nums;
}
\ No newline at end of file
diff --git a/ui/deckgenerator.html b/ui/deckgenerator.html
index e9b6363..34afe58 100644
--- a/ui/deckgenerator.html
+++ b/ui/deckgenerator.html
@@ -166,6 +166,15 @@
Generating deck via Local AI...
+
diff --git a/ui/deckgenerator.js b/ui/deckgenerator.js
index 01a2d8c..6ed1df8 100644
--- a/ui/deckgenerator.js
+++ b/ui/deckgenerator.js
@@ -53,6 +53,65 @@ const DeckGenerator = (() => {
let searchTimeout = null;
+ // ── Progress bar state ──────────────────────────────
+ const PROGRESS_PHASES = [
+ { at: 0, pct: 5, msg: 'Connecting to AI model...' },
+ { at: 3, pct: 10, msg: 'Resolving commander on Scryfall...' },
+ { at: 6, pct: 18, msg: 'Building collection summary...' },
+ { at: 10, pct: 25, msg: 'Sending prompt to LLM...' },
+ { at: 20, pct: 40, msg: 'AI is building the 99...' },
+ { at: 35, pct: 55, msg: 'Selecting cards and filling slots...' },
+ { at: 50, pct: 65, msg: 'Running smart substitution...' },
+ { at: 70, pct: 78, msg: 'Cross-referencing with collection...' },
+ { at: 90, pct: 88, msg: 'Almost there — finalizing deck...' },
+ { at: 110, pct: 93, msg: 'Still working — large model can take a moment...' },
+ ];
+ let _progressInterval = null;
+ let _progressStart = 0;
+
+ function startProgress() {
+ _progressStart = Date.now();
+ const wrap = $('dg-progress-wrap');
+ const fill = $('dg-progress-fill');
+ const phase = $('dg-progress-phase');
+ const elapsed = $('dg-progress-elapsed');
+ if (!wrap) return;
+ wrap.style.display = 'block';
+ fill.style.width = '0%';
+ phase.textContent = '';
+ elapsed.textContent = '0s elapsed';
+ let lastPhaseIdx = -1;
+ _progressInterval = setInterval(() => {
+ const secs = Math.floor((Date.now() - _progressStart) / 1000);
+ elapsed.textContent = secs + 's elapsed';
+ for (let i = PROGRESS_PHASES.length - 1; i >= 0; i--) {
+ if (secs >= PROGRESS_PHASES[i].at) {
+ fill.style.width = PROGRESS_PHASES[i].pct + '%';
+ if (i !== lastPhaseIdx) {
+ phase.textContent = PROGRESS_PHASES[i].msg;
+ logMessage(PROGRESS_PHASES[i].msg, 'info');
+ lastPhaseIdx = i;
+ }
+ break;
+ }
+ }
+ }, 1000);
+ }
+
+ function stopProgress(success) {
+ if (_progressInterval) {
+ clearInterval(_progressInterval);
+ _progressInterval = null;
+ }
+ const fill = $('dg-progress-fill');
+ const phase = $('dg-progress-phase');
+ const elapsed = $('dg-progress-elapsed');
+ if (fill) fill.style.width = success ? '100%' : '0%';
+ if (phase) phase.textContent = success ? 'Done!' : 'Failed';
+ const secs = Math.floor((Date.now() - _progressStart) / 1000);
+ if (elapsed) elapsed.textContent = secs + 's total';
+ }
+
const $ = (id) => document.getElementById(id);
// ── Init ────────────────────────────────────────────────
@@ -284,6 +343,7 @@ const DeckGenerator = (() => {
logMessage('Building request body with commander: ' + state.commander.name + '', 'info');
setLoading(true, 'Generating deck via Local AI...');
+ startProgress();
const body = buildRequestBody();
logMessage('Sending request to AI model... this may take a moment.', 'info');
@@ -307,10 +367,12 @@ const DeckGenerator = (() => {
const stats = state.previewResult.stats || {};
const cardCount = (stats.total_cards || 0);
logMessage('Deck generated: ' + cardCount + ' cards | $' + (stats.total_price_usd || 0).toFixed(2) + ' estimated', 'success');
+ stopProgress(true);
renderResults(state.previewResult);
toast('Deck generated: ' + (stats.total_cards || 0) + ' cards | $' + (stats.total_price_usd || 0), 'success');
} catch (e) {
+ stopProgress(false);
toast(e.message || 'Generation failed', 'error');
logMessage('Error: ' + (e.message || 'Unknown error'), 'error');
setLoading(false);