Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

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