feat: полный бэк и фронт (попытки, разбор, импорт, ИИ, назначения)
- Сервисы: 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
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user