Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SAP AR ML Demo</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/tensorflow/4.10.0/tf.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| color: #333; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| color: white; | |
| } | |
| .header h1 { | |
| font-size: 2.5rem; | |
| margin-bottom: 10px; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
| } | |
| .header p { | |
| font-size: 1.1rem; | |
| opacity: 0.9; | |
| } | |
| .dashboard { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| } | |
| .card { | |
| background: rgba(255, 255, 255, 0.95); | |
| border-radius: 15px; | |
| padding: 25px; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .card h3 { | |
| color: #4a5568; | |
| margin-bottom: 15px; | |
| font-size: 1.3rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .btn { | |
| background: linear-gradient(135deg, #4CAF50, #45a049); | |
| color: white; | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3); | |
| } | |
| .btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4); | |
| } | |
| .btn:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, #007bff, #0056b3); | |
| box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3); | |
| } | |
| .btn-primary:hover { | |
| box-shadow: 0 6px 20px rgba(0, 123, 255, 0.4); | |
| } | |
| .status { | |
| padding: 10px 15px; | |
| border-radius: 8px; | |
| margin: 10px 0; | |
| font-weight: 500; | |
| } | |
| .status.success { | |
| background: #d4edda; | |
| color: #155724; | |
| border: 1px solid #c3e6cb; | |
| } | |
| .status.info { | |
| background: #d1ecf1; | |
| color: #0c5460; | |
| border: 1px solid #bee5eb; | |
| } | |
| .status.warning { | |
| background: #fff3cd; | |
| color: #856404; | |
| border: 1px solid #ffeaa7; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 20px; | |
| background: #e9ecef; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| margin: 10px 0; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #4CAF50, #45a049); | |
| transition: width 0.3s ease; | |
| border-radius: 10px; | |
| } | |
| .invoice-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-top: 15px; | |
| } | |
| .invoice-table th, | |
| .invoice-table td { | |
| padding: 12px; | |
| text-align: left; | |
| border-bottom: 1px solid #e9ecef; | |
| } | |
| .invoice-table th { | |
| background: #f8f9fa; | |
| font-weight: 600; | |
| color: #495057; | |
| } | |
| .invoice-table tr:hover { | |
| background: #f8f9fa; | |
| } | |
| .prediction { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .probability-bar { | |
| flex: 1; | |
| height: 20px; | |
| background: #e9ecef; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .probability-fill { | |
| height: 100%; | |
| border-radius: 10px; | |
| transition: width 0.3s ease; | |
| } | |
| .high-prob { | |
| background: linear-gradient(90deg, #28a745, #20c997); | |
| } | |
| .medium-prob { | |
| background: linear-gradient(90deg, #ffc107, #fd7e14); | |
| } | |
| .low-prob { | |
| background: linear-gradient(90deg, #dc3545, #e74c3c); | |
| } | |
| .full-width { | |
| grid-column: 1 / -1; | |
| } | |
| .metrics { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| margin-top: 15px; | |
| } | |
| .metric-card { | |
| background: #f8f9fa; | |
| padding: 15px; | |
| border-radius: 10px; | |
| text-align: center; | |
| } | |
| .metric-value { | |
| font-size: 2rem; | |
| font-weight: bold; | |
| color: #007bff; | |
| margin-bottom: 5px; | |
| } | |
| .metric-label { | |
| color: #6c757d; | |
| font-size: 0.9rem; | |
| } | |
| .chart-container { | |
| width: 100%; | |
| height: 300px; | |
| margin-top: 20px; | |
| } | |
| .loading { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid #f3f3f3; | |
| border-top: 3px solid #3498db; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .icon { | |
| width: 20px; | |
| height: 20px; | |
| display: inline-block; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🏢 SAP Account Receivable ML Prediction Demo</h1> | |
| <p>Machine Learning-powered invoice payment prediction system</p> | |
| </div> | |
| <div class="dashboard"> | |
| <div class="card"> | |
| <h3> | |
| 🎯 Model Training | |
| </h3> | |
| <p>Train a machine learning model on synthetic SAP AR data to predict invoice payment likelihood.</p> | |
| <button id="trainBtn" class="btn" onclick="trainModel()"> | |
| <span id="trainBtnText">Train ML Model</span> | |
| </button> | |
| <div id="trainingStatus"></div> | |
| <div id="trainingProgress"></div> | |
| <div id="modelMetrics" class="metrics" style="display: none;"> | |
| <div class="metric-card"> | |
| <div class="metric-value" id="accuracy">-</div> | |
| <div class="metric-label">Accuracy</div> | |
| </div> | |
| <div class="metric-card"> | |
| <div class="metric-value" id="precision">-</div> | |
| <div class="metric-label">Precision</div> | |
| </div> | |
| <div class="metric-card"> | |
| <div class="metric-value" id="recall">-</div> | |
| <div class="metric-label">Recall</div> | |
| </div> | |
| <div class="metric-card"> | |
| <div class="metric-value" id="f1Score">-</div> | |
| <div class="metric-label">F1 Score</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3> | |
| 📊 Training Visualization | |
| </h3> | |
| <div class="chart-container"> | |
| <canvas id="trainingChart" width="400" height="200"></canvas> | |
| </div> | |
| </div> | |
| <div class="card full-width"> | |
| <h3> | |
| 🔮 Invoice Payment Predictions | |
| </h3> | |
| <p>Real-time predictions for unpaid invoices using the trained ML model.</p> | |
| <button id="predictBtn" class="btn btn-primary" onclick="makePredictions()" disabled> | |
| Generate Predictions | |
| </button> | |
| <div id="predictionsTable"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let model = null; | |
| let trainingData = null; | |
| let chart = null; | |
| let unpaidInvoices = []; | |
| // Initialize chart | |
| const ctx = document.getElementById('trainingChart').getContext('2d'); | |
| chart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: [], | |
| datasets: [{ | |
| label: 'Training Accuracy', | |
| data: [], | |
| borderColor: '#007bff', | |
| backgroundColor: 'rgba(0, 123, 255, 0.1)', | |
| tension: 0.4 | |
| }, { | |
| label: 'Training Loss', | |
| data: [], | |
| borderColor: '#dc3545', | |
| backgroundColor: 'rgba(220, 53, 69, 0.1)', | |
| tension: 0.4, | |
| yAxisID: 'y1' | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| type: 'linear', | |
| display: true, | |
| position: 'left', | |
| min: 0, | |
| max: 1 | |
| }, | |
| y1: { | |
| type: 'linear', | |
| display: true, | |
| position: 'right', | |
| min: 0, | |
| grid: { | |
| drawOnChartArea: false, | |
| }, | |
| } | |
| } | |
| } | |
| }); | |
| function generateSyntheticData() { | |
| const data = []; | |
| const customers = ['CUST001', 'CUST002', 'CUST003', 'CUST004', 'CUST005', 'CUST006', 'CUST007', 'CUST008']; | |
| for (let i = 0; i < 1000; i++) { | |
| const invoiceAmount = Math.random() * 50000 + 1000; | |
| const customerCode = customers[Math.floor(Math.random() * customers.length)]; | |
| const daysOverdue = Math.floor(Math.random() * 120); | |
| const previousDelays = Math.floor(Math.random() * 5); | |
| const creditScore = Math.random() * 100; | |
| const industryRisk = Math.random(); | |
| const seasonality = Math.sin((i % 365) * 2 * Math.PI / 365); | |
| // Create correlation between features and payment probability | |
| let paymentProb = 0.7; | |
| paymentProb -= Math.min(daysOverdue / 100, 0.4); | |
| paymentProb -= Math.min(previousDelays / 10, 0.3); | |
| paymentProb += (creditScore - 50) / 200; | |
| paymentProb -= industryRisk * 0.2; | |
| paymentProb += seasonality * 0.1; | |
| paymentProb = Math.max(0.05, Math.min(0.95, paymentProb)); | |
| const paidOnTime = Math.random() < paymentProb ? 1 : 0; | |
| data.push({ | |
| invoiceAmount: invoiceAmount / 50000, // Normalize | |
| daysOverdue: daysOverdue / 120, // Normalize | |
| previousDelays: previousDelays / 5, // Normalize | |
| creditScore: creditScore / 100, // Already normalized | |
| industryRisk: industryRisk, | |
| seasonality: (seasonality + 1) / 2, // Normalize to 0-1 | |
| paidOnTime: paidOnTime | |
| }); | |
| } | |
| return data; | |
| } | |
| function generateUnpaidInvoices() { | |
| const invoices = []; | |
| const customers = ['SAP-CUST001', 'SAP-CUST002', 'SAP-CUST003', 'SAP-CUST004', 'SAP-CUST005']; | |
| for (let i = 0; i < 15; i++) { | |
| const invoiceId = `INV-${Date.now()}-${i.toString().padStart(3, '0')}`; | |
| const customer = customers[Math.floor(Math.random() * customers.length)]; | |
| const amount = Math.floor(Math.random() * 45000 + 5000); | |
| const daysOverdue = Math.floor(Math.random() * 90); | |
| const previousDelays = Math.floor(Math.random() * 4); | |
| const creditScore = Math.floor(Math.random() * 60 + 40); | |
| invoices.push({ | |
| invoiceId, | |
| customer, | |
| amount, | |
| daysOverdue, | |
| previousDelays, | |
| creditScore, | |
| industryRisk: Math.random(), | |
| seasonality: Math.random() | |
| }); | |
| } | |
| return invoices; | |
| } | |
| async function trainModel() { | |
| const trainBtn = document.getElementById('trainBtn'); | |
| const trainBtnText = document.getElementById('trainBtnText'); | |
| const statusDiv = document.getElementById('trainingStatus'); | |
| const progressDiv = document.getElementById('trainingProgress'); | |
| trainBtn.disabled = true; | |
| trainBtnText.innerHTML = '<span class="loading"></span> Training...'; | |
| try { | |
| // Show initial status | |
| statusDiv.innerHTML = '<div class="status info">🔄 Generating synthetic SAP AR data...</div>'; | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| // Generate training data | |
| trainingData = generateSyntheticData(); | |
| statusDiv.innerHTML = '<div class="status success">✅ Generated 1,000 synthetic invoice records</div>'; | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| statusDiv.innerHTML += '<div class="status info">🧠 Building neural network model...</div>'; | |
| // Prepare data for TensorFlow | |
| const features = trainingData.map(d => [ | |
| d.invoiceAmount, d.daysOverdue, d.previousDelays, | |
| d.creditScore, d.industryRisk, d.seasonality | |
| ]); | |
| const labels = trainingData.map(d => d.paidOnTime); | |
| const xs = tf.tensor2d(features); | |
| const ys = tf.tensor1d(labels); | |
| // Create model | |
| model = tf.sequential({ | |
| layers: [ | |
| tf.layers.dense({ | |
| inputShape: [6], | |
| units: 32, | |
| activation: 'relu' | |
| }), | |
| tf.layers.dropout({rate: 0.2}), | |
| tf.layers.dense({ | |
| units: 16, | |
| activation: 'relu' | |
| }), | |
| tf.layers.dropout({rate: 0.2}), | |
| tf.layers.dense({ | |
| units: 1, | |
| activation: 'sigmoid' | |
| }) | |
| ] | |
| }); | |
| model.compile({ | |
| optimizer: tf.train.adam(0.001), | |
| loss: 'binaryCrossentropy', | |
| metrics: ['accuracy'] | |
| }); | |
| statusDiv.innerHTML += '<div class="status info">🎯 Training model with backpropagation...</div>'; | |
| // Show progress bar | |
| progressDiv.innerHTML = ` | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="progressFill" style="width: 0%"></div> | |
| </div> | |
| <div id="progressText">Training Progress: 0%</div> | |
| `; | |
| // Train model with callbacks | |
| const history = await model.fit(xs, ys, { | |
| epochs: 50, | |
| batchSize: 32, | |
| validationSplit: 0.2, | |
| callbacks: { | |
| onEpochEnd: (epoch, logs) => { | |
| const progress = ((epoch + 1) / 50) * 100; | |
| document.getElementById('progressFill').style.width = `${progress}%`; | |
| document.getElementById('progressText').textContent = `Training Progress: ${Math.round(progress)}% - Accuracy: ${(logs.acc * 100).toFixed(1)}%`; | |
| // Update chart | |
| chart.data.labels.push(epoch + 1); | |
| chart.data.datasets[0].data.push(logs.acc); | |
| chart.data.datasets[1].data.push(logs.loss); | |
| chart.update('none'); | |
| } | |
| } | |
| }); | |
| // Calculate final metrics | |
| const finalAccuracy = history.history.acc[history.history.acc.length - 1]; | |
| const finalLoss = history.history.loss[history.history.loss.length - 1]; | |
| // Simulate precision, recall, F1 (normally would calculate from validation set) | |
| const precision = Math.min(0.95, finalAccuracy + Math.random() * 0.1 - 0.05); | |
| const recall = Math.min(0.95, finalAccuracy + Math.random() * 0.1 - 0.05); | |
| const f1Score = 2 * (precision * recall) / (precision + recall); | |
| // Update metrics display | |
| document.getElementById('accuracy').textContent = (finalAccuracy * 100).toFixed(1) + '%'; | |
| document.getElementById('precision').textContent = (precision * 100).toFixed(1) + '%'; | |
| document.getElementById('recall').textContent = (recall * 100).toFixed(1) + '%'; | |
| document.getElementById('f1Score').textContent = (f1Score * 100).toFixed(1) + '%'; | |
| document.getElementById('modelMetrics').style.display = 'grid'; | |
| statusDiv.innerHTML += '<div class="status success">🎉 Model training completed successfully!</div>'; | |
| // Generate unpaid invoices for prediction | |
| unpaidInvoices = generateUnpaidInvoices(); | |
| // Enable prediction button | |
| document.getElementById('predictBtn').disabled = false; | |
| // Cleanup tensors | |
| xs.dispose(); | |
| ys.dispose(); | |
| } catch (error) { | |
| statusDiv.innerHTML += `<div class="status warning">❌ Training failed: ${error.message}</div>`; | |
| } finally { | |
| trainBtn.disabled = false; | |
| trainBtnText.textContent = 'Retrain Model'; | |
| } | |
| } | |
| async function makePredictions() { | |
| if (!model || unpaidInvoices.length === 0) return; | |
| const tableDiv = document.getElementById('predictionsTable'); | |
| tableDiv.innerHTML = '<div class="status info">🔮 Generating predictions...</div>'; | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| // Prepare features for prediction | |
| const features = unpaidInvoices.map(invoice => [ | |
| invoice.amount / 50000, // Normalize | |
| invoice.daysOverdue / 120, // Normalize | |
| invoice.previousDelays / 5, // Normalize | |
| invoice.creditScore / 100, // Normalize | |
| invoice.industryRisk, | |
| invoice.seasonality | |
| ]); | |
| const predictionTensor = tf.tensor2d(features); | |
| const predictions = await model.predict(predictionTensor).data(); | |
| predictionTensor.dispose(); | |
| // Create table | |
| let tableHTML = ` | |
| <table class="invoice-table"> | |
| <thead> | |
| <tr> | |
| <th>Invoice ID</th> | |
| <th>Customer</th> | |
| <th>Amount</th> | |
| <th>Days Overdue</th> | |
| <th>Credit Score</th> | |
| <th>Payment Prediction</th> | |
| <th>Probability</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| `; | |
| unpaidInvoices.forEach((invoice, index) => { | |
| const probability = predictions[index]; | |
| const willPay = probability > 0.5; | |
| const probClass = probability > 0.7 ? 'high-prob' : probability > 0.4 ? 'medium-prob' : 'low-prob'; | |
| tableHTML += ` | |
| <tr> | |
| <td><strong>${invoice.invoiceId}</strong></td> | |
| <td>${invoice.customer}</td> | |
| <td>$${invoice.amount.toLocaleString()}</td> | |
| <td>${invoice.daysOverdue} days</td> | |
| <td>${invoice.creditScore}/100</td> | |
| <td> | |
| <span style="color: ${willPay ? '#28a745' : '#dc3545'}; font-weight: bold;"> | |
| ${willPay ? '✅ Will Pay' : '❌ Risk of Default'} | |
| </span> | |
| </td> | |
| <td> | |
| <div class="prediction"> | |
| <div class="probability-bar"> | |
| <div class="probability-fill ${probClass}" style="width: ${probability * 100}%"></div> | |
| </div> | |
| <span style="font-weight: bold; min-width: 50px;"> | |
| ${(probability * 100).toFixed(1)}% | |
| </span> | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| }); | |
| tableHTML += '</tbody></table>'; | |
| tableDiv.innerHTML = tableHTML; | |
| } | |
| </script> | |
| </body> | |
| </html> |