// 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 (
);
}
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();