import React, { useState, useEffect, useMemo } from 'react'; import { Settings, List, Edit3, Activity, Upload, Play, FileText, ChevronLeft, Trash2, Plus, Download, FileSearch, BookOpen, Edit } from 'lucide-react'; // --- Fungsi Utilitas --- // Parser CSV sederhana yang menangani tanda kutip const parseCSV = (text) => { const result = []; let row = []; let inQuotes = false; let currentValue = ""; for (let i = 0; i < text.length; i++) { const char = text[i]; if (char === '"') { if (inQuotes && text[i + 1] === '"') { currentValue += '"'; i++; } else { inQuotes = !inQuotes; } } else if (char === ',' && !inQuotes) { row.push(currentValue); currentValue = ""; } else if (char === '\n' && !inQuotes) { row.push(currentValue); result.push(row); row = []; currentValue = ""; } else { currentValue += char; } } if (currentValue || text[text.length - 1] === ',') { row.push(currentValue); } if (row.length > 0) result.push(row); return result; }; // Fungsi untuk mengekspor data ke CSV const exportCSV = (papers) => { if(papers.length === 0) return alert("No data to export."); const headers = ['id', 'title', 'authors', 'abstract', 'year', 'doi']; const csvContent = [ headers.join(','), ...papers.map(p => headers.map(h => `"${(p[h]||'').toString().replace(/"/g, '""')}"`).join(',')) ].join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = "prisma_papers_export.csv"; link.click(); }; // Menghitung Cohen's Kappa const calculateKappa = (votes1, votes2) => { if (votes1.length === 0 || votes1.length !== votes2.length) return 0; let agree = 0; const counts1 = { Yes: 0, No: 0, Unclear: 0 }; const counts2 = { Yes: 0, No: 0, Unclear: 0 }; for (let i = 0; i < votes1.length; i++) { if (votes1[i] === votes2[i]) agree++; counts1[votes1[i]] = (counts1[votes1[i]] || 0) + 1; counts2[votes2[i]] = (counts2[votes2[i]] || 0) + 1; } const p0 = agree / votes1.length; const pe = ( (counts1.Yes * counts2.Yes) + (counts1.No * counts2.No) + (counts1.Unclear * counts2.Unclear) ) / (votes1.length * votes1.length); if (pe === 1) return 1; // Kesepakatan sempurna return ((p0 - pe) / (1 - pe)).toFixed(2); }; // Mock F1 Score const calculateMockF1 = (votes1, votes2) => { let truePositives = 0; let falsePositives = 0; let falseNegatives = 0; for (let i = 0; i < votes1.length; i++) { if (votes1[i] === 'Yes' && votes2[i] === 'Yes') truePositives++; if (votes1[i] === 'Yes' && votes2[i] !== 'Yes') falsePositives++; if (votes1[i] !== 'Yes' && votes2[i] === 'Yes') falseNegatives++; } const precision = truePositives / (truePositives + falsePositives || 1); const recall = truePositives / (truePositives + falseNegatives || 1); if (precision + recall === 0) return 0; return (2 * (precision * recall) / (precision + recall)).toFixed(2); }; export default function App() { // --- State --- const [activeTab, setActiveTab] = useState('importPapers'); // Pengaturan const [apiKeys, setApiKeys] = useState({ gemini: '', openai: '' }); const [simulateAI, setSimulateAI] = useState(true); // Kriteria const [criteria, setCriteria] = useState({ inclusion: [{ id: 1, text: 'Study published after 2010' }], exclusion: [{ id: 2, text: 'Study is a review article' }] }); // Prompts const [prompts, setPrompts] = useState({ system: 'You are an expert researcher conducting a systematic review using PRISMA guidelines. Evaluate the given paper abstract against the inclusion and exclusion criteria.', user: 'Evaluate the following paper:\nTitle: {"{{TITLE}}"}\nAbstract: {"{{ABSTRACT}}"}\n\nCriteria:\n{"{{CRITERIA}}"}\n\nOutput your evaluation in JSON format.' }); // Data const [papers, setPapers] = useState([]); const [testRuns, setTestRuns] = useState([]); // State Navigasi const [currentTestId, setCurrentTestId] = useState(null); const [currentPaperId, setCurrentPaperId] = useState(null); // --- Handlers --- const handleFileUpload = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { const csvData = parseCSV(event.target.result); if (csvData.length < 2) return alert("CSV file seems empty or invalid."); const headers = csvData[0].map(h => h.trim().toLowerCase()); const getIndex = (possibleNames) => { for (let name of possibleNames) { const idx = headers.findIndex(h => h.includes(name)); if (idx !== -1) return idx; } return -1; }; const titleIdx = getIndex(['title']); const abstractIdx = getIndex(['abstract']); const authorIdx = getIndex(['author']); const yearIdx = getIndex(['year']); const doiIdx = getIndex(['doi']); const covidenceIdx = getIndex(['covidence']); const parsedPapers = csvData.slice(1).map((row, index) => { if (row.length <= 1) return null; let covidenceId = covidenceIdx !== -1 ? row[covidenceIdx] : `ID-${index + 1}`; if (covidenceId && covidenceId.startsWith('#')) { covidenceId = covidenceId.substring(1).trim(); } return { id: covidenceId || `temp-${index}`, title: titleIdx !== -1 ? row[titleIdx] : 'Unknown Title', authors: authorIdx !== -1 ? row[authorIdx] : 'Unknown Authors', abstract: abstractIdx !== -1 ? row[abstractIdx] : 'No abstract available', year: yearIdx !== -1 ? row[yearIdx] : 'N/A', doi: doiIdx !== -1 ? row[doiIdx] : 'N/A', }; }).filter(Boolean); setPapers(parsedPapers); alert(`Successfully imported ${parsedPapers.length} papers.`); }; reader.readAsText(file); e.target.value = ''; // Reset input }; const handleDeletePaper = (id) => { if(window.confirm("Are you sure you want to delete this paper?")) { setPapers(papers.filter(p => p.id !== id)); } }; const runScreeningTest = () => { if (papers.length === 0) return alert("Please import papers first."); if (criteria.inclusion.length === 0 && criteria.exclusion.length === 0) return alert("Please add at least one criterion."); const newRunId = Date.now().toString(); const results = papers.map(paper => { const generateVotes = () => { const critVotes = { inclusion: {}, exclusion: {} }; const options = ['Yes', 'No', 'Unclear']; criteria.inclusion.forEach(c => { critVotes.inclusion[c.id] = Math.random() > 0.3 ? 'Yes' : options[Math.floor(Math.random() * 3)]; }); criteria.exclusion.forEach(c => { critVotes.exclusion[c.id] = Math.random() > 0.7 ? 'Yes' : options[Math.floor(Math.random() * 3)]; }); let hasUnclear = false; let hasIncFail = false; let hasExcHit = false; Object.values(critVotes.inclusion).forEach(v => { if (v === 'Unclear') hasUnclear = true; if (v === 'No') hasIncFail = true; }); Object.values(critVotes.exclusion).forEach(v => { if (v === 'Unclear') hasUnclear = true; if (v === 'Yes') hasExcHit = true; }); let finalVote = 'Yes'; if (hasIncFail || hasExcHit) finalVote = 'No'; else if (hasUnclear) finalVote = 'Unclear'; return { criteriaVotes: critVotes, vote: finalVote }; }; const geminiResult = generateVotes(); const openaiResult = generateVotes(); let finalConsensus = 'Unclear'; if (geminiResult.vote === openaiResult.vote) { finalConsensus = geminiResult.vote; } else if (geminiResult.vote === 'Unclear' || openaiResult.vote === 'Unclear') { finalConsensus = 'Unclear'; } else { finalConsensus = 'Unclear'; } return { paperId: paper.id, gemini: geminiResult, openai: openaiResult, consensusVote: finalConsensus }; }); const geminiVotes = results.map(r => r.gemini.vote); const openaiVotes = results.map(r => r.openai.vote); const kappa = calculateKappa(geminiVotes, openaiVotes); const f1 = calculateMockF1(geminiVotes, openaiVotes); let conclusion = "Moderate Agreement"; if (kappa > 0.8) conclusion = "Excellent Agreement"; else if (kappa < 0.4) conclusion = "Poor Agreement"; const newRun = { id: newRunId, date: new Date().toLocaleString(), paperCount: papers.length, kappa, f1, conclusion, results }; setTestRuns([newRun, ...testRuns]); setCurrentTestId(newRunId); setActiveTab('testDetail'); }; // --- Komponen Visual UI --- const VoteBadge = ({ vote }) => { const styles = { 'Yes': 'bg-emerald-100 text-emerald-800 border-emerald-200', 'No': 'bg-red-100 text-red-800 border-red-200', 'Unclear': 'bg-slate-100 text-slate-700 border-slate-200' }; return {vote}; }; const renderSidebar = () => { const navItem = (id, icon, label) => { const Icon = icon; const isActive = activeTab === id || (id === 'dashboard' && activeTab === 'testDetail') || (id === 'dashboard' && activeTab === 'paperDetail'); return ( ); }; return (

PRISMA AI

Systematic Review Tool

{/* Grup Preparation */}

Preparation

{/* Grup Review */}

Review

{navItem('settings', Settings, 'Settings')}
); }; const renderImportPapers = () => (

Import Papers

Total Papers: {papers.length}
{papers.length === 0 && ( )} {papers.map(paper => ( ))}
ID Title DOI Actions
No papers imported yet.
{paper.id} {paper.title} {paper.doi}
); const CriteriaTable = ({ type, data }) => { const [newText, setNewText] = useState(''); const handleAdd = () => { if (!newText.trim()) return; setCriteria({ ...criteria, [type]: [...data, { id: Date.now(), text: newText }] }); setNewText(''); }; const handleDelete = (id) => { setCriteria({ ...criteria, [type]: data.filter(c => c.id !== id) }); }; return (
{type === 'inclusion' ? 'Inclusion Criteria' : 'Exclusion Criteria'}
setNewText(e.target.value)} placeholder={`Add new ${type} criterion...`} className="flex-1 p-2 border border-slate-300 rounded-lg outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" onKeyDown={(e) => e.key === 'Enter' && handleAdd()} />
); }; const renderEligibilityCriteria = () => (

Eligibility Criteria

); const renderPromptTweaker = () => (

Prompt Tweaker

Instructs the AI on its role and general behavior.