myke69's picture
Add files using upload-large-folder tool
3752e5d verified
Raw
History Blame Contribute Delete
11.6 kB
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 <Login onSuccess={() => setAuth('ok')} />
return (
<div className="app">
<header className="topbar">
<button className="brand" title="Home" onClick={() => {
setAnalysis(null)
setSelectedClause(null)
setError(null)
setView('analyze')
setShowCompare(false)
}}>
<span className="brand-mark">§</span>
<span>
<strong>Contract Obligation &amp; Risk Extractor</strong>
<small>every finding traced to its source clause</small>
</span>
</button>
<div className="topbar-actions">
<button
className={'nav-btn' + (view === 'portfolio' ? ' active' : '')}
onClick={() => {
setView(view === 'portfolio' ? 'analyze' : 'portfolio')
setShowCompare(false)
}}
>
{view === 'portfolio' ? '← Back' : `Portfolio${existing.length ? ` (${existing.length})` : ''}`}
</button>
<label className="upload-btn">
{loading ? 'Analyzing…' : analysis ? 'Analyze another' : 'Upload contract (PDF)'}
<input
type="file"
accept="application/pdf"
disabled={loading}
onChange={(e) => e.target.files[0] && onUpload(e.target.files[0])}
/>
</label>
{authRequired && (
<button className="nav-btn signout" title="Sign out" onClick={signOut}>
Sign out
</button>
)}
</div>
</header>
{error && <div className="banner error">⚠ {error}</div>}
{view === 'analyze' && analysis?.warnings?.map((w, i) => (
<div key={i} className="banner warn">⚠ {w}</div>
))}
{view === 'portfolio' && (
<Portfolio onOpen={openExisting} onDeleted={refreshExisting} />
)}
{view === 'analyze' && !analysis && !loading && (
<div className="empty">
<div className="empty-card">
<h1>Point it at a contract.</h1>
<p>
Get a structured <strong>obligation &amp; key-date register</strong> and a{' '}
<strong>ranked risk-flag list</strong> — every item linked to the exact clause
it came from. Click any finding to see the source highlighted in the PDF.
</p>
<p className="muted">
100% open-source stack — rule engine + optional local LLM (Ollama).
Scanned PDFs are OCR'd automatically. No contract data ever leaves this server.
</p>
{existing.length > 0 && (
<div className="existing">
<small className="muted">Already analyzed:</small>
{existing.map((c) => (
<button key={c.contract_id} className="existing-btn"
onClick={() => openExisting(c.contract_id)}>
{c.filename} · {c.num_obligations} obligations · {c.num_risks} risks
</button>
))}
</div>
)}
</div>
</div>
)}
{view === 'analyze' && loading && (
<div className="empty">
<div className="empty-card">
<h1>Reading the fine print…</h1>
<p className="muted">parsing → clauses → classification → obligations → dates → risk baseline</p>
</div>
</div>
)}
{view === 'analyze' && analysis && !loading && (
<div className="layout">
<section className="left">
<div className="summary">
<div className="stat">
<b>{analysis.filename}</b>
<small>{analysis.parties.join(' · ') || 'parties unknown'}</small>
</div>
<div className="stat num"><b>{analysis.obligations.length}</b><small>obligations</small></div>
<div className="stat num"><b>{analysis.key_dates.length}</b><small>key dates</small></div>
<div className={'stat num' + (highCount ? ' danger' : '')}>
<b>{highCount}</b><small>high risks</small>
</div>
</div>
<div className="toolbar">
<a className="tool-btn" download
href={authUrl(`/api/contracts/${analysis.contract_id}/export.xlsx`)}>
⬇ Excel register
</a>
<a className="tool-btn" download
href={authUrl(`/api/contracts/${analysis.contract_id}/deadlines.ics`)}>
🗓 Add to calendar
</a>
{existing.length > 1 && (
<button className="tool-btn" onClick={() => setShowCompare(true)}>
⇄ Compare
</button>
)}
</div>
<nav className="tabs">
{TABS.map((t) => (
<button key={t} className={t === tab ? 'active' : ''} onClick={() => setTab(t)}>
{t}
{t === 'Risks' && <span className="pill">{analysis.risks.length}</span>}
{t === 'Obligations' && <span className="pill">{analysis.obligations.length}</span>}
{t === 'Dates' && <span className="pill">{analysis.key_dates.length}</span>}
{t === 'Glossary' && <span className="pill">{analysis.defined_terms.length}</span>}
</button>
))}
</nav>
<div className="panel">
{tab === 'Risks' && (
<RiskList risks={analysis.risks} clauseById={clauseById}
selected={selectedClause} onSelect={selectClause} />
)}
{tab === 'Heatmap' && (
<RiskHeatmap risks={analysis.risks} onSelect={selectClause} />
)}
{tab === 'Obligations' && (
<RegisterTable obligations={analysis.obligations} clauseById={clauseById}
selected={selectedClause} onSelect={selectClause} />
)}
{tab === 'Dates' && (
<KeyDates dates={analysis.key_dates} clauseById={clauseById}
selected={selectedClause} onSelect={selectClause} />
)}
{tab === 'Glossary' && (
<DefinedTerms terms={analysis.defined_terms} onSelect={selectClause} />
)}
{tab === 'Chat' && (
<ContractChat
contractId={analysis.contract_id}
onSelectClause={selectClause}
/>
)}
</div>
</section>
<section className="right">
<PdfViewer
fileUrl={authUrl(`/api/contracts/${analysis.contract_id}/pdf`)}
highlight={selectedClause ? selectedClause.span : null}
/>
</section>
{showCompare && (
<div className="compare-modal-backdrop" onClick={(e) => {
if (e.target === e.currentTarget) setShowCompare(false)
}}>
<div className="compare-modal">
<ContractCompare
currentId={analysis.contract_id}
currentFilename={analysis.filename}
existing={existing}
onClose={() => setShowCompare(false)}
/>
</div>
</div>
)}
</div>
)}
</div>
)
}