Spaces:
Running
Running
Download src/components/RepositoryQualityPage.tsx from tityos/OSA.Edu: direct link, hf CLI and curl.
- Browser
- Download file 20.4 kB
-
https://huggingface.co/spaces/tityos/OSA.Edu/resolve/main/src/components/RepositoryQualityPage.tsx
- Command line
-
hf download hf://spaces/tityos/OSA.Edu/src/components/RepositoryQualityPage.tsx
-
curl -L -o RepositoryQualityPage.tsx https://huggingface.co/spaces/tityos/OSA.Edu/resolve/main/src/components/RepositoryQualityPage.tsx
20.4 kB
| 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<unknown>(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 <section className="page narrow repository-quality-page"> | |
| <header className="rq-hero"> | |
| <h1>{t.title}</h1> | |
| <p>{t.subtitle}</p> | |
| </header> | |
| <div className="panel demo-launch-card"> | |
| <div> | |
| <strong>{selectedCase?.author || t.noCase}</strong> | |
| {selectedCase?.repository && <span>{selectedCase.repository} · {selectedModel.label}</span>} | |
| </div> | |
| <button className="button primary" type="button" onClick={() => void runCheck()} disabled={!selectedCase || running}> | |
| {running ? t.running : t.run} | |
| </button> | |
| </div> | |
| {(running || done) && <section className="panel demo-run-state"> | |
| <div className="demo-run-line"> | |
| <span className={`demo-spinner ${done ? "done" : ""}`}>{done ? "✓" : ""}</span> | |
| <div><strong>{done ? t.completed : t.running}</strong><span>{progress}%</span></div> | |
| </div> | |
| <progress max="100" value={progress} /> | |
| </section>} | |
| {error && <div className="inline-error rq-inline-message">{error}</div>} | |
| {report && <> | |
| <section className="panel rq-result-summary"> | |
| <div className="rq-result-main"> | |
| <span className="rq-result-kicker">{shortRepository(report.repository)}</span> | |
| <div className="rq-score-line"><strong>{report.score}</strong><span>/100</span></div> | |
| <span className="rq-score-caption">{t.score}</span> | |
| </div> | |
| <div className="rq-result-meta"> | |
| <div><span>{t.type}</span><strong>{report.repoTypeLabel}</strong></div> | |
| {report.repoTypeConfidence && <div><span>{t.confidence}</span><strong>{confidenceLabel(report.repoTypeConfidence, language)}</strong></div>} | |
| {report.analyzedAt && <div><span>{t.analyzed}</span><strong>{formatDateTime(report.analyzedAt, language)}</strong></div>} | |
| </div> | |
| </section> | |
| {(report.repoTypeError || report.repoTypeConfidence === "low") && <div className="rq-warning panel"> | |
| <strong>{t.uncertainType}</strong><span>{t.uncertainTypeText}</span> | |
| </div>} | |
| <section className="rq-criteria-section"> | |
| <div className="rq-section-heading rq-section-heading-inline"> | |
| <div><h2>{t.criteriaTitle}</h2></div> | |
| {selectedCase && <a className="text-button rq-json-link" href={selectedCase.repositoryQualityJson} download>{t.downloadJson}</a>} | |
| </div> | |
| <div className="panel rq-check-list"> | |
| {report.checks.map((check) => <article key={check.key} className={`rq-check-card ${check.tone}`}> | |
| <div className="rq-check-head"> | |
| <div className="rq-check-heading"><span className={`status-badge ${toneClass(check.tone)}`}>{checkStatusLabel(check.tone, language)}</span><h3>{check.label}</h3></div> | |
| {typeof check.weight === "number" && <strong className="rq-points">{check.earned ?? 0}/{check.weight}</strong>} | |
| </div> | |
| <p className="rq-check-description">{check.description}</p> | |
| <div className="rq-check-status"><span>{t.result}</span><p>{check.status}</p></div> | |
| {check.details && <p className="rq-check-details">{check.details}</p>} | |
| {check.recommendation && check.tone !== "pass" && check.tone !== "na" && <div className="rq-recommendation"><strong>{check.tone === "uncertain" ? t.whatToCheck : t.whatToDo}</strong><span>{check.recommendation}</span></div>} | |
| </article>)} | |
| </div> | |
| </section> | |
| {report.informational.length > 0 && <section className="rq-info-section"> | |
| <div className="rq-section-heading"><span>{t.infoKicker}</span><h2>{t.infoTitle}</h2></div> | |
| <div className="panel rq-info-list">{report.informational.map((check) => <div key={check.key} className="rq-info-card"><strong>{check.label}</strong><p>{check.status}</p>{check.details && <span>{check.details}</span>}</div>)}</div> | |
| </section>} | |
| </>} | |
| </section>; | |
| } | |
| 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<string, string>; | |
| 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<string, any>, score: Record<string, any>, language: UiLanguage, informational = false): QualityCheckView { | |
| const languageCopy = CHECK_COPY[language] as Record<string, readonly [string, string, string]>; | |
| 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<string, any>): boolean { | |
| if (key === "readme") return Boolean(raw.present && raw.meaningful); | |
| return Boolean(raw.present); | |
| } | |
| function buildDetails(raw: Record<string, any>, 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<string, any> { | |
| return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : {}; | |
| } | |
| function pickString(record: Record<string, any>, 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<string, any>, 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" }); | |
| } | |