import { useEffect, useMemo, useRef, useState } from "react"; import { loadMockJson, sleep, useDemoCase } from "../demo"; import { useLanguage, type UiLanguage } from "../i18n"; type CheckTone = "pass" | "fail" | "uncertain" | "na"; type QualityCheckView = { key: string; label: string; description: string; tone: CheckTone; status: string; details?: string; recommendation?: string; weight?: number; earned?: number; }; type QualityReportView = { repository: string; analyzedAt?: string; score: number; repoType: string; repoTypeLabel: string; repoTypeConfidence?: string; repoTypeError?: string; checks: QualityCheckView[]; informational: QualityCheckView[]; }; const COPY = { ru: { title: "Качество репозитория", subtitle: "Оценка структуры, документации и воспроизводимости проекта.", run: "Оценить репозиторий", running: "Анализ репозитория…", completed: "Проверка качества репозитория завершена.", type: "Тип", confidence: "Уверенность", score: "Оценка", analyzed: "Проверено", criteriaTitle: "Основные критерии", infoKicker: "Не влияют на итоговую оценку", infoTitle: "Дополнительные проверки", result: "Результат", whatToDo: "Что сделать", whatToCheck: "Что проверить", passed: "Выполнено", needsWork: "Нужно улучшить", manual: "Проверьте вручную", na: "Не применяется", downloadJson: "Скачать JSON", noCase: "Выберите диссертацию выше.", uncertainType: "Тип репозитория определён неуверенно", uncertainTypeText: "Проверьте, что определённый тип соответствует назначению проекта.", }, en: { title: "Repository quality", subtitle: "Evaluate project structure, documentation, and reproducibility.", run: "Evaluate repository", running: "Analyzing repository…", completed: "Repository quality check completed.", type: "Type", confidence: "Confidence", score: "Score", analyzed: "Analyzed", criteriaTitle: "Scored criteria", infoKicker: "Does not affect the final score", infoTitle: "Additional checks", result: "Result", whatToDo: "What to improve", whatToCheck: "What to verify", passed: "Passed", needsWork: "Needs improvement", manual: "Manual review", na: "Not applicable", downloadJson: "Download JSON", noCase: "Select a thesis above.", uncertainType: "Repository type is uncertain", uncertainTypeText: "Verify that the detected type matches the project.", }, } as const; const CHECK_COPY = { ru: { readme: ["README", "В корне репозитория есть содержательный README.", "Добавьте или дополните README: назначение проекта, запуск, основные возможности и пример использования."], license: ["Лицензия", "В репозитории есть файл лицензии.", "Добавьте LICENSE с подходящей лицензией."], commits: ["История разработки", "В истории репозитория не менее пяти коммитов.", "Сохраняйте этапы разработки отдельными осмысленными коммитами."], execution_files: ["Точка запуска", "В проекте удалось определить точку запуска.", "Добавьте понятную точку запуска и опишите команду запуска в README."], requirements: ["Зависимости", "Зависимости проекта зафиксированы.", "Добавьте requirements.txt или pyproject.toml с воспроизводимым набором зависимостей."], tests: ["Тесты", "В приложении обнаружены тесты.", "Добавьте автоматические тесты для основной логики."], data_files: ["Данные", "Для экспериментального проекта найдены используемые данные или связанные файлы.", "Добавьте данные, ссылки на них или инструкцию по получению данных."], experiment_scripts: ["Экспериментальные сценарии", "Найдены скрипты, позволяющие воспроизвести эксперименты.", "Добавьте отдельные команды/скрипты экспериментов."], syntax: ["Синтаксис Python", "Проверка синтаксиса Python-файлов.", "Исправьте найденные синтаксические ошибки."], docstrings: ["Документация кода", "Оценка покрытия функций и классов docstrings.", "Добавьте docstrings там, где их не хватает."], }, en: { readme: ["README", "A meaningful README is present at the repository root.", "Add or improve the README with the project purpose, setup, main features, and a usage example."], license: ["License", "A license file is present in the repository.", "Add an appropriate LICENSE file."], commits: ["Development history", "The repository contains at least five commits.", "Keep meaningful development stages as separate commits."], execution_files: ["Entry point", "A project entry point can be identified.", "Add a clear entry point and document the launch command in the README."], requirements: ["Dependencies", "Project dependencies are declared.", "Add requirements.txt or pyproject.toml with a reproducible dependency set."], tests: ["Tests", "Automated tests were found for the application.", "Add automated tests for the core logic."], data_files: ["Data", "Data files or related artifacts were found for the experimental project.", "Add the data, links to it, or clear instructions for obtaining it."], experiment_scripts: ["Experiment scripts", "Scripts that can reproduce the experiments were found.", "Add dedicated experiment scripts/commands."], syntax: ["Python syntax", "Checks Python files for syntax errors.", "Fix the reported syntax errors."], docstrings: ["Code documentation", "Measures docstring coverage for functions and classes.", "Add docstrings where coverage is low."], }, } as const; const TYPE_LABELS = { ru: { app: "Приложение", algorithm_experiments: "Алгоритмические эксперименты", model_training_experiments: "Обучение моделей / ML-эксперименты", unknown: "Тип не определён" }, en: { app: "Application", algorithm_experiments: "Algorithm experiments", model_training_experiments: "Model training / ML experiments", unknown: "Unknown type" }, } as const; export function RepositoryQualityPage() { const { language } = useLanguage(); const { selectedCase, selectedModel } = useDemoCase(); const t = COPY[language]; const [raw, setRaw] = useState(null); const [running, setRunning] = useState(false); const [progress, setProgress] = useState(0); const [error, setError] = useState(""); const [done, setDone] = useState(false); const generation = useRef(0); useEffect(() => { generation.current += 1; setRaw(null); setRunning(false); setProgress(0); setError(""); setDone(false); }, [selectedCase?.id, selectedModel.id]); const report = useMemo(() => raw ? normalizeReport(raw, language) : null, [raw, language]); async function runCheck() { if (!selectedCase || running) return; const token = ++generation.current; setRaw(null); setError(""); setDone(false); setRunning(true); setProgress(0); const start = performance.now(); const timer = window.setInterval(() => { const pct = Math.min(96, Math.round(((performance.now() - start) / 5000) * 100)); setProgress(pct); }, 100); await sleep(5000); window.clearInterval(timer); if (token !== generation.current) return; try { const value = await loadMockJson(selectedCase.repositoryQualityJson); if (token !== generation.current) return; setRaw(value); setProgress(100); setDone(true); } catch (runError) { if (token !== generation.current) return; setError(runError instanceof Error ? runError.message : String(runError)); } finally { if (token === generation.current) setRunning(false); } } return

{t.title}

{t.subtitle}

{selectedCase?.author || t.noCase} {selectedCase?.repository && {selectedCase.repository} · {selectedModel.label}}
{(running || done) &&
{done ? "✓" : ""}
{done ? t.completed : t.running}{progress}%
} {error &&
{error}
} {report && <>
{shortRepository(report.repository)}
{report.score}/100
{t.score}
{t.type}{report.repoTypeLabel}
{report.repoTypeConfidence &&
{t.confidence}{confidenceLabel(report.repoTypeConfidence, language)}
} {report.analyzedAt &&
{t.analyzed}{formatDateTime(report.analyzedAt, language)}
}
{(report.repoTypeError || report.repoTypeConfidence === "low") &&
{t.uncertainType}{t.uncertainTypeText}
}

{t.criteriaTitle}

{selectedCase && {t.downloadJson}}
{report.checks.map((check) =>
{checkStatusLabel(check.tone, language)}

{check.label}

{typeof check.weight === "number" && {check.earned ?? 0}/{check.weight}}

{check.description}

{t.result}

{check.status}

{check.details &&

{check.details}

} {check.recommendation && check.tone !== "pass" && check.tone !== "na" &&
{check.tone === "uncertain" ? t.whatToCheck : t.whatToDo}{check.recommendation}
}
)}
{report.informational.length > 0 &&
{t.infoKicker}

{t.infoTitle}

{report.informational.map((check) =>
{check.label}

{check.status}

{check.details && {check.details}}
)}
} }
; } function normalizeReport(raw: unknown, language: UiLanguage): QualityReportView { const root = asRecord(raw); const report = Object.keys(asRecord(root.result)).length ? asRecord(root.result) : root; const summary = asRecord(report.summary); const checks = asRecord(report.checks); const breakdown = asRecord(summary.score_breakdown); const repoTypeRaw = asRecord(checks.repo_type); const repoType = pickString(summary, ["repo_type"]) || pickString(repoTypeRaw, ["value"]) || "unknown"; const weighted = Object.entries(breakdown).map(([key, rawBreakdown]) => buildCheckView(key, asRecord(checks[key]), asRecord(rawBreakdown), language)); const informational = ["syntax", "docstrings"].filter((key) => checks[key]).map((key) => buildCheckView(key, asRecord(checks[key]), {}, language, true)); const typeLabels = TYPE_LABELS[language] as Record; return { repository: pickString(report, ["repo_url"]) || "Repository", analyzedAt: pickString(report, ["analyzed_at"]), score: pickNumber(summary, ["score"]) ?? 0, repoType, repoTypeLabel: typeLabels[repoType] || humanize(repoType), repoTypeConfidence: pickString(repoTypeRaw, ["confidence"]), repoTypeError: pickString(repoTypeRaw, ["error"]), checks: weighted, informational, }; } function buildCheckView(key: string, raw: Record, score: Record, language: UiLanguage, informational = false): QualityCheckView { const languageCopy = CHECK_COPY[language] as Record; const fallback: readonly [string, string, string] = language === "ru" ? [humanize(key), "Проверка критерия.", "Проверьте критерий вручную."] : [humanize(key), "Criterion check.", "Review this criterion manually."]; const entry = languageCopy[key] || fallback; const applicable = raw.applicable !== false && score.applicable !== false; const llmError = pickString(raw, ["error"]); const passed = typeof score.passed === "boolean" ? score.passed : inferPassed(key, raw); let tone: CheckTone = applicable ? (passed ? "pass" : "fail") : "na"; if (llmError) tone = "uncertain"; let status = language === "ru" ? (tone === "pass" ? "Критерий выполнен." : tone === "na" ? "Критерий не применяется к этому типу репозитория." : "Критерий не выполнен.") : (tone === "pass" ? "Criterion passed." : tone === "na" ? "This criterion does not apply to this repository type." : "Criterion not met."); if (llmError === "llm_failed") status = language === "ru" ? "Не удалось однозначно определить результат этой проверки." : "This result could not be determined reliably."; else if (llmError) status = language === "ru" ? `Проверка завершилась неоднозначно: ${llmError}.` : `The check was inconclusive: ${llmError}.`; if (key === "readme") { if (!raw.present) status = language === "ru" ? "README в корне репозитория не найден." : "No README was found at the repository root."; else if (raw.meaningful === false) status = language === "ru" ? "README найден, но он недостаточно подробный." : "README was found, but it is not detailed enough."; else if (tone === "pass") status = language === "ru" ? "README найден." : "README found."; } else if (key === "commits" && typeof raw.count === "number") { status = language === "ru" ? `${raw.count} коммитов найдено.` : `${raw.count} commits found.`; } else if (pickString(raw, ["matched_file"])) { status = language === "ru" ? `Найден файл: ${pickString(raw, ["matched_file"])}.` : `Found: ${pickString(raw, ["matched_file"])}.`; } else if (Array.isArray(raw.verified) && raw.verified.length) { status = language === "ru" ? `Подтверждено: ${raw.verified.slice(0, 4).join(", ")}.` : `Verified: ${raw.verified.slice(0, 4).join(", ")}.`; } else if (informational && pickString(raw, ["summary"])) { status = pickString(raw, ["summary"]) || status; } const details = buildDetails(raw, language); const recommendation = tone === "uncertain" ? `${language === "ru" ? "Проверьте этот критерий вручную." : "Review this criterion manually."} ${entry[2]}` : entry[2]; return { key, label: entry[0], description: entry[1], tone, status, details, recommendation, weight: pickNumber(score, ["weight"]), earned: pickNumber(score, ["earned"]) }; } function inferPassed(key: string, raw: Record): boolean { if (key === "readme") return Boolean(raw.present && raw.meaningful); return Boolean(raw.present); } function buildDetails(raw: Record, language: UiLanguage): string | undefined { const parts: string[] = []; const reason = pickString(raw, ["reason", "detail", "message"]); if (reason) parts.push(reason); if (Array.isArray(raw.verified) && raw.verified.length) parts.push(`${language === "ru" ? "Файлы" : "Files"}: ${raw.verified.slice(0, 6).join(", ")}.`); if (Array.isArray(raw.missing) && raw.missing.length) parts.push(`${language === "ru" ? "Не найдено" : "Missing"}: ${raw.missing.slice(0, 6).join(", ")}.`); return parts.length ? parts.join(" ") : undefined; } function toneClass(tone: CheckTone): "pass" | "violation" | "uncertain" | "not_applicable" { if (tone === "pass") return "pass"; if (tone === "fail") return "violation"; if (tone === "uncertain") return "uncertain"; return "not_applicable"; } function checkStatusLabel(tone: CheckTone, language: UiLanguage): string { const t = COPY[language]; if (tone === "pass") return t.passed; if (tone === "fail") return t.needsWork; if (tone === "uncertain") return t.manual; return t.na; } function shortRepository(value: string): string { try { const url = new URL(value); return url.pathname.replace(/^\//, "").replace(/\.git$/, "") || value; } catch { return value; } } function confidenceLabel(value: string, language: UiLanguage): string { const normalized = value.toLowerCase(); if (language === "ru") return normalized === "high" ? "высокая" : normalized === "medium" ? "средняя" : normalized === "low" ? "низкая" : value; return normalized; } function humanize(value: string): string { const normalized = value.replace(/_/g, " "); return normalized.charAt(0).toUpperCase() + normalized.slice(1); } function asRecord(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; } function pickString(record: Record, keys: string[]): string | undefined { for (const key of keys) { const value = record[key]; if (typeof value === "string" && value.trim()) return value.trim(); } return undefined; } function pickNumber(record: Record, keys: string[]): number | undefined { for (const key of keys) { const value = record[key]; if (typeof value === "number" && Number.isFinite(value)) return value; } return undefined; } function formatDateTime(value: string | null | undefined, language: UiLanguage): string { if (!value) return "—"; const date = new Date(value); return Number.isNaN(date.getTime()) ? value : date.toLocaleString(language === "ru" ? "ru-RU" : "en-GB", { dateStyle: "short", timeStyle: "short" }); }