Varun2007's picture
initial clean deployment commit with compilers
8c3e275
Raw
History Blame Contribute Delete
12.1 kB
import { useEffect, useState, useCallback } from 'react';
import { PageParseAPI, getAirgapMode, setAirgapMode, subscribeToJobs, API_BASE_URL } from './services/api';
import type { Task, PageRecord } from './services/api';
import { FileUploader } from './components/FileUploader';
import { CodeEditor } from './components/CodeEditor';
import { TaskList } from './components/TaskList';
import { TelemetryPanel } from './components/TelemetryPanel';
import { PageHistory } from './components/PageHistory';
import { PageDetails } from './components/PageDetails';
import { ChatPanel } from './components/ChatPanel';
import {
ScanFace, Upload, Database, History, MessageSquare, Code, Cpu,
Wifi, WifiOff, Sun, Moon, Settings
} from 'lucide-react';
type View = 'upload' | 'tasks' | 'history' | 'chat' | 'code';
function App() {
const [tasks, setTasks] = useState<Task[]>([]);
const [pages, setPages] = useState<PageRecord[]>([]);
const [isAirgapped, setIsAirgapped] = useState(getAirgapMode());
const [serverOnline, setServerOnline] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [selectedPage, setSelectedPage] = useState<PageRecord | null>(null);
const [activeView, setActiveView] = useState<View>('upload');
const [isDarkMode, setIsDarkMode] = useState(() => {
const saved = localStorage.getItem('pageparse_theme');
if (saved) return saved === 'dark';
return window.matchMedia('(prefers-color-scheme: dark)').matches;
});
const [showSettings, setShowSettings] = useState(false);
const [webhookUrl, setWebhookUrl] = useState('');
const [webhookConfigured, setWebhookConfigured] = useState(false);
const loadData = useCallback(async () => {
try {
const [allTasks, allPages] = await Promise.all([
PageParseAPI.getTasks(),
PageParseAPI.getPages(),
]);
setTasks(allTasks);
setPages(allPages);
if (selectedPage) {
const updated = allPages.find(p => p.id === selectedPage.id);
if (updated) setSelectedPage(updated);
}
} catch (err) {
console.error('Error loading data:', err);
}
}, [selectedPage]);
const checkConnectivity = useCallback(async () => {
if (isAirgapped) { setServerOnline(false); return; }
setServerOnline(await PageParseAPI.checkServerConnection());
}, [isAirgapped]);
useEffect(() => {
const unsub = subscribeToJobs((jobs) => {
const processing = jobs.some(j => j.status !== 'saved' && j.status !== 'failed');
setIsProcessing(processing);
if (jobs.some(j => j.status === 'saved')) loadData();
});
return () => unsub();
}, [loadData]);
useEffect(() => {
checkConnectivity();
loadData();
const interval = setInterval(() => { if (!isAirgapped) checkConnectivity(); }, 10000);
return () => clearInterval(interval);
}, [isAirgapped, checkConnectivity, loadData]);
useEffect(() => {
document.documentElement.setAttribute('data-theme', isDarkMode ? 'dark' : 'light');
localStorage.setItem('pageparse_theme', isDarkMode ? 'dark' : 'light');
}, [isDarkMode]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return;
if (e.ctrlKey || e.metaKey) return;
switch (e.key) {
case '1': setActiveView('upload'); break;
case '2': setActiveView('tasks'); break;
case '3': setActiveView('history'); break;
case '4': setActiveView('chat'); break;
case '5': setActiveView('code'); break;
case 't': case 'T': setIsDarkMode(prev => !prev); break;
case '?': setShowSettings(prev => !prev); break;
case 'Escape': setShowSettings(false); break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
useEffect(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/static/sw.js').catch(() => {});
}
}, []);
const handleAirgapChange = () => {
const newVal = !isAirgapped;
setIsAirgapped(newVal);
setAirgapMode(newVal);
};
const handleWebhookConfig = async () => {
try {
await fetch(`${API_BASE_URL}/webhook`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: webhookUrl, secret: '' }),
});
setWebhookConfigured(true);
setTimeout(() => setShowSettings(false), 1500);
} catch (err) {
console.error(err);
}
};
const navItems: { view: View; icon: typeof Upload; label: string; shortcut: string }[] = [
{ view: 'upload', icon: Upload, label: 'Ingest', shortcut: '1' },
{ view: 'tasks', icon: Database, label: 'Records', shortcut: '2' },
{ view: 'history', icon: History, label: 'History', shortcut: '3' },
{ view: 'chat', icon: MessageSquare, label: 'AI Chat', shortcut: '4' },
{ view: 'code', icon: Code, label: 'Code', shortcut: '5' },
];
if (selectedPage) {
return (
<PageDetails
page={selectedPage}
tasks={tasks}
onBack={() => setSelectedPage(null)}
onPageChanged={loadData}
/>
);
}
return (
<div className="app-shell">
{/* SIDEBAR */}
<aside className="sidebar">
<div className="sidebar-brand" onClick={() => setActiveView('upload')}>
<ScanFace />
<h1>PageParse</h1>
</div>
{navItems.map(item => (
<button
key={item.view}
className={`nav-item ${activeView === item.view ? 'active' : ''}`}
onClick={() => setActiveView(item.view)}
title={`${item.label} (${item.shortcut})`}
>
<item.icon />
<span>{item.label}</span>
<span className="shortcut-hint">{item.shortcut}</span>
</button>
))}
<div className="sidebar-spacer" />
<div className="sidebar-footer">
<button className="nav-item" onClick={() => setIsDarkMode(prev => !prev)} title="Toggle theme (T)">
{isDarkMode ? <Sun size={18} /> : <Moon size={18} />}
<span>{isDarkMode ? 'Light Mode' : 'Dark Mode'}</span>
</button>
<button className="nav-item" onClick={() => setShowSettings(true)} title="Settings (?)">
<Settings size={18} />
<span>Settings</span>
</button>
<div className="switch-row">
<span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>Air-gap</span>
<label className="switch">
<input type="checkbox" checked={isAirgapped} onChange={handleAirgapChange} />
<span className="slider" />
</label>
</div>
<div className={`connection-badge ${serverOnline ? 'online' : 'offline'}`}>
{serverOnline ? <Wifi size={14} /> : <WifiOff size={14} />}
<span>{isAirgapped ? 'Air-gapped' : serverOnline ? 'Online' : 'Offline'}</span>
</div>
<div className="text-xs text-tertiary" style={{ textAlign: 'center' }}>
? for keyboard shortcuts
</div>
</div>
</aside>
{/* MAIN */}
<main className="main-content">
<div className="page-header">
<h2>
{activeView === 'upload' && 'Ingestion Portal'}
{activeView === 'tasks' && `Records (${tasks.length})`}
{activeView === 'history' && `History (${pages.length})`}
{activeView === 'chat' && 'AI Assistant'}
{activeView === 'code' && 'Code Paste & Analyze'}
</h2>
<div className="header-actions">
<div className="switch-row">
<Cpu size={16} className={isProcessing ? 'animate-pulse' : ''} style={{ color: isProcessing ? 'var(--purple)' : 'var(--text-tertiary)' }} />
<span className="text-xs text-tertiary">{isProcessing ? 'Inference Active' : 'Idle'}</span>
</div>
</div>
</div>
{activeView === 'upload' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
<FileUploader
onJobStarted={() => setIsProcessing(true)}
onJobFinished={() => { setTimeout(loadData, 500); }}
isAirgapped={isAirgapped}
/>
<TelemetryPanel isProcessing={isProcessing} />
<TaskList tasks={tasks} onTasksChanged={loadData} variant="compact" />
</div>
)}
{activeView === 'tasks' && (
<TaskList tasks={tasks} onTasksChanged={loadData} />
)}
{activeView === 'history' && (
<PageHistory
pages={pages}
tasks={tasks}
onPagesChanged={loadData}
onSelectPage={setSelectedPage}
/>
)}
{activeView === 'chat' && (
<div className="section" style={{ maxWidth: 720 }}>
<div className="section-header">
<h3><MessageSquare size={16} className="text-secondary" /> PageParse AI Chat</h3>
</div>
<div className="section-body">
<ChatPanel />
</div>
</div>
)}
{activeView === 'code' && (
<CodeEditor />
)}
</main>
{/* SETTINGS MODAL */}
{showSettings && (
<div className="modal-overlay" onClick={() => setShowSettings(false)}>
<div className="modal" style={{ maxWidth: 500 }} onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h3><Settings size={16} /> Settings</h3>
</div>
<div className="modal-body" style={{ gridTemplateColumns: '1fr' }}>
<div className="flex flex-col gap-4">
<div>
<label className="text-xs font-semibold text-tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.5px' }}>Webhook URL</label>
<div className="flex gap-2 mt-2">
<input className="input" value={webhookUrl} onChange={e => setWebhookUrl(e.target.value)} placeholder="https://hooks.example.com/pageparse" />
<button className="btn btn-sm btn-primary" onClick={handleWebhookConfig}>Save</button>
</div>
{webhookConfigured && <div className="text-xs text-success mt-1">Webhook configured!</div>}
</div>
<div>
<label className="text-xs font-semibold text-tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.5px' }}>Keyboard Shortcuts</label>
<div className="flex flex-col gap-1 mt-2 text-sm">
<div className="flex justify-between"><span>Ingest view</span><span className="font-mono text-tertiary">1</span></div>
<div className="flex justify-between"><span>Records view</span><span className="font-mono text-tertiary">2</span></div>
<div className="flex justify-between"><span>History view</span><span className="font-mono text-tertiary">3</span></div>
<div className="flex justify-between"><span>AI Chat view</span><span className="font-mono text-tertiary">4</span></div>
<div className="flex justify-between"><span>Code view</span><span className="font-mono text-tertiary">5</span></div>
<div className="flex justify-between"><span>Toggle theme</span><span className="font-mono text-tertiary">T</span></div>
<div className="flex justify-between"><span>Settings</span><span className="font-mono text-tertiary">?</span></div>
<div className="flex justify-between"><span>Close modal</span><span className="font-mono text-tertiary">Esc</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
}
export default App;