shreerangss's picture
Prepare production Hugging Face deployment
885aa44
Raw
History Blame Contribute Delete
26.3 kB
import { useEffect, useRef, useState } from 'react';
import './App.css';
const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || '').trim().replace(/\/$/, '');
const API_BASE_LABEL = API_BASE_URL || window.location.origin;
function apiUrl(path) {
return `${API_BASE_URL}${path}`;
}
const MODE_OPTIONS = [
{ value: 'url', label: 'PDF URL' },
{ value: 'upload', label: 'Upload PDF' },
];
const VIEW_OPTIONS = [
{ value: 'query', label: 'Query' },
{ value: 'history', label: 'History' },
];
const ANSWER_STATUS_LABELS = {
ok: 'OK',
no_context: 'No Context',
error: 'Error',
};
const CLAIM_VERDICT_LABELS = {
supported: 'Supported',
weakly_supported: 'Weakly Supported',
unsupported: 'Unsupported',
};
function parseQuestions(text) {
return text
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
}
async function readResponseBody(response) {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return response.json();
}
const text = await response.text();
if (!text) {
return null;
}
try {
return JSON.parse(text);
} catch {
return text;
}
}
function getErrorMessage(payload, fallbackMessage) {
if (!payload) {
return fallbackMessage;
}
if (typeof payload === 'string') {
return payload;
}
if (typeof payload.detail === 'string') {
return payload.detail;
}
if (Array.isArray(payload.detail)) {
return payload.detail
.map((item) => {
if (typeof item === 'string') {
return item;
}
if (item && typeof item.msg === 'string') {
return item.msg;
}
return null;
})
.filter(Boolean)
.join(' ');
}
if (typeof payload.message === 'string') {
return payload.message;
}
return fallbackMessage;
}
function formatFileSize(bytes) {
if (!Number.isFinite(bytes) || bytes <= 0) {
return '';
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
function formatDateTime(value) {
if (!value) {
return 'Unknown date';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return 'Unknown date';
}
return date.toLocaleString();
}
function formatDocumentTitle(document) {
return document.filename || document.source_url || `Document ${document.id.slice(0, 8)}`;
}
function App() {
const fileInputRef = useRef(null);
const [activeView, setActiveView] = useState('query');
const [mode, setMode] = useState('url');
const [documentUrl, setDocumentUrl] = useState('');
const [selectedFile, setSelectedFile] = useState(null);
const [questionsText, setQuestionsText] = useState('');
const [token, setToken] = useState('');
const [showToken, setShowToken] = useState(false);
const [answers, setAnswers] = useState([]);
const [formError, setFormError] = useState('');
const [requestError, setRequestError] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [runMeta, setRunMeta] = useState({ cacheStatus: '', cacheEntries: '' });
const [historyDocuments, setHistoryDocuments] = useState([]);
const [historyQueries, setHistoryQueries] = useState([]);
const [selectedHistoryDocument, setSelectedHistoryDocument] = useState(null);
const [historyLoading, setHistoryLoading] = useState(false);
const [historyError, setHistoryError] = useState('');
const [health, setHealth] = useState({
loading: true,
error: '',
data: null,
});
const questions = parseQuestions(questionsText);
async function refreshHealth() {
setHealth((current) => ({
...current,
loading: true,
error: '',
}));
try {
const response = await fetch(apiUrl('/health'));
const payload = await readResponseBody(response);
if (!response.ok) {
throw new Error(getErrorMessage(payload, `Health check failed with status ${response.status}.`));
}
setHealth({
loading: false,
error: '',
data: payload,
});
} catch (error) {
setHealth({
loading: false,
error: error instanceof Error ? error.message : 'Unable to reach the backend.',
data: null,
});
}
}
useEffect(() => {
void refreshHealth();
}, []);
function getAuthHeader() {
return { Authorization: `Bearer ${token.trim()}` };
}
function validateHistoryAccess() {
if (!token.trim()) {
setHistoryError('API token is required.');
return false;
}
return true;
}
async function loadHistoryDocuments() {
if (!validateHistoryAccess()) {
return;
}
setHistoryLoading(true);
setHistoryError('');
setSelectedHistoryDocument(null);
setHistoryQueries([]);
try {
const response = await fetch(apiUrl('/history/documents'), {
headers: getAuthHeader(),
});
const payload = await readResponseBody(response);
if (!response.ok) {
throw new Error(getErrorMessage(payload, `History request failed with status ${response.status}.`));
}
setHistoryDocuments(Array.isArray(payload?.documents) ? payload.documents : []);
} catch (error) {
setHistoryError(error instanceof Error ? error.message : 'Unable to load history.');
} finally {
setHistoryLoading(false);
}
}
async function loadHistoryQueries(document) {
if (!validateHistoryAccess()) {
return;
}
setHistoryLoading(true);
setHistoryError('');
setSelectedHistoryDocument(document);
setHistoryQueries([]);
try {
const response = await fetch(apiUrl(`/history/documents/${encodeURIComponent(document.id)}/queries`), {
headers: getAuthHeader(),
});
const payload = await readResponseBody(response);
if (!response.ok) {
throw new Error(getErrorMessage(payload, `History request failed with status ${response.status}.`));
}
setHistoryQueries(Array.isArray(payload?.queries) ? payload.queries : []);
} catch (error) {
setHistoryError(error instanceof Error ? error.message : 'Unable to load query history.');
} finally {
setHistoryLoading(false);
}
}
function handleViewChange(nextView) {
setActiveView(nextView);
setHistoryError('');
if (nextView === 'history' && historyDocuments.length === 0 && token.trim()) {
void loadHistoryDocuments();
}
}
function handleModeChange(nextMode) {
setMode(nextMode);
setFormError('');
setRequestError('');
}
function handleFileChange(event) {
const nextFile = event.target.files?.[0] || null;
setFormError('');
if (!nextFile) {
setSelectedFile(null);
return;
}
const isPdf = nextFile.type === 'application/pdf' || nextFile.name.toLowerCase().endsWith('.pdf');
if (!isPdf) {
setSelectedFile(null);
setFormError('Please choose a PDF file.');
event.target.value = '';
return;
}
setSelectedFile(nextFile);
}
function handleReset() {
setMode('url');
setDocumentUrl('');
setSelectedFile(null);
setQuestionsText('');
setToken('');
setShowToken(false);
setAnswers([]);
setFormError('');
setRequestError('');
setRunMeta({ cacheStatus: '', cacheEntries: '' });
setHistoryDocuments([]);
setHistoryQueries([]);
setSelectedHistoryDocument(null);
setHistoryError('');
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
function validateForm() {
if (!token.trim()) {
return 'API token is required.';
}
if (mode === 'url' && !documentUrl.trim()) {
return 'PDF URL is required.';
}
if (mode === 'upload' && !selectedFile) {
return 'Please choose a PDF file to upload.';
}
if (questions.length === 0) {
return 'Enter at least one question.';
}
return '';
}
async function handleSubmit(event) {
event.preventDefault();
const validationMessage = validateForm();
if (validationMessage) {
setFormError(validationMessage);
return;
}
setFormError('');
setRequestError('');
setAnswers([]);
setRunMeta({ cacheStatus: '', cacheEntries: '' });
setIsSubmitting(true);
try {
const authHeader = getAuthHeader();
let response;
if (mode === 'url') {
response = await fetch(apiUrl('/hackrx/run'), {
method: 'POST',
headers: {
...authHeader,
'Content-Type': 'application/json',
},
body: JSON.stringify({
documents: documentUrl.trim(),
questions,
}),
});
} else {
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('questions_json', JSON.stringify(questions));
response = await fetch(apiUrl('/hackrx/upload-run'), {
method: 'POST',
headers: authHeader,
body: formData,
});
}
const payload = await readResponseBody(response);
if (!response.ok) {
throw new Error(getErrorMessage(payload, `Request failed with status ${response.status}.`));
}
setAnswers(Array.isArray(payload?.answers) ? payload.answers : []);
setRunMeta({
cacheStatus: response.headers.get('X-Document-Cache') || '',
cacheEntries: response.headers.get('X-Cache-Entries') || '',
});
await refreshHealth();
} catch (error) {
setRequestError(error instanceof Error ? error.message : 'Request failed.');
} finally {
setIsSubmitting(false);
}
}
const healthStatusClass = health.loading ? 'checking' : health.data?.status === 'healthy' ? 'healthy' : 'unhealthy';
const healthStatusLabel = health.loading ? 'Checking' : health.data?.status === 'healthy' ? 'Healthy' : 'Unhealthy';
return (
<div className="app-shell">
<header className="hero">
<p className="eyebrow">Intelligent Document Query Engine</p>
<h1>Ask questions over PDF documents</h1>
<p className="hero-subtitle">
Ask questions over PDF documents using retrieval-augmented generation.
</p>
</header>
<div className="view-switch" role="tablist" aria-label="Application view">
{VIEW_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
className={`view-button ${activeView === option.value ? 'is-active' : ''}`}
onClick={() => handleViewChange(option.value)}
>
{option.label}
</button>
))}
</div>
{activeView === 'query' ? (
<>
<div className="top-grid">
<form className="panel" onSubmit={handleSubmit}>
<div className="panel-header">
<div>
<p className="panel-kicker">Query setup</p>
<h2>Document input</h2>
</div>
<span className="subtle-copy">{questions.length} question{questions.length === 1 ? '' : 's'}</span>
</div>
<div className="mode-switch" role="tablist" aria-label="PDF input mode">
{MODE_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
className={`mode-button ${mode === option.value ? 'is-active' : ''}`}
onClick={() => handleModeChange(option.value)}
>
{option.label}
</button>
))}
</div>
{mode === 'url' ? (
<label className="field">
<span className="field-label">PDF URL</span>
<input
type="url"
className="field-input"
placeholder="https://example.com/document.pdf"
value={documentUrl}
onChange={(event) => setDocumentUrl(event.target.value)}
/>
</label>
) : (
<label className="field">
<span className="field-label">Upload PDF</span>
<input
ref={fileInputRef}
type="file"
className="field-input field-input-file"
accept=".pdf,application/pdf"
onChange={handleFileChange}
/>
<span className="field-help">
{selectedFile
? `${selectedFile.name} ${formatFileSize(selectedFile.size)}`.trim()
: 'Choose a PDF file from your machine.'}
</span>
</label>
)}
<label className="field">
<span className="field-label">Questions</span>
<textarea
className="field-input field-textarea"
placeholder={'What is this document about?\nWhat are the key dates?\nWhich sections mention exclusions?'}
value={questionsText}
onChange={(event) => setQuestionsText(event.target.value)}
/>
<span className="field-help">Enter one question per line</span>
</label>
<label className="field">
<span className="field-label">Bearer token</span>
<div className="token-row">
<input
type={showToken ? 'text' : 'password'}
className="field-input"
placeholder="Paste the API token used by the backend"
value={token}
onChange={(event) => setToken(event.target.value)}
/>
<button
type="button"
className="ghost-button"
onClick={() => setShowToken((current) => !current)}
>
{showToken ? 'Hide' : 'Show'}
</button>
</div>
</label>
{formError ? <div className="notice notice-error">{formError}</div> : null}
<div className="actions">
<button type="submit" className="primary-button" disabled={isSubmitting}>
{isSubmitting ? 'Running query...' : 'Run Query'}
</button>
<button type="button" className="secondary-button" onClick={handleReset} disabled={isSubmitting}>
Clear
</button>
</div>
</form>
<aside className="panel status-panel">
<div className="panel-header">
<div>
<p className="panel-kicker">Service status</p>
<h2>Backend health</h2>
</div>
<button type="button" className="ghost-button" onClick={() => void refreshHealth()} disabled={health.loading}>
Refresh
</button>
</div>
<div className="health-card">
<div className="health-summary">
<span className={`status-pill status-${healthStatusClass}`}>{healthStatusLabel}</span>
<span className="version-copy">
{health.data?.version ? `v${health.data.version}` : 'No version available'}
</span>
</div>
<dl className="health-grid">
<div>
<dt>API base</dt>
<dd>{API_BASE_LABEL}</dd>
</div>
<div>
<dt>Cache entries</dt>
<dd>{health.data?.cache_entries ?? 'n/a'}</dd>
</div>
<div>
<dt>Embeddings</dt>
<dd>{health.data ? (health.data.embedding_model_loaded ? 'Loaded' : 'Idle') : 'Unknown'}</dd>
</div>
<div>
<dt>Reranker</dt>
<dd>{health.data ? (health.data.reranker_loaded ? 'Loaded' : 'Idle') : 'Unknown'}</dd>
</div>
<div>
<dt>Groq client</dt>
<dd>{health.data ? (health.data.groq_client_loaded ? 'Loaded' : 'Idle') : 'Unknown'}</dd>
</div>
</dl>
{health.error ? <p className="status-message">{health.error}</p> : null}
</div>
<div className="support-card">
<p className="support-title">Supported modes</p>
<p className="support-copy">Run the existing URL workflow or upload a local PDF without changing the backend contract.</p>
</div>
</aside>
</div>
<section className="panel results-panel">
<div className="panel-header">
<div>
<p className="panel-kicker">Results</p>
<h2>Answers</h2>
</div>
<div className="results-meta">
{runMeta.cacheStatus ? (
<span>
Cache {runMeta.cacheStatus}
{runMeta.cacheEntries ? ` | ${runMeta.cacheEntries} entr${runMeta.cacheEntries === '1' ? 'y' : 'ies'}` : ''}
</span>
) : (
<span>Ready for a demo run</span>
)}
</div>
</div>
{requestError ? <div className="notice notice-error">{requestError}</div> : null}
{isSubmitting ? (
<div className="loading-box" aria-live="polite">
<span className="spinner" aria-hidden="true" />
Running document query...
</div>
) : null}
{!isSubmitting && !requestError && answers.length === 0 ? (
<div className="empty-state">
Submit a PDF URL or upload a PDF to see grounded answers with source excerpts.
</div>
) : null}
<div className="answers-grid">
{answers.map((answer, index) => (
<article className="answer-card" key={`${answer.question}-${index}`}>
<div className="answer-header">
<div>
<p className="section-label">Question</p>
<h3>{answer.question}</h3>
</div>
<span className={`status-pill status-${answer.status || 'error'}`}>
{ANSWER_STATUS_LABELS[answer.status] || answer.status || 'Unknown'}
</span>
</div>
<div className="answer-copy">
<p className="section-label">Answer</p>
<p>{answer.answer}</p>
</div>
{answer.claim_verifications?.length ? (
<div className="claim-section">
<div className="source-header">
<p className="section-label">Claim Verification</p>
<span className="subtle-copy">{answer.claim_verifications.length}</span>
</div>
<div className="claim-list">
{answer.claim_verifications.map((verification, verificationIndex) => (
<div className="claim-card" key={`${verification.claim}-${verificationIndex}`}>
<div className="claim-card-header">
<p className="claim-text">{verification.claim}</p>
<span className={`status-pill status-${verification.verdict || 'unsupported'}`}>
{CLAIM_VERDICT_LABELS[verification.verdict] || verification.verdict || 'Unknown'}
</span>
</div>
<p className="claim-rationale">{verification.rationale}</p>
<div className="claim-sources">
<div className="claim-sources-header">
<p className="section-label">Supporting Sources</p>
<span className="subtle-copy">{verification.sources?.length || 0}</span>
</div>
{verification.sources?.length ? (
<div className="source-list claim-source-list">
{verification.sources.map((source, sourceIndex) => (
<div className="source-card claim-source-card" key={`${source.page}-${source.chunk_id}-${sourceIndex}`}>
<div className="source-meta">
<span>Page {source.page}</span>
<span>Chunk {source.chunk_id}</span>
</div>
<p>{source.excerpt}</p>
</div>
))}
</div>
) : (
<p className="empty-sources">No supporting sources were selected for this claim.</p>
)}
</div>
</div>
))}
</div>
</div>
) : null}
<div className="source-section">
<div className="source-header">
<p className="section-label">Sources</p>
<span className="subtle-copy">{answer.sources?.length || 0}</span>
</div>
{answer.sources?.length ? (
<div className="source-list">
{answer.sources.map((source, sourceIndex) => (
<div className="source-card" key={`${source.page}-${source.chunk_id}-${sourceIndex}`}>
<div className="source-meta">
<span>Page {source.page}</span>
<span>Chunk {source.chunk_id}</span>
</div>
<p>{source.excerpt}</p>
</div>
))}
</div>
) : (
<p className="empty-sources">No source excerpts were returned for this answer.</p>
)}
</div>
</article>
))}
</div>
</section>
</>
) : (
<section className="panel history-panel">
<div className="panel-header">
<div>
<p className="panel-kicker">History</p>
<h2>{selectedHistoryDocument ? 'Document queries' : 'Documents'}</h2>
</div>
<div className="history-actions">
{selectedHistoryDocument ? (
<button
type="button"
className="ghost-button"
onClick={() => {
setSelectedHistoryDocument(null);
setHistoryQueries([]);
setHistoryError('');
}}
disabled={historyLoading}
>
Back
</button>
) : null}
<button
type="button"
className="ghost-button"
onClick={() => void loadHistoryDocuments()}
disabled={historyLoading}
>
Refresh
</button>
</div>
</div>
{selectedHistoryDocument ? (
<div className="history-context">
<p className="section-label">Selected document</p>
<h3>{formatDocumentTitle(selectedHistoryDocument)}</h3>
<p>
{selectedHistoryDocument.source_type} | {selectedHistoryDocument.chunk_count} chunks |{' '}
{selectedHistoryDocument.query_count} queries
</p>
</div>
) : null}
{historyError ? <div className="notice notice-error">{historyError}</div> : null}
{historyLoading ? (
<div className="loading-box" aria-live="polite">
<span className="spinner" aria-hidden="true" />
Loading history...
</div>
) : null}
{!historyLoading && !historyError && !selectedHistoryDocument && historyDocuments.length === 0 ? (
<div className="empty-state">No persisted documents found for the current user.</div>
) : null}
{!historyLoading && !historyError && selectedHistoryDocument && historyQueries.length === 0 ? (
<div className="empty-state">No persisted queries found for this document.</div>
) : null}
{!selectedHistoryDocument ? (
<div className="history-list">
{historyDocuments.map((document) => (
<button
type="button"
className="history-row"
key={document.id}
onClick={() => void loadHistoryQueries(document)}
>
<span>
<strong>{formatDocumentTitle(document)}</strong>
<span className="history-meta">
{document.source_type} | {document.status} | {formatDateTime(document.created_at)}
</span>
</span>
<span className="history-counts">
{document.chunk_count} chunks
<br />
{document.query_count} queries
</span>
</button>
))}
</div>
) : (
<div className="history-list">
{historyQueries.map((query) => (
<article className="history-query" key={query.id}>
<div className="history-query-header">
<div>
<p className="section-label">Question</p>
<h3>{query.question}</h3>
</div>
<span className={`status-pill status-${query.status || 'error'}`}>
{query.is_abstained ? 'Abstained' : ANSWER_STATUS_LABELS[query.status] || query.status || 'Unknown'}
</span>
</div>
<p>{query.answer}</p>
<div className="history-meta">
{formatDateTime(query.created_at)}
{Number.isFinite(query.latency_ms) ? ` | ${Math.round(query.latency_ms)} ms` : ''}
</div>
</article>
))}
</div>
)}
</section>
)}
</div>
);
}
export default App;