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> |