Files
plagiat/src/widgets/detail/ui/index.tsx
nabijonovdavronbek619@gmail.com dfb8d3bdbc last push
2026-04-08 20:15:28 +05:00

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&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>
);
}
// ─── 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&apos;lumot topilmadi
</p>
<p className="text-xs" style={{ color: blue[400] }}>
Ushbu tekshiruv mavjud emas yoki o&apos;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&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>
</>
);
}