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

Solution Consent #354

Merged
merged 16 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions frontend/src/app/assignment/model/solution.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export class AuthorInfo {
name: string;
studentId: string;
email: string;
github: string;
name?: string;
studentId?: string;
email?: string;
github?: string;
}

export const authorInfoProperties = [
Expand All @@ -12,6 +12,13 @@ export const authorInfoProperties = [
['GitHub Username', 'github'],
] as const;

export interface Consent {
demonstration?: boolean;
scientific?: boolean;
'3P'?: boolean;
}
export const consentKeys = ['demonstration', 'scientific', '3P'] as const;

export default class Solution {
_id?: string;
token?: string;
Expand All @@ -21,6 +28,7 @@ export default class Solution {
author: AuthorInfo;
solution: string;
commit?: string;
consent?: Consent;

timestamp?: Date;
points?: number;
Expand All @@ -35,6 +43,8 @@ export type ImportSolution = Pick<Solution,
>;

export interface EstimatedCosts {
solutions: number;
files: number;
tokens: number;
estimatedCost: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ import {assignmentChildRoutes} from './assignment-routes';
import {AssignmentComponent} from './assignment/assignment.component';
import {DeleteModalComponent} from './delete-modal/delete-modal.component';
import {ImportModalComponent} from './import-modal/import-modal.component';
import {ImportGithubComponent} from "./import-github/import-github.component";
import {ImportFilesComponent} from "./import-files/import-files.component";
import {ImportEmbeddingsComponent} from "./import-embeddings/import-embeddings.component";
import {ImportMossComponent} from "./import-moss/import-moss.component";
import {ImportConsentComponent} from "./import-consent/import-consent.component";

export const importChildren = [
{path: 'github', component: ImportGithubComponent, data: {title: 'GitHub'}},
{path: 'files', component: ImportFilesComponent, data: {title: 'Files'}},
{path: 'embeddings', component: ImportEmbeddingsComponent, data: {title: 'Embeddings'}},
{path: 'moss', component: ImportMossComponent, data: {title: 'MOSS'}},
{path: 'consent', component: ImportConsentComponent, data: {title: 'Consent'}},
];

const routes: Routes = [
{
Expand All @@ -14,7 +27,15 @@ const routes: Routes = [
children: [
...assignmentChildRoutes,
{path: 'token', component: TokenModalComponent, data: {title: 'Authorization Required'}},
{path: 'import', component: ImportModalComponent, data: {title: 'Import Solutions'}},
{
path: 'import',
component: ImportModalComponent,
data: {title: 'Import'},
children: [
...importChildren,
{path: '', redirectTo: 'github', pathMatch: 'full'},
],
},
{path: 'delete', component: DeleteModalComponent, data: {title: 'Delete Assignment'}},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ import {StatisticsBlockComponent} from './statistics-block/statistics-block.comp
import {StatisticsComponent} from './statistics/statistics.component';
import {SubmitModalComponent} from './submit-modal/submit-modal.component';
import {AssignmentTasksComponent} from './tasks/tasks.component';
import { ImportGithubComponent } from './import-github/import-github.component';
import { ImportFilesComponent } from './import-files/import-files.component';
import { ImportMossComponent } from './import-moss/import-moss.component';
import { ImportEmbeddingsComponent } from './import-embeddings/import-embeddings.component';
import { ImportConsentComponent } from './import-consent/import-consent.component';


@NgModule({
Expand All @@ -39,6 +44,11 @@ import {AssignmentTasksComponent} from './tasks/tasks.component';
ImportModalComponent,
StatisticsBlockComponent,
DeleteModalComponent,
ImportGithubComponent,
ImportFilesComponent,
ImportMossComponent,
ImportEmbeddingsComponent,
ImportConsentComponent,
],
imports: [
CommonModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<p>
Import consent flags by pasting tab-, space- or comma-separated values below.
</p>
<details>
<summary>More Info</summary>
<p>
The first line should specify the column names (case insensitive), for example:
</p>
<p>
<code>github,demonstration,scientific,3p</code>
</p>
<p>
Instead of or in addition to <code>github</code>,
you can also use <code>name</code>, <code>studentid</code> or <code>email</code>,
but at least one of must be specified.
</p>
<p>
The columns <code>demonstration</code>, <code>scientific</code> and <code>3p</code> are optional
and accept boolean values like <code>true</code> or <code>false</code> (case-insensitive).
</p>
</details>
<textarea class="form-control" rows="10" [(ngModel)]="consentText"></textarea>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {Component} from '@angular/core';
import {Observable} from "rxjs";
import Solution, {
AuthorInfo,
authorInfoProperties,
Consent,
consentKeys,
ImportSolution
} from "../../../model/solution";
import {map} from "rxjs/operators";
import {SolutionService} from "../../../services/solution.service";
import {ActivatedRoute} from "@angular/router";

@Component({
selector: 'app-import-consent',
templateUrl: './import-consent.component.html',
styleUrls: ['./import-consent.component.scss']
})
export class ImportConsentComponent {
consentText = '';

constructor(
private solutionService: SolutionService,
private route: ActivatedRoute,
) {
}

import(): Observable<ImportSolution[]> {
const assignment = this.route.snapshot.params.aid;
const lines = this.consentText.split('\n');
const splitter = /[\s,;]/;
const columns = lines[0].split(splitter);
const updates: Partial<Solution>[] = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(splitter);
const author: AuthorInfo = {};
const consent: Consent = {};
for (let j = 0; j < columns.length; j++) {
const column = columns[j].toLowerCase();
const value = values[j];
if (authorInfoProperties.find(([, key]) => key === column)) {
author[column as keyof AuthorInfo] = value;
}
if (consentKeys.includes(column as keyof Consent)) {
consent[column] = Boolean(value.toLowerCase());
}
}
if (Object.keys(author).length) {
updates.push({author, consent});
}
}
return this.solutionService.updateMany(assignment, updates).pipe(map(results => results.filter(s => s)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<p>
After importing from GitHub or Files, you can import embeddings.
</p>
<p>
Here is an estimate of the token costs.
Actual costs may be <strong>lower</strong> if the embeddings are already cached,
or <strong>slightly higher</strong> due to some tokens like filenames being added to improve results.
Charges will be applied to your OpenAI account.
</p>
<p>
Please review the results below and click <b>Import</b> to confirm.
</p>
<div class="row" *ngIf="estimatedCosts">
<app-statistic-value class="col" label="Solutions" [value]="estimatedCosts.solutions" [standalone]="true"></app-statistic-value>
<app-statistic-value class="col" label="Files" [value]="estimatedCosts.files" [standalone]="true"></app-statistic-value>
<app-statistic-value class="col" label="Tokens" [value]="estimatedCosts.tokens" [standalone]="true"></app-statistic-value>
<app-statistic-value class="col" label="Estimated Cost" [value]="estimatedCosts.estimatedCost | currency:'USD':true:'0.7'" [standalone]="true"></app-statistic-value>
</div>
<ng-container *ngIf="finalCosts">
<hr/>
<p>
The import has been completed.
The following costs have been applied to your OpenAI account.
</p>
<div class="row">
<app-statistic-value class="col" label="Tokens" [value]="finalCosts.tokens" [standalone]="true"></app-statistic-value>
<app-statistic-value class="col" label="Cost" [value]="finalCosts.estimatedCost | currency:'USD':true:'0.7'" [standalone]="true"></app-statistic-value>
</div>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {Component, OnInit} from '@angular/core';
import {EstimatedCosts} from "../../../model/solution";
import {SolutionService} from "../../../services/solution.service";
import {ActivatedRoute} from "@angular/router";
import {tap} from "rxjs/operators";

@Component({
selector: 'app-import-embeddings',
templateUrl: './import-embeddings.component.html',
styleUrls: ['./import-embeddings.component.scss']
})
export class ImportEmbeddingsComponent implements OnInit {
estimatedCosts?: EstimatedCosts;
finalCosts?: EstimatedCosts;

constructor(
private solutionService: SolutionService,
private route: ActivatedRoute,
) {
}

ngOnInit() {
this.solutionService.importEmbeddings(this.route.snapshot.params.aid, true).subscribe(costs => this.estimatedCosts = costs);
}

import() {
const assignmentId = this.route.snapshot.params.aid;
return this.solutionService.importEmbeddings(assignmentId).pipe(
tap(result => this.finalCosts = result),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<p>
Select multiple zip or jar files from your disk to upload.
The file name may have any of the following formats:
</p>
<ul>
<li>
Digits, e.g. <code>12345678.zip</code>, interpreted as Student IDs.
</li>
<li>
Prefixed with the same GitHub Classroom prefix as the assignment,
e.g. <code>assignment-1-Student.zip</code>,
which extracts the GitHub Username like <code>Student</code>.
</li>
<li>
Anything else, with the file name without extension becoming the student name.
</li>
</ul>
<input type="file" class="form-control" id="files" multiple accept="application/zip" #input
(change)="setFiles(input.files!)">
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {Component} from '@angular/core';
import {SolutionService} from "../../../services/solution.service";
import {ActivatedRoute} from "@angular/router";

@Component({
selector: 'app-import-files',
templateUrl: './import-files.component.html',
styleUrls: ['./import-files.component.scss']
})
export class ImportFilesComponent {
files: File[] = [];

constructor(
private solutionService: SolutionService,
private route: ActivatedRoute,
) {
}

setFiles(files: FileList) {
this.files = Array.from(files);
}

import() {
const assignmentId = this.route.snapshot.params.aid;
return this.solutionService.import(assignmentId, this.files);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<p>
Imports solutions from GitHub using the organisation and prefix configured in the
<a routerLink="../edit/classroom">assignment settings</a>.
No further configuration is required.
</p>
<details #previewDetails (toggle)="previewDetails.open && previewGitHubImport()">
<summary>Select Students</summary>
<ul class="list-unstyled">
<li *ngFor="let solution of previewSolutions" class="form-check">
<input class="form-check-input" type="checkbox" [id]="'check-' + solution.author.github"
[(ngModel)]="checkedUsernames[solution.author.github!]">
<label class="form-check-label" [for]="'check-' + solution.author.github">
{{ solution.author.github }}
<span class="text-muted"> &bull; {{ solution.timestamp | date:'short' }}</span>
</label>
</li>
</ul>
</details>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {Component} from '@angular/core';
import {ImportSolution} from "../../../model/solution";
import {SolutionService} from "../../../services/solution.service";
import {ActivatedRoute} from "@angular/router";

@Component({
selector: 'app-import-github',
templateUrl: './import-github.component.html',
styleUrls: ['./import-github.component.scss']
})
export class ImportGithubComponent {
checkedUsernames: Partial<Record<string, boolean>> = {};
previewSolutions: ImportSolution[];

constructor(
private solutionService: SolutionService,
private route: ActivatedRoute,
) {
}

previewGitHubImport() {
this.solutionService.previewImport(this.route.snapshot.params.aid).subscribe(solutions => this.previewSolutions = solutions);
}

import() {
const assignmentId = this.route.snapshot.params.aid;
const usernames = Object.keys(this.checkedUsernames).filter(username => this.checkedUsernames[username]);
return this.solutionService.import(assignmentId, undefined, usernames);
}
}
Loading