Skip to content

Commit bb4942e

Browse files
authored
Merge pull request #19 from ls1intum/dev
Release 0.2.6: Practice Mode
2 parents d6142ed + 661dead commit bb4942e

9 files changed

Lines changed: 307 additions & 29 deletions

File tree

iris-thaumantias/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to the Artemis VS Code extension will be documented in this file.
44

5+
## [0.2.6] - 2025-11-21
6+
7+
### Added
8+
9+
- **Practice Mode**: Added support for starting practice runs on programming exercises after the due date, including automatic repository detection and safety warnings.
10+
11+
### Fixed
12+
13+
- **Recently Cloned Notice**: Fixed an issue where the "Recently cloned" notice would persist even after the repository was successfully opened.
14+
515
## [0.2.5] - 2025-11-07
616

717
### Added

iris-thaumantias/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "iris-thaumantias",
33
"displayName": "Artemis - TUM",
44
"description": "Artemis brings interactive learning to life with instant, individual feedback on programming exercises, quizzes, modeling tasks, and more. This VS Code extension integrates Iris, an LLM-based virtual assistant that supports students with instant answers about exercises, lectures, and learning performance. Iris provides context-aware, personalized assistance for programming exercises, encouraging independent problem-solving through subtle hints and guiding questions instead of giving full solutions.",
5-
"version": "0.2.5",
5+
"version": "0.2.6",
66
"publisher": "aet-tum",
77
"license": "MIT",
88
"icon": "media/artemis-blue.png",

iris-thaumantias/src/api/artemisApi.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,15 @@ export class ArtemisApiService {
207207
return response.json();
208208
}
209209

210+
// Start practice participation in an exercise
211+
async startPracticeParticipation(exerciseId: number): Promise<any> {
212+
const response = await this.makeRequest(
213+
`/api/exercise/exercises/${exerciseId}/participations/practice`,
214+
{ method: 'POST' }
215+
);
216+
return response.json();
217+
}
218+
210219
// Authenticate user with username and password
211220
async authenticate(username: string, password: string, rememberMe: boolean = false): Promise<any> {
212221
const url = `${this.getServerUrl()}${CONFIG.API.ENDPOINTS.AUTHENTICATE}`;

iris-thaumantias/src/views/app/commands/repositoryCommands.ts

Lines changed: 103 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class RepositoryCommandModule {
3838
saveGitCredentials: this.handleSaveGitCredentials,
3939
saveGitIdentity: this.handleSaveGitIdentity,
4040
requestGitIdentity: this.handleRequestGitIdentity,
41+
startPractice: this.handleStartPractice,
4142
};
4243
}
4344

@@ -84,23 +85,67 @@ export class RepositoryCommandModule {
8485
for (const exercise of exercises) {
8586
const participations = exercise.studentParticipations || [];
8687

87-
if (participations.length > 0 && participations[0].repositoryUri) {
88-
const exerciseRepoUrl = normalizeUrl(participations[0].repositoryUri);
89-
90-
if (exerciseRepoUrl === normalizedRepoUrl) {
91-
matchedExercise = {
92-
id: exercise.id,
93-
title: exercise.title
94-
};
95-
break;
88+
// Check all participations (including practice ones)
89+
for (const participation of participations) {
90+
if (participation.repositoryUri) {
91+
const exerciseRepoUrl = normalizeUrl(participation.repositoryUri);
92+
93+
if (exerciseRepoUrl === normalizedRepoUrl) {
94+
matchedExercise = {
95+
id: exercise.id,
96+
title: exercise.title
97+
};
98+
break;
99+
}
96100
}
97101
}
102+
103+
if (matchedExercise) {
104+
break;
105+
}
98106
}
99107

100108
if (matchedExercise) {
101109
break;
102110
}
103111
}
112+
113+
// Fallback: If no exact match found, check if it's a practice repo that matches a graded repo
114+
if (!matchedExercise && normalizedRepoUrl.includes('-practice-')) {
115+
// Try to construct the graded repo URL by removing '-practice'
116+
// Example: .../slug-practice-user -> .../slug-user
117+
const potentialGradedUrl = normalizedRepoUrl.replace('-practice-', '-');
118+
119+
for (const courseData of coursesData.courses) {
120+
const exercises = courseData?.course?.exercises || courseData?.exercises || [];
121+
122+
for (const exercise of exercises) {
123+
const participations = exercise.studentParticipations || [];
124+
125+
for (const participation of participations) {
126+
if (participation.repositoryUri) {
127+
const exerciseRepoUrl = normalizeUrl(participation.repositoryUri);
128+
129+
if (exerciseRepoUrl === potentialGradedUrl) {
130+
matchedExercise = {
131+
id: exercise.id,
132+
title: exercise.title
133+
};
134+
break;
135+
}
136+
}
137+
}
138+
139+
if (matchedExercise) {
140+
break;
141+
}
142+
}
143+
144+
if (matchedExercise) {
145+
break;
146+
}
147+
}
148+
}
104149
}
105150

106151
this.context.sendMessage({
@@ -156,6 +201,7 @@ export class RepositoryCommandModule {
156201
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
157202
let isConnected = false;
158203
let hasChanges = false;
204+
let isGradedRepo = false;
159205

160206
this.currentRepoContext = { expectedRepoUrl, exerciseId };
161207
this.currentWorkspacePath = workspaceFolder?.uri.fsPath;
@@ -194,6 +240,29 @@ export class RepositoryCommandModule {
194240
console.warn('Failed to determine repository changes:', statusError);
195241
hasChanges = false;
196242
}
243+
} else {
244+
// Check if we are in the graded repository
245+
const coursesData = this.context.appStateManager.coursesData;
246+
if (coursesData?.courses) {
247+
for (const courseData of coursesData.courses) {
248+
const exercises = courseData?.course?.exercises || courseData?.exercises || [];
249+
const exercise = exercises.find((e: any) => e.id === exerciseId);
250+
251+
if (exercise) {
252+
const participations = exercise.studentParticipations || [];
253+
// Find graded participation (not testRun)
254+
const gradedParticipation = participations.find((p: any) => !p.testRun);
255+
256+
if (gradedParticipation?.repositoryUri) {
257+
const normalizedGraded = normalizeUrl(gradedParticipation.repositoryUri);
258+
if (normalizedCurrent === normalizedGraded) {
259+
isGradedRepo = true;
260+
}
261+
}
262+
break;
263+
}
264+
}
265+
}
197266
}
198267
} catch (gitError: any) {
199268
isConnected = false;
@@ -204,7 +273,8 @@ export class RepositoryCommandModule {
204273
this.context.sendMessage({
205274
command: 'updateRepoStatus',
206275
isConnected: isConnected,
207-
hasChanges: hasChanges
276+
hasChanges: hasChanges,
277+
isGradedRepo: isGradedRepo
208278
});
209279
} catch (error: any) {
210280
console.error('Check repository status error:', error);
@@ -947,4 +1017,27 @@ export class RepositoryCommandModule {
9471017
autoSaveEnabled: autoSave !== 'off'
9481018
});
9491019
}
1020+
1021+
private handleStartPractice = async (message: any): Promise<void> => {
1022+
const exerciseId: number = message.exerciseId;
1023+
const exerciseTitle: string = message.exerciseTitle;
1024+
1025+
try {
1026+
vscode.window.showInformationMessage('Starting practice mode...');
1027+
const participation = await this.context.artemisApi.startPracticeParticipation(exerciseId);
1028+
1029+
if (participation) {
1030+
vscode.window.showInformationMessage(
1031+
`Successfully started practice mode for "${exerciseTitle}". You can now clone the practice repository.`
1032+
);
1033+
1034+
await this.context.actionHandler.openExerciseDetails(exerciseId);
1035+
}
1036+
} catch (error) {
1037+
console.error('Failed to start practice participation:', error);
1038+
vscode.window.showErrorMessage(
1039+
`Failed to start practice mode for "${exerciseTitle}": ${error instanceof Error ? error.message : 'Unknown error'}`
1040+
);
1041+
}
1042+
};
9501043
}

iris-thaumantias/src/views/exerciseDetail/components/participationActionsComponent.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export interface ParticipationActionsData {
88
exerciseTitle?: string;
99
participationId?: number;
1010
uploadMessageIcon: string;
11+
isPracticeAvailable?: boolean;
12+
practiceParticipation?: any;
1113
}
1214

1315
/**
@@ -26,10 +28,12 @@ export class ParticipationActionsComponent {
2628
exerciseType,
2729
uploadMessageIcon,
2830
participationId,
31+
isPracticeAvailable,
32+
practiceParticipation,
2933
} = data;
3034

3135
const changeStatusHtml =
32-
hasParticipation && isProgrammingExercise
36+
(hasParticipation || practiceParticipation) && isProgrammingExercise
3337
? `
3438
<div class="changes-status" id="changesStatus" data-state="checking">
3539
<span class="changes-status-indicator"></span>
@@ -42,7 +46,9 @@ export class ParticipationActionsComponent {
4246
hasParticipation,
4347
isProgrammingExercise,
4448
changeStatusHtml,
45-
uploadMessageIcon
49+
uploadMessageIcon,
50+
isPracticeAvailable,
51+
practiceParticipation
4652
);
4753

4854
// Determine participation info based on exercise type
@@ -72,8 +78,22 @@ export class ParticipationActionsComponent {
7278
hasParticipation: boolean,
7379
isProgrammingExercise: boolean,
7480
changeStatusHtml: string,
75-
uploadMessageIcon: string
81+
uploadMessageIcon: string,
82+
isPracticeAvailable?: boolean,
83+
practiceParticipation?: any
7684
): string {
85+
if (isPracticeAvailable) {
86+
return this._generatePracticeAvailableActions();
87+
}
88+
89+
if (practiceParticipation) {
90+
return this._generateProgrammingExerciseActions(
91+
changeStatusHtml,
92+
uploadMessageIcon,
93+
true
94+
);
95+
}
96+
7797
if (hasParticipation) {
7898
if (isProgrammingExercise) {
7999
return this._generateProgrammingExerciseActions(
@@ -92,15 +112,37 @@ export class ParticipationActionsComponent {
92112
}
93113
}
94114

115+
/**
116+
* Generate actions for practice available state
117+
*/
118+
private static _generatePracticeAvailableActions(): string {
119+
return `
120+
<div class="participation-actions not-participated">
121+
<div class="action-button-row">
122+
<button class="participate-btn" onclick="startPractice()">Practice</button>
123+
<button class="participate-btn secondary" onclick="openExerciseInBrowser()">Open in browser</button>
124+
</div>
125+
</div>
126+
`;
127+
}
128+
95129
/**
96130
* Generate actions for participated programming exercises
97131
*/
98132
private static _generateProgrammingExerciseActions(
99133
changeStatusHtml: string,
100-
uploadMessageIcon: string
134+
uploadMessageIcon: string,
135+
isPractice: boolean = false
101136
): string {
137+
const practiceLabel = isPractice
138+
? `<div class="practice-mode-indicator">
139+
<span class="codicon codicon-beaker"></span> Practice Mode
140+
</div>`
141+
: '';
142+
102143
return `
103144
<div class="participation-actions">
145+
${practiceLabel}
104146
${changeStatusHtml}
105147
<div class="cloned-repo-notice" id="clonedRepoNotice" style="display: none;">
106148
<span id="clonedRepoMessage">Repository recently cloned.</span>

iris-thaumantias/src/views/exerciseDetail/components/repositoryStatusScripts.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,17 @@ export class RepositoryStatusScripts {
1717
return `
1818
// Repository status checking
1919
window.checkRepositoryStatus = function(showChecking = false) {
20-
const participation = exerciseData.exercise?.studentParticipations?.[0] || exerciseData.studentParticipations?.[0];
20+
const ex = exerciseData.exercise || exerciseData;
21+
const participations = ex.studentParticipations || [];
22+
23+
// Check for practice participation first
24+
let participation = participations.find(p => p.testRun);
25+
26+
// If no practice participation, use the first one (graded)
27+
if (!participation && participations.length > 0) {
28+
participation = participations[0];
29+
}
30+
2131
const repoUrl = participation?.repositoryUri;
2232
2333
if (!repoUrl) {

iris-thaumantias/src/views/exerciseDetail/exercise-detail.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@
685685
border-radius: 50%;
686686
background: var(--theme-border);
687687
transition: background 0.15s ease-in-out, transform 0.15s ease-in-out;
688+
flex-shrink: 0;
688689
}
689690

690691
.changes-status[data-state='dirty'] {
@@ -1459,3 +1460,36 @@
14591460
color: white;
14601461
border-color: var(--theme-success);
14611462
}
1463+
1464+
/* Practice Mode Styles */
1465+
.practice-mode-indicator {
1466+
display: flex;
1467+
align-items: center;
1468+
gap: 8px;
1469+
padding: 8px 12px;
1470+
background-color: var(--vscode-editorInfo-background, rgba(117, 190, 255, 0.1));
1471+
color: var(--vscode-editorInfo-foreground, #75beff);
1472+
border-radius: 4px;
1473+
margin-bottom: 12px;
1474+
font-weight: 600;
1475+
font-size: 13px;
1476+
}
1477+
1478+
.practice-mode-indicator .codicon {
1479+
font-size: 16px;
1480+
}
1481+
1482+
/* Wrong Repo Warning */
1483+
.changes-status[data-state="wrong-repo"] {
1484+
color: var(--vscode-editorWarning-foreground, #cca700);
1485+
font-weight: 600;
1486+
}
1487+
1488+
.changes-status[data-state="wrong-repo"] .changes-status-indicator {
1489+
background-color: var(--vscode-editorWarning-foreground, #cca700);
1490+
}
1491+
1492+
.repo-status-icon.warning {
1493+
color: var(--vscode-editorWarning-foreground, #cca700);
1494+
border-color: var(--vscode-editorWarning-foreground, #cca700);
1495+
}

0 commit comments

Comments
 (0)