gauthamnairy's picture
Upload 41 files
609c821 verified
import React, { useState, useCallback, useEffect } from 'react';
import * as XLSX from 'xlsx';
import { AppState, AnalysisResult, ExtractedDataRow, TableData, AIModel, AVAILABLE_MODELS } from './types';
import { Icons, APP_NAME, APP_VERSION } from './constants';
import FileUpload from './components/FileUpload';
import ProcessingView from './components/ProcessingView';
import DataGrid from './components/DataGrid';
import DataVisualizer from './components/DataVisualizer';
import PdfViewer from './components/PdfViewer';
import ChatInterface from './components/ChatInterface';
import ModelSelector from './components/ModelSelector';
import { analyzeDocument } from './services/geminiService';
const App: React.FC = () => {
const [state, setState] = useState<AppState>(AppState.IDLE);
const [currentFile, setCurrentFile] = useState<File | null>(null);
const [fileBase64, setFileBase64] = useState<string | null>(null);
const [result, setResult] = useState<AnalysisResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [selectedModel, setSelectedModel] = useState<AIModel>(AVAILABLE_MODELS[0]);
// UI State
const [activeTab, setActiveTab] = useState<'tables' | 'visuals' | 'chat'>('tables');
const [unitSystem, setUnitSystem] = useState<'Original' | 'Metric' | 'Imperial'>('Original');
const [currentPdfPage, setCurrentPdfPage] = useState<number>(1);
const [usingRLM, setUsingRLM] = useState<boolean>(false);
// RLM threshold - should match backend
const RLM_PAGE_THRESHOLD = 300;
const handleFileSelect = useCallback(async (file: File) => {
setCurrentFile(file);
setState(AppState.UPLOADING);
setError(null);
const reader = new FileReader();
reader.onload = async () => {
const base64 = (reader.result as string).split(',')[1];
setFileBase64(base64);
try {
setState(AppState.ANALYZING);
// Check if document is large enough to use RLM
// For PDFs, we can estimate page count from file size (rough: ~50KB per page average)
if (file.type === 'application/pdf') {
// Just estimation log, actual decision is now user-controlled
const estimatedPages = Math.ceil(file.size / (50 * 1024));
console.log(`[App] PDF detected (~${estimatedPages} pages estimated). RLM Mode: ${usingRLM}`);
}
// Analysis call with manual RLM toggle
const analysisData = await analyzeDocument(base64, file.type, selectedModel, usingRLM);
// Update usingRLM based on actual backend response
if (analysisData.using_rlm) {
setUsingRLM(true);
}
analysisData.metadata = {
...analysisData.metadata,
fileName: file.name,
fileSize: file.size,
fileType: file.type
};
setResult(analysisData);
setState(AppState.COMPLETE);
} catch (err: any) {
console.error(err);
setError(err.message || "An unexpected error occurred during processing.");
setState(AppState.ERROR);
}
};
reader.onerror = () => {
setError("Failed to read file.");
setState(AppState.ERROR);
};
reader.readAsDataURL(file);
}, [selectedModel]);
// Handle Editing from DataGrid
const handleTableUpdate = (tableIndex: number, newRows: ExtractedDataRow[]) => {
if (!result) return;
const newTables = [...result.tables];
newTables[tableIndex] = { ...newTables[tableIndex], rows: newRows };
setResult({ ...result, tables: newTables });
};
// Excel Export
const handleExcelExport = () => {
if (!result?.tables) return;
const wb = XLSX.utils.book_new();
// Create Summary Sheet
const summaryData = [
["File Name", result.metadata.fileName],
["Date Processed", result.metadata.dateProcessed],
["Executive Summary", result.summary]
];
const summaryWs = XLSX.utils.aoa_to_sheet(summaryData);
XLSX.utils.book_append_sheet(wb, summaryWs, "Summary");
// Create Sheet for each extracted table
result.tables.forEach((table, idx) => {
if (!table.rows || table.rows.length === 0) return;
// Remove internal confidence key for export
const cleanRows = table.rows.map(r => {
const { __confidence, ...rest } = r;
return rest;
});
const ws = XLSX.utils.json_to_sheet(cleanRows);
// Sheet names limited to 31 chars
const sheetName = table.title.replace(/[\[\]\*\/\\\?]/g, '').substring(0, 31) || `Table ${idx} `;
XLSX.utils.book_append_sheet(wb, ws, sheetName);
});
XLSX.writeFile(wb, `PetroMind_Export_${Date.now()}.xlsx`);
};
// Unit Conversion Helper
const getDisplayedTables = (): TableData[] => {
if (!result) return [];
if (unitSystem === 'Original') return result.tables;
// Deep copy to avoid mutating state directly during render
const convertedTables = JSON.parse(JSON.stringify(result.tables));
return convertedTables.map((table: TableData) => {
const newRows = table.rows.map((row) => {
const newRow: ExtractedDataRow = {};
Object.keys(row).forEach(key => {
let val = row[key];
let newKey = key;
// Simple Heuristic Conversion Logic
if (typeof val === 'number' || (typeof val === 'string' && !isNaN(parseFloat(val)))) {
const numVal = parseFloat(val as string);
if (unitSystem === 'Metric') {
if (key.match(/_ft$|_feet$/i)) {
newKey = key.replace(/_ft$|_feet$/i, '_m');
newRow[newKey] = (numVal * 0.3048).toFixed(2);
return;
}
if (key.match(/_psi$/i)) {
newKey = key.replace(/_psi$/i, '_bar');
newRow[newKey] = (numVal * 0.0689476).toFixed(2);
return;
}
} else if (unitSystem === 'Imperial') {
if (key.match(/_m$|_meter$/i)) {
newKey = key.replace(/_m$|_meter$/i, '_ft');
newRow[newKey] = (numVal * 3.28084).toFixed(2);
return;
}
if (key.match(/_bar$/i)) {
newKey = key.replace(/_bar$/i, '_psi');
newRow[newKey] = (numVal * 14.5038).toFixed(2);
return;
}
}
}
newRow[newKey] = val;
});
return newRow;
});
return { ...table, rows: newRows };
});
};
const displayedTables = getDisplayedTables();
const reset = () => {
setState(AppState.IDLE);
setCurrentFile(null);
setFileBase64(null);
setResult(null);
setError(null);
};
return (
<div className="h-screen bg-industrial-950 text-gray-100 font-sans selection:bg-petro-500 selection:text-white flex flex-col overflow-hidden">
{/* Header */}
<header className="border-b border-industrial-900 bg-industrial-950/80 backdrop-blur-md shrink-0">
<div className="w-full px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="p-2 bg-gradient-to-br from-petro-600 to-petro-800 rounded-lg shadow-lg shadow-petro-900/50">
<Icons.Activity />
</div>
<div>
<h1 className="text-xl font-display font-bold tracking-tight text-white">{APP_NAME}</h1>
<p className="text-xs text-petro-400 font-mono">ENTERPRISE EDITION</p>
</div>
</div>
<div className="flex items-center space-x-4">
{state === AppState.COMPLETE && (
<div className="flex bg-industrial-900 rounded-lg p-1 border border-industrial-800">
<button onClick={() => setUnitSystem('Original')} className={`px-3 py-1 text-xs font-medium rounded ${unitSystem === 'Original' ? 'bg-industrial-700 text-white' : 'text-gray-400'}`}>Orig</button>
<button onClick={() => setUnitSystem('Metric')} className={`px-3 py-1 text-xs font-medium rounded ${unitSystem === 'Metric' ? 'bg-petro-700 text-white' : 'text-gray-400'}`}>Met</button>
<button onClick={() => setUnitSystem('Imperial')} className={`px-3 py-1 text-xs font-medium rounded ${unitSystem === 'Imperial' ? 'bg-petro-700 text-white' : 'text-gray-400'}`}>Imp</button>
</div>
)}
<span className="hidden md:inline text-xs text-gray-600 font-mono">{APP_VERSION}</span>
</div>
</div>
</header>
{/* Main Content Area */}
<main className="flex-1 overflow-hidden relative">
{state === AppState.IDLE && (
<div className="h-full overflow-y-auto p-8 flex items-center justify-center">
<div className="max-w-2xl w-full animate-fade-in">
<div className="text-center mb-10">
<h2 className="text-4xl md:text-5xl font-display font-bold text-white mb-4">
Intelligent <span className="text-petro-500">O&G Data</span> <br />Extraction
</h2>
<p className="text-lg text-gray-400 max-w-xl mx-auto">
Upload logs, reports, or surveys. Validate with confidence scores. Visualize trends. Export to Excel.
</p>
</div>
<div className="mb-6 flex flex-col md:flex-row gap-4 items-center justify-between bg-industrial-900/40 p-4 rounded-xl border border-industrial-800/50">
<div className="flex-1 w-full">
<ModelSelector
selectedModel={selectedModel}
onModelChange={setSelectedModel}
disabled={false}
/>
</div>
<div className="flex items-center gap-3 px-4 py-2 bg-industrial-950/50 rounded-lg border border-industrial-800/50">
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-200">Deep Research (RLM)</span>
<span className="text-[10px] text-gray-500">For large/complex docs</span>
</div>
<button
onClick={() => setUsingRLM(!usingRLM)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-petro-500 focus:ring-offset-2 focus:ring-offset-industrial-950 ${usingRLM ? 'bg-petro-600' : 'bg-industrial-700'}`}
>
<span
className={`${usingRLM ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
/>
</button>
</div>
</div>
<FileUpload onFileSelect={handleFileSelect} disabled={false} />
</div>
</div>
)}
{(state === AppState.UPLOADING || state === AppState.ANALYZING) && (
<div className="h-full flex items-center justify-center p-8">
<ProcessingView fileName={currentFile?.name || 'Document'} usingRLM={usingRLM} />
</div>
)}
{state === AppState.ERROR && (
<div className="h-full flex items-center justify-center p-8">
<div className="max-w-xl text-center">
<div className="inline-block p-4 bg-red-900/20 text-red-500 rounded-full mb-4 border border-red-900/50">
<Icons.AlertTriangle />
</div>
<h3 className="text-2xl font-bold text-white mb-2">Extraction Failed</h3>
<p className="text-gray-400 mb-8">{error}</p>
<button onClick={reset} className="px-6 py-3 bg-industrial-800 hover:bg-industrial-700 text-white rounded-lg transition-colors border border-industrial-700">
Try Again
</button>
</div>
</div>
)}
{state === AppState.COMPLETE && result && (
<div className="h-full grid grid-cols-1 lg:grid-cols-2 gap-0 divide-x divide-industrial-900">
{/* Left Panel: Document Viewer (Source of Truth) */}
<div className="hidden lg:block h-full bg-industrial-950 p-4 overflow-hidden">
{fileBase64 && currentFile && (
<PdfViewer fileBase64={fileBase64} mimeType={currentFile.type} currentPage={currentPdfPage} />
)}
</div>
{/* Right Panel: Data & Tools */}
<div className="h-full flex flex-col bg-industrial-950 overflow-hidden">
{/* Toolbar */}
<div className="p-4 border-b border-industrial-900 flex items-center justify-between bg-industrial-950/50">
<div className="flex space-x-2">
<button
onClick={() => setActiveTab('tables')}
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-colors ${activeTab === 'tables' ? 'bg-industrial-800 text-white border border-industrial-700' : 'text-gray-400 hover:text-white'}`}
>
<Icons.Table /> Data Tables
</button>
<button
onClick={() => setActiveTab('visuals')}
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-colors ${activeTab === 'visuals' ? 'bg-industrial-800 text-white border border-industrial-700' : 'text-gray-400 hover:text-white'}`}
>
<Icons.Chart /> Visualization
</button>
<button
onClick={() => setActiveTab('chat')}
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-colors ${activeTab === 'chat' ? 'bg-industrial-800 text-white border border-industrial-700' : 'text-gray-400 hover:text-white'}`}
>
<Icons.MessageSquare /> Chat
</button>
</div>
<div className="flex space-x-2">
<button onClick={reset} className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:text-white bg-industrial-800 hover:bg-industrial-700 rounded-lg transition-colors border border-industrial-700">
<Icons.Refresh /> Home
</button>
<button onClick={handleExcelExport} className="flex items-center gap-2 px-4 py-2 text-sm bg-petro-700 hover:bg-petro-600 text-white rounded-lg shadow-lg shadow-petro-900/20 transition-all">
<Icons.FileSpreadsheet /> Export Excel
</button>
</div>
</div>
{/* Content Scroll Area */}
<div className="flex-1 overflow-y-auto p-4 custom-scrollbar">
{/* Summary */}
<div className="mb-6 p-4 bg-industrial-900/50 rounded-lg border border-industrial-800">
<h4 className="text-xs uppercase text-gray-500 font-bold mb-2">Analysis Summary</h4>
<p className="text-sm text-gray-300">{result.summary}</p>
</div>
{activeTab === 'tables' && (
<div className="space-y-8 pb-20">
{displayedTables.map((table, idx) => (
<div key={idx} className="animate-fade-in-up">
<div className="flex items-center justify-between mb-3">
<h3
className="text-md font-display font-bold text-petro-100 uppercase tracking-wide flex items-center gap-2 cursor-pointer hover:text-petro-300 transition-colors"
onClick={() => {
if (table.page_number) {
setCurrentPdfPage(table.page_number);
} else {
setCurrentPdfPage(1);
}
}}
title="Click to jump to page in document"
>
<span className="w-1.5 h-1.5 rounded-full bg-petro-500"></span>
{table.title}
</h3>
</div>
<DataGrid
data={table.rows}
onDataChange={(newRows) => handleTableUpdate(idx, newRows)}
/>
</div>
))}
</div>
)}
{activeTab === 'visuals' && (
<div className="animate-fade-in pb-20">
<DataVisualizer tables={displayedTables} />
</div>
)}
{activeTab === 'chat' && (
<div className="h-full animate-fade-in">
<ChatInterface onNavigateToPage={(page) => setCurrentPdfPage(page)} />
</div>
)}
</div>
</div>
</div>
)}
</main>
</div>
);
};
export default App;