fakeshield-api / fakeshield /src /pages /TextLab /TextLabPage.tsx
Akash4911's picture
Production Deploy: Improved robustness and logging
66b6851
import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { scanTextAsync, downloadForensicReport, type TextResult } from '../../services/textService';
import { useAuth } from '../../hooks/useAuth.tsx';
const ForensicGauge: React.FC<{
score: number;
label: string;
color: string;
sublabel: string;
size?: "sm" | "lg" | "xl";
}> = ({ score, label, color, sublabel, size = "lg" }) => {
const radius = size === "xl" ? 90 : size === "lg" ? 75 : 35;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference - (score / 100) * circumference;
const viewSize = size === "xl" ? 220 : size === "lg" ? 200 : 100;
return (
<div className="relative flex flex-col items-center justify-center">
<div className={`relative ${size === "xl" ? 'w-64 h-64' : size === "lg" ? 'w-56 h-56' : 'w-24 h-24'} flex items-center justify-center`}>
<svg className="w-full h-full transform -rotate-90" viewBox={`0 0 ${viewSize} ${viewSize}`}>
<circle
cx={viewSize/2}
cy={viewSize/2}
r={radius}
fill="transparent"
stroke="currentColor"
strokeWidth={size === "sm" ? "4" : "8"}
className="text-[var(--panel-border)]"
/>
<circle
cx={viewSize/2}
cy={viewSize/2}
r={radius}
fill="transparent"
stroke={color}
strokeWidth={size === "sm" ? "4" : "8"}
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
className="transition-all duration-1000 ease-out"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
<span className={`${size === "xl" ? 'text-6xl' : size === "lg" ? 'text-5xl' : 'text-xl'} font-display font-black tracking-tighter`} style={{ color }}>
{Math.round(score)}%
</span>
{(size === "lg" || size === "xl") && <span className="text-[10px] font-bold uppercase tracking-[0.3em] text-[var(--text-secondary)] mt-2">{sublabel}</span>}
</div>
</div>
{label && (
<h2 className={`${size === "xl" ? 'mt-8 text-4xl' : size === "lg" ? 'mt-8 text-3xl' : 'mt-2 text-[10px]'} font-display font-black tracking-[0.05em] uppercase text-center`} style={{ color }}>
{label}
</h2>
)}
</div>
);
};
import DashboardLayout from '../../components/layout/DashboardLayout';
const TextLabPage: React.FC = () => {
const { token } = useAuth();
const location = useLocation();
const [text, setText] = useState('');
const [loading, setLoading] = useState(false);
const [statusMsg, setStatusMsg] = useState('');
const [result, setResult] = useState<TextResult | null>(null);
const [downloading, setDownloading] = useState(false);
useEffect(() => {
const params = new URLSearchParams(location.search);
const scanId = params.get('scan_id');
if (scanId && token) {
const fetchScan = async () => {
setLoading(true);
setStatusMsg("RECOVERING FORENSIC DATA...");
try {
const res = await fetch(`http://127.0.0.1:8001/api/v1/dashboard/scan/${scanId}`, {
headers: { Authorization: `Bearer ${token}` }
});
if (res.ok) {
const json = await res.json();
// Use full_result if available, otherwise fallback to data
const scanData = json.data.full_result || json.data;
setResult(scanData);
if (scanData.text_preview) setText(scanData.text_preview);
}
} catch (err) {
console.error("Failed to load historical scan:", err);
} finally {
setLoading(false);
}
};
fetchScan();
}
}, [location.search, token]);
const handleAnalyze = async () => {
if (!text.trim()) return;
setLoading(true);
setResult(null);
try {
const res = await scanTextAsync(text, token, (msg) => setStatusMsg(msg));
setResult(res);
} catch (err: any) {
console.error("Forensic analysis failed:", err);
alert(`Forensic Engine Error: ${err.message || "Unknown Failure"}`);
} finally {
setLoading(false);
}
};
const handleDownload = async () => {
if (!result?.scan_id) return;
setDownloading(true);
try {
await downloadForensicReport(result.scan_id, token);
} catch (err) {
alert("Error generating PDF. Please try again.");
} finally {
setDownloading(false);
}
};
return (
<DashboardLayout activeTab="Text lab">
<div className="max-w-7xl mx-auto px-4 md:px-8 space-y-6 md:space-y-8 pb-16">
<header className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4 mt-12 mb-8">
<div className="space-y-1">
<h1 className="text-4xl md:text-5xl font-display font-black text-[#00E5CC] tracking-tighter uppercase">Text lab</h1>
<div className="flex items-center gap-2">
<span className="w-8 h-[1px] bg-[var(--panel-border)]"></span>
<p className="text-[10px] font-bold text-[var(--text-muted)] tracking-[0.4em] uppercase">Forensic linguistic audit engine</p>
</div>
</div>
<div className="flex flex-wrap gap-3">
<button
onClick={() => { setResult(null); setText(""); }}
className="px-4 md:px-6 py-2 md:py-2.5 rounded-xl border border-[var(--panel-border)] bg-[var(--bg-secondary)] hover:bg-[var(--btn-secondary-bg)] font-bold uppercase text-[9px] md:text-[10px] tracking-widest transition-all"
>
NEW ANALYSIS
</button>
{result && (
<button
onClick={handleDownload}
disabled={downloading}
className="px-4 md:px-6 py-2 md:py-2.5 rounded-xl bg-[rgba(0,229,204,0.05)] text-[#00E5CC] border border-[var(--accent-blue-border)] font-bold uppercase text-[9px] md:text-[10px] tracking-widest hover:bg-[#00E5CC] hover:text-[#000000] transition-all flex items-center gap-2"
>
{downloading ? "EXPORTING..." : "EXPORT TECHNICAL REPORT"}
</button>
)}
</div>
</header>
{/* TOP ROW: EVIDENCE & MASTER VERDICT */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 md:gap-8">
{/* EVIDENCE AREA (Col 1-8) */}
<div className="col-span-1 lg:col-span-8 space-y-6">
<div className="card-border p-1.5 rounded-2xl bg-[var(--panel-border)]">
<div className="p-5 md:p-8 rounded-[0.9rem] border border-[var(--panel-border)] bg-[var(--panel-bg)] relative overflow-hidden min-h-[430px] md:min-h-[520px] flex flex-col">
<div className="absolute inset-0 opacity-[0.03] pointer-events-none" style={{ backgroundImage: 'radial-gradient(var(--text-secondary) 1px, transparent 1px)', backgroundSize: '32px 32px' }} />
{!result ? (
<>
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-2 mb-6 relative z-10">
<span className="text-sm font-bold text-[var(--text-secondary)]">Input content</span>
<span className="text-[10px] font-mono text-[var(--accent-blue)]">CHARACTER COUNT: {text.length}</span>
</div>
<textarea
className="flex-1 bg-transparent text-base md:text-xl leading-relaxed resize-none outline-none placeholder-[var(--text-muted)] relative z-10 font-medium custom-scrollbar"
placeholder="Paste or type text for professional forensic analysis..."
value={text}
onChange={(e) => setText(e.target.value)}
disabled={loading}
/>
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mt-6 pt-6 border-t border-[var(--panel-border)] relative z-10">
<button
onClick={handleAnalyze}
disabled={loading || text.trim().length < 20}
className={`w-full sm:w-auto px-6 sm:px-12 py-4 rounded-2xl font-black uppercase tracking-[0.2em] text-xs sm:text-sm transition-all relative overflow-hidden ${
loading || text.trim().length < 20
? 'bg-[var(--btn-secondary-bg)] text-[var(--text-muted)] cursor-not-allowed opacity-50'
: 'bg-gradient-to-r from-[#00E5CC] to-[#0092ff] text-[#000000] hover:scale-[1.02] hover:shadow-[0_0_30px_rgba(0,229,204,0.3)] active:scale-95'
}`}
>
{loading ? "PROCESSING PATTERNS..." : "ANALYZE CONTENT"}
</button>
</div>
</>
) : (
<div className="flex flex-col h-full">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-6 relative z-10">
<span className="text-sm font-bold text-[#00E5CC]">AI probability heatmap</span>
<div className="flex gap-4">
<span className="text-[8px] text-red-500 font-bold uppercase tracking-widest flex items-center gap-1">
SYNTHETIC
</span>
<span className="text-[8px] text-green-500 font-bold uppercase tracking-widest flex items-center gap-1">
HUMAN
</span>
</div>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar relative z-10 pr-4">
<div className="prose prose-invert max-w-none leading-[1.8] text-base md:text-xl font-medium">
{result.sentence_highlights.map((h, i) => {
const bgColor = h.label === "AI" ? `rgba(239, 68, 68, 0.2)`
: h.label === "UNCERTAIN" ? `rgba(234, 179, 8, 0.2)`
: h.label === "HUMAN" ? `rgba(34, 197, 94, 0.2)` : "transparent";
return (
<span
key={i}
className="px-0.5 py-0.5 rounded transition-all group relative cursor-crosshair hover:ring-1 hover:ring-white/20"
style={{ backgroundColor: bgColor }}
>
{h.sentence}{" "}
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--panel-border)] text-white text-xs font-mono font-bold rounded-xl opacity-0 scale-90 group-hover:opacity-100 group-hover:scale-100 transition-all duration-200 pointer-events-none whitespace-nowrap z-50 shadow-2xl">
<div className="flex flex-col items-center gap-1">
<span className="text-[8px] text-[var(--text-muted)] uppercase tracking-[0.2em] font-black border-b border-[var(--panel-border)] pb-1 w-full text-center mb-1">Signal Meta</span>
<span className={(h.ai_score ?? 0) > 50 ? 'text-red-400' : 'text-green-400'}>{h.ai_score ?? 0}% AI</span>
<span className="text-[7px] font-bold text-[var(--text-muted)] uppercase tracking-widest mt-0.5">PPL: {h.perplexity?.toFixed(1) || '0.0'}</span>
</div>
</div>
</span>
);
})}
</div>
</div>
</div>
)}
{loading && (
<div className="absolute inset-0 z-50 backdrop-blur-sm flex flex-col items-center justify-center p-12 bg-[rgba(2,6,23,0.9)] animate-in fade-in duration-300">
<div className="w-48 h-1 bg-[var(--panel-border)] rounded-full overflow-hidden mb-6">
<div className="h-full bg-[var(--accent-blue)] animate-[loading-bar_2s_infinite]" style={{ width: '30%' }} />
</div>
<p className="font-mono text-sm text-[var(--text-muted)] uppercase tracking-[0.4em] text-center">{statusMsg}</p>
</div>
)}
</div>
</div>
</div>
{/* MASTER VERDICT (Col 9-12) */}
<div className="col-span-1 lg:col-span-4">
<div className={`min-h-[430px] md:min-h-[520px] lg:sticky lg:top-8 p-5 md:p-8 pb-8 md:pb-12 rounded-2xl border border-[var(--panel-border)] flex flex-col items-center justify-center text-center relative overflow-hidden transition-all duration-700 ${
!result ? 'bg-[var(--panel-bg)] opacity-60' :
result.threat_level === 'CRITICAL' || result.threat_level === 'HIGH' ? 'bg-[rgba(239,68,68,0.02)]' :
'bg-[rgba(16,185,129,0.02)]'
}`}>
<span className="text-sm font-bold mb-6 md:mb-10 text-[var(--text-secondary)]">Consolidated verdict</span>
<ForensicGauge
size="xl"
score={result ? Math.round(result.score * 100) : 0}
label={result?.verdict || "STANDBY"}
sublabel={result ? `AI PROBABILITY SCORE` : "AWAITING INPUT"}
color={
!result ? 'var(--text-muted)' :
result.threat_level === 'CRITICAL' || result.threat_level === 'HIGH' ? '#EF4444' :
result.threat_level === 'MEDIUM' ? '#F59E0B' : '#10B981'
}
/>
{result && (
<div className="mt-8 w-full grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="p-4 rounded-2xl bg-[var(--bg-secondary)] border border-[var(--panel-border)] flex flex-col items-center justify-center text-center">
<span className="block text-[10px] uppercase tracking-widest text-[var(--text-secondary)] font-bold mb-2">Stability</span>
<div className="flex flex-col items-center">
<span className="text-lg font-black text-white leading-none">{result.confidence}</span>
<span className="text-[8px] font-bold text-[#00E5CC] uppercase tracking-[0.2em] mt-1">{result.confidence_level}</span>
</div>
</div>
<div className="p-4 rounded-2xl bg-[var(--bg-secondary)] border border-[var(--panel-border)] flex flex-col items-center justify-center text-center">
<span className="block text-[10px] uppercase tracking-widest text-[var(--text-secondary)] font-bold mb-2">Risk level</span>
<span className={`text-lg font-black leading-none ${result.threat_level === 'CRITICAL' || result.threat_level === 'HIGH' ? 'text-red-500' : 'text-orange-500'}`}>{result.threat_level}</span>
</div>
</div>
)}
{/* Pipeline Status indicator removed */}
</div>
</div>
</div>
{/* BELOW ROW: MULTI-VECTOR AUDIT STRIP */}
<div className={`mt-8 p-5 md:p-8 rounded-2xl border border-[var(--panel-border)] bg-[var(--panel-bg)] transition-all duration-700 ${!result && 'opacity-30'}`}>
<h3 className="text-sm font-bold mb-10 text-[var(--text-secondary)] text-center">Diagnostic signal analytics</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 items-center">
<ForensicGauge size="sm" score={result ? (result.signals.neural || 0) * 100 : 0} label="Neural Pulse" color="#8b5cf6" sublabel="" />
<ForensicGauge size="sm" score={result ? (result.signals.statistical || 0) * 100 : 0} label="Binoculars" color="#0ea5e9" sublabel="" />
<ForensicGauge size="sm" score={result ? (result.signals.rhythm || 0) * 100 : 0} label="Rhythm" color="#00E5CC" sublabel="" />
<ForensicGauge size="sm" score={result ? (result.signals.flow || 0) * 100 : 0} label="Semantic Flow" color="#f59e0b" sublabel="" />
</div>
</div>
{/* BOTTOM SECTION: DIAGNOSTICS & REASONING */}
{result && (
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 animate-in slide-in-from-bottom-8 duration-1000">
{/* LINGUISTIC DNA DIAGNOSTICS */}
<div className="col-span-1 md:col-span-12 lg:col-span-5 flex flex-col gap-6">
<div className="p-8 rounded-2xl border border-[var(--panel-border)] bg-[var(--panel-bg)] h-full">
<h3 className="text-sm font-bold mb-8 text-[var(--text-secondary)] border-b border-[var(--panel-border)] pb-4">Internal metric breakdown</h3>
<div className="space-y-4">
<div className="flex justify-between items-center py-2 border-b border-[var(--panel-border)] last:border-0 border-dashed">
<span className="text-[10px] text-[var(--text-muted)] font-mono uppercase">Syntactic Complexity</span>
<span className="text-sm font-mono font-bold text-cyan-400">{result.structural_details?.depth_variance || 0}</span>
</div>
<div className="flex justify-between items-center py-2 border-b border-[var(--panel-border)] last:border-0 border-dashed">
<span className="text-[10px] text-[var(--text-muted)] font-mono uppercase">Semantic Consistency</span>
<span className="text-sm font-mono font-bold text-amber-400">{Math.round((result.semantic_details?.semantic_consistency || 0) * 100)}%</span>
</div>
<div className="flex justify-between items-center py-2 border-b border-[var(--panel-border)] last:border-0 border-dashed">
<span className="text-[10px] text-[var(--text-muted)] font-mono uppercase">Structural Balance</span>
<span className="text-sm font-mono font-bold text-pink-400 uppercase">{result.linguistic_profile?.syntactic_complexity || "MODERATE"}</span>
</div>
<div className="flex justify-between items-center py-2 border-b border-[var(--panel-border)] last:border-0 border-dashed">
<span className="text-[10px] text-[var(--text-muted)] font-mono uppercase">Entropy Efficiency</span>
<span className="text-sm font-mono font-bold text-blue-400">{result.linguistic_profile?.entropy_bits_per_char || "0.0"}</span>
</div>
</div>
</div>
</div>
{/* EXPERT AUDIT persona */}
<div className="col-span-1 md:col-span-12 lg:col-span-7 space-y-6">
<div className="p-6 md:p-10 rounded-2xl border border-[var(--panel-border)] bg-[var(--panel-bg)] h-full flex flex-col justify-center">
<div className="flex items-center gap-4 mb-8">
<div className="w-10 h-10 rounded-xl bg-[var(--bg-secondary)] flex items-center justify-center text-[var(--text-muted)] border border-[var(--panel-border)]">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" />
</svg>
</div>
<div className="flex flex-col">
<h3 className="text-sm font-bold uppercase tracking-[0.2em] text-white">System assessment note</h3>
<span className="text-[8px] text-[var(--text-muted)] font-mono uppercase">Report Ref / ID: {result.scan_id?.split('_')[1] || "0x0"}</span>
</div>
</div>
<div className="relative">
<p className="text-lg font-medium leading-relaxed text-[var(--text-primary)] relative z-10 pl-6 border-l border-[var(--panel-border)]">
{result.forensic_reasoning || "No anomalous linguistic patterns detected during structural and semantic trajectory audit."}
</p>
</div>
<div className="mt-10 flex flex-wrap gap-2">
{result.indicators && result.indicators.map((ind, i) => (
<span key={i} className="px-3 py-1 rounded bg-[var(--bg-secondary)] border border-[var(--panel-border)] text-[9px] font-mono text-[var(--text-muted)] uppercase">
METRIC: {ind}
</span>
))}
</div>
</div>
</div>
</div>
)}
</div>
<style>{`
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: var(--panel-border); border-radius: 10px; }
.font-display { font-family: 'Outfit', sans-serif; }
.font-mono { font-family: 'JetBrains Mono', monospace; }
@keyframes scan-line {
0% { transform: translateY(-100%); }
100% { transform: translateY(100%); }
}
`}</style>
</DashboardLayout>
);
};
export default TextLabPage;