Spaces:
Build error
Build error
| """ | |
| Frontend HTML template for the Rossmann Store Sales Predictor. | |
| A clean, modern professional dashboard for making predictions. | |
| """ | |
| FRONTEND_HTML = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Rossmann Sales Analytics | Decision Support</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;600;700&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| :root { | |
| --primary: #3b82f6; | |
| --primary-dark: #2563eb; | |
| --bg-sidebar: #1e293b; | |
| --bg-input: #334155; | |
| --bg-page: #f8fafc; | |
| --text-light: #f1f5f9; | |
| --text-dark: #0f172a; | |
| --text-muted: #64748b; | |
| --success: #10b981; | |
| --danger: #ef4444; | |
| --border: #e2e8f0; | |
| --shadow-sm: 0 4px 6px -1px rgb(0 0 0 / 0.1); | |
| } | |
| * { margin:0; padding:0; box-sizing: border-box; } | |
| body { background: var(--bg-page); color: var(--text-dark); font-family: 'Inter', sans-serif; height: 100vh; overflow: hidden; display: flex; } | |
| /* Sidebar - Dark Professional */ | |
| .sidebar { width: 340px; background: var(--bg-sidebar); padding: 1.5rem; display: flex; flex-direction: column; overflow-y: auto; color: white; border-right: 1px solid rgba(255,255,255,0.1); } | |
| .sidebar-header { margin-bottom: 2rem; } | |
| .sidebar-header h1 { font-family: 'Outfit'; font-size: 1.25rem; background: linear-gradient(90deg, #60a5fa, #3b82f6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } | |
| .param-section { background: rgba(255,255,255,0.05); padding: 1.25rem; border-radius: 16px; margin-bottom: 1.5rem; } | |
| .section-header { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--primary); margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; } | |
| .input-group { margin-bottom: 1rem; } | |
| label { display: block; font-size: 0.75rem; color: #94a3b8; margin-bottom: 0.25rem; font-weight: 500; } | |
| .input-styled { width: 100%; background: var(--bg-input); border: 1px solid rgba(255,255,255,0.05); color: white; padding: 0.6rem 0.75rem; border-radius: 8px; font-size: 0.9rem; transition: 0.2s; } | |
| .input-styled:focus { outline: none; border-color: var(--primary); ring: 2px solid var(--primary); } | |
| .btn-update { width: 100%; background: var(--primary); color: white; border: none; padding: 0.8rem; border-radius: 10px; font-weight: 700; cursor: pointer; transition: 0.2s; margin-top: 1rem; box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); } | |
| .btn-update:hover { background: var(--primary-dark); transform: translateY(-1px); } | |
| /* Main Workspace */ | |
| .main { flex: 1; padding: 2rem; display: flex; flex-direction: column; gap: 1.5rem; overflow-y: auto; } | |
| .header-bar { display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid var(--border); } | |
| .breadcrumb { font-size: 0.85rem; color: var(--text-muted); } | |
| /* Dashboard Grid */ | |
| .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; } | |
| .card { background: white; border-radius: 20px; padding: 1.5rem; box-shadow: var(--shadow-sm); border: 1px solid var(--border); display: flex; flex-direction: column; } | |
| /* KPI Styling */ | |
| .kpi-title { font-size: 0.85rem; color: var(--text-muted); font-weight: 600; margin-bottom: 1rem; } | |
| .kpi-value { font-size: 3.5rem; font-family: 'Outfit'; font-weight: 700; color: #1e293b; margin-bottom: 0.5rem; } | |
| .pill { display: inline-flex; align-items: center; background: #dcfce7; color: #166534; padding: 0.35rem 0.85rem; border-radius: 99px; font-size: 0.75rem; font-weight: 700; } | |
| .pill.blue { background: #dbeafe; color: #1e40af; } | |
| /* Impact Drivers */ | |
| .driver-row { margin-bottom: 1rem; } | |
| .driver-header { display: flex; justify-content: space-between; margin-bottom: 0.35rem; font-size: 0.8rem; font-weight: 600; } | |
| .progress-bar-bg { height: 8px; background: #f1f5f9; border-radius: 4px; overflow: hidden; } | |
| .progress-bar-fill { height: 100%; border-radius: 4px; transition: 1s cubic-bezier(0.18, 0.89, 0.32, 1.28); } | |
| /* Chart Canvas */ | |
| .chart-container { flex: 1; min-height: 400px; position: relative; margin-top: 1rem; } | |
| /* Hidden initially */ | |
| #dashboard-grid { display: none; } | |
| .empty-state { height: 80vh; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--text-muted); text-align: center; } | |
| #loading-overlay { display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.7); backdrop-filter: blur(4px); z-index: 1000; align-items: center; justify-content: center; color: white; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="loading-overlay"> | |
| <div style="text-align: center;"> | |
| <div style="width: 40px; height: 40px; border: 4px solid var(--primary); border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 1rem;"></div> | |
| <p>Processing Revenue Models...</p> | |
| </div> | |
| </div> | |
| <aside class="sidebar"> | |
| <div class="sidebar-header"> | |
| <h1>Rossmann Analytics</h1> | |
| <p style="font-size: 0.7rem; color: #64748b; margin-top: 5px;">AI-POWERED REVENUE FORECASTING</p> | |
| </div> | |
| <form id="prediction-form"> | |
| <div class="param-section"> | |
| <div class="section-header"><span></span> Parameter Context</div> | |
| <div class="input-group"> | |
| <label>Store Identifier</label> | |
| <input type="number" id="store_id" class="input-styled" value="1" min="1" max="1115"> | |
| </div> | |
| <div class="input-group"> | |
| <label>Forecast Start Date</label> | |
| <input type="date" id="start_date" class="input-styled"> | |
| </div> | |
| <div class="input-group"> | |
| <label>Prediction Horizon</label> | |
| <select id="horizon" class="input-styled"> | |
| <option value="7" selected>7-Day Outlook</option> | |
| <option value="14">14-Day Outlook</option> | |
| <option value="30">30-Day Outlook</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="param-section"> | |
| <div class="section-header"><span></span> Operational Shifts</div> | |
| <div class="input-group"> | |
| <label>Promotion Active (Y/N)</label> | |
| <select id="promo" class="input-styled"> | |
| <option value="1" selected>Active Sales Promo</option> | |
| <option value="0">No Promotion</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label>State Holiday Context</label> | |
| <select id="state_holiday" class="input-styled"> | |
| <option value="0" selected>Normal Operations</option> | |
| <option value="a">Public Holiday</option> | |
| <option value="b">Easter Period</option> | |
| <option value="c">Christmas Period</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label>School Holiday Active</label> | |
| <select id="school_holiday" class="input-styled"> | |
| <option value="0">No</option> | |
| <option value="1">Yes</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="param-section"> | |
| <div class="section-header"><span></span> Store Profile</div> | |
| <div class="input-group"> | |
| <label>Competitor Distance (m)</label> | |
| <input type="number" id="comp_dist" class="input-styled" value="1270"> | |
| </div> | |
| <div class="input-group"> | |
| <label>Store Category</label> | |
| <select id="store_type" class="input-styled"> | |
| <option value="a">Type A</option> | |
| <option value="b">Type B</option> | |
| <option value="c">Type C</option> | |
| <option value="d">Type D</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label>Assortment Level</label> | |
| <select id="assortment" class="input-styled"> | |
| <option value="a">Basic</option> | |
| <option value="b">Extra</option> | |
| <option value="c">Extended</option> | |
| </select> | |
| </div> | |
| </div> | |
| <button type="submit" class="btn-update">Update Prediction</button> | |
| </form> | |
| </aside> | |
| <main class="main"> | |
| <div class="header-bar"> | |
| <div> | |
| <span class="breadcrumb">Store Analytics / Forecast Dashboard</span> | |
| <h2 style="font-family: Outfit; font-size: 1.5rem; margin-top: 5px;" id="page-title">Real-time Revenue Outlook</h2> | |
| </div> | |
| <div style="display: flex; gap: 1rem;"> | |
| <!-- Extra UI icons/buttons could go here --> | |
| </div> | |
| </div> | |
| <div id="welcome-view" class="empty-state"> | |
| <div style="background: white; padding: 3rem; border-radius: 30px; border: 2px dashed #e2e8f0; max-width: 500px;"> | |
| <span style="font-size: 3rem;">🚀</span> | |
| <h2 style="margin: 1rem 0;">Ready to Forecast</h2> | |
| <p>Configure prediction parameters in the sidebar to generate a high-precision sales outlook based on current store constraints.</p> | |
| </div> | |
| </div> | |
| <div id="dashboard-grid"> | |
| <div class="grid"> | |
| <!-- Sales KPI Card --> | |
| <div class="card" style="grid-column: span 1.5;"> | |
| <span class="kpi-title">PROJECTED FIRST-DAY REVENUE</span> | |
| <h2 class="kpi-value" id="kpi-sales">€0</h2> | |
| <div> | |
| <span class="pill blue" id="ci-pill">95% Confidence Interval: €0 - €0</span> | |
| </div> | |
| <div style="margin-top: auto; padding-top: 1rem; color: var(--text-muted); font-size: 0.8rem; border-top: 1px solid #f1f5f9;"> | |
| Confidence intervals calculated via historical model precision (RMSPE). | |
| </div> | |
| </div> | |
| <!-- Drivers Card --> | |
| <div class="card" style="grid-column: span 1.5;"> | |
| <span class="kpi-title">IMPACT DRIVERS (TOP FACTORS)</span> | |
| <div id="drivers-container"> | |
| <!-- Populated by JS --> | |
| </div> | |
| </div> | |
| <!-- Chart Card --> | |
| <div class="card" style="grid-column: span 3;"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> | |
| <span class="kpi-title">SALES FORECAST TREND (WITH CONFIDENCE BAND)</span> | |
| </div> | |
| <div class="chart-container"> | |
| <canvas id="forecast-chart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| // Set default date to today | |
| document.getElementById('start_date').valueAsDate = new Date(); | |
| let forecastChart = null; | |
| // Form handler | |
| document.getElementById('prediction-form').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| document.getElementById('loading-overlay').style.display = 'flex'; | |
| const payload = { | |
| Store: parseInt(document.getElementById('store_id').value), | |
| Date: document.getElementById('start_date').value, | |
| Promo: parseInt(document.getElementById('promo').value), | |
| StateHoliday: document.getElementById('state_holiday').value, | |
| SchoolHoliday: parseInt(document.getElementById('school_holiday').value), | |
| Assortment: document.getElementById('assortment').value, | |
| StoreType: document.getElementById('store_type').value, | |
| CompetitionDistance: parseInt(document.getElementById('comp_dist').value) || 0, | |
| ForecastDays: parseInt(document.getElementById('horizon').value) | |
| }; | |
| try { | |
| const response = await fetch('/predict', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| const data = await response.json(); | |
| console.log("Prediction Result:", data); | |
| // Switch views | |
| document.getElementById('welcome-view').style.display = 'none'; | |
| document.getElementById('dashboard-grid').style.display = 'block'; | |
| // Update KPI | |
| document.getElementById('kpi-sales').textContent = '€' + data.PredictedSales.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}); | |
| document.getElementById('ci-pill').textContent = `95% Confidence Interval: €${data.ConfidenceInterval[0].toLocaleString()} - €${data.ConfidenceInterval[1].toLocaleString()}`; | |
| document.getElementById('page-title').textContent = `Store ${payload.Store} Outlook`; | |
| // Update Drivers | |
| const driversContainer = document.getElementById('drivers-container'); | |
| driversContainer.innerHTML = ''; | |
| data.Explanation.forEach(item => { | |
| const absImpact = Math.abs(item.impact); | |
| const color = item.impact >= 0 ? 'var(--primary)' : 'var(--danger)'; | |
| const row = document.createElement('div'); | |
| row.className = 'driver-row'; | |
| row.innerHTML = ` | |
| <div class="driver-header"> | |
| <span>${item.feature}</span> | |
| <span style="color:${color}">${item.impact >= 0 ? '+' : ''}${item.impact.toFixed(1)}%</span> | |
| </div> | |
| <div class="progress-bar-bg"> | |
| <div class="progress-bar-fill" style="width: ${Math.min(absImpact * 1.5, 100)}%; background: ${color};"></div> | |
| </div> | |
| `; | |
| driversContainer.appendChild(row); | |
| }); | |
| // Update Chart | |
| updateChart(data.Forecast); | |
| } catch (err) { | |
| console.error(err); | |
| alert("Internal Engine Error: Could not generate prediction. Check console."); | |
| } finally { | |
| document.getElementById('loading-overlay').style.display = 'none'; | |
| } | |
| }); | |
| function updateChart(forecast) { | |
| const ctx = document.getElementById('forecast-chart').getContext('2d'); | |
| const labels = forecast.map(f => new Date(f.date).toLocaleDateString(undefined, {month: 'short', day: 'numeric'})); | |
| const sales = forecast.map(f => f.sales); | |
| const lbs = forecast.map(f => f.lb); | |
| const ubs = forecast.map(f => f.ub); | |
| if (forecastChart) forecastChart.destroy(); | |
| forecastChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: labels, | |
| datasets: [ | |
| { | |
| label: 'Projection', | |
| data: sales, | |
| borderColor: '#3b82f6', | |
| borderWidth: 4, | |
| pointRadius: 4, | |
| pointBackgroundColor: '#fff', | |
| pointBorderColor: '#3b82f6', | |
| pointBorderWidth: 2, | |
| tension: 0.3, | |
| fill: false, | |
| zIndex: 10 | |
| }, | |
| { | |
| label: 'Upper Bound', | |
| data: ubs, | |
| borderColor: 'transparent', | |
| backgroundColor: 'rgba(59, 130, 246, 0.1)', | |
| fill: '+1', // Fill to lower bound | |
| pointRadius: 0, | |
| tension: 0.3 | |
| }, | |
| { | |
| label: 'Lower Bound', | |
| data: lbs, | |
| borderColor: 'transparent', | |
| backgroundColor: 'rgba(59, 130, 246, 0.1)', | |
| fill: false, | |
| pointRadius: 0, | |
| tension: 0.3 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| interaction: { mode: 'index', intersect: false }, | |
| plugins: { | |
| legend: { display: false }, | |
| tooltip: { | |
| backgroundColor: '#0f172a', | |
| padding: 12, | |
| cornerRadius: 8, | |
| titleFont: { size: 14, weight: 'bold' }, | |
| bodyFont: { size: 14 }, | |
| callbacks: { | |
| label: function(context) { | |
| if (context.datasetIndex === 0) { | |
| return 'Revenue: €' + context.raw.toLocaleString(undefined, {minimumFractionDigits: 2}); | |
| } | |
| return null; | |
| } | |
| } | |
| } | |
| }, | |
| scales: { | |
| x: { grid: { display: false } }, | |
| y: { | |
| ticks: { | |
| callback: value => '€' + value.toLocaleString() | |
| }, | |
| grid: { color: '#f1f5f9' } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // CSS Animation | |
| const styleSheet = document.createElement("style"); | |
| styleSheet.innerText = `@keyframes spin { to { transform: rotate(360deg); } }`; | |
| document.head.appendChild(styleSheet); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |