Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } | |