0fe04d4d99
- Сервисы: testAttemptService, testAccess, document import/gen/extract, LLM, assignment, aiEditor - Конфиг: devAuthor, featureFlags; messages/ru; интеграция V.9 (skip без БД) - API/роуты: app, auth, server; Dockerfile и env example - Фронт: TestAttempt, TestAttemptReview, AttemptReviewBlock, стили, правки App/api/login/vite - compose и README; смоук-тесты расширены Закрывает отсутствие модулей в origin после клона. Made-with: Cursor
113 lines
3.9 KiB
React
113 lines
3.9 KiB
React
import { Link } from 'react-router-dom';
|
|
|
|
/**
|
|
* @param {{ review: {
|
|
* testTitle?: string,
|
|
* attempterName?: string,
|
|
* attempterLogin?: string,
|
|
* startedAt?: string,
|
|
* completedAt?: string,
|
|
* correctCount: number,
|
|
* totalQuestions: number,
|
|
* percent: number,
|
|
* passed: boolean,
|
|
* passingThreshold: number,
|
|
* questions: Array<{
|
|
* id: string,
|
|
* text: string,
|
|
* isUserCorrect: boolean,
|
|
* options: Array<{ id: string, text: string, isCorrect: boolean, selected: boolean }>
|
|
* }>
|
|
* }, showAttempter?: boolean, backLink: { to: string, label: string } }} p
|
|
*/
|
|
export default function AttemptReviewBlock({ review, showAttempter, backLink }) {
|
|
if (!review?.questions?.length) {
|
|
return null;
|
|
}
|
|
return (
|
|
<div className="attempt-review" style={{ maxWidth: 640 }}>
|
|
{showAttempter && (review.attempterName || review.attempterLogin) && (
|
|
<p className="text-muted" style={{ marginTop: 0, fontSize: 14 }}>
|
|
Участник: {review.attempterName || '—'}{' '}
|
|
{review.attempterLogin && (
|
|
<span className="code-inline" style={{ fontSize: 12 }}>
|
|
{review.attempterLogin}
|
|
</span>
|
|
)}
|
|
</p>
|
|
)}
|
|
{review.completedAt && (
|
|
<p className="text-muted" style={{ marginTop: 0, fontSize: 13 }}>
|
|
Завершено: {new Date(review.completedAt).toLocaleString('ru-RU')}
|
|
</p>
|
|
)}
|
|
<ol style={{ paddingLeft: '1.1rem', marginTop: '0.75rem' }}>
|
|
{review.questions.map((q, i) => (
|
|
<li
|
|
key={q.id}
|
|
style={{
|
|
marginBottom: '1.1rem',
|
|
borderLeft: '3px solid',
|
|
borderColor: q.isUserCorrect
|
|
? 'color-mix(in srgb, var(--primary) 50%, var(--outline-variant))'
|
|
: 'color-mix(in srgb, var(--error, #c00) 45%, var(--outline-variant))',
|
|
paddingLeft: 10,
|
|
}}
|
|
>
|
|
<p style={{ marginTop: 0, marginBottom: 8, fontWeight: 600 }}>{i + 1}. {q.text}</p>
|
|
<p
|
|
className={q.isUserCorrect ? 'text-muted' : 'error-text'}
|
|
style={{ margin: '0 0 6px', fontSize: 13 }}
|
|
>
|
|
{q.isUserCorrect ? 'Верно' : 'Ошибка'}
|
|
</p>
|
|
<ul style={{ listStyle: 'none', padding: 0, margin: 0, fontSize: 14 }}>
|
|
{q.options.map((o) => {
|
|
const mark =
|
|
o.selected && o.isCorrect
|
|
? '✓ верно'
|
|
: o.selected && !o.isCorrect
|
|
? '✗ выбрано'
|
|
: !o.selected && o.isCorrect
|
|
? '— правильный вариант'
|
|
: '';
|
|
return (
|
|
<li
|
|
key={o.id}
|
|
style={{
|
|
marginBottom: 4,
|
|
opacity: o.selected || o.isCorrect ? 1 : 0.7,
|
|
}}
|
|
>
|
|
<span
|
|
style={
|
|
o.isCorrect
|
|
? { fontWeight: 600, color: 'var(--primary, #007168)' }
|
|
: o.selected
|
|
? {}
|
|
: { color: 'var(--secondary)' }
|
|
}
|
|
>
|
|
{o.text}
|
|
</span>
|
|
{mark && (
|
|
<span className="text-muted" style={{ marginLeft: 8, fontSize: 12 }}>
|
|
{mark}
|
|
</span>
|
|
)}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
{backLink && (
|
|
<p className="link-back" style={{ marginTop: '1rem' }}>
|
|
<Link to={backLink.to}>{backLink.label}</Link>
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|