heymenn's picture
add features stop and filter class
36fa73c
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<DocumentFile[]>([]);
const [innovations, setInnovations] = useState<Innovation[]>([]);
const [isFetchingDocs, setIsFetchingDocs] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
// Progress tracking
const [processedCount, setProcessedCount] = useState(0);
const [currentProcessingFile, setCurrentProcessingFile] = useState<string>("");
const [currentView, setCurrentView] = useState<'list' | 'dashboard'>('list');
// New state for raw docs from API
const [rawDocs, setRawDocs] = useState<MeetingDoc[]>([]);
// Pattern state
const [patterns, setPatterns] = useState<any[]>([]);
const [selectedPatternId, setSelectedPatternId] = useState<number | null>(null);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const stopRef = React.useRef(false);
// Metadata state for Backend
const [selectedWG, setSelectedWG] = useState<string>("");
const [selectedMeeting, setSelectedMeeting] = useState<string>("");
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<HTMLInputElement>) => {
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 (
<div className="min-h-screen bg-slate-50 font-sans text-slate-900">
{/* 1. Header */}
<header className="bg-slate-900 text-white shadow-md sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16 items-center">
<div className="flex items-center">
<div className="bg-blue-500 p-1.5 rounded mr-3">
<FileText className="h-5 w-5 text-white" />
</div>
<div>
<h1 className="text-lg font-bold tracking-tight">3GPP Innovation Extractor</h1>
<p className="text-xs text-slate-400">SQLite Backend Powered</p>
</div>
</div>
<nav className="flex space-x-4">
<button
onClick={() => setCurrentView('list')}
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${currentView === 'list' ? 'bg-slate-800 text-white' : 'text-slate-300 hover:text-white hover:bg-slate-800'}`}
>
Extraction
</button>
<button
onClick={() => setCurrentView('dashboard')}
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${currentView === 'dashboard' ? 'bg-slate-800 text-white' : 'text-slate-300 hover:text-white hover:bg-slate-800'}`}
>
Analytics
</button>
</nav>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* View: Extraction List */}
<div className={currentView === 'list' ? 'animate-fade-in' : 'hidden'}>
<FilterBar onFetch={handleFetchDocuments} isFetching={isFetchingDocs} />
{rawDocs.length > 0 && (
<DocFilterBar docs={rawDocs} onFilterChange={handleFilterChange} />
)}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Left Column: Files & Status */}
<div className="lg:col-span-4 space-y-6">
{/* File Stats Card */}
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-5">
<div className="flex items-center justify-between mb-4">
<h2 className="font-semibold text-slate-800">Documents</h2>
<span className="text-xs font-mono bg-slate-100 px-2 py-1 rounded text-slate-600">
{files.length} Files
</span>
</div>
{files.length === 0 ? (
<div className="text-center py-8 text-slate-400 text-sm border-2 border-dashed border-slate-100 rounded-lg">
No documents selected
</div>
) : (
<div className="mb-4">
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span>Processing Progress</span>
<span>{processedCount} / {files.length}</span>
</div>
<div className="h-2 w-full bg-slate-100 rounded-full overflow-hidden">
<div className="h-full bg-blue-500 transition-all duration-300" style={{ width: `${progressPercent}%` }}></div>
</div>
{isProcessing && (
<p className="text-xs text-blue-500 mt-2 animate-pulse">
Processing: {currentProcessingFile}
</p>
)}
</div>
)}
<div className="mt-4 flex space-x-2">
{!isProcessing && !isAnalyzing ? (
<button
onClick={handleExtractInnovations}
disabled={files.length === 0}
className="w-full bg-slate-900 hover:bg-slate-800 text-white font-medium py-3 rounded-lg shadow-md transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
<Play className="w-4 h-4 mr-2 fill-current" /> Start Extraction
</button>
) : (
<button
onClick={handleStop}
className="w-full bg-red-600 hover:bg-red-700 text-white font-medium py-3 rounded-lg shadow-md transition-all flex items-center justify-center animate-pulse"
>
<CircleDashed className="w-4 h-4 mr-2 animate-spin" /> Stop {isProcessing ? "Extraction" : "Analysis"}
</button>
)}
</div>
</div>
{/* Pattern Analysis Card */}
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-5">
<h2 className="font-semibold text-slate-800 mb-4">Pattern Analysis</h2>
<div className="space-y-4">
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">Select Pattern</label>
<select
className="w-full bg-slate-50 border border-slate-200 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={selectedPatternId || ''}
onChange={(e) => setSelectedPatternId(Number(e.target.value))}
>
{patterns.map(p => (
<option key={p.pattern_id} value={p.pattern_id}>{p.pattern_name}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-slate-500 mb-1">Upload .txt File (Optional)</label>
<input
type="file"
accept=".txt"
onChange={handleFileUpload}
className="w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-xs file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
/>
{/* Upload status is now handled in the main list */}
</div>
{!isAnalyzing && !isProcessing ? (
<button
onClick={handleAnalyze}
disabled={innovations.length === 0}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 rounded-lg shadow-sm transition-all disabled:opacity-50 flex items-center justify-center text-sm"
>
<Play className="w-4 h-4 mr-2 fill-current" /> Analyse
</button>
) : isAnalyzing ? (
<button
onClick={handleStop}
className="w-full bg-red-600 hover:bg-red-700 text-white font-medium py-2 rounded-lg shadow-sm transition-all flex items-center justify-center text-sm animate-pulse"
>
<CircleDashed className="w-4 h-4 mr-2 animate-spin" /> Stop Analysis
</button>
) : (
<button disabled className="w-full bg-slate-300 text-white font-medium py-2 rounded-lg shadow-sm cursor-not-allowed flex items-center justify-center text-sm">
Processing...
</button>
)}
</div>
</div>
</div>
{/* Right Column: Results */}
<div className="lg:col-span-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-slate-800">Files content refined</h2>
{innovations.length > 0 && (
<span className="text-sm text-slate-500">
Showing {pendingInnovations.length} potential items
</span>
)}
</div>
{innovations.length === 0 ? (
<div className="bg-white rounded-xl border border-dashed border-slate-300 p-12 text-center">
<div className="mx-auto bg-slate-50 w-16 h-16 rounded-full flex items-center justify-center mb-4">
<Settings className="w-8 h-8 text-slate-400" />
</div>
<h3 className="text-lg font-medium text-slate-900">Ready to Process</h3>
<p className="text-slate-500 max-w-sm mx-auto mt-2">
Load documents and start the extraction engine. The Data will be mocked and stored in your local SQLite database.
</p>
</div>
) : (
<div className="space-y-4">
{pendingInnovations.map(inv => (
<div key={inv.id}>
<FileContentCard
innovation={inv}
onClassify={handleClassify}
/>
{inv.analysis_result && (
<div className="ml-8 mt-2">
<InnovationCard
innovation={{
id: inv.result_id || 0,
file_name: inv.file_name,
content: inv.analysis_result || "",
classification: inv.classification,
methodology: inv.methodology || "N/A",
context: inv.context || "N/A",
problem: inv.problem || "N/A",
}}
onClassify={handleClassify}
/>
</div>
)}
</div>
))}
{pendingInnovations.length === 0 && innovations.length > 0 && (
<div className="text-center py-10 text-slate-500">
All items have been deleted. Check the Analytics tab for stats.
</div>
)}
</div>
)}
</div>
</div>
</div>
{/* View: Analytics Dashboard */}
<div className={currentView === 'dashboard' ? 'animate-fade-in' : 'hidden'}>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-slate-900">Classification Analytics</h2>
</div>
<StatsDashboard innovations={[]} isVisible={currentView === 'dashboard'} />
</div>
</main>
</div>
);
};
export default App;