290 lines
11 KiB
TypeScript
290 lines
11 KiB
TypeScript
'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'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>
|
|
);
|
|
}
|
|
|
|
// ─── 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 (
|
|
<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'lumot topilmadi
|
|
</p>
|
|
<p className="text-xs" style={{ color: blue[400] }}>
|
|
Ushbu tekshiruv mavjud emas yoki o'chirilgan
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const data: PlagiatData = transformResponse(rawData);
|
|
|
|
const handleSave = () => {
|
|
setSaving(true);
|
|
setTimeout(() => setSaving(false), 1500);
|
|
};
|
|
|
|
const handleRecheck = () => {
|
|
alert('Yangi fayl yuklash sahifasiga yo'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>
|
|
</>
|
|
);
|
|
}
|