diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java index 898b4456de07..4d549bb3fe66 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java @@ -418,7 +418,7 @@ else if (exercise instanceof ProgrammingExercise) { // Process feedback request StudentParticipation updatedParticipation; if (exercise instanceof TextExercise) { - updatedParticipation = textExerciseFeedbackService.handleNonGradedFeedbackRequest(exercise.getId(), participation, (TextExercise) exercise); + updatedParticipation = textExerciseFeedbackService.handleNonGradedFeedbackRequest(participation, (TextExercise) exercise); } else if (exercise instanceof ModelingExercise) { updatedParticipation = modelingExerciseFeedbackService.handleNonGradedFeedbackRequest(participation, (ModelingExercise) exercise); diff --git a/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java index 14a2558f6c9d..d95a76755e6c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java @@ -3,8 +3,11 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import org.slf4j.Logger; @@ -21,11 +24,10 @@ import de.tum.cit.aet.artemis.assessment.web.ResultWebsocketService; import de.tum.cit.aet.artemis.athena.service.AthenaFeedbackSuggestionsService; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; -import de.tum.cit.aet.artemis.core.exception.InternalServerErrorException; -import de.tum.cit.aet.artemis.exercise.domain.participation.Participation; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.exercise.service.ParticipationService; import de.tum.cit.aet.artemis.exercise.service.SubmissionService; +import de.tum.cit.aet.artemis.text.domain.TextBlock; import de.tum.cit.aet.artemis.text.domain.TextExercise; import de.tum.cit.aet.artemis.text.domain.TextSubmission; @@ -47,14 +49,18 @@ public class TextExerciseFeedbackService { private final ResultRepository resultRepository; + private final TextBlockService textBlockService; + public TextExerciseFeedbackService(Optional athenaFeedbackSuggestionsService, SubmissionService submissionService, - ResultService resultService, ResultRepository resultRepository, ResultWebsocketService resultWebsocketService, ParticipationService participationService) { + ResultService resultService, ResultRepository resultRepository, ResultWebsocketService resultWebsocketService, ParticipationService participationService, + TextBlockService textBlockService) { this.athenaFeedbackSuggestionsService = athenaFeedbackSuggestionsService; this.submissionService = submissionService; this.resultService = resultService; this.resultRepository = resultRepository; this.resultWebsocketService = resultWebsocketService; this.participationService = participationService; + this.textBlockService = textBlockService; } private void checkRateLimitOrThrow(StudentParticipation participation) { @@ -64,7 +70,7 @@ private void checkRateLimitOrThrow(StudentParticipation participation) { long countOfAthenaResults = athenaResults.size(); if (countOfAthenaResults >= 10) { - throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "preconditions not met"); + throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "maxAthenaResultsReached", true); } } @@ -72,12 +78,11 @@ private void checkRateLimitOrThrow(StudentParticipation participation) { * Handles the request for generating feedback for a text exercise. * Unlike programming exercises a tutor is not notified if Athena is not available. * - * @param exerciseId the id of the text exercise. * @param participation the student participation associated with the exercise. * @param textExercise the text exercise object. * @return StudentParticipation updated text exercise for an AI assessment */ - public StudentParticipation handleNonGradedFeedbackRequest(Long exerciseId, StudentParticipation participation, TextExercise textExercise) { + public StudentParticipation handleNonGradedFeedbackRequest(StudentParticipation participation, TextExercise textExercise) { if (this.athenaFeedbackSuggestionsService.isPresent()) { this.checkRateLimitOrThrow(participation); CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(participation, textExercise)); @@ -101,50 +106,81 @@ public void generateAutomaticNonGradedFeedback(StudentParticipation participatio if (submissionOptional.isEmpty()) { throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission"); } - var submission = submissionOptional.get(); + TextSubmission textSubmission = (TextSubmission) submissionOptional.get(); Result automaticResult = new Result(); automaticResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); automaticResult.setRated(true); automaticResult.setScore(0.0); automaticResult.setSuccessful(null); - automaticResult.setSubmission(submission); + automaticResult.setSubmission(textSubmission); automaticResult.setParticipation(participation); try { - this.resultWebsocketService.broadcastNewResult((Participation) participation, automaticResult); + // This broadcast signals the client that feedback is being generated, does not save empty result + this.resultWebsocketService.broadcastNewResult(participation, automaticResult); + + log.debug("Submission id: {}", textSubmission.getId()); - log.debug("Submission id: {}", submission.getId()); + var athenaResponse = this.athenaFeedbackSuggestionsService.orElseThrow().getTextFeedbackSuggestions(textExercise, textSubmission, true); - var athenaResponse = this.athenaFeedbackSuggestionsService.orElseThrow().getTextFeedbackSuggestions(textExercise, (TextSubmission) submission, false); + Set textBlocks = new HashSet<>(); + List feedbacks = new ArrayList<>(); - List feedbacks = athenaResponse.stream().filter(individualFeedbackItem -> individualFeedbackItem.description() != null).map(individualFeedbackItem -> { + athenaResponse.stream().filter(individualFeedbackItem -> individualFeedbackItem.description() != null).forEach(individualFeedbackItem -> { + var textBlock = new TextBlock(); var feedback = new Feedback(); + feedback.setText(individualFeedbackItem.title()); feedback.setDetailText(individualFeedbackItem.description()); feedback.setHasLongFeedbackText(false); feedback.setType(FeedbackType.AUTOMATIC); feedback.setCredits(individualFeedbackItem.credits()); - return feedback; - }).toList(); + + if (textSubmission.getText() != null && individualFeedbackItem.indexStart() != null && individualFeedbackItem.indexEnd() != null) { + textBlock.setStartIndex(individualFeedbackItem.indexStart()); + textBlock.setEndIndex(individualFeedbackItem.indexEnd()); + textBlock.setSubmission(textSubmission); + textBlock.setTextFromSubmission(); + textBlock.automatic(); + textBlock.computeId(); + feedback.setReference(textBlock.getId()); + textBlock.setFeedback(feedback); + log.debug(textBlock.toString()); + + textBlocks.add(textBlock); + } + feedbacks.add(feedback); + }); double totalFeedbacksScore = 0.0; for (Feedback feedback : feedbacks) { totalFeedbacksScore += feedback.getCredits(); } totalFeedbacksScore = totalFeedbacksScore / textExercise.getMaxPoints() * 100; - automaticResult.setSuccessful(true); automaticResult.setCompletionDate(ZonedDateTime.now()); - automaticResult.setScore(Math.clamp(totalFeedbacksScore, 0, 100)); + // For Athena automatic results successful = true will mean that the generation was successful + // undefined in progress and false it failed + automaticResult.setSuccessful(true); + automaticResult = this.resultRepository.save(automaticResult); resultService.storeFeedbackInResult(automaticResult, feedbacks, true); - submissionService.saveNewResult(submission, automaticResult); - this.resultWebsocketService.broadcastNewResult((Participation) participation, automaticResult); + textBlockService.saveAll(textBlocks); + textSubmission.setBlocks(textBlocks); + submissionService.saveNewResult(textSubmission, automaticResult); + // This broadcast signals the client that feedback generation succeeded, result is saved in this case only + this.resultWebsocketService.broadcastNewResult(participation, automaticResult); } catch (Exception e) { log.error("Could not generate feedback", e); - throw new InternalServerErrorException("Something went wrong... AI Feedback could not be generated"); + // Broadcast the failed result but don't save, note that successful = false is normally used to indicate a score < 100 + // but since we do not differentiate for athena feedback we use it to indicate a failed generation + automaticResult.setSuccessful(false); + automaticResult.setCompletionDate(null); + participation.addResult(automaticResult); // for proper change detection + // This broadcast signals the client that feedback generation failed, does not save empty result + this.resultWebsocketService.broadcastNewResult(participation, automaticResult); } } } diff --git a/src/main/java/de/tum/cit/aet/artemis/text/web/TextExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/text/web/TextExerciseResource.java index 58b63332e519..f9894c729a4f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/text/web/TextExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/text/web/TextExerciseResource.java @@ -11,6 +11,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -421,44 +422,49 @@ public ResponseEntity getDataForTextEditor(@PathVariable L participation.setResults(new HashSet<>(results)); } - Optional optionalSubmission = participation.findLatestSubmission(); + if (!ExerciseDateService.isAfterAssessmentDueDate(textExercise)) { + // We want to have the preliminary feedback before the assessment due date too + Set athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA) + .collect(Collectors.toSet()); + participation.setResults(athenaResults); + } + + Set submissions = participation.getSubmissions(); participation.setSubmissions(new HashSet<>()); - if (optionalSubmission.isPresent()) { - TextSubmission textSubmission = (TextSubmission) optionalSubmission.get(); + for (Submission submission : submissions) { + if (submission != null) { + TextSubmission textSubmission = (TextSubmission) submission; - // set reference to participation to null, since we are already inside a participation - textSubmission.setParticipation(null); + // set reference to participation to null, since we are already inside a participation + textSubmission.setParticipation(null); - if (!ExerciseDateService.isAfterAssessmentDueDate(textExercise)) { - // We want to have the preliminary feedback before the assessment due date too - List athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); - textSubmission.setResults(athenaResults); - Set athenaResultsSet = new HashSet(athenaResults); - participation.setResults(athenaResultsSet); - } + if (!ExerciseDateService.isAfterAssessmentDueDate(textExercise)) { + // We want to have the preliminary feedback before the assessment due date too + List athenaResults = submission.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); + textSubmission.setResults(athenaResults); + } - Result result = textSubmission.getLatestResult(); - if (result != null) { - // Load TextBlocks for the Submission. They are needed to display the Feedback in the client. - final var textBlocks = textBlockRepository.findAllBySubmissionId(textSubmission.getId()); - textSubmission.setBlocks(textBlocks); + Result result = textSubmission.getLatestResult(); + if (result != null) { + // Load TextBlocks for the Submission. They are needed to display the Feedback in the client. + final var textBlocks = textBlockRepository.findAllBySubmissionId(textSubmission.getId()); + textSubmission.setBlocks(textBlocks); - if (textSubmission.isSubmitted() && result.getCompletionDate() != null) { - List assessments = feedbackRepository.findByResult(result); - result.setFeedbacks(assessments); - } + if (textSubmission.isSubmitted() && result.getCompletionDate() != null) { + List assessments = feedbackRepository.findByResult(result); + result.setFeedbacks(assessments); + } - if (!authCheckService.isAtLeastTeachingAssistantForExercise(textExercise, user)) { - result.filterSensitiveInformation(); - } + if (!authCheckService.isAtLeastTeachingAssistantForExercise(textExercise, user)) { + result.filterSensitiveInformation(); + } - // only send the one latest result to the client - textSubmission.setResults(List.of(result)); - participation.setResults(Set.of(result)); + // only send the one latest result to the client + textSubmission.setResults(List.of(result)); + } + participation.addSubmission(textSubmission); } - - participation.addSubmission(textSubmission); } if (!(authCheckService.isAtLeastInstructorForExercise(textExercise, user) || participation.isOwnedBy(user))) { diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.ts index da7c2b549735..975808bde2ce 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.ts @@ -56,10 +56,10 @@ export class HeaderParticipationPageComponent implements OnInit, OnChanges { this.exerciseStatusBadge = hasExerciseDueDatePassed(this.exercise, this.participation) ? 'bg-danger' : 'bg-success'; this.exerciseCategories = this.exercise.categories || []; this.dueDate = getExerciseDueDate(this.exercise, this.participation); - if (this.participation?.results?.[0]?.rated) { + if (this.participation?.results?.last()?.rated) { this.achievedPoints = roundValueSpecifiedByCourseSettings( // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - (this.participation.results?.[0].score! * this.exercise.maxPoints!) / 100, + (this.participation.results?.last()?.score! * this.exercise.maxPoints!) / 100, getCourseFromExercise(this.exercise), ); } diff --git a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts index dbc3b28d2e83..35b86f595fca 100644 --- a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts @@ -171,7 +171,6 @@ export class ExerciseScoresComponent implements OnInit, OnDestroy { // the result of the first correction round will be at index 0, // the result of a complaints or the second correction at index 1. participation.results?.sort((result1, result2) => (result1.id ?? 0) - (result2.id ?? 0)); - const resultsWithoutAthena = participation.results?.filter((result) => result.assessmentType !== AssessmentType.AUTOMATIC_ATHENA); if (resultsWithoutAthena?.length != 0) { if (resultsWithoutAthena?.[0].submission) { diff --git a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html index 95d72394f220..b3ddd8125e7e 100644 --- a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html +++ b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html @@ -28,8 +28,8 @@ id="allowFeedbackRequests" (change)="toggleFeedbackRequests($event)" /> - - + + } @if (!!this.exercise.feedbackSuggestionModule) { diff --git a/src/main/webapp/app/exercises/shared/feedback/feedback.component.ts b/src/main/webapp/app/exercises/shared/feedback/feedback.component.ts index 576445b7831d..4386aa37f323 100644 --- a/src/main/webapp/app/exercises/shared/feedback/feedback.component.ts +++ b/src/main/webapp/app/exercises/shared/feedback/feedback.component.ts @@ -79,7 +79,6 @@ export class FeedbackComponent implements OnInit, OnChanges { faXmark = faXmark; faCircleNotch = faCircleNotch; faExclamationTriangle = faExclamationTriangle; - private showTestDetails = false; isLoading = false; loadingFailed = false; diff --git a/src/main/webapp/app/exercises/shared/rating/rating.component.html b/src/main/webapp/app/exercises/shared/rating/rating.component.html index f7703d8b6230..21c90c003115 100644 --- a/src/main/webapp/app/exercises/shared/rating/rating.component.html +++ b/src/main/webapp/app/exercises/shared/rating/rating.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/main/webapp/app/exercises/shared/rating/rating.component.ts b/src/main/webapp/app/exercises/shared/rating/rating.component.ts index d7c81c0c68eb..41173d0c219f 100644 --- a/src/main/webapp/app/exercises/shared/rating/rating.component.ts +++ b/src/main/webapp/app/exercises/shared/rating/rating.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { RatingService } from 'app/exercises/shared/rating/rating.service'; import { StarRatingComponent } from 'app/exercises/shared/rating/star-rating/star-rating.component'; import { Result } from 'app/entities/result.model'; @@ -11,7 +11,7 @@ import { Observable } from 'rxjs'; templateUrl: './rating.component.html', styleUrls: ['./rating.component.scss'], }) -export class RatingComponent implements OnInit { +export class RatingComponent implements OnInit, OnChanges { public rating: number; public disableRating = false; @Input() result?: Result; @@ -22,10 +22,19 @@ export class RatingComponent implements OnInit { ) {} ngOnInit(): void { + this.loadRating(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['result'] && !changes['result'].isFirstChange()) { + this.loadRating(); + } + } + + loadRating() { if (!this.result?.id || !this.result.participation || !this.accountService.isOwnerOfParticipation(this.result.participation as StudentParticipation)) { return; } - this.ratingService.getRating(this.result.id).subscribe((rating) => { this.rating = rating ?? 0; }); diff --git a/src/main/webapp/app/exercises/shared/result/result.component.ts b/src/main/webapp/app/exercises/shared/result/result.component.ts index f9edf80994d9..6d1c2d1e9cc2 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.ts +++ b/src/main/webapp/app/exercises/shared/result/result.component.ts @@ -10,6 +10,7 @@ import { } from 'app/exercises/shared/result/result.utils'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; +import { Router } from '@angular/router'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; import dayjs from 'dayjs/esm'; import { isProgrammingExerciseStudentParticipation, isResultPreliminary } from 'app/exercises/programming/shared/utils/programming-exercise.utils'; @@ -89,6 +90,7 @@ export class ResultComponent implements OnInit, OnChanges, OnDestroy { @Optional() private exerciseCacheService: ExerciseCacheService, private resultService: ResultService, private csvDownloadService: CsvDownloadService, + private router: Router, ) {} /** @@ -190,7 +192,6 @@ export class ResultComponent implements OnInit, OnChanges, OnDestroy { */ evaluate() { this.templateStatus = evaluateTemplateStatus(this.exercise, this.participation, this.result, this.isBuilding, this.missingResultInfo); - if (this.templateStatus === ResultTemplateStatus.LATE) { this.textColorClass = getTextColorClass(this.result, this.templateStatus); this.resultIconClass = getResultIconClass(this.result, this.templateStatus); @@ -260,6 +261,17 @@ export class ResultComponent implements OnInit, OnChanges, OnDestroy { */ showDetails(result: Result) { const exerciseService = this.exerciseCacheService ?? this.exerciseService; + if (this.exercise?.type === ExerciseType.TEXT) { + const courseId = getCourseFromExercise(this.exercise)?.id; + let submissionId = result.submission?.id; + // In case of undefined result submission try the latest submission as this can happen before reloading the component + if (!submissionId) { + submissionId = result.participation?.submissions?.last()?.id; + } + this.router.navigate(['/courses', courseId, 'exercises', 'text-exercises', this.exercise?.id, 'participate', result.participation?.id, 'submission', submissionId]); + return undefined; + } + const feedbackComponentParameters = prepareFeedbackComponentParameters(this.exercise, result, this.participation, this.templateStatus, this.latestDueDate, exerciseService); if (this.exercise?.type === ExerciseType.QUIZ) { diff --git a/src/main/webapp/app/exercises/shared/result/result.service.ts b/src/main/webapp/app/exercises/shared/result/result.service.ts index ea4c9eaaca4c..83141b71c674 100644 --- a/src/main/webapp/app/exercises/shared/result/result.service.ts +++ b/src/main/webapp/app/exercises/shared/result/result.service.ts @@ -116,6 +116,9 @@ export class ResultService implements IResultService { if (result && isAthenaAIResult(result) && result.successful === undefined) { return this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackInProgress'); } + if (result && isAthenaAIResult(result) && result.successful === false) { + return this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackFailed'); + } aiFeedbackMessage = this.getResultStringNonProgrammingExercise(relativeScore, points, short); return `${aiFeedbackMessage} (${this.translateService.instant('artemisApp.result.preliminary')})`; } diff --git a/src/main/webapp/app/exercises/shared/result/result.utils.ts b/src/main/webapp/app/exercises/shared/result/result.utils.ts index cee9c6dbb07f..ad0abafcd42d 100644 --- a/src/main/webapp/app/exercises/shared/result/result.utils.ts +++ b/src/main/webapp/app/exercises/shared/result/result.utils.ts @@ -179,6 +179,8 @@ export const evaluateTemplateStatus = ( // the assessment due date has passed (or there was none) (or it is not manual feedback) if (result?.assessmentType === AssessmentType.AUTOMATIC_ATHENA && result?.successful === undefined) { return ResultTemplateStatus.IS_GENERATING_FEEDBACK; + } else if (result?.assessmentType === AssessmentType.AUTOMATIC_ATHENA && result?.successful === false) { + return ResultTemplateStatus.FEEDBACK_GENERATION_FAILED; } return ResultTemplateStatus.HAS_RESULT; } else { @@ -306,8 +308,17 @@ export const getResultIconClass = (result: Result | undefined, templateStatus: R return faQuestionCircle; } - if (isAIResultAndProcessed(result)) { - return faCheckCircle; + if (result.assessmentType === AssessmentType.AUTOMATIC_ATHENA) { + // result loading + if (result.successful === undefined) { + return faCircleNotch; + } + // result done successfuly + if (result.successful) { + return faCheckCircle; + } + // generating failed + return faTimesCircle; } if (isBuildFailedAndResultIsAutomatic(result) || isAIResultAndFailed(result)) { diff --git a/src/main/webapp/app/exercises/text/participate/text-editor.component.html b/src/main/webapp/app/exercises/text/participate/text-editor.component.html index 75a81fd26d0f..c070d57e29ea 100644 --- a/src/main/webapp/app/exercises/text/participate/text-editor.component.html +++ b/src/main/webapp/app/exercises/text/participate/text-editor.component.html @@ -5,24 +5,68 @@ {{ 'artemisApp.textSubmission.textEditor' | artemisTranslate }}: {{ examMode ? textExercise.exerciseGroup?.title : textExercise?.title }} - + @if (isOwnerOfParticipation) { - + @if (isReadOnlyWithShowResult) { + @if ((sortedHistoryResults?.length || 0) > 1) { + + } + @if (isActive || !textExercise.dueDate) { + + } + } @else { + @if (textExercise.allowFeedbackRequests && (!this.textExercise.dueDate || !hasExerciseDueDatePassed(this.textExercise, this.participation))) { + + } + } + @if (!this.isReadOnlyWithShowResult) { + + } } } +
+ @if (isReadOnlyWithShowResult) { + @if (showHistory) { +
+ +
+ } + } +
+ @if (textExercise) { - +
@if (textExercise?.teamMode && isActive) { @@ -49,14 +93,14 @@ >
- @if (!result || isAutomaticResult) { + @if (!((result && !isAutomaticResult) || isReadOnlyWithShowResult)) {