796 lines
22 KiB
HTML
796 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Plaid Local Learning Demo - RuVector Edge</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0a0a0f;
|
||
--card: #12121a;
|
||
--border: #2a2a3a;
|
||
--text: #e0e0e8;
|
||
--text-dim: #8888a0;
|
||
--accent: #6366f1;
|
||
--accent-glow: rgba(99, 102, 241, 0.3);
|
||
--success: #22c55e;
|
||
--warning: #f59e0b;
|
||
--error: #ef4444;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 2rem;
|
||
}
|
||
|
||
header {
|
||
text-align: center;
|
||
margin-bottom: 3rem;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 2.5rem;
|
||
background: linear-gradient(135deg, var(--accent), #a855f7);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.subtitle {
|
||
color: var(--text-dim);
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.privacy-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
background: rgba(34, 197, 94, 0.1);
|
||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||
color: var(--success);
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 2rem;
|
||
margin-top: 1rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||
gap: 1.5rem;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.card {
|
||
background: var(--card);
|
||
border: 1px solid var(--border);
|
||
border-radius: 1rem;
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.card h2 {
|
||
font-size: 1.2rem;
|
||
margin-bottom: 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 1rem;
|
||
}
|
||
|
||
.stat {
|
||
background: rgba(99, 102, 241, 0.05);
|
||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||
border-radius: 0.5rem;
|
||
padding: 1rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 1.8rem;
|
||
font-weight: 700;
|
||
color: var(--accent);
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.8rem;
|
||
color: var(--text-dim);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
button {
|
||
background: var(--accent);
|
||
color: white;
|
||
border: none;
|
||
padding: 0.75rem 1.5rem;
|
||
border-radius: 0.5rem;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
button:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 20px var(--accent-glow);
|
||
}
|
||
|
||
button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
button.secondary {
|
||
background: transparent;
|
||
border: 1px solid var(--border);
|
||
color: var(--text);
|
||
}
|
||
|
||
button.danger {
|
||
background: var(--error);
|
||
}
|
||
|
||
.patterns-list {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.pattern-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.75rem;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.pattern-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.pattern-category {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.pattern-amount {
|
||
color: var(--accent);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.confidence-bar {
|
||
width: 60px;
|
||
height: 4px;
|
||
background: var(--border);
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.confidence-fill {
|
||
height: 100%;
|
||
background: var(--accent);
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
.heatmap {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
gap: 4px;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.heatmap-cell {
|
||
aspect-ratio: 1;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0.7rem;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.heatmap-label {
|
||
font-size: 0.7rem;
|
||
color: var(--text-dim);
|
||
text-align: center;
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
.transaction-form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1rem;
|
||
}
|
||
|
||
input, select {
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
color: var(--text);
|
||
padding: 0.75rem;
|
||
border-radius: 0.5rem;
|
||
font-size: 1rem;
|
||
width: 100%;
|
||
}
|
||
|
||
input:focus, select:focus {
|
||
outline: none;
|
||
border-color: var(--accent);
|
||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
font-size: 0.9rem;
|
||
color: var(--text-dim);
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.result-card {
|
||
background: rgba(99, 102, 241, 0.05);
|
||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||
border-radius: 0.5rem;
|
||
padding: 1rem;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.result-card.anomaly {
|
||
background: rgba(239, 68, 68, 0.05);
|
||
border-color: rgba(239, 68, 68, 0.3);
|
||
}
|
||
|
||
.result-card.normal {
|
||
background: rgba(34, 197, 94, 0.05);
|
||
border-color: rgba(34, 197, 94, 0.3);
|
||
}
|
||
|
||
.log {
|
||
background: var(--bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 0.5rem;
|
||
padding: 1rem;
|
||
font-family: 'Fira Code', monospace;
|
||
font-size: 0.85rem;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.log-entry {
|
||
padding: 0.25rem 0;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.log-entry:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.log-time {
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.log-success {
|
||
color: var(--success);
|
||
}
|
||
|
||
.log-info {
|
||
color: var(--accent);
|
||
}
|
||
|
||
footer {
|
||
text-align: center;
|
||
margin-top: 3rem;
|
||
padding-top: 2rem;
|
||
border-top: 1px solid var(--border);
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
}
|
||
|
||
.loading {
|
||
animation: pulse 1.5s infinite;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<h1>🧠 Plaid Local Learning</h1>
|
||
<p class="subtitle">Privacy-preserving financial intelligence powered by RuVector Edge</p>
|
||
<div class="privacy-badge">
|
||
🔒 100% Browser-Local • No Data Leaves Your Device
|
||
</div>
|
||
</header>
|
||
|
||
<div class="grid">
|
||
<!-- Stats Card -->
|
||
<div class="card">
|
||
<h2>📊 Learning Statistics</h2>
|
||
<div class="stats-grid">
|
||
<div class="stat">
|
||
<div class="stat-value" id="stat-patterns">0</div>
|
||
<div class="stat-label">Patterns Learned</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-value" id="stat-version">0</div>
|
||
<div class="stat-label">Learning Version</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-value" id="stat-index">0</div>
|
||
<div class="stat-label">Index Size</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-value" id="stat-qvalues">0</div>
|
||
<div class="stat-label">Q-Values</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: 1rem; display: flex; gap: 0.5rem;">
|
||
<button id="btn-init" onclick="initLearner()">
|
||
⚡ Initialize
|
||
</button>
|
||
<button class="secondary" onclick="refreshStats()">
|
||
🔄 Refresh
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Patterns Card -->
|
||
<div class="card">
|
||
<h2>🎯 Learned Spending Patterns</h2>
|
||
<div class="patterns-list" id="patterns-list">
|
||
<p style="color: var(--text-dim); text-align: center; padding: 2rem;">
|
||
Process transactions to learn patterns
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Transaction Input -->
|
||
<div class="card">
|
||
<h2>💳 Test Transaction</h2>
|
||
<div class="transaction-form">
|
||
<div class="form-row">
|
||
<div>
|
||
<label>Amount ($)</label>
|
||
<input type="number" id="tx-amount" value="45.99" step="0.01">
|
||
</div>
|
||
<div>
|
||
<label>Date</label>
|
||
<input type="date" id="tx-date" value="2024-03-15">
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label>Merchant Name</label>
|
||
<input type="text" id="tx-merchant" value="Starbucks">
|
||
</div>
|
||
<div>
|
||
<label>Category</label>
|
||
<select id="tx-category">
|
||
<option value="Food and Drink">Food and Drink</option>
|
||
<option value="Shopping">Shopping</option>
|
||
<option value="Transportation">Transportation</option>
|
||
<option value="Entertainment">Entertainment</option>
|
||
<option value="Bills">Bills</option>
|
||
<option value="Healthcare">Healthcare</option>
|
||
</select>
|
||
</div>
|
||
<div style="display: flex; gap: 0.5rem;">
|
||
<button onclick="analyzeTransaction()">
|
||
🔍 Analyze
|
||
</button>
|
||
<button class="secondary" onclick="addToLearning()">
|
||
➕ Add & Learn
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div id="analysis-result"></div>
|
||
</div>
|
||
|
||
<!-- Temporal Heatmap -->
|
||
<div class="card">
|
||
<h2>📅 Spending Heatmap</h2>
|
||
<p style="color: var(--text-dim); font-size: 0.9rem; margin-bottom: 1rem;">
|
||
Day-of-week spending patterns (learned from your transactions)
|
||
</p>
|
||
<div class="heatmap" id="heatmap">
|
||
<!-- Generated by JS -->
|
||
</div>
|
||
<div class="heatmap-label">Sun → Sat</div>
|
||
</div>
|
||
|
||
<!-- Sample Data -->
|
||
<div class="card">
|
||
<h2>📦 Load Sample Data</h2>
|
||
<p style="color: var(--text-dim); margin-bottom: 1rem;">
|
||
Load sample transactions to see the learning in action.
|
||
</p>
|
||
<button onclick="loadSampleData()">
|
||
📥 Load 50 Sample Transactions
|
||
</button>
|
||
<div style="margin-top: 1rem;">
|
||
<button class="danger" onclick="clearAllData()">
|
||
🗑️ Clear All Data
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Activity Log -->
|
||
<div class="card">
|
||
<h2>📝 Activity Log</h2>
|
||
<div class="log" id="activity-log">
|
||
<div class="log-entry">
|
||
<span class="log-time">[--:--:--]</span>
|
||
<span class="log-info">Ready to initialize...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<footer>
|
||
<p>Powered by <strong>RuVector Edge</strong> • WASM-based ML • Zero server dependencies</p>
|
||
<p style="margin-top: 0.5rem; font-size: 0.85rem;">
|
||
Your financial data never leaves this browser. All learning happens locally.
|
||
</p>
|
||
</footer>
|
||
</div>
|
||
|
||
<script type="module">
|
||
import init, {
|
||
PlaidLocalLearner,
|
||
WasmHnswIndex,
|
||
WasmSpikingNetwork,
|
||
} from './ruvector_edge.js';
|
||
|
||
// Global state
|
||
let learner = null;
|
||
let isInitialized = false;
|
||
|
||
// Make functions available globally
|
||
window.initLearner = initLearner;
|
||
window.refreshStats = refreshStats;
|
||
window.analyzeTransaction = analyzeTransaction;
|
||
window.addToLearning = addToLearning;
|
||
window.loadSampleData = loadSampleData;
|
||
window.clearAllData = clearAllData;
|
||
|
||
// Logging helper
|
||
function log(message, type = 'info') {
|
||
const logEl = document.getElementById('activity-log');
|
||
const time = new Date().toLocaleTimeString();
|
||
const typeClass = type === 'success' ? 'log-success' : 'log-info';
|
||
|
||
logEl.innerHTML = `
|
||
<div class="log-entry">
|
||
<span class="log-time">[${time}]</span>
|
||
<span class="${typeClass}">${message}</span>
|
||
</div>
|
||
` + logEl.innerHTML;
|
||
}
|
||
|
||
// Initialize the learner
|
||
async function initLearner() {
|
||
const btn = document.getElementById('btn-init');
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="loading">⏳ Loading WASM...</span>';
|
||
|
||
try {
|
||
await init();
|
||
log('WASM module loaded');
|
||
|
||
// Create learner instance
|
||
learner = new PlaidLocalLearner();
|
||
log('PlaidLocalLearner created');
|
||
|
||
// Try to load existing state from IndexedDB
|
||
try {
|
||
const stateJson = localStorage.getItem('plaid_learner_state');
|
||
if (stateJson) {
|
||
learner.loadState(stateJson);
|
||
log('Previous learning state restored', 'success');
|
||
}
|
||
} catch (e) {
|
||
log('Starting with fresh state');
|
||
}
|
||
|
||
isInitialized = true;
|
||
btn.innerHTML = '✅ Initialized';
|
||
btn.style.background = 'var(--success)';
|
||
|
||
refreshStats();
|
||
updateHeatmap();
|
||
|
||
} catch (error) {
|
||
console.error(error);
|
||
log(`Error: ${error.message}`, 'error');
|
||
btn.innerHTML = '❌ Error';
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// Refresh statistics display
|
||
function refreshStats() {
|
||
if (!isInitialized) return;
|
||
|
||
try {
|
||
const stats = learner.getStats();
|
||
document.getElementById('stat-patterns').textContent = stats.patterns_count;
|
||
document.getElementById('stat-version').textContent = stats.version;
|
||
document.getElementById('stat-index').textContent = stats.index_size;
|
||
document.getElementById('stat-qvalues').textContent = stats.q_values_count;
|
||
|
||
// Update patterns list
|
||
const patterns = learner.getPatternsSummary();
|
||
const listEl = document.getElementById('patterns-list');
|
||
|
||
if (patterns.length === 0) {
|
||
listEl.innerHTML = `
|
||
<p style="color: var(--text-dim); text-align: center; padding: 2rem;">
|
||
Process transactions to learn patterns
|
||
</p>
|
||
`;
|
||
} else {
|
||
listEl.innerHTML = patterns.map(p => `
|
||
<div class="pattern-item">
|
||
<div>
|
||
<span class="pattern-category">${p.category}</span>
|
||
<div style="font-size: 0.8rem; color: var(--text-dim);">
|
||
${p.frequency_days.toFixed(0)} day avg frequency
|
||
</div>
|
||
</div>
|
||
<div style="text-align: right;">
|
||
<span class="pattern-amount">$${p.avg_amount.toFixed(2)}</span>
|
||
<div class="confidence-bar">
|
||
<div class="confidence-fill" style="width: ${p.confidence * 100}%"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
log('Stats refreshed');
|
||
} catch (e) {
|
||
log(`Error refreshing stats: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
// Update heatmap visualization
|
||
function updateHeatmap() {
|
||
if (!isInitialized) return;
|
||
|
||
try {
|
||
const heatmap = learner.getTemporalHeatmap();
|
||
const maxVal = Math.max(...heatmap.day_of_week, 1);
|
||
|
||
const heatmapEl = document.getElementById('heatmap');
|
||
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||
|
||
heatmapEl.innerHTML = heatmap.day_of_week.map((val, i) => {
|
||
const intensity = val / maxVal;
|
||
const color = `rgba(99, 102, 241, ${0.1 + intensity * 0.9})`;
|
||
return `
|
||
<div class="heatmap-cell" style="background: ${color}">
|
||
${days[i]}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
} catch (e) {
|
||
console.error('Heatmap error:', e);
|
||
}
|
||
}
|
||
|
||
// Create transaction object from form
|
||
function getTransactionFromForm() {
|
||
return {
|
||
transaction_id: 'tx_' + Date.now(),
|
||
account_id: 'acc_demo',
|
||
amount: parseFloat(document.getElementById('tx-amount').value),
|
||
date: document.getElementById('tx-date').value,
|
||
name: document.getElementById('tx-merchant').value,
|
||
merchant_name: document.getElementById('tx-merchant').value,
|
||
category: [document.getElementById('tx-category').value],
|
||
pending: false,
|
||
payment_channel: 'online',
|
||
};
|
||
}
|
||
|
||
// Analyze a single transaction
|
||
function analyzeTransaction() {
|
||
if (!isInitialized) {
|
||
log('Please initialize first');
|
||
return;
|
||
}
|
||
|
||
const tx = getTransactionFromForm();
|
||
const txJson = JSON.stringify(tx);
|
||
|
||
try {
|
||
// Detect anomaly
|
||
const anomaly = learner.detectAnomaly(txJson);
|
||
|
||
// Predict category
|
||
const prediction = learner.predictCategory(txJson);
|
||
|
||
// Get budget recommendation
|
||
const budget = learner.getBudgetRecommendation(
|
||
tx.category[0],
|
||
tx.amount,
|
||
200 // Default budget
|
||
);
|
||
|
||
const resultEl = document.getElementById('analysis-result');
|
||
const isAnomaly = anomaly.is_anomaly;
|
||
|
||
resultEl.innerHTML = `
|
||
<div class="result-card ${isAnomaly ? 'anomaly' : 'normal'}">
|
||
<h3 style="margin-bottom: 0.5rem;">
|
||
${isAnomaly ? '⚠️ Anomaly Detected' : '✅ Normal Transaction'}
|
||
</h3>
|
||
<p style="margin-bottom: 0.5rem;">${anomaly.reason}</p>
|
||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-top: 1rem;">
|
||
<div>
|
||
<div style="font-size: 0.8rem; color: var(--text-dim);">Anomaly Score</div>
|
||
<div style="font-weight: 600;">${anomaly.anomaly_score.toFixed(2)}</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size: 0.8rem; color: var(--text-dim);">Expected Amount</div>
|
||
<div style="font-weight: 600;">$${anomaly.expected_amount.toFixed(2)}</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size: 0.8rem; color: var(--text-dim);">Trend</div>
|
||
<div style="font-weight: 600;">${budget.trend}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
log(`Analyzed: ${tx.merchant_name} - $${tx.amount}`, 'success');
|
||
} catch (e) {
|
||
log(`Analysis error: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
// Add transaction to learning
|
||
function addToLearning() {
|
||
if (!isInitialized) {
|
||
log('Please initialize first');
|
||
return;
|
||
}
|
||
|
||
const tx = getTransactionFromForm();
|
||
|
||
try {
|
||
const insights = learner.processTransactions(JSON.stringify([tx]));
|
||
log(`Learned from transaction: ${tx.merchant_name}`, 'success');
|
||
|
||
// Save state
|
||
const stateJson = learner.saveState();
|
||
localStorage.setItem('plaid_learner_state', stateJson);
|
||
|
||
refreshStats();
|
||
updateHeatmap();
|
||
} catch (e) {
|
||
log(`Learning error: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
// Load sample transactions
|
||
function loadSampleData() {
|
||
if (!isInitialized) {
|
||
log('Please initialize first');
|
||
return;
|
||
}
|
||
|
||
const categories = ['Food and Drink', 'Shopping', 'Transportation', 'Entertainment', 'Bills'];
|
||
const merchants = {
|
||
'Food and Drink': ['Starbucks', 'Chipotle', 'Whole Foods', 'McDonalds', 'Subway'],
|
||
'Shopping': ['Amazon', 'Target', 'Walmart', 'Best Buy', 'Nike'],
|
||
'Transportation': ['Uber', 'Lyft', 'Shell Gas', 'Metro', 'Parking'],
|
||
'Entertainment': ['Netflix', 'Spotify', 'AMC Theaters', 'Steam', 'Apple TV'],
|
||
'Bills': ['Electric Co', 'Water Utility', 'Internet Provider', 'Phone Bill', 'Insurance'],
|
||
};
|
||
|
||
const amounts = {
|
||
'Food and Drink': [5, 50],
|
||
'Shopping': [10, 200],
|
||
'Transportation': [5, 80],
|
||
'Entertainment': [10, 50],
|
||
'Bills': [50, 300],
|
||
};
|
||
|
||
const transactions = [];
|
||
const today = new Date();
|
||
|
||
for (let i = 0; i < 50; i++) {
|
||
const category = categories[Math.floor(Math.random() * categories.length)];
|
||
const merchant = merchants[category][Math.floor(Math.random() * 5)];
|
||
const [min, max] = amounts[category];
|
||
const amount = min + Math.random() * (max - min);
|
||
|
||
const date = new Date(today);
|
||
date.setDate(date.getDate() - Math.floor(Math.random() * 90));
|
||
|
||
transactions.push({
|
||
transaction_id: `tx_sample_${i}`,
|
||
account_id: 'acc_demo',
|
||
amount: parseFloat(amount.toFixed(2)),
|
||
date: date.toISOString().split('T')[0],
|
||
name: merchant,
|
||
merchant_name: merchant,
|
||
category: [category],
|
||
pending: false,
|
||
payment_channel: 'online',
|
||
});
|
||
}
|
||
|
||
try {
|
||
const insights = learner.processTransactions(JSON.stringify(transactions));
|
||
log(`Loaded ${insights.transactions_processed} sample transactions`, 'success');
|
||
|
||
// Save state
|
||
const stateJson = learner.saveState();
|
||
localStorage.setItem('plaid_learner_state', stateJson);
|
||
|
||
refreshStats();
|
||
updateHeatmap();
|
||
} catch (e) {
|
||
log(`Error loading sample data: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
// Clear all data
|
||
function clearAllData() {
|
||
if (!confirm('This will delete all learned patterns. Are you sure?')) return;
|
||
|
||
if (isInitialized) {
|
||
learner.clear();
|
||
}
|
||
|
||
localStorage.removeItem('plaid_learner_state');
|
||
|
||
// Clear IndexedDB
|
||
indexedDB.deleteDatabase('plaid_local_learning');
|
||
|
||
log('All data cleared', 'success');
|
||
refreshStats();
|
||
updateHeatmap();
|
||
|
||
document.getElementById('analysis-result').innerHTML = '';
|
||
}
|
||
|
||
// Auto-initialize on page load
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
log('Page loaded. Click Initialize to start.');
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|