// Main app — state machine + scenario routing + tweaks. // Supports two modes: // - Mock mode (useMocks=true, default): fixture scenarios from mock-data.jsx. // - Live mode (useMocks=false): real fetch to the backend via api-client.jsx. const { useState: useStateApp, useEffect: useEffectApp, useMemo: useMemoApp } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "scenario": "ai", "errorState": "none", "apiKeyConfigured": true, "showLayersDialogOnLoad": false, "skipAnimation": false, "showConflictForMixed": true, "theme": "dark", "useMocks": false }/*EDITMODE-END*/; const SCEN = { ai: { payload: () => window.SCEN_AI, file: { name:'essay_draft.pdf', sizeBytes:248192, type:'pdf', wordCount:1847 } }, human: { payload: () => window.SCEN_HUMAN, file: { name:'thesis_chapter_03.docx', sizeBytes:612803, type:'docx', wordCount:4218 } }, mixed: { payload: () => window.SCEN_MIXED, file: { name:'lit_review_v4.pdf', sizeBytes:391022, type:'pdf', wordCount:2604 } }, }; function fileFromUpload(input){ // Normalizes either a real File (live mode) or a mock metadata object // (preview / recent click) into a consistent shape for the analyzing screen. if (input instanceof File){ const ext = (input.name.split('.').pop() || '').toLowerCase(); return { name: input.name, sizeBytes: input.size, type: ext || 'pdf', wordCount: 0, _real: input, }; } return input; } function Header({ apiOk, scenario, mode, onOpenSettings }){ return (
True Detect
Not a score. A verdict with evidence.
engine v0.4.2 mode {mode} scope {scenario}
); } function App(){ const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); // App state machine const [phase, setPhase] = useStateApp('idle'); // idle | analyzing | report const [activeScenario, setActiveScenario] = useStateApp(t.scenario); const [drawerLayer, setDrawerLayer] = useStateApp(null); const [settingsOpen, setSettingsOpen] = useStateApp(false); const [layersOpen, setLayersOpen] = useStateApp(t.showLayersDialogOnLoad); const [apiKey, setApiKey] = useStateApp(t.apiKeyConfigured ? 'sk-td-•••••••••stored' : ''); const [toast, setToast] = useStateApp(null); // Live-mode state const [livePayload, setLivePayload] = useStateApp(null); const [liveFile, setLiveFile] = useStateApp(null); const [pendingResponse, setPendingResponse] = useStateApp(false); const [animationDone, setAnimationDone] = useStateApp(false); // Sync scenario tweak into active scenario when on idle useEffectApp(() => { if (phase === 'idle') setActiveScenario(t.scenario); }, [t.scenario, phase]); // Apply theme to body useEffectApp(() => { document.body.dataset.theme = t.theme || 'dark'; }, [t.theme]); // Trigger error scenarios via tweak useEffectApp(() => { if (t.errorState === 'too_large'){ setToast({ kind:'error', title:'413 · file too large', body:'Maximum upload size is 10 MB. Please compress your PDF or split the document.' }); setPhase('idle'); } else if (t.errorState === 'too_short'){ setToast({ kind:'error', title:'422 · document too short', body:'47 words detected. Minimum 100 words for reliable forensic analysis.' }); setPhase('idle'); } else if (t.errorState === 'invalid_key'){ setToast({ kind:'error', title:'401 · invalid API key', body:'The API key was rejected. Re-enter your key in Settings.', onAction: () => { setSettingsOpen(true); setToast(null); setTweak('errorState','none'); }, actionLabel: 'Open settings' }); setPhase('idle'); } else { setToast(null); } }, [t.errorState]); // Transition from analyzing → report when BOTH the local animation has // finished AND (mock mode OR live response arrived). useEffectApp(() => { if (phase !== 'analyzing') return; if (!animationDone) return; if (t.useMocks){ setPhase('report'); setAnimationDone(false); return; } if (livePayload && !pendingResponse){ setPhase('report'); setAnimationDone(false); } }, [phase, animationDone, livePayload, pendingResponse, t.useMocks]); const apiOk = t.apiKeyConfigured && t.errorState !== 'invalid_key'; const mode = t.useMocks ? 'mock' : 'live'; const startAnalysis = async (input, scenario) => { if (!apiOk){ setSettingsOpen(true); return; } // Mock-mode flow (or non-File input): use fixture scenarios. const isRealFile = input instanceof File; if (t.useMocks || !isRealFile){ setActiveScenario(scenario); setLivePayload(null); setLiveFile(null); setPendingResponse(false); setAnimationDone(false); setPhase(t.skipAnimation ? 'report' : 'analyzing'); return; } // Live-mode flow. setLivePayload(null); setLiveFile(fileFromUpload(input)); setPendingResponse(true); setAnimationDone(false); setPhase('analyzing'); try { const payload = await window.TrueDetectAPI.analyzeDocument(input); setLivePayload(payload); setPendingResponse(false); } catch(err){ setPendingResponse(false); setPhase('idle'); const titles = { invalid_key: '401 · invalid API key', too_large: '413 · file too large', too_short: '422 · document too short', server_error: 'engine error', }; const errKind = (err && err.kind) || 'server_error'; const errBody = (err && err.detail) || 'Unknown error'; const opts = { kind:'error', title: titles[errKind] || 'error', body: errBody }; if (errKind === 'invalid_key'){ opts.onAction = () => { setSettingsOpen(true); setToast(null); }; opts.actionLabel = 'Open settings'; } setToast(opts); } }; const onUploadFromDrop = (info) => startAnalysis(info, t.scenario); const onPickRecent = (scenario) => startAnalysis(SCEN[scenario].file, scenario); const onAnalysisDone = () => setAnimationDone(true); const onCancel = () => { setPhase('idle'); setLivePayload(null); setLiveFile(null); setPendingResponse(false); setAnimationDone(false); }; const onReset = () => { setPhase('idle'); setDrawerLayer(null); setLivePayload(null); setLiveFile(null); }; // Resolve which payload + file to render with. const usingLive = !t.useMocks && livePayload; const payload = usingLive ? livePayload : SCEN[activeScenario].payload(); const file = usingLive ? liveFile : (liveFile && !t.useMocks ? liveFile : SCEN[activeScenario].file); // For mixed scenario, allow toggling whether conflicts are visible const renderPayload = useMemoApp(() => { if (t.useMocks && activeScenario === 'mixed' && !t.showConflictForMixed){ return { ...payload, verdict: { ...payload.verdict, conflicts_detected: false, conflicts: [] } }; } return payload; }, [payload, activeScenario, t.showConflictForMixed, t.useMocks]); return (
setSettingsOpen(true)} />
{phase === 'idle' && ( setLayersOpen(true)} recent={window.FAKE_RECENT} /> )} {phase === 'analyzing' && ( )} {phase === 'report' && ( )}
{drawerLayer && ( setDrawerLayer(null)} /> )} {settingsOpen && ( { setApiKey(k); setTweak('apiKeyConfigured', !!k); if (window.TrueDetectAPI) window.TrueDetectAPI.setApiKey(k); if (t.errorState === 'invalid_key') setTweak('errorState','none'); }} onClose={() => setSettingsOpen(false)} /> )} {layersOpen && setLayersOpen(false)} />} {toast && ( { setToast(null); setTweak('errorState','none'); }} /> )} setTweak('useMocks', v === 'mock')} /> setTweak('theme', v)} /> {t.useMocks && ( <> { setTweak('scenario', v); if (phase !== 'idle') setActiveScenario(v); }} /> {t.scenario === 'mixed' && ( setTweak('showConflictForMixed', v)} /> )} )} setPhase(v)} /> setTweak('skipAnimation', v)} /> setTweak('apiKeyConfigured', v)} /> setTweak('errorState', v)} /> setSettingsOpen(true)} /> setLayersOpen(true)} /> ({ value: l.key, label: `${l.short} · ${l.name}` })), ]} onChange={(v) => { if (phase !== 'report') setPhase('report'); setDrawerLayer(v === 'none' ? null : v); }} />
); } function ReportLayout({ payload, onLayer, onReset }){ return (
{/* Executive summary */}
▸ executive summary
{payload.report.executive_summary}
{/* Doc panel + layer table */}
downloadJson(payload)} onDownloadPdf={() => downloadPdf(payload)} onReset={onReset} />
); } function downloadJson(payload){ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `truedetect-${(payload.document && payload.document.filename) || 'report'}.json`; a.click(); URL.revokeObjectURL(url); } function downloadPdf(payload){ if (payload.pdf_report_url && window.TrueDetectAPI){ const base = window.TrueDetectAPI.getBaseUrl().replace(/\/$/, ''); window.open(base + payload.pdf_report_url, '_blank'); } else { alert('PDF report not available (mock mode or backend not configured).'); } } ReactDOM.createRoot(document.getElementById('root')).render();