Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
795
vendor/ruvector/examples/edge/pkg/plaid-demo.html
vendored
Normal file
795
vendor/ruvector/examples/edge/pkg/plaid-demo.html
vendored
Normal file
@@ -0,0 +1,795 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user