import React, { useState, useCallback, useEffect } from 'react'; import { Classification, DocumentFile, Innovation, MeetingDoc } from './types'; import { backendService } from './services/backendService'; import FilterBar from './components/FilterBar'; import DocFilterBar from './components/DocFilterBar'; import StatsDashboard from './components/StatsDashboard'; import FileContentCard from './components/FileContentCard'; import InnovationCard from './components/InnovationCard'; import { LayoutDashboard, FileText, Settings, Play, CheckCircle2, CircleDashed } from 'lucide-react'; import { useLiveQuery } from 'dexie-react-hooks'; const App: React.FC = () => { // Application State const [files, setFiles] = useState([]); const [innovations, setInnovations] = useState([]); const [isFetchingDocs, setIsFetchingDocs] = useState(false); const [isProcessing, setIsProcessing] = useState(false); // Progress tracking const [processedCount, setProcessedCount] = useState(0); const [currentProcessingFile, setCurrentProcessingFile] = useState(""); const [currentView, setCurrentView] = useState<'list' | 'dashboard'>('list'); // New state for raw docs from API const [rawDocs, setRawDocs] = useState([]); // Pattern state const [patterns, setPatterns] = useState([]); const [selectedPatternId, setSelectedPatternId] = useState(null); const [isAnalyzing, setIsAnalyzing] = useState(false); const stopRef = React.useRef(false); // Metadata state for Backend const [selectedWG, setSelectedWG] = useState(""); const [selectedMeeting, setSelectedMeeting] = useState(""); useEffect(() => { const fetchPatterns = async () => { const data = await backendService.getPatterns(); setPatterns(data); if (data.length > 0) { setSelectedPatternId(data[0].pattern_id); } }; fetchPatterns(); }, []); const handleFetchDocuments = useCallback(async (wg: string, meeting: string) => { setIsFetchingDocs(true); setFiles([]); setInnovations([]); setRawDocs([]); setProcessedCount(0); setSelectedWG(wg); setSelectedMeeting(meeting); try { const response = await fetch('https://organizedprogrammers-docxtract.hf.space/docs/get_meeting_docs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ "working_group": wg, "meeting": meeting, "custom_url": null }) }); if (response.ok) { const data = await response.json(); if (data.data && Array.isArray(data.data)) { setRawDocs(data.data); } } else { console.error("Failed to fetch docs"); } } catch (error) { console.error("Error fetching docs:", error); } finally { setIsFetchingDocs(false); } }, []); const handleFilterChange = useCallback((filteredDocs: MeetingDoc[]) => { // Convert MeetingDoc to DocumentFile const newFiles: DocumentFile[] = filteredDocs.map((doc, index) => ({ id: doc.TDoc || `doc-${index}`, filename: doc.TDoc ? `${doc.TDoc}.zip` : `Document ${index}`, size: 'Unknown', // API doesn't provide size type: doc.Type, uploaded: false, status: doc["TDoc Status"], agendaItem: doc["Agenda item description"], url: doc.URL })); setFiles(newFiles); setProcessedCount(0); setInnovations([]); }, []); const handleStop = () => { stopRef.current = true; }; // Logic: Backend Processing Engine (Phase 2 - SQLite) const handleExtractInnovations = async () => { if (files.length === 0) return; setIsProcessing(true); let startIndex = 0; // Resume logic: If we have processed some but not all if (processedCount > 0 && processedCount < files.length) { startIndex = processedCount; } else { // Start fresh setInnovations([]); setProcessedCount(0); startIndex = 0; } stopRef.current = false; // Sequential Processing Logic for (let i = startIndex; i < files.length; i++) { if (stopRef.current) break; const file = files[i]; setCurrentProcessingFile(file.filename); // Process individually via Backend const result = await backendService.processDocument(file, selectedWG, selectedMeeting); if (result) { setInnovations(prev => [...prev, result]); } setProcessedCount(prev => prev + 1); } setIsProcessing(false); setCurrentProcessingFile(""); stopRef.current = false; }; const handleClassify = async (id: string, classification: Classification) => { // Find the innovation by ID or result_id (since InnovationCard passes result_id) const numId = Number(id); const targetInv = innovations.find(i => i.id === id || (!isNaN(numId) && i.result_id === numId)); if (!targetInv) return; // 1. Update Local State setInnovations(prev => prev.map(inv => inv.id === targetInv.id ? { ...inv, classification } : inv )); // 2. Persist to Backend if we have a result_id if (targetInv.result_id) { await backendService.saveClassification(targetInv.result_id, classification); } }; const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file && file.type === 'text/plain') { const reader = new FileReader(); reader.onload = (event) => { const text = event.target?.result as string; const newInnovation: Innovation = { id: `uploaded-${Date.now()}`, file_name: file.name, answer: text, classification: Classification.UNCLASSIFIED }; setInnovations(prev => [...prev, newInnovation]); }; reader.readAsText(file); } else { alert("Please upload a .txt file"); } }; const handleAnalyze = async () => { if (!selectedPatternId) { alert("Please select a pattern"); return; } if (innovations.length === 0) { alert("No documents to analyze."); return; } setIsAnalyzing(true); stopRef.current = false; try { // Use index to iterate const total = innovations.length; for (let i = 0; i < total; i++) { if (stopRef.current) break; // Always get fresh state in loop if needed, but here we can iterate by index // However, updates need to be functional // Let's just grab the item from current state reference if possible or just use the initial list // Since we are not adding items during analysis, looking up by ID is safer for updates // But iterating the initial list is fine. const inv = innovations[i]; // accessing closed-over innovations is fine as we only read static data from it for the request // Skip if already analyzed or classification is DELETE if (inv.classification === Classification.DELETE) continue; // optimization let analysisData: any = null; // If it's an uploaded file (id starts with uploaded-), pass text content if (inv.id.startsWith('uploaded-')) { const res = await backendService.analyzeContent(selectedPatternId, undefined, inv.answer); if (res) analysisData = res; } else { // It's a processed doc, pass the ID const res = await backendService.analyzeContent(selectedPatternId, inv.id); if (res) analysisData = res; } if (analysisData) { // Real-time update setInnovations(current => current.map(item => item.id === inv.id ? { ...item, analysis_result: analysisData.content, result_id: analysisData.result_id, methodology: analysisData.methodology, context: analysisData.context, problem: analysisData.problem, pattern_name: analysisData.pattern_name } : item ) ); } } } catch (error) { console.error("Error during analysis:", error); } finally { setIsAnalyzing(false); stopRef.current = false; } }; const pendingInnovations = innovations.filter(i => i.classification !== Classification.DELETE); const progressPercent = files.length > 0 ? (processedCount / files.length) * 100 : 0; return (
{/* 1. Header */}

3GPP Innovation Extractor

SQLite Backend Powered

{/* View: Extraction List */}
{rawDocs.length > 0 && ( )}
{/* Left Column: Files & Status */}
{/* File Stats Card */}

Documents

{files.length} Files
{files.length === 0 ? (
No documents selected
) : (
Processing Progress {processedCount} / {files.length}
{isProcessing && (

Processing: {currentProcessingFile}

)}
)}
{!isProcessing && !isAnalyzing ? ( ) : ( )}
{/* Pattern Analysis Card */}

Pattern Analysis

{/* Upload status is now handled in the main list */}
{!isAnalyzing && !isProcessing ? ( ) : isAnalyzing ? ( ) : ( )}
{/* Right Column: Results */}

Files content refined

{innovations.length > 0 && ( Showing {pendingInnovations.length} potential items )}
{innovations.length === 0 ? (

Ready to Process

Load documents and start the extraction engine. The Data will be mocked and stored in your local SQLite database.

) : (
{pendingInnovations.map(inv => (
{inv.analysis_result && (
)}
))} {pendingInnovations.length === 0 && innovations.length > 0 && (
All items have been deleted. Check the Analytics tab for stats.
)}
)}
{/* View: Analytics Dashboard */}

Classification Analytics

); }; export default App;