import { useState, useRef } from 'react' import { Button, Card, Label, Icon, Spinner } from '../components/primitives' import Heatmap from '../components/Heatmap' import { SuspiciousPair } from '../components/DetailSidebar' import Dropzone from '../components/Dropzone' import CodeEditor from '../components/CodeEditor' import { computeSimilarity } from '../api/client' export default function SimilarityScreen() { const [files, setFiles] = useState([]) const [result, setResult] = useState(null) const [submissions, setSubmissions] = useState([]) // čuvamo kod za usporedbu const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [threshold, setThreshold] = useState(0.70) const [selectedPair, setSelectedPair] = useState(null) // { i, j, idA, idB, similarity } const compareRef = useRef(null) const run = async () => { if (files.length < 2) { setError('Potrebne su najmanje 2 datoteke.'); return } setLoading(true); setError(null); setSelectedPair(null) const subs = await Promise.all(files.map(async f => ({ id: f.name.replace(/\.[^.]+$/, ''), file: f.name, code: await f.text(), }))) try { const data = await computeSimilarity(subs) setResult(data) setSubmissions(subs) // sačuvamo kod da ga možemo prikazati } catch (e) { setError(e.message) } finally { setLoading(false) } } // Kad korisnik klikne na ćeliju heatmape const handleSelectPair = (pair) => { setSelectedPair(pair) // Scroll do usporedbe if (pair) { setTimeout(() => compareRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 100) } } const suspiciousPairs = result?.suspicious_pairs?.filter(p => p.similarity >= threshold) || [] // Pronađi kod za odabrani par const codeA = selectedPair ? submissions.find(s => s.id === selectedPair.idA)?.code || '' : '' const codeB = selectedPair ? submissions.find(s => s.id === selectedPair.idB)?.code || '' : '' const fileA = selectedPair ? submissions.find(s => s.id === selectedPair.idA)?.file : '' const fileB = selectedPair ? submissions.find(s => s.id === selectedPair.idB)?.file : '' return (

Submission similarity

Klikni na ćeliju heatmape za prikaz i usporedbu dvaju kodova. Visoka sličnost može upućivati na korištenje istog AI prompta.

{!result && (
{ setFiles(Array.from(f)); setResult(null); setError(null) }}/> {files.length >= 2 && (
)}
)} {loading && (
Computing pairwise similarity…
)} {error && (
{error}
)} {result && ( <> {/* Heatmap + filter */}
Similarity threshold {Math.round(threshold * 100)}%
setThreshold(e.target.value / 100)} style={{ width: '100%', accentColor: '#00D4FF' }}/>
Suspicious pairs
0 ? '#EF4444' : '#10B981' }}> {suspiciousPairs.length}
{/* ── Usporedba kodova (prikazuje se kad kliknemo na ćeliju) ── */} {selectedPair && (
{/* Header usporedbe */}
{selectedPair.idA} ↔ {selectedPair.idB} = 80 ? '#EF4444' : selectedPair.similarity >= 60 ? '#F59E0B' : '#10B981', }}> {selectedPair.similarity}% similar
{/* Dva editora side by side */}
{selectedPair.idA} {fileA}
{selectedPair.idB} {fileB}
)} {/* Suspicious pairs lista */}

Suspicious pairs

{suspiciousPairs.length} pairs above {Math.round(threshold * 100)}%
{suspiciousPairs.map((p, i) => (
{ const idxA = result.ids.indexOf(p.id_a) const idxB = result.ids.indexOf(p.id_b) handleSelectPair({ i: idxA, j: idxB, idA: p.id_a, idB: p.id_b, similarity: Math.round(p.similarity * 100) }) }} style={{ cursor: 'pointer' }}>
))} {suspiciousPairs.length === 0 && (
No pairs exceed the {Math.round(threshold * 100)}% threshold
)}
)}
) }