pageparse.ai / frontend /src /components /PageHistory.tsx
Varun2007's picture
initial clean deployment commit with compilers
8c3e275
Raw
History Blame Contribute Delete
10.7 kB
import { useState } from 'react';
import type { PageRecord, Task } from '../services/api';
import { PageParseAPI, API_BASE_URL } from '../services/api';
import {
History, FileText, X, Calendar, Clock, Copy, Check, Sparkles, Download
} from 'lucide-react';
interface PageHistoryProps {
pages: PageRecord[];
tasks: Task[];
onPagesChanged?: () => void;
onSelectPage?: (page: PageRecord) => void;
}
export const PageHistory = ({ pages, tasks, onPagesChanged, onSelectPage }: PageHistoryProps) => {
const [selectedPage, setSelectedPage] = useState<PageRecord | null>(null);
const [showCleaned, setShowCleaned] = useState(true);
const [copiedRaw, setCopiedRaw] = useState(false);
const [copiedTasks, setCopiedTasks] = useState(false);
const [copiedJSON, setCopiedJSON] = useState(false);
const [isSummarizing, setIsSummarizing] = useState(false);
const openModal = (page: PageRecord) => {
if (onSelectPage) { onSelectPage(page); return; }
setSelectedPage(page);
setShowCleaned(true);
};
const pageTasks = selectedPage ? tasks.filter(t => t.page_id === selectedPage.id || t.source_id === selectedPage.id) : [];
const handleSummarize = async () => {
if (!selectedPage) return;
setIsSummarizing(true);
try {
const summary = await PageParseAPI.summarizePage(selectedPage.id);
setSelectedPage({ ...selectedPage, summary });
onPagesChanged?.();
} catch (err) { console.error(err); }
finally { setIsSummarizing(false); }
};
return (
<div className="section">
<div className="section-header">
<h3><History size={16} className="text-secondary" /> Ingestion History</h3>
<span className="text-xs text-tertiary">{pages.length} sources</span>
</div>
<div className="section-body">
<div className="history-list">
{pages.length === 0 ? (
<div className="history-empty">No pages ingested yet. Upload a file to get started.</div>
) : (
pages.map(page => (
<div key={page.id} className="history-item" onClick={() => openModal(page)}>
<FileText size={16} className="text-tertiary" />
<div style={{ flex: 1 }}>
<div className="history-name">{page.filename}</div>
<div className="history-date">{page.captured_date || new Date(page.created_at).toLocaleDateString()}</div>
</div>
{page.summary && <Sparkles size={14} style={{ color: 'var(--purple)' }} />}
</div>
))
)}
</div>
</div>
{/* MODAL */}
{selectedPage && !onSelectPage && (
<div className="modal-overlay" onClick={() => setSelectedPage(null)}>
<div className="modal" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h3><FileText size={16} className="text-secondary" /> {selectedPage.filename}</h3>
<div className="flex gap-2 items-center">
<button className="btn btn-sm btn-ghost" onClick={async () => {
const json = JSON.stringify(selectedPage, null, 2);
await navigator.clipboard.writeText(json);
setCopiedJSON(true); setTimeout(() => setCopiedJSON(false), 2000);
}}>{copiedJSON ? <Check size={12} /> : <Copy size={12} />} JSON</button>
<button className="btn btn-sm btn-ghost" onClick={() => {
const blob = new Blob([JSON.stringify(selectedPage, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `${selectedPage.filename.split('.')[0]}_data.json`;
a.click(); URL.revokeObjectURL(url);
}}><Download size={12} /></button>
<button className="btn-icon" onClick={() => setSelectedPage(null)}><X size={18} /></button>
</div>
</div>
<div className="modal-body">
<div className="modal-image-col">
<div className="flex items-center justify-between">
<span className="text-xs text-tertiary font-semibold">View</span>
<div className="toggle-group">
<button className={`toggle-btn ${showCleaned ? 'active' : ''}`} onClick={() => setShowCleaned(true)}>Cleaned</button>
<button className={`toggle-btn ${!showCleaned ? 'active' : ''}`} onClick={() => setShowCleaned(false)}>Original</button>
</div>
</div>
<div className="image-box">
{(showCleaned && selectedPage.cleaned_image_path) ? (
<img src={selectedPage.cleaned_image_path.startsWith('/') ? `${API_BASE_URL}${selectedPage.cleaned_image_path}` : selectedPage.cleaned_image_path} alt="Cleaned" />
) : selectedPage.image_path ? (
<img src={selectedPage.image_path.startsWith('/') ? `${API_BASE_URL}${selectedPage.image_path}` : selectedPage.image_path} alt="Original" />
) : (
<div className="flex flex-col items-center gap-2 text-tertiary"><FileText size={48} /><span className="text-xs">No preview</span></div>
)}
</div>
</div>
<div className="modal-info-col">
<div className="meta-grid">
<div className="meta-item">
<Calendar size={14} className="text-tertiary" />
<span className="label">Captured:</span>
<span className="value">{selectedPage.captured_date || '—'}</span>
</div>
<div className="meta-item">
<Clock size={14} className="text-tertiary" />
<span className="label">Ingested:</span>
<span className="value">{new Date(selectedPage.created_at).toLocaleDateString()}</span>
</div>
</div>
{/* Summary */}
<div className="summary-box">
<div className="flex justify-between items-center mb-3">
<div className="flex items-center gap-2">
<Sparkles size={14} className="text-secondary" />
<span className="text-xs font-semibold" style={{ textTransform: 'uppercase', letterSpacing: '0.5px' }}>AI Summary</span>
</div>
<button className="btn btn-sm btn-ghost" onClick={handleSummarize} disabled={isSummarizing}>
{isSummarizing ? '...' : selectedPage.summary ? 'Regenerate' : 'Generate'}
</button>
</div>
{selectedPage.summary ? (
<p className="summary-text">{selectedPage.summary}</p>
) : (
<p className="text-xs text-tertiary" style={{ fontStyle: 'italic' }}>Click Generate to run local AI summarization (Qwen2.5-1.5B).</p>
)}
</div>
{/* Tasks */}
<div>
<div className="flex justify-between items-center mb-3">
<span className="text-xs font-semibold text-tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.5px' }}>Extracted Records ({pageTasks.length})</span>
{pageTasks.length > 0 && (
<button className="btn btn-sm btn-ghost" onClick={async () => {
const text = pageTasks.map(t => `- [${t.status === 'done' ? 'x' : ' '}] ${t.task} (${t.priority})`).join('\n');
await navigator.clipboard.writeText(text);
setCopiedTasks(true); setTimeout(() => setCopiedTasks(false), 2000);
}}>{copiedTasks ? <Check size={12} /> : <Copy size={12} />}</button>
)}
</div>
{pageTasks.length === 0 ? (
<p className="text-xs text-tertiary" style={{ fontStyle: 'italic' }}>No records extracted.</p>
) : (
<div className="derived-list">
{pageTasks.map(t => (
<div key={t.id} className="derived-item">
<span className="dot" />
<span style={{ flex: 1 }}>{t.task}</span>
<span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--text-tertiary)' }}>{(t.ocr_confidence * 100).toFixed(0)}%</span>
{t.priority && <span className={`badge badge-${t.priority}`} style={{ fontSize: 10, padding: '1px 6px' }}>{t.priority}</span>}
</div>
))}
</div>
)}
</div>
{/* Raw Text */}
<div>
<div className="flex justify-between items-center mb-3">
<span className="text-xs font-semibold text-tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.5px' }}>Raw Extract</span>
<div className="flex gap-2">
<button className="btn btn-sm btn-ghost" onClick={() => {
const raw = selectedPage.raw_ocr_text || selectedPage.raw_text || '';
const blob = new Blob([raw], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `${selectedPage.filename.split('.')[0]}.txt`;
a.click(); URL.revokeObjectURL(url);
}}><Download size={12} /></button>
<button className="btn btn-sm btn-ghost" onClick={async () => {
await navigator.clipboard.writeText(selectedPage.raw_ocr_text || selectedPage.raw_text || '');
setCopiedRaw(true); setTimeout(() => setCopiedRaw(false), 2000);
}}>{copiedRaw ? <Check size={12} /> : <Copy size={12} />}</button>
</div>
</div>
<div className="raw-text">
<pre style={{ fontFamily: 'var(--font-mono)', whiteSpace: 'pre-wrap', margin: 0 }}>{(selectedPage.raw_ocr_text || selectedPage.raw_text || 'No text.').slice(0, 1500)}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};