From 238c2c16535ef7391a415de134efd1e05f2a2531 Mon Sep 17 00:00:00 2001 From: "nabijonovdavronbek619@gmail.com" Date: Thu, 2 Apr 2026 19:19:06 +0500 Subject: [PATCH] complate detail page --- src/app/[locale]/[detail]/page.tsx | 4 +- src/shared/request/links.ts | 1 + src/widgets/detail/lib/constant.ts | 10 + src/widgets/detail/lib/formatter.ts | 260 ++++++ src/widgets/detail/lib/mock.ts | 130 +++ src/widgets/detail/lib/types.ts | 84 ++ src/widgets/detail/pageDetail.tsx | 662 +++++++++++++++ src/widgets/detail/ui/PlagiatResult.tsx | 460 ----------- src/widgets/detail/ui/PlagiatResult2.tsx | 782 ++++++++++++++++++ src/widgets/detail/ui/components/BarRow.tsx | 42 + .../detail/ui/components/CircleGauge.tsx | 50 ++ src/widgets/detail/ui/components/Divider.tsx | 13 + .../detail/ui/components/GaugewithBars.tsx | 44 + src/widgets/detail/ui/components/Header.tsx | 61 ++ .../detail/ui/components/HighlightedText.tsx | 49 ++ .../detail/ui/components/HumanAiBar.tsx | 34 + .../detail/ui/components/Metriccard.tsx | 19 + .../detail/ui/components/SectionTitle.tsx | 16 + .../detail/ui/components/SemanticModal.tsx | 133 +++ .../detail/ui/components/SertificateCard.tsx | 51 ++ .../detail/ui/components/SourcesList.tsx | 65 ++ .../detail/ui/components/TextAnalysis.tsx | 38 + .../detail/ui/components/TopMetrics.tsx | 23 + .../detail/ui/components/actionbuttons.tsx | 49 ++ src/widgets/detail/ui/detailPage.tsx | 510 ------------ src/widgets/detail/ui/emptyList.tsx | 30 + src/widgets/detail/ui/index.tsx | 294 +++++++ src/widgets/fileUpload/lib/usePlagiraism.ts | 8 +- 28 files changed, 2949 insertions(+), 973 deletions(-) create mode 100644 src/widgets/detail/lib/formatter.ts create mode 100644 src/widgets/detail/lib/mock.ts create mode 100644 src/widgets/detail/pageDetail.tsx delete mode 100644 src/widgets/detail/ui/PlagiatResult.tsx create mode 100644 src/widgets/detail/ui/PlagiatResult2.tsx create mode 100644 src/widgets/detail/ui/components/BarRow.tsx create mode 100644 src/widgets/detail/ui/components/CircleGauge.tsx create mode 100644 src/widgets/detail/ui/components/Divider.tsx create mode 100644 src/widgets/detail/ui/components/GaugewithBars.tsx create mode 100644 src/widgets/detail/ui/components/Header.tsx create mode 100644 src/widgets/detail/ui/components/HighlightedText.tsx create mode 100644 src/widgets/detail/ui/components/HumanAiBar.tsx create mode 100644 src/widgets/detail/ui/components/Metriccard.tsx create mode 100644 src/widgets/detail/ui/components/SectionTitle.tsx create mode 100644 src/widgets/detail/ui/components/SemanticModal.tsx create mode 100644 src/widgets/detail/ui/components/SertificateCard.tsx create mode 100644 src/widgets/detail/ui/components/SourcesList.tsx create mode 100644 src/widgets/detail/ui/components/TextAnalysis.tsx create mode 100644 src/widgets/detail/ui/components/TopMetrics.tsx create mode 100644 src/widgets/detail/ui/components/actionbuttons.tsx delete mode 100644 src/widgets/detail/ui/detailPage.tsx create mode 100644 src/widgets/detail/ui/emptyList.tsx create mode 100644 src/widgets/detail/ui/index.tsx diff --git a/src/app/[locale]/[detail]/page.tsx b/src/app/[locale]/[detail]/page.tsx index 9aa3d63..1e6ad49 100644 --- a/src/app/[locale]/[detail]/page.tsx +++ b/src/app/[locale]/[detail]/page.tsx @@ -1,4 +1,4 @@ -import PlagiatResult from '@/widgets/detail/ui/PlagiatResult'; +import DocumentDetailPage from '@/widgets/detail/pageDetail'; interface Props { params: Promise<{ detail: string }>; @@ -7,5 +7,5 @@ interface Props { export default async function DetailPage({ params }: Props) { const { detail } = await params; console.log(detail); - return ; + return ; } diff --git a/src/shared/request/links.ts b/src/shared/request/links.ts index 4a6839c..3b901a1 100644 --- a/src/shared/request/links.ts +++ b/src/shared/request/links.ts @@ -3,4 +3,5 @@ export const links = { register: '/users/register/', plagiarismCheck: '/shared/documents/', history: '/shared/documents/list/', + detail: (id: number) => `/shared/documents/${id}/`, }; diff --git a/src/widgets/detail/lib/constant.ts b/src/widgets/detail/lib/constant.ts index d55ea99..1b1ee46 100644 --- a/src/widgets/detail/lib/constant.ts +++ b/src/widgets/detail/lib/constant.ts @@ -62,3 +62,13 @@ export const MOCK_CHECKS: Record = { }, }, }; + +export const blue = { + 50: '#E6F1FB', + 100: '#B5D4F4', + 200: '#85B7EB', + 400: '#378ADD', + 600: '#185FA5', + 800: '#0C447C', + 900: '#042C53', +} as const; diff --git a/src/widgets/detail/lib/formatter.ts b/src/widgets/detail/lib/formatter.ts new file mode 100644 index 0000000..44e8f6a --- /dev/null +++ b/src/widgets/detail/lib/formatter.ts @@ -0,0 +1,260 @@ +// utils/transformPlagiatResponse.ts + +interface HighlightSegment { + text: string; + plagiarized: boolean; +} + +interface SemanticMetrics { + totalWords: number; + uniqueWords: number; + lexicalUniqueness: number; + avgWordLength: number; + geoAvgWordLength: number; + minWordLength: number; + maxWordLength: number; + sentences: number; + avgWordsPerSentence: number; + polysyllabicWords: number; + polysyllabicPercent: number; + totalChars: number; + charsNoSpaces: number; + vowels: number; + consonants: number; + punctuation: number; + uppercase: number; + lowercase: number; + digits: number; + capsLockWords: number; + stopWords: number; + stopWordsPercent: number; + junkWords: number; + junkPercent: number; + maxConsecutiveRepeats: number; + top5Words: string; + spamRatio: number; + hasHtml: boolean; + hasEmail: boolean; + hasUrl: boolean; + hasDate: boolean; + hasPhone: boolean; + startsEqualsEnd: boolean; + isPalindrome: boolean; + hasQuestion: boolean; + hasExclamation: boolean; + paragraphs: number; + lines: number; + latinPercent: number; + cyrillicPercent: number; + longWords16plus: number; +} + +interface Certificate { + verificationCode: string; + issuerName: string; + issuedAt: string; + expiresAt: string; + downloadUrl: string; +} + +interface Source { + url: string; + matchPercentage: number; + module: string; +} + +interface PlagiatData { + name: string; + initials: string; + email: string; + location: string; + fileName: string; + checkedAt: string; + humanPercent: number; + aiPercent: number; + plagiarismPercent: number; + originalityPercent: number; + citationPercent: number; + highlightedText: HighlightSegment[]; + sources: Source[]; + semantic: SemanticMetrics; + certificate: Certificate | null; +} + +interface ApiRes { + ai: number; + hash: string; + text: string; + citation: number; + plagiarism: number; + originality: number; +} + +interface ApiAnalyzeText { + Цифр: number; + Гласных: number; + 'URL найден': string; + 'Стоп-слов': number; + 'ТОП-5 слов': string; + 'Email найден': string; + Палиндром: string; + Согласных: number; + 'Слов в CAPSLOCK': number; + 'Без пробелов': number; + 'Дата найдена': string; + 'Доля мусора (%)': number; + 'Начало = Конец': string; + 'Строчных букв': number; + 'Заглавных букв': number; + 'Символов всего': number; + 'Телефон найден': string; + 'Доля латиницы (%)': number; + 'Мин. длина слова': number; + 'Уникальных слов': number; + 'Доля стоп-слов (%)': number; + 'Наличие HTML-тегов': string; + 'Доля кириллицы (%)': number; + 'Количество строк': number; + 'Макс. длина слова': number; + 'Знаков препинания': number; + 'Мусорные слова (шт)': number; + 'Средняя длина слов': number; + 'Количество абзацев': number; + 'Вопросительный знак': string; + 'Восклицательный знак': string; + 'Заспамленность (max/total)': number; + 'Общее количество слов': number; + 'Слов длиной 16+ символов': number; + 'Количество предложений': number; + 'Полисиллабических слов': number; + 'Макс. подряд повторов слова': number; + 'Лексическая уникальность (%)': number; + 'Доля полисиллабических слов (%)': number; + 'Геометрическая средняя длина слов': number; + 'Среднее количество слов в предложении': number; +} + +interface ApiResultJson { + ok: boolean; + res: ApiRes; + error: string; + success: string; + text_res: string; + analyze_text: ApiAnalyzeText; +} + +interface ApiResult { + id: number; + document: number; + result_json: ApiResultJson; + created_at: string; + updated_at: string; +} + +interface ApiDocument { + id: number; + title: string; + file: string; + certificate: boolean; + text: string; + created_at: string; + updated_at: string; + results: ApiResult[]; +} + +// ─── Parsers ────────────────────────────────────────────────────────────────── + +function parseHighlightedText(textRes: string): HighlightSegment[] { + const segments: HighlightSegment[] = []; + const parts = textRes.split(/([\s\S]*?<\/sel>)/g); + + for (const part of parts) { + if (part.startsWith('')) { + segments.push({ text: part.replace(/<\/?sel>/g, ''), plagiarized: true }); + } else if (part) { + segments.push({ text: part, plagiarized: false }); + } + } + + return segments; +} + +function parseAnalyzeText(a: ApiAnalyzeText): SemanticMetrics { + const bool = (v: string): boolean => v === 'Да'; + + return { + totalWords: a['Общее количество слов'], + uniqueWords: a['Уникальных слов'], + lexicalUniqueness: a['Лексическая уникальность (%)'], + avgWordLength: a['Средняя длина слов'], + geoAvgWordLength: a['Геометрическая средняя длина слов'], + minWordLength: a['Мин. длина слова'], + maxWordLength: a['Макс. длина слова'], + sentences: a['Количество предложений'], + avgWordsPerSentence: a['Среднее количество слов в предложении'], + polysyllabicWords: a['Полисиллабических слов'], + polysyllabicPercent: a['Доля полисиллабических слов (%)'], + totalChars: a['Символов всего'], + charsNoSpaces: a['Без пробелов'], + vowels: a['Гласных'], + consonants: a['Согласных'], + punctuation: a['Знаков препинания'], + uppercase: a['Заглавных букв'], + lowercase: a['Строчных букв'], + digits: a['Цифр'], + capsLockWords: a['Слов в CAPSLOCK'], + stopWords: a['Стоп-слов'], + stopWordsPercent: a['Доля стоп-слов (%)'], + junkWords: a['Мусорные слова (шт)'], + junkPercent: a['Доля мусора (%)'], + maxConsecutiveRepeats: a['Макс. подряд повторов слова'], + top5Words: a['ТОП-5 слов'], + spamRatio: a['Заспамленность (max/total)'], + 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: a['Количество абзацев'], + lines: a['Количество строк'], + latinPercent: a['Доля латиницы (%)'], + cyrillicPercent: a['Доля кириллицы (%)'], + longWords16plus: a['Слов длиной 16+ символов'], + }; +} + +// ─── Main transformer ───────────────────────────────────────────────────────── + +export function transformPlagiatResponse(apiDoc: ApiDocument): PlagiatData { + const result = apiDoc.results[0]; + const res = result.result_json.res; + const analyze = result.result_json.analyze_text; + + const nameParts = apiDoc.text.trim().split(/\s+/); + const initials = nameParts + .slice(0, 2) + .map((n) => n[0]?.toUpperCase() ?? '') + .join(''); + + return { + name: apiDoc.text, + initials, + email: '', + location: '', + fileName: apiDoc.file.split('/').pop() ?? apiDoc.file, + checkedAt: apiDoc.created_at.slice(0, 10), + humanPercent: 100 - res.ai, + aiPercent: res.ai, + plagiarismPercent: res.plagiarism, + originalityPercent: res.originality, + citationPercent: res.citation, + highlightedText: parseHighlightedText(result.result_json.text_res), + sources: [], + semantic: parseAnalyzeText(analyze), + certificate: null, + }; +} diff --git a/src/widgets/detail/lib/mock.ts b/src/widgets/detail/lib/mock.ts new file mode 100644 index 0000000..10eb57f --- /dev/null +++ b/src/widgets/detail/lib/mock.ts @@ -0,0 +1,130 @@ +import { PlagiatData } from '../lib/types'; + +export 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', + }, +}; diff --git a/src/widgets/detail/lib/types.ts b/src/widgets/detail/lib/types.ts index 5491bc3..7340eff 100644 --- a/src/widgets/detail/lib/types.ts +++ b/src/widgets/detail/lib/types.ts @@ -51,3 +51,87 @@ export interface PlagiarismCheck { result?: PlagiarismResult; certificate?: Certificate; } + +// ─── Detail page Types ──────────────────────────────────────────────────────────────────── + +export interface Source { + url: string; + matchPercentage: number; + module: string; +} + +export interface SemanticMetrics { + totalWords: number; + uniqueWords: number; + lexicalUniqueness: number; + avgWordLength: number; + geoAvgWordLength: number; + minWordLength: number; + maxWordLength: number; + sentences: number; + avgWordsPerSentence: number; + polysyllabicWords: number; + polysyllabicPercent: number; + totalChars: number; + charsNoSpaces: number; + vowels: number; + consonants: number; + punctuation: number; + uppercase: number; + lowercase: number; + digits: number; + capsLockWords: number; + stopWords: number; + stopWordsPercent: number; + junkWords: number; + junkPercent: number; + maxConsecutiveRepeats: number; + top5Words: string; + spamRatio: number; + hasHtml: boolean; + hasEmail: boolean; + hasUrl: boolean; + hasDate: boolean; + hasPhone: boolean; + startsEqualsEnd: boolean; + isPalindrome: boolean; + hasQuestion: boolean; + hasExclamation: boolean; + paragraphs: number; + lines: number; + latinPercent: number; + cyrillicPercent: number; + longWords16plus: number; +} + +export interface CertificateDetail { + id: string; + verificationCode: string; + issuerName: string; + issuedAt: string; + expiresAt: string; + downloadUrl: string; +} + +export interface PlagiatData { + name: string; + initials: string; + email: string; + location: string; + fileName: string; + checkedAt: string; + humanPercent: number; + aiPercent: number; + plagiarismPercent: number; + originalityPercent: number; + citationPercent: number; + highlightedText: HighlightSegment[]; + sources: Source[]; + semantic: SemanticMetrics | null; + certificate: CertificateDetail | null; +} + +export interface HighlightSegment { + text: string; + plagiarized: boolean; +} diff --git a/src/widgets/detail/pageDetail.tsx b/src/widgets/detail/pageDetail.tsx new file mode 100644 index 0000000..4cf236c --- /dev/null +++ b/src/widgets/detail/pageDetail.tsx @@ -0,0 +1,662 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { links } from '@/shared/request/links'; +import { apiRequest } from '@/shared/request/apiRequest'; + +// ── Types ──────────────────────────────────────────────────────────────────── + +interface AnalyzeText { + [key: string]: number | string; +} + +interface ResultJson { + ok: boolean; + res: { + ai: number; + hash: string; + text: string; + citation: number; + plagiarism: number; + originality: number; + }; + error: string; + success: string; + text_res: string; + analyze_text: AnalyzeText; +} + +interface Result { + id: number; + document: number; + result_json: ResultJson; + created_at: string; + updated_at: string; +} + +interface Document { + id: number; + title: string; + file: string; + certificate: boolean; + text: string; + created_at: string; + updated_at: string; + results: Result[]; +} + +// ── Props ───────────────────────────────────────────────────────────────────── + +// ── Helpers ────────────────────────────────────────────────────────────────── + +function parseHighlightedText(text_res: string): React.ReactNode[] { + const parts = text_res.split(/(.*?<\/sel>)/gs); + return parts.map((part, i) => { + if (part.startsWith('') && part.endsWith('')) { + const inner = part.slice(5, -6); + return ( + + {inner} + + ); + } + return {part}; + }); +} + +function formatDate(iso: string): string { + return new Date(iso).toLocaleString('uz-UZ', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); +} + +/** A flag key is one whose value is purely a yes/no boolean string */ +function isBooleanString(val: string | number): boolean { + if (typeof val !== 'string') return false; + const lower = val.toLowerCase(); + return ( + lower === 'нет' || + lower === 'да' || + lower === 'yes' || + lower === 'no' || + lower === 'true' || + lower === 'false' + ); +} + +function isFlagPositive(val: string | number): boolean { + if (typeof val === 'number') return val !== 0; + const lower = val.toLowerCase(); + return lower !== 'нет' && lower !== 'no' && lower !== 'false' && lower !== ''; +} + +// ── Score Ring ─────────────────────────────────────────────────────────────── + +function ScoreRing({ + value, + label, + color, +}: { + value: number; + label: string; + color: string; +}) { + const r = 38; + const circ = 2 * Math.PI * r; + const dash = (Math.min(Math.max(value, 0), 100) / 100) * circ; + + return ( +
+
+ + + + +
+ {value}% +
+
+ + {label} + +
+ ); +} + +// ── Stat Card ──────────────────────────────────────────────────────────────── + +function StatCard({ label, value }: { label: string; value: string | number }) { + return ( +
+ + {label} + + + {String(value)} + +
+ ); +} + +// ── Section Shell ───────────────────────────────────────────────────────────── + +function Section({ + title, + children, + accent, +}: { + title: string; + children: React.ReactNode; + accent?: string; +}) { + return ( +
+
+ {accent && } +

+ {title} +

+
+ {children} +
+ ); +} + +// ── Skeleton ────────────────────────────────────────────────────────────────── + +function Skeleton({ + className, + style, +}: { + className?: string; + style?: React.CSSProperties; +}) { + return ( +
+ ); +} + +function LoadingSkeleton() { + return ( +
+
+ + + +
+
+
+ {[...Array(4)].map((_, i) => ( +
+ + +
+ ))} +
+ +
+
+ {[...Array(6)].map((_, i) => ( + + ))} +
+
+ {[...Array(12)].map((_, i) => ( + + ))} +
+
+ ); +} + +// ── Error State ─────────────────────────────────────────────────────────────── + +function ErrorState({ message }: { message?: string }) { + return ( +
+
+ + + +
+

Failed to load document

+

+ {message ?? 'An unexpected error occurred.'} +

+
+ ); +} + +// ── Main Page ───────────────────────────────────────────────────────────────── + +export default function DocumentDetailPage({ id }: { id: number }) { + const { + data: doc, + isLoading, + isError, + error, + } = useQuery({ + queryKey: ['detail', id], + queryFn: () => apiRequest('GET', links.detail(id)), + enabled: !!id, + staleTime: 1000 * 60 * 5, + select: (data) => data?.data as Document, + }); + + const [activeTab, setActiveTab] = useState<'highlighted' | 'plain'>( + 'highlighted', + ); + + // ── Loading ── + if (isLoading) { + return ( +
+
+ +
+ ); + } + + // ── Error ── + if (isError || !doc) { + return ( +
+
+ +
+ ); + } + + // ── Derived data ── + const result = doc.results?.[0]; + const res = result?.result_json?.res; + const analyze: AnalyzeText = result?.result_json?.analyze_text ?? {}; + const textRes = result?.result_json?.text_res ?? ''; + + const aiRisk = !res + ? 'low' + : res.ai >= 70 + ? 'high' + : res.ai >= 40 + ? 'medium' + : 'low'; + const aiColors: Record = { + high: 'text-red-600 bg-red-50 border-red-200', + medium: 'text-amber-600 bg-amber-50 border-amber-200', + low: 'text-emerald-600 bg-emerald-50 border-emerald-200', + }; + const aiLabel: Record = { + high: 'AI Content Detected', + medium: 'Possible AI Content', + low: 'Likely Original', + }; + + // Partition analyze_text dynamically: + // 1. Flag keys → Yes/No boolean strings → Detection Flags section + // 2. Top-words key → contains "топ" or "top" → Top Words section + // 3. Everything else → stat grid + const allKeys = Object.keys(analyze); + const topWordsKey = allKeys.find( + (k) => k.toLowerCase().includes('топ') || k.toLowerCase().includes('top'), + ); + const flagKeys = allKeys.filter( + (k) => k !== topWordsKey && isBooleanString(analyze[k] as string | number), + ); + const statKeys = allKeys.filter( + (k) => k !== topWordsKey && !flagKeys.includes(k), + ); + + const topWordsValue = topWordsKey ? String(analyze[topWordsKey]) : null; + + return ( +
+ {/* ── Header ── */} +
+
+
+ +
+

+ Document #{doc.id} +

+

+ {doc.title} +

+
+
+ +
+ {doc.certificate && ( + + + + + Certificate + + )} + {doc.file && ( + + + + + Download PDF + + )} +
+
+
+ +
+ {/* ── Meta row ── */} +
+ + + + + Created: {formatDate(doc.created_at)} + + + + + + Updated: {formatDate(doc.updated_at)} + + {res?.hash && ( + + + + + Hash: {res.hash} + + )} +
+ + {/* ── Score Overview ── */} + {res && ( +
+
+
+ + + + +
+ +
+ + + +
+

{aiLabel[aiRisk]}

+

+ AI probability score: {res.ai}% — content + may have been generated or assisted by an AI model. +

+
+
+
+
+ )} + + {/* ── Document Text ── */} + {(res?.text || textRes) && ( +
+
+
+ {(['highlighted', 'plain'] as const).map((tab) => ( + + ))} +
+ +
+
+                  {activeTab === 'highlighted'
+                    ? parseHighlightedText(textRes)
+                    : (res?.text ?? textRes)}
+                
+
+ + {activeTab === 'highlighted' && ( +
+ + Highlighted fragments indicate potential plagiarism matches +
+ )} +
+
+ )} + + {/* ── Text Statistics (all numeric / non-boolean keys) ── */} + {statKeys.length > 0 && ( +
+
+ {statKeys.map((key) => ( + + ))} +
+
+ )} + + {/* ── Detection Flags (all boolean Yes/No keys) ── */} + {flagKeys.length > 0 && ( +
+
+ {flagKeys.map((key) => { + const val = analyze[key]; + const positive = isFlagPositive(val as string | number); + return ( +
+
+
+

+ {key} +

+

{String(val)}

+
+
+ ); + })} +
+
+ )} + + {/* ── Top Words (key detected by "топ"/"top" substring) ── */} + {topWordsValue && ( +
+
+
+ {topWordsValue + .split(',') + .map((w) => w.trim()) + .filter(Boolean) + .map((word, i) => { + const match = word.match(/^(.*)\((\d+)\)$/); + const term = match ? match[1].trim() : word; + const cnt = match ? parseInt(match[2]) : 1; + return ( + + {term} + + {cnt} + + + ); + })} +
+
+
+ )} + + {/* ── Footer ── */} + {result && ( +
+ Result ID #{result.id} · Analyzed {formatDate(result.created_at)} +
+ )} +
+
+ ); +} diff --git a/src/widgets/detail/ui/PlagiatResult.tsx b/src/widgets/detail/ui/PlagiatResult.tsx deleted file mode 100644 index cdcc58e..0000000 --- a/src/widgets/detail/ui/PlagiatResult.tsx +++ /dev/null @@ -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 ( - - - - - {value}% - - - plagiat - - - ); -} - -function BarRow({ - label, - value, - color, -}: { - label: string; - value: number; - color: string; -}) { - return ( -
-
- - - {label} - - - {value}% - -
-
-
-
-
- ); -} - -function MetricCard({ label, value }: { label: string; value: string }) { - return ( -
-

- {label} -

-

- {value} -

-
- ); -} - -function SourceItem({ source, index }: { source: Source; index: number }) { - const colors = [blue[900], blue[600], blue[400]]; - const color = colors[index] ?? blue[400]; - - return ( -
-
- - {source.title} - - - {source.matchPercentage}% - -
-

- {source.url} -

-
-
-
-
- ); -} - -// ─── 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 = ( -
- ); - - return ( -
-
- {/* ── Header ── */} -
-
- {data.initials} -
-
-
- - {data.name} - - - {result.plagiarism}% plagiat - -
-

- {data.fileName} · {data.email} · {data.location} -

-
-
-

- Tekshirilgan -

-

- {data.checkedAt} -

-
-
- - {divider} - - {/* ── Top metrics ── */} -
- - - - -
- - {/* ── Gauge + bars ── */} -
- -
- - - - -
-
- - {divider} - - {/* ── Text analysis ── */} -

- Matn tahlili -

-
- - - - - - -
- - {divider} - - {/* ── Sources ── */} -

- Manba yo‘nalishlari -

-
- {result.sources.map((source, i) => ( - - ))} -
- - {divider} - - {/* ── Certificate ── */} -

- Sertifikat -

-
-
- -
-
-

- {certificate.issuerName} sertifikati -

-

- {certificate.verificationCode} · Amal qilish:{' '} - {certificate.expiresAt} -

-
- -
-
-
- ); -} diff --git a/src/widgets/detail/ui/PlagiatResult2.tsx b/src/widgets/detail/ui/PlagiatResult2.tsx new file mode 100644 index 0000000..3380563 --- /dev/null +++ b/src/widgets/detail/ui/PlagiatResult2.tsx @@ -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 ( +//
+// ); +// } + +// function SectionTitle({ children }: { children: React.ReactNode }) { +// return ( +//

+// {children} +//

+// ); +// } + +// function MetricCard({ label, value }: { label: string; value: string }) { +// return ( +//
+//

+// {label} +//

+//

+// {value} +//

+//
+// ); +// } + +// function BarRow({ +// label, +// value, +// color, +// }: { +// label: string; +// value: number; +// color: string; +// }) { +// return ( +//
+//
+// +// +// {label} +// +// +// {value}% +// +//
+//
+//
+//
+//
+// ); +// } + +// function CircleGauge({ value }: { value: number }) { +// const r = 48; +// const circ = 2 * Math.PI * r; +// const offset = circ - (value / 100) * circ; +// return ( +// +// +// +// +// {value}% +// +// +// plagiat +// +// +// ); +// } + +// // ─── 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 ( +//
+//
e.stopPropagation()} +// > +// {/* Modal header */} +//
+// +// Semantik tahlil +// +// +//
+ +// {/* Modal body */} +//
+// +// +// +// +// +// +// +// +// {rows.map(([label, value], i) => ( +// +// +// +// +// ))} +// +//
+// Metrika +// +// Qiymat +//
+// {label} +// +// {value} +//
+//
+//
+//
+// ); +// } + +// // ─── 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 && ( +// setShowSemantic(false)} +// /> +// )} + +//
+//
+// {/* ── Certificate (top) ── */} +// {data.certificate && ( +//
+//
+// +//
+//
+//

+// {data.certificate.issuerName} sertifikati +//

+//

+// {data.certificate.verificationCode} +// {data.certificate.expiresAt} +//

+//
+// +// Yuklab olish ↗ +// +//
+// )} + +// {/* ── Header ── */} +//
+//
+// {data.initials} +//
+//
+//
+// +// {data.name} +// +// +// {data.plagiarismPercent}% plagiat +// +//
+//

+// {data.fileName} · {data.email} · {data.location} +//

+//
+//
+//

+// Tekshirilgan +//

+//

+// {data.checkedAt} +//

+//
+//
+ +// {/* ── Human vs AI bar ── */} +//
+//
+// Inson: {data.humanPercent}% +// II: {data.aiPercent}% +//
+//
+//
+//
+//
+//
+ +// + +// {/* ── Top metrics ── */} +//
+// +// +// +// +//
+ +// {/* ── Gauge + bars ── */} +//
+// +//
+// +// +// +// +//
+//
+ +// + +// {/* ── Sources list ── */} +// Manbalar ro'yxati +// {data.sources.length > 0 ? ( +//
+// +// +// +// +// +// +// +// +// {data.sources.map((s, i) => ( +// +// +// +// +// ))} +// +//
+// URL +// +// Modul +//
+//
+//
+// +// {s.url} +// +//
+//
+// {s.module} +//
+//
+// ) : ( +// +// )} + +// + +// {/* ── Highlighted text ── */} +// Matn ko'rish +//
+// {data.highlightedText.map((seg, i) => +// seg.plagiarized ? ( +// +// {seg.text} +// +// ) : ( +// {seg.text} +// ), +// )} +//

+// +// Qizil bilan belgilangan qismlar plagiat deb topilgan. +//

+//
+ +// {/* ── 3 Buttons ── */} +//
+// +// +// +//
+ +// + +// {/* ── Text analysis mini metrics ── */} +// Matn tahlili +// {data.semantic ? ( +//
+// +// +// +// +// +// +//
+// ) : ( +// +// )} +//
+//
+// +// ); +// } diff --git a/src/widgets/detail/ui/components/BarRow.tsx b/src/widgets/detail/ui/components/BarRow.tsx new file mode 100644 index 0000000..b8cebe4 --- /dev/null +++ b/src/widgets/detail/ui/components/BarRow.tsx @@ -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 ( +
+
+ + + {label} + + + {value}% + +
+
+
+
+
+ ); +} diff --git a/src/widgets/detail/ui/components/CircleGauge.tsx b/src/widgets/detail/ui/components/CircleGauge.tsx new file mode 100644 index 0000000..6484e4a --- /dev/null +++ b/src/widgets/detail/ui/components/CircleGauge.tsx @@ -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 ( + + + + + {value}% + + + plagiat + + + ); +} diff --git a/src/widgets/detail/ui/components/Divider.tsx b/src/widgets/detail/ui/components/Divider.tsx new file mode 100644 index 0000000..e2e1235 --- /dev/null +++ b/src/widgets/detail/ui/components/Divider.tsx @@ -0,0 +1,13 @@ +import { blue } from '../../lib/constant'; + +export default function Divider() { + return ( +
+ ); +} diff --git a/src/widgets/detail/ui/components/GaugewithBars.tsx b/src/widgets/detail/ui/components/GaugewithBars.tsx new file mode 100644 index 0000000..1396ace --- /dev/null +++ b/src/widgets/detail/ui/components/GaugewithBars.tsx @@ -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 ( +
+ +
+ + + + +
+
+ ); +} diff --git a/src/widgets/detail/ui/components/Header.tsx b/src/widgets/detail/ui/components/Header.tsx new file mode 100644 index 0000000..98470b2 --- /dev/null +++ b/src/widgets/detail/ui/components/Header.tsx @@ -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 ( +
+
+ {initials} +
+
+
+ + {name} + + + {plagiarismPercent}% plagiat + +
+

+ {fileName} · {email} · {location} +

+
+
+

+ Tekshirilgan +

+

+ {checkedAt} +

+
+
+ ); +} diff --git a/src/widgets/detail/ui/components/HighlightedText.tsx b/src/widgets/detail/ui/components/HighlightedText.tsx new file mode 100644 index 0000000..8ada30a --- /dev/null +++ b/src/widgets/detail/ui/components/HighlightedText.tsx @@ -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 ( + <> + Matn ko'rish +
+ {segments.map((seg, i) => + seg.plagiarized ? ( + + {seg.text} + + ) : ( + {seg.text} + ), + )} +

+ + Qizil bilan belgilangan qismlar plagiat deb topilgan. +

+
+ + ); +} diff --git a/src/widgets/detail/ui/components/HumanAiBar.tsx b/src/widgets/detail/ui/components/HumanAiBar.tsx new file mode 100644 index 0000000..873b387 --- /dev/null +++ b/src/widgets/detail/ui/components/HumanAiBar.tsx @@ -0,0 +1,34 @@ +import { blue } from '../../lib/constant'; + +interface Props { + humanPercent: number; + aiPercent: number; +} + +export default function HumanAiBar({ humanPercent, aiPercent }: Props) { + return ( +
+
+ Inson: {humanPercent}% + II: {aiPercent}% +
+
+
+
+
+
+ ); +} diff --git a/src/widgets/detail/ui/components/Metriccard.tsx b/src/widgets/detail/ui/components/Metriccard.tsx new file mode 100644 index 0000000..71a215c --- /dev/null +++ b/src/widgets/detail/ui/components/Metriccard.tsx @@ -0,0 +1,19 @@ +import { blue } from '../../lib/constant'; + +interface Props { + label: string; + value: string; +} + +export default function MetricCard({ label, value }: Props) { + return ( +
+

+ {label} +

+

+ {value} +

+
+ ); +} diff --git a/src/widgets/detail/ui/components/SectionTitle.tsx b/src/widgets/detail/ui/components/SectionTitle.tsx new file mode 100644 index 0000000..fd29c48 --- /dev/null +++ b/src/widgets/detail/ui/components/SectionTitle.tsx @@ -0,0 +1,16 @@ +import { blue } from '../../lib/constant'; + +export default function SectionTitle({ + children, +}: { + children: React.ReactNode; +}) { + return ( +

+ {children} +

+ ); +} diff --git a/src/widgets/detail/ui/components/SemanticModal.tsx b/src/widgets/detail/ui/components/SemanticModal.tsx new file mode 100644 index 0000000..4e5f8c7 --- /dev/null +++ b/src/widgets/detail/ui/components/SemanticModal.tsx @@ -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 ( +
+
e.stopPropagation()} + > +
+ + Semantik tahlil + + +
+
+ + + + + + + + + {rows.map(([label, value], i) => ( + + + + + ))} + +
+ Metrika + + Qiymat +
+ {label} + + {value} +
+
+
+
+ ); +} diff --git a/src/widgets/detail/ui/components/SertificateCard.tsx b/src/widgets/detail/ui/components/SertificateCard.tsx new file mode 100644 index 0000000..1ed69fb --- /dev/null +++ b/src/widgets/detail/ui/components/SertificateCard.tsx @@ -0,0 +1,51 @@ +import { blue } from '../../lib/constant'; +import { Certificate } from '../../lib/types'; + +export default function CertificateCard({ + certificate, +}: { + certificate: Certificate; +}) { + return ( +
+
+ +
+
+

+ {certificate.issuerName} sertifikati +

+

+ {certificate.verificationCode} +

+
+ + Yuklab olish ↗ + +
+ ); +} diff --git a/src/widgets/detail/ui/components/SourcesList.tsx b/src/widgets/detail/ui/components/SourcesList.tsx new file mode 100644 index 0000000..4fac58b --- /dev/null +++ b/src/widgets/detail/ui/components/SourcesList.tsx @@ -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 ( + <> + Manbalar ro'yxati + {sources.length > 0 ? ( +
+ + + + + + + + + {sources.map((s, i) => ( + + + + + ))} + +
+ URL + + Modul +
+
+
+ + {s.url} + +
+
+ {s.module} +
+
+ ) : ( + + )} + + ); +} diff --git a/src/widgets/detail/ui/components/TextAnalysis.tsx b/src/widgets/detail/ui/components/TextAnalysis.tsx new file mode 100644 index 0000000..a6be844 --- /dev/null +++ b/src/widgets/detail/ui/components/TextAnalysis.tsx @@ -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

Ma'lumot topilmadi

; + } + return ( + <> + Matn tahlili +
+ + + + + + +
+ + ); +} diff --git a/src/widgets/detail/ui/components/TopMetrics.tsx b/src/widgets/detail/ui/components/TopMetrics.tsx new file mode 100644 index 0000000..49a1145 --- /dev/null +++ b/src/widgets/detail/ui/components/TopMetrics.tsx @@ -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 ( +
+ + + + +
+ ); +} diff --git a/src/widgets/detail/ui/components/actionbuttons.tsx b/src/widgets/detail/ui/components/actionbuttons.tsx new file mode 100644 index 0000000..5fa3873 --- /dev/null +++ b/src/widgets/detail/ui/components/actionbuttons.tsx @@ -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 ( +
+ + + +
+ ); +} diff --git a/src/widgets/detail/ui/detailPage.tsx b/src/widgets/detail/ui/detailPage.tsx deleted file mode 100644 index 4aefd02..0000000 --- a/src/widgets/detail/ui/detailPage.tsx +++ /dev/null @@ -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 = () => ( - - - -); -const IconFile = () => ( - - - -); -const IconCalendar = () => ( - - - -); -const IconPayment = () => ( - - - -); -const IconShield = () => ( - - - -); -const IconCert = () => ( - - - -); -const IconDownload = () => ( - - - -); -// const IconBack = () => ( -// -// -// -// ); -const IconSource = () => ( - - - -); - -// ── Sub-components ──────────────────────────────────────────── - -interface CheckDetailViewProps { - check: PlagiarismCheck; -} - -const CheckHeader: React.FC = ({ check }) => { - const t = useTranslations('DetailPage'); - - return ( -
-
-
- -
-

- {check.sender.name} -

-

{check.sender.email}

-

- {t('id')}: {check.id} -

-
-
- -
-
- ); -}; - -const SubmissionInfoCard: React.FC = ({ check }) => { - const t = useTranslations('DetailPage'); - - return ( - } - accent="blue" - > - } - value={ - - - {check.sender.name} - - } - /> - } - value={ - - - - {check.fileName} - - - } - /> - - {formatFileSize(check.fileSize)} - - } - /> - } - value={formatDate(check.submittedAt)} - /> - } - value={ - - {formatCurrency(check.paymentAmount, check.currency)} - - } - /> - - ); -}; - -const ResultCard: React.FC = ({ check }) => { - const t = useTranslations('DetailPage'); - - if (check.status === 'processing' || check.status === 'pending') { - return ( - } - accent="violet" - > -
-
- - - - -
-

- {t('analysisInProgress')} -

-

- {t('resultsReadyAfterProcessing')} -

-
-
- ); - } - - if (!check.result) { - return ( - } - accent="violet" - > -

{t('noResultAvailable')}

-
- ); - } - - const { result } = check; - - return ( - } - accent={ - result.similarityLevel === 'low' - ? 'green' - : result.similarityLevel === 'high' - ? 'red' - : 'amber' - } - > -
- {/* Meter */} - - - {/* Stats grid */} -
-
-

- {result.checkedWords.toLocaleString()} -

-

{t('wordsChecked')}

-
-
-

- {result.matchedWords.toLocaleString()} -

-

{t('wordsMatched')}

-
-
- - {/* Sources */} - {result.sources.length > 0 && ( -
-

- {t('matchedSources')} -

-
- {result.sources.map((src, i) => ( -
-
-

- {src.title} -

- - - {src.url} - -
-
- = 30 ? 'text-red-600' : src.matchPercentage >= 15 ? 'text-amber-600' : 'text-emerald-600'}`} - > - {src.matchPercentage}% - -

- {src.matchedWords.toLocaleString()} {t('words')} -

-
-
- ))} -
-
- )} - - } - value={formatDate(result.processedAt)} - /> -
-
- ); -}; - -const CertificateCard: React.FC = ({ check }) => { - const t = useTranslations('DetailPage'); - - if (!check.certificate) { - return ( - } accent="violet"> -
-
- -
-

{t('noCertificate')}

- {check.result?.similarityLevel === 'high' && ( -

- {t('noCertificateHighSimilarity')} -

- )} -
-
- ); - } - - const { certificate } = check; - - return ( - } accent="green"> - {/* Certificate visual */} -
-
- - - -
-

- {certificate.issuerName} -

-

- {certificate.verificationCode} -

-

- {t('certificateId')}: {certificate.id} -

-
- - } - value={formatDate(certificate.issuedAt)} - /> - } - value={formatDate(certificate.expiresAt)} - /> - - - -
- ); -}; - -// ── Detail View (assembled) ─────────────────────────────────── - -const CheckDetailView: React.FC = ({ check }) => ( -
- -
- - -
- -
-); - -// ── Page ────────────────────────────────────────────────────── - -interface PlagiarismDetailPageProps { - checkId: string; - onBack?: () => void; -} - -export const PlagiarismDetailPage: React.FC = ({ - checkId, -}) => { - const t = useTranslations('DetailPage'); - const { check, loadingState, error, reload } = usePlagiarismDetail(checkId); - - return ( -
-
- {loadingState === 'loading' && } - {loadingState === 'error' && ( - - )} - {loadingState === 'success' && check && ( - - )} -
-
- ); -}; diff --git a/src/widgets/detail/ui/emptyList.tsx b/src/widgets/detail/ui/emptyList.tsx new file mode 100644 index 0000000..d88f511 --- /dev/null +++ b/src/widgets/detail/ui/emptyList.tsx @@ -0,0 +1,30 @@ +import { Search } from 'lucide-react'; + +export default function EmptySourcesList() { + return ( +
+
+ +
+

+ Manbalar topilmadi +

+

+ Plagiat manbalari aniqlanmagan +

+
+ ); +} diff --git a/src/widgets/detail/ui/index.tsx b/src/widgets/detail/ui/index.tsx new file mode 100644 index 0000000..2d14174 --- /dev/null +++ b/src/widgets/detail/ui/index.tsx @@ -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(/([\s\S]*?<\/sel>)/g); + for (const part of parts) { + if (part.startsWith('')) { + segments.push({ text: part.replace(/<\/?sel>/g, ''), plagiarized: true }); + } else if (part) { + segments.push({ text: part, plagiarized: false }); + } + } + return segments; +} + +function parseAnalyzeText(a: Record): 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 ( +

+ {label} — ma'lumot mavjud emas +

+ ); +} + +// ─── Loading skeleton ───────────────────────────────────────────────────────── + +function Skeleton() { + return ( +
+
+ {[100, 60, 80, 40, 90, 55].map((w, i) => ( +
+ ))} +
+
+ ); +} + +// ─── Error state ────────────────────────────────────────────────────────────── + +function ErrorState() { + return ( +
+
+

+ Ma'lumot topilmadi +

+

+ Ushbu tekshiruv mavjud emas yoki o'chirilgan +

+
+
+ ); +} + +// ─── 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 ; + if (isError || !rawData) return ; + + 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 && ( + setShowSemantic(false)} + /> + )} + +
+
+ {/* Certificate */} + {data.certificate ? ( + + ) : null} + + {/* Header */} + {data.name ? ( +
+ ) : ( + + )} + + {/* Human / AI bar */} + + + + + {/* Score cards */} + + + {/* Gauge */} + + + + + {/* Sources */} + + + + + {/* Highlighted text */} + {data.highlightedText.length > 0 ? ( + + ) : ( + + )} + + {/* Buttons */} + setShowSemantic(true)} + onRecheck={handleRecheck} + /> + + + + {/* Semantic analysis */} + {data.semantic ? ( + + ) : ( + + )} +
+
+ + ); +} diff --git a/src/widgets/fileUpload/lib/usePlagiraism.ts b/src/widgets/fileUpload/lib/usePlagiraism.ts index c1010a1..da648c0 100644 --- a/src/widgets/fileUpload/lib/usePlagiraism.ts +++ b/src/widgets/fileUpload/lib/usePlagiraism.ts @@ -11,6 +11,7 @@ import { useUserPlagiatStore } from '@/shared/zustand/user'; import { useMutation } from '@tanstack/react-query'; import { links } from '@/shared/request/links'; import { apiRequest } from '@/shared/request/apiRequest'; +import { useRouter } from '@/shared/config/i18n/navigation'; // ─── Initial States ────────────────────────────────────────────────────────── @@ -36,12 +37,17 @@ export function usePlagiarismForm() { const [isPaymentOpen, setIsPaymentOpen] = useState(false); const [submission, setSubmission] = useState(INITIAL_SUBMISSION); + const route = useRouter(); const checkdocumentRequest = useMutation({ mutationKey: ['plagiarismCheck'], mutationFn: (data: FormData) => apiRequest('POST', links.plagiarismCheck, data), - onSuccess: () => { + onSuccess: (res) => { + console.log('uploda: ', res); + const resdata = res.data as { id: number; order_id: number }; + console.log('order_id:', resdata.id); + route.push(`/${resdata.id}`); setSubmission({ status: 'success', error: null }); setForm(INITIAL_FORM); setIsPaymentOpen(false);