complate detail page

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-02 19:19:06 +05:00
parent 10cf895262
commit 238c2c1653
28 changed files with 2949 additions and 973 deletions

View File

@@ -1,460 +0,0 @@
'use client';
import { useState } from 'react';
interface Source {
url: string;
title: string;
matchPercentage: number;
matchedWords: number;
}
interface CheckResult {
ai: number;
plagiarism: number;
originality: number;
citation: number;
checkedWords: number;
uniqueWords: number;
lexicalUniqueness: number;
sentences: number;
avgWordsPerSentence: number;
lines: number;
sources: Source[];
}
interface Certificate {
id: string;
issuedAt: string;
expiresAt: string;
verificationCode: string;
issuerName: string;
downloadUrl: string;
}
interface PlagiatResultProps {
name: string;
initials: string;
email: string;
location: string;
fileName: string;
checkedAt: string;
result: CheckResult;
certificate: Certificate;
}
// Blue palette
const blue = {
50: '#E6F1FB',
100: '#B5D4F4',
200: '#85B7EB',
400: '#378ADD',
600: '#185FA5',
800: '#0C447C',
900: '#042C53',
};
const mockData: PlagiatResultProps = {
name: 'Sokhibjon Orzikulov',
initials: 'SO',
email: 'sakhib@orzklv.uz',
location: 'Tashkent, Uzbekistan',
fileName: 'resume_sokhibjon.pdf',
checkedAt: '2026-04-02',
result: {
ai: 86,
plagiarism: 92,
originality: 5,
citation: 3,
checkedWords: 1477,
uniqueWords: 593,
lexicalUniqueness: 40,
sentences: 105,
avgWordsPerSentence: 14.1,
lines: 191,
sources: [
{
url: 'https://arxiv.org/abs/1706.03762',
title: 'arxiv.org — Attention Is All You Need',
matchPercentage: 9,
matchedWords: 937,
},
{
url: 'https://en.wikipedia.org/wiki/Machine_learning',
title: 'Wikipedia — Machine Learning',
matchPercentage: 7,
matchedWords: 730,
},
{
url: 'https://towardsdatascience.com/introduction-to-neural-networks',
title: 'Towards Data Science — Introduction to Neural Networks',
matchPercentage: 6,
matchedWords: 625,
},
],
},
certificate: {
id: 'cert-9001',
issuedAt: '2026-03-30',
expiresAt: '2027-03-30',
verificationCode: 'PLAG-9001-VERIFY',
issuerName: 'Global Plagiarism Checker',
downloadUrl: '/certificates/cert-9001.pdf',
},
};
// ─── Sub-components ───────────────────────────────────────────────────────────
function CircleGauge({ value }: { value: number }) {
const r = 50;
const circ = 2 * Math.PI * r;
const offset = circ - (value / 100) * circ;
return (
<svg
width="130"
height="130"
viewBox="0 0 130 130"
aria-label={`${value}% plagiat`}
>
<circle
cx="65"
cy="65"
r={r}
fill="none"
stroke={blue[100]}
strokeWidth="14"
/>
<circle
cx="65"
cy="65"
r={r}
fill="none"
stroke={blue[900]}
strokeWidth="14"
strokeDasharray={circ}
strokeDashoffset={offset}
strokeLinecap="round"
transform="rotate(-90 65 65)"
style={{ transition: 'stroke-dashoffset 0.8s ease' }}
/>
<text
x="65"
y="60"
textAnchor="middle"
fontSize="26"
fontWeight="500"
fill={blue[900]}
>
{value}%
</text>
<text x="65" y="78" textAnchor="middle" fontSize="11" fill={blue[400]}>
plagiat
</text>
</svg>
);
}
function BarRow({
label,
value,
color,
}: {
label: string;
value: number;
color: string;
}) {
return (
<div className="mb-2.5">
<div className="flex justify-between items-center text-sm mb-1">
<span className="flex items-center gap-1.5 text-slate-500">
<span
className="inline-block w-2.5 h-2.5 rounded-full"
style={{ background: color }}
/>
{label}
</span>
<span className="font-medium" style={{ color: blue[900] }}>
{value}%
</span>
</div>
<div
className="h-1.5 rounded-full overflow-hidden"
style={{ background: blue[100] }}
>
<div
className="h-full rounded-full"
style={{
width: `${value}%`,
background: color,
transition: 'width 0.8s ease',
}}
/>
</div>
</div>
);
}
function MetricCard({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-lg p-3.5" style={{ background: blue[50] }}>
<p className="text-xs mb-1" style={{ color: blue[600] }}>
{label}
</p>
<p className="text-xl font-medium" style={{ color: blue[900] }}>
{value}
</p>
</div>
);
}
function SourceItem({ source, index }: { source: Source; index: number }) {
const colors = [blue[900], blue[600], blue[400]];
const color = colors[index] ?? blue[400];
return (
<div
className="rounded-lg p-3"
style={{ border: `0.5px solid ${blue[100]}` }}
>
<div className="flex items-center justify-between mb-1">
<span
className="text-sm font-medium truncate max-w-[72%]"
style={{ color: blue[900] }}
>
{source.title}
</span>
<span className="text-sm font-medium" style={{ color }}>
{source.matchPercentage}%
</span>
</div>
<p className="text-[11px] mb-1.5 truncate" style={{ color: blue[400] }}>
{source.url}
</p>
<div
className="h-1.5 rounded-full overflow-hidden"
style={{ background: blue[100] }}
>
<div
className="h-full rounded-full"
style={{
width: `${source.matchPercentage}%`,
background: color,
transition: 'width 0.8s ease',
}}
/>
</div>
</div>
);
}
// ─── Main component ───────────────────────────────────────────────────────────
export default function PlagiatResult({
data = mockData,
}: {
data?: PlagiatResultProps;
}) {
const [downloading, setDownloading] = useState(false);
const { result, certificate } = data;
const handleDownload = () => {
setDownloading(true);
setTimeout(() => setDownloading(false), 1500);
};
const divider = (
<hr
style={{
borderColor: blue[100],
borderTopWidth: '0.5px',
borderStyle: 'solid',
margin: '1.25rem 0',
}}
/>
);
return (
<div className="min-h-screen flex items-start justify-center p-6 bg-slate-50">
<div
className="w-full max-w-2xl rounded-xl p-5"
style={{ background: '#ffffff', border: `0.5px solid ${blue[100]}` }}
>
{/* ── Header ── */}
<div className="flex items-center gap-3 mb-5">
<div
className="w-11 h-11 rounded-full flex items-center justify-center text-sm font-medium shrink-0"
style={{ background: blue[50], color: blue[600] }}
>
{data.initials}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span
className="text-[15px] font-medium"
style={{ color: blue[900] }}
>
{data.name}
</span>
<span
className="text-xs px-2.5 py-0.5 rounded-md font-medium"
style={{ background: blue[100], color: blue[800] }}
>
{result.plagiarism}% plagiat
</span>
</div>
<p
className="text-[13px] mt-0.5 truncate"
style={{ color: blue[400] }}
>
{data.fileName} · {data.email} · {data.location}
</p>
</div>
<div className="text-right shrink-0">
<p className="text-[11px]" style={{ color: blue[400] }}>
Tekshirilgan
</p>
<p className="text-[12px] mt-0.5" style={{ color: blue[600] }}>
{data.checkedAt}
</p>
</div>
</div>
{divider}
{/* ── Top metrics ── */}
<div className="grid grid-cols-4 gap-2.5 mb-5">
<MetricCard
label="Plagiat darajasi"
value={`${result.plagiarism}%`}
/>
<MetricCard label="AI yozgan" value={`${result.ai}%`} />
<MetricCard label="Originallik" value={`${result.originality}%`} />
<MetricCard label="Iqtibos" value={`${result.citation}%`} />
</div>
{/* ── Gauge + bars ── */}
<div className="flex items-center gap-5 mb-5">
<CircleGauge value={result.plagiarism} />
<div className="flex-1">
<BarRow
label="Plagiat"
value={result.plagiarism}
color={blue[900]}
/>
<BarRow
label="AI generatsiya"
value={result.ai}
color={blue[600]}
/>
<BarRow
label="Original"
value={result.originality}
color={blue[400]}
/>
<BarRow label="Iqtibos" value={result.citation} color={blue[200]} />
</div>
</div>
{divider}
{/* ── Text analysis ── */}
<p
className="text-[13px] font-medium uppercase tracking-wider mb-3"
style={{ color: blue[400] }}
>
Matn tahlili
</p>
<div className="grid grid-cols-3 gap-2 mb-5">
<MetricCard
label="Jami so'z"
value={result.checkedWords.toLocaleString()}
/>
<MetricCard
label="Unikal so'z"
value={result.uniqueWords.toLocaleString()}
/>
<MetricCard
label="Leksik unikalligi"
value={`${result.lexicalUniqueness}%`}
/>
<MetricCard label="Jumlalar" value={String(result.sentences)} />
<MetricCard
label="O'rt. so'z/juml."
value={String(result.avgWordsPerSentence)}
/>
<MetricCard label="Qatorlar" value={String(result.lines)} />
</div>
{divider}
{/* ── Sources ── */}
<p
className="text-[13px] font-medium uppercase tracking-wider mb-3"
style={{ color: blue[400] }}
>
Manba yo&lsquo;nalishlari
</p>
<div className="flex flex-col gap-2.5 mb-5">
{result.sources.map((source, i) => (
<SourceItem key={source.url} source={source} index={i} />
))}
</div>
{divider}
{/* ── Certificate ── */}
<p
className="text-[13px] font-medium uppercase tracking-wider mb-3"
style={{ color: blue[400] }}
>
Sertifikat
</p>
<div
className="flex items-center gap-3 rounded-lg p-3.5"
style={{ background: blue[50], border: `0.5px solid ${blue[100]}` }}
>
<div
className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0"
style={{ background: blue[600] }}
>
<svg
width="16"
height="16"
viewBox="0 0 18 18"
fill="none"
aria-hidden="true"
>
<path
d="M9 1L11.2 6.5H17L12.4 10L14.1 16L9 12.8L3.9 16L5.6 10L1 6.5H6.8L9 1Z"
fill="#E6F1FB"
/>
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-[13px] font-medium" style={{ color: blue[900] }}>
{certificate.issuerName} sertifikati
</p>
<p
className="text-[11px] mt-0.5 font-mono truncate"
style={{ color: blue[600] }}
>
{certificate.verificationCode} · Amal qilish:{' '}
{certificate.expiresAt}
</p>
</div>
<button
onClick={handleDownload}
className="text-xs px-3 py-1.5 rounded-md shrink-0 transition-colors cursor-pointer"
style={{
border: `0.5px solid ${blue[400]}`,
color: blue[600],
background: downloading ? blue[100] : 'transparent',
}}
>
{downloading ? 'Yuklanmoqda...' : 'Yuklab olish ↗'}
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,782 @@
// 'use client';
// import { useState } from 'react';
// import EmptySourcesList from './emptyList';
// import { PlagiatData, SemanticMetrics } from '../lib/types';
// // ─── Mock data ────────────────────────────────────────────────────────────────
// const mockData: PlagiatData = {
// name: 'Sokhibjon Orzikulov',
// initials: 'SO',
// email: 'sakhib@orzklv.uz',
// location: 'Tashkent, Uzbekistan',
// fileName: 'resume_sokhibjon.pdf',
// checkedAt: '2026-04-02',
// humanPercent: 90,
// aiPercent: 10,
// plagiarismPercent: 92,
// originalityPercent: 5,
// citationPercent: 3,
// highlightedText: [
// { text: 'Sokhibjon Orzikulov — ', plagiarized: false },
// {
// text: 'tashkentlik dasturchi va open source muhandis.',
// plagiarized: true,
// },
// { text: ' U ', plagiarized: false },
// { text: 'NixOS, Rust va embedded tizimlar', plagiarized: true },
// { text: ' ustida ishlaydi. ', plagiarized: false },
// { text: 'Uzinfocom kompaniyasida', plagiarized: true },
// {
// text: " Open Source bo'lim rahbari sifatida faoliyat yuritadi va ",
// plagiarized: false,
// },
// { text: 'Floss Uzbekistan tashkilotini boshqaradi.', plagiarized: true },
// { text: ' U shuningdek ', plagiarized: false },
// {
// text: "LLVM committer bo'lib, bir nechta dasturlash tillari va Linux distributivlarini",
// plagiarized: true,
// },
// { text: ' maintain qilgan.', plagiarized: false },
// ],
// sources: [
// {
// url: 'https://www.b-soc.ru/partner/everland-soczialno-predprinimatelskij-proekt/',
// matchPercentage: 9,
// module: 'Internet Free',
// },
// {
// url: 'https://legalacts.ru/doc/prikaz-rosstata-ot-30072014-n-493-ob/',
// matchPercentage: 7,
// module: 'Internet Free',
// },
// {
// url: 'https://www.consultant.ru/document/cons_doc_LAW_402170/',
// matchPercentage: 6,
// module: 'Internet Free',
// },
// {
// url: 'https://be5.biz/pravo/t029/12.html',
// matchPercentage: 5,
// module: 'Internet Free',
// },
// {
// url: 'https://kn51.ru/wp-content/uploads/2022/03/pravila_priema_2022n-1-1.pdf',
// matchPercentage: 4,
// module: 'Internet Free',
// },
// {
// url: 'http://knacits.ru/index.php/rumts',
// matchPercentage: 3,
// module: 'Internet Free',
// },
// {
// url: 'https://spb.ucheba.ru/program/703329',
// matchPercentage: 3,
// module: 'Internet Free',
// },
// {
// url: 'https://nnovcons.ru/files/priem2022/priem_pravila_2022_p.pdf',
// matchPercentage: 2,
// module: 'Internet Free',
// },
// ],
// semantic: {
// totalWords: 1477,
// uniqueWords: 593,
// lexicalUniqueness: 40.15,
// avgWordLength: 5.92,
// geoAvgWordLength: 5.25,
// minWordLength: 1,
// maxWordLength: 15,
// sentences: 105,
// avgWordsPerSentence: 14.07,
// polysyllabicWords: 561,
// polysyllabicPercent: 37.98,
// totalChars: 10742,
// charsNoSpaces: 9386,
// vowels: 3177,
// consonants: 5357,
// punctuation: 274,
// uppercase: 575,
// lowercase: 7959,
// digits: 203,
// capsLockWords: 49,
// stopWords: 0,
// stopWordsPercent: 0,
// junkWords: 0,
// junkPercent: 0,
// maxConsecutiveRepeats: 2,
// top5Words: 'of(42), for(39), the(35), and(33), on(27)',
// spamRatio: 0.03,
// hasHtml: false,
// hasEmail: true,
// hasUrl: true,
// hasDate: false,
// hasPhone: true,
// startsEqualsEnd: false,
// isPalindrome: false,
// hasQuestion: true,
// hasExclamation: false,
// paragraphs: 3,
// lines: 191,
// latinPercent: 100,
// cyrillicPercent: 0,
// longWords16plus: 0,
// },
// certificate: {
// id: '1',
// verificationCode: 'PLAG-9001-VERIFY',
// issuerName: 'Global Plagiarism Checker',
// issuedAt: '2026-03-30',
// expiresAt: '2027-03-30',
// downloadUrl: '/certificates/cert-9001.pdf',
// },
// };
// // ─── Blue palette ─────────────────────────────────────────────────────────────
// const blue = {
// 50: '#E6F1FB',
// 100: '#B5D4F4',
// 200: '#85B7EB',
// 400: '#378ADD',
// 600: '#185FA5',
// 800: '#0C447C',
// 900: '#042C53',
// };
// // ─── Small helpers ────────────────────────────────────────────────────────────
// function Divider() {
// return (
// <hr
// style={{
// border: 'none',
// borderTop: `0.5px solid ${blue[100]}`,
// margin: '1.25rem 0',
// }}
// />
// );
// }
// function SectionTitle({ children }: { children: React.ReactNode }) {
// return (
// <p
// className="text-[12px] font-medium uppercase tracking-widest mb-3"
// style={{ color: blue[400] }}
// >
// {children}
// </p>
// );
// }
// function MetricCard({ label, value }: { label: string; value: string }) {
// return (
// <div className="rounded-lg p-3" style={{ background: blue[50] }}>
// <p className="text-[11px] mb-1" style={{ color: blue[600] }}>
// {label}
// </p>
// <p className="text-lg font-medium" style={{ color: blue[900] }}>
// {value}
// </p>
// </div>
// );
// }
// function BarRow({
// label,
// value,
// color,
// }: {
// label: string;
// value: number;
// color: string;
// }) {
// return (
// <div className="mb-2.5">
// <div className="flex justify-between items-center text-sm mb-1">
// <span
// className="flex items-center gap-1.5"
// style={{ color: blue[600] }}
// >
// <span
// className="inline-block w-2.5 h-2.5 rounded-full shrink-0"
// style={{ background: color }}
// />
// {label}
// </span>
// <span className="font-medium" style={{ color: blue[900] }}>
// {value}%
// </span>
// </div>
// <div
// className="h-1.5 rounded-full overflow-hidden"
// style={{ background: blue[100] }}
// >
// <div
// className="h-full rounded-full"
// style={{
// width: `${value}%`,
// background: color,
// transition: 'width 0.8s ease',
// }}
// />
// </div>
// </div>
// );
// }
// function CircleGauge({ value }: { value: number }) {
// const r = 48;
// const circ = 2 * Math.PI * r;
// const offset = circ - (value / 100) * circ;
// return (
// <svg
// width="120"
// height="120"
// viewBox="0 0 120 120"
// aria-label={`${value}% plagiat`}
// >
// <circle
// cx="60"
// cy="60"
// r={r}
// fill="none"
// stroke={blue[100]}
// strokeWidth="12"
// />
// <circle
// cx="60"
// cy="60"
// r={r}
// fill="none"
// stroke={blue[900]}
// strokeWidth="12"
// strokeDasharray={circ}
// strokeDashoffset={offset}
// strokeLinecap="round"
// transform="rotate(-90 60 60)"
// style={{ transition: 'stroke-dashoffset 0.9s ease' }}
// />
// <text
// x="60"
// y="56"
// textAnchor="middle"
// fontSize="22"
// fontWeight="500"
// fill={blue[900]}
// >
// {value}%
// </text>
// <text x="60" y="72" textAnchor="middle" fontSize="10" fill={blue[400]}>
// plagiat
// </text>
// </svg>
// );
// }
// // ─── Semantic modal ───────────────────────────────────────────────────────────
// function SemanticModal({
// data,
// onClose,
// }: {
// data: SemanticMetrics | null;
// onClose: () => void;
// }) {
// const rows: [string, string][] | null = data
// ? [
// ["Umumiy so'zlar", String(data.totalWords)],
// ["Unikal so'zlar", String(data.uniqueWords)],
// ['Leksik unikalligi (%)', data.lexicalUniqueness.toFixed(2)],
// ["O'rtacha so'z uzunligi", data.avgWordLength.toFixed(2)],
// ["Geometrik o'rtacha uzunlik", data.geoAvgWordLength.toFixed(2)],
// ["Min. so'z uzunligi", String(data.minWordLength)],
// ["Maks. so'z uzunligi", String(data.maxWordLength)],
// ['Jumlalar soni', String(data.sentences)],
// ["O'rtacha so'z/jumla", data.avgWordsPerSentence.toFixed(2)],
// ["Ko'p bo'g'inli so'zlar", String(data.polysyllabicWords)],
// ["Ko'p bo'g'inli ulushi (%)", data.polysyllabicPercent.toFixed(2)],
// ['Jami belgilar', String(data.totalChars)],
// ["Bo'shliqlarsiz", String(data.charsNoSpaces)],
// ['Unlilar', String(data.vowels)],
// ['Undoshlar', String(data.consonants)],
// ['Tinish belgilari', String(data.punctuation)],
// ['Bosh harflar', String(data.uppercase)],
// ['Kichik harflar', String(data.lowercase)],
// ['Raqamlar', String(data.digits)],
// ["CAPSLOCK so'zlar", String(data.capsLockWords)],
// ["Stop-so'zlar", String(data.stopWords)],
// ["Stop-so'zlar ulushi (%)", data.stopWordsPercent.toFixed(2)],
// ["Keraksiz so'zlar", String(data.junkWords)],
// ['Keraksiz ulushi (%)', data.junkPercent.toFixed(2)],
// ['Maks. ketma-ket takrorlar', String(data.maxConsecutiveRepeats)],
// ["TOP-5 so'zlar", data.top5Words],
// ['Spam darajasi', data.spamRatio.toFixed(2)],
// ['HTML teglar', data.hasHtml ? 'Ha' : "Yo'q"],
// ['Email topildi', data.hasEmail ? 'Ha' : "Yo'q"],
// ['URL topildi', data.hasUrl ? 'Ha' : "Yo'q"],
// ['Sana topildi', data.hasDate ? 'Ha' : "Yo'q"],
// ['Telefon topildi', data.hasPhone ? 'Ha' : "Yo'q"],
// ['Boshi = Oxiri', data.startsEqualsEnd ? 'Ha' : "Yo'q"],
// ['Palindrom', data.isPalindrome ? 'Ha' : "Yo'q"],
// ['Savol belgisi', data.hasQuestion ? 'Ha' : "Yo'q"],
// ['Undov belgisi', data.hasExclamation ? 'Ha' : "Yo'q"],
// ['Paragraflar', String(data.paragraphs)],
// ['Qatorlar', String(data.lines)],
// ['Lotin ulushi (%)', data.latinPercent.toFixed(2)],
// ['Kirill ulushi (%)', data.cyrillicPercent.toFixed(2)],
// ["16+ belgili so'zlar", String(data.longWords16plus)],
// ]
// : [];
// return (
// <div
// className="fixed inset-0 z-50 flex items-center justify-center p-4"
// style={{ background: 'rgba(4,44,83,0.45)' }}
// onClick={onClose}
// >
// <div
// className="w-full max-w-xl rounded-xl overflow-hidden"
// style={{
// background: '#fff',
// border: `0.5px solid ${blue[100]}`,
// maxHeight: '80vh',
// display: 'flex',
// flexDirection: 'column',
// }}
// onClick={(e) => e.stopPropagation()}
// >
// {/* Modal header */}
// <div
// className="flex items-center justify-between px-5 py-4"
// style={{ borderBottom: `0.5px solid ${blue[100]}` }}
// >
// <span
// className="text-[14px] font-medium"
// style={{ color: blue[900] }}
// >
// Semantik tahlil
// </span>
// <button
// onClick={onClose}
// className="text-xl leading-none cursor-pointer"
// style={{ color: blue[400], background: 'none', border: 'none' }}
// >
// ×
// </button>
// </div>
// {/* Modal body */}
// <div className="overflow-y-auto flex-1">
// <table className="w-full text-sm">
// <thead>
// <tr style={{ background: blue[50] }}>
// <th
// className="text-left px-5 py-2.5 font-medium"
// style={{ color: blue[600] }}
// >
// Metrika
// </th>
// <th
// className="text-right px-5 py-2.5 font-medium"
// style={{ color: blue[600] }}
// >
// Qiymat
// </th>
// </tr>
// </thead>
// <tbody>
// {rows.map(([label, value], i) => (
// <tr
// key={label}
// style={{
// background: i % 2 === 0 ? '#fff' : blue[50],
// borderBottom: `0.5px solid ${blue[100]}`,
// }}
// >
// <td className="px-5 py-2" style={{ color: blue[800] }}>
// {label}
// </td>
// <td
// className="px-5 py-2 text-right font-medium"
// style={{ color: blue[900] }}
// >
// {value}
// </td>
// </tr>
// ))}
// </tbody>
// </table>
// </div>
// </div>
// </div>
// );
// }
// // ─── Main component ───────────────────────────────────────────────────────────
// export default function PlagiatResult({
// data = mockData,
// }: {
// data?: PlagiatData;
// }) {
// const [showSemantic, setShowSemantic] = useState(false);
// const [saving, setSaving] = useState(false);
// const handleSave = () => {
// setSaving(true);
// setTimeout(() => setSaving(false), 1500);
// };
// const handleRecheck = () => {
// alert("Yangi fayl yuklash sahifasiga yo'naltirish...");
// };
// return (
// <>
// {showSemantic && (
// <SemanticModal
// data={data.semantic}
// onClose={() => setShowSemantic(false)}
// />
// )}
// <div className="min-h-screen flex items-start justify-center p-6 bg-slate-50">
// <div
// className="w-full max-w-2xl rounded-xl p-5"
// style={{ background: '#fff', border: `0.5px solid ${blue[100]}` }}
// >
// {/* ── Certificate (top) ── */}
// {data.certificate && (
// <div
// className="flex items-center gap-3 rounded-lg p-3.5 mb-5"
// style={{
// background: blue[50],
// border: `0.5px solid ${blue[100]}`,
// }}
// >
// <div
// className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0"
// style={{ background: blue[600] }}
// >
// <svg
// width="16"
// height="16"
// viewBox="0 0 18 18"
// fill="none"
// aria-hidden="true"
// >
// <path
// d="M9 1L11.2 6.5H17L12.4 10L14.1 16L9 12.8L3.9 16L5.6 10L1 6.5H6.8L9 1Z"
// fill="#E6F1FB"
// />
// </svg>
// </div>
// <div className="flex-1 min-w-0">
// <p
// className="text-[13px] font-medium"
// style={{ color: blue[900] }}
// >
// {data.certificate.issuerName} sertifikati
// </p>
// <p
// className="text-[11px] mt-0.5 truncate"
// style={{ color: blue[600], fontFamily: 'monospace' }}
// >
// {data.certificate.verificationCode}
// {data.certificate.expiresAt}
// </p>
// </div>
// <a
// href={data.certificate.downloadUrl}
// className="text-xs px-3 py-1.5 rounded-md shrink-0 transition-colors no-underline"
// style={{
// border: `0.5px solid ${blue[400]}`,
// color: blue[600],
// }}
// >
// Yuklab olish ↗
// </a>
// </div>
// )}
// {/* ── Header ── */}
// <div className="flex items-center gap-3 mb-5">
// <div
// className="w-11 h-11 rounded-full flex items-center justify-center text-sm font-medium shrink-0"
// style={{ background: blue[50], color: blue[600] }}
// >
// {data.initials}
// </div>
// <div className="flex-1 min-w-0">
// <div className="flex items-center gap-2 flex-wrap">
// <span
// className="text-[15px] font-medium"
// style={{ color: blue[900] }}
// >
// {data.name}
// </span>
// <span
// className="text-xs px-2.5 py-0.5 rounded-md font-medium"
// style={{ background: blue[100], color: blue[800] }}
// >
// {data.plagiarismPercent}% plagiat
// </span>
// </div>
// <p
// className="text-[13px] mt-0.5 truncate"
// style={{ color: blue[400] }}
// >
// {data.fileName} · {data.email} · {data.location}
// </p>
// </div>
// <div className="text-right shrink-0">
// <p className="text-[11px]" style={{ color: blue[400] }}>
// Tekshirilgan
// </p>
// <p className="text-[12px] mt-0.5" style={{ color: blue[600] }}>
// {data.checkedAt}
// </p>
// </div>
// </div>
// {/* ── Human vs AI bar ── */}
// <div className="mb-5">
// <div
// className="flex justify-between text-xs mb-1.5"
// style={{ color: blue[600] }}
// >
// <span>Inson: {data.humanPercent}%</span>
// <span>II: {data.aiPercent}%</span>
// </div>
// <div
// className="h-3 rounded-full overflow-hidden flex"
// style={{ background: blue[100] }}
// >
// <div
// className="h-full"
// style={{
// width: `${data.humanPercent}%`,
// background: `linear-gradient(to right, ${blue[600]}, ${blue[400]})`,
// transition: 'width 0.8s ease',
// }}
// />
// <div
// className="h-full flex-1"
// style={{ background: '#e879a0' }}
// />
// </div>
// </div>
// <Divider />
// {/* ── Top metrics ── */}
// <div className="grid grid-cols-4 gap-2.5 mb-5">
// <MetricCard
// label="Plagiat darajasi"
// value={`${data.plagiarismPercent}%`}
// />
// <MetricCard label="AI yozgan" value={`${data.aiPercent}%`} />
// <MetricCard
// label="Originallik"
// value={`${data.originalityPercent}%`}
// />
// <MetricCard label="Iqtibos" value={`${data.citationPercent}%`} />
// </div>
// {/* ── Gauge + bars ── */}
// <div className="flex items-center gap-5 mb-5">
// <CircleGauge value={data.plagiarismPercent} />
// <div className="flex-1">
// <BarRow
// label="Plagiat"
// value={data.plagiarismPercent}
// color={blue[900]}
// />
// <BarRow
// label="AI generatsiya"
// value={data.aiPercent}
// color={blue[600]}
// />
// <BarRow
// label="Original"
// value={data.originalityPercent}
// color={blue[400]}
// />
// <BarRow
// label="Iqtibos"
// value={data.citationPercent}
// color={blue[200]}
// />
// </div>
// </div>
// <Divider />
// {/* ── Sources list ── */}
// <SectionTitle>Manbalar ro'yxati</SectionTitle>
// {data.sources.length > 0 ? (
// <div className="mb-5">
// <table className="w-full text-sm">
// <thead>
// <tr style={{ borderBottom: `0.5px solid ${blue[100]}` }}>
// <th
// className="text-left pb-2 font-medium"
// style={{ color: blue[600] }}
// >
// URL
// </th>
// <th
// className="text-right pb-2 font-medium"
// style={{ color: blue[600] }}
// >
// Modul
// </th>
// </tr>
// </thead>
// <tbody>
// {data.sources.map((s, i) => (
// <tr
// key={s.url}
// style={{ borderBottom: `0.5px solid ${blue[50]}` }}
// >
// <td className="py-1.5 pr-4">
// <div className="flex items-center gap-2">
// <div className="w-1.5 h-1.5 rounded-full shrink-0 bg-blue-600" />
// <span
// className="text-[12px] truncate max-w-95 block"
// style={{ color: blue[800] }}
// >
// {s.url}
// </span>
// </div>
// </td>
// <td className="py-1.5 text-right text-[12px] whitespace-nowrap text-blue-500">
// {s.module}
// </td>
// </tr>
// ))}
// </tbody>
// </table>
// </div>
// ) : (
// <EmptySourcesList />
// )}
// <Divider />
// {/* ── Highlighted text ── */}
// <SectionTitle>Matn ko'rish</SectionTitle>
// <div
// className="rounded-lg p-4 text-sm leading-relaxed mb-5"
// style={{
// background: blue[50],
// border: `0.5px solid ${blue[100]}`,
// color: blue[900],
// lineHeight: '1.8',
// }}
// >
// {data.highlightedText.map((seg, i) =>
// seg.plagiarized ? (
// <mark
// key={i}
// style={{
// background: '#FEE2E2',
// color: '#991B1B',
// borderRadius: '3px',
// padding: '1px 2px',
// }}
// >
// {seg.text}
// </mark>
// ) : (
// <span key={i}>{seg.text}</span>
// ),
// )}
// <p className="mt-3 text-[11px]" style={{ color: blue[400] }}>
// <span
// className="inline-block w-3 h-3 rounded-sm mr-1 align-middle"
// style={{ background: '#FEE2E2', border: '1px solid #FCA5A5' }}
// />
// Qizil bilan belgilangan qismlar plagiat deb topilgan.
// </p>
// </div>
// {/* ── 3 Buttons ── */}
// <div className="flex gap-2.5 flex-wrap mb-5">
// <button
// onClick={handleSave}
// className="text-sm px-4 py-2 rounded-lg font-medium transition-colors cursor-pointer"
// style={{
// background: saving ? blue[800] : blue[900],
// color: '#fff',
// border: 'none',
// }}
// >
// {saving ? 'Saqlanmoqda...' : 'Hisobotni saqlash'}
// </button>
// <button
// onClick={() => setShowSemantic(true)}
// className="text-sm px-4 py-2 rounded-lg font-medium transition-colors cursor-pointer"
// style={{
// background: blue[600],
// color: '#fff',
// border: 'none',
// }}
// >
// Semantik tahlil
// </button>
// <button
// onClick={handleRecheck}
// className="text-sm px-4 py-2 rounded-lg font-medium transition-colors cursor-pointer"
// style={{
// background: 'transparent',
// color: blue[600],
// border: `0.5px solid ${blue[400]}`,
// }}
// >
// Yana bir fayl tekshirish
// </button>
// </div>
// <Divider />
// {/* ── Text analysis mini metrics ── */}
// <SectionTitle>Matn tahlili</SectionTitle>
// {data.semantic ? (
// <div className="grid grid-cols-3 gap-2">
// <MetricCard
// label="Jami so'z"
// value={data.semantic.totalWords.toLocaleString()}
// />
// <MetricCard
// label="Unikal so'z"
// value={data.semantic.uniqueWords.toLocaleString()}
// />
// <MetricCard
// label="Leksik unikalligi"
// value={`${data.semantic.lexicalUniqueness.toFixed(1)}%`}
// />
// <MetricCard
// label="Jumlalar"
// value={String(data.semantic.sentences)}
// />
// <MetricCard
// label="O'rt. so'z/juml."
// value={data.semantic.avgWordsPerSentence.toFixed(1)}
// />
// <MetricCard
// label="Qatorlar"
// value={String(data.semantic.lines)}
// />
// </div>
// ) : (
// <EmptySourcesList />
// )}
// </div>
// </div>
// </>
// );
// }

View File

@@ -0,0 +1,42 @@
import { blue } from '../../lib/constant';
interface Props {
label: string;
value: number;
color: string;
}
export default function BarRow({ label, value, color }: Props) {
return (
<div className="mb-2.5">
<div className="flex justify-between items-center text-sm mb-1">
<span
className="flex items-center gap-1.5"
style={{ color: blue[600] }}
>
<span
className="inline-block w-2.5 h-2.5 rounded-full shrink-0"
style={{ background: color }}
/>
{label}
</span>
<span className="font-medium" style={{ color: blue[900] }}>
{value}%
</span>
</div>
<div
className="h-1.5 rounded-full overflow-hidden"
style={{ background: blue[100] }}
>
<div
className="h-full rounded-full"
style={{
width: `${value}%`,
background: color,
transition: 'width 0.8s ease',
}}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,50 @@
import { blue } from '../../lib/constant';
export default function CircleGauge({ value }: { value: number }) {
const r = 48;
const circ = 2 * Math.PI * r;
const offset = circ - (value / 100) * circ;
return (
<svg
width="120"
height="120"
viewBox="0 0 120 120"
aria-label={`${value}% plagiat`}
>
<circle
cx="60"
cy="60"
r={r}
fill="none"
stroke={blue[100]}
strokeWidth="12"
/>
<circle
cx="60"
cy="60"
r={r}
fill="none"
stroke={blue[900]}
strokeWidth="12"
strokeDasharray={circ}
strokeDashoffset={offset}
strokeLinecap="round"
transform="rotate(-90 60 60)"
style={{ transition: 'stroke-dashoffset 0.9s ease' }}
/>
<text
x="60"
y="56"
textAnchor="middle"
fontSize="22"
fontWeight="500"
fill={blue[900]}
>
{value}%
</text>
<text x="60" y="72" textAnchor="middle" fontSize="10" fill={blue[400]}>
plagiat
</text>
</svg>
);
}

View File

@@ -0,0 +1,13 @@
import { blue } from '../../lib/constant';
export default function Divider() {
return (
<hr
style={{
border: 'none',
borderTop: `0.5px solid ${blue[100]}`,
margin: '1.25rem 0',
}}
/>
);
}

View File

@@ -0,0 +1,44 @@
import { blue } from '../../lib/constant';
import { PlagiatData } from '../../lib/types';
import BarRow from './BarRow';
import CircleGauge from './CircleGauge';
type Props = Pick<
PlagiatData,
'plagiarismPercent' | 'aiPercent' | 'originalityPercent' | 'citationPercent'
>;
export default function GaugeWithBars({
plagiarismPercent,
aiPercent,
originalityPercent,
citationPercent,
}: Props) {
return (
<div className="flex items-center gap-5 mb-5">
<CircleGauge value={plagiarismPercent || 0} />
<div className="flex-1">
<BarRow
label="Plagiat"
value={plagiarismPercent || 0}
color={blue[900]}
/>
<BarRow
label="AI generatsiya"
value={aiPercent || 0}
color={blue[600]}
/>
<BarRow
label="Original"
value={originalityPercent || 0}
color={blue[400]}
/>
<BarRow
label="Iqtibos"
value={citationPercent || 0}
color={blue[200]}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,61 @@
import { blue } from '../../lib/constant';
import { PlagiatData } from '../../lib/types';
type Props = Pick<
PlagiatData,
| 'initials'
| 'name'
| 'plagiarismPercent'
| 'fileName'
| 'email'
| 'location'
| 'checkedAt'
>;
export default function Header({
initials,
name,
plagiarismPercent,
fileName,
email,
location,
checkedAt,
}: Props) {
return (
<div className="flex items-center gap-3 mb-5">
<div
className="w-11 h-11 rounded-full flex items-center justify-center text-sm font-medium shrink-0"
style={{ background: blue[50], color: blue[600] }}
>
{initials}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span
className="text-[15px] font-medium"
style={{ color: blue[900] }}
>
{name}
</span>
<span
className="text-xs px-2.5 py-0.5 rounded-md font-medium"
style={{ background: blue[100], color: blue[800] }}
>
{plagiarismPercent}% plagiat
</span>
</div>
<p className="text-[13px] mt-0.5 truncate" style={{ color: blue[400] }}>
{fileName} · {email} · {location}
</p>
</div>
<div className="text-right shrink-0">
<p className="text-[11px]" style={{ color: blue[400] }}>
Tekshirilgan
</p>
<p className="text-[12px] mt-0.5" style={{ color: blue[600] }}>
{checkedAt}
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,49 @@
import { blue } from '../../lib/constant';
import { HighlightSegment } from '../../lib/types';
import SectionTitle from './SectionTitle';
export default function HighlightedText({
segments,
}: {
segments: HighlightSegment[];
}) {
return (
<>
<SectionTitle>Matn ko&apos;rish</SectionTitle>
<div
className="rounded-lg p-4 text-sm leading-relaxed mb-5"
style={{
background: blue[50],
border: `0.5px solid ${blue[100]}`,
color: blue[900],
lineHeight: '1.8',
}}
>
{segments.map((seg, i) =>
seg.plagiarized ? (
<mark
key={i}
style={{
background: '#FEE2E2',
color: '#991B1B',
borderRadius: '3px',
padding: '1px 2px',
}}
>
{seg.text}
</mark>
) : (
<span key={i}>{seg.text}</span>
),
)}
<p className="mt-3 text-[11px]" style={{ color: blue[400] }}>
<span
className="inline-block w-3 h-3 rounded-sm mr-1 align-middle"
style={{ background: '#FEE2E2', border: '1px solid #FCA5A5' }}
/>
Qizil bilan belgilangan qismlar plagiat deb topilgan.
</p>
</div>
</>
);
}

View File

@@ -0,0 +1,34 @@
import { blue } from '../../lib/constant';
interface Props {
humanPercent: number;
aiPercent: number;
}
export default function HumanAiBar({ humanPercent, aiPercent }: Props) {
return (
<div className="mb-5">
<div
className="flex justify-between text-xs mb-1.5"
style={{ color: blue[600] }}
>
<span>Inson: {humanPercent}%</span>
<span>II: {aiPercent}%</span>
</div>
<div
className="h-3 rounded-full overflow-hidden flex"
style={{ background: blue[100] }}
>
<div
className="h-full"
style={{
width: `${humanPercent}%`,
background: `linear-gradient(to right, ${blue[600]}, ${blue[400]})`,
transition: 'width 0.8s ease',
}}
/>
<div className="h-full flex-1" style={{ background: '#e879a0' }} />
</div>
</div>
);
}

View File

@@ -0,0 +1,19 @@
import { blue } from '../../lib/constant';
interface Props {
label: string;
value: string;
}
export default function MetricCard({ label, value }: Props) {
return (
<div className="rounded-lg p-3" style={{ background: blue[50] }}>
<p className="text-[11px] mb-1" style={{ color: blue[600] }}>
{label}
</p>
<p className="text-lg font-medium" style={{ color: blue[900] }}>
{value}
</p>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import { blue } from '../../lib/constant';
export default function SectionTitle({
children,
}: {
children: React.ReactNode;
}) {
return (
<p
className="text-[12px] font-medium uppercase tracking-widest mb-3"
style={{ color: blue[400] }}
>
{children}
</p>
);
}

View File

@@ -0,0 +1,133 @@
import { blue } from '../../lib/constant';
import { SemanticMetrics } from '../../lib/types';
interface Props {
data: SemanticMetrics;
onClose: () => void;
}
export default function SemanticModal({ data, onClose }: Props) {
const rows: [string, string][] = [
["Umumiy so'zlar", String(data.totalWords)],
["Unikal so'zlar", String(data.uniqueWords)],
['Leksik unikalligi (%)', data.lexicalUniqueness.toFixed(2)],
["O'rtacha so'z uzunligi", data.avgWordLength.toFixed(2)],
["Geometrik o'rtacha uzunlik", data.geoAvgWordLength.toFixed(2)],
["Min. so'z uzunligi", String(data.minWordLength)],
["Maks. so'z uzunligi", String(data.maxWordLength)],
['Jumlalar soni', String(data.sentences)],
["O'rtacha so'z/jumla", data.avgWordsPerSentence.toFixed(2)],
["Ko'p bo'g'inli so'zlar", String(data.polysyllabicWords)],
["Ko'p bo'g'inli ulushi (%)", data.polysyllabicPercent.toFixed(2)],
['Jami belgilar', String(data.totalChars)],
["Bo'shliqlarsiz", String(data.charsNoSpaces)],
['Unlilar', String(data.vowels)],
['Undoshlar', String(data.consonants)],
['Tinish belgilari', String(data.punctuation)],
['Bosh harflar', String(data.uppercase)],
['Kichik harflar', String(data.lowercase)],
['Raqamlar', String(data.digits)],
["CAPSLOCK so'zlar", String(data.capsLockWords)],
["Stop-so'zlar", String(data.stopWords)],
["Stop-so'zlar ulushi (%)", data.stopWordsPercent.toFixed(2)],
["Keraksiz so'zlar", String(data.junkWords)],
['Keraksiz ulushi (%)', data.junkPercent.toFixed(2)],
['Maks. ketma-ket takrorlar', String(data.maxConsecutiveRepeats)],
["TOP-5 so'zlar", data.top5Words],
['Spam darajasi', data.spamRatio.toFixed(2)],
['HTML teglar', data.hasHtml ? 'Ha' : "Yo'q"],
['Email topildi', data.hasEmail ? 'Ha' : "Yo'q"],
['URL topildi', data.hasUrl ? 'Ha' : "Yo'q"],
['Sana topildi', data.hasDate ? 'Ha' : "Yo'q"],
['Telefon topildi', data.hasPhone ? 'Ha' : "Yo'q"],
['Boshi = Oxiri', data.startsEqualsEnd ? 'Ha' : "Yo'q"],
['Palindrom', data.isPalindrome ? 'Ha' : "Yo'q"],
['Savol belgisi', data.hasQuestion ? 'Ha' : "Yo'q"],
['Undov belgisi', data.hasExclamation ? 'Ha' : "Yo'q"],
['Paragraflar', String(data.paragraphs)],
['Qatorlar', String(data.lines)],
['Lotin ulushi (%)', data.latinPercent.toFixed(2)],
['Kirill ulushi (%)', data.cyrillicPercent.toFixed(2)],
["16+ belgili so'zlar", String(data.longWords16plus)],
];
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
style={{ background: 'rgba(4,44,83,0.45)' }}
onClick={onClose}
>
<div
className="w-full max-w-xl rounded-xl overflow-hidden"
style={{
background: '#fff',
border: `0.5px solid ${blue[100]}`,
maxHeight: '80vh',
display: 'flex',
flexDirection: 'column',
}}
onClick={(e) => e.stopPropagation()}
>
<div
className="flex items-center justify-between px-5 py-4"
style={{ borderBottom: `0.5px solid ${blue[100]}` }}
>
<span
className="text-[14px] font-medium"
style={{ color: blue[900] }}
>
Semantik tahlil
</span>
<button
onClick={onClose}
className="text-xl leading-none cursor-pointer"
style={{ color: blue[400], background: 'none', border: 'none' }}
>
×
</button>
</div>
<div className="overflow-y-auto flex-1">
<table className="w-full text-sm">
<thead>
<tr style={{ background: blue[50] }}>
<th
className="text-left px-5 py-2.5 font-medium"
style={{ color: blue[600] }}
>
Metrika
</th>
<th
className="text-right px-5 py-2.5 font-medium"
style={{ color: blue[600] }}
>
Qiymat
</th>
</tr>
</thead>
<tbody>
{rows.map(([label, value], i) => (
<tr
key={label}
style={{
background: i % 2 === 0 ? '#fff' : blue[50],
borderBottom: `0.5px solid ${blue[100]}`,
}}
>
<td className="px-5 py-2" style={{ color: blue[800] }}>
{label}
</td>
<td
className="px-5 py-2 text-right font-medium"
style={{ color: blue[900] }}
>
{value}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,51 @@
import { blue } from '../../lib/constant';
import { Certificate } from '../../lib/types';
export default function CertificateCard({
certificate,
}: {
certificate: Certificate;
}) {
return (
<div
className="flex items-center gap-3 rounded-lg p-3.5 mb-5"
style={{ background: blue[50], border: `0.5px solid ${blue[100]}` }}
>
<div
className="w-9 h-9 rounded-lg flex items-center justify-center shrink-0"
style={{ background: blue[600] }}
>
<svg
width="16"
height="16"
viewBox="0 0 18 18"
fill="none"
aria-hidden="true"
>
<path
d="M9 1L11.2 6.5H17L12.4 10L14.1 16L9 12.8L3.9 16L5.6 10L1 6.5H6.8L9 1Z"
fill="#E6F1FB"
/>
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-[13px] font-medium" style={{ color: blue[900] }}>
{certificate.issuerName} sertifikati
</p>
<p
className="text-[11px] mt-0.5 truncate"
style={{ color: blue[600], fontFamily: 'monospace' }}
>
{certificate.verificationCode}
</p>
</div>
<a
href={certificate.downloadUrl}
className="text-xs px-3 py-1.5 rounded-md shrink-0 transition-colors no-underline"
style={{ border: `0.5px solid ${blue[400]}`, color: blue[600] }}
>
Yuklab olish
</a>
</div>
);
}

View File

@@ -0,0 +1,65 @@
import { blue } from '../../lib/constant';
import { Source } from '../../lib/types';
import EmptySourcesList from '../emptyList';
import SectionTitle from './SectionTitle';
export default function SourcesList({ sources }: { sources: Source[] }) {
return (
<>
<SectionTitle>Manbalar ro&apos;yxati</SectionTitle>
{sources.length > 0 ? (
<div className="mb-5">
<table className="w-full text-sm">
<thead>
<tr style={{ borderBottom: `0.5px solid ${blue[100]}` }}>
<th
className="text-left pb-2 font-medium"
style={{ color: blue[600] }}
>
URL
</th>
<th
className="text-right pb-2 font-medium"
style={{ color: blue[600] }}
>
Modul
</th>
</tr>
</thead>
<tbody>
{sources.map((s, i) => (
<tr
key={s.url}
style={{ borderBottom: `0.5px solid ${blue[50]}` }}
>
<td className="py-1.5 pr-4">
<div className="flex items-center gap-2">
<div
className="w-1.5 h-1.5 rounded-full shrink-0"
style={{ background: i < 3 ? blue[900] : blue[200] }}
/>
<span
className="text-[12px] truncate max-w-95 block"
style={{ color: blue[800] }}
>
{s.url}
</span>
</div>
</td>
<td
className="py-1.5 text-right text-[12px] whitespace-nowrap"
style={{ color: blue[600] }}
>
{s.module}
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<EmptySourcesList />
)}
</>
);
}

View File

@@ -0,0 +1,38 @@
import { SemanticMetrics } from '../../lib/types';
import MetricCard from './Metriccard';
import SectionTitle from './SectionTitle';
export default function TextAnalysis({
semantic,
}: {
semantic?: SemanticMetrics;
}) {
if (semantic === undefined) {
return <p>Ma&apos;lumot topilmadi</p>;
}
return (
<>
<SectionTitle>Matn tahlili</SectionTitle>
<div className="grid grid-cols-3 gap-2">
<MetricCard
label="Jami so'z"
value={semantic.totalWords.toLocaleString()}
/>
<MetricCard
label="Unikal so'z"
value={semantic.uniqueWords.toLocaleString()}
/>
<MetricCard
label="Leksik unikalligi"
value={`${semantic.lexicalUniqueness.toFixed(1)}%`}
/>
<MetricCard label="Jumlalar" value={String(semantic.sentences)} />
<MetricCard
label="O'rt. so'z/juml."
value={semantic.avgWordsPerSentence.toFixed(1)}
/>
<MetricCard label="Qatorlar" value={String(semantic.lines)} />
</div>
</>
);
}

View File

@@ -0,0 +1,23 @@
import { PlagiatData } from '../../lib/types';
import MetricCard from './Metriccard';
type Props = Pick<
PlagiatData,
'plagiarismPercent' | 'aiPercent' | 'originalityPercent' | 'citationPercent'
>;
export default function TopMetrics({
plagiarismPercent,
aiPercent,
originalityPercent,
citationPercent,
}: Props) {
return (
<div className="grid grid-cols-4 gap-2.5 mb-5">
<MetricCard label="Plagiat darajasi" value={`${plagiarismPercent}%`} />
<MetricCard label="AI yozgan" value={`${aiPercent}%`} />
<MetricCard label="Originallik" value={`${originalityPercent}%`} />
<MetricCard label="Iqtibos" value={`${citationPercent}%`} />
</div>
);
}

View File

@@ -0,0 +1,49 @@
import { blue } from '../../lib/constant';
interface Props {
saving: boolean;
onSave: () => void;
onSemantic: () => void;
onRecheck: () => void;
}
export default function ActionButtons({
saving,
onSave,
onSemantic,
onRecheck,
}: Props) {
return (
<div className="flex gap-2.5 flex-wrap mb-5">
<button
onClick={onSave}
className="text-sm px-4 py-2 rounded-lg font-medium transition-colors cursor-pointer"
style={{
background: saving ? blue[800] : blue[900],
color: '#fff',
border: 'none',
}}
>
{saving ? 'Saqlanmoqda...' : 'Hisobotni saqlash'}
</button>
<button
onClick={onSemantic}
className="text-sm px-4 py-2 rounded-lg font-medium transition-colors cursor-pointer"
style={{ background: blue[600], color: '#fff', border: 'none' }}
>
Semantik tahlil
</button>
<button
onClick={onRecheck}
className="text-sm px-4 py-2 rounded-lg font-medium transition-colors cursor-pointer"
style={{
background: 'transparent',
color: blue[600],
border: `0.5px solid ${blue[400]}`,
}}
>
Yana bir fayl tekshirish
</button>
</div>
);
}

View File

@@ -1,510 +0,0 @@
// ─────────────────────────────────────────────────────────────
// PlagiarismDetailPage — Main Feature Component
// ─────────────────────────────────────────────────────────────
'use client';
import React from 'react';
import { useTranslations } from 'next-intl';
import { PlagiarismCheck } from '../lib/types';
import {
getFileExtension,
formatDate,
formatFileSize,
formatCurrency,
} from '../lib/api';
import { usePlagiarismDetail } from '../lib/useDetail';
import {
FileTypeBadge,
InfoRow,
SectionCard,
StatusBadge,
SimilarityMeter,
Avatar,
SkeletonLoader,
ErrorState,
} from '..';
// ── Icons (inline SVG for zero-dep) ──────────────────────────
const IconUser = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
);
const IconFile = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</svg>
);
const IconCalendar = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
);
const IconPayment = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
/>
</svg>
);
const IconShield = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
);
const IconCert = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
/>
</svg>
);
const IconDownload = () => (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.8}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
);
// const IconBack = () => (
// <svg
// className="w-4 h-4"
// fill="none"
// stroke="currentColor"
// viewBox="0 0 24 24"
// >
// <path
// strokeLinecap="round"
// strokeLinejoin="round"
// strokeWidth={2}
// d="M10 19l-7-7m0 0l7-7m-7 7h18"
// />
// </svg>
// );
const IconSource = () => (
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
);
// ── Sub-components ────────────────────────────────────────────
interface CheckDetailViewProps {
check: PlagiarismCheck;
}
const CheckHeader: React.FC<CheckDetailViewProps> = ({ check }) => {
const t = useTranslations('DetailPage');
return (
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4">
<Avatar
name={check.sender.name}
avatarUrl={check.sender.avatarUrl}
size="lg"
/>
<div>
<h1 className="text-xl font-bold text-slate-900">
{check.sender.name}
</h1>
<p className="text-sm text-slate-500">{check.sender.email}</p>
<p className="text-xs text-slate-400 mt-1 font-mono">
{t('id')}: {check.id}
</p>
</div>
</div>
<StatusBadge status={check.status} />
</div>
</div>
);
};
const SubmissionInfoCard: React.FC<CheckDetailViewProps> = ({ check }) => {
const t = useTranslations('DetailPage');
return (
<SectionCard
title={t('submissionDetails')}
icon={<IconFile />}
accent="blue"
>
<InfoRow
label={t('sender')}
icon={<IconUser />}
value={
<span className="flex items-center gap-2 justify-end">
<Avatar name={check.sender.name} size="sm" />
{check.sender.name}
</span>
}
/>
<InfoRow
label={t('fileName')}
icon={<IconFile />}
value={
<span className="flex items-center gap-2 justify-end flex-wrap">
<FileTypeBadge extension={getFileExtension(check.fileName)} />
<span className="font-mono text-xs text-slate-700 break-all max-w-60 text-right">
{check.fileName}
</span>
</span>
}
/>
<InfoRow
label={t('fileSize')}
value={
<span className="text-slate-600">
{formatFileSize(check.fileSize)}
</span>
}
/>
<InfoRow
label={t('submitted')}
icon={<IconCalendar />}
value={formatDate(check.submittedAt)}
/>
<InfoRow
label={t('payment')}
icon={<IconPayment />}
value={
<span className="text-emerald-600 font-bold">
{formatCurrency(check.paymentAmount, check.currency)}
</span>
}
/>
</SectionCard>
);
};
const ResultCard: React.FC<CheckDetailViewProps> = ({ check }) => {
const t = useTranslations('DetailPage');
if (check.status === 'processing' || check.status === 'pending') {
return (
<SectionCard
title={t('resultTitle')}
icon={<IconShield />}
accent="violet"
>
<div className="flex flex-col items-center justify-center py-10 text-center space-y-3">
<div className="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center">
<svg
className="w-6 h-6 text-blue-400 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
</div>
<p className="text-sm font-semibold text-slate-600">
{t('analysisInProgress')}
</p>
<p className="text-xs text-slate-400">
{t('resultsReadyAfterProcessing')}
</p>
</div>
</SectionCard>
);
}
if (!check.result) {
return (
<SectionCard
title={t('resultTitle')}
icon={<IconShield />}
accent="violet"
>
<p className="text-sm text-slate-500 py-4">{t('noResultAvailable')}</p>
</SectionCard>
);
}
const { result } = check;
return (
<SectionCard
title={t('plagiarismResult')}
icon={<IconShield />}
accent={
result.similarityLevel === 'low'
? 'green'
: result.similarityLevel === 'high'
? 'red'
: 'amber'
}
>
<div className="space-y-6">
{/* Meter */}
<SimilarityMeter
percentage={result.overallSimilarity}
level={result.similarityLevel}
/>
{/* Stats grid */}
<div className="grid grid-cols-2 gap-3">
<div className="bg-slate-50 rounded-xl p-4 text-center">
<p className="text-2xl font-bold text-slate-800 tabular-nums">
{result.checkedWords.toLocaleString()}
</p>
<p className="text-xs text-slate-500 mt-1">{t('wordsChecked')}</p>
</div>
<div className="bg-slate-50 rounded-xl p-4 text-center">
<p className="text-2xl font-bold text-slate-800 tabular-nums">
{result.matchedWords.toLocaleString()}
</p>
<p className="text-xs text-slate-500 mt-1">{t('wordsMatched')}</p>
</div>
</div>
{/* Sources */}
{result.sources.length > 0 && (
<div>
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-widest mb-3">
{t('matchedSources')}
</h3>
<div className="space-y-2">
{result.sources.map((src, i) => (
<div
key={i}
className="flex items-center gap-3 p-3 bg-slate-50 rounded-xl hover:bg-slate-100 transition-colors group"
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-700 truncate">
{src.title}
</p>
<a
href={src.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-slate-400 hover:text-blue-500 flex items-center gap-1 mt-0.5 transition-colors"
>
<IconSource />
<span className="truncate">{src.url}</span>
</a>
</div>
<div className="text-right shrink-0">
<span
className={`text-sm font-bold ${src.matchPercentage >= 30 ? 'text-red-600' : src.matchPercentage >= 15 ? 'text-amber-600' : 'text-emerald-600'}`}
>
{src.matchPercentage}%
</span>
<p className="text-xs text-slate-400">
{src.matchedWords.toLocaleString()} {t('words')}
</p>
</div>
</div>
))}
</div>
</div>
)}
<InfoRow
label={t('processedAt')}
icon={<IconCalendar />}
value={formatDate(result.processedAt)}
/>
</div>
</SectionCard>
);
};
const CertificateCard: React.FC<CheckDetailViewProps> = ({ check }) => {
const t = useTranslations('DetailPage');
if (!check.certificate) {
return (
<SectionCard title={t('certificate')} icon={<IconCert />} accent="violet">
<div className=" flex flex-col items-center justify-center py-8 text-center space-y-2">
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center">
<IconCert />
</div>
<p className="text-sm text-slate-500">{t('noCertificate')}</p>
{check.result?.similarityLevel === 'high' && (
<p className="text-xs text-red-500">
{t('noCertificateHighSimilarity')}
</p>
)}
</div>
</SectionCard>
);
}
const { certificate } = check;
return (
<SectionCard title={t('certificate')} icon={<IconCert />} accent="green">
{/* Certificate visual */}
<div className="relative rounded-xl border-2 border-dashed border-emerald-200 bg-linear-to-br from-emerald-50 to-white p-5 mb-4 overflow-hidden">
<div className="absolute top-2 right-2 opacity-10">
<svg
className="w-20 h-20 text-emerald-600"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z" />
</svg>
</div>
<p className="text-xs font-semibold text-emerald-700 uppercase tracking-widest mb-1">
{certificate.issuerName}
</p>
<p className="text-lg font-bold text-slate-800 font-mono tracking-wide">
{certificate.verificationCode}
</p>
<p className="text-xs text-slate-500 mt-1">
{t('certificateId')}: {certificate.id}
</p>
</div>
<InfoRow
label={t('issued')}
icon={<IconCalendar />}
value={formatDate(certificate.issuedAt)}
/>
<InfoRow
label={t('expires')}
icon={<IconCalendar />}
value={formatDate(certificate.expiresAt)}
/>
<InfoRow label={t('issuer')} value={certificate.issuerName} />
<div className="mt-4">
<a
href={certificate.downloadUrl}
className="flex items-center justify-center gap-2 py-2.5 px-4 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-semibold rounded-xl transition-colors"
>
<IconDownload />
{t('downloadCertificate')}
</a>
</div>
</SectionCard>
);
};
// ── Detail View (assembled) ───────────────────────────────────
const CheckDetailView: React.FC<CheckDetailViewProps> = ({ check }) => (
<div className="space-y-4">
<CheckHeader check={check} />
<div className="flex items-start gap-4 w-full">
<CertificateCard check={check} />
<SubmissionInfoCard check={check} />
</div>
<ResultCard check={check} />
</div>
);
// ── Page ──────────────────────────────────────────────────────
interface PlagiarismDetailPageProps {
checkId: string;
onBack?: () => void;
}
export const PlagiarismDetailPage: React.FC<PlagiarismDetailPageProps> = ({
checkId,
}) => {
const t = useTranslations('DetailPage');
const { check, loadingState, error, reload } = usePlagiarismDetail(checkId);
return (
<div className="bg-slate-50 font-sans">
<main className="max-w-300 mx-auto px-4 py-6">
{loadingState === 'loading' && <SkeletonLoader />}
{loadingState === 'error' && (
<ErrorState message={error ?? t('unknownError')} onRetry={reload} />
)}
{loadingState === 'success' && check && (
<CheckDetailView check={check} />
)}
</main>
</div>
);
};

View File

@@ -0,0 +1,30 @@
import { Search } from 'lucide-react';
export default function EmptySourcesList() {
return (
<div
className="flex flex-col items-center justify-center py-10 rounded-lg"
style={{ background: 'var(--color-background-secondary)' }}
>
<div
className="w-10 h-10 rounded-full flex items-center justify-center mb-3"
style={{ background: 'var(--color-background-tertiary)' }}
>
<Search
size={18}
style={{ color: 'var(--color-text-tertiary)' }}
strokeWidth={1.5}
/>
</div>
<p
className="text-sm font-medium mb-1"
style={{ color: 'var(--color-text-secondary)' }}
>
Manbalar topilmadi
</p>
<p className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>
Plagiat manbalari aniqlanmagan
</p>
</div>
);
}

View File

@@ -0,0 +1,294 @@
'use client';
import { useState } from 'react';
import { PlagiatData, HighlightSegment, SemanticMetrics } from '../lib/types';
import TextAnalysis from './components/TextAnalysis';
import ActionButtons from './components/actionbuttons';
import HighlightedText from './components/HighlightedText';
import SourcesList from './components/SourcesList';
import GaugeWithBars from './components/GaugewithBars';
import TopMetrics from './components/TopMetrics';
import HumanAiBar from './components/HumanAiBar';
import Header from './components/Header';
import CertificateCard from './components/SertificateCard';
import SemanticModal from './components/SemanticModal';
import Divider from './components/Divider';
import { blue } from '../lib/constant';
import { useQuery } from '@tanstack/react-query';
import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links';
// ─── Helpers ──────────────────────────────────────────────────────────────────
function parseHighlightedText(textRes: string): HighlightSegment[] {
const segments: HighlightSegment[] = [];
const parts = textRes.split(/(<sel>[\s\S]*?<\/sel>)/g);
for (const part of parts) {
if (part.startsWith('<sel>')) {
segments.push({ text: part.replace(/<\/?sel>/g, ''), plagiarized: true });
} else if (part) {
segments.push({ text: part, plagiarized: false });
}
}
return segments;
}
function parseAnalyzeText(a: Record<string, unknown>): SemanticMetrics {
const bool = (v: unknown) => v === 'Да';
return {
totalWords: Number(a['Общее количество слов'] ?? 0),
uniqueWords: Number(a['Уникальных слов'] ?? 0),
lexicalUniqueness: Number(a['Лексическая уникальность (%)'] ?? 0),
avgWordLength: Number(a['Средняя длина слов'] ?? 0),
geoAvgWordLength: Number(a['Геометрическая средняя длина слов'] ?? 0),
minWordLength: Number(a['Мин. длина слова'] ?? 0),
maxWordLength: Number(a['Макс. длина слова'] ?? 0),
sentences: Number(a['Количество предложений'] ?? 0),
avgWordsPerSentence: Number(
a['Среднее количество слов в предложении'] ?? 0,
),
polysyllabicWords: Number(a['Полисиллабических слов'] ?? 0),
polysyllabicPercent: Number(a['Доля полисиллабических слов (%)'] ?? 0),
totalChars: Number(a['Символов всего'] ?? 0),
charsNoSpaces: Number(a['Без пробелов'] ?? 0),
vowels: Number(a['Гласных'] ?? 0),
consonants: Number(a['Согласных'] ?? 0),
punctuation: Number(a['Знаков препинания'] ?? 0),
uppercase: Number(a['Заглавных букв'] ?? 0),
lowercase: Number(a['Строчных букв'] ?? 0),
digits: Number(a['Цифр'] ?? 0),
capsLockWords: Number(a['Слов в CAPSLOCK'] ?? 0),
stopWords: Number(a['Стоп-слов'] ?? 0),
stopWordsPercent: Number(a['Доля стоп-слов (%)'] ?? 0),
junkWords: Number(a['Мусорные слова (шт)'] ?? 0),
junkPercent: Number(a['Доля мусора (%)'] ?? 0),
maxConsecutiveRepeats: Number(a['Макс. подряд повторов слова'] ?? 0),
top5Words: String(a['ТОП-5 слов'] ?? ''),
spamRatio: Number(a['Заспамленность (max/total)'] ?? 0),
hasHtml: bool(a['Наличие HTML-тегов']),
hasEmail: bool(a['Email найден']),
hasUrl: bool(a['URL найден']),
hasDate: bool(a['Дата найдена']),
hasPhone: bool(a['Телефон найден']),
startsEqualsEnd: bool(a['Начало = Конец']),
isPalindrome: bool(a['Палиндром']),
hasQuestion: bool(a['Вопросительный знак']),
hasExclamation: bool(a['Восклицательный знак']),
paragraphs: Number(a['Количество абзацев'] ?? 0),
lines: Number(a['Количество строк'] ?? 0),
latinPercent: Number(a['Доля латиницы (%)'] ?? 0),
cyrillicPercent: Number(a['Доля кириллицы (%)'] ?? 0),
longWords16plus: Number(a['Слов длиной 16+ символов'] ?? 0),
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function transformResponse(raw: any): PlagiatData {
const result = raw?.results?.[0];
const resultJson = result?.result_json;
const res = resultJson?.res ?? {};
const analyze = resultJson?.analyze_text ?? {};
const textRes = resultJson?.text_res ?? '';
const nameParts = (raw?.text ?? '').trim().split(/\s+/);
const initials = nameParts
.slice(0, 2)
.map((n: string) => n[0]?.toUpperCase() ?? '')
.join('');
return {
name: raw?.text ?? '',
initials: initials ?? '',
email: '',
location: '',
fileName: raw?.file?.split('/').pop() ?? '',
checkedAt: raw?.created_at?.slice(0, 10) ?? '',
aiPercent: res?.ai ?? 0,
humanPercent: res?.ai !== null ? 100 - res.ai : 0,
plagiarismPercent: res?.plagiarism ?? 0,
originalityPercent: res?.originality ?? 0,
citationPercent: res?.citation ?? 0,
highlightedText: textRes ? parseHighlightedText(textRes) : [],
sources: [],
semantic: Object.keys(analyze).length ? parseAnalyzeText(analyze) : null,
certificate: null,
};
}
// ─── Not found state ──────────────────────────────────────────────────────────
function NotFound({ label }: { label: string }) {
return (
<p className="text-[12px] italic" style={{ color: blue[400] }}>
{label} ma&apos;lumot mavjud emas
</p>
);
}
// ─── Loading skeleton ─────────────────────────────────────────────────────────
function Skeleton() {
return (
<div className="min-h-screen flex items-start justify-center p-6 bg-slate-50">
<div
className="w-full max-w-2xl rounded-xl p-5 space-y-4"
style={{ background: '#fff', border: `0.5px solid ${blue[100]}` }}
>
{[100, 60, 80, 40, 90, 55].map((w, i) => (
<div
key={i}
className="rounded-lg animate-pulse"
style={{ height: 18, width: `${w}%`, background: blue[50] }}
/>
))}
</div>
</div>
);
}
// ─── Error state ──────────────────────────────────────────────────────────────
function ErrorState() {
return (
<div className="min-h-screen flex items-center justify-center p-6 bg-slate-50">
<div
className="rounded-xl p-8 text-center"
style={{ background: '#fff', border: `0.5px solid ${blue[100]}` }}
>
<p className="text-sm font-medium mb-1" style={{ color: blue[900] }}>
Ma&apos;lumot topilmadi
</p>
<p className="text-xs" style={{ color: blue[400] }}>
Ushbu tekshiruv mavjud emas yoki o&apos;chirilgan
</p>
</div>
</div>
);
}
// ─── Component ────────────────────────────────────────────────────────────────
export default function PlagiatResult({ id }: { id: number }) {
const [showSemantic, setShowSemantic] = useState(false);
const [saving, setSaving] = useState(false);
const {
data: rawData,
isLoading,
isError,
} = useQuery({
queryKey: ['detail', id],
queryFn: async () => {
const res = await apiRequest('GET', links.detail(id));
return res;
},
enabled: !!id,
staleTime: 1000 * 60 * 5,
});
if (isLoading) return <Skeleton />;
if (isError || !rawData) return <ErrorState />;
const data: PlagiatData = transformResponse(rawData);
const handleSave = () => {
setSaving(true);
setTimeout(() => setSaving(false), 1500);
};
const handleRecheck = () => {
alert('Yangi fayl yuklash sahifasiga yo&apos;naltirish...');
};
return (
<>
{showSemantic && data.semantic && (
<SemanticModal
data={data.semantic}
onClose={() => setShowSemantic(false)}
/>
)}
<div className="min-h-screen flex items-start justify-center p-6 bg-slate-50">
<div
className="w-full max-w-2xl rounded-xl p-5"
style={{ background: '#fff', border: `0.5px solid ${blue[100]}` }}
>
{/* Certificate */}
{data.certificate ? (
<CertificateCard certificate={data.certificate} />
) : null}
{/* Header */}
{data.name ? (
<Header
initials={data.initials}
name={data.name}
plagiarismPercent={data.plagiarismPercent}
fileName={data.fileName}
email={data.email}
location={data.location}
checkedAt={data.checkedAt}
/>
) : (
<NotFound label="Foydalanuvchi ma'lumotlari" />
)}
{/* Human / AI bar */}
<HumanAiBar
humanPercent={data.humanPercent}
aiPercent={data.aiPercent}
/>
<Divider />
{/* Score cards */}
<TopMetrics
plagiarismPercent={data.plagiarismPercent}
aiPercent={data.aiPercent}
originalityPercent={data.originalityPercent}
citationPercent={data.citationPercent}
/>
{/* Gauge */}
<GaugeWithBars
plagiarismPercent={data.plagiarismPercent}
aiPercent={data.aiPercent}
originalityPercent={data.originalityPercent}
citationPercent={data.citationPercent}
/>
<Divider />
{/* Sources */}
<SourcesList sources={data.sources} />
<Divider />
{/* Highlighted text */}
{data.highlightedText.length > 0 ? (
<HighlightedText segments={data.highlightedText} />
) : (
<NotFound label="Matn tahlili" />
)}
{/* Buttons */}
<ActionButtons
saving={saving}
onSave={handleSave}
onSemantic={() => setShowSemantic(true)}
onRecheck={handleRecheck}
/>
<Divider />
{/* Semantic analysis */}
{data.semantic ? (
<TextAnalysis semantic={data.semantic} />
) : (
<NotFound label="Semantik tahlil" />
)}
</div>
</div>
</>
);
}