Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iris: Add a temporary ChatGPT interface for specific user groups and exercises #10167

Merged
merged 11 commits into from
Jan 21, 2025
Merged
2 changes: 1 addition & 1 deletion gradle/jacoco.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ ext {
"CLASS": 0
],
"iris" : [
"INSTRUCTION": 0.795,
"INSTRUCTION": 0.792,
"CLASS": 17
],
"lecture" : [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,9 @@ public final class Constants {

public static final Pattern ALLOWED_CHECKOUT_DIRECTORY = Pattern.compile("[\\w-]+(/[\\w-]+)*$");

// TODO TW: This "feature" is only temporary for a paper.
public static final String ICER_PAPER_FLAG = "ICER 2025 Paper a5157934-9092-4a72-addc-3aaf489debdc";

private Constants() {
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.tum.cit.aet.artemis.exercise.web;

import static de.tum.cit.aet.artemis.core.config.Constants.ICER_PAPER_FLAG;
import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.time.ZonedDateTime;
Expand All @@ -10,6 +11,7 @@
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
Expand Down Expand Up @@ -344,6 +346,13 @@ public ResponseEntity<ExerciseDetailsDTO> getExerciseDetails(@PathVariable Long
.orElse(null);
PlagiarismCaseInfoDTO plagiarismCaseInfo = plagiarismCaseService.getPlagiarismCaseInfoForExerciseAndUser(exercise.getId(), user.getId()).orElse(null);

// TODO TW: This "feature" is only temporary for a paper.
if (StringUtils.contains(exercise.getProblemStatement(), ICER_PAPER_FLAG)) {
if (user.getId() % 3 == 2) {
irisSettings = null;
}
}

return ResponseEntity.ok(new ExerciseDetailsDTO(exercise, irisSettings, plagiarismCaseInfo));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.tum.cit.aet.artemis.iris.service.session;

import static de.tum.cit.aet.artemis.core.config.Constants.ICER_PAPER_FLAG;
import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS;

import java.util.List;
Expand All @@ -8,6 +9,7 @@
import java.util.concurrent.CompletableFuture;
import java.util.stream.IntStream;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
Expand Down Expand Up @@ -171,6 +173,14 @@
var latestSubmission = getLatestSubmissionIfExists(exercise, chatSession.getUser());

var variant = irisSettingsService.getCombinedIrisSettingsFor(session.getExercise(), false).irisChatSettings().selectedVariant();

// TODO TW: This "feature" is only temporary for a paper.
if (StringUtils.contains(exercise.getProblemStatement(), ICER_PAPER_FLAG)) {
if (chatSession.getUser().getId() % 3 == 0) {
variant = "chat-gpt-wrapper";
}
}

pyrisPipelineService.executeExerciseChatPipeline(variant, latestSubmission, exercise, chatSession, event);
}

Expand Down Expand Up @@ -208,7 +218,7 @@
* @param result The result of the submission
*/
public void onNewResult(Result result) {
var participation = result.getSubmission().getParticipation();

Check failure on line 221 in src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java

View workflow job for this annotation

GitHub Actions / H2 Tests

de.tum.cit.aet.artemis.iris.PyrisEventSystemIntegrationTest ► testShouldNotFireProgressStalledEventWithLessThanThreeSubmissions()

Failed test found in: build/test-results/test/TEST-de.tum.cit.aet.artemis.iris.PyrisEventSystemIntegrationTest.xml Error: org.mockito.exceptions.verification.NeverWantedButInvoked:
Raw output
org.mockito.exceptions.verification.NeverWantedButInvoked: 
irisExerciseChatSessionService.onNewResult(
    <any de.tum.cit.aet.artemis.assessment.domain.Result>
);
Never wanted here:
-> at de.tum.cit.aet.artemis.iris.service.session.IrisExerciseChatSessionService.onNewResult(IrisExerciseChatSessionService.java:221)
But invoked here:
-> at de.tum.cit.aet.artemis.iris.service.pyris.event.NewResultEvent.handleEvent(NewResultEvent.java:30) with arguments: [Result{id35, completionDate=2025-01-21T21:35:02.911053117Z[Etc/UTC], successful=false, score=20.0, rated=true, assessmentType=AUTOMATIC, hasComplaint=null, testCaseCount=0, passedTestCaseCount=0, codeIssueCount=0}]

	at app//de.tum.cit.aet.artemis.iris.service.session.IrisExerciseChatSessionService.onNewResult(IrisExerciseChatSessionService.java:221)
	at app//de.tum.cit.aet.artemis.iris.PyrisEventSystemIntegrationTest.testShouldNotFireProgressStalledEventWithLessThanThreeSubmissions(PyrisEventSystemIntegrationTest.java:302)
	at [email protected]/java.lang.reflect.Method.invoke(Method.java:580)
	at [email protected]/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)
	at [email protected]/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312)
	at [email protected]/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843)
	at [email protected]/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808)
	at [email protected]/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)
if (!(participation instanceof ProgrammingExerciseStudentParticipation studentParticipation)) {
return;
}
Expand Down
10 changes: 10 additions & 0 deletions src/main/resources/public/images/chatgpt-temp/ChatGPT_logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/main/webapp/app/app.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ export const PROFILE_LTI = 'lti';
export const PROFILE_ATHENA = 'athena';

export const PROFILE_THEIA = 'theia';

// TODO TW: This "feature" is only temporary for a paper.
export const ICER_PAPER_FLAG = 'ICER 2025 Paper a5157934-9092-4a72-addc-3aaf489debdc';
krusche marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
<!-- client -->
<div class="chat-header">
<div class="header-start">
<jhi-iris-logo [size]="IrisLogoSize.FLUID" />
<div class="word-iris">Iris</div>
<a [routerLink]="'/about-iris'" target="_blank">
<fa-icon [icon]="faCircleInfo" class="info-button" />
</a>
@if (!isChatGptWrapper) {
<jhi-iris-logo [size]="IrisLogoSize.FLUID" />
} @else {
<!-- TODO TW: This "feature" is only temporary for a paper. -->
<img src="public/images/chatgpt-temp/ChatGPT_logo.svg" alt="Iris Logo" style="height: 27px" class="iris-logo" />
krusche marked this conversation as resolved.
Show resolved Hide resolved
}

<div class="word-iris">{{ isChatGptWrapper ? 'ChatGPT' : 'Iris' }}</div>
@if (!isChatGptWrapper) {
<a [routerLink]="'/about-iris'" target="_blank">
<fa-icon [icon]="faCircleInfo" class="info-button" />
</a>
}
</div>
<div class="d-flex gap-2">
@if (rateLimitInfo.rateLimit > 0) {
Expand Down Expand Up @@ -129,7 +137,12 @@ <h4 class="modal-title">
}
@if (!messages?.length) {
<div class="empty-chat-message">
<jhi-iris-logo [size]="IrisLogoSize.SMALL" />
@if (!isChatGptWrapper) {
<jhi-iris-logo [size]="IrisLogoSize.SMALL" />
} @else {
<!-- TODO TW: This "feature" is only temporary for a paper. -->
<img src="public/images/chatgpt-temp/ChatGPT_logo.svg" alt="Iris Logo" style="height: 35px" class="iris-logo" />
}
<h3 jhiTranslate="artemisApp.iris.chat.helpOffer"></h3>
</div>
}
Expand Down Expand Up @@ -186,7 +199,11 @@ <h3 jhiTranslate="artemisApp.iris.chat.helpOffer"></h3>
this.hasActiveStage
"
/>
<span class="disclaimer-message" jhiTranslate="artemisApp.exerciseChatbot.disclaimer"></span>
@if (!isChatGptWrapper) {
<span class="disclaimer-message" jhiTranslate="artemisApp.exerciseChatbot.disclaimer"></span>
} @else {
<span class="disclaimer-message" jhiTranslate="artemisApp.exerciseChatbot.disclaimerGPT"></span>
}
</div>
}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -162,25 +162,37 @@
gap: 8px;
}

.bubble-left,
.bubble-right {
--r: 13px; /* the radius */
--t: 10px; /* the size of the tail */

max-width: 100%;
margin-bottom: 10px;
color: var(--bs-body-color);
padding: 10px;
// prettier-ignore
-webkit-mask:
radial-gradient(var(--t) at var(--_d) 0, #0000 98%, #000 102%) var(--_d) 100% / calc(100% - var(--r)) var(--t) no-repeat,
:host {
.bubble-left,
.bubble-right {
--r: 13px; /* the radius */
--t: 10px; /* the size of the tail */

max-width: 100%;
margin-bottom: 10px;
color: var(--bs-body-color);
padding: 10px;
// prettier-ignore
-webkit-mask: radial-gradient(var(--t) at var(--_d) 0, #0000 98%, #000 102%) var(--_d) 100% / calc(100% - var(--r)) var(--t) no-repeat,
conic-gradient(at var(--r) var(--r), #000 75%, #0000 0) calc(var(--r) / -2) calc(var(--r) / -2) padding-box,
radial-gradient(50% 50%, #000 98%, #0000 101%) 0 0 / var(--r) var(--r) space padding-box;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
::ng-deep p {
margin-bottom: 0;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;

::ng-deep > span > p,
::ng-deep > p {
margin-bottom: 0;
}

::ng-deep > span > p:not(:last-child),
::ng-deep > p:not(:last-child) {
margin-bottom: 7px;
}

::ng-deep pre code {
line-height: 0.8;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export class IrisBaseChatbotComponent implements OnInit, OnDestroy, AfterViewIni

@Input() fullSize: boolean | undefined;
@Input() showCloseButton = false;
@Input() isChatGptWrapper = false;
@Output() fullSizeToggle = new EventEmitter<void>();
@Output() closeClicked = new EventEmitter<void>();

Expand All @@ -166,6 +167,7 @@ export class IrisBaseChatbotComponent implements OnInit, OnDestroy, AfterViewIni
this.messagesSubscription = this.chatService.currentMessages().subscribe((messages) => {
if (messages.length !== this.messages?.length) {
this.scrollToBottom('auto');
setTimeout(() => this.messageTextarea?.nativeElement?.focus(), 10);
}
this.messages = _.cloneDeep(messages).reverse();
this.messages.forEach((message) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
}
</div>
<div class="chatbot-button">
<jhi-iris-logo [size]="IrisLogoSize.MEDIUM" [look]="IrisLogoLookDirection.LEFT" (click)="handleButtonClick()" />
@if (isChatGptWrapper) {
<!-- TODO TW: This "feature" is only temporary for a paper. -->
<img src="public/images/chatgpt-temp/ChatGPT_logo.svg" alt="Iris Logo" style="height: 70px" class="iris-logo" (click)="handleButtonClick()" />
} @else {
<jhi-iris-logo [size]="IrisLogoSize.MEDIUM" [look]="IrisLogoLookDirection.LEFT" (click)="handleButtonClick()" />
}
@if (hasNewMessages) {
<fa-icon [icon]="faCircle" size="xl" class="unread-indicator" />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export class IrisExerciseChatbotButtonComponent implements OnInit, OnDestroy {
@Input()
mode: ChatServiceMode;

@Input()
isChatGptWrapper: boolean = false; // TODO TW: This "feature" is only temporary for a paper.

dialogRef: MatDialogRef<IrisChatbotWidgetComponent> | null = null;
chatOpen = false;
isOverflowing = false;
Expand Down Expand Up @@ -153,6 +156,7 @@ export class IrisExerciseChatbotButtonComponent implements OnInit, OnDestroy {
scrollStrategy: this.overlay.scrollStrategies.noop(),
position: { bottom: '0px', right: '0px' },
disableClose: true,
data: { isChatGptWrapper: this.isChatGptWrapper },
});
this.dialogRef.afterClosed().subscribe(() => this.handleDialogClose());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<div class="container" (mouseenter)="toggleScrollLock(true)" (mouseleave)="toggleScrollLock(false)">
<!-- chat box -->
<div class="chat-widget">
<jhi-iris-base-chatbot [fullSize]="fullSize" [showCloseButton]="true" (fullSizeToggle)="toggleFullSize()" (closeClicked)="closeChat()" />
<jhi-iris-base-chatbot
[fullSize]="fullSize"
[showCloseButton]="true"
(fullSizeToggle)="toggleFullSize()"
(closeClicked)="closeChat()"
[isChatGptWrapper]="dialogData?.isChatGptWrapper || false"
/>
<div class="chat-widget-top-resize-area"></div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { NavigationStart, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { ButtonType } from 'app/shared/components/button.component';
import { IrisBaseChatbotComponent } from '../../base-chatbot/iris-base-chatbot.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';

@Component({
selector: 'jhi-chatbot-widget',
Expand All @@ -18,6 +19,8 @@ export class IrisChatbotWidgetComponent implements OnDestroy, AfterViewInit {
private router = inject(Router);
private dialog = inject(MatDialog);

dialogData = inject<{ isChatGptWrapper: boolean }>(MAT_DIALOG_DATA);

// User preferences
initialWidth = 400;
initialHeight = 600;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ <h4 class="mt-2">
</div>
}
@if (exercise.type === PROGRAMMING && !exercise.exerciseGroup && irisSettings?.irisChatSettings?.enabled) {
<jhi-exercise-chatbot-button [mode]="ChatServiceMode.EXERCISE" />
<jhi-exercise-chatbot-button [mode]="ChatServiceMode.EXERCISE" [isChatGptWrapper]="isChatGptWrapper" />
<!-- TODO TW: This "feature" is only temporary for a paper. -->
}
@if (plagiarismCaseInfo?.verdict === PlagiarismVerdict.NO_PLAGIARISM) {
<a class="btn btn-info btn-sm me-2" [routerLink]="['/courses', courseId, 'plagiarism-cases', plagiarismCaseInfo?.id]">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { ExerciseCacheService } from 'app/exercises/shared/exercise/exercise-cac
import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model';
import { AbstractScienceComponent } from 'app/shared/science/science.component';
import { ScienceEventType } from 'app/shared/science/science.model';
import { PROFILE_IRIS } from 'app/app.constants';
import { ICER_PAPER_FLAG, PROFILE_IRIS } from 'app/app.constants';
import { ChatServiceMode } from 'app/iris/iris-chat.service';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { NgClass } from '@angular/common';
Expand All @@ -59,6 +59,7 @@ import { DiscussionSectionComponent } from '../discussion-section/discussion-sec
import { LtiInitializerComponent } from './lti-initializer.component';
import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe';
import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe';
import { AccountService } from 'app/core/auth/account.service';

interface InstructorActionItem {
routerLink: string;
Expand Down Expand Up @@ -112,6 +113,7 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp
private quizExerciseService = inject(QuizExerciseService);
private complaintService = inject(ComplaintService);
private artemisMarkdown = inject(ArtemisMarkdownService);
private accountService = inject(AccountService); // TODO TW: This "feature" is only temporary for a paper.

readonly AssessmentType = AssessmentType;
readonly PlagiarismVerdict = PlagiarismVerdict;
Expand All @@ -129,6 +131,7 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp
readonly isCommunicationEnabled = isCommunicationEnabled;
readonly isMessagingEnabled = isMessagingEnabled;

isChatGptWrapper: boolean = false; // TODO TW: This "feature" is only temporary for a paper.
public learningPathMode = false;
public exerciseId: number;
public courseId: number;
Expand Down Expand Up @@ -222,6 +225,13 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp
handleNewExercise(newExerciseDetails: ExerciseDetailsType) {
this.exercise = newExerciseDetails.exercise;

// TODO TW: This "feature" is only temporary for a paper.
if (this.exercise.problemStatement?.includes(ICER_PAPER_FLAG)) {
this.accountService.identity().then((user) => {
this.isChatGptWrapper = user && user.id ? user.id % 3 == 0 : false;
});
}

this.filterUnfinishedResults(this.exercise.studentParticipations);
this.mergeResultsAndSubmissionsForParticipations();
this.isAfterAssessmentDueDate = !this.exercise.assessmentDueDate || dayjs().isAfter(this.exercise.assessmentDueDate);
Expand Down
1 change: 1 addition & 0 deletions src/main/webapp/i18n/de/exerciseChatbot.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"rateLimitExceeded": "Du hast die maximale Anzahl von Nachrichten, die du in einem {{ hours }}-Stunden-Zeitfenster an Iris senden kannst, erreicht. Bitte versuche es später erneut!"
},
"disclaimer": "Iris kann Fehler machen. Überprüfe wichtige Informationen.",
"disclaimerGPT": "ChatGPT kann Fehler machen. Überprüfe wichtige Informationen.",
"tutorFirstMessage": "Hallo, ich bin Iris! Ich kann dir bei deiner Programmieraufgabe helfen. Du kannst <a href='/about-iris' target='_blank'>hier</a> mehr über mich erfahren.",
"codeEditorFirstMessage": "Hallo, ich bin Iris! Ich kann dir bei der Bearbeitung von Programmieraufgaben helfen. Ich kann zum Beispiel: <ul><li>eine neue Aufgabe erstellen</li><li><a href='/' target='_blank'>eine Variante von einer bestehenden Aufgabe erstellen</a></li><li>eine bestehende Aufgabe neuen Anforderungen anpassen</li></ul> Du kannst <a href='/about-iris' target='_blank'>hier</a> mehr über mich erfahren.",
"rateLimitTooltip": "Dies ist die maximale Anzahl von Nachrichten, die du in einem {{ hours }}-Stunden-Zeitfenster an Iris senden kannst."
Expand Down
1 change: 1 addition & 0 deletions src/main/webapp/i18n/en/exerciseChatbot.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"rateLimitExceeded": "You have reached the maximum number of messages you can send to Iris in a {{ hours }} hour window. Please try again later!"
},
"disclaimer": "Iris can make mistakes. Consider checking important information.",
"disclaimerGPT": "ChatGPT can make mistakes. Consider checking important information.",
"tutorFirstMessage": "Hi, I'm Iris! I can help you with your programming exercise. You can learn more about me <a href='/about-iris' target='_blank'>here</a>.",
"codeEditorFirstMessage": "Hi, I'm Iris! I can help you to create programming exercises. For example: <ul><li>create a brand-new exercise</li><li>{link:create a variant of another existing exercise}</li><li>adapt an existing exercise to new requirements</li></ul> You can learn more about me <a href='/about-iris' target='_blank'>here</a>.",
"rateLimitTooltip": "This is the maximum number of messages you can send to Iris in a {{ hours }} hour window."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IrisChatbotWidgetComponent } from 'app/iris/exercise-chatbot/widget/chatbot-widget.component';
import { IrisChatService } from 'app/iris/iris-chat.service';
import { MockComponent, MockProvider } from 'ng-mocks';
import { MatDialog } from '@angular/material/dialog';
import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { of } from 'rxjs';
import { By } from '@angular/platform-browser';
Expand All @@ -16,7 +16,12 @@ describe('IrisChatbotWidgetComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [IrisChatbotWidgetComponent, MockComponent(IrisBaseChatbotComponent)],
providers: [MockProvider(IrisChatService), { provide: MatDialog, useValue: { closeAll: jest.fn() } }, { provide: Router, useValue: { events: of() } }],
providers: [
MockProvider(IrisChatService),
{ provide: MatDialog, useValue: { closeAll: jest.fn() } },
{ provide: Router, useValue: { events: of() } },
{ provide: MAT_DIALOG_DATA, useValue: { isChatGptWrapper: false } },
],
}).compileComponents();

fixture = TestBed.createComponent(IrisChatbotWidgetComponent);
Expand Down
Loading