NeuralVault / frontend /src /components /TriggerSim.js
Gaurav711's picture
feat: implement self-healing online model retraining loop on statistical data drift detection
16cd614
Raw
History Blame Contribute Delete
30.9 kB
import React, { useState, useCallback } from 'react';
import { apiCall } from '../utils/api';
import { FALLBACK_TRIGGER_REVIEW, FALLBACK_TRIGGER_PRODUCT, FALLBACK_TRIGGER_TRANSACTION } from '../constants/fallbacks';
const ENTITIES = [
{ id: 'review', label: 'Customer Review' },
{ id: 'product', label: 'Product' },
{ id: 'transaction', label: 'Transaction' },
];
const ENTITY_STEPS = {
review: [
{ label: 'BEGIN TRANSACTION', sub: 'INSERT INTO reviews (text, customer_id) executing...', color: 'var(--text)' },
{ label: 'TRIGGER: ai_sentiment_analyzer FIRES', sub: 'DistilBERT sentiment analysis (confidence, label)', color: 'var(--amber)' },
{ label: 'TRIGGER: ai_embedder FIRES', sub: 'text-embedding-3-small generating 1536-dim vector', color: 'var(--purple-l)' },
{ label: 'TRIGGER: fulltext_indexer FIRES', sub: 'tsvector lexeme array updated for hybrid search', color: 'var(--teal)' },
{ label: 'COMMIT', sub: 'ACID transaction finalized successfully ✓', color: 'var(--green)' },
],
product: [
{ label: 'BEGIN TRANSACTION', sub: 'INSERT INTO products (name, description) executing...', color: 'var(--text)' },
{ label: 'TRIGGER: ai_product_classifier FIRES', sub: 'LLM classification for categories, subcategories, tags', color: 'var(--amber)' },
{ label: 'TRIGGER: ai_embedder FIRES', sub: 'Generating product vector representation', color: 'var(--purple-l)' },
{ label: 'COMMIT', sub: 'Product catalog entry committed successfully ✓', color: 'var(--green)' },
],
transaction: [
{ label: 'BEGIN TRANSACTION', sub: 'INSERT INTO transactions (amount, customer_id, merchant, location) executing...', color: 'var(--text)' },
{ label: 'TRIGGER: xgboost_fraud_scorer FIRES', sub: 'Running live XGBoost Classifier inference on server...', color: 'var(--purple-l)' },
{ label: 'TRIGGER: shap_tree_explainer FIRES', sub: 'SHAP TreeExplainer calculating local feature attribution...', color: 'var(--amber)' },
{ label: 'COMMIT', sub: 'Transaction authorized or flagged, details logged ✓', color: 'var(--green)' },
],
};
export default function TriggerSim() {
const [entity, setEntity] = useState('review');
const [reviewText, setReviewText] = useState('The battery on this product died completely after 11 days. Customer support never responded to my emails.');
const [productName, setProductName] = useState('WirelessPro X5 Headphones');
const [productDesc, setProductDesc] = useState('Premium over-ear headphones with 40hr battery, active noise cancellation, and multi-device connectivity.');
const [txAmount, setTxAmount] = useState('4821.50');
const [txCustomer, setTxCustomer] = useState('cust_0044 (avg spend $89)');
const [txMerchant, setTxMerchant] = useState('Electronics Store');
const [txLocation, setTxLocation] = useState('Lagos, NG');
const [loading, setLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(-1);
const [result, setResult] = useState(null);
const [usedFallback, setUsedFallback] = useState(false);
const [showRawRow, setShowRawRow] = useState(false);
const [retraining, setRetraining] = useState(false);
const [retrainSuccess, setRetrainSuccess] = useState(false);
const handleRetrain = useCallback(async () => {
setRetraining(true);
setRetrainSuccess(false);
try {
const res = await apiCall('/api/demo/retrain', {}, { timeout: 30000 });
if (res.success) {
setRetrainSuccess(true);
setResult(prev => {
if (!prev) return null;
return {
...prev,
model_version: res.new_version || "XGBoost-v1.1-SelfHealed",
drift_result: {
drift_detected: false,
ks_p_value: 1.0,
message: "Statistical distribution has been recalibrated. System status: STABLE"
}
};
});
}
} catch (err) {
console.error("Retraining failed", err);
} finally {
setRetraining(false);
}
}, []);
const handleInsert = useCallback(async () => {
setLoading(true);
setCurrentStep(0);
setResult(null);
setUsedFallback(false);
setShowRawRow(false);
setRetrainSuccess(false);
// Animate steps
const steps = ENTITY_STEPS[entity];
for (let i = 0; i < steps.length; i++) {
await new Promise((r) => setTimeout(r, 600));
setCurrentStep(i);
}
try {
let payload = { entity_type: entity };
if (entity === 'review') payload.text = reviewText;
else if (entity === 'product') {
payload.product_name = productName;
payload.product_description = productDesc;
} else {
payload.amount = parseFloat(txAmount) || 0;
payload.customer_id = txCustomer;
payload.merchant = txMerchant;
payload.location = txLocation;
}
const res = await apiCall('/api/demo/trigger', payload, { timeout: 30000 });
if (res.ok && res.data) {
setResult(res.data);
} else {
throw new Error(res.error || 'Failed');
}
} catch {
setUsedFallback(true);
if (entity === 'review') setResult(FALLBACK_TRIGGER_REVIEW);
else if (entity === 'product') setResult(FALLBACK_TRIGGER_PRODUCT);
else setResult(FALLBACK_TRIGGER_TRANSACTION);
} finally {
setLoading(false);
}
}, [entity, reviewText, productName, productDesc, txAmount, txCustomer, txMerchant, txLocation]);
return (
<div style={{ maxWidth: 680, margin: '0 auto' }}>
<p style={{ color: 'var(--text2)', fontSize: 14, marginBottom: 20, textAlign: 'center' }}>
Simulate a database INSERT and watch three AI models fire automatically inside PostgreSQL trigger functions — in a single ACID transaction.
</p>
{/* Entity selector */}
<div className="nv-tabs" style={{ marginBottom: 24, justifyContent: 'center', maxWidth: 420, margin: '0 auto 24px' }}>
{ENTITIES.map((e) => (
<button key={e.id} className={`nv-tab ${entity === e.id ? 'active' : ''}`} onClick={() => { setEntity(e.id); setResult(null); setCurrentStep(-1); setShowRawRow(false); }}>
{e.label}
</button>
))}
</div>
{/* Input area */}
{entity === 'review' && (
<div style={{ marginBottom: 16 }}>
<label style={{ fontSize: 13, color: 'var(--text2)', marginBottom: 8, display: 'block' }}>Type a product review</label>
<textarea className="nv-textarea" rows={4} value={reviewText} onChange={(e) => setReviewText(e.target.value)} data-testid="trigger-input" />
</div>
)}
{entity === 'product' && (
<div style={{ marginBottom: 16 }}>
<label style={{ fontSize: 13, color: 'var(--text2)', marginBottom: 6, display: 'block' }}>Product Name</label>
<input className="nv-input" value={productName} onChange={(e) => setProductName(e.target.value)} style={{ marginBottom: 10 }} />
<label style={{ fontSize: 13, color: 'var(--text2)', marginBottom: 6, display: 'block' }}>Description</label>
<textarea className="nv-textarea" rows={3} value={productDesc} onChange={(e) => setProductDesc(e.target.value)} />
</div>
)}
{entity === 'transaction' && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 16 }}>
<div>
<label style={{ fontSize: 13, color: 'var(--text2)', marginBottom: 6, display: 'block' }}>Amount ($)</label>
<input className="nv-input" value={txAmount} onChange={(e) => setTxAmount(e.target.value)} />
</div>
<div>
<label style={{ fontSize: 13, color: 'var(--text2)', marginBottom: 6, display: 'block' }}>Customer ID</label>
<input className="nv-input" value={txCustomer} onChange={(e) => setTxCustomer(e.target.value)} />
</div>
<div>
<label style={{ fontSize: 13, color: 'var(--text2)', marginBottom: 6, display: 'block' }}>Merchant</label>
<input className="nv-input" value={txMerchant} onChange={(e) => setTxMerchant(e.target.value)} />
</div>
<div>
<label style={{ fontSize: 13, color: 'var(--text2)', marginBottom: 6, display: 'block' }}>Location</label>
<input className="nv-input" value={txLocation} onChange={(e) => setTxLocation(e.target.value)} />
</div>
</div>
)}
<button
className="nv-btn-primary"
onClick={handleInsert}
disabled={loading}
data-testid="trigger-submit"
style={{ width: '100%', justifyContent: 'center', marginBottom: 24 }}
>
{loading ? 'Processing...' : `INSERT INTO ${entity === 'review' ? 'reviews' : entity === 'product' ? 'products' : 'transactions'} →`}
</button>
{/* Processing animation */}
{(loading || currentStep >= 0) && (
<div style={{ marginBottom: 24 }}>
{ENTITY_STEPS[entity].map((step, i) => (
<div
key={i}
style={{
display: 'flex',
alignItems: 'flex-start',
gap: 12,
padding: '10px 14px',
marginBottom: 4,
borderRadius: 8,
background: i <= currentStep ? 'rgba(255,255,255,0.02)' : 'transparent',
opacity: i <= currentStep ? 1 : 0.3,
transition: 'opacity 0.4s ease',
}}
>
<div style={{ minWidth: 20, textAlign: 'center', marginTop: 2 }}>
{i < currentStep ? (
<span style={{ color: 'var(--green)' }}></span>
) : i === currentStep && loading ? (
<span className="nv-spinner" />
) : i <= currentStep ? (
<span style={{ color: 'var(--green)' }}></span>
) : null}
</div>
<div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: step.color, fontWeight: 500 }}>{step.label}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text3)', marginTop: 2 }}>{step.sub}</div>
</div>
</div>
))}
</div>
)}
{/* Result card */}
{result && !loading && (
<div>
{usedFallback && (
<div style={{ background: 'rgba(245,158,11,0.08)', border: '1px solid rgba(245,158,11,0.3)', borderRadius: 8, padding: '10px 14px', marginBottom: 12, fontSize: 13, color: 'var(--amber)' }}>
Gemini API unavailable — showing cached example response
</div>
)}
<div className="nv-card" data-testid="trigger-result" style={{ padding: entity === 'transaction' && !showRawRow ? '24px' : '20px 24px' }}>
{entity === 'transaction' && !showRawRow ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{/* Header row with badges */}
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center', gap: '12px', borderBottom: '1px solid var(--border)', paddingBottom: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text)' }}>
XGBoost Fraud Scoring & SHAP Analysis
</span>
{result.source === 'xgb_shap_model' ? (
<span className="nv-badge nv-badge-purple" style={{ fontSize: '10px' }}>LIVE MODEL</span>
) : (
<span className="nv-badge nv-badge-gray" style={{ fontSize: '10px' }}>FALLBACK ENGINE</span>
)}
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<span className="nv-badge nv-badge-purple">
AUC: {result.model_auc || '0.9455'}
</span>
<span className="nv-badge nv-badge-teal">
Latency: {result.inference_ms ? `${result.inference_ms}ms` : 'N/A'}
</span>
{result.drift_result && (
<span className={`nv-badge ${result.drift_result.drift_detected ? 'nv-badge-red' : 'nv-badge-green'}`}>
Drift: {result.drift_result.drift_detected ? 'DETECTED' : 'STABLE'}
</span>
)}
</div>
</div>
{/* Two column grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6" style={{ display: 'grid', gap: '24px' }}>
{/* Left Column: Recommended Action and Core Details */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{/* Recommended Action Alert */}
<div style={{
padding: '16px',
borderRadius: '8px',
background: result.recommended_action === 'BLOCK'
? 'rgba(239, 68, 68, 0.06)'
: result.recommended_action === 'REVIEW'
? 'rgba(245, 158, 11, 0.06)'
: 'rgba(74, 222, 128, 0.06)',
border: `1px solid ${
result.recommended_action === 'BLOCK'
? 'rgba(239, 68, 68, 0.25)'
: result.recommended_action === 'REVIEW'
? 'rgba(245, 158, 11, 0.25)'
: 'rgba(74, 222, 128, 0.25)'
}`,
display: 'flex',
alignItems: 'center',
gap: '12px'
}}>
{/* Action icon */}
<div style={{
width: '32px',
height: '32px',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: result.recommended_action === 'BLOCK'
? 'rgba(239, 68, 68, 0.15)'
: result.recommended_action === 'REVIEW'
? 'rgba(245, 158, 11, 0.15)'
: 'rgba(74, 222, 128, 0.15)',
color: result.recommended_action === 'BLOCK'
? 'var(--red)'
: result.recommended_action === 'REVIEW'
? 'var(--amber)'
: 'var(--green)',
fontSize: '18px',
fontWeight: 'bold'
}}>
{result.recommended_action === 'BLOCK' ? '✕' : result.recommended_action === 'REVIEW' ? '⚠' : '✓'}
</div>
<div>
<div style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text3)', fontWeight: 'bold' }}>
RECOMMENDED ACTION
</div>
<div style={{
fontSize: '16px',
fontWeight: 'bold',
color: result.recommended_action === 'BLOCK'
? 'var(--red)'
: result.recommended_action === 'REVIEW'
? 'var(--amber)'
: 'var(--green)'
}}>
{result.recommended_action}
</div>
</div>
</div>
{/* Fraud Risk Score Indicator */}
<div style={{ background: 'var(--surface2)', padding: '16px', borderRadius: '8px', border: '1px solid var(--border)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px', fontSize: '13px' }}>
<span style={{ color: 'var(--text2)', fontWeight: '500' }}>Fraud Risk Probability</span>
<span style={{
color: result.recommended_action === 'BLOCK'
? 'var(--red)'
: result.recommended_action === 'REVIEW'
? 'var(--amber)'
: 'var(--green)',
fontWeight: '700',
fontFamily: 'var(--font-mono)'
}}>
{(result.fraud_score * 100).toFixed(2)}%
</span>
</div>
<div style={{ height: '8px', background: 'var(--bg)', borderRadius: '4px', overflow: 'hidden', marginBottom: '12px' }}>
<div style={{
height: '100%',
width: `${result.fraud_score * 100}%`,
background: result.recommended_action === 'BLOCK'
? 'var(--red)'
: result.recommended_action === 'REVIEW'
? 'var(--amber)'
: 'var(--green)',
borderRadius: '4px',
transition: 'width 1s cubic-bezier(0.4, 0, 0.2, 1)'
}} />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '10px', color: 'var(--text3)' }}>
<span>SAFE</span>
<span>REVIEW REQ.</span>
<span>BLOCK REQ.</span>
</div>
</div>
{/* Transaction Core Details */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', fontSize: '13px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', borderBottom: '1px dashed var(--border)', paddingBottom: '6px' }}>
<span style={{ color: 'var(--text2)' }}>Amount</span>
<span style={{ color: 'var(--text)', fontWeight: 600, fontFamily: 'var(--font-mono)' }}>${parseFloat(txAmount).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', borderBottom: '1px dashed var(--border)', paddingBottom: '6px' }}>
<span style={{ color: 'var(--text2)' }}>Customer</span>
<span style={{ color: 'var(--text)', fontWeight: 500 }}>{txCustomer}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', borderBottom: '1px dashed var(--border)', paddingBottom: '6px' }}>
<span style={{ color: 'var(--text2)' }}>Merchant</span>
<span style={{ color: 'var(--text)', fontWeight: 500 }}>{txMerchant}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', borderBottom: '1px dashed var(--border)', paddingBottom: '6px' }}>
<span style={{ color: 'var(--text2)' }}>Location</span>
<span style={{ color: 'var(--text)', fontWeight: 500 }}>{txLocation}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', paddingBottom: '2px' }}>
<span style={{ color: 'var(--text2)' }}>Confidence</span>
<span style={{ color: 'var(--text)', fontWeight: 500 }}>{(result.confidence * 100).toFixed(0)}%</span>
</div>
</div>
</div>
{/* Right Column: SHAP Bar Chart & Explanations */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{/* SHAP attributions */}
<div>
<div style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text)', marginBottom: '4px' }}>
SHAP Local Explanations
</div>
<div style={{ fontSize: '11px', color: 'var(--text3)', marginBottom: '14px' }}>
Engineered feature impact on fraud decision (log-odds contribution)
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{result.shap_attributions && result.shap_attributions.map((attr) => {
const isPositive = attr.shap_value >= 0;
const maxVal = Math.max(...result.shap_attributions.map(a => Math.abs(a.shap_value)), 0.1);
const percentage = Math.min((Math.abs(attr.shap_value) / maxVal) * 100, 100);
const displayValue = (isPositive ? '+' : '') + (attr.shap_value * 100).toFixed(1) + '%';
return (
<div key={attr.feature} style={{ display: 'flex', flexDirection: 'column', gap: '3px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '11px' }}>
<span style={{ color: 'var(--text2)', fontWeight: '500' }}>{attr.feature}</span>
<span style={{
fontFamily: 'var(--font-mono)',
fontWeight: '600',
color: isPositive ? 'var(--red)' : 'var(--green)'
}}>
{displayValue}
</span>
</div>
<div style={{ height: '6px', background: 'var(--surface2)', borderRadius: '3px', overflow: 'hidden', position: 'relative' }}>
<div style={{
height: '100%',
width: `${percentage}%`,
background: isPositive
? 'linear-gradient(90deg, var(--amber), var(--red))'
: 'linear-gradient(90deg, var(--green), var(--teal))',
borderRadius: '3px',
transition: 'width 0.8s cubic-bezier(0.4, 0, 0.2, 1)'
}} />
</div>
</div>
);
})}
</div>
</div>
{/* Risk factors bullet list */}
<div style={{ borderTop: '1px solid var(--border)', paddingTop: '12px' }}>
<div style={{ fontSize: '12px', fontWeight: 600, color: 'var(--text2)', marginBottom: '8px' }}>
Risk Analysis Summary
</div>
<ul style={{ listStyleType: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '6px' }}>
{result.risk_factors && result.risk_factors.map((factor, idx) => (
<li key={idx} style={{
fontSize: '11.5px',
color: factor.includes('contributed') || factor.includes('risk') || factor.includes('Anomaly') || factor.includes('above')
? 'var(--text2)'
: 'var(--text3)',
display: 'flex',
alignItems: 'flex-start',
gap: '8px'
}}>
<span style={{
color: factor.includes('All transaction parameters') ? 'var(--green)' : 'var(--amber)',
marginTop: '1px'
}}>
{factor.includes('All transaction parameters') ? '✓' : '•'}
</span>
<span>{factor}</span>
</li>
))}
</ul>
</div>
</div>
</div>
{/* Footer row with raw details toggle */}
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center', marginTop: '12px', borderTop: '1px solid var(--border)', paddingTop: '12px', gap: '12px' }}>
{result.drift_result && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', maxWidth: '75%' }}>
<span style={{ fontSize: '11px', color: result.drift_result.drift_detected ? 'var(--amber)' : 'var(--text3)', fontWeight: result.drift_result.drift_detected ? '600' : 'normal' }}>
{result.drift_result.message}
</span>
{result.drift_result.drift_detected && (
<button
onClick={handleRetrain}
disabled={retraining}
style={{
background: 'linear-gradient(135deg, var(--purple), var(--purple-l))',
border: 'none',
borderRadius: '4px',
color: 'white',
padding: '4px 10px',
fontSize: '10px',
fontWeight: '600',
cursor: 'pointer',
width: 'fit-content',
marginTop: '4px',
boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
}}
>
{retraining ? 'Retraining Model...' : '⚡ Retrain & Hot-Reload Model'}
</button>
)}
{retrainSuccess && (
<span style={{ fontSize: '10px', color: 'var(--green)', fontWeight: '600', marginTop: '2px' }}>
✓ Model recalibrated! Covariate shift resolved.
</span>
)}
</div>
)}
<button
onClick={() => setShowRawRow(true)}
style={{
background: 'transparent',
border: 'none',
color: 'var(--purple-l)',
fontSize: '11px',
fontFamily: 'var(--font-mono)',
cursor: 'pointer',
textDecoration: 'underline',
marginLeft: 'auto',
alignSelf: 'flex-start'
}}
>
View Database Row
</button>
</div>
</div>
) : (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>Stored Database Row</div>
{entity === 'transaction' && (
<button
onClick={() => setShowRawRow(false)}
style={{
background: 'transparent',
border: 'none',
color: 'var(--purple-l)',
fontSize: '11px',
fontFamily: 'var(--font-mono)',
cursor: 'pointer',
textDecoration: 'underline'
}}
>
View Analysis Visuals
</button>
)}
</div>
<div className="nv-code-block" style={{ fontSize: 12 }}>
{entity === 'review' && (
<pre style={{ margin: 0 }}>
{`review_id: ${crypto.randomUUID ? crypto.randomUUID().substring(0,8) : 'a1b2c3d4'}...
customer_id: cust_8821
text: ${reviewText.substring(0, 60)}...
sentiment_score: ${result.score}
sentiment_label: ${result.label}
sentiment_conf: ${result.confidence}
key_phrases: ${JSON.stringify(result.key_phrases || [])}
embedding: [0.231, -0.445, 0.882 ... vector(1536)]
embedding_model: text-embedding-3-small
tsvector: 'batteri':1 'complet':3 'die':4 'email':9
fraud_score: 0.14
flagged: false
ai_metadata: {"trigger_version": "1.0", "processed_ms": 0.48}
created_at: ${new Date().toISOString()}`}
</pre>
)}
{entity === 'product' && (
<pre style={{ margin: 0 }}>
{`product_id: ${Math.random().toString(36).substring(2, 10)}...
name: ${productName}
category: ${result.category}
subcategory: ${result.subcategory}
tags: ${JSON.stringify(result.tags || [])}
quality_score: ${result.quality_score}
similar_cats: ${JSON.stringify(result.similar_categories || [])}
embedding: [0.112, -0.332, 0.751 ... vector(1536)]
created_at: ${new Date().toISOString()}`}
</pre>
)}
{entity === 'transaction' && (
<pre style={{ margin: 0 }}>
{`transaction_id: ${Math.random().toString(36).substring(2, 10)}...
customer_id: ${txCustomer}
amount: $${txAmount}
fraud_score: ${result.fraud_score}
risk_level: ${result.risk_level}
risk_factors: ${JSON.stringify(result.risk_factors || [])}
recommendation: ${result.recommended_action}
confidence: ${result.confidence}
flagged: ${(result.fraud_score || 0) > 0.7}
created_at: ${new Date().toISOString()}`}
</pre>
)}
</div>
</div>
)}
</div>
</div>
)}
</div>
);
}