import React, { useEffect, useMemo, useState } from 'react' import { api, authUrl, setToken } from './api.js' import PdfViewer from './components/PdfViewer.jsx' import RegisterTable from './components/RegisterTable.jsx' import KeyDates from './components/KeyDates.jsx' import RiskList from './components/RiskList.jsx' import RiskHeatmap from './components/RiskHeatmap.jsx' import DefinedTerms from './components/DefinedTerms.jsx' import ContractChat from './components/ContractChat.jsx' import ContractCompare from './components/ContractCompare.jsx' import Portfolio from './components/Portfolio.jsx' import Login from './components/Login.jsx' const TABS = ['Risks', 'Heatmap', 'Obligations', 'Dates', 'Glossary', 'Chat'] export default function App() { const [auth, setAuth] = useState('checking') // checking | required | ok const [authRequired, setAuthRequired] = useState(false) const [analysis, setAnalysis] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [tab, setTab] = useState('Risks') const [selectedClause, setSelectedClause] = useState(null) const [existing, setExisting] = useState([]) const [view, setView] = useState('analyze') // analyze | portfolio const [showCompare, setShowCompare] = useState(false) useEffect(() => { fetch('/api/health') .then((r) => r.json()) .then(async ({ auth_required }) => { setAuthRequired(auth_required) if (!auth_required) return setAuth('ok') const probe = await api('/api/contracts') setAuth(probe.ok ? 'ok' : 'required') }) .catch(() => setAuth('ok')) }, []) async function refreshExisting() { try { const res = await api('/api/contracts') if (res.status === 401) { setToken(''); setAuth('required'); return } if (res.ok) setExisting(await res.json()) } catch { /* backend down */ } } useEffect(() => { if (auth === 'ok') refreshExisting() }, [auth]) const clauseById = useMemo(() => { const m = new Map() analysis?.clauses?.forEach((c) => m.set(c.id, c)) return m }, [analysis]) async function openExisting(id, clauseId = null) { setLoading(true) setView('analyze') setShowCompare(false) try { const res = await api(`/api/contracts/${id}`) if (res.ok) { const a = await res.json() setAnalysis(a) setTab('Risks') const clause = clauseId && a.clauses.find((c) => c.id === clauseId) setSelectedClause(clause || null) } } finally { setLoading(false) } } async function onUpload(file) { setLoading(true) setError(null) setSelectedClause(null) setShowCompare(false) setView('analyze') try { const form = new FormData() form.append('file', file) const res = await api('/api/contracts', { method: 'POST', body: form }) if (res.status === 401) { setToken(''); setAuth('required'); return } if (!res.ok) { const body = await res.json().catch(() => ({})) throw new Error(body.detail || `Upload failed (${res.status})`) } setAnalysis(await res.json()) setTab('Risks') refreshExisting() } catch (e) { setError(e.message) } finally { setLoading(false) } } const selectClause = (clauseId) => { const clause = clauseById.get(clauseId) if (clause) setSelectedClause(clause) } function signOut() { setToken('') setAnalysis(null) setSelectedClause(null) setExisting([]) setView('analyze') setShowCompare(false) setAuth('required') } const highCount = analysis?.risks?.filter((r) => r.level === 'HIGH').length ?? 0 if (auth === 'checking') return null if (auth === 'required') return setAuth('ok')} /> return (
{authRequired && ( )}
{error &&
⚠ {error}
} {view === 'analyze' && analysis?.warnings?.map((w, i) => (
⚠ {w}
))} {view === 'portfolio' && ( )} {view === 'analyze' && !analysis && !loading && (

Point it at a contract.

Get a structured obligation & key-date register and a{' '} ranked risk-flag list — every item linked to the exact clause it came from. Click any finding to see the source highlighted in the PDF.

100% open-source stack — rule engine + optional local LLM (Ollama). Scanned PDFs are OCR'd automatically. No contract data ever leaves this server.

{existing.length > 0 && (
Already analyzed: {existing.map((c) => ( ))}
)}
)} {view === 'analyze' && loading && (

Reading the fine print…

parsing → clauses → classification → obligations → dates → risk baseline

)} {view === 'analyze' && analysis && !loading && (
{analysis.filename} {analysis.parties.join(' · ') || 'parties unknown'}
{analysis.obligations.length}obligations
{analysis.key_dates.length}key dates
{highCount}high risks
⬇ Excel register 🗓 Add to calendar {existing.length > 1 && ( )}
{tab === 'Risks' && ( )} {tab === 'Heatmap' && ( )} {tab === 'Obligations' && ( )} {tab === 'Dates' && ( )} {tab === 'Glossary' && ( )} {tab === 'Chat' && ( )}
{showCompare && (
{ if (e.target === e.currentTarget) setShowCompare(false) }}>
setShowCompare(false)} />
)}
)}
) }