Skip to content

Commit

Permalink
Programming exercises: Add Artemis intelligence rewriting for problem…
Browse files Browse the repository at this point in the history
… statement (#10156)
  • Loading branch information
FelixTJDietrich authored Jan 30, 2025
1 parent 32212f8 commit f2bff3b
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<jhi-markdown-editor-monaco
class="overflow-hidden flex-grow-1"
[domainActions]="domainActions"
[artemisIntelligenceActions]="artemisIntelligenceActions()"
[initialEditorHeight]="initialEditorHeight"
[useDefaultMarkdownEditorOptions]="false"
[enableResize]="enableResize"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import { AfterViewInit, Component, EventEmitter, HostListener, Input, OnChanges, OnDestroy, Output, SimpleChanges, ViewChild, ViewEncapsulation, inject } from '@angular/core';
import {
AfterViewInit,
Component,
EventEmitter,
HostListener,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges,
ViewChild,
ViewEncapsulation,
computed,
inject,
} from '@angular/core';
import { AlertService } from 'app/core/util/alert.service';
import { ProgrammingExerciseInstructionComponent } from 'app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component';
import { Observable, Subject, Subscription, of, throwError } from 'rxjs';
import { catchError, map as rxMap, switchMap, tap } from 'rxjs/operators';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { ProgrammingExerciseTestCase } from 'app/entities/programming/programming-exercise-test-case.model';
import { ProblemStatementAnalysis } from 'app/exercises/programming/manage/instructions-editor/analysis/programming-exercise-instruction-analysis.model';
import { Participation } from 'app/entities/participation/participation.model';
Expand All @@ -25,6 +40,13 @@ import { TranslateDirective } from 'app/shared/language/translate.directive';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { ProgrammingExerciseInstructionAnalysisComponent } from './analysis/programming-exercise-instruction-analysis.component';
import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe';
import { RewriteAction } from 'app/shared/monaco-editor/model/actions/artemis-intelligence/rewrite.action';
import { PROFILE_IRIS } from 'app/app.constants';
import RewritingVariant from 'app/shared/monaco-editor/model/actions/artemis-intelligence/rewriting-variant';
import { ProfileService } from 'app/shared/layouts/profiles/profile.service';
import { ArtemisIntelligenceService } from 'app/shared/monaco-editor/model/actions/artemis-intelligence/artemis-intelligence.service';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';

@Component({
selector: 'jhi-programming-exercise-editable-instructions',
Expand All @@ -42,11 +64,14 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe';
ArtemisTranslatePipe,
],
})
export class ProgrammingExerciseEditableInstructionComponent implements AfterViewInit, OnChanges, OnDestroy {
export class ProgrammingExerciseEditableInstructionComponent implements AfterViewInit, OnChanges, OnDestroy, OnInit {
private activatedRoute = inject(ActivatedRoute);
private programmingExerciseService = inject(ProgrammingExerciseService);
private alertService = inject(AlertService);
private programmingExerciseParticipationService = inject(ProgrammingExerciseParticipationService);
private testCaseService = inject(ProgrammingExerciseGradingService);
private profileService = inject(ProfileService);
private artemisIntelligenceService = inject(ArtemisIntelligenceService);

participationValue: Participation;
programmingExercise: ProgrammingExercise;
Expand All @@ -57,6 +82,12 @@ export class ProgrammingExerciseEditableInstructionComponent implements AfterVie
testCaseAction = new TestCaseAction();
domainActions: TextEditorDomainAction[] = [new FormulaAction(), new TaskAction(), this.testCaseAction];

courseId: number;
irisEnabled = toSignal(this.profileService.getProfileInfo().pipe(map((profileInfo) => profileInfo.activeProfiles.includes(PROFILE_IRIS))), { initialValue: false });
artemisIntelligenceActions = computed(() =>
this.irisEnabled() ? [new RewriteAction(this.artemisIntelligenceService, RewritingVariant.PROBLEM_STATEMENT, this.courseId)] : [],
);

savingInstructions = false;
unsavedChangesValue = false;

Expand Down Expand Up @@ -117,6 +148,10 @@ export class ProgrammingExerciseEditableInstructionComponent implements AfterVie

protected readonly MarkdownEditorHeight = MarkdownEditorHeight;

ngOnInit() {
this.courseId = Number(this.activatedRoute.snapshot.paramMap.get('courseId'));
}

ngOnChanges(changes: SimpleChanges): void {
if (hasExerciseChanged(changes)) {
this.setupTestCaseSubscription();
Expand Down Expand Up @@ -236,9 +271,9 @@ export class ProgrammingExerciseEditableInstructionComponent implements AfterVie
loadTestCasesFromTemplateParticipationResult = (templateParticipationId: number): Observable<Array<string | undefined>> => {
// Fallback for exercises that don't have test cases yet.
return this.programmingExerciseParticipationService.getLatestResultWithFeedback(templateParticipationId).pipe(
rxMap((result) => (!result?.feedbacks ? throwError(() => new Error('no result available')) : result)),
map((result) => (!result?.feedbacks ? throwError(() => new Error('no result available')) : result)),
// use the text (legacy case) or the name of the provided test case attribute
rxMap(({ feedbacks }: Result) => feedbacks!.map((feedback) => feedback.text ?? feedback.testCase?.testName).sort()),
map(({ feedbacks }: Result) => feedbacks!.map((feedback) => feedback.text ?? feedback.testCase?.testName).sort()),
catchError(() => of([])),
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, fakeAsync, flush, tick } from '@angular/core
import { TranslateService } from '@ngx-translate/core';
import { By } from '@angular/platform-browser';
import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service';
import { MockComponent, MockDirective, MockPipe } from 'ng-mocks';
import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks';
import { Subject, of, throwError } from 'rxjs';
import { DebugElement } from '@angular/core';
import { ArtemisTestModule } from '../../test.module';
Expand All @@ -28,6 +28,9 @@ import { HttpResponse } from '@angular/common/http';
import { AlertService } from 'app/core/util/alert.service';
import { MockAlertService } from '../../helpers/mocks/service/mock-alert.service';
import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { ProfileService } from 'app/shared/layouts/profiles/profile.service';
import { ProfileInfo } from '../../../../../main/webapp/app/shared/layouts/profiles/profile-info.model';

describe('ProgrammingExerciseEditableInstructionComponent', () => {
let comp: ProgrammingExerciseEditableInstructionComponent;
Expand All @@ -53,6 +56,17 @@ describe('ProgrammingExerciseEditableInstructionComponent', () => {
{ testName: 'test3', active: false },
];

const mockProfileInfo = { activeProfiles: ['iris'] } as ProfileInfo;

const route = {
snapshot: { paramMap: convertToParamMap({ courseId: '1' }) },
url: {
pipe: () => ({
subscribe: () => {},
}),
},
} as ActivatedRoute;

beforeEach(() => {
return TestBed.configureTestingModule({
imports: [ArtemisTestModule, MockDirective(NgbTooltip)],
Expand All @@ -69,6 +83,10 @@ describe('ProgrammingExerciseEditableInstructionComponent', () => {
{ provide: ParticipationWebsocketService, useClass: MockParticipationWebsocketService },
{ provide: TranslateService, useClass: MockTranslateService },
{ provide: AlertService, useClass: MockAlertService },
{ provide: ActivatedRoute, useValue: route },
MockProvider(ProfileService, {
getProfileInfo: () => of(mockProfileInfo),
}),
],
})
.compileComponents()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Subject } from 'rxjs';
import { Params } from '@angular/router';
import { convertToParamMap, Params } from '@angular/router';

export class MockActivatedRouteWithSubjects {
private subject = new Subject<Params>();
params = this.subject;

snapshot = { paramMap: convertToParamMap({ courseId: '1' }) };
setSubject = (subject: Subject<Params>) => {
this.params = subject;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { MockParticipationService } from '../../helpers/mocks/service/mock-parti
import { MockProgrammingExerciseService } from '../../helpers/mocks/service/mock-programming-exercise.service';
import { WebsocketService } from 'app/core/websocket/websocket.service';
import { MockWebsocketService } from '../../helpers/mocks/service/mock-websocket.service';
import { MockComponent, MockModule, MockPipe } from 'ng-mocks';
import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks';
import { CodeEditorContainerComponent } from 'app/exercises/programming/shared/code-editor/container/code-editor-container.component';
import { IncludedInScoreBadgeComponent } from 'app/exercises/shared/exercise-headers/included-in-score-badge.component';
import { ProgrammingExerciseInstructorExerciseStatusComponent } from 'app/exercises/programming/manage/status/programming-exercise-instructor-exercise-status.component';
Expand All @@ -67,6 +67,8 @@ import { CodeEditorMonacoComponent } from 'app/exercises/programming/shared/code
import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component';
import { mockCodeEditorMonacoViewChildren } from '../../helpers/mocks/mock-instance.helper';
import { REPOSITORY } from 'app/exercises/programming/manage/code-editor/code-editor-instructor-base-container.component';
import { ProfileService } from 'app/shared/layouts/profiles/profile.service';
import { ProfileInfo } from 'app/shared/layouts/profiles/profile-info.model';

describe('CodeEditorInstructorIntegration', () => {
let comp: CodeEditorInstructorAndEditorContainerComponent;
Expand All @@ -90,6 +92,8 @@ describe('CodeEditorInstructorIntegration', () => {
let findWithParticipationsSubject: Subject<{ body: ProgrammingExercise }>;
let routeSubject: Subject<Params>;

const mockProfileInfo = { activeProfiles: ['iris'] } as ProfileInfo;

// Workaround for an error with MockComponent(). You can remove this once https://github.com/help-me-mom/ng-mocks/issues/8634 is resolved.
mockCodeEditorMonacoViewChildren();

Expand Down Expand Up @@ -138,6 +142,9 @@ describe('CodeEditorInstructorIntegration', () => {
{ provide: ProgrammingExerciseParticipationService, useClass: MockProgrammingExerciseParticipationService },
{ provide: ProgrammingExerciseService, useClass: MockProgrammingExerciseService },
{ provide: WebsocketService, useClass: MockWebsocketService },
MockProvider(ProfileService, {
getProfileInfo: () => of(mockProfileInfo),
}),
],
})
.compileComponents()
Expand Down

0 comments on commit f2bff3b

Please sign in to comment.