ai-code-detector / frontend /src /screens /BatchScreen2.jsx
Karim Krklec
Obavljene optimizacije
a641ed5
Raw
History Blame Contribute Delete
7.74 kB
import { useState } from 'react'
import { Button, Card, Label, Icon, Spinner, Input } from '../components/primitives'
import Dropzone from '../components/Dropzone'
import ResultsTable from '../components/ResultsTable'
import { StatCard } from '../components/FeatureCard'
import { analyzeBatch } from '../api/client'
export default function BatchScreen({ onOpenRow }) {
const [files, setFiles] = useState([])
const [results, setResults] = useState([])
const [summary, setSummary] = useState(null)
const [loading, setLoading] = useState(false)
const [progress, setProgress] = useState(0)
const [error, setError] = useState(null)
const [search, setSearch] = useState('')
const handleFiles = (fileList) => {
const arr = Array.from(fileList)
setFiles(arr); setResults([]); setSummary(null); setError(null)
}
const runBatch = async () => {
if (!files.length) return
setLoading(true); setError(null); setProgress(0)
const submissions = await Promise.all(files.map(async (f) => ({
id: f.name.replace(/\.[^.]+$/, ''),
file: f.name,
code: await f.text(),
})))
try {
const data = await analyzeBatch(submissions)
// Uz file naziv priložimo i originalni kod da ga DetailScreen može prikazati
const withFile = data.results.map((r, i) => ({
...r,
file: submissions[i]?.file || r.id,
code: submissions[i]?.code || '', // ← ovo je bio problem, nedostajalo
}))
setResults(withFile)
setSummary(data.summary)
setProgress(100)
} catch (e) {
setError(e.message)
} finally {
setLoading(false)
}
}
const exportCSV = () => {
if (!results.length) return
const headers = ['ID', 'Datoteka', 'Jezik', 'AI vjerojatnost (%)', 'Zakljucak']
const keys = ['id', 'file', 'detected_language', 'ai_probability', 'verdict']
// Escapiranje polja — ako polje sadrži točku-zarez, navodnike ili novi red,
// omotamo ga navodnicima i udvostručimo unutarnje navodnike
const escapeCell = (val) => {
const s = String(val ?? '')
if (s.includes(';') || s.includes('"') || s.includes('\n')) {
return '"' + s.replace(/"/g, '""') + '"'
}
return s
}
const rows = results.map(r => keys.map(h => {
if (h === 'ai_probability') return escapeCell(Math.round((r[h] || 0) * 100) + '%')
return escapeCell(r[h] || '')
}))
// Točka-zarez (;) kao separator — Excel u europskim regijama koristi ; ne ,
// UTF-8 BOM (\ufeff) na početku — Excel tada ispravno čita čšćžđ i sl.
const sep = ';'
const csvContent = [
headers.map(escapeCell).join(sep),
...rows.map(r => r.join(sep)),
].join('\r\n')
const BOM = '\uFEFF'
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' })
const a = Object.assign(document.createElement('a'), {
href: URL.createObjectURL(blob),
download: 'codesentinel_rezultati.csv',
})
a.click()
}
const filtered = results.filter(r =>
!search || r.id?.toLowerCase().includes(search.toLowerCase()) || r.file?.toLowerCase().includes(search.toLowerCase())
)
return (
<div style={{ padding: '40px 80px 80px', maxWidth: 1280, margin: '0 auto' }}>
<div style={{ marginBottom: 32 }}>
<Label style={{ marginBottom: 8 }}>Batch analysis</Label>
<h1 style={{ fontSize: 40, fontWeight: 700, letterSpacing: '-0.02em', color: '#F8FAFC', margin: 0, lineHeight: 1.1 }}>
Class overview
</h1>
<p style={{ fontSize: 15, color: '#94A3B8', marginTop: 12, marginBottom: 0, maxWidth: 600 }}>
Drop a folder of submissions, then sort by AI probability to triage your grading queue.
</p>
</div>
{/* Upload zone */}
{!results.length && (
<div style={{ marginBottom: 24 }}>
<Dropzone files={files} onUpload={handleFiles}/>
{files.length > 0 && (
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
<Button variant="primary" icon="play" onClick={runBatch} disabled={loading}>
{loading ? 'Analyzing…' : `Analyze ${files.length} submission${files.length !== 1 ? 's' : ''}`}
</Button>
</div>
)}
</div>
)}
{loading && (
<Card style={{ padding: 40, textAlign: 'center', marginBottom: 24 }}>
<Spinner size={40} style={{ margin: '0 auto 16px' }}/>
<div style={{ fontSize: 14, color: '#94A3B8' }}>Analyzing {files.length} files…</div>
<div style={{ width: 240, height: 4, background: 'rgba(148,163,184,0.10)', borderRadius: 2, margin: '16px auto 0', overflow: 'hidden' }}>
<div style={{ width: '60%', height: '100%', background: '#00D4FF', animation: 'pulse 1.2s ease-in-out infinite' }}/>
</div>
</Card>
)}
{error && (
<Card style={{ padding: 20, marginBottom: 24, borderColor: 'rgba(239,68,68,0.35)', background: 'rgba(239,68,68,0.06)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<Icon name="alert" size={16} color="#EF4444"/>
<span style={{ fontSize: 14, color: '#EF4444' }}>{error}</span>
</div>
</Card>
)}
{summary && (
<>
{/* Stat cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 24 }}>
<StatCard label="Total submissions" value={summary.total} delta="files analyzed" icon="users" iconColor="#00D4FF"/>
<StatCard label="High risk" value={summary.high_risk} delta="≥ 70% AI probability" deltaColor="#EF4444" icon="alert" iconColor="#EF4444"/>
<StatCard label="Medium risk" value={summary.medium_risk} delta="40–69% probability" deltaColor="#F59E0B" icon="alert" iconColor="#F59E0B"/>
<StatCard label="Low risk" value={summary.low_risk} delta="< 40% probability" deltaColor="#10B981" icon="check" iconColor="#10B981"/>
</div>
{/* Success bar */}
<div style={{ marginBottom: 24, padding: '14px 18px', background: 'rgba(16,185,129,0.06)', border: '1px solid rgba(16,185,129,0.20)', borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<Icon name="check" size={16} color="#10B981"/>
<span style={{ fontSize: 13, color: '#CBD5E1' }}>
Analyzed <strong style={{ color: '#F8FAFC' }}>{summary.total} files</strong> — avg. AI probability:{' '}
<strong style={{ color: '#F8FAFC' }}>{Math.round(summary.avg_ai_probability * 100)}%</strong>
</span>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<Button variant="secondary" size="sm" icon="upload" onClick={() => { setResults([]); setSummary(null); setFiles([]) }}>
Re-upload
</Button>
<Button variant="primary" size="sm" icon="download" onClick={exportCSV}>
Export CSV
</Button>
</div>
</div>
{/* Search toolbar */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<Input
icon="search"
placeholder="Search by student ID or filename…"
value={search}
onChange={setSearch}
style={{ width: 320 }}
/>
</div>
<ResultsTable rows={filtered} onOpen={onOpenRow}/>
</>
)}
</div>
)
}