Skip to content

Commit

Permalink
Revert "Text exercises: Replace feedback modal with inline feedback v…
Browse files Browse the repository at this point in the history
…iew (#9395)"

This reverts commit a9d3399.
  • Loading branch information
muradium committed Nov 1, 2024
1 parent 7c2b5bc commit 61f805d
Show file tree
Hide file tree
Showing 44 changed files with 368 additions and 704 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ else if (exercise instanceof ProgrammingExercise) {
// Process feedback request
StudentParticipation updatedParticipation;
if (exercise instanceof TextExercise) {
updatedParticipation = textExerciseFeedbackService.handleNonGradedFeedbackRequest(participation, (TextExercise) exercise);
updatedParticipation = textExerciseFeedbackService.handleNonGradedFeedbackRequest(exercise.getId(), participation, (TextExercise) exercise);
}
else if (exercise instanceof ModelingExercise) {
updatedParticipation = modelingExerciseFeedbackService.handleNonGradedFeedbackRequest(participation, (ModelingExercise) exercise);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
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;
Expand All @@ -24,10 +21,11 @@
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;

Expand All @@ -49,18 +47,14 @@ public class TextExerciseFeedbackService {

private final ResultRepository resultRepository;

private final TextBlockService textBlockService;

public TextExerciseFeedbackService(Optional<AthenaFeedbackSuggestionsService> athenaFeedbackSuggestionsService, SubmissionService submissionService,
ResultService resultService, ResultRepository resultRepository, ResultWebsocketService resultWebsocketService, ParticipationService participationService,
TextBlockService textBlockService) {
ResultService resultService, ResultRepository resultRepository, ResultWebsocketService resultWebsocketService, ParticipationService participationService) {
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) {
Expand All @@ -70,19 +64,20 @@ private void checkRateLimitOrThrow(StudentParticipation participation) {
long countOfAthenaResults = athenaResults.size();

if (countOfAthenaResults >= 10) {
throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "maxAthenaResultsReached", true);
throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "preconditions not met");
}
}

/**
* 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(StudentParticipation participation, TextExercise textExercise) {
public StudentParticipation handleNonGradedFeedbackRequest(Long exerciseId, StudentParticipation participation, TextExercise textExercise) {
if (this.athenaFeedbackSuggestionsService.isPresent()) {
this.checkRateLimitOrThrow(participation);
CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(participation, textExercise));
Expand All @@ -106,81 +101,50 @@ public void generateAutomaticNonGradedFeedback(StudentParticipation participatio
if (submissionOptional.isEmpty()) {
throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission");
}
TextSubmission textSubmission = (TextSubmission) submissionOptional.get();
var submission = submissionOptional.get();

Result automaticResult = new Result();
automaticResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA);
automaticResult.setRated(true);
automaticResult.setScore(0.0);
automaticResult.setSuccessful(null);
automaticResult.setSubmission(textSubmission);
automaticResult.setSubmission(submission);
automaticResult.setParticipation(participation);
try {
// 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());
this.resultWebsocketService.broadcastNewResult((Participation) participation, automaticResult);

var athenaResponse = this.athenaFeedbackSuggestionsService.orElseThrow().getTextFeedbackSuggestions(textExercise, textSubmission, true);
log.debug("Submission id: {}", submission.getId());

Set<TextBlock> textBlocks = new HashSet<>();
List<Feedback> feedbacks = new ArrayList<>();
var athenaResponse = this.athenaFeedbackSuggestionsService.orElseThrow().getTextFeedbackSuggestions(textExercise, (TextSubmission) submission, false);

athenaResponse.stream().filter(individualFeedbackItem -> individualFeedbackItem.description() != null).forEach(individualFeedbackItem -> {
var textBlock = new TextBlock();
List<Feedback> feedbacks = athenaResponse.stream().filter(individualFeedbackItem -> individualFeedbackItem.description() != null).map(individualFeedbackItem -> {
var feedback = new Feedback();

feedback.setText(individualFeedbackItem.title());
feedback.setDetailText(individualFeedbackItem.description());
feedback.setHasLongFeedbackText(false);
feedback.setType(FeedbackType.AUTOMATIC);
feedback.setCredits(individualFeedbackItem.credits());

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);
});
return feedback;
}).toList();

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.setScore(Math.clamp(totalFeedbacksScore, 0, 100));

automaticResult = this.resultRepository.save(automaticResult);
resultService.storeFeedbackInResult(automaticResult, feedbacks, true);
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);
submissionService.saveNewResult(submission, automaticResult);
this.resultWebsocketService.broadcastNewResult((Participation) participation, automaticResult);
}
catch (Exception e) {
log.error("Could not generate feedback", e);
// 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);
throw new InternalServerErrorException("Something went wrong... AI Feedback could not be generated");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
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;
Expand Down Expand Up @@ -422,49 +421,44 @@ public ResponseEntity<StudentParticipation> getDataForTextEditor(@PathVariable L
participation.setResults(new HashSet<>(results));
}

if (!ExerciseDateService.isAfterAssessmentDueDate(textExercise)) {
// We want to have the preliminary feedback before the assessment due date too
Set<Result> athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA)
.collect(Collectors.toSet());
participation.setResults(athenaResults);
}

Set<Submission> submissions = participation.getSubmissions();
Optional<Submission> optionalSubmission = participation.findLatestSubmission();
participation.setSubmissions(new HashSet<>());

for (Submission submission : submissions) {
if (submission != null) {
TextSubmission textSubmission = (TextSubmission) submission;
if (optionalSubmission.isPresent()) {
TextSubmission textSubmission = (TextSubmission) optionalSubmission.get();

// 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<Result> athenaResults = submission.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList();
textSubmission.setResults(athenaResults);
}
// set reference to participation to null, since we are already inside a participation
textSubmission.setParticipation(null);

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 (!ExerciseDateService.isAfterAssessmentDueDate(textExercise)) {
// We want to have the preliminary feedback before the assessment due date too
List<Result> athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList();
textSubmission.setResults(athenaResults);
Set<Result> athenaResultsSet = new HashSet<Result>(athenaResults);
participation.setResults(athenaResultsSet);
}

if (textSubmission.isSubmitted() && result.getCompletionDate() != null) {
List<Feedback> assessments = feedbackRepository.findByResult(result);
result.setFeedbacks(assessments);
}
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 (!authCheckService.isAtLeastTeachingAssistantForExercise(textExercise, user)) {
result.filterSensitiveInformation();
}
if (textSubmission.isSubmitted() && result.getCompletionDate() != null) {
List<Feedback> assessments = feedbackRepository.findByResult(result);
result.setFeedbacks(assessments);
}

// only send the one latest result to the client
textSubmission.setResults(List.of(result));
if (!authCheckService.isAtLeastTeachingAssistantForExercise(textExercise, user)) {
result.filterSensitiveInformation();
}
participation.addSubmission(textSubmission);

// only send the one latest result to the client
textSubmission.setResults(List.of(result));
participation.setResults(Set.of(result));
}

participation.addSubmission(textSubmission);
}

if (!(authCheckService.isAtLeastInstructorForExercise(textExercise, user) || participation.isOwnedBy(user))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?.last()?.rated) {
if (this.participation?.results?.[0]?.rated) {
this.achievedPoints = roundValueSpecifiedByCourseSettings(
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
(this.participation.results?.last()?.score! * this.exercise.maxPoints!) / 100,
(this.participation.results?.[0].score! * this.exercise.maxPoints!) / 100,
getCourseFromExercise(this.exercise),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
id="allowFeedbackRequests"
(change)="toggleFeedbackRequests($event)"
/>
<label class="form-control-label" for="allowFeedbackRequests" jhiTranslate="artemisApp.textExercise.allowPreliminaryAthenaFeedbackRequests"></label>
<jhi-help-icon placement="right auto" [text]="'artemisApp.textExercise.allowPreliminaryAthenaFeedbackRequestsTooltip'" />
<label class="form-control-label" for="allowFeedbackRequests" jhiTranslate="artemisApp.programmingExercise.timeline.allowFeedbackRequests"></label>
<jhi-help-icon placement="right auto" [text]="'artemisApp.programmingExercise.timeline.allowFeedbackRequestsTooltip'" />
</div>
}
@if (!!this.exercise.feedbackSuggestionModule) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export class FeedbackComponent implements OnInit, OnChanges {
faXmark = faXmark;
faCircleNotch = faCircleNotch;
faExclamationTriangle = faExclamationTriangle;

private showTestDetails = false;
isLoading = false;
loadingFailed = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div [class.non-clickable]="disableRating" [id]="'rating'">
<div [class.non-clickable]="disableRating">
<b>
<span jhiTranslate="artemisApp.rating.label"></span>
</b>
Expand Down
15 changes: 3 additions & 12 deletions src/main/webapp/app/exercises/shared/rating/rating.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { Component, Input, OnInit } 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';
Expand All @@ -11,7 +11,7 @@ import { Observable } from 'rxjs';
templateUrl: './rating.component.html',
styleUrls: ['./rating.component.scss'],
})
export class RatingComponent implements OnInit, OnChanges {
export class RatingComponent implements OnInit {
public rating: number;
public disableRating = false;
@Input() result?: Result;
Expand All @@ -22,19 +22,10 @@ export class RatingComponent implements OnInit, OnChanges {
) {}

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;
});
Expand Down
Loading

0 comments on commit 61f805d

Please sign in to comment.