Add progress overlay to AI Catalog Price Check
Shows a modal overlay with animated progress bar and batch-aware status messages while Claude is analyzing. Progress animates in two phases: ease-out to ~85% over the estimated duration, then a slow crawl to 99% so it never falsely "completes" before the server responds. - Overlay driven by CSS (hidden until .active added by JS) - Item count passed from controller as data-item-count on the run button - Batch count derived from item count (batches of 25) to show accurate "Analyzing batch N of M…" messages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -938,6 +938,9 @@ namespace PowderCoating.Web.Controllers
|
||||
r => r.CompanyId == currentUser.CompanyId);
|
||||
var report = existing.OrderByDescending(r => r.RunAt).FirstOrDefault();
|
||||
|
||||
var activeItems = await _unitOfWork.CatalogItems.FindAsync(ci => ci.IsActive);
|
||||
ViewBag.ActiveItemCount = activeItems.Count();
|
||||
|
||||
CatalogPriceCheckReportDto? dto = null;
|
||||
if (report != null)
|
||||
{
|
||||
|
||||
@@ -35,16 +35,69 @@
|
||||
.summary-stat .num { font-size: 2rem; font-weight: 700; line-height: 1; }
|
||||
.summary-stat .lbl { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; }
|
||||
.run-btn-wrap { min-height: 3rem; }
|
||||
|
||||
/* Progress overlay */
|
||||
#price-check-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
z-index: 1050;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#price-check-overlay.active { display: flex; }
|
||||
.progress-card {
|
||||
background: #fff;
|
||||
border-radius: 1rem;
|
||||
padding: 2.5rem 2rem;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.25);
|
||||
}
|
||||
.progress-card .icon { font-size: 3rem; color: #4f46e5; margin-bottom: 1rem; }
|
||||
.progress-card h5 { font-weight: 700; margin-bottom: 0.25rem; }
|
||||
.progress-card .status-msg { font-size: 0.9rem; color: #64748b; min-height: 1.4em; margin-bottom: 1.25rem; }
|
||||
.progress-bar-track {
|
||||
height: 8px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 99px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4f46e5, #7c3aed);
|
||||
border-radius: 99px;
|
||||
width: 0%;
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
.progress-card .pct-label { font-size: 0.8rem; color: #94a3b8; }
|
||||
</style>
|
||||
}
|
||||
|
||||
<!-- Progress overlay (shown while AI is running) -->
|
||||
<div id="price-check-overlay">
|
||||
<div class="progress-card">
|
||||
<div class="icon"><i class="bi bi-robot"></i></div>
|
||||
<h5>Analyzing your catalog</h5>
|
||||
<p class="status-msg" id="overlay-status">Preparing items…</p>
|
||||
<div class="progress-bar-track">
|
||||
<div class="progress-bar-fill" id="overlay-bar"></div>
|
||||
</div>
|
||||
<div class="pct-label"><span id="overlay-pct">0</span>% complete</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i> Back to Catalog
|
||||
</a>
|
||||
<form asp-action="RunAiPriceCheck" method="post" id="runForm" class="run-btn-wrap">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit" class="btn btn-primary" id="runBtn">
|
||||
<button type="submit" class="btn btn-primary" id="runBtn"
|
||||
data-item-count="@(ViewBag.ActiveItemCount ?? 0)">
|
||||
<i class="bi bi-robot me-2"></i>
|
||||
@(Model == null ? "Run Price Check" : "Re-run Price Check")
|
||||
</button>
|
||||
|
||||
@@ -3,10 +3,83 @@
|
||||
|
||||
var form = document.getElementById('runForm');
|
||||
var btn = document.getElementById('runBtn');
|
||||
if (!form || !btn) return;
|
||||
var overlay = document.getElementById('price-check-overlay');
|
||||
var bar = document.getElementById('overlay-bar');
|
||||
var pctLabel = document.getElementById('overlay-pct');
|
||||
var statusMsg = document.getElementById('overlay-status');
|
||||
|
||||
if (!form || !btn || !overlay) return;
|
||||
|
||||
// Estimate total seconds based on item count (roughly 12s per batch of 25, min 15s).
|
||||
function estimateDuration(itemCount) {
|
||||
var batches = Math.max(1, Math.ceil(itemCount / 25));
|
||||
return Math.max(15, batches * 12);
|
||||
}
|
||||
|
||||
// Messages keyed to approximate progress milestones (0–100).
|
||||
function messageAt(pct, batchCount) {
|
||||
if (pct < 10) return 'Loading catalog items…';
|
||||
if (pct < 20) return 'Sending items to Claude…';
|
||||
if (batchCount <= 1) {
|
||||
if (pct < 75) return 'Analyzing your catalog with AI…';
|
||||
if (pct < 92) return 'Reviewing pricing data…';
|
||||
} else {
|
||||
var batchDone = Math.floor((pct / 100) * batchCount);
|
||||
if (batchDone < batchCount) {
|
||||
return 'Analyzing batch ' + (batchDone + 1) + ' of ' + batchCount + '…';
|
||||
}
|
||||
}
|
||||
if (pct < 97) return 'Compiling results…';
|
||||
return 'Almost done…';
|
||||
}
|
||||
|
||||
function setProgress(pct, batchCount) {
|
||||
var clamped = Math.min(99, Math.max(0, pct));
|
||||
bar.style.width = clamped + '%';
|
||||
pctLabel.textContent = Math.round(clamped);
|
||||
statusMsg.textContent = messageAt(clamped, batchCount);
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function () {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Analyzing with AI…';
|
||||
|
||||
var itemCount = parseInt(btn.getAttribute('data-item-count') || '0', 10);
|
||||
var batchCount = Math.max(1, Math.ceil(itemCount / 25));
|
||||
var totalSecs = estimateDuration(itemCount);
|
||||
|
||||
overlay.classList.add('active');
|
||||
setProgress(0, batchCount);
|
||||
|
||||
// Animate progress: fast to ~85%, then slow crawl toward 99%.
|
||||
// Uses two easing phases so it never "finishes" before the server responds.
|
||||
var start = Date.now();
|
||||
var phase1End = totalSecs * 0.80 * 1000; // 80% of time → 85% progress
|
||||
var raf;
|
||||
|
||||
function tick() {
|
||||
var elapsed = Date.now() - start;
|
||||
var pct;
|
||||
|
||||
if (elapsed < phase1End) {
|
||||
// Phase 1: ease-out from 0 → 85
|
||||
var t = elapsed / phase1End;
|
||||
pct = 85 * (1 - Math.pow(1 - t, 2));
|
||||
} else {
|
||||
// Phase 2: slow crawl 85 → 99 (never quite reaches 99)
|
||||
var t2 = (elapsed - phase1End) / (totalSecs * 1000);
|
||||
pct = 85 + 14 * (1 - Math.exp(-t2 * 1.5));
|
||||
}
|
||||
|
||||
setProgress(pct, batchCount);
|
||||
raf = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
raf = requestAnimationFrame(tick);
|
||||
|
||||
// Safety: if the server takes much longer than estimated, keep the overlay up.
|
||||
// The page navigation itself will tear it down.
|
||||
window.addEventListener('pagehide', function () {
|
||||
cancelAnimationFrame(raf);
|
||||
});
|
||||
});
|
||||
}());
|
||||
|
||||
Reference in New Issue
Block a user