Spaces:
Runtime error
Runtime error
| 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 & 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 & 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> | |
| ) | |
| } | |