complate detail page
This commit is contained in:
294
src/widgets/detail/ui/index.tsx
Normal file
294
src/widgets/detail/ui/index.tsx
Normal 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'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'lumot topilmadi
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: blue[400] }}>
|
||||
Ushbu tekshiruv mavjud emas yoki o'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'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user