From d72747117aabd37263f2ca130a4901dd01841794 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Wed, 8 Jan 2025 23:02:12 -0500 Subject: [PATCH 01/29] Created new isScoreRun property for CK projects; Added menu item and modal for creating SCORE Runs --- backend/src/api/projects.ts | 35 +++++++-- backend/src/models/Project.ts | 3 + frontend/src/app/app.module.ts | 2 + .../add-project-modal.component.html | 2 +- .../add-score-run-modal.component.html | 56 +++++++++++++++ .../add-score-run-modal.component.scss | 12 ++++ .../add-score-run-modal.component.spec.ts | 23 ++++++ .../add-score-run-modal.component.ts | 71 +++++++++++++++++++ .../dashboard/dashboard.component.html | 14 ++-- .../dashboard/dashboard.component.ts | 15 +++- .../join-project-modal.component.html | 2 +- frontend/src/app/models/project.ts | 1 + 12 files changed, 218 insertions(+), 18 deletions(-) create mode 100644 frontend/src/app/components/add-score-run-modal/add-score-run-modal.component.html create mode 100644 frontend/src/app/components/add-score-run-modal/add-score-run-modal.component.scss create mode 100644 frontend/src/app/components/add-score-run-modal/add-score-run-modal.component.spec.ts create mode 100644 frontend/src/app/components/add-score-run-modal/add-score-run-modal.component.ts diff --git a/backend/src/api/projects.ts b/backend/src/api/projects.ts index c670245e..e4b90422 100644 --- a/backend/src/api/projects.ts +++ b/backend/src/api/projects.ts @@ -24,13 +24,16 @@ router.post('/', async (req, res) => { return res.status(403).end('Unauthorized to create project.'); } + // Set isScoreRun to false if not provided + project.isScoreRun = project.isScoreRun || false; + let savedProject = await dalProject.create(project); if (project.personalBoardSetting.enabled) { const image = project.personalBoardSetting.bgImage; - const boardID = new mongo.ObjectId().toString(); - const board = await dalBoard.create({ + const personalBoardID = new mongo.ObjectId().toString(); + const personalBoard = await dalBoard.create({ projectID: project.projectID, - boardID: boardID, + boardID: personalBoardID, ownerID: user.userID, name: `${user.username}'s Personal Board`, scope: BoardScope.PROJECT_PERSONAL, @@ -38,14 +41,36 @@ router.post('/', async (req, res) => { permissions: getDefaultBoardPermissions(), bgImage: image, type: BoardType.BRAINSTORMING, - tags: getDefaultBoardTags(boardID), + tags: getDefaultBoardTags(personalBoardID), + initialZoom: 100, + upvoteLimit: 5, + visible: true, + defaultView: ViewType.CANVAS, + viewSettings: getAllViewsAllowed(), + }); + savedProject = await savedProject.updateOne({ boards: [personalBoard.boardID] }); + + // --- Create default shared board --- + const communityBoardID = new mongo.ObjectId().toString(); + const communityBoard = await dalBoard.create({ + projectID: savedProject.projectID, // Use the saved project's ID + boardID: communityBoardID, + ownerID: user.userID, + name: 'Demo Community Board', // Or any default name you prefer + scope: BoardScope.PROJECT_SHARED, + task: undefined, + permissions: getDefaultBoardPermissions(), + bgImage: undefined, + type: BoardType.BRAINSTORMING, // Or another default type + tags: getDefaultBoardTags(communityBoardID), initialZoom: 100, upvoteLimit: 5, visible: true, defaultView: ViewType.CANVAS, viewSettings: getAllViewsAllowed(), }); - savedProject = await savedProject.updateOne({ boards: [board.boardID] }); + // Add the board to the project's boards array + savedProject = await savedProject.updateOne({ $push: { boards: communityBoard.boardID } }); } res.status(200).json(savedProject); diff --git a/backend/src/models/Project.ts b/backend/src/models/Project.ts index 2e7b53ac..529f2d59 100644 --- a/backend/src/models/Project.ts +++ b/backend/src/models/Project.ts @@ -14,6 +14,9 @@ export class ProjectModel { @prop({ required: true }) public projectID!: string; + @prop({ required: true, default: false }) + public isScoreRun!: boolean; + @prop({ required: true }) public teacherIDs!: string[]; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 45054cbb..a656f715 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -64,6 +64,7 @@ import { CkBucketsComponent } from './components/ck-buckets/ck-buckets.component import { ToolbarMenuComponent } from './components/toolbar-menu/toolbar-menu.component'; import { ViewNavigationComponent } from './components/view-navigation/view-navigation.component'; import { MarkdownModule } from 'ngx-markdown'; +import { AddScoreRunModalComponent } from './components/add-score-run-modal/add-score-run-modal.component'; const config: SocketIoConfig = { url: environment.socketUrl, @@ -117,6 +118,7 @@ export function tokenGetter() { CkBucketsComponent, ToolbarMenuComponent, ViewNavigationComponent, + AddScoreRunModalComponent, ], imports: [ BrowserModule, diff --git a/frontend/src/app/components/add-project-modal/add-project-modal.component.html b/frontend/src/app/components/add-project-modal/add-project-modal.component.html index d87aec19..f0f86843 100644 --- a/frontend/src/app/components/add-project-modal/add-project-modal.component.html +++ b/frontend/src/app/components/add-project-modal/add-project-modal.component.html @@ -1,4 +1,4 @@ -

Project Configuration

+

Standalone CK Project Configuration

SCORE Run Configuration +
+ + SCORE Run Name + + + Project name is required. + + +
Membership Settings:
+
+ Create Personal Boards for Project Members + + + Restrict users from joining this project + +
+
+ + + Image Saved! + cancel + + + upload + Upload Default Background Image For Personal Boards + + +
+
+
+ + +
diff --git a/frontend/src/app/components/add-score-run-modal/add-score-run-modal.component.scss b/frontend/src/app/components/add-score-run-modal/add-score-run-modal.component.scss new file mode 100644 index 00000000..a1848810 --- /dev/null +++ b/frontend/src/app/components/add-score-run-modal/add-score-run-modal.component.scss @@ -0,0 +1,12 @@ +.cancel { + padding-left: 10px; + width: 15px; + height: 15px; + font-size: medium; + } + + .membership-checkbox-list { + display: flex; + flex-direction: column; + } + \ No newline at end of file diff --git a/frontend/src/app/components/add-score-run-modal/add-score-run-modal.component.spec.ts b/frontend/src/app/components/add-score-run-modal/add-score-run-modal.component.spec.ts new file mode 100644 index 00000000..dd184975 --- /dev/null +++ b/frontend/src/app/components/add-score-run-modal/add-score-run-modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddScoreRunModalComponent } from './add-score-run-modal.component'; + +describe('AddScoreRunModalComponent', () => { + let component: AddScoreRunModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AddScoreRunModalComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AddScoreRunModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/add-score-run-modal/add-score-run-modal.component.ts b/frontend/src/app/components/add-score-run-modal/add-score-run-modal.component.ts new file mode 100644 index 00000000..cbb4b170 --- /dev/null +++ b/frontend/src/app/components/add-score-run-modal/add-score-run-modal.component.ts @@ -0,0 +1,71 @@ + +import { Component, OnInit, Inject } from '@angular/core'; +import { + MatLegacyDialogRef as MatDialogRef, + MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, +} from '@angular/material/legacy-dialog'; +import { fabric } from 'fabric'; +import { PersonalBoardSetting } from 'src/app/models/project'; +// import { FileUploadService } from 'src/app/services/fileUpload.service'; +import { UserService } from 'src/app/services/user.service'; +import { FabricUtils, ImageSettings } from 'src/app/utils/FabricUtils'; +import { generateCode, generateUniqueID } from 'src/app/utils/Utils'; + +@Component({ + selector: 'app-add-score-run-modal', + templateUrl: './add-score-run-modal.component.html', + styleUrls: ['./add-score-run-modal.component.scss'] +}) +export class AddScoreRunModalComponent implements OnInit { + name = ''; + + personalBoardSetting: PersonalBoardSetting = { + enabled: false, + bgImage: null, + }; + membershipDisabledEditable = false; + + constructor( + public dialogRef: MatDialogRef, + public fabricUtils: FabricUtils, + public userService: UserService, + // public fileUploadService: FileUploadService, + @Inject(MAT_DIALOG_DATA) public data: any + ) {} + + ngOnInit(): void {} + + async compressFile() { + // const image = await this.fileUploadService.compressFile(); + // const url = await this.fileUploadService.upload(image); + // fabric.Image.fromURL(url, async (image) => { + // const imgSettings = this.fabricUtils.createImageSettings(image); + // this.personalBoardSetting.bgImage = { url, imgSettings }; + // }); + } + + removeBgImg() { + this.personalBoardSetting.bgImage = null; + } + + handleDialogSubmit() { + const projectID = generateUniqueID(); + this.data.createProject({ + projectID: projectID, + isScoreRun: true, + teacherIDs: [this.data.user.userID], + name: this.name, + members: [this.data.user.userID], + boards: [], + studentJoinCode: generateCode(5).toString(), + teacherJoinCode: generateCode(5).toString(), + personalBoardSetting: this.personalBoardSetting, + membershipDisabled: this.membershipDisabledEditable, + }); + this.dialogRef.close(); + } + + onNoClick(): void { + this.dialogRef.close(); + } +} diff --git a/frontend/src/app/components/dashboard/dashboard.component.html b/frontend/src/app/components/dashboard/dashboard.component.html index a535a68c..5e08258b 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.html +++ b/frontend/src/app/components/dashboard/dashboard.component.html @@ -18,15 +18,11 @@ add - - +
-

My CK Projects

+ +

My SCORE Runs

My CK Projects
+ +
+ +

My Standalone CK Projects

+
+ + +
+ + grid_view + {{ project.name }} + + + + + + +
+ +

+ 0 members + 1 member + {{ + project.members.length + ' members' + }} +

+

+ 0 boards + 1 board + {{ + project.boards.length + ' boards' + }} +

+
+
+
+
+
diff --git a/frontend/src/app/components/dashboard/dashboard.component.scss b/frontend/src/app/components/dashboard/dashboard.component.scss index c0611d0c..f0f3305c 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.scss +++ b/frontend/src/app/components/dashboard/dashboard.component.scss @@ -9,3 +9,7 @@ .heading { color: gray; } + +.spacer { + margin-top: 2rem; +} \ No newline at end of file From 2bb09b629ce0cc8cdf11b139ac8c3c56f8a98b5e Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Thu, 9 Jan 2025 00:20:33 -0500 Subject: [PATCH 03/29] Rename Boards to Activity Spaces on project dashboard; Add Script SCORE Run button to SCORE Run dashboard --- .../project-dashboard.component.html | 32 +++++++++++++------ .../project-dashboard.component.scss | 27 +++++++++++++++- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/components/project-dashboard/project-dashboard.component.html b/frontend/src/app/components/project-dashboard/project-dashboard.component.html index e4722f3d..aff0a12c 100644 --- a/frontend/src/app/components/project-dashboard/project-dashboard.component.html +++ b/frontend/src/app/components/project-dashboard/project-dashboard.component.html @@ -13,14 +13,14 @@ mat-icon-button [matMenuTriggerFor]="addMenu" *ngIf="user && user.role === Role.TEACHER" - matTooltip="Create Board" + matTooltip="Create Activity Space" > add @@ -81,8 +81,20 @@
-
-

Boards for {{ project.name }}

+
+
+ +
+
+
+

Activity Spaces for "{{ project.name }}"

@@ -92,7 +104,7 @@

Boards for {{ project.name }}

mat-button (click)="showSharedBoards = !showSharedBoards" > - Shared Project Boards ({{ sharedBoards.length }}) + Shared Activity Spaces ({{ sharedBoards.length }}) {{ showSharedBoards ? 'expand_less' : 'expand_more' }} @@ -136,7 +148,7 @@

Boards for {{ project.name }}

+ +
\ No newline at end of file diff --git a/frontend/src/app/components/create-activity-modal/create-activity-modal.component.scss b/frontend/src/app/components/create-activity-modal/create-activity-modal.component.scss new file mode 100644 index 00000000..a1848810 --- /dev/null +++ b/frontend/src/app/components/create-activity-modal/create-activity-modal.component.scss @@ -0,0 +1,12 @@ +.cancel { + padding-left: 10px; + width: 15px; + height: 15px; + font-size: medium; + } + + .membership-checkbox-list { + display: flex; + flex-direction: column; + } + \ No newline at end of file diff --git a/frontend/src/app/components/create-activity-modal/create-activity-modal.component.spec.ts b/frontend/src/app/components/create-activity-modal/create-activity-modal.component.spec.ts new file mode 100644 index 00000000..dc4b10bb --- /dev/null +++ b/frontend/src/app/components/create-activity-modal/create-activity-modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreateActivityModalComponent } from './create-activity-modal.component'; + +describe('CreateActivityModalComponent', () => { + let component: CreateActivityModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CreateActivityModalComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CreateActivityModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/create-activity-modal/create-activity-modal.component.ts b/frontend/src/app/components/create-activity-modal/create-activity-modal.component.ts new file mode 100644 index 00000000..1f06a0c5 --- /dev/null +++ b/frontend/src/app/components/create-activity-modal/create-activity-modal.component.ts @@ -0,0 +1,71 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { + MatLegacyDialogRef as MatDialogRef, + MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, +} from '@angular/material/legacy-dialog'; +import { Board } from 'src/app/models/board'; +import { Group } from 'src/app/models/group'; +import { generateUniqueID } from 'src/app/utils/Utils'; +import { BoardService } from 'src/app/services/board.service'; +import { GroupService } from 'src/app/services/group.service'; + +@Component({ + selector: 'app-create-activity-modal', + templateUrl: './create-activity-modal.component.html', + styleUrls: ['./create-activity-modal.component.scss'] +}) +export class CreateActivityModalComponent implements OnInit { + + activityName = ''; + boards: Board[] = []; // To store the boards for the project + selectedBoardID = ''; // To store the selected board ID + availableGroups: Group[] = []; + groups: Group[] = []; // To store the groups for the project + selectedGroupIDs: string[] = []; // To store the selected group IDs + + constructor( + public dialogRef: MatDialogRef, + private boardService: BoardService, + private groupService: GroupService, + @Inject(MAT_DIALOG_DATA) public data: any + ) { } + + ngOnInit(): void { + this.fetchBoards(); + this.fetchAvailableGroups(); + } + + async fetchBoards() { + try { + this.boards = await this.boardService.getByProject(this.data.project.projectID) || []; + } catch (error) { + // Handle error (e.g., display an error message) + console.error("Error fetching boards:", error); + } + } + + async fetchAvailableGroups() { + try { + this.availableGroups = await this.groupService.getByProjectId(this.data.project.projectID); + } catch (error) { + // Handle error (e.g., display an error message) + console.error("Error fetching groups:", error); + } + } + + onNoClick(): void { + this.dialogRef.close(); + } + + handleCreateActivity() { + const newActivity = { + activityID: generateUniqueID(), + name: this.activityName, + projectID: this.data.project.projectID, + boardID: this.selectedBoardID, + groupIDs: this.selectedGroupIDs + }; + + this.dialogRef.close(newActivity); // Close the dialog and return the new activity data + } +} \ No newline at end of file diff --git a/frontend/src/app/components/error/error.component.scss b/frontend/src/app/components/error/error.component.scss index cdd52156..1baefb07 100644 --- a/frontend/src/app/components/error/error.component.scss +++ b/frontend/src/app/components/error/error.component.scss @@ -127,6 +127,7 @@ body { .error__title { font-size: 10em; + margin-bottom: 0.5em; } .error__subtitle { diff --git a/frontend/src/app/components/project-dashboard/project-dashboard.component.html b/frontend/src/app/components/project-dashboard/project-dashboard.component.html index aff0a12c..804de668 100644 --- a/frontend/src/app/components/project-dashboard/project-dashboard.component.html +++ b/frontend/src/app/components/project-dashboard/project-dashboard.component.html @@ -88,12 +88,13 @@ mat-raised-button color="primary" class="script-score-run-button" + (click)="navigateToScoreAuthoring()" > Script SCORE Run
-
+

Activity Spaces for "{{ project.name }}"

diff --git a/frontend/src/app/components/project-dashboard/project-dashboard.component.ts b/frontend/src/app/components/project-dashboard/project-dashboard.component.ts index f97e881d..7108990d 100644 --- a/frontend/src/app/components/project-dashboard/project-dashboard.component.ts +++ b/frontend/src/app/components/project-dashboard/project-dashboard.component.ts @@ -148,6 +148,10 @@ export class ProjectDashboardComponent implements OnInit { ]); } + navigateToScoreAuthoring() { + this.router.navigate(['/score-authoring', this.project.projectID]); + } + async handleEditBoard(board: Board) { const boardID: string = board.boardID; this.dialog.open(ConfigurationModalComponent, { diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html new file mode 100644 index 00000000..15c8abdc --- /dev/null +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -0,0 +1,98 @@ +
+ + + SCORE Authoring - {{ project.name }} + + + + + + +
+

Activities

+
+
+ {{ activity.name }} +
+
+ +
+
+ + +
+
+

{{ selectedActivity.name }}

+
+
+ {{ resource.name }} +
+
+
+ + +
+

Available Resources

+
+
+ {{ resource.name }} +
+
+
+
+ +
+

Groups

+
+ {{ group.name }} +
+
+
+
+
+
diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.scss b/frontend/src/app/components/score-authoring/score-authoring.component.scss new file mode 100644 index 00000000..4110b3a0 --- /dev/null +++ b/frontend/src/app/components/score-authoring/score-authoring.component.scss @@ -0,0 +1,75 @@ + +.toolbar { + width: 100%; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + } + + .drawer-container { + position: absolute; + padding-top: 56px; + left: 0; + right: 0; + bottom: 0; + overflow: auto; + + @media (min-width: 600px) { + padding-top: 64px; + } + } + + .activities-container { + padding: 1rem; + } + + .activities-list { + // Add styles for the activities list as needed (e.g., padding, margin, etc.) + } + + .add-activity-button { + position: absolute; + bottom: 1rem; + right: 1rem; + transform: none; + } + + mat-sidenav { + width: 350px; + } + + .main-content { + display: flex; + height: 100%; + } + + .middle-pane { + flex: 1; + padding: 1rem; + border-right: 1px solid #ccc; + + h3 { + margin-bottom: 1rem; + } + } + + .activity-resources-list { + // Add styles for the drag and drop list as needed (e.g., min-height, etc.) + } + + .resources-pane { + width: 300px; + padding: 1rem; + } + + .available-resources-list { + // Add styles for the available resources list as needed (e.g., padding, margin, etc.) + } + + .groups-list { + flex: 0 0 200px; + padding: 1rem; + border-left: 1px solid #ccc; + } \ No newline at end of file diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.spec.ts b/frontend/src/app/components/score-authoring/score-authoring.component.spec.ts new file mode 100644 index 00000000..e1d8c5bb --- /dev/null +++ b/frontend/src/app/components/score-authoring/score-authoring.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ScoreAuthoringComponent } from './score-authoring.component'; + +describe('ScoreAuthoringComponent', () => { + let component: ScoreAuthoringComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ScoreAuthoringComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ScoreAuthoringComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts new file mode 100644 index 00000000..376b1842 --- /dev/null +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -0,0 +1,152 @@ +// score-authoring.component.ts + +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Project } from 'src/app/models/project'; +import { AuthUser } from 'src/app/models/user'; +import { Group } from 'src/app/models/group'; +import { GroupService } from 'src/app/services/group.service'; +import { SnackbarService } from 'src/app/services/snackbar.service'; +import { UserService } from 'src/app/services/user.service'; +import { SocketService } from 'src/app/services/socket.service'; +import { ProjectService } from 'src/app/services/project.service'; +import { Subscription } from 'rxjs'; +import { CreateActivityModalComponent } from '../create-activity-modal/create-activity-modal.component'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { HttpClient } from '@angular/common/http'; +import { Activity } from 'src/app/models/activity'; + + +interface Resource { + resourceID: string; + name: string; + // ... other properties as needed +} + +@Component({ + selector: 'app-score-authoring', + templateUrl: './score-authoring.component.html', + styleUrls: ['./score-authoring.component.scss'] +}) +export class ScoreAuthoringComponent implements OnInit, OnDestroy { + + project: Project; + user: AuthUser; + listeners: Subscription[] = []; + + activities: Activity[] = []; + selectedActivity: Activity | null = null; + selectedActivityResources: Resource[] = []; + selectedActivityGroups: Group[] = []; + availableResources: Resource[] = []; + + showResourcesPane = false; + + constructor( + public userService: UserService, + public snackbarService: SnackbarService, + public socketService: SocketService, + private projectService: ProjectService, + private groupService: GroupService, + private router: Router, + private activatedRoute: ActivatedRoute, + public dialog: MatDialog, + private http: HttpClient + ) { } + + ngOnInit(): void { + this.user = this.userService.user!; + this.loadScoreAuthoringData(); + } + + async loadScoreAuthoringData(): Promise { + const projectID = this.activatedRoute.snapshot.paramMap.get('projectID'); + if (!projectID) { + this.router.navigate(['error']); + return; + } + + try { + this.project = await this.projectService.get(projectID); + } catch (error) { + this.snackbarService.queueSnackbar("Error loading project data."); + console.error("Error loading project data:", error); + this.router.navigate(['error']); + return; + } + + try { + // this.activities = await this.http.get(`/api/activities/project/${projectID}`).toPromise() || []; + } catch (error) { + this.snackbarService.queueSnackbar("Error loading activities."); + console.error("Error loading activities:", error); + this.router.navigate(['error']); + return; + } + } + + selectActivity(activity: Activity) { + this.selectedActivity = activity; + this.fetchActivityResources(activity.activityID); + this.fetchActivityGroups(activity.groupIDs); + } + + async fetchActivityResources(activityID: string) { + try { + // ... (Implement logic to fetch resources for the activity) ... + this.selectedActivityResources = await this.http.get(`/api/resources/activity/${activityID}`).toPromise() || []; + } catch (error) { + this.snackbarService.queueSnackbar("Error fetching activity resources."); + console.error("Error fetching activity resources:", error); + } + } + + async fetchActivityGroups(groupIDs: string[]) { + try { + this.selectedActivityGroups = await this.groupService.getMultipleBy(groupIDs); // Example using GroupService + } catch (error) { + this.snackbarService.queueSnackbar("Error fetching activity groups."); + console.error("Error fetching activity groups:", error); + } + } + + openCreateActivityModal() { + const dialogRef = this.dialog.open(CreateActivityModalComponent, { + data: { + project: this.project, + createActivity: this.createActivity + } + }); + } + + createActivity = async (activityData: Activity) => { + try { + const response = await this.http.post('/api/activities', activityData).toPromise(); + const newActivity: Activity = response as Activity; + + this.activities.push(newActivity); + this.selectActivity(newActivity); + } catch (error) { + this.snackbarService.queueSnackbar("Error creating activity."); + console.error("Error creating activity:", error); + } + }; + + addResourceToActivity(resource: Resource) { + this.showResourcesPane = false; + } + + drop(event: CdkDragDrop) { + moveItemInArray(this.selectedActivityResources, event.previousIndex, event.currentIndex); + } + + ngOnDestroy(): void { + this.listeners.map(l => l.unsubscribe()); + } + + signOut(): void { + this.userService.logout(); + this.router.navigate(['login']); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/toolbar-menu/toolbar-menu.component.html b/frontend/src/app/components/toolbar-menu/toolbar-menu.component.html index c415a7e0..ad372423 100644 --- a/frontend/src/app/components/toolbar-menu/toolbar-menu.component.html +++ b/frontend/src/app/components/toolbar-menu/toolbar-menu.component.html @@ -18,7 +18,7 @@ assignment_ind View All Todo Lists - diff --git a/frontend/src/app/models/activity.ts b/frontend/src/app/models/activity.ts new file mode 100644 index 00000000..ff5c4073 --- /dev/null +++ b/frontend/src/app/models/activity.ts @@ -0,0 +1,7 @@ +export interface Activity { + activityID: string; + name: string; + projectID: string; + boardID: string; + groupIDs: string[]; + } \ No newline at end of file diff --git a/frontend/src/app/services/group.service.ts b/frontend/src/app/services/group.service.ts index fef09524..e5ae4c14 100644 --- a/frontend/src/app/services/group.service.ts +++ b/frontend/src/app/services/group.service.ts @@ -44,6 +44,16 @@ export class GroupService { .then((groups) => groups ?? []); // Default to an empty array } + async getMultipleBy(groupIDs: string[]): Promise { + try { + const groups = await this.http.post('/api/groups/multiple', { groupIDs }).toPromise(); + return groups ?? []; // Default to an empty array if null or undefined + } catch (error) { + console.error("Error fetching multiple groups:", error); + return []; // Return an empty array in case of an error + } + } + create(group: Group): Promise { return this.http .post('groups/', group) From 896c2ff7639c2d3f0f4cc1a212b51cb05769ec1e Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Fri, 10 Jan 2025 19:10:33 -0500 Subject: [PATCH 05/29] Fixed adding and fetching activities from DB --- backend/src/server.ts | 3 +++ .../create-workflow-modal.component.ts | 2 +- .../score-authoring/score-authoring.component.ts | 12 +++++++++--- frontend/src/app/services/group.service.ts | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/backend/src/server.ts b/backend/src/server.ts index dd13122d..313ffe07 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -23,6 +23,8 @@ import { isAuthenticated } from './utils/auth'; import RedisClient from './utils/redis'; import aiRouter from './api/ai'; import chatHistoryRouter from './api/chatHistory'; +import activitiesRouter from './api/activities'; + dotenv.config(); const port = process.env.PORT || 8001; @@ -70,6 +72,7 @@ app.use('/api/todoItems', isAuthenticated, todoItems); app.use('/api/learner', isAuthenticated, learner); app.use('/api/ai', isAuthenticated, aiRouter); app.use('/api/chat-history', chatHistoryRouter); +app.use('/api/activities', activitiesRouter); app.get('*', (req, res) => { res.sendFile(path.join(staticFilesPath, 'index.html')); diff --git a/frontend/src/app/components/create-workflow-modal/create-workflow-modal.component.ts b/frontend/src/app/components/create-workflow-modal/create-workflow-modal.component.ts index 46288ca0..88e934e4 100644 --- a/frontend/src/app/components/create-workflow-modal/create-workflow-modal.component.ts +++ b/frontend/src/app/components/create-workflow-modal/create-workflow-modal.component.ts @@ -511,7 +511,7 @@ export class CreateWorkflowModalComponent implements OnInit, OnDestroy { userId: this.user.userID }; - this.http.post('chat-history', data, { + this.http.post('chat-history/', data, { responseType: 'blob' }).subscribe( (response) => { diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts index 376b1842..c50143e5 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.ts +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -21,7 +21,6 @@ import { Activity } from 'src/app/models/activity'; interface Resource { resourceID: string; name: string; - // ... other properties as needed } @Component({ @@ -77,7 +76,7 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } try { - // this.activities = await this.http.get(`/api/activities/project/${projectID}`).toPromise() || []; + this.activities = await this.http.get(`activities/project/${projectID}`).toPromise() || []; } catch (error) { this.snackbarService.queueSnackbar("Error loading activities."); console.error("Error loading activities:", error); @@ -118,15 +117,22 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { createActivity: this.createActivity } }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.createActivity(result); + } + }); } createActivity = async (activityData: Activity) => { try { - const response = await this.http.post('/api/activities', activityData).toPromise(); + const response = await this.http.post('activities/', activityData).toPromise(); const newActivity: Activity = response as Activity; this.activities.push(newActivity); this.selectActivity(newActivity); + console.log("New Activity created.") } catch (error) { this.snackbarService.queueSnackbar("Error creating activity."); console.error("Error creating activity:", error); diff --git a/frontend/src/app/services/group.service.ts b/frontend/src/app/services/group.service.ts index e5ae4c14..f7d51e35 100644 --- a/frontend/src/app/services/group.service.ts +++ b/frontend/src/app/services/group.service.ts @@ -46,7 +46,7 @@ export class GroupService { async getMultipleBy(groupIDs: string[]): Promise { try { - const groups = await this.http.post('/api/groups/multiple', { groupIDs }).toPromise(); + const groups = await this.http.post('groups/multiple', { groupIDs }).toPromise(); return groups ?? []; // Default to an empty array if null or undefined } catch (error) { console.error("Error fetching multiple groups:", error); From 5dd1b94327a078f0f541d766bd3dc25fc1e6089a Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Fri, 10 Jan 2025 22:26:50 -0500 Subject: [PATCH 06/29] Add resources as the means of assigning views (e.g., canvas, workspace, etc.) to activities --- backend/src/api/resources.ts | 21 +++ backend/src/models/Resource.ts | 32 ++++ backend/src/repository/dalResource.ts | 48 ++++++ backend/src/server.ts | 2 + .../score-authoring.component.html | 140 ++++++++---------- .../score-authoring.component.scss | 74 ++++----- .../score-authoring.component.ts | 13 +- 7 files changed, 219 insertions(+), 111 deletions(-) create mode 100644 backend/src/api/resources.ts create mode 100644 backend/src/models/Resource.ts create mode 100644 backend/src/repository/dalResource.ts diff --git a/backend/src/api/resources.ts b/backend/src/api/resources.ts new file mode 100644 index 00000000..6bbaf0e1 --- /dev/null +++ b/backend/src/api/resources.ts @@ -0,0 +1,21 @@ +// backend/src/api/resources.ts + +import express from 'express'; +import dalResource from '../repository/dalResource'; + +const router = express.Router(); + +router.get('/activity/:activityID', async (req, res) => { + try { + const activityID = req.params.activityID; + const resources = await dalResource.getByActivity(activityID); + res.status(200).json(resources); + } catch (error) { + console.error("Error fetching resources:", error); + res.status(500).json({ error: 'Failed to fetch resources.' }); + } +}); + +// ... add other routes for creating, updating, deleting, reordering resources ... + +export default router; \ No newline at end of file diff --git a/backend/src/models/Resource.ts b/backend/src/models/Resource.ts new file mode 100644 index 00000000..579e086b --- /dev/null +++ b/backend/src/models/Resource.ts @@ -0,0 +1,32 @@ +// backend/src/models/Resource.ts + +import { prop, getModelForClass, modelOptions } from '@typegoose/typegoose'; + +@modelOptions({ schemaOptions: { collection: 'resources', timestamps: true } }) +export class ResourceModel { + @prop({ required: true }) + public resourceID!: string; + + @prop({ required: true }) + public activityID!: string; + + @prop({ required: true }) + public order!: number; + + @prop({ required: true, default: false }) + public canvas!: boolean; + + @prop({ required: true, default: false }) + public bucketView!: boolean; + + @prop({ required: true, default: false }) + public workspace!: boolean; + + @prop({ required: true, default: false }) + public monitor!: boolean; + + // ... add more properties for future resource types as needed ... +} + +const Resource = getModelForClass(ResourceModel); +export default Resource; \ No newline at end of file diff --git a/backend/src/repository/dalResource.ts b/backend/src/repository/dalResource.ts new file mode 100644 index 00000000..2f7bfd0e --- /dev/null +++ b/backend/src/repository/dalResource.ts @@ -0,0 +1,48 @@ +// backend/src/repository/dalResource.ts + +import Resource, { ResourceModel } from '../models/Resource'; + +const dalResource = { + + getByActivity: async (activityID: string): Promise => { + try { + const resources = await Resource.find({ activityID }).sort({ order: 1 }); + return resources; + } catch (error) { + console.error("Error fetching resources by activity:", error); // Log the error + return undefined; // Return undefined in case of an error + } + }, + + create: async (resource: ResourceModel): Promise => { + try { + // Ensure all binary values are set to false (if not already provided) + const newResource = { + ...resource, + canvas: resource.canvas || false, + bucketView: resource.bucketView || false, + workspace: resource.workspace || false, + monitor: resource.monitor || false, + // ... set other binary values to false as needed ... + }; + + return await Resource.create(newResource); + } catch (error) { + console.error("Error creating resource:", error); + return undefined; + } + }, + + update: async (id: string, resource: Partial): Promise => { + try { + return await Resource.findOneAndUpdate({ resourceID: id }, resource, { new: true }); + } catch (error) { + console.error("Error updating resource:", error); // Log the error + return undefined; + } + }, + + // ... add other methods for deleting, reordering, etc. as needed ... +}; + +export default dalResource; \ No newline at end of file diff --git a/backend/src/server.ts b/backend/src/server.ts index 313ffe07..e22e99fc 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -24,6 +24,7 @@ import RedisClient from './utils/redis'; import aiRouter from './api/ai'; import chatHistoryRouter from './api/chatHistory'; import activitiesRouter from './api/activities'; +import resourcesRouter from './api/resources'; dotenv.config(); @@ -73,6 +74,7 @@ app.use('/api/learner', isAuthenticated, learner); app.use('/api/ai', isAuthenticated, aiRouter); app.use('/api/chat-history', chatHistoryRouter); app.use('/api/activities', activitiesRouter); +app.use('/api/resources', resourcesRouter); app.get('*', (req, res) => { res.sendFile(path.join(staticFilesPath, 'index.html')); diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index 15c8abdc..46766943 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -1,98 +1,84 @@
- - - SCORE Authoring - {{ project.name }} - - - - - - -
+ + SCORE Authoring - {{ project.name }} + + + + +
+ +

Activities

-
-
+
- {{ activity.name }} +
+ {{ activity.name }} +
+ + +
+
-
- - - -
+ +

{{ selectedActivity.name }}

-
+
{{ resource.name }}
- - -
-

Available Resources

-
-
- {{ resource.name }} + +
+ +
+

Available Resources

+
+
+ {{ resource.name }} +
-
- - -
-

Groups

-
- {{ group.name }} + + +
+

Groups

+
+ {{ group.name }} +
-
- - -
+ +
+ +
+
\ No newline at end of file diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.scss b/frontend/src/app/components/score-authoring/score-authoring.component.scss index 4110b3a0..f7e46e65 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.scss +++ b/frontend/src/app/components/score-authoring/score-authoring.component.scss @@ -1,3 +1,4 @@ +// score-authoring.component.scss .toolbar { width: 100%; @@ -8,68 +9,75 @@ z-index: 999; } - .drawer-container { - position: absolute; - padding-top: 56px; - left: 0; - right: 0; - bottom: 0; - overflow: auto; - - @media (min-width: 600px) { - padding-top: 64px; - } + .main-content { + display: flex; + height: calc(100vh - 64px); + margin-top: 64px; } - .activities-container { + .activities-pane { + width: 250px; padding: 1rem; + border-right: 1px solid #ccc; } .activities-list { - // Add styles for the activities list as needed (e.g., padding, margin, etc.) + div.activity-item { + display: flex; + align-items: center; + cursor: pointer; + padding: 0.5rem; + margin-bottom: 0.5rem; + + .activity-buttons { + margin-left: auto; + + button { + margin-left: 0.5rem; + } + } + + &.selected { + background-color: #e0e0e0; + } + } } .add-activity-button { position: absolute; bottom: 1rem; - right: 1rem; + left: 210px; transform: none; } - - mat-sidenav { - width: 350px; - } - .main-content { + .content-and-resources { display: flex; + flex: 1; height: 100%; } .middle-pane { - flex: 1; + flex: 1; // Allow middle pane to take up available space padding: 1rem; - border-right: 1px solid #ccc; - - h3 { - margin-bottom: 1rem; - } } - .activity-resources-list { - // Add styles for the drag and drop list as needed (e.g., min-height, etc.) + .right-pane-container { + position: relative; // Required for absolute positioning of children + flex: 0 0 200px; // Set a fixed width for the right pane container + padding: 1rem; } .resources-pane { - width: 300px; + position: absolute; // Position the resources pane absolutely + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; // Ensure resources pane is on top when visible padding: 1rem; } - .available-resources-list { - // Add styles for the available resources list as needed (e.g., padding, margin, etc.) - } - .groups-list { flex: 0 0 200px; padding: 1rem; - border-left: 1px solid #ccc; } \ No newline at end of file diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts index c50143e5..2ff27ff6 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.ts +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -77,6 +77,9 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { try { this.activities = await this.http.get(`activities/project/${projectID}`).toPromise() || []; + if (this.activities.length > 0) { + this.selectActivity(this.activities[0]); // Select the first activity + } } catch (error) { this.snackbarService.queueSnackbar("Error loading activities."); console.error("Error loading activities:", error); @@ -91,10 +94,18 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { this.fetchActivityGroups(activity.groupIDs); } + start(activity: Activity) { + // ... (Implement logic to start the activity) ... + } + + deleteActivity(activity: Activity) { + // ... (Implement logic to delete the activity) ... + } + async fetchActivityResources(activityID: string) { try { // ... (Implement logic to fetch resources for the activity) ... - this.selectedActivityResources = await this.http.get(`/api/resources/activity/${activityID}`).toPromise() || []; + this.selectedActivityResources = await this.http.get(`resources/activity/${activityID}`).toPromise() || []; } catch (error) { this.snackbarService.queueSnackbar("Error fetching activity resources."); console.error("Error fetching activity resources:", error); From 1062c6a02713ba62d04cde14023fc70270e78c19 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Fri, 10 Jan 2025 23:08:01 -0500 Subject: [PATCH 07/29] Added reordering of activities to frontend, backend, and styling --- backend/src/api/activities.ts | 14 +++++++++++++ backend/src/models/Activity.ts | 3 +++ .../score-authoring.component.html | 20 ++++++++++--------- .../score-authoring.component.scss | 19 ++++++++++++++---- .../score-authoring.component.ts | 19 ++++++++++++++++++ 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/backend/src/api/activities.ts b/backend/src/api/activities.ts index fe9cf9d3..e9819d9b 100644 --- a/backend/src/api/activities.ts +++ b/backend/src/api/activities.ts @@ -61,5 +61,19 @@ router.put('/:activityID', async (req, res) => { res.status(500).json({ error: 'Failed to delete activity.' }); } }); + + router.post('/order', async (req, res) => { + try { + const activities: { activityID: string; order: number }[] = req.body.activities; + const updatePromises = activities.map(activity => + dalActivity.update(activity.activityID, { order: activity.order }) + ); + await Promise.all(updatePromises); + res.status(200).json({ message: 'Activity order updated successfully.' }); + } catch (error) { + console.error("Error updating activity order:", error); + res.status(500).json({ error: 'Failed to update activity order.' }); + } + }); export default router; \ No newline at end of file diff --git a/backend/src/models/Activity.ts b/backend/src/models/Activity.ts index bf303e27..4f9a9d93 100644 --- a/backend/src/models/Activity.ts +++ b/backend/src/models/Activity.ts @@ -17,6 +17,9 @@ export class ActivityModel { @prop({ required: true, type: () => [String] }) public groupIDs!: string[]; + + @prop({ required: true, default: 0 }) + public order!: number; } const Activity = getModelForClass(ActivityModel); diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index 46766943..e82eaa6d 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -24,15 +24,17 @@

Activities

-
-
-
- {{ activity.name }} -
+
+
+
+ drag_indicator + {{ activity.name }} +
diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.scss b/frontend/src/app/components/score-authoring/score-authoring.component.scss index f7e46e65..ad90f585 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.scss +++ b/frontend/src/app/components/score-authoring/score-authoring.component.scss @@ -26,8 +26,8 @@ display: flex; align-items: center; cursor: pointer; - padding: 0.5rem; - margin-bottom: 0.5rem; + padding: 0 0 0 0.5rem; + margin-bottom: 0; .activity-buttons { margin-left: auto; @@ -37,11 +37,22 @@ } } - &.selected { - background-color: #e0e0e0; + &[cdkDrag] { // Style for draggable activities + cursor: move; /* Show the move cursor when hovering */ + } + + .drag-handle { /* Styles for the drag handle icon */ + cursor: move; + margin-right: 0.5rem; /* Add some space to the right */ } } } + + .selected { + background-color: #e0e0e0; + border-left: 4px solid #3f51b5; /* Add a thicker left border to highlight */ + padding-left: 0.25rem; /* Adjust padding to account for the border */ + } .add-activity-button { position: absolute; diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts index 2ff27ff6..b221ca48 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.ts +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -102,6 +102,25 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { // ... (Implement logic to delete the activity) ... } + dropActivity(event: CdkDragDrop) { + moveItemInArray(this.activities, event.previousIndex, event.currentIndex); + this.updateActivityOrder(); + } + + async updateActivityOrder() { + try { + const updatedActivities = this.activities.map((activity, index) => ({ + activityID: activity.activityID, + order: index + 1 // Assign new order based on index + })); + + await this.http.post('activities/order/', { activities: updatedActivities }).toPromise(); + } catch (error) { + this.snackbarService.queueSnackbar("Error updating activity order."); + console.error("Error updating activity order:", error); + } + } + async fetchActivityResources(activityID: string) { try { // ... (Implement logic to fetch resources for the activity) ... From 4a8b5eb0d539daab23f56cd61a6636742cba15dd Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Mon, 13 Jan 2025 20:22:20 -0500 Subject: [PATCH 08/29] Created a Resources model, api routing, and database calls; Added resources list to activities, created with drag and drop from available resources pane --- backend/src/api/resources.ts | 44 ++++++ backend/src/models/Resource.ts | 3 + backend/src/repository/dalResource.ts | 10 ++ .../score-authoring.component.html | 66 +++++++-- .../score-authoring.component.scss | 115 +++++++++++++++- .../score-authoring.component.ts | 129 ++++++++++++++++-- frontend/src/app/models/resource.ts | 11 ++ 7 files changed, 351 insertions(+), 27 deletions(-) create mode 100644 frontend/src/app/models/resource.ts diff --git a/backend/src/api/resources.ts b/backend/src/api/resources.ts index 6bbaf0e1..8e7df0fe 100644 --- a/backend/src/api/resources.ts +++ b/backend/src/api/resources.ts @@ -2,9 +2,37 @@ import express from 'express'; import dalResource from '../repository/dalResource'; +import { ResourceModel } from '../models/Resource'; const router = express.Router(); +router.post('/create', async (req, res) => { // Define the POST route + try { + const resourceData: ResourceModel = req.body; + const newResource = await dalResource.create(resourceData); + res.status(201).json(newResource); // 201 Created status + } catch (error) { + console.error("Error creating resource:", error); + res.status(500).json({ error: 'Failed to create resource.' }); + } +}); + +router.delete('/delete/:resourceID', async (req, res) => { // Add the /delete segment to the path + try { + const resourceID = req.params.resourceID; + const deletedResource = await dalResource.remove(resourceID); // Assuming you have a remove() method in dalResource + + if (deletedResource) { + res.status(200).json(deletedResource); + } else { + res.status(404).json({ error: 'Resource not found.' }); + } + } catch (error) { + console.error("Error deleting resource:", error); + res.status(500).json({ error: 'Failed to delete resource.' }); + } +}); + router.get('/activity/:activityID', async (req, res) => { try { const activityID = req.params.activityID; @@ -16,6 +44,22 @@ router.get('/activity/:activityID', async (req, res) => { } }); +router.post('/order', async (req, res) => { + try { + const activityID = req.body.activityID; + const resources = req.body.resources; + const updatePromises = resources.map((resource: any) => + dalResource.update(resource.resourceID, { order: resource.order }) + ); + await Promise.all(updatePromises); + res.status(200).json({ message: 'Resource order updated successfully.' }); + } catch (error) { + console.error("Error updating resource order:", error); + res.status(500).json({ error: 'Failed to update resource order.' }); + } +}); + + // ... add other routes for creating, updating, deleting, reordering resources ... export default router; \ No newline at end of file diff --git a/backend/src/models/Resource.ts b/backend/src/models/Resource.ts index 579e086b..0e0a3fe9 100644 --- a/backend/src/models/Resource.ts +++ b/backend/src/models/Resource.ts @@ -7,6 +7,9 @@ export class ResourceModel { @prop({ required: true }) public resourceID!: string; + @prop({ required: true }) + public name!: string; + @prop({ required: true }) public activityID!: string; diff --git a/backend/src/repository/dalResource.ts b/backend/src/repository/dalResource.ts index 2f7bfd0e..1aaafb7b 100644 --- a/backend/src/repository/dalResource.ts +++ b/backend/src/repository/dalResource.ts @@ -33,6 +33,16 @@ const dalResource = { } }, + remove: async (id: string): Promise => { + try { + const deletedResource = await Resource.findOneAndDelete({ resourceID: id }); + return deletedResource; + } catch (error) { + console.error("Error deleting resource:", error); + return undefined; + } + }, + update: async (id: string, resource: Partial): Promise => { try { return await Resource.findOneAndUpdate({ resourceID: id }, resource, { new: true }); diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index e82eaa6d..17e74c70 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -35,12 +35,12 @@

Activities

drag_indicator {{ activity.name }}
+ -
@@ -50,23 +50,65 @@

Activities

-
+
+

{{ selectedActivity.name }}

-
-
- {{ resource.name }} +
+
+
+ drag_indicator + Tab {{ i + 1 }}: {{ resource.name }} + +
+
- +
-

Available Resources

-
-
- {{ resource.name }} + +

Resources

+

View

+
+
+
+ {{ resource.name }} +
diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.scss b/frontend/src/app/components/score-authoring/score-authoring.component.scss index ad90f585..14e7a96e 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.scss +++ b/frontend/src/app/components/score-authoring/score-authoring.component.scss @@ -55,10 +55,9 @@ } .add-activity-button { - position: absolute; - bottom: 1rem; - left: 210px; - transform: none; + position: static; + margin-top: 1rem; + margin-left: 195px; } .content-and-resources { @@ -70,11 +69,54 @@ .middle-pane { flex: 1; // Allow middle pane to take up available space padding: 1rem; + + .add-resource-button { + position: absolute; + bottom: 1rem; + right: 440px; + } + + .activity-resources-list { + .resource-item { /* Add styles for the resource items */ + display: flex; + align-items: center; + height: 2rem; + + &.canvas { + background-color: #81d4fa; /* Lighter Blue */ + color: #000; + } + + &.bucketView { + background-color: #80cbc4; /* Lighter Teal */ + color: #000; + } + + &.workspace { + background-color: #ffcc80; /* Lighter Orange */ + color: #000; + } + + &.monitor { + background-color: #ef9a9a; /* Lighter Red */ + color: #000; + } + + .drag-handle { + cursor: move; + margin-right: 0.5rem; + } + + .delete-button { /* Add styles for the delete button */ + margin-left: auto; /* Push the button to the right */ + } + } + } } .right-pane-container { position: relative; // Required for absolute positioning of children - flex: 0 0 200px; // Set a fixed width for the right pane container + flex: 0 0 400px; // Set a fixed width for the right pane container padding: 1rem; } @@ -86,9 +128,70 @@ height: 100%; z-index: 1; // Ensure resources pane is on top when visible padding: 1rem; + + background-color: #fff; // White background + border: 1px solid #ccc; // Light gray border + border-radius: 5px; // Add rounded corners + box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); // Add a subtle shadow + + h3 { // Styles for h3 headings + font-size: 1.5rem; // Increase font size + font-weight: bold; + margin-bottom: 1rem; // Add space below + } + + h4 { // Styles for h4 headings + font-size: 1.2rem; // Increase font size + font-weight: bold; + margin-bottom: 0.5rem; // Add space below + } + + .close-resource-button { + float: right; + margin-right: 1.25rem; + } + + .available-resources-list { + .available-resource-item { + cursor: grab; /* Use grab cursor to indicate draggability */ + padding: 0.5rem; + padding-right: 1rem; + border: 1px solid #ccc; /* Add a border to resemble material */ + border-radius: 4px; + margin-bottom: 0.5rem; + max-width: 375px; + + &.canvas { + background-color: #81d4fa; /* Lighter Blue */ + color: #000; + } + + &.bucketView { + background-color: #80cbc4; /* Lighter Teal */ + color: #000; + } + + &.workspace { + background-color: #ffcc80; /* Lighter Orange */ + color: #000; + } + + &.monitor { + background-color: #ef9a9a; /* Lighter Red */ + color: #000; + } + } + } } + + /* Style for the drop placeholder */ +.cdk-drop-list-dragging .cdk-drop-list-placeholder { + background: #ccc; /* Light gray background */ + border: dotted 3px #999; /* Dotted border */ + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} .groups-list { - flex: 0 0 200px; + flex: 0 0 400px; padding: 1rem; } \ No newline at end of file diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts index b221ca48..b50ac81d 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.ts +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -16,13 +16,10 @@ import { CreateActivityModalComponent } from '../create-activity-modal/create-ac import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { HttpClient } from '@angular/common/http'; import { Activity } from 'src/app/models/activity'; +import { generateUniqueID } from 'src/app/utils/Utils'; +import { Resource } from 'src/app/models/resource'; -interface Resource { - resourceID: string; - name: string; -} - @Component({ selector: 'app-score-authoring', templateUrl: './score-authoring.component.html', @@ -38,7 +35,16 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { selectedActivity: Activity | null = null; selectedActivityResources: Resource[] = []; selectedActivityGroups: Group[] = []; - availableResources: Resource[] = []; + + allAvailableResources: any[] = [ //define available resources + { name: 'Canvas', type: 'canvas' }, + { name: 'Bucket View', type: 'bucketView' }, + { name: 'Workspace', type: 'workspace' }, + { name: 'Monitor', type: 'monitor' } + ]; + + availableResources: any[] = [...this.allAvailableResources]; // Duplicate the array to be filtered based on selected values + showResourcesPane = false; @@ -88,9 +94,17 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } } - selectActivity(activity: Activity) { + async selectActivity(activity: Activity) { this.selectedActivity = activity; - this.fetchActivityResources(activity.activityID); + this.showResourcesPane = false; //Close the resources pane + try { + // Fetch resources for the selected activity + this.selectedActivityResources = await this.http.get(`resources/activity/${activity.activityID}`).toPromise() || []; + } catch (error) { + this.snackbarService.queueSnackbar("Error fetching activity resources."); + console.error("Error fetching activity resources:", error); + } + this.fetchActivityGroups(activity.groupIDs); } @@ -98,10 +112,70 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { // ... (Implement logic to start the activity) ... } - deleteActivity(activity: Activity) { + editActivity(activity: Activity) { // ... (Implement logic to delete the activity) ... } + dropResource(event: CdkDragDrop) { + moveItemInArray(this.selectedActivityResources, event.previousIndex, event.currentIndex); + this.updateResourceOrder(); + } + + dropResourceFromAvailable(event: CdkDragDrop) { + const resource = this.availableResources[event.previousIndex]; + + this.createResource(resource) + .then(newResource => { + this.availableResources.splice(event.previousIndex, 1); + this.selectedActivityResources.splice(event.currentIndex, 0, newResource); + this.updateResourceOrder(); + }) + .catch(error => { + // Handle error (e.g., display an error message) + console.error("Error creating resource:", error); + }); + } + + async createResource(resourceData: any): Promise { + try { + const newResourceData = { + resourceID: generateUniqueID(), // Add resourceID + name: resourceData.name, + activityID: this.selectedActivity!.activityID, + [resourceData.type]: true, + order: this.selectedActivityResources.length + 1, + }; + + const response = await this.http.post('resources/create', newResourceData).toPromise(); + return response as Resource; + } catch (error) { + this.snackbarService.queueSnackbar("Error creating resource."); + console.error("Error creating resource:", error); + throw error; // Re-throw the error to be caught in the calling function + } + } + + async deleteResource(resource: Resource, index: number) { + try { + // 1. Delete the resource from the database + await this.http.delete(`resources/delete/${resource.resourceID}`).toPromise(); + + // 2. Remove the resource from the list + this.selectedActivityResources.splice(index, 1); + + // 3. Update the resource order in the database + this.updateResourceOrder(); + + // 4. If the resources pane is open, update the available resources + if (this.showResourcesPane) { + this.filterAvailableResources(); + } + } catch (error) { + this.snackbarService.queueSnackbar("Error deleting resource."); + console.error("Error deleting resource:", error); + } + } + dropActivity(event: CdkDragDrop) { moveItemInArray(this.activities, event.previousIndex, event.currentIndex); this.updateActivityOrder(); @@ -121,6 +195,28 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } } + async updateResourceOrder() { + if (!this.selectedActivity) { + return; // Do nothing if no activity is selected + } + + try { + const updatedResources = this.selectedActivityResources.map((resource, index) => ({ + resourceID: resource.resourceID, + order: index + 1, + })); + + await this.http.post('resources/order/', { + activityID: this.selectedActivity.activityID, + resources: updatedResources + }).toPromise(); + + } catch (error) { + this.snackbarService.queueSnackbar("Error updating resource order."); + console.error("Error updating resource order:", error); + } + } + async fetchActivityResources(activityID: string) { try { // ... (Implement logic to fetch resources for the activity) ... @@ -173,6 +269,21 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { this.showResourcesPane = false; } + toggleResourcesPane() { + this.showResourcesPane = !this.showResourcesPane; + + if (this.showResourcesPane) { + this.filterAvailableResources(); // Filter resources when the pane is opened + } + } + + filterAvailableResources() { + const existingResourceNames = new Set(this.selectedActivityResources.map(r => r.name)); + this.availableResources = this.allAvailableResources.filter(resource => + !existingResourceNames.has(resource.name) + ); + } + drop(event: CdkDragDrop) { moveItemInArray(this.selectedActivityResources, event.previousIndex, event.currentIndex); } diff --git a/frontend/src/app/models/resource.ts b/frontend/src/app/models/resource.ts new file mode 100644 index 00000000..d3b77711 --- /dev/null +++ b/frontend/src/app/models/resource.ts @@ -0,0 +1,11 @@ +export interface Resource { + resourceID: string; + activityID: string; + order: number; + name: string; + canvas: boolean; // Add the canvas property + bucketView: boolean; + workspace: boolean; + monitor: boolean; + // ... other properties as needed ... + } \ No newline at end of file From 020b14dd2ed33fe1f57eb5189fe12c86372df261 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Wed, 15 Jan 2025 14:06:35 -0500 Subject: [PATCH 09/29] Add edit activity component; Add deleting and updating activities; Add associated activity space name to activity title --- frontend/src/app/app.module.ts | 2 + .../edit-activity-modal.component.html | 41 ++++++++++ .../edit-activity-modal.component.scss | 6 ++ .../edit-activity-modal.component.spec.ts | 23 ++++++ .../edit-activity-modal.component.ts | 66 ++++++++++++++++ .../score-authoring.component.html | 3 +- .../score-authoring.component.scss | 44 +++++++++-- .../score-authoring.component.ts | 78 ++++++++++++++++++- 8 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.html create mode 100644 frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.scss create mode 100644 frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.spec.ts create mode 100644 frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 5a53584a..c6c6e822 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -67,6 +67,7 @@ import { MarkdownModule } from 'ngx-markdown'; import { AddScoreRunModalComponent } from './components/add-score-run-modal/add-score-run-modal.component'; import { ScoreAuthoringComponent } from './components/score-authoring/score-authoring.component'; import { CreateActivityModalComponent } from './components/create-activity-modal/create-activity-modal.component'; +import { EditActivityModalComponent } from './components/edit-activity-modal/edit-activity-modal.component'; const config: SocketIoConfig = { url: environment.socketUrl, @@ -123,6 +124,7 @@ export function tokenGetter() { AddScoreRunModalComponent, ScoreAuthoringComponent, CreateActivityModalComponent, + EditActivityModalComponent, ], imports: [ BrowserModule, diff --git a/frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.html b/frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.html new file mode 100644 index 00000000..d04b7e61 --- /dev/null +++ b/frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.html @@ -0,0 +1,41 @@ +

Edit Activity

+
+ + Activity Name + + Activity name is required + + + + Activity Space + + + {{ board.name }} + + + Activity Space is required + + + + Groups + + + {{ group.name }} + + + At least one group is required + +
+ +
+ + + + +
\ No newline at end of file diff --git a/frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.scss b/frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.scss new file mode 100644 index 00000000..812579f9 --- /dev/null +++ b/frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.scss @@ -0,0 +1,6 @@ +.cancel { + padding-left: 10px; + width: 15px; + height: 15px; + font-size: medium; + } \ No newline at end of file diff --git a/frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.spec.ts b/frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.spec.ts new file mode 100644 index 00000000..9c070569 --- /dev/null +++ b/frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditActivityModalComponent } from './edit-activity-modal.component'; + +describe('EditActivityModalComponent', () => { + let component: EditActivityModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EditActivityModalComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(EditActivityModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.ts b/frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.ts new file mode 100644 index 00000000..33cf3f41 --- /dev/null +++ b/frontend/src/app/components/edit-activity-modal/edit-activity-modal.component.ts @@ -0,0 +1,66 @@ + +import { Component, Inject, OnInit } from '@angular/core'; +import { + MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, + MatLegacyDialogRef as MatDialogRef, +} from '@angular/material/legacy-dialog'; +import { Board } from 'src/app/models/board'; +import { Group } from 'src/app/models/group'; +import { BoardService } from 'src/app/services/board.service'; +import { GroupService } from 'src/app/services/group.service'; + +@Component({ + selector: 'app-edit-activity-modal', + templateUrl: './edit-activity-modal.component.html', + styleUrls: ['./edit-activity-modal.component.scss'] +}) +export class EditActivityModalComponent implements OnInit { + + activityName = ''; + boards: Board[] = []; + selectedBoardID = ''; + availableGroups: Group[] = []; + selectedGroupIDs: string[] = []; + showDeleteButton = false; + + constructor( + public dialogRef: MatDialogRef, + private boardService: BoardService, + private groupService: GroupService, + @Inject(MAT_DIALOG_DATA) public data: any + ) { } + + async ngOnInit(): Promise { + this.activityName = this.data.activity.name; + this.selectedBoardID = this.data.activity.boardID; + this.selectedGroupIDs = this.data.activity.groupIDs; + + this.boards = await this.boardService.getByProject(this.data.project.projectID) || []; + this.availableGroups = await this.groupService.getByProjectId(this.data.project.projectID); + } + + onNoClick(): void { + this.dialogRef.close(); + } + + handleEditActivity() { + const updatedActivity = { + activityID: this.data.activity.activityID, // Include the activityID + name: this.activityName, + projectID: this.data.project.projectID, + boardID: this.selectedBoardID, + groupIDs: this.selectedGroupIDs + }; + + this.dialogRef.close({ activity: updatedActivity, delete: false }); + } + + toggleDeleteButton() { // Add this method + this.showDeleteButton = !this.showDeleteButton; + } + + handleDeleteActivity() { + // May want to confirm here before closing the dialog + this.dialogRef.close({ activity: this.data.activity, delete: true }); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index 17e74c70..a44a3e02 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -57,11 +57,12 @@

Activities

color="primary" class="add-resource-button" (click)="toggleResourcesPane()" - *ngIf="!showResourcesPane" + *ngIf="!showResourcesPane && selectedActivity" > add

{{ selectedActivity.name }}

+

Activity Space: {{ selectedBoardName }}

(`resources/activity/${activity.activityID}`).toPromise() || []; + this.selectedBoardName = await this.getSelectedBoardName(); } catch (error) { this.snackbarService.queueSnackbar("Error fetching activity resources."); console.error("Error fetching activity resources:", error); @@ -113,7 +118,78 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } editActivity(activity: Activity) { - // ... (Implement logic to delete the activity) ... + const dialogRef = this.dialog.open(EditActivityModalComponent, { + data: { + project: this.project, + activity: activity + } + }); + + dialogRef.afterClosed().subscribe((result: { activity: Activity, delete: boolean }) => { + if (result) { + if (result.delete) { + this.deleteActivity(result.activity); + } else { + this.updateActivity(result.activity); + } + } + }); + } + + async updateActivity(activity: Activity) { + try { + const updatedActivity = await this.http.put(`activities/${activity.activityID}`, activity).toPromise(); + + // Update the activity in the activities list + const index = this.activities.findIndex(a => a.activityID === activity.activityID); + if (index !== -1) { + this.activities[index] = updatedActivity as Activity; + } + + // If the updated activity is the selected one, update the selectedActivity + if (this.selectedActivity?.activityID === activity.activityID) { + this.selectedActivity = updatedActivity as Activity; + } + } catch (error) { + this.snackbarService.queueSnackbar("Error updating activity."); + console.error("Error updating activity:", error); + } + } + + async deleteActivity(activity: Activity) { + try { + // 1. (Optional) Ask for confirmation before deleting + + // 2. Call the API to delete the activity + await this.http.delete(`activities/${activity.activityID}`).toPromise(); + + // 3. Remove the activity from the activities list + this.activities = this.activities.filter(a => a.activityID !== activity.activityID); + + // 4. If the deleted activity was the selected one, clear the selection + if (this.selectedActivity?.activityID === activity.activityID) { + this.selectedActivity = null; + this.selectedActivityResources = []; // Clear resources as well + this.selectedActivityGroups = []; // Clear groups as well + } + } catch (error) { + this.snackbarService.queueSnackbar("Error deleting activity."); + console.error("Error deleting activity:", error); + } + } + + async getSelectedBoardName(): Promise { + if (this.selectedActivity) { + try { + const board = await this.boardService.get(this.selectedActivity.boardID); + return board ? board.name : ''; + } catch (error) { + console.error("Error fetching board:", error); + return ''; + } + } else { + return ''; + } } dropResource(event: CdkDragDrop) { From f8ebe1d6e0322b662fb1f8aa15b2d320d4bf6c65 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Wed, 15 Jan 2025 16:23:16 -0500 Subject: [PATCH 10/29] Added group to resource assignment logic and UI --- backend/src/api/activities.ts | 39 +- backend/src/api/resources.ts | 36 ++ backend/src/models/Resource.ts | 3 + backend/src/repository/dalActivity.ts | 10 + backend/src/repository/dalResource.ts | 38 ++ .../score-authoring.component.html | 26 +- .../score-authoring.component.scss | 453 ++++++++++-------- .../score-authoring.component.ts | 29 +- frontend/src/app/models/resource.ts | 1 + 9 files changed, 429 insertions(+), 206 deletions(-) diff --git a/backend/src/api/activities.ts b/backend/src/api/activities.ts index e9819d9b..f1e95e36 100644 --- a/backend/src/api/activities.ts +++ b/backend/src/api/activities.ts @@ -1,6 +1,7 @@ import express from 'express'; import dalActivity from '../repository/dalActivity'; +import dalResource from '../repository/dalResource'; import { ActivityModel } from '../models/Activity'; const router = express.Router(); @@ -31,20 +32,32 @@ router.get('/project/:projectID', async (req, res) => { // Update an activity router.put('/:activityID', async (req, res) => { - try { - const activityID = req.params.activityID; - const updatedData: Partial = req.body; - const updatedActivity = await dalActivity.update(activityID, updatedData); - if (updatedActivity) { - res.status(200).json(updatedActivity); - } else { - res.status(404).json({ error: 'Activity not found.' }); - } - } catch (error) { - console.error("Error updating activity:", error); - res.status(500).json({ error: 'Failed to update activity.' }); + try { + const activityID = req.params.activityID; + const updatedData: Partial = req.body; + + // If groupIDs are modified, update the resources + if (updatedData.groupIDs) { + const originalGroupIDs = (await dalActivity.getById(activityID))?.groupIDs; + const removedGroupIDs = originalGroupIDs?.filter(id => !updatedData.groupIDs?.includes(id)) || []; + const removePromises = removedGroupIDs.map(groupID => + dalResource.removeGroupFromActivityResources(activityID, groupID) + ); + await Promise.all(removePromises); } - }); + + const updatedActivity = await dalActivity.update(activityID, updatedData); + + if (updatedActivity) { + res.status(200).json(updatedActivity); + } else { + res.status(404).json({ error: 'Activity not found.' }); + } + } catch (error) { + console.error("Error updating activity:", error); + res.status(500).json({ error: 'Failed to update activity.' }); + } +}); // Delete an activity router.delete('/:activityID', async (req, res) => { diff --git a/backend/src/api/resources.ts b/backend/src/api/resources.ts index 8e7df0fe..ef01d59d 100644 --- a/backend/src/api/resources.ts +++ b/backend/src/api/resources.ts @@ -59,6 +59,42 @@ router.post('/order', async (req, res) => { } }); +// Add a group to a resource +router.post('/:resourceID/groups/:groupID', async (req, res) => { + try { + const resourceID = req.params.resourceID; + const groupID = req.params.groupID; + const updatedResource = await dalResource.addGroupToResource(resourceID, groupID); + + if (updatedResource) { + res.status(200).json(updatedResource); + } else { + res.status(404).json({ error: 'Resource not found.' }); + } + } catch (error) { + console.error("Error adding group to resource:", error); + res.status(500).json({ error: 'Failed to add group to resource.' }); + } +}); + +// Remove a group from a resource +router.delete('/:resourceID/groups/:groupID', async (req, res) => { + try { + const resourceID = req.params.resourceID; + const groupID = req.params.groupID; + const updatedResource = await dalResource.removeGroupFromResource(resourceID, groupID); + + if (updatedResource) { + res.status(200).json(updatedResource); + } else { + res.status(404).json({ error: 'Resource not found.' }); + } + } catch (error) { + console.error("Error removing group from resource:", error); + res.status(500).json({ error: 'Failed to remove group from resource.' }); + } +}); + // ... add other routes for creating, updating, deleting, reordering resources ... diff --git a/backend/src/models/Resource.ts b/backend/src/models/Resource.ts index 0e0a3fe9..b291756d 100644 --- a/backend/src/models/Resource.ts +++ b/backend/src/models/Resource.ts @@ -28,6 +28,9 @@ export class ResourceModel { @prop({ required: true, default: false }) public monitor!: boolean; + @prop({ required: true, type: () => [String], default: [] }) + public groupIDs!: string[]; + // ... add more properties for future resource types as needed ... } diff --git a/backend/src/repository/dalActivity.ts b/backend/src/repository/dalActivity.ts index d3be4128..8f484945 100644 --- a/backend/src/repository/dalActivity.ts +++ b/backend/src/repository/dalActivity.ts @@ -12,6 +12,16 @@ const dalActivity = { } }, + getById: async (id: string): Promise => { + try { + const activity = await Activity.findOne({ activityID: id }); + return activity; + } catch (error) { + console.error("Error getting activity by ID:", error); + return undefined; + } + }, + getByProject: async (projectID: string): Promise => { try { const activities = await Activity.find({ projectID }); diff --git a/backend/src/repository/dalResource.ts b/backend/src/repository/dalResource.ts index 1aaafb7b..12b756d5 100644 --- a/backend/src/repository/dalResource.ts +++ b/backend/src/repository/dalResource.ts @@ -49,6 +49,44 @@ const dalResource = { } catch (error) { console.error("Error updating resource:", error); // Log the error return undefined; + } + }, + + addGroupToResource: async (resourceID: string, groupID: string): Promise => { + try { + return await Resource.findOneAndUpdate( + { resourceID: resourceID }, + { $addToSet: { groupIDs: groupID } }, // Add to set to avoid duplicates + { new: true } + ); + } catch (error) { + console.error("Error adding group to resource:", error); // Log the error + return undefined; + } + }, + + removeGroupFromResource: async (resourceID: string, groupID: string): Promise => { + try { + return await Resource.findOneAndUpdate( + { resourceID: resourceID }, + { $pull: { groupIDs: groupID } }, // Remove from array + { new: true } + ); + } catch (error) { + console.error("Error removing group from resource:", error); // Log the error + return undefined; + } + }, + + removeGroupFromActivityResources: async (activityID: string, groupID: string): Promise => { + try { + await Resource.updateMany( + { activityID: activityID }, + { $pull: { groupIDs: groupID } } + ); + } catch (error) { + console.error("Error removing group from activity resources:", error); // Log the error + return undefined; } }, diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index a44a3e02..b5c9c481 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -115,10 +115,28 @@

View

-
-

Groups

-
- {{ group.name }} +
+

Groups

+ +
+
+
+ {{ group.name }} +
+
+
+
+
+
diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.scss b/frontend/src/app/components/score-authoring/score-authoring.component.scss index c1ca6c0d..df44afc4 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.scss +++ b/frontend/src/app/components/score-authoring/score-authoring.component.scss @@ -1,220 +1,221 @@ // score-authoring.component.scss .toolbar { - width: 100%; - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 999; + width: 100%; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; +} + +.main-content { + display: flex; + height: calc(100vh - 64px); + margin-top: 64px; +} + +.activities-pane { + width: 250px; + padding: 1rem; + border-right: 1px solid #ccc; + + h3 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 1rem; } - - .main-content { - display: flex; - height: calc(100vh - 64px); - margin-top: 64px; + + h4 { + font-size: 1.2rem; + font-weight: bold; + margin-bottom: 0.5rem; } - - .activities-pane { - width: 250px; - padding: 1rem; - border-right: 1px solid #ccc; - - h3 { - font-size: 1.5rem; - font-weight: bold; - margin-bottom: 1rem; +} + +.activities-list { + div.activity-item { + display: flex; + align-items: center; + cursor: pointer; + padding: 0 0 0 0.5rem; + margin-bottom: 0; + + .activity-buttons { + margin-left: auto; + + button { + margin-left: 0.5rem; + } } - - h4 { - font-size: 1.2rem; - font-weight: bold; - margin-bottom: 0.5rem; + + &[cdkDrag] { // Style for draggable activities + cursor: move; /* Show the move cursor when hovering */ + } + + .drag-handle { /* Styles for the drag handle icon */ + cursor: move; + margin-right: 0.5rem; /* Add some space to the right */ } } +} + +.selected { + background-color: #e0e0e0; + border-left: 4px solid #3f51b5; /* Add a thicker left border to highlight */ + padding-left: 0.25rem; /* Adjust padding to account for the border */ +} + +.add-activity-button { + position: static; + margin-top: 1rem; + margin-left: 195px; +} + +.content-and-resources { + display: flex; + flex: 1; + height: 100%; +} + +.middle-pane { + flex: 1; // Allow middle pane to take up available space + padding: 1rem; + padding-top: 2.5rem; - .activities-list { - div.activity-item { - display: flex; + .add-resource-button { + position: absolute; + bottom: 1rem; + right: 440px; + } + + .activity-resources-list { + margin-top: 2rem; + + .resource-item { /* Add styles for the resource items */ + display: flex; align-items: center; - cursor: pointer; - padding: 0 0 0 0.5rem; - margin-bottom: 0; - - .activity-buttons { - margin-left: auto; - - button { - margin-left: 0.5rem; - } + height: 40px; + margin-bottom: 5px; + + &.canvas { + background-color: #81d4fa; /* Lighter Blue */ + color: #000; } - - &[cdkDrag] { // Style for draggable activities - cursor: move; /* Show the move cursor when hovering */ + + &.bucketView { + background-color: #80cbc4; /* Lighter Teal */ + color: #000; + } + + &.workspace { + background-color: #ffcc80; /* Lighter Orange */ + color: #000; + } + + &.monitor { + background-color: #ef9a9a; /* Lighter Red */ + color: #000; } - .drag-handle { /* Styles for the drag handle icon */ + .drag-handle { cursor: move; - margin-right: 0.5rem; /* Add some space to the right */ + margin-right: 0.5rem; + } + + .delete-button { /* Add styles for the delete button */ + margin-left: auto; /* Push the button to the right */ } } } - .selected { - background-color: #e0e0e0; - border-left: 4px solid #3f51b5; /* Add a thicker left border to highlight */ - padding-left: 0.25rem; /* Adjust padding to account for the border */ + h3, h4 { /* Target both h3 and h4 */ + text-align: center; } - - .add-activity-button { - position: static; - margin-top: 1rem; - margin-left: 195px; + + h3 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 0.25rem; } - - .content-and-resources { - display: flex; - flex: 1; - height: 100%; + + h4 { + font-size: 1.2rem; + font-weight: bold; + margin-bottom: 0.5rem; + color: #888; // Medium gray color } - - .middle-pane { - flex: 1; // Allow middle pane to take up available space - padding: 1rem; - padding-top: 2rem; - - .add-resource-button { - position: absolute; - bottom: 1rem; - right: 440px; - } +} - .activity-resources-list { - margin-top: 2rem; +.right-pane-container { + position: relative; // Required for absolute positioning of children + flex: 0 0 400px; // Set a fixed width for the right pane container + padding: 1rem; +} - .resource-item { /* Add styles for the resource items */ - display: flex; - align-items: center; - height: 2rem; +.resources-pane { + position: absolute; // Position the resources pane absolutely + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; // Ensure resources pane is on top when visible + padding: 1rem; - &.canvas { - background-color: #81d4fa; /* Lighter Blue */ - color: #000; - } - - &.bucketView { - background-color: #80cbc4; /* Lighter Teal */ - color: #000; - } - - &.workspace { - background-color: #ffcc80; /* Lighter Orange */ - color: #000; - } - - &.monitor { - background-color: #ef9a9a; /* Lighter Red */ - color: #000; - } - - .drag-handle { - cursor: move; - margin-right: 0.5rem; - } - - .delete-button { /* Add styles for the delete button */ - margin-left: auto; /* Push the button to the right */ - } - } - } + background-color: #fff; // White background + border: 1px solid #ccc; // Light gray border + border-radius: 5px; // Add rounded corners + box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); // Add a subtle shadow - h3, h4 { /* Target both h3 and h4 */ - text-align: center; - } + h3 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 1rem; + } - h3 { - font-size: 1.5rem; - font-weight: bold; - margin-bottom: 0.25rem; - } - - h4 { - font-size: 1.2rem; - font-weight: bold; - margin-bottom: 0.5rem; - color: #888; // Medium gray color - } + h4 { + font-size: 1.2rem; + font-weight: bold; + margin-bottom: 0.5rem; } - - .right-pane-container { - position: relative; // Required for absolute positioning of children - flex: 0 0 400px; // Set a fixed width for the right pane container - padding: 1rem; + + .close-resource-button { + float: right; + margin-right: 1.25rem; } - - .resources-pane { - position: absolute; // Position the resources pane absolutely - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 1; // Ensure resources pane is on top when visible - padding: 1rem; - - background-color: #fff; // White background - border: 1px solid #ccc; // Light gray border - border-radius: 5px; // Add rounded corners - box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); // Add a subtle shadow - - h3 { - font-size: 1.5rem; - font-weight: bold; - margin-bottom: 1rem; - } - - h4 { - font-size: 1.2rem; - font-weight: bold; - margin-bottom: 0.5rem; - } - .close-resource-button { - float: right; - margin-right: 1.25rem; - } + .available-resources-list { + .available-resource-item { + cursor: grab; /* Use grab cursor to indicate draggability */ + padding: 0.5rem; + padding-right: 1rem; + border: 1px solid #ccc; /* Add a border to resemble material */ + border-radius: 4px; + margin-bottom: 0.5rem; + max-width: 375px; - .available-resources-list { - .available-resource-item { - cursor: grab; /* Use grab cursor to indicate draggability */ - padding: 0.5rem; - padding-right: 1rem; - border: 1px solid #ccc; /* Add a border to resemble material */ - border-radius: 4px; - margin-bottom: 0.5rem; - max-width: 375px; - - &.canvas { - background-color: #81d4fa; /* Lighter Blue */ - color: #000; - } - - &.bucketView { - background-color: #80cbc4; /* Lighter Teal */ - color: #000; - } - - &.workspace { - background-color: #ffcc80; /* Lighter Orange */ - color: #000; - } - - &.monitor { - background-color: #ef9a9a; /* Lighter Red */ - color: #000; - } + &.canvas { + background-color: #81d4fa; /* Lighter Blue */ + color: #000; + } + + &.bucketView { + background-color: #80cbc4; /* Lighter Teal */ + color: #000; + } + + &.workspace { + background-color: #ffcc80; /* Lighter Orange */ + color: #000; + } + + &.monitor { + background-color: #ef9a9a; /* Lighter Red */ + color: #000; } } } +} /* Style for the drop placeholder */ .cdk-drop-list-dragging .cdk-drop-list-placeholder { @@ -223,7 +224,83 @@ transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } - .groups-list { - flex: 0 0 400px; - padding: 1rem; - } \ No newline at end of file +.groups-list { + h3 { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 1rem; + } + + .group-resource-table { + display: flex; + flex-direction: column; + width: 100%; + margin-top: 3rem; + } + + .group-header-row { + display: flex; + margin-bottom: 0.5rem; + } + + .group-header-cell { + flex: 1; + text-align: left; + max-width: 40px; + margin-bottom: 0.4rem; + transform: rotate(-45deg); /* Rotate the text */ + white-space: nowrap; + } + + .resource-row { + display: flex; + align-items: center; + margin-bottom: 0; + } + + .resource-label-cell { + white-space: nowrap; + margin-right: 1rem; + } + + .group-resource-cell { + flex: 1; + height: 40px; + border: 0; + cursor: pointer; + margin-right: 5px; + margin-bottom: 5px; + max-width: 40px; // Set max-width to create a square + + &.selected { + position: relative; // Required for positioning the overlay + padding: 0; + + &::before { // Create a pseudo-element overlay + content: ''; + position: absolute; + top: 7px; + left: 7px; + width: 26px; + height: 26px; + background-color: rgba(0, 0, 0, 0.4); // Semi-transparent black overlay + } + } + + &.canvas { + background-color: #81d4fa; + } + + &.bucketView { + background-color: #80cbc4; + } + + &.workspace { + background-color: #ffcc80; + } + + &.monitor { + background-color: #ef9a9a; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts index c34d45db..3c2af414 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.ts +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -146,9 +146,10 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { this.activities[index] = updatedActivity as Activity; } - // If the updated activity is the selected one, update the selectedActivity + // If the updated activity is the selected one, update the selectedActivity and selectedActivityGroups if (this.selectedActivity?.activityID === activity.activityID) { this.selectedActivity = updatedActivity as Activity; + this.fetchActivityGroups(this.selectedActivity.groupIDs); } } catch (error) { this.snackbarService.queueSnackbar("Error updating activity."); @@ -353,6 +354,32 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } } + async toggleResourceGroupAssignment(resource: Resource, group: Group) { + try { + let updatedResource; + if (this.isResourceAssignedToGroup(resource, group)) { + // Remove the group from the resource + updatedResource = await this.http.delete(`resources/${resource.resourceID}/groups/${group.groupID}`).toPromise(); + } else { + // Add the group to the resource + updatedResource = await this.http.post(`resources/${resource.resourceID}/groups/${group.groupID}`, {}).toPromise(); + } + + // Update the resource in the list + const resourceIndex = this.selectedActivityResources.findIndex(r => r.resourceID === resource.resourceID); + if (resourceIndex !== -1) { + this.selectedActivityResources[resourceIndex] = updatedResource as Resource; + } + } catch (error) { + this.snackbarService.queueSnackbar(`Error ${this.isResourceAssignedToGroup(resource, group) ? 'removing' : 'adding'} group assignment.`); + console.error(`Error ${this.isResourceAssignedToGroup(resource, group) ? 'removing' : 'adding'} group assignment:`, error); + } + } + + isResourceAssignedToGroup(resource: Resource, group: Group): boolean { + return resource.groupIDs.includes(group.groupID); + } + filterAvailableResources() { const existingResourceNames = new Set(this.selectedActivityResources.map(r => r.name)); this.availableResources = this.allAvailableResources.filter(resource => diff --git a/frontend/src/app/models/resource.ts b/frontend/src/app/models/resource.ts index d3b77711..e7465976 100644 --- a/frontend/src/app/models/resource.ts +++ b/frontend/src/app/models/resource.ts @@ -7,5 +7,6 @@ export interface Resource { bucketView: boolean; workspace: boolean; monitor: boolean; + groupIDs: string[]; // ... other properties as needed ... } \ No newline at end of file From 6181505c3b7ddc3b27731920fb4969e005309368 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Thu, 16 Jan 2025 00:53:27 -0500 Subject: [PATCH 11/29] Added a classroom canvas for SCORE authoring; Added a classroom objects resource pane; Added grid layout and classroom outline that supports dynamic resizing --- .../score-authoring.component.html | 41 ++++++- .../score-authoring.component.scss | 73 ++++++++++-- .../score-authoring.component.ts | 104 ++++++++++++++++++ 3 files changed, 206 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index b5c9c481..2fb154ee 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -23,6 +23,9 @@
+

Activities

Activities
+
+
+ +
-
- -
+
+ +
@@ -140,7 +146,32 @@

Groups

- + +
+ +
+ +

Resources

+ +

Classroom Objects

+
+
+
+ {{ resource.icon }} + {{ resource.name }} +
+
+
+
+
+
diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.scss b/frontend/src/app/components/score-authoring/score-authoring.component.scss index df44afc4..0fcd5e76 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.scss +++ b/frontend/src/app/components/score-authoring/score-authoring.component.scss @@ -31,6 +31,13 @@ font-weight: bold; margin-bottom: 0.5rem; } + + .classroom-bindings-button { + position: static; + margin-top: 0.25rem; + margin-bottom: 2rem; + width: 100%; // Make the button take full width + } } .activities-list { @@ -85,8 +92,9 @@ .add-resource-button { position: absolute; - bottom: 1rem; - right: 440px; + bottom: 2rem; + right: 2rem; + z-index: 999; } .activity-resources-list { @@ -97,6 +105,7 @@ align-items: center; height: 40px; margin-bottom: 5px; + border-radius: 5px; &.canvas { background-color: #81d4fa; /* Lighter Blue */ @@ -145,12 +154,30 @@ margin-bottom: 0.5rem; color: #888; // Medium gray color } + + .canvas-container { + position: absolute; + top: 64px; + left: 283px; + width: 100%; + height: 100%; + z-index: 997; // Ensure canvas is on top of everything except resource pane and add button + } } -.right-pane-container { +.resources-right-pane-container { position: relative; // Required for absolute positioning of children flex: 0 0 400px; // Set a fixed width for the right pane container padding: 1rem; + + .canvas-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #000; + } } .resources-pane { @@ -159,12 +186,11 @@ left: 0; width: 100%; height: 100%; - z-index: 1; // Ensure resources pane is on top when visible padding: 1rem; - background-color: #fff; // White background - border: 1px solid #ccc; // Light gray border - border-radius: 5px; // Add rounded corners + background-color: #fff; + border: 1px solid #ccc; + border-radius: 5px; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); // Add a subtle shadow h3 { @@ -217,6 +243,14 @@ } } +.classroom-right-pane-container { + position: relative; + flex: 0 0 400px; + padding: 1rem; + z-index: 998; // This pane will be above the canvas + background-color: #f5f5f5; +} + /* Style for the drop placeholder */ .cdk-drop-list-dragging .cdk-drop-list-placeholder { background: #ccc; /* Light gray background */ @@ -271,6 +305,7 @@ margin-right: 5px; margin-bottom: 5px; max-width: 40px; // Set max-width to create a square + border-radius: 5px; &.selected { position: relative; // Required for positioning the overlay @@ -284,6 +319,7 @@ width: 26px; height: 26px; background-color: rgba(0, 0, 0, 0.4); // Semi-transparent black overlay + border-radius: 3px; } } @@ -303,4 +339,27 @@ background-color: #ef9a9a; } } +} + +.timeline-container { + position: fixed; + bottom: 0; + left: 283px; // Adjust based on the width of the activities pane + width: calc(100% - 283px); // Adjust based on the width of the activities pane + height: 100px; // Adjust as needed + background-color: #f5f5f5; // Light gray background + border-top: 1px solid #ccc; + z-index: 997; // Ensure timeline is on top of everything except resource pane +} + +.middle-pane .canvas-container { + // ... other styles ... + + canvas { + background-color: #f5f5f5; /* Set a white background */ + } +} + +.classroom-resources-pane { + z-index: 999; } \ No newline at end of file diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts index 3c2af414..9e7f2e67 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.ts +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -20,6 +20,8 @@ import { HttpClient } from '@angular/common/http'; import { Activity } from 'src/app/models/activity'; import { generateUniqueID } from 'src/app/utils/Utils'; import { Resource } from 'src/app/models/resource'; +import { fabric } from 'fabric'; +import { HostListener } from '@angular/core'; @Component({ @@ -38,6 +40,7 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { selectedActivityResources: Resource[] = []; selectedActivityGroups: Group[] = []; selectedBoardName = ''; + canvas: fabric.Canvas | undefined; allAvailableResources: any[] = [ //define available resources { name: 'Canvas', type: 'canvas' }, @@ -48,8 +51,26 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { availableResources: any[] = [...this.allAvailableResources]; // Duplicate the array to be filtered based on selected values + availableClassroomObjects: any[] = [ + { name: 'Table', type: 'table', icon: 'table_restaurant' }, + { name: 'Projector', type: 'projector', icon: 'videocam' }, + { name: 'Student', type: 'student', icon: 'person' }, + { name: 'Student Group', type: 'studentGroup', icon: 'groups' }, + { name: 'Teacher', type: 'teacher', icon: 'school' } + // ... add more classroom objects + ]; showResourcesPane = false; + showClassroomBindings = false; + + @HostListener('window:resize', ['$event']) + onResize(event: any) { + if (this.showClassroomBindings) { + this.canvas?.dispose(); + this.canvas = undefined; + this.initializeCanvas(); + } + } constructor( public userService: UserService, @@ -69,6 +90,75 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { this.loadScoreAuthoringData(); } + initializeCanvas() { + const canvasContainer = document.getElementById('classroomCanvas')?.parentElement; // Get the parent div + + if (canvasContainer) { + this.canvas = new fabric.Canvas('classroomCanvas', { + width: canvasContainer.offsetWidth - 283, // Set width to parent's width + height: canvasContainer.offsetHeight -64 // Set height to parent's height + }); + + this.createDotGrid(); + this.drawInnerBox(); + + // Add event listeners for object:added and object:moving + this.canvas.on('object:added', this.onObjectAdded); + this.canvas.on('object:moving', this.onObjectMoving); + } + } + + createDotGrid() { + if (this.canvas) { + const canvasWidth = this.canvas.getWidth(); + const canvasHeight = this.canvas.getHeight(); + const gridSpacing = 40; // Adjust the spacing between dots as needed + + for (let x = 0; x <= canvasWidth; x += gridSpacing) { + for (let y = 0; y <= canvasHeight; y += gridSpacing) { + const dot = new fabric.Circle({ + left: x, + top: y, + radius: 2, // Adjust the dot size as needed + fill: '#ddd' // Adjust the dot color as needed + }); + this.canvas.add(dot); + } + } + + this.canvas.renderAll(); // Render the canvas to show the dots + } + } + + drawInnerBox() { + if (this.canvas) { + const canvasWidth = this.canvas.getWidth(); + const canvasHeight = this.canvas.getHeight(); + const inset = 41; // Adjust the inset value as needed + + const rect = new fabric.Rect({ + left: inset, + top: inset, + width: canvasWidth - inset * 2, + height: canvasHeight - inset * 2, + fill: 'transparent', // Or any fill color you prefer + stroke: '#ccc', // Or any stroke color you prefer + strokeWidth: 2 // Adjust the stroke width as needed + }); + + this.canvas.add(rect); + this.canvas.sendToBack(rect); // Send the rectangle to the back + } + } + + onObjectAdded(event: any) { + // ... (Implement logic to save the added object to the database) ... + } + + onObjectMoving(event: any) { + // ... (Implement logic to update the object's position in the database) ... + } + async loadScoreAuthoringData(): Promise { const projectID = this.activatedRoute.snapshot.paramMap.get('projectID'); if (!projectID) { @@ -376,6 +466,20 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } } + toggleClassroomBindings() { + this.showClassroomBindings = !this.showClassroomBindings; + + if (this.showClassroomBindings) { + // Add a slight delay before initializing the canvas + setTimeout(() => { + this.initializeCanvas(); + }, 100); // Adjust the delay as needed + } else { + this.canvas?.dispose(); + this.canvas = undefined; + } + } + isResourceAssignedToGroup(resource: Resource, group: Group): boolean { return resource.groupIDs.includes(group.groupID); } From e425c6c6cbf78de3d5f0c1e6ca2541154ef965f7 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Mon, 20 Jan 2025 18:54:05 -0500 Subject: [PATCH 12/29] Added and styled teacher activity list for SCORE authoring; Minor styling and header changes; Temporarily removed button for Classroom Authoring --- backend/src/services/vertexAI/index.ts | 12 +- frontend/src/app/app.module.ts | 6 + ...custom-teacher-prompt-modal.component.html | 1 + ...custom-teacher-prompt-modal.component.scss | 0 ...tom-teacher-prompt-modal.component.spec.ts | 23 ++++ .../custom-teacher-prompt-modal.component.ts | 10 ++ .../score-authoring.component.html | 60 +++++++- .../score-authoring.component.scss | 94 ++++++++++++- .../score-authoring.component.ts | 129 +++++++++++++++++- .../select-ai-agent-modal.component.html | 1 + .../select-ai-agent-modal.component.scss | 0 .../select-ai-agent-modal.component.spec.ts | 23 ++++ .../select-ai-agent-modal.component.ts | 10 ++ .../select-workflow-modal.component.html | 1 + .../select-workflow-modal.component.scss | 0 .../select-workflow-modal.component.spec.ts | 23 ++++ .../select-workflow-modal.component.ts | 10 ++ 17 files changed, 384 insertions(+), 19 deletions(-) create mode 100644 frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.html create mode 100644 frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.scss create mode 100644 frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.spec.ts create mode 100644 frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.ts create mode 100644 frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.html create mode 100644 frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.scss create mode 100644 frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.spec.ts create mode 100644 frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.ts create mode 100644 frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.html create mode 100644 frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.scss create mode 100644 frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.spec.ts create mode 100644 frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.ts diff --git a/backend/src/services/vertexAI/index.ts b/backend/src/services/vertexAI/index.ts index 0dc76e69..a5a6c5ba 100644 --- a/backend/src/services/vertexAI/index.ts +++ b/backend/src/services/vertexAI/index.ts @@ -693,12 +693,12 @@ async function performDatabaseOperations( async function constructAndSendMessage( postsWithBucketIds: any[], - bucketsToSend: any[], + bucketData: any[], prompt: string ): Promise { - const postsAsKeyValuePairs = postsToKeyValuePairs(postsWithBucketIds); + const postData = postsToKeyValuePairs(postsWithBucketIds); - const message = + const promptTemplate = ` Please provide your response in the following JSON format, including the "response" key and optionally any of the following keys: "create_bucket", @@ -738,13 +738,13 @@ async function constructAndSendMessage( } Here are the posts from the project:` + - postsAsKeyValuePairs + // Concatenate variables here + postData + `\nHere are the buckets:\n` + - JSON.stringify(bucketsToSend, null, 2) + + JSON.stringify(bucketData, null, 2) + `\nUser prompt: ${prompt}`; try { - const result = await chat.sendMessageStream(message); // Get the StreamGenerateContentResult + const result = await chat.sendMessageStream(promptTemplate); // Get the StreamGenerateContentResult return result; } catch (error) { diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index c6c6e822..08811db3 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -68,6 +68,9 @@ import { AddScoreRunModalComponent } from './components/add-score-run-modal/add- import { ScoreAuthoringComponent } from './components/score-authoring/score-authoring.component'; import { CreateActivityModalComponent } from './components/create-activity-modal/create-activity-modal.component'; import { EditActivityModalComponent } from './components/edit-activity-modal/edit-activity-modal.component'; +import { SelectWorkflowModalComponent } from './components/select-workflow-modal/select-workflow-modal.component'; +import { SelectAiAgentModalComponent } from './components/select-ai-agent-modal/select-ai-agent-modal.component'; +import { CustomTeacherPromptModalComponent } from './components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component'; const config: SocketIoConfig = { url: environment.socketUrl, @@ -125,6 +128,9 @@ export function tokenGetter() { ScoreAuthoringComponent, CreateActivityModalComponent, EditActivityModalComponent, + SelectWorkflowModalComponent, + SelectAiAgentModalComponent, + CustomTeacherPromptModalComponent, ], imports: [ BrowserModule, diff --git a/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.html b/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.html new file mode 100644 index 00000000..749f89ac --- /dev/null +++ b/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.html @@ -0,0 +1 @@ +

custom-teacher-prompt-modal works!

diff --git a/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.scss b/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.spec.ts b/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.spec.ts new file mode 100644 index 00000000..5c0eb900 --- /dev/null +++ b/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CustomTeacherPromptModalComponent } from './custom-teacher-prompt-modal.component'; + +describe('CustomTeacherPromptModalComponent', () => { + let component: CustomTeacherPromptModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CustomTeacherPromptModalComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CustomTeacherPromptModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.ts b/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.ts new file mode 100644 index 00000000..7997fcb3 --- /dev/null +++ b/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-custom-teacher-prompt-modal', + templateUrl: './custom-teacher-prompt-modal.component.html', + styleUrls: ['./custom-teacher-prompt-modal.component.scss'] +}) +export class CustomTeacherPromptModalComponent { + +} diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index 2fb154ee..559af6a0 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -23,9 +23,9 @@
-

{{ selectedActivity.name }}

Activity Space: {{ selectedBoardName }}

+ +
Resource Assignments
Activity Space: {{ selectedBoardName }}
+ +
Teacher Tasks
+
+
+
+ drag_indicator + Task {{ i + 1 }}: {{ task.name }} + +
+
+
@@ -98,8 +113,8 @@

Activity Space: {{ selectedBoardName }}

-

Resources

-

View

+

Resources & Actions

+

Resources

View
+ +

Teacher Actions

+
+
+
+ {{ action.icon }} + {{ action.name }} +
+
+
-

Groups

- +
+

Groups

+ +
{{ group.name }}
+
+ +
{ - // Handle error (e.g., display an error message) console.error("Error creating resource:", error); }); } @@ -403,6 +418,14 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } } + openGroupDialog() { + this.dialog.open(ManageGroupModalComponent, { + data: { + project: this.project, + }, + }); + } + openCreateActivityModal() { const dialogRef = this.dialog.open(CreateActivityModalComponent, { data: { @@ -436,6 +459,110 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { this.showResourcesPane = false; } + dropTeacherActionFromAvailable(event: CdkDragDrop) { + const action = this.availableTeacherActions[event.previousIndex]; + + // Call a function to handle creating the teacher task + this.createTeacherTask(action); + } + + dropTeacherAction(event: CdkDragDrop) { + moveItemInArray(this.teacherTasks, event.previousIndex, event.currentIndex); + this.updateTeacherTaskOrder(); + } + + async createTeacherTask(actionData: any) { + try { + let taskData = { + taskID: generateUniqueID(), + name: actionData.name, + activityID: this.selectedActivity!.activityID, + order: this.teacherTasks.length + 1, + type: actionData.type, + // ... other properties you might need for teacher tasks ... + }; + + // Open a modal based on the action type + if (actionData.type === 'activateWorkflow') { + taskData = await this.openWorkflowModal(taskData); + } else if (actionData.type === 'activateAiAgent') { + taskData = await this.openAiAgentModal(taskData); + } else if (actionData.type === 'customPrompt') { + taskData = await this.openCustomPromptModal(taskData); + } + // ... add more cases for other action types as needed ... + + // If taskData is not null (i.e., the modal was not canceled), create the task + if (taskData) { + const newTask = await this.http.post('/api/teacher-tasks', taskData).toPromise(); + this.teacherTasks.push(newTask); + this.updateTeacherTaskOrder(); + } + } catch (error) { + this.snackbarService.queueSnackbar("Error creating teacher task."); + console.error("Error creating teacher task:", error); + } + } + + async openWorkflowModal(taskData: any): Promise { + const dialogRef = this.dialog.open(SelectWorkflowModalComponent, { // Assuming you create this component + data: { + boardID: this.selectedActivity!.boardID, // Pass the board ID + taskData: taskData + } + }); + + return dialogRef.afterClosed().toPromise(); + } + + async openAiAgentModal(taskData: any): Promise { + const dialogRef = this.dialog.open(SelectAiAgentModalComponent, { // Assuming you create this component + data: { + // ... pass any necessary data for AI agent selection ... + taskData: taskData + } + }); + + return dialogRef.afterClosed().toPromise(); + } + + async openCustomPromptModal(taskData: any): Promise { + const dialogRef = this.dialog.open(CustomTeacherPromptModalComponent, { // Assuming you create this component + data: { + // ... pass any necessary data for the custom prompt ... + taskData: taskData + } + }); + + return dialogRef.afterClosed().toPromise(); + } + + async updateTeacherTaskOrder() { + try { + // ... (Implement logic to update teacher task order in the database) ... + } catch (error) { + // ... (error handling) ... + } + } + + async deleteTeacherTask(task: any, index: number) { + try { + // 1. (Optional) Ask for confirmation before deleting + + // 2. Call the API to delete the task + await this.http.delete(`/api/teacher-tasks/delete/${task.taskID}`).toPromise(); + + // 3. Remove the task from the teacherTasks array + this.teacherTasks.splice(index, 1); + + // 4. Update the task order in the database + this.updateTeacherTaskOrder(); + } catch (error) { + this.snackbarService.queueSnackbar("Error deleting teacher task."); + console.error("Error deleting teacher task:", error); + } + } + toggleResourcesPane() { this.showResourcesPane = !this.showResourcesPane; diff --git a/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.html b/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.html new file mode 100644 index 00000000..0c306e70 --- /dev/null +++ b/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.html @@ -0,0 +1 @@ +

select-ai-agent-modal works!

diff --git a/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.scss b/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.spec.ts b/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.spec.ts new file mode 100644 index 00000000..90609496 --- /dev/null +++ b/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectAiAgentModalComponent } from './select-ai-agent-modal.component'; + +describe('SelectAiAgentModalComponent', () => { + let component: SelectAiAgentModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SelectAiAgentModalComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SelectAiAgentModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.ts b/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.ts new file mode 100644 index 00000000..f5d2b685 --- /dev/null +++ b/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-select-ai-agent-modal', + templateUrl: './select-ai-agent-modal.component.html', + styleUrls: ['./select-ai-agent-modal.component.scss'] +}) +export class SelectAiAgentModalComponent { + +} diff --git a/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.html b/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.html new file mode 100644 index 00000000..1fa02239 --- /dev/null +++ b/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.html @@ -0,0 +1 @@ +

select-workflow-modal works!

diff --git a/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.scss b/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.spec.ts b/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.spec.ts new file mode 100644 index 00000000..8bc316e1 --- /dev/null +++ b/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectWorkflowModalComponent } from './select-workflow-modal.component'; + +describe('SelectWorkflowModalComponent', () => { + let component: SelectWorkflowModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SelectWorkflowModalComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SelectWorkflowModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.ts b/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.ts new file mode 100644 index 00000000..9fa98065 --- /dev/null +++ b/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-select-workflow-modal', + templateUrl: './select-workflow-modal.component.html', + styleUrls: ['./select-workflow-modal.component.scss'] +}) +export class SelectWorkflowModalComponent { + +} From a7fc0a7a8b7be50797cd0551382b06d6f2d35de3 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Tue, 21 Jan 2025 16:13:34 -0500 Subject: [PATCH 13/29] Added View Canvas as an additional teacher task and created SCORE view modal for displaying other components; Added support for opening the canvas in the SCORE view modal; Added SCORE authoring links for editing groups, activity groups, and board configurations --- backend/src/api/teacherTasks.ts | 66 ++++++ backend/src/models/TeacherTask.ts | 35 ++++ backend/src/repository/dalTeacherTask.ts | 47 +++++ backend/src/server.ts | 2 + frontend/src/app/app.module.ts | 5 + .../components/canvas/canvas.component.html | 2 +- .../app/components/canvas/canvas.component.ts | 72 +++++-- .../score-authoring.component.html | 52 ++++- .../score-authoring.component.scss | 112 ++++++++++ .../score-authoring.component.ts | 184 +++++++++++++++-- .../score-view-modal.component.html | 9 + .../score-view-modal.component.scss | 26 +++ .../score-view-modal.component.spec.ts | 23 +++ .../score-view-modal.component.ts | 194 ++++++++++++++++++ frontend/src/app/models/teacherTask.ts | 15 ++ 15 files changed, 796 insertions(+), 48 deletions(-) create mode 100644 backend/src/api/teacherTasks.ts create mode 100644 backend/src/models/TeacherTask.ts create mode 100644 backend/src/repository/dalTeacherTask.ts create mode 100644 frontend/src/app/components/score-view-modal/score-view-modal.component.html create mode 100644 frontend/src/app/components/score-view-modal/score-view-modal.component.scss create mode 100644 frontend/src/app/components/score-view-modal/score-view-modal.component.spec.ts create mode 100644 frontend/src/app/components/score-view-modal/score-view-modal.component.ts create mode 100644 frontend/src/app/models/teacherTask.ts diff --git a/backend/src/api/teacherTasks.ts b/backend/src/api/teacherTasks.ts new file mode 100644 index 00000000..fa8d2eda --- /dev/null +++ b/backend/src/api/teacherTasks.ts @@ -0,0 +1,66 @@ +// backend/src/api/teacherTasks.ts + +import express from 'express'; +import dalTeacherTask from '../repository/dalTeacherTask'; +import { TeacherTaskModel } from '../models/TeacherTask'; + +const router = express.Router(); + +// Create a new teacher task +router.post('/', async (req, res) => { + try { + const taskData: TeacherTaskModel = req.body; + const newTask = await dalTeacherTask.create(taskData); + res.status(201).json(newTask); + } catch (error) { + console.error("Error creating teacher task:", error); + res.status(500).json({ error: 'Failed to create teacher task.' }); + } +}); + +// Delete a teacher task +router.delete('/delete/:taskID', async (req, res) => { + try { + const taskID = req.params.taskID; + const deletedTask = await dalTeacherTask.remove(taskID); + + if (deletedTask) { + res.status(200).json(deletedTask); + } else { + res.status(404).json({ error: 'Teacher task not found.' }); + } + } catch (error) { + console.error("Error deleting teacher task:", error); + res.status(500).json({ error: 'Failed to delete teacher task.' }); + } +}); + +// Get all teacher tasks for an activity +router.get('/activity/:activityID', async (req, res) => { + try { + const activityID = req.params.activityID; + const tasks = await dalTeacherTask.getByActivity(activityID); + res.status(200).json(tasks); + } catch (error) { + console.error("Error fetching teacher tasks:", error); + res.status(500).json({ error: 'Failed to fetch teacher tasks.' }); + } +}); + +// Update the order of teacher tasks for an activity +router.post('/order', async (req, res) => { + try { + const activityID = req.body.activityID; + const tasks = req.body.tasks; + const updatePromises = tasks.map((task: any) => + dalTeacherTask.update(task.taskID, { order: task.order }) + ); + await Promise.all(updatePromises); + res.status(200).json({ message: 'Teacher task order updated successfully.' }); + } catch (error) { + console.error("Error updating teacher task order:", error); + res.status(500).json({ error: 'Failed to update teacher task order.' }); + } +}); + +export default router; \ No newline at end of file diff --git a/backend/src/models/TeacherTask.ts b/backend/src/models/TeacherTask.ts new file mode 100644 index 00000000..b082c149 --- /dev/null +++ b/backend/src/models/TeacherTask.ts @@ -0,0 +1,35 @@ +// backend/src/models/TeacherTask.ts + +import { prop, getModelForClass, modelOptions } from '@typegoose/typegoose'; + +@modelOptions({ schemaOptions: { collection: 'teacher-tasks', timestamps: true } }) +export class TeacherTaskModel { + @prop({ required: true }) + public taskID!: string; + + @prop({ required: true }) + public name!: string; + + @prop({ required: true }) + public activityID!: string; + + @prop({ required: true }) + public order!: number; + + @prop({ required: true }) + public type!: string; + + @prop({ required: false }) + public workflowID?: string; + + @prop({ required: false }) + public aiAgentID?: string; + + @prop({ required: false }) + public customPrompt?: string; + + // ... add more properties for different task types as needed ... +} + +const TeacherTask = getModelForClass(TeacherTaskModel); +export default TeacherTask; \ No newline at end of file diff --git a/backend/src/repository/dalTeacherTask.ts b/backend/src/repository/dalTeacherTask.ts new file mode 100644 index 00000000..cca8e47e --- /dev/null +++ b/backend/src/repository/dalTeacherTask.ts @@ -0,0 +1,47 @@ +// backend/src/models/dalTeacherTask.ts + +import TeacherTask, { TeacherTaskModel } from '../models/TeacherTask'; + +const dalTeacherTask = { + getByActivity: async (activityID: string): Promise => { + try { + const tasks = await TeacherTask.find({ activityID }).sort({ order: 1 }); + return tasks; + } catch (error) { + console.error("Error fetching teacher tasks by activity:", error); + return undefined; + } + }, + + create: async (task: TeacherTaskModel): Promise => { + try { + return await TeacherTask.create(task); + } catch (error) { + console.error("Error creating teacher task:", error); + return undefined; + } + }, + + remove: async (id: string): Promise => { + try { + const deletedTask = await TeacherTask.findOneAndDelete({ taskID: id }); + return deletedTask; + } catch (error) { + console.error("Error deleting teacher task:", error); + return undefined; + } + }, + + update: async (id: string, task: Partial): Promise => { + try { + return await TeacherTask.findOneAndUpdate({ taskID: id }, task, { new: true }); + } catch (error) { + console.error("Error updating teacher task:", error); + return undefined; + } + }, + + // ... add other methods for deleting, reordering, etc. as needed ... +}; + +export default dalTeacherTask; \ No newline at end of file diff --git a/backend/src/server.ts b/backend/src/server.ts index e22e99fc..3b5b715d 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -25,6 +25,7 @@ import aiRouter from './api/ai'; import chatHistoryRouter from './api/chatHistory'; import activitiesRouter from './api/activities'; import resourcesRouter from './api/resources'; +import teacherTaskRouter from './api/teacherTasks' dotenv.config(); @@ -75,6 +76,7 @@ app.use('/api/ai', isAuthenticated, aiRouter); app.use('/api/chat-history', chatHistoryRouter); app.use('/api/activities', activitiesRouter); app.use('/api/resources', resourcesRouter); +app.use('/api/teacher-tasks', teacherTaskRouter); app.get('*', (req, res) => { res.sendFile(path.join(staticFilesPath, 'index.html')); diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 08811db3..14655d89 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -71,6 +71,7 @@ import { EditActivityModalComponent } from './components/edit-activity-modal/edi import { SelectWorkflowModalComponent } from './components/select-workflow-modal/select-workflow-modal.component'; import { SelectAiAgentModalComponent } from './components/select-ai-agent-modal/select-ai-agent-modal.component'; import { CustomTeacherPromptModalComponent } from './components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component'; +import { ScoreViewModalComponent } from './components/score-view-modal/score-view-modal.component'; const config: SocketIoConfig = { url: environment.socketUrl, @@ -131,6 +132,10 @@ export function tokenGetter() { SelectWorkflowModalComponent, SelectAiAgentModalComponent, CustomTeacherPromptModalComponent, + ScoreViewModalComponent, + ], + entryComponents: [ + ScoreViewModalComponent, ], imports: [ BrowserModule, diff --git a/frontend/src/app/components/canvas/canvas.component.html b/frontend/src/app/components/canvas/canvas.component.html index 9fd20aad..9ac32cb4 100644 --- a/frontend/src/app/components/canvas/canvas.component.html +++ b/frontend/src/app/components/canvas/canvas.component.html @@ -1,6 +1,6 @@
- +

{{ selectedActivity.name }}

-

Activity Space: {{ selectedBoardName }}

+
+
+

Activity Space: {{ selectedBoardName }}

+ +
+
Resource Assignments
Resource Assignments
Teacher Tasks
-
-
-
- drag_indicator - Task {{ i + 1 }}: {{ task.name }} - +
+
+
+ drag_indicator + Task {{ task.order }}: + {{ getIconForTask(task) }} + {{ task.name }}
+
-
@@ -147,6 +178,7 @@

Teacher Actions

[class.activateWorkflow]="action.type === 'activateWorkflow'" [class.activateAiAgent]="action.type === 'activateAiAgent'" [class.regroupStudents]="action.type === 'regroupStudents'" + [class.viewCanvas]="action.type === 'viewCanvas'" [class.viewBuckets]="action.type === 'viewBuckets'" [class.viewTodos]="action.type === 'viewTodos'" [class.monitorTask]="action.type === 'monitorTask'" diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.scss b/frontend/src/app/components/score-authoring/score-authoring.component.scss index 63accdd8..7315f441 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.scss +++ b/frontend/src/app/components/score-authoring/score-authoring.component.scss @@ -1,3 +1,4 @@ +// score-authoring.component.scss .toolbar { width: 100%; @@ -283,6 +284,11 @@ color: #000; } + &.viewCanvas { + background-color: #ffcc80; + color: #000; + } + &.viewBuckets { background-color: #f48fb1; // Slightly darker red color: #000; @@ -314,6 +320,77 @@ background-color: #f5f5f5; } +.teacher-task-list { + .teacher-task-item { + display: flex; + align-items: center; + height: 40px; + margin-bottom: 5px; + border-radius: 5px; + cursor: pointer; + + .task-content { + flex-grow: 1; + display: flex; + align-items: center; + } + + &.activateWorkflow { + background-color: #90caf9; // Slightly darker blue + color: #000; + } + + &.activateAiAgent { + background-color: #a5d6a7; // Slightly darker green + color: #000; + } + + &.regroupStudents { + background-color: #fff59d; // Slightly darker yellow + color: #000; + cursor: pointer; + } + + &.viewCanvas { + background-color: #ffcc80; + color: #000; + } + + &.viewBuckets { + background-color: #f48fb1; // Slightly darker red + color: #000; + } + + &.viewTodos { + background-color: #b39ddb; // Slightly darker pink + color: #000; + } + + &.monitorTask { + background-color: #ce93d8; // Slightly darker purple + color: #000; + } + + &.customPrompt { + background-color: #bcaaa4; // Slightly darker brown + color: #000; + } + + .drag-handle { + cursor: move; + margin-right: 0.5rem; + } + + button { /* Target the delete button */ + margin-left: auto; // Push the button to the right + } + + .teacher-task-icon { + margin: 0 0.5rem; + } + } +} + /* Style for the drop placeholder */ .cdk-drop-list-dragging .cdk-drop-list-placeholder { background: #ccc; /* Light gray background */ @@ -444,4 +521,39 @@ .classroom-resources-pane { z-index: 999; +} + +.activity-space-header { + display: flex; + justify-content: center; // Center the container horizontally + align-items: center; // Align items to the center vertically + margin-bottom: 1rem; + + .heading-and-settings-container { + display: flex; + align-items: center; // Align the heading and button vertically + + h4 { + margin-bottom: 0; // Remove default margin if needed + margin-right: 0.5rem; // Add some space between heading and button + } + } + + .config-button { + // Add any specific styles for the button if needed + } +} + +:host { + display: block; + padding: 0 !important; // Remove default padding +} + +.mat-dialog-container { + padding: 0 !important; // Remove default padding for dialog container +} + +.mat-dialog-content { + margin: 0 !important; // Remove default margin + max-height: 100vh; // Allow content to take up full height } \ No newline at end of file diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts index dddfd1fc..da411fdf 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.ts +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -24,8 +24,12 @@ import { HttpClient } from '@angular/common/http'; import { Activity } from 'src/app/models/activity'; import { generateUniqueID } from 'src/app/utils/Utils'; import { Resource } from 'src/app/models/resource'; +import { TeacherTask } from 'src/app/models/teacherTask'; import { fabric } from 'fabric'; import { HostListener } from '@angular/core'; +import { ScoreViewModalComponent } from '../score-view-modal/score-view-modal.component'; +import { Board } from 'src/app/models/board' +import { ConfigurationModalComponent } from '../configuration-modal/configuration-modal.component'; @Component({ @@ -68,9 +72,10 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { { name: 'Activate Workflow', type: 'activateWorkflow', icon: 'timeline' }, { name: 'Activate AI Agent', type: 'activateAiAgent', icon: 'smart_toy' }, { name: 'Manually Regroup Students', type: 'regroupStudents', icon: 'group_work' }, + { name: 'View Canvas', type: 'viewCanvas', icon: 'visibility' }, { name: 'View Buckets', type: 'viewBuckets', icon: 'view_module' }, - { name: 'View TODOs', type: 'viewTodos', icon: 'assignment' }, - { name: 'Monitor Task', type: 'monitorTask', icon: 'monitoring' }, + { name: 'View Monitor', type: 'viewTodos', icon: 'assignment' }, + { name: 'Monitor Workspace', type: 'monitorTask', icon: 'monitoring' }, { name: 'Custom Teacher Prompt', type: 'customPrompt', icon: 'chat_bubble' } ]; @@ -202,11 +207,16 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { this.router.navigate(['error']); return; } + + // Load teacher tasks after selecting the first activity + if (this.selectedActivity) { + this.loadTeacherTasks(); + } } async selectActivity(activity: Activity) { this.selectedActivity = activity; - this.showResourcesPane = false; //Close the resources pane + this.showResourcesPane = false; try { // Fetch resources for the selected activity this.selectedActivityResources = await this.http.get(`resources/activity/${activity.activityID}`).toPromise() || []; @@ -217,6 +227,76 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } this.fetchActivityGroups(activity.groupIDs); + + this.loadTeacherTasks(); + } + + async loadTeacherTasks() { + if (!this.selectedActivity) { + console.warn("No activity selected."); + return; + } + + try { + this.teacherTasks = await this.http.get(`teacher-tasks/activity/${this.selectedActivity.activityID}`).toPromise() || []; + this.teacherTasks.sort((a, b) => a.order - b.order); + } catch (error) { + this.snackbarService.queueSnackbar("Error loading teacher tasks."); + console.error("Error loading teacher tasks:", error); + } + } + + async getSelectedBoard(): Promise { + if (this.selectedActivity) { + try { + const board = await this.boardService.get(this.selectedActivity.boardID); + return board; + } catch (error) { + console.error("Error fetching board:", error); + return undefined; + } + } else { + return undefined; + } + } + + handleTeacherTaskClick(task: TeacherTask) { + switch (task.type) { + case 'regroupStudents': + this.openGroupDialog(); + break; + case 'viewCanvas': + case 'viewBuckets': + case 'monitorWorkspaceTask': + case 'workspace': + this.getSelectedBoard().then((board) => { + if (board) { + // Map task.type to componentType + let componentType = ''; + switch (task.type) { + case 'viewCanvas': + componentType = 'canvas'; + break; + case 'viewBuckets': + componentType = 'bucketView'; + break; + case 'viewTodos': + componentType = 'monitor'; + break; + case 'monitorTask': + componentType = 'workspace'; + break; + } + this.openViewModal(componentType, this.project, board); + } else { + console.error('Selected board is undefined'); + } + }); + break; + default: + // Handle other task types or do nothing + break; + } } start(activity: Activity) { @@ -471,6 +551,11 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { this.updateTeacherTaskOrder(); } + getIconForTask(task: any): string { + const action = this.availableTeacherActions.find(a => a.type === task.type); + return action ? action.icon : ''; + } + async createTeacherTask(actionData: any) { try { let taskData = { @@ -484,9 +569,11 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { // Open a modal based on the action type if (actionData.type === 'activateWorkflow') { - taskData = await this.openWorkflowModal(taskData); + const { updatedTaskData, selectedWorkflowId } = await this.openWorkflowModal(taskData); + taskData = updatedTaskData; } else if (actionData.type === 'activateAiAgent') { - taskData = await this.openAiAgentModal(taskData); + const { updatedTaskData, selectedAiAgentId } = await this.openAiAgentModal(taskData); + taskData = updatedTaskData; } else if (actionData.type === 'customPrompt') { taskData = await this.openCustomPromptModal(taskData); } @@ -494,7 +581,7 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { // If taskData is not null (i.e., the modal was not canceled), create the task if (taskData) { - const newTask = await this.http.post('/api/teacher-tasks', taskData).toPromise(); + const newTask = await this.http.post('teacher-tasks/', taskData).toPromise(); this.teacherTasks.push(newTask); this.updateTeacherTaskOrder(); } @@ -503,6 +590,60 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { console.error("Error creating teacher task:", error); } } + + openConfigurationModal() { + if (!this.selectedActivity) { + return; + } + + this.boardService.get(this.selectedActivity.boardID).then((board) => { + if (board) { + this.dialog.open(ConfigurationModalComponent, { + data: { + project: this.project, + board: board, + update: (updatedBoard: Board, removed = false) => { + if (removed) { + // Handle board removal if necessary + this.snackbarService.queueSnackbar('Board removed successfully.'); + // You might want to update your UI here, e.g., by refreshing the activities list + } else { + // Update the board in your component + this.selectedBoardName = updatedBoard.name; // Assuming you want to update the displayed board name + // You might need to update other parts of your UI that depend on the board data + } + }, + }, + }); + } else { + this.snackbarService.queueSnackbar('Error: Could not find selected board.'); + } + }).catch((error) => { + console.error('Error fetching board:', error); + this.snackbarService.queueSnackbar('Error: Could not open configuration.'); + }); + } + + openViewModal(componentType: string, project: Project, board: Board) { + const dialogRef = this.dialog.open(ScoreViewModalComponent, { + maxWidth: '80vw', + maxHeight: '80vh', + height: '80%', + width: '80%', + data: { + componentType, + project, + board, + user: this.user, + projectID: project.projectID, + boardID: board.boardID + } + }); + + dialogRef.afterClosed().subscribe(result => { + // Handle any actions after closing the modal (if needed) + }); + } async openWorkflowModal(taskData: any): Promise { const dialogRef = this.dialog.open(SelectWorkflowModalComponent, { // Assuming you create this component @@ -538,25 +679,34 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } async updateTeacherTaskOrder() { + if (!this.selectedActivity) { + console.warn("No activity selected."); + return; + } + try { - // ... (Implement logic to update teacher task order in the database) ... + const updatedTasks = this.teacherTasks.map((task, index) => ({ + taskID: task.taskID, + order: index + 1, + })); + + await this.http.post('teacher-tasks/order/', { + activityID: this.selectedActivity.activityID, + tasks: updatedTasks + }).toPromise(); + + await this.loadTeacherTasks(); } catch (error) { - // ... (error handling) ... + this.snackbarService.queueSnackbar("Error updating teacher task order."); + console.error("Error updating teacher task order:", error); } } async deleteTeacherTask(task: any, index: number) { try { - // 1. (Optional) Ask for confirmation before deleting - - // 2. Call the API to delete the task - await this.http.delete(`/api/teacher-tasks/delete/${task.taskID}`).toPromise(); - - // 3. Remove the task from the teacherTasks array + await this.http.delete(`teacher-tasks/delete/${task.taskID}`).toPromise(); this.teacherTasks.splice(index, 1); - - // 4. Update the task order in the database - this.updateTeacherTaskOrder(); + this.updateTeacherTaskOrder(); // Update order after deleting a task } catch (error) { this.snackbarService.queueSnackbar("Error deleting teacher task."); console.error("Error deleting teacher task:", error); diff --git a/frontend/src/app/components/score-view-modal/score-view-modal.component.html b/frontend/src/app/components/score-view-modal/score-view-modal.component.html new file mode 100644 index 00000000..19362c89 --- /dev/null +++ b/frontend/src/app/components/score-view-modal/score-view-modal.component.html @@ -0,0 +1,9 @@ +
+

{{ title }}

+ +
+ +
+
\ No newline at end of file diff --git a/frontend/src/app/components/score-view-modal/score-view-modal.component.scss b/frontend/src/app/components/score-view-modal/score-view-modal.component.scss new file mode 100644 index 00000000..f2d83d21 --- /dev/null +++ b/frontend/src/app/components/score-view-modal/score-view-modal.component.scss @@ -0,0 +1,26 @@ +.dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; // Add some spacing between header and content + + .dialog-title { + margin-bottom: 0; // Remove default margin from h2 + } + + .close-button { + // Add any specific styles for the close button if needed + } + } + + // Other styles for mat-dialog-content, etc. (as before) + .mat-dialog-container { + display: flex; + flex-direction: column; + } + + .mat-dialog-content { + flex-grow: 1; // Allow content to take up available space + overflow: auto; // Add scrollbar if content overflows + padding: 0px; // Remove padding to extend to the full width of the modal + } \ No newline at end of file diff --git a/frontend/src/app/components/score-view-modal/score-view-modal.component.spec.ts b/frontend/src/app/components/score-view-modal/score-view-modal.component.spec.ts new file mode 100644 index 00000000..7a95cb96 --- /dev/null +++ b/frontend/src/app/components/score-view-modal/score-view-modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ScoreViewModalComponent } from './score-view-modal.component'; + +describe('ScoreViewModalComponent', () => { + let component: ScoreViewModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ScoreViewModalComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ScoreViewModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/score-view-modal/score-view-modal.component.ts b/frontend/src/app/components/score-view-modal/score-view-modal.component.ts new file mode 100644 index 00000000..20ee092b --- /dev/null +++ b/frontend/src/app/components/score-view-modal/score-view-modal.component.ts @@ -0,0 +1,194 @@ +import { + Component, + OnInit, + Inject, + ViewChild, + ViewContainerRef, + ComponentFactoryResolver, + OnDestroy, + AfterViewInit, + ChangeDetectorRef +} from '@angular/core'; +import { + MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, + MatLegacyDialogRef as MatDialogRef, + MatLegacyDialog as MatDialog, +} from '@angular/material/legacy-dialog'; +import { CanvasComponent } from '../canvas/canvas.component'; +import { CkBucketsComponent } from '../ck-buckets/ck-buckets.component'; +import { CkMonitorComponent } from '../ck-monitor/ck-monitor.component'; +import { CkWorkspaceComponent } from '../ck-workspace/ck-workspace.component'; +import { Project } from 'src/app/models/project'; +import { Board } from 'src/app/models/board'; +import { Subscription, Subject, Observable } from 'rxjs'; +import { BoardService } from 'src/app/services/board.service'; +import { ProjectService } from 'src/app/services/project.service'; +import { SocketService } from 'src/app/services/socket.service'; +import { AuthUser } from 'src/app/models/user'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; + +@Component({ + selector: 'app-score-view-modal', + templateUrl: './score-view-modal.component.html', + styleUrls: ['./score-view-modal.component.scss'], + providers: [ + // Provide a mock ActivatedRoute + { + provide: ActivatedRoute, + useFactory: (data: any) => { + // Create a mock ActivatedRoute with defined properties + const mockActivatedRoute = { + snapshot: { + paramMap: { + get: (key: string) => { + switch (key) { + case 'projectID': + return data.projectID || null; // Return null if undefined + case 'boardID': + return data.boardID || null; // Return null if undefined + default: + return null; + } + }, + has: (key: string) => { + // Check if the key exists in data + return data.hasOwnProperty(key); + }, + }, + queryParams: { + subscribe: (fn: (value: any) => void) => { + fn({}); // Provide mock queryParams + return { unsubscribe: () => {} }; // Return a dummy subscription + }, + }, + }, + queryParams: new Subject(), + }; + return mockActivatedRoute; + }, + deps: [MAT_DIALOG_DATA], // Inject MAT_DIALOG_DATA as a dependency + }, + ], +}) +export class ScoreViewModalComponent implements OnInit, AfterViewInit, OnDestroy { + @ViewChild('viewContainer', { read: ViewContainerRef }) + viewContainer: ViewContainerRef; + + title = ''; + componentRef: any; + project: Project; + board: Board; + user: AuthUser; + + private projectSubscription: Subscription; + private boardSubscription: Subscription; + private queryParamsSubscription: Subscription; + + constructor( + @Inject(MAT_DIALOG_DATA) public data: any, + private componentFactoryResolver: ComponentFactoryResolver, + private boardService: BoardService, + private projectService: ProjectService, + private route: ActivatedRoute, + public dialog: MatDialog, + private cdr: ChangeDetectorRef + ) {} + + ngOnInit(): void { + this.project = this.data.project; + this.board = this.data.board; + this.user = this.data.user; + this.title = `View ${this.data.componentType}`; + + // Subscribe to queryParams changes if needed + this.queryParamsSubscription = this.route.queryParams.subscribe(params => { + // Handle queryParams changes if your component logic depends on it + }); + } + + ngAfterViewInit(): void { + this.loadComponent(); + } + + async loadComponent() { + if (this.data.componentType && this.viewContainer) { + this.viewContainer.clear(); + + let componentToLoad: any; + switch (this.data.componentType) { + case 'canvas': + componentToLoad = CanvasComponent; + break; + case 'bucketView': + componentToLoad = CkBucketsComponent; + break; + case 'monitor': + componentToLoad = CkMonitorComponent; + break; + case 'workspace': + componentToLoad = CkWorkspaceComponent; + break; + default: + console.error('Unknown component type:', this.data.componentType); + return; + } + + const componentFactory = this.componentFactoryResolver.resolveComponentFactory( + componentToLoad + ); + this.componentRef = this.viewContainer.createComponent(componentFactory); + + // Pass necessary data to the dynamically loaded component + this.componentRef.instance.projectID = this.project?.projectID; + this.componentRef.instance.user = this.user; + this.componentRef.instance.boardID = this.data.board?.boardID; // Assuming boardID is needed + + if (componentToLoad === CanvasComponent) { + this.componentRef.instance.isModalView = true; + this.componentRef.instance.onResize(); + } + + // Handle board and project data for specific components + if ( + componentToLoad === CkBucketsComponent || + componentToLoad === CkMonitorComponent + ) { + try { + this.project = await this.projectService.get(this.project.projectID); + this.componentRef.instance.project = this.project; + + const board = await this.boardService.get(this.data.board.boardID); + if (board) { + this.board = board; + this.componentRef.instance.board = this.board; + + if (componentToLoad === CkMonitorComponent) { + this.componentRef.instance.viewType = 'monitor'; + } else if (componentToLoad === CkBucketsComponent) { + this.componentRef.instance.viewType = 'buckets'; + } + } else { + console.error('Board not found for boardID:', this.data.board.boardID); + } + } catch (error) { + console.error('Error fetching project or board:', error); + } + } + } + } + + ngOnDestroy(): void { + if (this.componentRef) { + this.componentRef.destroy(); + } + if (this.projectSubscription) { + this.projectSubscription.unsubscribe(); + } + if (this.boardSubscription) { + this.boardSubscription.unsubscribe(); + } + if (this.queryParamsSubscription) { + this.queryParamsSubscription.unsubscribe(); + } + } +} \ No newline at end of file diff --git a/frontend/src/app/models/teacherTask.ts b/frontend/src/app/models/teacherTask.ts new file mode 100644 index 00000000..9f7a4bed --- /dev/null +++ b/frontend/src/app/models/teacherTask.ts @@ -0,0 +1,15 @@ +// src/app/models/teacher-task.ts + +export interface TeacherTask { + taskID: string; + name: string; + activityID: string; + order: number; + type: string; + workflowID?: string; // Optional + aiAgentID?: string; // Optional + customPrompt?: string; // Optional + // ... add more properties for different task types as needed ... + createdAt?: string; + updatedAt?: string; + } \ No newline at end of file From e1603c2c259be44f7012c44fb67f332c21324ea5 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Tue, 21 Jan 2025 22:12:13 -0500 Subject: [PATCH 14/29] Added bucket and workflow editing to SCORE authoring --- .../score-authoring.component.html | 30 ++++++++ .../score-authoring.component.scss | 39 ++++++++++- .../score-authoring.component.ts | 68 +++++++++++++++++++ .../score-view-modal.component.ts | 2 +- 4 files changed, 137 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index 44d5dc89..c735bf3c 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -53,6 +53,36 @@

Activities

+ +
+

Buckets & Workflows

+ +
+
+ Buckets: +
+
+ {{ bucketCount }} +
+
+ +
+ +
+ Workflows: +
+
+ {{ workflowCount }} +
+
+ +
+
+
diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.scss b/frontend/src/app/components/score-authoring/score-authoring.component.scss index 7315f441..14070c9a 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.scss +++ b/frontend/src/app/components/score-authoring/score-authoring.component.scss @@ -38,6 +38,43 @@ margin-bottom: 2rem; width: 100%; // Make the button take full width } + + .item-info-section { + margin-top: 2rem; // Add some spacing above the section + + h4 { + text-align: left; // Center the heading + margin-bottom: 1rem; // Add spacing below heading + } + + .item-info-grid { + display: grid; + grid-template-columns: auto auto 1fr; + gap: 0.5rem; + align-items: center; + margin-left: 10%; + + .grid-item { + // Add any styling for grid cells if needed + } + + .item-label { + font-weight: bold; + text-align: left; // Left-align labels + } + + .item-count { + text-align: left; // Left-align counts + } + + .add-item-button { + .mat-icon { + color: black; + } + } + } + } + } .activities-list { @@ -409,7 +446,7 @@ display: flex; flex-direction: column; width: 100%; - margin-top: 3rem; + margin-top: 4rem; } .groups-header { diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts index da411fdf..b74722ed 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.ts +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -1,6 +1,7 @@ // score-authoring.component.ts import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { ComponentType } from '@angular/cdk/portal'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { Project } from 'src/app/models/project'; @@ -30,6 +31,9 @@ import { HostListener } from '@angular/core'; import { ScoreViewModalComponent } from '../score-view-modal/score-view-modal.component'; import { Board } from 'src/app/models/board' import { ConfigurationModalComponent } from '../configuration-modal/configuration-modal.component'; +import { CreateWorkflowModalComponent } from '../create-workflow-modal/create-workflow-modal.component'; +import { BucketService } from 'src/app/services/bucket.service'; +import { WorkflowService } from 'src/app/services/workflow.service'; @Component({ @@ -49,6 +53,8 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { selectedActivityGroups: Group[] = []; selectedBoardName = ''; canvas: fabric.Canvas | undefined; + bucketCount: number = 0; + workflowCount: number = 0; allAvailableResources: any[] = [ //define available resources { name: 'Canvas', type: 'canvas' }, @@ -100,6 +106,8 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { private projectService: ProjectService, private boardService: BoardService, private groupService: GroupService, + private bucketService: BucketService, + private workflowService: WorkflowService, private router: Router, private activatedRoute: ActivatedRoute, public dialog: MatDialog, @@ -109,6 +117,9 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { ngOnInit(): void { this.user = this.userService.user!; this.loadScoreAuthoringData(); + if (this.selectedActivity) { + this.updateBucketAndWorkflowCounts(); + } } initializeCanvas() { @@ -229,6 +240,8 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { this.fetchActivityGroups(activity.groupIDs); this.loadTeacherTasks(); + + this.updateBucketAndWorkflowCounts(); } async loadTeacherTasks() { @@ -498,6 +511,29 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } } + async updateBucketAndWorkflowCounts() { + if (!this.selectedActivity) { + return; + } + + try { + const board = await this.boardService.get(this.selectedActivity.boardID); + if (board) { + // Fetch buckets using BucketService + const buckets = await this.bucketService.getAllByBoard(board.boardID); + this.bucketCount = buckets ? buckets.length : 0; + + // Fetch workflows using WorkflowService's getAll() method + const workflows = await this.workflowService.getAll(board.boardID); + this.workflowCount = workflows.length; + } else { + this.snackbarService.queueSnackbar('Error: Could not find selected board.'); + } + } catch (error) { + console.error('Error fetching counts:', error); + } + } + openGroupDialog() { this.dialog.open(ManageGroupModalComponent, { data: { @@ -506,6 +542,38 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { }); } + async openWorkflowBucketModal(selectedTabIndex: number) { + if (!this.selectedActivity) { + return; + } + + // Get the board (similar to how you did it in openConfigurationModal) + try { + const board = await this.boardService.get(this.selectedActivity.boardID); + if (board) { + this._openDialog(CreateWorkflowModalComponent, { + board: board, + project: this.project, + selectedTabIndex: selectedTabIndex, + }); + } else { + this.snackbarService.queueSnackbar('Error: Could not find selected board.'); + } + } catch (error) { + console.error('Error fetching board:', error); + this.snackbarService.queueSnackbar('Error opening Workflows & Buckets modal.'); + } + } + + private _openDialog(component: ComponentType, data: any, width = '700px') { + this.dialog.open(component, { + maxWidth: 1280, + width: width, + autoFocus: false, // Add this line to disable autofocus + data: data, + }); + } + openCreateActivityModal() { const dialogRef = this.dialog.open(CreateActivityModalComponent, { data: { diff --git a/frontend/src/app/components/score-view-modal/score-view-modal.component.ts b/frontend/src/app/components/score-view-modal/score-view-modal.component.ts index 20ee092b..83b0505b 100644 --- a/frontend/src/app/components/score-view-modal/score-view-modal.component.ts +++ b/frontend/src/app/components/score-view-modal/score-view-modal.component.ts @@ -98,7 +98,7 @@ export class ScoreViewModalComponent implements OnInit, AfterViewInit, OnDestroy this.project = this.data.project; this.board = this.data.board; this.user = this.data.user; - this.title = `View ${this.data.componentType}`; + this.title = `${this.data.componentType}`; // Subscribe to queryParams changes if needed this.queryParamsSubscription = this.route.queryParams.subscribe(params => { From 57fa8ade1b7f7cafd21b1c5116890b9cfabf64ac Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Tue, 21 Jan 2025 22:15:36 -0500 Subject: [PATCH 15/29] Renamed Activities to Activity Phases in activity pane --- .../components/score-authoring/score-authoring.component.html | 2 +- .../components/score-authoring/score-authoring.component.scss | 2 +- .../app/components/score-authoring/score-authoring.component.ts | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index c735bf3c..0d556551 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -28,7 +28,7 @@ -

Activities

+

Activity Phases

Date: Sun, 2 Feb 2025 10:06:08 -0500 Subject: [PATCH 16/29] Fixed resizing of canvas in SCORE view modal; Removed heading from bucket view in SCORE view modal; Removed CK Buckets heading from bucket view --- .../app/components/canvas/canvas.component.ts | 3 +- .../ck-buckets/ck-buckets.component.html | 5 +- .../ck-buckets/ck-buckets.component.scss | 2 +- .../ck-buckets/ck-buckets.component.ts | 4 +- .../score-authoring.component.ts | 1 + .../score-view-modal.component.html | 20 ++++---- .../score-view-modal.component.scss | 47 +++++++++---------- .../score-view-modal.component.ts | 40 ++++------------ 8 files changed, 51 insertions(+), 71 deletions(-) diff --git a/frontend/src/app/components/canvas/canvas.component.ts b/frontend/src/app/components/canvas/canvas.component.ts index 239d18dc..58c2a698 100644 --- a/frontend/src/app/components/canvas/canvas.component.ts +++ b/frontend/src/app/components/canvas/canvas.component.ts @@ -610,7 +610,7 @@ export class CanvasComponent implements OnInit, OnDestroy { return; } - this.canvas.setWidth(width); + this.canvas.setWidth(width - 40); this.canvas.setHeight(height); this.canvas.calcOffset(); this.canvas.renderAll(); @@ -657,7 +657,6 @@ export class CanvasComponent implements OnInit, OnDestroy { if (!(isStudentAndVisible || IsTeacherAndVisisble)) { this.updateAuthorNames(post.postID, 'Anonymous'); } else { - console.log('can'); this.userService.getOneById(post.userID).then((user: any) => { this.updateAuthorNames(post.postID, user.username); }); diff --git a/frontend/src/app/components/ck-buckets/ck-buckets.component.html b/frontend/src/app/components/ck-buckets/ck-buckets.component.html index 0b0f85b8..938daa02 100644 --- a/frontend/src/app/components/ck-buckets/ck-buckets.component.html +++ b/frontend/src/app/components/ck-buckets/ck-buckets.component.html @@ -1,6 +1,7 @@ - + @@ -33,7 +35,6 @@
-

CK Buckets

-

{{ title }}

- -
- -
-
\ No newline at end of file +
+
+

{{ title }}

+ +
+ + + +
\ No newline at end of file diff --git a/frontend/src/app/components/score-view-modal/score-view-modal.component.scss b/frontend/src/app/components/score-view-modal/score-view-modal.component.scss index f2d83d21..4d35696b 100644 --- a/frontend/src/app/components/score-view-modal/score-view-modal.component.scss +++ b/frontend/src/app/components/score-view-modal/score-view-modal.component.scss @@ -1,26 +1,25 @@ + .dialog-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; // Add some spacing between header and content - - .dialog-title { - margin-bottom: 0; // Remove default margin from h2 - } - - .close-button { - // Add any specific styles for the close button if needed - } + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + + .dialog-title { + margin-bottom: 0; } - - // Other styles for mat-dialog-content, etc. (as before) - .mat-dialog-container { - display: flex; - flex-direction: column; - } - - .mat-dialog-content { - flex-grow: 1; // Allow content to take up available space - overflow: auto; // Add scrollbar if content overflows - padding: 0px; // Remove padding to extend to the full width of the modal - } \ No newline at end of file +} + +.mat-dialog-container { + display: flex; + flex-direction: column; +} + +.dialog-content { + height: 100%; + overflow: scroll; +} + +.close-button { + cursor: pointer; +} diff --git a/frontend/src/app/components/score-view-modal/score-view-modal.component.ts b/frontend/src/app/components/score-view-modal/score-view-modal.component.ts index 83b0505b..cb86cbac 100644 --- a/frontend/src/app/components/score-view-modal/score-view-modal.component.ts +++ b/frontend/src/app/components/score-view-modal/score-view-modal.component.ts @@ -94,11 +94,16 @@ export class ScoreViewModalComponent implements OnInit, AfterViewInit, OnDestroy private cdr: ChangeDetectorRef ) {} + capitalizeFirstLetter(str: string): string { + if (!str) return ''; // Handle empty or null strings + return str.charAt(0).toUpperCase() + str.slice(1); + } + ngOnInit(): void { this.project = this.data.project; this.board = this.data.board; this.user = this.data.user; - this.title = `${this.data.componentType}`; + this.title = this.capitalizeFirstLetter(this.data.componentType); // Subscribe to queryParams changes if needed this.queryParamsSubscription = this.route.queryParams.subscribe(params => { @@ -138,42 +143,13 @@ export class ScoreViewModalComponent implements OnInit, AfterViewInit, OnDestroy ); this.componentRef = this.viewContainer.createComponent(componentFactory); - // Pass necessary data to the dynamically loaded component - this.componentRef.instance.projectID = this.project?.projectID; - this.componentRef.instance.user = this.user; - this.componentRef.instance.boardID = this.data.board?.boardID; // Assuming boardID is needed + this.componentRef.instance.isModalView = true; if (componentToLoad === CanvasComponent) { - this.componentRef.instance.isModalView = true; this.componentRef.instance.onResize(); } - // Handle board and project data for specific components - if ( - componentToLoad === CkBucketsComponent || - componentToLoad === CkMonitorComponent - ) { - try { - this.project = await this.projectService.get(this.project.projectID); - this.componentRef.instance.project = this.project; - - const board = await this.boardService.get(this.data.board.boardID); - if (board) { - this.board = board; - this.componentRef.instance.board = this.board; - - if (componentToLoad === CkMonitorComponent) { - this.componentRef.instance.viewType = 'monitor'; - } else if (componentToLoad === CkBucketsComponent) { - this.componentRef.instance.viewType = 'buckets'; - } - } else { - console.error('Board not found for boardID:', this.data.board.boardID); - } - } catch (error) { - console.error('Error fetching project or board:', error); - } - } + this.cdr.detectChanges(); } } From 3b829e88a9a9f3408b1a4be6364a656b69bf45b1 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Tue, 4 Feb 2025 15:54:33 -0500 Subject: [PATCH 17/29] Added display of CK Monitor and CK workspace within the SCORE authoring/orchestration environment --- .../ck-monitor/ck-monitor.component.html | 12 +-- .../ck-monitor/ck-monitor.component.scss | 89 +++++++++++++------ .../ck-monitor/ck-monitor.component.ts | 3 + .../ck-workspace/ck-workspace.component.html | 8 +- .../ck-workspace/ck-workspace.component.scss | 41 ++++++--- .../ck-workspace/ck-workspace.component.ts | 3 + .../score-authoring.component.html | 6 +- .../score-authoring.component.scss | 8 +- .../score-authoring.component.ts | 10 +-- 9 files changed, 118 insertions(+), 62 deletions(-) diff --git a/frontend/src/app/components/ck-monitor/ck-monitor.component.html b/frontend/src/app/components/ck-monitor/ck-monitor.component.html index 71b6cd2c..0b59b0fa 100644 --- a/frontend/src/app/components/ck-monitor/ck-monitor.component.html +++ b/frontend/src/app/components/ck-monitor/ck-monitor.component.html @@ -1,7 +1,7 @@
@@ -33,10 +33,11 @@ > diff --git a/frontend/src/app/components/ck-monitor/ck-monitor.component.scss b/frontend/src/app/components/ck-monitor/ck-monitor.component.scss index b8282408..010b871a 100644 --- a/frontend/src/app/components/ck-monitor/ck-monitor.component.scss +++ b/frontend/src/app/components/ck-monitor/ck-monitor.component.scss @@ -1,7 +1,12 @@ @import './../../../../node_modules/swiper/swiper.min.css'; +.spinner { + margin: 70px auto; + background: transparent; +} + .toolbar { - width: 100%; + width: 100%; position: fixed; top: 0; left: 0; @@ -21,12 +26,12 @@ .section-title { padding: 10px 15px; - margin: 0px !important; + margin: 0px !important; } .no-workflow-select { - position: absolute; - top: 30%; + position: absolute; + top: 30%; left: 40%; } @@ -34,7 +39,7 @@ width: 380px; height: 100%; - margin: 20% 10%; + margin: 150px 70px 0px 50px; text-align: center; } @@ -83,14 +88,22 @@ .swiper-slide:nth-child(7n) { background-color: rgb(54, 94, 77); -} +} -.drawer-container { - position: absolute; - left: 0; - right: 0; - bottom: 0; +.ck-monitor-drawer-container { overflow: auto; + display: flex !important; + + &.modal-view { + // Styles for when the component is inside a modal + position: relative; + height: 100%; + + mat-sidenav-content { + height: 100%; + margin-left: 0 !important; + } + } @media (min-width: breakpoint('sm', 'min')) { padding-top: 64px; @@ -98,9 +111,9 @@ } .feature-buttons { - display: flex; - flex-direction: column; - padding: 20px; + display: flex; + flex-direction: column; + padding: 20px; row-gap: 10px; } @@ -114,19 +127,24 @@ margin-left: 10px !important; } -mat-sidenav { - width: 30%; - height: 100%; -} - -mat-sidenav-content { - padding-top: 10px; - display: flex; - flex-direction: column; - align-items: flex-start; - - min-height: 90vh; - overflow: hidden; +.ck-monitor-drawer-container { + mat-sidenav { + width: 30%; + height: 100%; + } + + mat-sidenav-content { + padding-top: 10px; + display: flex; + flex-direction: column; + align-items: flex-start; + + min-height: 90vh; + overflow: hidden; + height: 100%; + flex-grow: 1; + margin-left: 0 !important; + } } .table { @@ -136,7 +154,7 @@ mat-sidenav-content { .graph-toggle { position: absolute; z-index: 99; - + padding: 20px; width: 130px; } @@ -176,4 +194,19 @@ mat-sidenav-content { .todoItemRow:hover { cursor: pointer; background: #f5f5f5; +} + +.workflow-create-post{ + margin: 0; + padding: 0; + line-height: inherit; + height: inherit; + margin: 20px; + background-color: #6c63ff; +} + +.create-post-button-container{ + display: flex; + justify-content: flex-end; + margin-right: 20px; } \ No newline at end of file diff --git a/frontend/src/app/components/ck-monitor/ck-monitor.component.ts b/frontend/src/app/components/ck-monitor/ck-monitor.component.ts index c27970ae..ecd98dc4 100644 --- a/frontend/src/app/components/ck-monitor/ck-monitor.component.ts +++ b/frontend/src/app/components/ck-monitor/ck-monitor.component.ts @@ -2,6 +2,7 @@ import { ComponentType } from '@angular/cdk/overlay'; import { Component, OnDestroy, + Input, OnInit, ViewChild, ViewEncapsulation, @@ -96,6 +97,8 @@ export class CkMonitorComponent implements OnInit, OnDestroy { this.todoDataSource.sort = sort; } + @Input() isModalView = false; + user: AuthUser; group: Group; diff --git a/frontend/src/app/components/ck-workspace/ck-workspace.component.html b/frontend/src/app/components/ck-workspace/ck-workspace.component.html index 68a55b06..2a1cf2bb 100644 --- a/frontend/src/app/components/ck-workspace/ck-workspace.component.html +++ b/frontend/src/app/components/ck-workspace/ck-workspace.component.html @@ -1,7 +1,7 @@
@@ -32,14 +32,14 @@ [project]="project" > - + diff --git a/frontend/src/app/components/ck-workspace/ck-workspace.component.scss b/frontend/src/app/components/ck-workspace/ck-workspace.component.scss index 6c0a16ab..1df6c570 100644 --- a/frontend/src/app/components/ck-workspace/ck-workspace.component.scss +++ b/frontend/src/app/components/ck-workspace/ck-workspace.component.scss @@ -85,7 +85,7 @@ background-color: rgb(54, 94, 77); } -.drawer-container { +.ck-workspace-drawer-container { position: absolute; padding-top: 56px; left: 0; @@ -93,6 +93,17 @@ bottom: 0; overflow: auto; + &.modal-view { + // Styles for when the component is inside a modal + position: relative; // Reset to default + height: 100%; // Take up full height of parent (dialog content) + overflow: auto; + + mat-sidenav-content { + height: 100%; + } + } + @media (min-width: breakpoint('sm', 'min')) { padding-top: 64px; } @@ -103,19 +114,21 @@ text-align: left !important; } -mat-sidenav { - width: 30%; - height: 100%; -} - -mat-sidenav-content { - padding: 10px; - display: flex; - flex-direction: column; - align-items: flex-start; - - min-height: 90vh; - overflow: hidden; +.ck-workspace-drawer-container { + mat-sidenav { + width: 30%; + height: 100%; + } + + mat-sidenav-content { + padding: 10px; + display: flex; + flex-direction: column; + align-items: flex-start; + + min-height: 90vh; + overflow: hidden; + } } .workflow-create-post{ diff --git a/frontend/src/app/components/ck-workspace/ck-workspace.component.ts b/frontend/src/app/components/ck-workspace/ck-workspace.component.ts index 42289849..4732ca68 100644 --- a/frontend/src/app/components/ck-workspace/ck-workspace.component.ts +++ b/frontend/src/app/components/ck-workspace/ck-workspace.component.ts @@ -2,6 +2,7 @@ import { ComponentType } from '@angular/cdk/overlay'; import { Component, OnDestroy, + Input, OnInit, ViewChild, ViewEncapsulation, @@ -60,6 +61,8 @@ SwiperCore.use([EffectCards]); export class CkWorkspaceComponent implements OnInit, OnDestroy { @ViewChild(SwiperComponent) swiper: SwiperComponent; + @Input() isModalView = false; + loading = false; showInactive = true; diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index 0d556551..83d1b285 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -149,7 +149,7 @@
Teacher Tasks
[class.viewCanvas]="task.type === 'viewCanvas'" [class.viewBuckets]="task.type === 'viewBuckets'" [class.viewTodos]="task.type === 'viewTodos'" - [class.monitorTask]="task.type === 'monitorTask'" + [class.viewWorkspace]="task.type === 'viewWorkspace'" [class.customPrompt]="task.type === 'customPrompt'" >
Teacher Tasks
- +
+ +
\ No newline at end of file diff --git a/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.ts b/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.ts index 7997fcb3..4a45e79a 100644 --- a/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.ts +++ b/frontend/src/app/components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component.ts @@ -1,10 +1,35 @@ -import { Component } from '@angular/core'; +// custom-teacher-prompt-modal.component.ts +import { Component, Inject, OnInit } from '@angular/core'; // Import OnInit +import { + MatLegacyDialogRef as MatDialogRef, + MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, +} from '@angular/material/legacy-dialog'; @Component({ selector: 'app-custom-teacher-prompt-modal', templateUrl: './custom-teacher-prompt-modal.component.html', - styleUrls: ['./custom-teacher-prompt-modal.component.scss'] + styleUrls: ['./custom-teacher-prompt-modal.component.scss'], }) -export class CustomTeacherPromptModalComponent { +export class CustomTeacherPromptModalComponent implements OnInit { // Implement OnInit + prompt: string = ''; -} + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { prompt: string } // Type the data + ) {} + + ngOnInit(): void { + // Initialize the prompt with the value from data, if provided + if (this.data && this.data.prompt) { + this.prompt = this.data.prompt; + } + } + + onNoClick(): void { + this.dialogRef.close(); + } + + onSave(): void { + this.dialogRef.close(this.prompt); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index 83d1b285..2b866af8 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -77,7 +77,7 @@

Buckets & Workflows

{{ workflowCount }}
-
@@ -159,7 +159,17 @@
Teacher Tasks
drag_indicator Task {{ task.order }}: {{ getIconForTask(task) }} - {{ task.name }} + Prompt: {{ task.customPrompt || task.name }} + + Activate Workflow: {{ getWorkflowName(task.workflowID) }} + + {{ (task.type !== 'customPrompt' && task.type !== 'activateWorkflow') ? task.name : '' }} + +
+ +
+ +
+ + +
\ No newline at end of file diff --git a/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.ts b/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.ts index 9fa98065..074d334f 100644 --- a/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.ts +++ b/frontend/src/app/components/select-workflow-modal/select-workflow-modal.component.ts @@ -1,10 +1,49 @@ -import { Component } from '@angular/core'; +import { Component, Inject, OnInit } from '@angular/core'; +import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA } from '@angular/material/legacy-dialog'; +import { WorkflowService } from 'src/app/services/workflow.service'; +import { Workflow } from 'src/app/models/workflow'; // Import the Workflow interface @Component({ selector: 'app-select-workflow-modal', templateUrl: './select-workflow-modal.component.html', styleUrls: ['./select-workflow-modal.component.scss'] }) -export class SelectWorkflowModalComponent { +export class SelectWorkflowModalComponent implements OnInit { -} + workflows: Workflow[] = []; // Use the Workflow interface + selectedWorkflowId: string | null = null; // Allow null for no selection + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { boardID: string }, // Simplify data + private workflowService: WorkflowService, + ) {} + + ngOnInit(): void { + this.loadWorkflows(); + } + + async loadWorkflows() { + if (this.data.boardID) { + try { + this.workflows = await this.workflowService.getAll(this.data.boardID); + } catch (error) { + console.error('Error loading workflows:', error); + // Handle error (e.g., display an error message to the user) + } + } + } + + onNoClick(): void { + this.dialogRef.close(null); // Explicitly close with null (consistent with CustomTeacherPromptModalComponent) + } + + onSelect(): void { + this.dialogRef.close(this.selectedWorkflowId); // Close with the selected ID (or null) + } + + openCreateWorkflowModal() { + // Close the current dialog (SelectWorkflowModalComponent) + this.dialogRef.close({ shouldOpenCreateModal: true }); + } +} \ No newline at end of file From 9e75808cd9c5147bc5c98ade7873fb9b440ea656 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Wed, 5 Feb 2025 15:56:33 -0500 Subject: [PATCH 19/29] Added show join code teacher task; Set all teacher tasks as clickable --- frontend/src/app/app.module.ts | 2 ++ .../score-authoring.component.html | 11 ++++--- .../score-authoring.component.scss | 10 +++++++ .../score-authoring.component.ts | 29 +++++++++++++++---- .../show-join-code.component.html | 7 +++++ .../show-join-code.component.scss | 20 +++++++++++++ .../show-join-code.component.spec.ts | 23 +++++++++++++++ .../show-join-code.component.ts | 19 ++++++++++++ 8 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 frontend/src/app/components/show-join-code/show-join-code.component.html create mode 100644 frontend/src/app/components/show-join-code/show-join-code.component.scss create mode 100644 frontend/src/app/components/show-join-code/show-join-code.component.spec.ts create mode 100644 frontend/src/app/components/show-join-code/show-join-code.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 14655d89..6fbcec3d 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -72,6 +72,7 @@ import { SelectWorkflowModalComponent } from './components/select-workflow-modal import { SelectAiAgentModalComponent } from './components/select-ai-agent-modal/select-ai-agent-modal.component'; import { CustomTeacherPromptModalComponent } from './components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component'; import { ScoreViewModalComponent } from './components/score-view-modal/score-view-modal.component'; +import { ShowJoinCodeComponent } from './components/show-join-code/show-join-code.component'; const config: SocketIoConfig = { url: environment.socketUrl, @@ -133,6 +134,7 @@ export function tokenGetter() { SelectAiAgentModalComponent, CustomTeacherPromptModalComponent, ScoreViewModalComponent, + ShowJoinCodeComponent, ], entryComponents: [ ScoreViewModalComponent, diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index 2b866af8..ee75e5ab 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -151,6 +151,7 @@
Teacher Tasks
[class.viewTodos]="task.type === 'viewTodos'" [class.viewWorkspace]="task.type === 'viewWorkspace'" [class.customPrompt]="task.type === 'customPrompt'" + [class.showJoinCode]="task.type === 'showJoinCode'" >
Teacher Tasks Activate Workflow: {{ getWorkflowName(task.workflowID) }} + + Show Join Code + {{ (task.type !== 'customPrompt' && task.type !== 'activateWorkflow') ? task.name : '' }} - -
+
\ No newline at end of file diff --git a/frontend/src/app/components/show-join-code/show-join-code.component.scss b/frontend/src/app/components/show-join-code/show-join-code.component.scss new file mode 100644 index 00000000..6bdcc97b --- /dev/null +++ b/frontend/src/app/components/show-join-code/show-join-code.component.scss @@ -0,0 +1,20 @@ +.join-code { + font-size: 6em; + font-weight: bold; + text-align: center; + word-break: break-all; +} + +.dialog-content { + display: flex; // Use flexbox + flex-direction: column; // Stack items vertically + justify-content: center; // Center vertically + align-items: center; // Center horizontally (for good measure) + min-height: 200px; // Set a minimum height - IMPORTANT! + padding: 24px 24px 0px 24px; // Add padding around the content +} + +// Style the mat-dialog-actions to push button to bottom and add margin +.mat-dialog-actions { + margin-bottom: 12px; // Add some space below buttons. +} \ No newline at end of file diff --git a/frontend/src/app/components/show-join-code/show-join-code.component.spec.ts b/frontend/src/app/components/show-join-code/show-join-code.component.spec.ts new file mode 100644 index 00000000..3eac432a --- /dev/null +++ b/frontend/src/app/components/show-join-code/show-join-code.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ShowJoinCodeComponent } from './show-join-code.component'; + +describe('ShowJoinCodeComponent', () => { + let component: ShowJoinCodeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ShowJoinCodeComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ShowJoinCodeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/show-join-code/show-join-code.component.ts b/frontend/src/app/components/show-join-code/show-join-code.component.ts new file mode 100644 index 00000000..8fffee3d --- /dev/null +++ b/frontend/src/app/components/show-join-code/show-join-code.component.ts @@ -0,0 +1,19 @@ +// src/app/components/show-join-code/show-join-code.component.ts +import { Component, Inject } from '@angular/core'; +import { MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, MatLegacyDialogRef as MatDialogRef } from '@angular/material/legacy-dialog'; + +@Component({ + selector: 'app-show-join-code', + templateUrl: './show-join-code.component.html', + styleUrls: ['./show-join-code.component.scss'] +}) +export class ShowJoinCodeComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { joinCode: string } + ) {} + + close(): void { + this.dialogRef.close(); + } +} \ No newline at end of file From 242b6760bd677017b8b9d5c2d8f524d27deee6d3 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Wed, 5 Feb 2025 16:20:01 -0500 Subject: [PATCH 20/29] Set Teacher Tasks as an expandable module in SCORE authoring --- frontend/src/app/app.module.ts | 2 + .../score-authoring.component.html | 75 ++++++++++--------- .../score-authoring.component.scss | 5 ++ .../score-authoring.component.ts | 7 +- 4 files changed, 54 insertions(+), 35 deletions(-) diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 6fbcec3d..4018d94c 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -73,6 +73,8 @@ import { SelectAiAgentModalComponent } from './components/select-ai-agent-modal/ import { CustomTeacherPromptModalComponent } from './components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component'; import { ScoreViewModalComponent } from './components/score-view-modal/score-view-modal.component'; import { ShowJoinCodeComponent } from './components/show-join-code/show-join-code.component'; +import { MatExpansionModule } from '@angular/material/expansion'; + const config: SocketIoConfig = { url: environment.socketUrl, diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index ee75e5ab..8238cf20 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -137,43 +137,50 @@
Resource Assignments
-
Teacher Tasks
-
-
-
+ + +
Teacher Tasks
+
+
+ +
+
- drag_indicator - Task {{ task.order }}: - {{ getIconForTask(task) }} - Prompt: {{ task.customPrompt || task.name }} - - Activate Workflow: {{ getWorkflowName(task.workflowID) }} - - - Show Join Code - - {{ (task.type !== 'customPrompt' && task.type !== 'activateWorkflow') ? task.name : '' }} +
+ drag_indicator + Task {{ task.order }}: + {{ getIconForTask(task) }} + Prompt: {{ task.customPrompt || task.name }} + + Activate Workflow: {{ getWorkflowName(task.workflowID) }} + + + Show Join Code + + {{ (task.type !== 'customPrompt' && task.type !== 'activateWorkflow') ? task.name : '' }} +
+
-
-
+
diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.scss b/frontend/src/app/components/score-authoring/score-authoring.component.scss index 734569b4..e7ead30a 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.scss +++ b/frontend/src/app/components/score-authoring/score-authoring.component.scss @@ -169,6 +169,7 @@ margin-left: auto; /* Push the button to the right */ } } + } h3, h4 { /* Target both h3 and h4 */ @@ -195,6 +196,10 @@ margin-bottom: 0.5rem; } + mat-expansion-panel-header h5 { + margin-bottom: 0; + } + .canvas-container { position: absolute; top: 64px; diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts index 2bf63cf1..3b72af62 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.ts +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -34,7 +34,8 @@ import { ConfigurationModalComponent } from '../configuration-modal/configuratio import { CreateWorkflowModalComponent } from '../create-workflow-modal/create-workflow-modal.component'; import { BucketService } from 'src/app/services/bucket.service'; import { WorkflowService } from 'src/app/services/workflow.service'; -import { ShowJoinCodeComponent } from '../show-join-code/show-join-code.component'; // Import +import { ShowJoinCodeComponent } from '../show-join-code/show-join-code.component'; +import {MatExpansionModule} from '@angular/material/expansion'; @Component({ @@ -95,6 +96,8 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { showResourcesPane = false; showClassroomBindings = false; + isTeacherTasksExpanded: boolean = true; + @HostListener('window:resize', ['$event']) onResize(event: any) { if (this.showClassroomBindings) { @@ -703,6 +706,8 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { const newTask = await this.http.post('teacher-tasks/', taskData).toPromise(); this.teacherTasks.push(newTask); this.updateTeacherTaskOrder(); + + this.isTeacherTasksExpanded = true; } } catch (error) { this.snackbarService.queueSnackbar("Error creating teacher task."); From cbbaea3c0fa26343049a904e95589def6d052299 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Mon, 10 Feb 2025 12:03:19 -0500 Subject: [PATCH 21/29] Created AI Agent model, AI Agent class, sub-class agents, ai-agent configuration file, and modal in score-authoring for authoring AI agent subclasses in an activity --- backend/src/api/aiAgents.ts | 97 +++ backend/src/models/AiAgent.ts | 74 ++ backend/src/models/Resource.ts | 3 + backend/src/repository/dalAiAgent.ts | 47 + backend/src/server.ts | 2 + frontend/src/app/app.module.ts | 5 +- .../configure-ai-agent-modal.component.html | 64 ++ .../configure-ai-agent-modal.component.scss | 5 + ...configure-ai-agent-modal.component.spec.ts | 23 + .../configure-ai-agent-modal.component.ts | 206 +++++ .../score-authoring.component.html | 620 ++++++++------ .../score-authoring.component.scss | 32 + .../score-authoring.component.ts | 801 ++++++++++++------ .../select-ai-agent-modal.component.html | 1 - .../select-ai-agent-modal.component.scss | 0 .../select-ai-agent-modal.component.spec.ts | 23 - .../select-ai-agent-modal.component.ts | 10 - frontend/src/app/models/ai-agent-config.ts | 773 +++++++++++++++++ frontend/src/app/models/ai-agent.ts | 134 +++ frontend/src/app/models/bucket.ts | 20 +- frontend/src/app/models/resource.ts | 1 + 21 files changed, 2390 insertions(+), 551 deletions(-) create mode 100644 backend/src/api/aiAgents.ts create mode 100644 backend/src/models/AiAgent.ts create mode 100644 backend/src/repository/dalAiAgent.ts create mode 100644 frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.html create mode 100644 frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.scss create mode 100644 frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.spec.ts create mode 100644 frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.ts delete mode 100644 frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.html delete mode 100644 frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.scss delete mode 100644 frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.spec.ts delete mode 100644 frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.ts create mode 100644 frontend/src/app/models/ai-agent-config.ts create mode 100644 frontend/src/app/models/ai-agent.ts diff --git a/backend/src/api/aiAgents.ts b/backend/src/api/aiAgents.ts new file mode 100644 index 00000000..e3bd03c4 --- /dev/null +++ b/backend/src/api/aiAgents.ts @@ -0,0 +1,97 @@ +// src/api/aiAgents.ts +import express from 'express'; +import dalAiAgent from '../repository/dalAiAgent'; +import AiAgent, { AiAgentModel } from '../models/AiAgent'; + +const router = express.Router(); + +// Create a new AI agent +router.post('/', async (req, res) => { + try { + const agentData: AiAgentModel = req.body; + const newAgent = await dalAiAgent.create(agentData); + res.status(201).json(newAgent); + } catch (error) { + console.error("Error creating AI agent:", error); + res.status(500).json({ error: 'Failed to create AI agent.' }); + } +}); + +// Get all AI agents for an activity +router.get('/activity/:activityID', async (req, res) => { + try { + const activityID = req.params.activityID; + const agents = await dalAiAgent.getByActivity(activityID); + res.status(200).json(agents); + } catch (error) { + console.error("Error fetching AI agents:", error); + res.status(500).json({ error: 'Failed to fetch AI agents.' }); + } +}); + +// Update an AI agent +router.put('/:aiAgentID', async (req, res) => { + try { + const aiAgentID = req.params.aiAgentID; + const updatedData: Partial = req.body; + const updatedAgent = await dalAiAgent.update(aiAgentID, updatedData); + if (updatedAgent) { + res.status(200).json(updatedAgent); + } else { + res.status(404).json({ error: 'AI Agent not found.' }); + } + } catch (error) { + console.error("Error updating AI agent:", error); + res.status(500).json({ error: 'Failed to update AI agent.' }); + } +}); + +// Delete an AI agent +router.delete('/:aiAgentID', async (req, res) => { + try { + const aiAgentID = req.params.aiAgentID; + const deletedAgent = await dalAiAgent.remove(aiAgentID); + if (deletedAgent) { + res.status(200).json(deletedAgent); + } else { + res.status(404).json({ error: 'AI Agent not found.' }); + } + } catch (error) { + console.error("Error deleting AI agent:", error); + res.status(500).json({ error: 'Failed to delete AI agent.' }); + } +}); + +// Get a single AI Agent by ID. Good for editing. +router.get('/:aiAgentID', async (req, res) => { + try { + const aiAgentID = req.params.aiAgentID; + const agent = await dalAiAgent.getById(aiAgentID); + if (agent) { + res.status(200).json(agent); + } else { + res.status(404).json({ error: 'AI Agent not found.' }); + } + } catch (error) { + console.error("Error getting AI agent:", error); + res.status(500).json({ error: "Failed to get AI Agent."}) + } +}); + +//Update order +router.post('/order', async (req, res) => { + try { + const activityID = req.body.activityID; //used for scoping + const agents: { aiAgentID: string; order: number }[] = req.body.agents; + const updatePromises = agents.map(agent => + dalAiAgent.update(agent.aiAgentID, { order: agent.order }) + ); + await Promise.all(updatePromises); + res.status(200).json({ message: 'AI Agent order updated successfully.' }); + } catch (error) { + console.error("Error updating AI Agent order:", error); + res.status(500).json({ error: 'Failed to update AI Agent order.' }); + } + }); + +export default router; \ No newline at end of file diff --git a/backend/src/models/AiAgent.ts b/backend/src/models/AiAgent.ts new file mode 100644 index 00000000..8e822604 --- /dev/null +++ b/backend/src/models/AiAgent.ts @@ -0,0 +1,74 @@ +// src/models/AiAgent.ts +import { prop, getModelForClass, modelOptions } from '@typegoose/typegoose'; + +@modelOptions({ schemaOptions: { collection: 'aiagents', timestamps: true } }) +export class AiAgentModel { + @prop({ required: true }) + public aiAgentID!: string; + + @prop({ required: true }) + public activityID!: string; + + @prop({ required: true }) + public name!: string; + + @prop({ required: true }) + public type!: string; // "teacher", "idea_chat", "idea_ambient", "personal_learning", "group_interaction", "workflow" + + @prop() + public persona?: string; // Optional, use ? + + @prop() + public agentType?: string; // "chat" or "ambient" + + @prop() + public trigger?: string; // "chat", "manual", "subscription", "event" + + @prop({ type: () => [String] }) // Array of strings + public triggerEventTypes?: string[]; + + @prop() + public eventThreshold?: number; + + @prop() + public aiPublishChannel?: string; + + @prop() + public aiSubscriptionChannel?: string; + + @prop({ type: () => [String] }) // Array of strings + public payloadScope?: string[]; + + @prop() + public userScope?: string; + + @prop() + public task?: string; + + @prop() + public databaseWriteAccess?: boolean; + + @prop({ type: () => [String] }) // Array of strings + public uiIntegrations?: string[]; + + @prop() + public enabled?: boolean; + + @prop() + public topic?: string; + + @prop() + public criteriaToGroupStudents?: string; + + @prop({ type: () => [String] }) // Array of strings. + public workflowsToActivate?: string[]; + + @prop() + public criteriaToActivateWorkflow?: string; + + @prop({ required: true, default: 0 }) //for ordering + public order!: number; +} + +const AiAgent = getModelForClass(AiAgentModel); +export default AiAgent; \ No newline at end of file diff --git a/backend/src/models/Resource.ts b/backend/src/models/Resource.ts index b291756d..01cd1b6c 100644 --- a/backend/src/models/Resource.ts +++ b/backend/src/models/Resource.ts @@ -28,6 +28,9 @@ export class ResourceModel { @prop({ required: true, default: false }) public monitor!: boolean; + @prop({ required: true, default: false }) + public ideaAgent!: boolean; + @prop({ required: true, type: () => [String], default: [] }) public groupIDs!: string[]; diff --git a/backend/src/repository/dalAiAgent.ts b/backend/src/repository/dalAiAgent.ts new file mode 100644 index 00000000..5c03dee4 --- /dev/null +++ b/backend/src/repository/dalAiAgent.ts @@ -0,0 +1,47 @@ +// src/repository/dalAiAgent.ts +import AiAgent, { AiAgentModel } from '../models/AiAgent'; + +const dalAiAgent = { + create: async (agentData: AiAgentModel): Promise => { + try { + const newAgent = new AiAgent(agentData); + return await newAgent.save(); + } catch (error) { + throw error; // Or handle more specifically + } + }, + + getByActivity: async (activityID: string): Promise => { + try { + return await AiAgent.find({ activityID }); + } catch (error) { + throw error; + } + }, + + update: async (aiAgentID: string, agentData: Partial): Promise => { + try { + return await AiAgent.findOneAndUpdate({ aiAgentID }, agentData, { new: true }); + } catch (error) { + throw error; + } + }, + + remove: async (aiAgentID: string): Promise => { + try { + return await AiAgent.findOneAndDelete({ aiAgentID }); + } catch (error) { + throw error; + } + }, + getById: async (aiAgentID: string): Promise => { + try { + return await AiAgent.findOne({ aiAgentID }); + } catch (error) { + console.error("Error getting agent by ID:", error); + return null; // Or re-throw, depending on your error handling strategy + } + }, +}; + +export default dalAiAgent; \ No newline at end of file diff --git a/backend/src/server.ts b/backend/src/server.ts index 3b5b715d..720e3d3d 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -26,6 +26,7 @@ import chatHistoryRouter from './api/chatHistory'; import activitiesRouter from './api/activities'; import resourcesRouter from './api/resources'; import teacherTaskRouter from './api/teacherTasks' +import aiAgentRoutes from './api/aiAgents'; dotenv.config(); @@ -73,6 +74,7 @@ app.use('/api/trace', isAuthenticated, trace); app.use('/api/todoItems', isAuthenticated, todoItems); app.use('/api/learner', isAuthenticated, learner); app.use('/api/ai', isAuthenticated, aiRouter); +app.use('/api/ai-agents', aiAgentRoutes); app.use('/api/chat-history', chatHistoryRouter); app.use('/api/activities', activitiesRouter); app.use('/api/resources', resourcesRouter); diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 4018d94c..59f610e6 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -69,11 +69,10 @@ import { ScoreAuthoringComponent } from './components/score-authoring/score-auth import { CreateActivityModalComponent } from './components/create-activity-modal/create-activity-modal.component'; import { EditActivityModalComponent } from './components/edit-activity-modal/edit-activity-modal.component'; import { SelectWorkflowModalComponent } from './components/select-workflow-modal/select-workflow-modal.component'; -import { SelectAiAgentModalComponent } from './components/select-ai-agent-modal/select-ai-agent-modal.component'; +import { ConfigureAiAgentModalComponent } from './components/configure-ai-agent-modal/configure-ai-agent-modal.component'; import { CustomTeacherPromptModalComponent } from './components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component'; import { ScoreViewModalComponent } from './components/score-view-modal/score-view-modal.component'; import { ShowJoinCodeComponent } from './components/show-join-code/show-join-code.component'; -import { MatExpansionModule } from '@angular/material/expansion'; const config: SocketIoConfig = { @@ -133,7 +132,7 @@ export function tokenGetter() { CreateActivityModalComponent, EditActivityModalComponent, SelectWorkflowModalComponent, - SelectAiAgentModalComponent, + ConfigureAiAgentModalComponent, CustomTeacherPromptModalComponent, ScoreViewModalComponent, ShowJoinCodeComponent, diff --git a/frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.html b/frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.html new file mode 100644 index 00000000..a1709101 --- /dev/null +++ b/frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.html @@ -0,0 +1,64 @@ +

Configure AI Agent: {{ agentInstance.name }}

+
+
+
+ + + {{ agentConfig[field[0]].label }} + + + {{ agentConfig[field[0]].label }} is required. + + {{ agentConfig[field[0]].tooltip }} + + + + {{ agentConfig[field[0]].label }} + + + {{ agentConfig[field[0]].label }} is required. + + {{ agentConfig[field[0]].tooltip }} + + + + {{ agentConfig[field[0]].label }} + + + {{ option.label }} + + + + {{ agentConfig[field[0]].label }} is required. + + {{ agentConfig[field[0]].tooltip }} + + + + {{ agentConfig[field[0]].label }} + + + {{ option.label }} + + + + {{ agentConfig[field[0]].label }} is required. + + + Invalid workflow ID(s) selected. + + + Server Error. + + {{ agentConfig[field[0]].tooltip }} + + + +
+ +
+ + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.scss b/frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.scss new file mode 100644 index 00000000..d85d6547 --- /dev/null +++ b/frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.scss @@ -0,0 +1,5 @@ +// configure-ai-agent-modal.component.scss + +.mat-form-field { + margin-bottom: 1rem; // Add some spacing between form fields + } \ No newline at end of file diff --git a/frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.spec.ts b/frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.spec.ts new file mode 100644 index 00000000..28029569 --- /dev/null +++ b/frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConfigureAiAgentModalComponent } from './configure-ai-agent-modal.component'; + +describe('ConfigureAiAgentModalComponent', () => { + let component: ConfigureAiAgentModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ConfigureAiAgentModalComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ConfigureAiAgentModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.ts b/frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.ts new file mode 100644 index 00000000..a04cd0c5 --- /dev/null +++ b/frontend/src/app/components/configure-ai-agent-modal/configure-ai-agent-modal.component.ts @@ -0,0 +1,206 @@ +import { Component, Inject, OnInit, NgZone } from '@angular/core'; +import { FormBuilder, FormGroup, Validators, FormControl, AbstractControl, ValidationErrors, ValidatorFn, AsyncValidatorFn } from '@angular/forms'; +import { MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, MatLegacyDialogRef as MatDialogRef } from '@angular/material/legacy-dialog'; +import { AiAgent, TeacherAgent, IdeaAgentChat, IdeaAgentAmbient, PersonalLearningAgent, GroupInteractionAgent, WorkflowAgent } from 'src/app/models/ai-agent'; +import { AI_AGENT_CONFIG, AiAgentTypeConfig, AiAgentFieldConfig } from 'src/app/models/ai-agent-config'; +import { WorkflowService } from 'src/app/services/workflow.service'; +import { Workflow } from 'src/app/models/workflow'; +import { BucketService } from 'src/app/services/bucket.service'; + +@Component({ + selector: 'app-configure-ai-agent-modal', + templateUrl: './configure-ai-agent-modal.component.html', + styleUrls: ['./configure-ai-agent-modal.component.scss'] +}) +export class ConfigureAiAgentModalComponent implements OnInit { + agentForm: FormGroup; + agentConfig: AiAgentTypeConfig; // Use the new type + agentInstance: AiAgent; // Keep this for the name in the title. + agentType: string; + workflows: Workflow[] = []; //To store workflows + sortedFieldConfigs: [string, AiAgentFieldConfig][]; // Array to store sorted fields + + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { agentType: string; topic?: string, enabled?: boolean, payloadScope?: string[], boardId: string }, // Add boardId + private fb: FormBuilder, + private workflowService: WorkflowService, // Inject WorkflowService + private bucketService: BucketService, //Inject BucketService + private zone: NgZone + ) {} + + async ngOnInit(): Promise { + // 1. Get agent configuration based on agentType + this.agentConfig = AI_AGENT_CONFIG[this.data.agentType]; + if (!this.agentConfig) { + throw new Error(`Invalid agent type: ${this.data.agentType}`); + } + this.agentType = this.data.agentType; + + // 2. Create agent instance with appropriate initial data + let initialData: Partial = {}; + if (this.data.agentType === 'idea_ambient') { + initialData = { + topic: this.data.topic, + enabled: this.data.enabled, + payloadScope: this.data.payloadScope + }; + } + //Dynamically create agent instance + switch (this.data.agentType) { + case 'teacher': + this.agentInstance = new TeacherAgent(initialData); + break; + case 'idea_chat': + this.agentInstance = new IdeaAgentChat(initialData); + break; + case 'idea_ambient': + this.agentInstance = new IdeaAgentAmbient(initialData); + break; + case 'personal_learning': + this.agentInstance = new PersonalLearningAgent(initialData); + break; + case 'group_interaction': + this.agentInstance = new GroupInteractionAgent(initialData); + break; + case 'workflow': + this.agentInstance = new WorkflowAgent(initialData); + break; + default: + throw new Error(`Invalid agent type: ${this.data.agentType}`); + } + //Load Workflows for the workflow agent + if(this.data.agentType == 'workflow'){ + await this.loadWorkflows(); + } + + //Load buckets for payload scope + await this.loadBuckets(); + // 3. Sort fields by order + this.sortedFieldConfigs = Object.entries(this.agentConfig).sort(([, a], [, b]) => a.order - b.order); + + // 4. Create the form + this.zone.run(() => { + this.createForm(); + }); + } + + async loadWorkflows() { + try { + if(!this.data.boardId) {return;} + this.workflows = await this.workflowService.getAll(this.data.boardId); + + } catch (error) { + console.error('Failed to load workflows', error); + // Optionally handle the error (e.g., display a message to the user) + } + } + + async loadBuckets() { + try { + if(!this.data.boardId) { return; } + const buckets = await this.bucketService.getAllByBoard(this.data.boardId); + // Add buckets to payloadScope options if not already there: + if (buckets) { + for (const bucket of buckets) { + // Check if option already included (by value) + if (!this.agentConfig.payloadScope.options!.some(option => option.value === bucket.bucketID)) { //the ! tells typescript this won't be undefined + this.agentConfig.payloadScope.options!.push({value: bucket.bucketID, label: bucket.name}) // Typescript knows this is the correct shape + } + } + } + } catch(error) { + console.error('Failed to load buckets', error); + } + } + + createForm() { + const group: any = {}; + + // Iterate over sorted fields + for (const [fieldName, fieldConfig] of this.sortedFieldConfigs) { + const syncValidators: ValidatorFn[] = []; + const asyncValidators: AsyncValidatorFn[] = []; + + if (fieldConfig.required) { + syncValidators.push(Validators.required); + } + + let defaultValue = fieldConfig.defaultValue; + if (defaultValue === undefined) { + defaultValue = (this.agentInstance as any)[fieldName] !== undefined ? (this.agentInstance as any)[fieldName] : null; // Use instance value or null + } + + // Add custom validator if it exists + if (fieldName === 'workflowsToActivate') { + asyncValidators.push(this.validateWorkflowsBelongToBoard.bind(this)); + } + + group[fieldName] = new FormControl({ value: defaultValue, disabled: !fieldConfig.editable }, syncValidators, asyncValidators); + } + this.agentForm = this.fb.group(group); +} + + + + onNoClick(): void { + this.dialogRef.close(); + } + + onSubmit(): void { + if (this.agentForm.valid) { + const agentData = { + ...this.agentInstance, //keep fixed + ...this.agentForm.value, //form values overwrite + type: this.agentInstance.type, //keep type + name: this.agentInstance.name, //keep name + }; + this.dialogRef.close(agentData); + } + } + + getControl(fieldName: string): AbstractControl | null { + return this.agentForm.get(fieldName); + } + + // ASYNC Validator: Must return a Promise or Observable + async validateWorkflowsBelongToBoard(control: AbstractControl): Promise { + const workflowIDs: string[] = control.value; + + if (!workflowIDs || workflowIDs.length === 0) { + return null; // No workflows selected, so it's valid + } + if(!this.data.boardId) + { + return {invalidBoardId: true}; + } + + try { + const workflows = await this.workflowService.getAll(this.data.boardId); + const validWorkflowIds = new Set(workflows.map(w => w.workflowID)); + const allValid = workflowIDs.every(id => validWorkflowIds.has(id)); + + return allValid ? null : { invalidWorkflowId: true }; // Return error object if invalid. + + } catch (error) { + console.error("Error in validateWorkflowsBelongToBoard:", error); + return { apiError: true }; // Indicate an API/network error + } + } + + // Helper function to get options, handling both string[] and object[] + getOptions(fieldName: string): { value: any; label: string }[] { + const config = this.agentConfig[fieldName]; + if (!config || !config.options) { + return []; // Return empty array if no options defined + } + // Ensure options are *always* in {value, label} format. + return config.options.map(option => { + if (typeof option === 'string' || typeof option === 'number' || typeof option === 'boolean') { + return { value: option, label: String(option) }; + } + return option; + }); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index 8238cf20..59ebd738 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -1,304 +1,406 @@
- + - SCORE Authoring - {{ project.name }} - - - - -
- -
- -

Activity Phases

-
-
Activity Phases +
+
+
+ drag_indicator -
- drag_indicator - {{ activity.name }} -
- - -
+ {{ activity.name }} +
+ +
- +
+ -
-

Buckets & Workflows

- -
-
- Buckets: -
-
- {{ bucketCount }} -
-
- -
+
+

Buckets & Workflows

-
- Workflows: -
-
- {{ workflowCount }} -
-
- -
+
+
+ Buckets: +
+
+ {{ bucketCount }} +
+
+ +
+ +
+ Workflows: +
+
+ {{ workflowCount }} +
+
+
- -
-
-
- +
+ +
+
+
+ +
+ +

{{ selectedActivity.name }}

+
+
+

+ Activity Space: {{ selectedBoardName }} +

+
- -

{{ selectedActivity.name }}

-
-
-

Activity Space: {{ selectedBoardName }}

+
+ +
Resource Assignments
+
+
+
+ drag_indicator + Tab {{ i + 1 }}: {{ resource.name }}
- -
Resource Assignments
-
-
-
- drag_indicator - Tab {{ i + 1 }}: {{ resource.name }} - -
-
-
+
- - - -
Teacher Tasks
-
-
+ + +
+
+
+ drag_indicator + Task {{ task.order }}: + {{ getIconForTask(task) }} + Prompt: {{ task.customPrompt }} + + Activate Workflow: {{ getWorkflowName(task.workflowID) }} + + + Show Join Code + + {{ (task.type !== 'customPrompt' && task.type !== 'activateWorkflow' && task.type != 'showJoinCode') ? task.name : '' }} +
+ +
+
+
+ +
+
+
+ drag_indicator + {{ agent.name }} + +
+
+
+
+
+
-
-
-
+ +
+ +

Resources & Actions

+

Resources

+
+
+
- drag_indicator - Task {{ task.order }}: - {{ getIconForTask(task) }} - Prompt: {{ task.customPrompt || task.name }} - - Activate Workflow: {{ getWorkflowName(task.workflowID) }} - - - Show Join Code - - {{ (task.type !== 'customPrompt' && task.type !== 'activateWorkflow') ? task.name : '' }} + {{ resource.name }}
-
- -
- -
- -
- -

Resources & Actions

-

Resources

-
-
-
- {{ resource.name }} -
+ +

AI Agents

+
+
+
+ {{ agent.name }}
+
-

Teacher Actions

-
-
-
- {{ action.icon }} - {{ action.name }} -
+

Teacher Actions

+
+
+
+ {{ action.icon }} + {{ action.name }}
- - -
-
-

Groups

- -
-
-
-
- {{ group.name }} -
-
- -
+
+ + +
+
+

Groups

+ +
+
+
+
+ {{ group.name }}
-
-
+
+ edit +
+
+
+
+
-
- -
- -

Resources

- -

Classroom Objects

-
-
-
- {{ resource.icon }} - {{ resource.name }} -
+
+ +
+ +

Resources

+ +

Classroom Objects

+
+
+
+ {{ resource.icon }} + {{ resource.name }}
- -
-
- +
+ +
-
\ No newline at end of file +
+
diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.scss b/frontend/src/app/components/score-authoring/score-authoring.component.scss index e7ead30a..5c766883 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.scss +++ b/frontend/src/app/components/score-authoring/score-authoring.component.scss @@ -160,6 +160,11 @@ color: #000; } + &.ideaAgent { + background-color: #c5cae9; // Indigo + color: #000; + } + .drag-handle { cursor: move; margin-right: 0.5rem; @@ -289,6 +294,11 @@ background-color: #ef9a9a; /* Lighter Red */ color: #000; } + + &.ideaAgent { + background-color: #c5cae9; // Indigo 100 + color: #000; + } } } .available-teacher-actions-list { @@ -549,6 +559,11 @@ &.monitor { background-color: #ef9a9a; } + + &.ideaAgent { + background-color: #c5cae9; + color: #000; + } } } @@ -604,4 +619,21 @@ .mat-dialog-content { margin: 0 !important; // Remove default margin max-height: 100vh; // Allow content to take up full height +} + +mat-tab-group { + width: 100%; // Make the tabs take up the full width of the middle pane + + ::ng-deep .mat-tab-header { + margin-bottom: 20px; + } + + ::ng-deep .mat-tab-label { // Target the tab labels (::ng-deep is needed for shadow DOM piercing) + // Example styles: + font-size: 1.1rem; + font-weight: bold; + } + ::ng-deep .mat-tab-body-wrapper{ + min-height: 100px; //THIS IS IMPORTANT, or else it won't show up + } } \ No newline at end of file diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts index 3b72af62..973fd7b6 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.ts +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -1,12 +1,18 @@ // score-authoring.component.ts -import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core'; +import { + Component, + OnInit, + OnDestroy, + ElementRef, + ViewChild, +} from '@angular/core'; import { ComponentType } from '@angular/cdk/portal'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { Project } from 'src/app/models/project'; import { AuthUser } from 'src/app/models/user'; -import { Group } from 'src/app/models/group'; +import { Group } from 'src/app/models/group'; import { GroupService } from 'src/app/services/group.service'; import { SnackbarService } from 'src/app/services/snackbar.service'; import { UserService } from 'src/app/services/user.service'; @@ -17,34 +23,40 @@ import { Subscription } from 'rxjs'; import { CreateActivityModalComponent } from '../create-activity-modal/create-activity-modal.component'; import { EditActivityModalComponent } from '../edit-activity-modal/edit-activity-modal.component'; import { SelectWorkflowModalComponent } from '../select-workflow-modal/select-workflow-modal.component'; -import { SelectAiAgentModalComponent } from '../select-ai-agent-modal/select-ai-agent-modal.component'; +import { ConfigureAiAgentModalComponent } from '../configure-ai-agent-modal/configure-ai-agent-modal.component'; import { CustomTeacherPromptModalComponent } from '../custom-teacher-prompt-modal/custom-teacher-prompt-modal.component'; import { ManageGroupModalComponent } from '../groups/manage-group-modal/manage-group-modal.component'; -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { HttpClient } from '@angular/common/http'; import { Activity } from 'src/app/models/activity'; import { generateUniqueID } from 'src/app/utils/Utils'; import { Resource } from 'src/app/models/resource'; import { TeacherTask } from 'src/app/models/teacherTask'; -import { fabric } from 'fabric'; +import { fabric } from 'fabric'; import { HostListener } from '@angular/core'; import { ScoreViewModalComponent } from '../score-view-modal/score-view-modal.component'; -import { Board } from 'src/app/models/board' +import { Board } from 'src/app/models/board'; import { ConfigurationModalComponent } from '../configuration-modal/configuration-modal.component'; import { CreateWorkflowModalComponent } from '../create-workflow-modal/create-workflow-modal.component'; import { BucketService } from 'src/app/services/bucket.service'; import { WorkflowService } from 'src/app/services/workflow.service'; import { ShowJoinCodeComponent } from '../show-join-code/show-join-code.component'; -import {MatExpansionModule} from '@angular/material/expansion'; - +import { + AiAgent, + TeacherAgent, + IdeaAgentChat, + IdeaAgentAmbient, + PersonalLearningAgent, + GroupInteractionAgent, + WorkflowAgent, +} from 'src/app/models/ai-agent'; @Component({ selector: 'app-score-authoring', templateUrl: './score-authoring.component.html', - styleUrls: ['./score-authoring.component.scss'] + styleUrls: ['./score-authoring.component.scss'], }) export class ScoreAuthoringComponent implements OnInit, OnDestroy { - @ViewChild('editWorkflowsButton') editWorkflowsButton: ElementRef; workflowMap: Map = new Map(); @@ -53,19 +65,22 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { listeners: Subscription[] = []; activities: Activity[] = []; - selectedActivity: Activity | null = null; - selectedActivityResources: Resource[] = []; - selectedActivityGroups: Group[] = []; + selectedActivity: Activity | null = null; + selectedActivityResources: Resource[] = []; + selectedActivityGroups: Group[] = []; selectedBoardName = ''; canvas: fabric.Canvas | undefined; bucketCount: number = 0; workflowCount: number = 0; + selectedTabIndex: number = 0; - allAvailableResources: any[] = [ //define available resources + allAvailableResources: any[] = [ + //define available resources { name: 'Canvas', type: 'canvas' }, { name: 'Bucket View', type: 'bucketView' }, { name: 'Workspace', type: 'workspace' }, - { name: 'Monitor', type: 'monitor' } + { name: 'Monitor', type: 'monitor' }, + { name: 'Idea Agent', type: 'ideaAgent' }, ]; availableResources: any[] = [...this.allAvailableResources]; // Duplicate the array to be filtered based on selected values @@ -75,33 +90,64 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { { name: 'Projector', type: 'projector', icon: 'videocam' }, { name: 'Student', type: 'student', icon: 'person' }, { name: 'Student Group', type: 'studentGroup', icon: 'groups' }, - { name: 'Teacher', type: 'teacher', icon: 'school' } + { name: 'Teacher', type: 'teacher', icon: 'school' }, // ... add more classroom objects ]; availableTeacherActions: any[] = [ { name: 'Activate Workflow', type: 'activateWorkflow', icon: 'timeline' }, { name: 'Activate AI Agent', type: 'activateAiAgent', icon: 'smart_toy' }, - { name: 'Manually Regroup Students', type: 'regroupStudents', icon: 'group_work' }, + { + name: 'Manually Regroup Students', + type: 'regroupStudents', + icon: 'group_work', + }, { name: 'View Canvas', type: 'viewCanvas', icon: 'visibility' }, { name: 'View Buckets', type: 'viewBuckets', icon: 'view_module' }, - { name: 'View All Tasks, TODOs, & Analytics', type: 'viewTodos', icon: 'assignment' }, - { name: 'View Personal Workspace', type: 'viewWorkspace', icon: 'monitoring' }, - { name: 'Custom Teacher Prompt', type: 'customPrompt', icon: 'chat_bubble' }, + { + name: 'View All Tasks, TODOs, & Analytics', + type: 'viewTodos', + icon: 'assignment', + }, + { + name: 'View Personal Workspace', + type: 'viewWorkspace', + icon: 'monitoring', + }, + { + name: 'Custom Teacher Prompt', + type: 'customPrompt', + icon: 'chat_bubble', + }, { name: 'Show Student Join Code', type: 'showJoinCode', icon: 'qr_code' }, ]; + availableAiAgents: any[] = [ + { name: 'Teacher Agent', type: 'teacher', class: TeacherAgent }, + { name: 'Idea Agent', type: 'idea', class: IdeaAgentChat }, + { + name: 'Personal Learning Agent', + type: 'personal_learning', + class: PersonalLearningAgent, + }, + { + name: 'Group Interaction Agent', + type: 'group_interaction', + class: GroupInteractionAgent, + }, + { name: 'Workflow Agent', type: 'workflow', class: WorkflowAgent }, + ]; + teacherTasks: any[] = []; + activeAiAgents: AiAgent[] = []; - showResourcesPane = false; + showResourcesPane = false; showClassroomBindings = false; - isTeacherTasksExpanded: boolean = true; - @HostListener('window:resize', ['$event']) onResize(event: any) { if (this.showClassroomBindings) { - this.canvas?.dispose(); + this.canvas?.dispose(); this.canvas = undefined; this.initializeCanvas(); } @@ -110,7 +156,7 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { constructor( public userService: UserService, public snackbarService: SnackbarService, - public socketService: SocketService, + public socketService: SocketService, private projectService: ProjectService, private boardService: BoardService, private groupService: GroupService, @@ -120,23 +166,24 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { private activatedRoute: ActivatedRoute, public dialog: MatDialog, private http: HttpClient - ) { } + ) {} ngOnInit(): void { this.user = this.userService.user!; - this.loadScoreAuthoringData(); + this.loadScoreAuthoringData(); if (this.selectedActivity) { this.updateBucketAndWorkflowCounts(); } } initializeCanvas() { - const canvasContainer = document.getElementById('classroomCanvas')?.parentElement; // Get the parent div + const canvasContainer = + document.getElementById('classroomCanvas')?.parentElement; // Get the parent div if (canvasContainer) { this.canvas = new fabric.Canvas('classroomCanvas', { - width: canvasContainer.offsetWidth - 283, // Set width to parent's width - height: canvasContainer.offsetHeight -64 // Set height to parent's height + width: canvasContainer.offsetWidth - 283, // Set width to parent's width + height: canvasContainer.offsetHeight - 64, // Set height to parent's height }); this.createDotGrid(); @@ -153,19 +200,19 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { const canvasWidth = this.canvas.getWidth(); const canvasHeight = this.canvas.getHeight(); const gridSpacing = 40; // Adjust the spacing between dots as needed - + for (let x = 0; x <= canvasWidth; x += gridSpacing) { for (let y = 0; y <= canvasHeight; y += gridSpacing) { const dot = new fabric.Circle({ left: x, top: y, radius: 2, // Adjust the dot size as needed - fill: '#ddd' // Adjust the dot color as needed + fill: '#ddd', // Adjust the dot color as needed }); this.canvas.add(dot); } } - + this.canvas.renderAll(); // Render the canvas to show the dots } } @@ -183,7 +230,7 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { height: canvasHeight - inset * 2, fill: 'transparent', // Or any fill color you prefer stroke: '#ccc', // Or any stroke color you prefer - strokeWidth: 2 // Adjust the stroke width as needed + strokeWidth: 2, // Adjust the stroke width as needed }); this.canvas.add(rect); @@ -202,28 +249,31 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { async loadScoreAuthoringData(): Promise { const projectID = this.activatedRoute.snapshot.paramMap.get('projectID'); if (!projectID) { - this.router.navigate(['error']); + this.router.navigate(['error']); return; } try { this.project = await this.projectService.get(projectID); } catch (error) { - this.snackbarService.queueSnackbar("Error loading project data."); - console.error("Error loading project data:", error); - this.router.navigate(['error']); - return; + this.snackbarService.queueSnackbar('Error loading project data.'); + console.error('Error loading project data:', error); + this.router.navigate(['error']); + return; } - + try { - this.activities = await this.http.get(`activities/project/${projectID}`).toPromise() || []; + this.activities = + (await this.http + .get(`activities/project/${projectID}`) + .toPromise()) || []; if (this.activities.length > 0) { this.selectActivity(this.activities[0]); // Select the first activity } } catch (error) { - this.snackbarService.queueSnackbar("Error loading activities."); - console.error("Error loading activities:", error); - this.router.navigate(['error']); + this.snackbarService.queueSnackbar('Error loading activities.'); + console.error('Error loading activities:', error); + this.router.navigate(['error']); return; } @@ -238,11 +288,15 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { this.showResourcesPane = false; try { // Fetch resources for the selected activity - this.selectedActivityResources = await this.http.get(`resources/activity/${activity.activityID}`).toPromise() || []; + this.selectedActivityResources = + (await this.http + .get(`resources/activity/${activity.activityID}`) + .toPromise()) || []; this.selectedBoardName = await this.getSelectedBoardName(); + await this.loadActiveAiAgents(); } catch (error) { - this.snackbarService.queueSnackbar("Error fetching activity resources."); - console.error("Error fetching activity resources:", error); + this.snackbarService.queueSnackbar('Error fetching activity resources.'); + console.error('Error fetching activity resources:', error); } this.fetchActivityGroups(activity.groupIDs); @@ -252,21 +306,176 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { this.updateBucketAndWorkflowCounts(); } + async loadActiveAiAgents() { + if (!this.selectedActivity) { + return; + } + try { + this.activeAiAgents = + (await this.http + .get( + `ai-agents/activity/${this.selectedActivity.activityID}` + ) + .toPromise()) || []; + this.activeAiAgents.sort((a, b) => a.order - b.order); //sort by order + } catch (error) { + this.snackbarService.queueSnackbar('Error loading active AI agents.'); + console.error('Error loading active AI agents:', error); + } + } + + async dropAiAgentFromAvailable(event: CdkDragDrop) { + const agentType = this.availableAiAgents[event.previousIndex].type; + const agentClass = this.availableAiAgents[event.previousIndex].class; + // Open configuration modal *before* creating in the database + let newAgentData: Partial = { type: agentType }; //for idea agents + if (agentType === 'idea') { + const ideaAgentDialogRef = this.dialog.open( + ConfigureAiAgentModalComponent, + { + width: '800px', + height: '600px', + data: { agentType: 'idea_chat', agentClass: IdeaAgentChat }, + } + ); + const ideaChatResult = await ideaAgentDialogRef + .afterClosed() + .toPromise(); + + if (!ideaChatResult) { + //if cancelled, don't make any agents + return; + } + const ideaAgentAmbientDialogRef = this.dialog.open( + ConfigureAiAgentModalComponent, + { + width: '600px', + data: { + agentType: 'idea_ambient', + agentClass: IdeaAgentAmbient, + topic: ideaChatResult.topic, + enabled: ideaChatResult.enabled, + payloadScope: ideaChatResult.payloadScope, + }, // Pass the *class* + } + ); + const ideaAmbientResult = await ideaAgentAmbientDialogRef + .afterClosed() + .toPromise(); + if (!ideaAmbientResult) { + //if cancelled, don't make any agents + return; + } + //Create and add both agents + await this.createAndAddAiAgent(ideaChatResult); + await this.createAndAddAiAgent(ideaAmbientResult); + } else { + const dialogRef = this.dialog.open(ConfigureAiAgentModalComponent, { + width: '600px', + data: { agentType, agentClass: agentClass }, // Pass the *class* + }); + + const result = await dialogRef.afterClosed().toPromise(); + + if (result) { + // Create the agent in the database and add to the active list + newAgentData = result; + await this.createAndAddAiAgent(newAgentData); + } + } + + } + async createAndAddAiAgent(agentData: Partial) { + if (!this.selectedActivity) { + console.error('No selected activity!'); + return; + } + try { + const newAgentData: Partial = { + ...agentData, //get the agent data from the modal + aiAgentID: generateUniqueID(), // Generate a unique ID + activityID: this.selectedActivity.activityID, // Set the activity ID + order: this.activeAiAgents.length + 1, + }; + + const newAgent = await this.http + .post('ai-agents/', newAgentData) + .toPromise(); + if (newAgent) { + //check agent created + this.activeAiAgents.push(newAgent); + + this.selectedTabIndex = 1; + } + } catch (error) { + this.snackbarService.queueSnackbar('Error creating AI agent.'); + console.error('Error creating AI agent:', error); + } + } + + dropAiAgent(event: CdkDragDrop) { + moveItemInArray( + this.activeAiAgents, + event.previousIndex, + event.currentIndex + ); + this.updateAiAgentOrder(); + } + + async updateAiAgentOrder() { + if (!this.selectedActivity) { + return; + } + + try { + const updatedAgents = this.activeAiAgents.map((agent, index) => ({ + aiAgentID: agent.aiAgentID, + order: index + 1, // Assign new order based on index + })); + + await this.http + .post('ai-agents/order', { + activityID: this.selectedActivity.activityID, + agents: updatedAgents, + }) + .toPromise(); + } catch (error) { + this.snackbarService.queueSnackbar('Error updating AI agent order.'); + console.error('Error updating AI agent order:', error); + } + } + + async deleteAiAgent(agent: AiAgent, index: number) { + try { + await this.http.delete(`ai-agents/delete/${agent.aiAgentID}`).toPromise(); + this.activeAiAgents.splice(index, 1); + this.updateAiAgentOrder(); //update order after deletion + } catch (error) { + this.snackbarService.queueSnackbar('Error deleting AI Agent'); + console.error('Error deleting AI Agent', error); + } + } + async loadTeacherTasks() { if (!this.selectedActivity) { - console.warn("No activity selected."); + console.warn('No activity selected.'); return; } try { - this.teacherTasks = await this.http.get(`teacher-tasks/activity/${this.selectedActivity.activityID}`).toPromise() || []; + this.teacherTasks = + (await this.http + .get( + `teacher-tasks/activity/${this.selectedActivity.activityID}` + ) + .toPromise()) || []; this.teacherTasks.sort((a, b) => a.order - b.order); // Populate workflowMap await this.loadWorkflowsForActivity(); } catch (error) { - this.snackbarService.queueSnackbar("Error loading teacher tasks."); - console.error("Error loading teacher tasks:", error); + this.snackbarService.queueSnackbar('Error loading teacher tasks.'); + console.error('Error loading teacher tasks:', error); } } @@ -275,23 +484,23 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { return; } try { - const board = await this.boardService.get(this.selectedActivity.boardID); - if(!board) {return;} - const workflows = await this.workflowService.getAll(board.boardID); - this.workflowMap.clear(); // Clear existing entries - workflows.forEach(workflow => { - this.workflowMap.set(workflow.workflowID, workflow.name); - }); - - } - catch(error) { - console.error('Failed to load workflows', error); + const board = await this.boardService.get(this.selectedActivity.boardID); + if (!board) { + return; + } + const workflows = await this.workflowService.getAll(board.boardID); + this.workflowMap.clear(); // Clear existing entries + workflows.forEach((workflow) => { + this.workflowMap.set(workflow.workflowID, workflow.name); + }); + } catch (error) { + console.error('Failed to load workflows', error); } } getWorkflowName(workflowID: string | null | undefined): string { if (!workflowID) { - return 'Workflow Not Found'; // Or some other placeholder + return 'Workflow Not Found'; // Or some other placeholder } return this.workflowMap.get(workflowID) || 'Loading...'; } @@ -299,10 +508,12 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { async getSelectedBoard(): Promise { if (this.selectedActivity) { try { - const board = await this.boardService.get(this.selectedActivity.boardID); + const board = await this.boardService.get( + this.selectedActivity.boardID + ); return board; } catch (error) { - console.error("Error fetching board:", error); + console.error('Error fetching board:', error); return undefined; } } else { @@ -333,7 +544,7 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { case 'viewTodos': componentType = 'monitor'; break; - case 'viewWorkspace': + case 'viewWorkspace': componentType = 'workspace'; break; } @@ -344,13 +555,13 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { }); break; case 'activateWorkflow': - this.openWorkflowBucketModal(2); + this.openWorkflowBucketModal(2); break; case 'customPrompt': - this.editCustomPrompt(task); + this.editCustomPrompt(task); break; case 'showJoinCode': - this.showJoinCode(task); + this.showJoinCode(task); break; default: // Handle other task types or do nothing @@ -361,32 +572,38 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { start(activity: Activity) { // ... (Implement logic to start the activity) ... } - + editActivity(activity: Activity) { const dialogRef = this.dialog.open(EditActivityModalComponent, { - data: { + data: { project: this.project, - activity: activity - } + activity: activity, + }, }); - dialogRef.afterClosed().subscribe((result: { activity: Activity, delete: boolean }) => { - if (result) { - if (result.delete) { - this.deleteActivity(result.activity); - } else { - this.updateActivity(result.activity); + dialogRef + .afterClosed() + .subscribe((result: { activity: Activity; delete: boolean }) => { + if (result) { + if (result.delete) { + this.deleteActivity(result.activity); + } else { + this.updateActivity(result.activity); + } } - } - }); + }); } async updateActivity(activity: Activity) { try { - const updatedActivity = await this.http.put(`activities/${activity.activityID}`, activity).toPromise(); + const updatedActivity = await this.http + .put(`activities/${activity.activityID}`, activity) + .toPromise(); // Update the activity in the activities list - const index = this.activities.findIndex(a => a.activityID === activity.activityID); + const index = this.activities.findIndex( + (a) => a.activityID === activity.activityID + ); if (index !== -1) { this.activities[index] = updatedActivity as Activity; } @@ -397,8 +614,8 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { this.fetchActivityGroups(this.selectedActivity.groupIDs); } } catch (error) { - this.snackbarService.queueSnackbar("Error updating activity."); - console.error("Error updating activity:", error); + this.snackbarService.queueSnackbar('Error updating activity.'); + console.error('Error updating activity:', error); } } @@ -407,10 +624,12 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { // 1. (Optional) Ask for confirmation before deleting // 2. Call the API to delete the activity - await this.http.delete(`activities/${activity.activityID}`).toPromise(); + await this.http.delete(`activities/${activity.activityID}`).toPromise(); // 3. Remove the activity from the activities list - this.activities = this.activities.filter(a => a.activityID !== activity.activityID); + this.activities = this.activities.filter( + (a) => a.activityID !== activity.activityID + ); // 4. If the deleted activity was the selected one, clear the selection if (this.selectedActivity?.activityID === activity.activityID) { @@ -419,18 +638,20 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { this.selectedActivityGroups = []; // Clear groups as well } } catch (error) { - this.snackbarService.queueSnackbar("Error deleting activity."); - console.error("Error deleting activity:", error); + this.snackbarService.queueSnackbar('Error deleting activity.'); + console.error('Error deleting activity:', error); } } async getSelectedBoardName(): Promise { if (this.selectedActivity) { try { - const board = await this.boardService.get(this.selectedActivity.boardID); + const board = await this.boardService.get( + this.selectedActivity.boardID + ); return board ? board.name : ''; } catch (error) { - console.error("Error fetching board:", error); + console.error('Error fetching board:', error); return ''; } } else { @@ -439,7 +660,11 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } dropResource(event: CdkDragDrop) { - moveItemInArray(this.selectedActivityResources, event.previousIndex, event.currentIndex); + moveItemInArray( + this.selectedActivityResources, + event.previousIndex, + event.currentIndex + ); this.updateResourceOrder(); } @@ -447,17 +672,21 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { const resource = this.availableResources[event.previousIndex]; this.createResource(resource) - .then(newResource => { + .then((newResource) => { this.availableResources.splice(event.previousIndex, 1); - this.selectedActivityResources.splice(event.currentIndex, 0, newResource); + this.selectedActivityResources.splice( + event.currentIndex, + 0, + newResource + ); this.updateResourceOrder(); }) - .catch(error => { - console.error("Error creating resource:", error); + .catch((error) => { + console.error('Error creating resource:', error); }); } - async createResource(resourceData: any): Promise { + async createResource(resourceData: any): Promise { try { const newResourceData = { resourceID: generateUniqueID(), // Add resourceID @@ -467,11 +696,13 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { order: this.selectedActivityResources.length + 1, }; - const response = await this.http.post('resources/create', newResourceData).toPromise(); - return response as Resource; + const response = await this.http + .post('resources/create', newResourceData) + .toPromise(); + return response as Resource; } catch (error) { - this.snackbarService.queueSnackbar("Error creating resource."); - console.error("Error creating resource:", error); + this.snackbarService.queueSnackbar('Error creating resource.'); + console.error('Error creating resource:', error); throw error; // Re-throw the error to be caught in the calling function } } @@ -479,21 +710,23 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { async deleteResource(resource: Resource, index: number) { try { // 1. Delete the resource from the database - await this.http.delete(`resources/delete/${resource.resourceID}`).toPromise(); + await this.http + .delete(`resources/delete/${resource.resourceID}`) + .toPromise(); // 2. Remove the resource from the list - this.selectedActivityResources.splice(index, 1); + this.selectedActivityResources.splice(index, 1); // 3. Update the resource order in the database - this.updateResourceOrder(); + this.updateResourceOrder(); // 4. If the resources pane is open, update the available resources if (this.showResourcesPane) { - this.filterAvailableResources(); + this.filterAvailableResources(); } } catch (error) { - this.snackbarService.queueSnackbar("Error deleting resource."); - console.error("Error deleting resource:", error); + this.snackbarService.queueSnackbar('Error deleting resource.'); + console.error('Error deleting resource:', error); } } @@ -506,13 +739,15 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { try { const updatedActivities = this.activities.map((activity, index) => ({ activityID: activity.activityID, - order: index + 1 // Assign new order based on index + order: index + 1, // Assign new order based on index })); - await this.http.post('activities/order/', { activities: updatedActivities }).toPromise(); + await this.http + .post('activities/order/', { activities: updatedActivities }) + .toPromise(); } catch (error) { - this.snackbarService.queueSnackbar("Error updating activity order."); - console.error("Error updating activity order:", error); + this.snackbarService.queueSnackbar('Error updating activity order.'); + console.error('Error updating activity order:', error); } } @@ -522,38 +757,46 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } try { - const updatedResources = this.selectedActivityResources.map((resource, index) => ({ - resourceID: resource.resourceID, - order: index + 1, - })); - - await this.http.post('resources/order/', { - activityID: this.selectedActivity.activityID, - resources: updatedResources - }).toPromise(); - + const updatedResources = this.selectedActivityResources.map( + (resource, index) => ({ + resourceID: resource.resourceID, + order: index + 1, + }) + ); + + await this.http + .post('resources/order/', { + activityID: this.selectedActivity.activityID, + resources: updatedResources, + }) + .toPromise(); } catch (error) { - this.snackbarService.queueSnackbar("Error updating resource order."); - console.error("Error updating resource order:", error); + this.snackbarService.queueSnackbar('Error updating resource order.'); + console.error('Error updating resource order:', error); } } async fetchActivityResources(activityID: string) { try { // ... (Implement logic to fetch resources for the activity) ... - this.selectedActivityResources = await this.http.get(`resources/activity/${activityID}`).toPromise() || []; + this.selectedActivityResources = + (await this.http + .get(`resources/activity/${activityID}`) + .toPromise()) || []; } catch (error) { - this.snackbarService.queueSnackbar("Error fetching activity resources."); - console.error("Error fetching activity resources:", error); + this.snackbarService.queueSnackbar('Error fetching activity resources.'); + console.error('Error fetching activity resources:', error); } } - + async fetchActivityGroups(groupIDs: string[]) { try { - this.selectedActivityGroups = await this.groupService.getMultipleBy(groupIDs); // Example using GroupService + this.selectedActivityGroups = await this.groupService.getMultipleBy( + groupIDs + ); // Example using GroupService } catch (error) { - this.snackbarService.queueSnackbar("Error fetching activity groups."); - console.error("Error fetching activity groups:", error); + this.snackbarService.queueSnackbar('Error fetching activity groups.'); + console.error('Error fetching activity groups:', error); } } @@ -573,7 +816,9 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { const workflows = await this.workflowService.getAll(board.boardID); this.workflowCount = workflows.length; } else { - this.snackbarService.queueSnackbar('Error: Could not find selected board.'); + this.snackbarService.queueSnackbar( + 'Error: Could not find selected board.' + ); } } catch (error) { console.error('Error fetching counts:', error); @@ -602,15 +847,23 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { selectedTabIndex: selectedTabIndex, }); } else { - this.snackbarService.queueSnackbar('Error: Could not find selected board.'); + this.snackbarService.queueSnackbar( + 'Error: Could not find selected board.' + ); } } catch (error) { console.error('Error fetching board:', error); - this.snackbarService.queueSnackbar('Error opening Workflows & Buckets modal.'); + this.snackbarService.queueSnackbar( + 'Error opening Workflows & Buckets modal.' + ); } } - private _openDialog(component: ComponentType, data: any, width = '700px') { + private _openDialog( + component: ComponentType, + data: any, + width = '700px' + ) { this.dialog.open(component, { maxWidth: 1280, width: width, @@ -621,42 +874,44 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { openCreateActivityModal() { const dialogRef = this.dialog.open(CreateActivityModalComponent, { - data: { + data: { project: this.project, - createActivity: this.createActivity - } + createActivity: this.createActivity, + }, }); - dialogRef.afterClosed().subscribe(result => { + dialogRef.afterClosed().subscribe((result) => { if (result) { - this.createActivity(result); + this.createActivity(result); } }); } - createActivity = async (activityData: Activity) => { + createActivity = async (activityData: Activity) => { try { - const response = await this.http.post('activities/', activityData).toPromise(); - const newActivity: Activity = response as Activity; - - this.activities.push(newActivity); - this.selectActivity(newActivity); - console.log("New Activity created.") + const response = await this.http + .post('activities/', activityData) + .toPromise(); + const newActivity: Activity = response as Activity; + + this.activities.push(newActivity); + this.selectActivity(newActivity); + console.log('New Activity created.'); } catch (error) { - this.snackbarService.queueSnackbar("Error creating activity."); - console.error("Error creating activity:", error); + this.snackbarService.queueSnackbar('Error creating activity.'); + console.error('Error creating activity:', error); } }; addResourceToActivity(resource: Resource) { - this.showResourcesPane = false; + this.showResourcesPane = false; } dropTeacherActionFromAvailable(event: CdkDragDrop) { const action = this.availableTeacherActions[event.previousIndex]; // Call a function to handle creating the teacher task - this.createTeacherTask(action); + this.createTeacherTask(action); } dropTeacherAction(event: CdkDragDrop) { @@ -665,21 +920,23 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } getIconForTask(task: any): string { - const action = this.availableTeacherActions.find(a => a.type === task.type); - return action ? action.icon : ''; + const action = this.availableTeacherActions.find( + (a) => a.type === task.type + ); + return action ? action.icon : ''; } async createTeacherTask(actionData: any) { try { let taskData: Partial = { taskID: generateUniqueID(), - name: actionData.name, + name: actionData.name, activityID: this.selectedActivity!.activityID, order: this.teacherTasks.length + 1, type: actionData.type, // ... other properties you might need for teacher tasks ... }; - + // Open a modal based on the action type if (actionData.type === 'activateWorkflow') { const updatedTaskData = await this.openWorkflowModal(taskData); @@ -689,7 +946,8 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { return; } } else if (actionData.type === 'activateAiAgent') { - const { updatedTaskData, selectedAiAgentId } = await this.openAiAgentModal(taskData); + const { updatedTaskData, selectedAiAgentId } = + await this.openAiAgentModal(taskData); taskData = updatedTaskData; } else if (actionData.type === 'customPrompt') { const updatedTaskData = await this.openCustomPromptModal(taskData); @@ -698,20 +956,22 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } else { return; } - } + } // ... add more cases for other action types as needed ... - + // If taskData is not null (i.e., the modal was not canceled), create the task if (taskData) { - const newTask = await this.http.post('teacher-tasks/', taskData).toPromise(); + const newTask = await this.http + .post('teacher-tasks/', taskData) + .toPromise(); this.teacherTasks.push(newTask); this.updateTeacherTaskOrder(); - this.isTeacherTasksExpanded = true; + this.selectedTabIndex = 0; } } catch (error) { - this.snackbarService.queueSnackbar("Error creating teacher task."); - console.error("Error creating teacher task:", error); + this.snackbarService.queueSnackbar('Error creating teacher task.'); + console.error('Error creating teacher task:', error); } } @@ -719,33 +979,42 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { if (!this.selectedActivity) { return; } - - this.boardService.get(this.selectedActivity.boardID).then((board) => { - if (board) { - this.dialog.open(ConfigurationModalComponent, { - data: { - project: this.project, - board: board, - update: (updatedBoard: Board, removed = false) => { - if (removed) { - // Handle board removal if necessary - this.snackbarService.queueSnackbar('Board removed successfully.'); - // You might want to update your UI here, e.g., by refreshing the activities list - } else { - // Update the board in your component - this.selectedBoardName = updatedBoard.name; // Assuming you want to update the displayed board name - // You might need to update other parts of your UI that depend on the board data - } + + this.boardService + .get(this.selectedActivity.boardID) + .then((board) => { + if (board) { + this.dialog.open(ConfigurationModalComponent, { + data: { + project: this.project, + board: board, + update: (updatedBoard: Board, removed = false) => { + if (removed) { + // Handle board removal if necessary + this.snackbarService.queueSnackbar( + 'Board removed successfully.' + ); + // You might want to update your UI here, e.g., by refreshing the activities list + } else { + // Update the board in your component + this.selectedBoardName = updatedBoard.name; // Assuming you want to update the displayed board name + // You might need to update other parts of your UI that depend on the board data + } + }, }, - }, - }); - } else { - this.snackbarService.queueSnackbar('Error: Could not find selected board.'); - } - }).catch((error) => { - console.error('Error fetching board:', error); - this.snackbarService.queueSnackbar('Error: Could not open configuration.'); - }); + }); + } else { + this.snackbarService.queueSnackbar( + 'Error: Could not find selected board.' + ); + } + }) + .catch((error) => { + console.error('Error fetching board:', error); + this.snackbarService.queueSnackbar( + 'Error: Could not open configuration.' + ); + }); } openViewModal(componentType: string, project: Project, board: Board) { @@ -760,40 +1029,40 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { project, board, user: this.user, - projectID: project.projectID, - boardID: board.boardID - } + projectID: project.projectID, + boardID: board.boardID, + }, }); - dialogRef.afterClosed().subscribe(result => { + dialogRef.afterClosed().subscribe((result) => { // Handle any actions after closing the modal (if needed) }); } - + async openWorkflowModal(taskData: Partial): Promise { const dialogRef = this.dialog.open(SelectWorkflowModalComponent, { width: '500px', data: { boardID: this.selectedActivity!.boardID, - taskData: taskData, //still needed for the create workflow option + taskData: taskData, //still needed for the create workflow option board: await this.boardService.get(this.selectedActivity!.boardID), - project: this.project - } + project: this.project, + }, }); - + const result = await dialogRef.afterClosed().toPromise(); - + if (result && result.shouldOpenCreateModal) { - this.openWorkflowBucketModal(1); - return null; // Important: Consistent return type - } else if (result){ //user selected workflow - const updatedTaskData = { + this.openWorkflowBucketModal(1); + return null; // Important: Consistent return type + } else if (result) { + //user selected workflow + const updatedTaskData = { ...taskData, - workflowID: result + workflowID: result, }; return updatedTaskData; - } - else { + } else { return null; // User cancelled } } @@ -805,30 +1074,31 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { data: { joinCode: this.project.studentJoinCode }, // Pass the project's join code }); } - + async openAiAgentModal(taskData: any): Promise { - const dialogRef = this.dialog.open(SelectAiAgentModalComponent, { // Assuming you create this component + const dialogRef = this.dialog.open(ConfigureAiAgentModalComponent, { + // Assuming you create this component data: { // ... pass any necessary data for AI agent selection ... - taskData: taskData - } + taskData: taskData, + }, }); - + return dialogRef.afterClosed().toPromise(); } - + async openCustomPromptModal(taskData: any): Promise { const dialogRef = this.dialog.open(CustomTeacherPromptModalComponent, { width: '500px', - data: { taskData: taskData } // Pass any data needed by the modal + data: { taskData: taskData }, // Pass any data needed by the modal }); - + const result = await dialogRef.afterClosed().toPromise(); - + if (result) { const updatedTaskData = { ...taskData, - customPrompt: result // Store the prompt in the taskData + customPrompt: result, // Store the prompt in the taskData }; return updatedTaskData; } else { @@ -839,37 +1109,46 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { async editCustomPrompt(task: TeacherTask) { const dialogRef = this.dialog.open(CustomTeacherPromptModalComponent, { width: '500px', - data: { prompt: task.customPrompt } // Pass the existing prompt + data: { prompt: task.customPrompt }, // Pass the existing prompt }); - + const result = await dialogRef.afterClosed().toPromise(); - + if (result) { // Update the task with the new prompt try { - const updatedTask = await this.http.put(`teacher-tasks/${task.taskID}`, { ...task, customPrompt: result }).toPromise(); - + const updatedTask = await this.http + .put(`teacher-tasks/${task.taskID}`, { + ...task, + customPrompt: result, + }) + .toPromise(); + if (updatedTask) { // Update the task in the teacherTasks array - const index = this.teacherTasks.findIndex(t => t.taskID === updatedTask.taskID); + const index = this.teacherTasks.findIndex( + (t) => t.taskID === updatedTask.taskID + ); if (index !== -1) { this.teacherTasks[index] = updatedTask; } } else { // Handle the case where the update failed (e.g., show an error) - this.snackbarService.queueSnackbar("Error updating prompt: Update failed."); - console.error("Error updating prompt: API returned null/undefined"); + this.snackbarService.queueSnackbar( + 'Error updating prompt: Update failed.' + ); + console.error('Error updating prompt: API returned null/undefined'); } } catch (error) { - this.snackbarService.queueSnackbar("Error updating prompt."); - console.error("Error updating prompt:", error); + this.snackbarService.queueSnackbar('Error updating prompt.'); + console.error('Error updating prompt:', error); } } } async updateTeacherTaskOrder() { if (!this.selectedActivity) { - console.warn("No activity selected."); + console.warn('No activity selected.'); return; } @@ -879,15 +1158,17 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { order: index + 1, })); - await this.http.post('teacher-tasks/order/', { - activityID: this.selectedActivity.activityID, - tasks: updatedTasks - }).toPromise(); + await this.http + .post('teacher-tasks/order/', { + activityID: this.selectedActivity.activityID, + tasks: updatedTasks, + }) + .toPromise(); await this.loadTeacherTasks(); } catch (error) { - this.snackbarService.queueSnackbar("Error updating teacher task order."); - console.error("Error updating teacher task order:", error); + this.snackbarService.queueSnackbar('Error updating teacher task order.'); + console.error('Error updating teacher task order:', error); } } @@ -897,8 +1178,8 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { this.teacherTasks.splice(index, 1); this.updateTeacherTaskOrder(); // Update order after deleting a task } catch (error) { - this.snackbarService.queueSnackbar("Error deleting teacher task."); - console.error("Error deleting teacher task:", error); + this.snackbarService.queueSnackbar('Error deleting teacher task.'); + console.error('Error deleting teacher task:', error); } } @@ -915,29 +1196,49 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { let updatedResource; if (this.isResourceAssignedToGroup(resource, group)) { // Remove the group from the resource - updatedResource = await this.http.delete(`resources/${resource.resourceID}/groups/${group.groupID}`).toPromise(); + updatedResource = await this.http + .delete(`resources/${resource.resourceID}/groups/${group.groupID}`) + .toPromise(); } else { // Add the group to the resource - updatedResource = await this.http.post(`resources/${resource.resourceID}/groups/${group.groupID}`, {}).toPromise(); + updatedResource = await this.http + .post(`resources/${resource.resourceID}/groups/${group.groupID}`, {}) + .toPromise(); } // Update the resource in the list - const resourceIndex = this.selectedActivityResources.findIndex(r => r.resourceID === resource.resourceID); + const resourceIndex = this.selectedActivityResources.findIndex( + (r) => r.resourceID === resource.resourceID + ); if (resourceIndex !== -1) { - this.selectedActivityResources[resourceIndex] = updatedResource as Resource; + this.selectedActivityResources[resourceIndex] = + updatedResource as Resource; } } catch (error) { - this.snackbarService.queueSnackbar(`Error ${this.isResourceAssignedToGroup(resource, group) ? 'removing' : 'adding'} group assignment.`); - console.error(`Error ${this.isResourceAssignedToGroup(resource, group) ? 'removing' : 'adding'} group assignment:`, error); + this.snackbarService.queueSnackbar( + `Error ${ + this.isResourceAssignedToGroup(resource, group) + ? 'removing' + : 'adding' + } group assignment.` + ); + console.error( + `Error ${ + this.isResourceAssignedToGroup(resource, group) + ? 'removing' + : 'adding' + } group assignment:`, + error + ); } } - toggleClassroomBindings() { + toggleClassroomBindings() { this.showClassroomBindings = !this.showClassroomBindings; if (this.showClassroomBindings) { // Add a slight delay before initializing the canvas - setTimeout(() => { + setTimeout(() => { this.initializeCanvas(); }, 100); // Adjust the delay as needed } else { @@ -951,22 +1252,28 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } filterAvailableResources() { - const existingResourceNames = new Set(this.selectedActivityResources.map(r => r.name)); - this.availableResources = this.allAvailableResources.filter(resource => - !existingResourceNames.has(resource.name) + const existingResourceNames = new Set( + this.selectedActivityResources.map((r) => r.name) + ); + this.availableResources = this.allAvailableResources.filter( + (resource) => !existingResourceNames.has(resource.name) ); } drop(event: CdkDragDrop) { - moveItemInArray(this.selectedActivityResources, event.previousIndex, event.currentIndex); + moveItemInArray( + this.selectedActivityResources, + event.previousIndex, + event.currentIndex + ); } ngOnDestroy(): void { - this.listeners.map(l => l.unsubscribe()); + this.listeners.map((l) => l.unsubscribe()); } signOut(): void { this.userService.logout(); this.router.navigate(['login']); } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.html b/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.html deleted file mode 100644 index 0c306e70..00000000 --- a/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.html +++ /dev/null @@ -1 +0,0 @@ -

select-ai-agent-modal works!

diff --git a/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.scss b/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.spec.ts b/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.spec.ts deleted file mode 100644 index 90609496..00000000 --- a/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { SelectAiAgentModalComponent } from './select-ai-agent-modal.component'; - -describe('SelectAiAgentModalComponent', () => { - let component: SelectAiAgentModalComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ SelectAiAgentModalComponent ] - }) - .compileComponents(); - - fixture = TestBed.createComponent(SelectAiAgentModalComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.ts b/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.ts deleted file mode 100644 index f5d2b685..00000000 --- a/frontend/src/app/components/select-ai-agent-modal/select-ai-agent-modal.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-select-ai-agent-modal', - templateUrl: './select-ai-agent-modal.component.html', - styleUrls: ['./select-ai-agent-modal.component.scss'] -}) -export class SelectAiAgentModalComponent { - -} diff --git a/frontend/src/app/models/ai-agent-config.ts b/frontend/src/app/models/ai-agent-config.ts new file mode 100644 index 00000000..e20435c9 --- /dev/null +++ b/frontend/src/app/models/ai-agent-config.ts @@ -0,0 +1,773 @@ +// src/app/models/ai-agent-config.ts + +export interface AiAgentFieldConfig { + required: boolean; + editable: boolean; + options?: { value: string | number | boolean; label: string }[]; + defaultValue?: any; + type: 'string' | 'number' | 'boolean' | 'string[]'; + label: string; + tooltip?: string; // Optional tooltip + order: number; +} + +export interface AiAgentTypeConfig { + [fieldName: string]: AiAgentFieldConfig; +} + +export const AI_AGENT_CONFIG: { [agentType: string]: AiAgentTypeConfig } = { + teacher: { + persona: { + required: false, + editable: false, + type: 'string', + label: 'Persona', + order: 1, + tooltip: 'The purpose, role, or identity of the AI. This is fixed.', + }, + task: { + required: false, + editable: false, + type: 'string', + label: 'Task', + order: 2, + tooltip: 'How the AI should behave or respond to prompts. This is fixed.', + }, + topic: { + required: false, + editable: false, + type: 'string', + label: 'Topic', + order: 3, + tooltip: + 'The topic of discussion or question that students are responding to.', + }, // Fixed for teacher + payloadScope: { + required: false, + editable: true, + type: 'string[]', + options: [], + defaultValue: ['all'], + label: 'Data Context', + order: 4, + tooltip: + 'Select the scope of data the agent will have access to. Select "All" or individual sources.', + }, + userScope: { + required: true, + editable: false, + type: 'string', + options: [{ value: 'all', label: 'All' }], + label: 'User Scope', + order: 5, + tooltip: 'This agent applies to the whole class.', + }, + triggerEventTypes: { + required: false, + editable: false, + type: 'string[]', + options: [], + label: 'Trigger Events', + order: 6, + tooltip: 'This agent is triggered by user chat.', + }, + eventThreshold: { + required: false, + editable: false, + type: 'number', + label: 'Trigger Threshold', + order: 7, + tooltip: 'This agent does not have a trigger theshold', + }, + criteriaToGroupStudents: { + required: false, + editable: false, + type: 'string', + label: 'Grouping Criteria', + order: 8, + tooltip: 'This agent does not regroup students.', + }, + workflowsToActivate: { + required: false, + editable: false, + type: 'string[]', + label: 'Workflows to Activate', + order: 9, + tooltip: 'This agent does not trigger workflows', + }, + criteriaToActivateWorkflow: { + required: false, + editable: false, + type: 'string', + label: 'Workflow Activation Criteria', + order: 10, + tooltip: 'This agent does not use workflow activation criteria', + }, + databaseWriteAccess: { + required: true, + editable: true, + type: 'boolean', + options: [ + { value: true, label: 'True' }, + { value: false, label: 'False' }, + ], + label: 'Database Write Access', + order: 11, + tooltip: + 'Allow the agent to write to the database (e.g., add or remove posts)?', + }, + uiIntegrations: { + required: false, + editable: true, + type: 'string[]', + options: [ + { value: 'Workspace', label: 'Workspace' }, + { value: 'Canvas', label: 'Canvas' }, + { value: 'Bucket View', label: 'Bucket View' }, + { value: 'Monitor', label: 'Monitor' }, + ], + label: 'AI Access Locations', + order: 12, + tooltip: + 'Locations where teachers can interact with the AI. Students cannot interact with this AI Agent', + }, + enabled: { + required: true, + editable: false, + type: 'boolean', + defaultValue: true, + label: 'Enabled', + order: 13, + tooltip: 'This agent is always enabled.', + }, // ALWAYS TRUE, not editable + }, + idea_chat: { + persona: { + required: false, + editable: false, + type: 'string', + label: 'Persona', + order: 1, + tooltip: 'The purpose, role, or identity of the AI. This is fixed', + }, + task: { + required: false, + editable: false, + type: 'string', + label: 'Task', + order: 2, + tooltip: + 'How the AI should behave or respond to prompts. This is fixed.', + }, + topic: { + required: true, + editable: true, + type: 'string', + label: 'Topic', + order: 3, + tooltip: 'The topic for idea generation.', + }, + payloadScope: { + required: false, + editable: true, + type: 'string[]', + options: [], + defaultValue: ['all'], + label: 'Data Context', + order: 4, + tooltip: + 'Select the scope of data the agent will have access to. Select "All" or individual sources.', + }, + userScope: { + required: true, + editable: false, + type: 'string', + options: [{ value: 'all', label: 'All' }], + label: 'User Scope', + order: 5, + tooltip: 'This agent applies to the whole class.', + }, // Fixed for idea_chat + triggerEventTypes: { + required: false, + editable: false, + type: 'string[]', + options: [], + label: 'Trigger Events', + order: 6, + tooltip: 'This agent is triggered by user chat.', + }, + eventThreshold: { + required: false, + editable: false, + type: 'number', + label: 'Trigger Threshold', + order: 7, + tooltip: 'This agent does not have a trigger theshold', + }, + criteriaToGroupStudents: { + required: false, + editable: false, + type: 'string', + label: 'Grouping Criteria', + order: 8, + tooltip: 'This agent does not regroup students.', + }, + workflowsToActivate: { + required: false, + editable: false, + type: 'string[]', + label: 'Workflows to Activate', + order: 9, + tooltip: 'This agent does not trigger workflows', + }, + criteriaToActivateWorkflow: { + required: false, + editable: false, + type: 'string', + label: 'Workflow Activation Criteria', + order: 10, + tooltip: 'This agent does not use workflow activation criteria', + }, + databaseWriteAccess: { + required: false, + editable: false, + type: 'boolean', + label: 'Database Write Access', + order: 11, + tooltip: 'This agent cannot write to the database.', + }, + uiIntegrations: { + required: false, + editable: false, + type: 'string[]', + label: 'AI Access Locations', + order: 12, + tooltip: 'This agent has no UI integrations.', + }, + enabled: { + required: true, + editable: false, + type: 'boolean', + defaultValue: true, + label: 'Enabled', + order: 13, + tooltip: 'This agent is always enabled.', + }, // ALWAYS TRUE + }, + idea_ambient: { + persona: { + required: false, + editable: false, + type: 'string', + label: 'Persona', + order: 1, + tooltip: 'The purpose, role, or identity of the AI. This is fixed', + }, + task: { + required: false, + editable: false, + type: 'string', + label: 'Task', + order: 2, + tooltip: 'How the AI should behave or respond to prompts. This is fixed.', + }, + topic: { + required: true, + editable: true, + type: 'string', + label: 'Topic', + order: 3, + tooltip: 'The topic for idea generation.', + }, + payloadScope: { + required: false, + editable: true, + type: 'string[]', + options: [], + defaultValue: ['all'], + label: 'Data Context', + order: 4, + tooltip: + 'Select the scope of data the agent will have access to. Select "All" or individual sources.', + }, + userScope: { + required: true, + editable: false, + type: 'string', + options: [{ value: 'all', label: 'All' }], + label: 'User Scope', + order: 5, + tooltip: 'This agent applies to the whole class.', + }, + triggerEventTypes: { + required: true, + editable: true, + type: 'string[]', + options: [ + { value: 'POST_CREATE', label: 'Post Create' }, + { value: 'POST_COMMENT', label: 'Post Comment' }, + { value: 'POST_UPVOTE', label: 'Post Upvote' }, + ], + label: 'Trigger Events', + order: 6, + tooltip: 'Select the events that will trigger this agent.', + }, + eventThreshold: { + required: false, + editable: false, + type: 'number', + label: 'Trigger Threshold', + order: 7, + tooltip: 'This agent does not have a trigger theshold', + }, + criteriaToGroupStudents: { + required: false, + editable: false, + type: 'string', + label: 'Grouping Criteria', + order: 8, + tooltip: 'This agent does not regroup students.', + }, + workflowsToActivate: { + required: false, + editable: false, + type: 'string[]', + label: 'Workflows to Activate', + order: 9, + tooltip: 'This agent does not trigger workflows', + }, + criteriaToActivateWorkflow: { + required: false, + editable: false, + type: 'string', + label: 'Workflow Activation Criteria', + order: 10, + tooltip: 'This agent does not use workflow activation criteria', + }, + databaseWriteAccess: { + required: false, + editable: false, + type: 'boolean', + label: 'Database Write Access', + order: 11, + tooltip: 'This agent cannot write to the database.', + }, + uiIntegrations: { + required: false, + editable: false, + type: 'string[]', + label: 'AI Access Locations', + order: 12, + tooltip: 'This agent has no UI integrations.', + }, + enabled: { + required: true, + editable: false, + type: 'boolean', + defaultValue: true, + label: 'Enabled', + order: 13, + tooltip: 'This agent is always enabled.', + }, // ALWAYS TRUE + }, + personal_learning: { + persona: { + required: true, + editable: true, + type: 'string', + label: 'Persona', + order: 1, + tooltip: 'Enter the purpose, role, or identity of the AI.', + }, + task: { + required: true, + editable: true, + type: 'string', + label: 'Task', + order: 2, + tooltip: 'Describe how the AI should behave or respond to prompts.', + }, + topic: { + required: true, + editable: true, + type: 'string', + label: 'Topic', + order: 3, + tooltip: + 'The topic of discussion or question that students are responding to.', + }, + payloadScope: { + required: false, + editable: true, + type: 'string[]', + options: [ + { value: 'canvas', label: 'Canvas' }, + { value: 'all', label: 'All' }, + { value: 'buckets', label: 'Buckets' }, + ], + defaultValue: ['all'], + label: 'Data Context', + order: 4, + tooltip: + 'Select the scope of data the agent will have access to. Select "All" or individual sources.', + }, + userScope: { + required: true, + editable: true, + type: 'string', + options: [ + { value: 'personal', label: 'Personal' }, + { value: 'all', label: 'All' }, + ], + label: 'User Scope', + order: 5, + tooltip: + 'Select if the agent should respond to all users, or just a single user.', + }, + triggerEventTypes: { + required: false, + editable: false, + type: 'string[]', + options: [], + label: 'Trigger Events', + order: 6, + tooltip: 'This agent is triggered by user chat.', + }, + eventThreshold: { + required: false, + editable: false, + type: 'number', + label: 'Trigger Threshold', + order: 7, + tooltip: 'This agent does not have a trigger theshold', + }, + criteriaToGroupStudents: { + required: false, + editable: false, + type: 'string', + label: 'Grouping Criteria', + order: 8, + tooltip: 'This agent does not regroup students.', + }, + workflowsToActivate: { + required: false, + editable: false, + type: 'string[]', + label: 'Workflows to Activate', + order: 9, + tooltip: 'This agent does not trigger workflows', + }, + criteriaToActivateWorkflow: { + required: false, + editable: false, + type: 'string', + label: 'Workflow Activation Criteria', + order: 10, + tooltip: 'This agent does not use workflow activation criteria', + }, + databaseWriteAccess: { + required: true, + editable: true, + type: 'boolean', + options: [ + { value: true, label: 'True' }, + { value: false, label: 'False' }, + ], + label: 'Database Write Access', + order: 11, + tooltip: 'Allow the agent to write to the database?', + }, + uiIntegrations: { + required: false, + editable: true, + type: 'string[]', + options: [ + { value: 'Workspace', label: 'Workspace' }, + { value: 'Canvas', label: 'Canvas' }, + { value: 'Bucket View', label: 'Bucket View' }, + { value: 'Monitor', label: 'Monitor' }, + ], + label: 'AI Access Locations', + order: 12, + tooltip: 'Select where students can interact with the AI.', + }, + enabled: { + required: true, + editable: true, + type: 'boolean', + options: [ + { value: true, label: 'True' }, + { value: false, label: 'False' }, + ], + defaultValue: true, + label: 'Enabled', + order: 13, + tooltip: 'Enable or disable this agent.', + }, + }, + group_interaction: { + persona: { + required: false, + editable: false, + type: 'string', + label: 'Persona', + order: 1, + tooltip: 'The purpose, role, or identity of the AI. This is fixed.', + }, + task: { + required: true, + editable: true, + type: 'string', + label: 'Task', + order: 2, + tooltip: 'Describe how the AI should behave or respond to prompts.', + }, + topic: { + required: true, + editable: true, + type: 'string', + label: 'Topic', + order: 3, + tooltip: + 'The topic of discussion or question that students are responding to.', + }, + payloadScope: { + required: false, + editable: true, + type: 'string[]', + options: [ + { value: 'canvas', label: 'Canvas' }, + { value: 'all', label: 'All' }, + { value: 'buckets', label: 'Buckets' }, + ], + defaultValue: ['all'], + label: 'Data Context', + order: 4, + tooltip: + 'Select the scope of data the agent will have access to. Select "All" or individual sources.', + }, + userScope: { + required: true, + editable: false, + type: 'string', + options: [{ value: 'group', label: 'Group' }], + label: 'User Scope', + order: 5, + tooltip: 'This agent applies to student groups.', + }, + triggerEventTypes: { + required: true, + editable: true, + type: 'string[]', + options: [ + { value: 'POST_CREATE', label: 'Post Create' }, + { value: 'POST_COMMENT', label: 'Post Comment' }, + { value: 'POST_UPVOTE', label: 'Post Upvote' }, + { value: 'POST_TAG', label: 'Post Tag' }, + ], + label: 'Trigger Events', + order: 6, + tooltip: 'Select the events that will trigger this agent.', + }, + eventThreshold: { + required: true, + editable: true, + type: 'number', + label: 'Trigger Threshold', + order: 7, + tooltip: + 'The number of trigger events required before the AI Agent is triggered.', + }, + criteriaToGroupStudents: { + required: false, + editable: false, + type: 'string', + label: 'Grouping Criteria', + order: 8, + tooltip: 'This agent does not regroup students.', + }, + workflowsToActivate: { + required: false, + editable: false, + type: 'string[]', + label: 'Workflows to Activate', + order: 9, + tooltip: 'This agent does not trigger workflows', + }, + criteriaToActivateWorkflow: { + required: false, + editable: false, + type: 'string', + label: 'Workflow Activation Criteria', + order: 10, + tooltip: 'This agent does not use workflow activation criteria', + }, + databaseWriteAccess: { + required: true, + editable: true, + type: 'boolean', + options: [ + { value: true, label: 'True' }, + { value: false, label: 'False' }, + ], + label: 'Database Write Access', + order: 11, + tooltip: 'Allow the agent to write to the database?', + }, + uiIntegrations: { + required: false, + editable: true, + type: 'string[]', + options: [ + { value: 'Workspace', label: 'Workspace' }, + { value: 'Canvas', label: 'Canvas' }, + { value: 'Bucket View', label: 'Bucket View' }, + { value: 'Monitor', label: 'Monitor' }, + ], + label: 'AI Access Locations', + order: 12, + tooltip: 'Select where students can interact with the AI.', + }, + enabled: { + required: true, + editable: true, + type: 'boolean', + options: [ + { value: true, label: 'True' }, + { value: false, label: 'False' }, + ], + defaultValue: true, + label: 'Enabled', + order: 13, + tooltip: 'Enable or disable this agent.', + }, + }, + workflow: { + persona: { + required: false, + editable: false, + type: 'string', + label: 'Persona', + order: 1, + tooltip: 'The purpose, role, or identity of the AI. This is fixed.', + }, + task: { + required: true, + editable: true, + type: 'string', + options: [{ value: 'group students', label: 'Group Students' }], + label: 'Task', + order: 2, + tooltip: 'Select what the agent will do.', + }, + topic: { + required: true, + editable: true, + type: 'string', + label: 'Topic', + order: 3, + tooltip: + 'The topic of discussion or question that students are responding to.', + }, + payloadScope: { + required: false, + editable: true, + type: 'string[]', + options: [ + { value: 'canvas', label: 'Canvas' }, + { value: 'all', label: 'All' }, + { value: 'buckets', label: 'Buckets' }, + ], + defaultValue: ['all'], + label: 'Data Context', + order: 4, + tooltip: + 'Select the scope of data the agent will have access to. Select "All" or individual sources.', + }, + userScope: { + required: true, + editable: false, + type: 'string', + options: [{ value: 'all', label: 'All' }], + label: 'User Scope', + order: 5, + tooltip: 'This agent applies to all users.', + }, + triggerEventTypes: { + required: false, + editable: false, + type: 'string[]', + options: [], + label: 'Trigger Events', + order: 6, + tooltip: 'This agent is triggered manually.', + }, + eventThreshold: { + required: false, + editable: false, + type: 'number', + label: 'Trigger Threshold', + order: 7, + tooltip: 'This agent does not have a trigger theshold', + }, + criteriaToGroupStudents: { + required: true, + editable: true, + type: 'string', + label: 'Grouping Criteria', + order: 8, + tooltip: 'Define criteria by which to group students', + }, + workflowsToActivate: { + required: true, + editable: true, + type: 'string[]', + label: 'Workflows to Activate', + order: 9, + tooltip: 'Select workflows to activate', + }, + criteriaToActivateWorkflow: { + required: true, + editable: true, + type: 'string', + label: 'Workflow Activation Criteria', + order: 10, + tooltip: 'Define criteria for activating workflows', + }, + databaseWriteAccess: { + required: true, + editable: true, + type: 'boolean', + options: [ + { value: true, label: 'True' }, + { value: false, label: 'False' }, + ], + label: 'Database Write Access', + order: 11, + tooltip: 'Allow the agent to write to the database?', + }, + uiIntegrations: { + required: false, + editable: true, + type: 'string[]', + options: [ + { value: 'Workspace', label: 'Workspace' }, + { value: 'Canvas', label: 'Canvas' }, + { value: 'Bucket View', label: 'Bucket View' }, + { value: 'Monitor', label: 'Monitor' }, + ], + label: 'AI Access Locations', + order: 12, + tooltip: 'Select where students can interact with the AI.', + }, + enabled: { + required: true, + editable: false, + type: 'boolean', + defaultValue: true, + label: 'Enabled', + order: 13, + tooltip: 'This agent is always enabled.', + }, + }, +}; diff --git a/frontend/src/app/models/ai-agent.ts b/frontend/src/app/models/ai-agent.ts new file mode 100644 index 00000000..342435c9 --- /dev/null +++ b/frontend/src/app/models/ai-agent.ts @@ -0,0 +1,134 @@ +// src/app/models/ai-agent.ts + +// Interface for configuration data (used in getConfig) is no longer used. + +// Base class for all AI Agents (Simplified) +export abstract class AiAgent { + aiAgentID: string; + activityID: string; + name: string; + type: string; + persona: string | null = null; + agentType: string | null = null; + trigger: string | null = null; + triggerEventTypes?: string[] | null = null; + eventThreshold?: number | null = null; + aiPublishChannel: string | null = null; + aiSubscriptionChannel: string | null = null; + payloadScope: string[] | null = null; + userScope: string | null = null; + task: string | null = null; + databaseWriteAccess: boolean | null = null; + uiIntegrations: string[] | null = null; + enabled: boolean | null = null; + topic: string | null = null; + criteriaToGroupStudents?: string | null = null; + workflowsToActivate?: string[] | null = null; + criteriaToActivateWorkflow?: string | null = null; + order: number = 0; + + constructor(data: Partial) { + Object.assign(this, data); + } +} + +// --- Teacher Agent --- +export class TeacherAgent extends AiAgent { // <--- EXPORT added + constructor(data: Partial = {}) { + super({ + ...data, + type: 'teacher', + name: 'Teacher Agent', + agentType: 'chat', + trigger: 'chat', + userScope: 'all', + persona: "You are a helpful teaching assistant...", + task: "Guide classroom discussion.", + payloadScope: data.payloadScope ?? ['all'], + enabled: data.enabled ?? true, + // No need to set undefined properties: + // databaseWriteAccess: data.databaseWriteAccess ?? undefined, + // uiIntegrations: data.uiIntegrations ?? undefined, + }); + } +} + +// --- Idea Agent (Chat) --- +export class IdeaAgentChat extends AiAgent { // <--- EXPORT added + constructor(data: Partial = {}) { + super({ + ...data, + type: 'idea_chat', + name: 'Idea Agent (Chat)', + agentType: 'chat', + trigger: 'chat', + userScope: 'all', + persona: "You are an idea generation assistant...", + task: "Generate diverse ideas related to the topic.", + databaseWriteAccess: false, + uiIntegrations: [], // Correctly initialize to empty array + payloadScope: data.payloadScope ?? ['all'], + enabled: data.enabled ?? true, + }); + } +} + +// --- Idea Agent (Ambient) --- +export class IdeaAgentAmbient extends AiAgent { // <--- EXPORT added + constructor(data: Partial = {}) { + super({ + ...data, + type: 'idea_ambient', + name: 'Idea Agent (Ambient)', + agentType: 'ambient', + trigger: 'event', + userScope: 'all', + persona: "You are an idea generation assistant that monitors student activity...", + task: "Generate ideas based on student posts and comments.", + databaseWriteAccess: false, + uiIntegrations: [], // Correctly initialize + payloadScope: data.payloadScope ?? ['all'], + enabled: data.enabled ?? true, + }); + } +} +// --- Personal Learning Agent --- +export class PersonalLearningAgent extends AiAgent { // <--- EXPORT added + constructor(data: Partial = {}) { + super({ ...data, type: 'personal_learning', name: 'Personal Learning Agent' }); + this.agentType = 'chat'; + this.trigger = 'chat'; + if (this.enabled === undefined || this.enabled === null) { + this.enabled = true; //default to true + } + } + } + + // --- Group Interaction Agent --- + export class GroupInteractionAgent extends AiAgent { // <--- EXPORT added + constructor(data: Partial = {}) { + super({...data, type: 'group_interaction', name: 'Group Interaction Agent'}); + this.agentType = 'ambient'; + this.trigger = 'event'; + this.userScope = 'group'; + this.persona = "You are a group interaction monitor..."; // Fixed persona + if (this.enabled === undefined || this.enabled === null) { + this.enabled = true; //default to true + } + } +} + +// --- Workflow Agent --- +export class WorkflowAgent extends AiAgent { // <--- EXPORT added + constructor(data: Partial = {}) { + super({...data, type: 'workflow', name: 'Workflow Agent'}); + this.agentType = 'ambient'; + this.trigger = 'manual'; + this.userScope = 'all'; + this.persona = "You are a workflow automation agent..."; // Fixed persona + this.task = "group students"; // Fixed task for now + if (this.enabled === undefined || this.enabled === null) { + this.enabled = true; //default to true + } + } +} \ No newline at end of file diff --git a/frontend/src/app/models/bucket.ts b/frontend/src/app/models/bucket.ts index 535bb4f0..cb370384 100644 --- a/frontend/src/app/models/bucket.ts +++ b/frontend/src/app/models/bucket.ts @@ -1,11 +1,15 @@ -import Post from './post'; +// src/app/models/bucket.ts +import Post from './post'; export default class Bucket { - bucketID: string; - boardID: string; + public bucketID: string; + public boardID: string; + public name: string; + public posts: string[]; + public addedToView: boolean = false; - name: string; - posts: string[]; - - addedToView: boolean = false; -} + // Add a constructor for easier object creation + constructor(data: Partial) { + Object.assign(this, data); + } +} \ No newline at end of file diff --git a/frontend/src/app/models/resource.ts b/frontend/src/app/models/resource.ts index e7465976..771b3865 100644 --- a/frontend/src/app/models/resource.ts +++ b/frontend/src/app/models/resource.ts @@ -7,6 +7,7 @@ export interface Resource { bucketView: boolean; workspace: boolean; monitor: boolean; + ideaAgent: boolean; groupIDs: string[]; // ... other properties as needed ... } \ No newline at end of file From 77d121bf963d8ca5590966ee23c4218b8eb5d470 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Mon, 10 Feb 2025 20:14:36 -0500 Subject: [PATCH 22/29] Added roomcasting component, logic, and socket connections; modified canvas, buckets, workspace, and monitor to also read active route properties from Input() properties (from html element); Added SCORE Authoring emits RESOURCES_UPDATED but listening in SCORE Roomcasting Environment is not yet complete --- frontend/src/app/app-routing.module.ts | 5 + frontend/src/app/app.module.ts | 2 + .../app/components/canvas/canvas.component.ts | 322 ++++++++++++------ .../ck-buckets/ck-buckets.component.ts | 54 ++- .../ck-monitor/ck-monitor.component.ts | 73 ++-- .../ck-workspace/ck-workspace.component.ts | 70 ++-- .../score-authoring.component.html | 7 + .../score-authoring.component.ts | 145 +++++--- ...ore-roomcasting-environment.component.html | 52 +++ ...ore-roomcasting-environment.component.scss | 66 ++++ ...-roomcasting-environment.component.spec.ts | 23 ++ ...score-roomcasting-environment.component.ts | 186 ++++++++++ frontend/src/app/utils/constants.ts | 2 + 13 files changed, 795 insertions(+), 212 deletions(-) create mode 100644 frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.html create mode 100644 frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.scss create mode 100644 frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.spec.ts create mode 100644 frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index b3ba1a2c..69a57e3c 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -5,6 +5,7 @@ import { CkBucketsComponent } from './components/ck-buckets/ck-buckets.component import { CkMonitorComponent } from './components/ck-monitor/ck-monitor.component'; import { CkWorkspaceComponent } from './components/ck-workspace/ck-workspace.component'; import { ScoreAuthoringComponent } from './components/score-authoring/score-authoring.component'; +import { ScoreRoomcastingEnvironmentComponent } from './components/score-roomcasting-environment/score-roomcasting-environment.component'; import { DashboardComponent } from './components/dashboard/dashboard.component'; import { ErrorComponent } from './components/error/error.component'; import { LoginComponent } from './components/login/login.component'; @@ -75,6 +76,10 @@ const routes: Routes = [ component: ScoreAuthoringComponent, canActivate: [SsoGuard, AuthGuard, ProjectGuard], }, + { path: 'roomcast/:projectID', + component: ScoreRoomcastingEnvironmentComponent, + canActivate: [SsoGuard, AuthGuard, ProjectGuard] + }, { path: 'error', component: ErrorComponent }, { path: '**', redirectTo: 'error' }, ]; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 59f610e6..dc5f8dee 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -73,6 +73,7 @@ import { ConfigureAiAgentModalComponent } from './components/configure-ai-agent- import { CustomTeacherPromptModalComponent } from './components/custom-teacher-prompt-modal/custom-teacher-prompt-modal.component'; import { ScoreViewModalComponent } from './components/score-view-modal/score-view-modal.component'; import { ShowJoinCodeComponent } from './components/show-join-code/show-join-code.component'; +import { ScoreRoomcastingEnvironmentComponent } from './components/score-roomcasting-environment/score-roomcasting-environment.component'; const config: SocketIoConfig = { @@ -136,6 +137,7 @@ export function tokenGetter() { CustomTeacherPromptModalComponent, ScoreViewModalComponent, ShowJoinCodeComponent, + ScoreRoomcastingEnvironmentComponent, ], entryComponents: [ ScoreViewModalComponent, diff --git a/frontend/src/app/components/canvas/canvas.component.ts b/frontend/src/app/components/canvas/canvas.component.ts index 58c2a698..736e42c5 100644 --- a/frontend/src/app/components/canvas/canvas.component.ts +++ b/frontend/src/app/components/canvas/canvas.component.ts @@ -1,4 +1,13 @@ -import { Component, Input, OnDestroy, OnInit, HostListener, ElementRef, Renderer2, ChangeDetectorRef } from '@angular/core'; +import { + Component, + Input, + OnDestroy, + OnInit, + HostListener, + ElementRef, + Renderer2, + ChangeDetectorRef, +} from '@angular/core'; import { fabric } from 'fabric'; import { Canvas } from 'fabric/fabric-impl'; @@ -57,8 +66,6 @@ import { ProjectTodoListModalComponent } from '../project-todo-list-modal/projec styleUrls: ['./canvas.component.scss'], }) export class CanvasComponent implements OnInit, OnDestroy { - boardID: string; - projectID: string; canvas: Canvas; user: AuthUser; @@ -73,8 +80,6 @@ export class CanvasComponent implements OnInit, OnDestroy { finalClientX = 0; finalClientY = 0; - embedded = false; - numSavedPosts: number = 0; zoom = 1; @@ -108,6 +113,10 @@ export class CanvasComponent implements OnInit, OnDestroy { } @Input() isModalView = false; + @Input() projectID: string; + @Input() boardID: string; + @Input() embedded: boolean = false; + private resizeObserver: ResizeObserver; constructor( @@ -163,29 +172,51 @@ export class CanvasComponent implements OnInit, OnDestroy { } async ngOnInit() { - this.activatedRoute.queryParams.subscribe((params) => { - if (params.embedded == 'true') { - this.embedded = true; - } - }); + // Prioritize @Input() properties if provided + if (this.projectID && this.boardID) { + this.user = this.userService.user!; + this.isTeacher = this.user.role === Role.TEACHER; + this.canvas = new fabric.Canvas( + 'canvas', + this.embedded + ? this.fabricUtils.embeddedCanvasConfig + : this.fabricUtils.canvasConfig + ); + this.fabricUtils._canvas = this.canvas; + await this.configureBoard(); // Load board data + this.socketService.connect(this.user.userID, this.boardID); + this.initCanvasEventsListener(); + this.initGroupEventsListener(); + window.onbeforeunload = () => this.ngOnDestroy(); - this.user = this.userService.user!; - this.isTeacher = this.user.role === Role.TEACHER; - this.canvas = new fabric.Canvas( - 'canvas', - this.embedded - ? this.fabricUtils.embeddedCanvasConfig - : this.fabricUtils.canvasConfig - ); - this.fabricUtils._canvas = this.canvas; - await this.configureBoard(); - this.socketService.connect(this.user.userID, this.boardID); + } else { + // Fallback to ActivatedRoute (for direct routing) + this.activatedRoute.queryParams.subscribe((params) => { + if (params.embedded == 'true') { + this.embedded = true; + } + }); - this.initCanvasEventsListener(); - this.initGroupEventsListener(); + this.user = this.userService.user!; + this.isTeacher = this.user.role === Role.TEACHER; + this.canvas = new fabric.Canvas( + 'canvas', + this.embedded + ? this.fabricUtils.embeddedCanvasConfig + : this.fabricUtils.canvasConfig + ); + this.fabricUtils._canvas = this.canvas; - window.onbeforeunload = () => this.ngOnDestroy(); + this.configureBoard().then(() => { //Use then as configure board is now async + + this.socketService.connect(this.user.userID, this.boardID); + this.initCanvasEventsListener(); + this.initGroupEventsListener(); + + }); + window.onbeforeunload = () => this.ngOnDestroy(); + } } initCanvasEventsListener() { @@ -465,78 +496,173 @@ export class CanvasComponent implements OnInit, OnDestroy { async configureBoard() { const map = this.activatedRoute.snapshot.paramMap; - if (map.has('boardID') && map.has('projectID')) { - this.boardID = this.activatedRoute.snapshot.paramMap.get('boardID') ?? ''; - this.projectID = - this.activatedRoute.snapshot.paramMap.get('projectID') ?? ''; - this.traceService.setTraceContext(this.projectID, this.boardID); - this.postService.getAllByBoard(this.boardID).then((data) => { - data.forEach(async (post) => { - if (post.type == PostType.BOARD) { - const upvotes = await this.upvotesService.getUpvotesByPost( - post.postID - ); - const comments = await this.commentService.getCommentsByPost( - post.postID - ); - this.canvas.add( - new FabricPostComponent(post, { - upvotes: upvotes.length, - comments: comments.length, - }) - ); - } - }); - this.boardService.get(this.boardID).then((board) => { - if (board) this.intermediateBoardConfig(board); - if ( - !this.isTeacher && - this.board && - !this.board.viewSettings?.allowCanvas - ) { - this.router.navigateByUrl( - `project/${this.projectID}/board/${ - this.boardID - }/${this.board.defaultView?.toLowerCase()}` - ); - } - }); - }); - } else if (map.has('projectID')) { - this.projectID = - this.activatedRoute.snapshot.paramMap.get('projectID') ?? ''; - const personalBoard = await this.boardService.getPersonal(this.projectID); - if (personalBoard) { - this.boardID = personalBoard.boardID; + if (this.projectID && this.boardID) { + //use inputs if available + try { + const tempBoard = await this.boardService.get(this.boardID); // Await here + if (!tempBoard) { + // Handle the case where the board is not found. + console.error("Board not found:", this.boardID); + this.snackbarService.queueSnackbar("Board not found."); + this.router.navigate(['/error']); // Redirect, or show error + return; // IMPORTANT: Stop execution + } + this.board = tempBoard; + this.project = await this.projectService.get(this.projectID); //load project + if(!this.project){ + console.error("Project not found:", this.projectID); + this.snackbarService.queueSnackbar("Project not found."); + this.router.navigate(['/error']); // Redirect, or show error + return; // IMPORTANT: Stop execution + } + + this.intermediateBoardConfig(this.board); // Configure + this.traceService.setTraceContext(this.projectID, this.boardID); //moved here + this.postService.getAllByBoard(this.boardID).then((data) => { //get all posts + data.forEach(async (post) => { + if (post.type == PostType.BOARD) { + const upvotes = await this.upvotesService.getUpvotesByPost( + post.postID + ); + const comments = await this.commentService.getCommentsByPost( + post.postID + ); + this.canvas.add( + new FabricPostComponent(post, { + upvotes: upvotes.length, + comments: comments.length, + }) + ); + } + }); + }); + } + catch(error: any) + { + console.error("Error in configure board", error); + this.snackbarService.queueSnackbar("Error in configure board"); + this.router.navigate(['/error']); + return; + } + + } + else if (map.has('boardID') && map.has('projectID')) { //get from routed params + this.boardID = this.activatedRoute.snapshot.paramMap.get('boardID') ?? ''; + this.projectID = + this.activatedRoute.snapshot.paramMap.get('projectID') ?? ''; this.traceService.setTraceContext(this.projectID, this.boardID); - } else this.router.navigate(['error']); - - this.postService.getAllByBoard(this.boardID).then((data) => { - data.forEach(async (post) => { - if (post.type == PostType.BOARD) { - const upvotes = await this.upvotesService.getUpvotesByPost( - post.postID - ); - const comments = await this.commentService.getCommentsByPost( - post.postID - ); - this.canvas.add( - new FabricPostComponent(post, { - upvotes: upvotes.length, - comments: comments.length, - }) - ); - } - }); - if (personalBoard) this.intermediateBoardConfig(personalBoard); - }); - } else { - this.router.navigate(['error']); + + try { // *** ADDED try/catch *** + const tempBoard = await this.boardService.get(this.boardID); // Await here + if (!tempBoard) { + console.error("Board not found for ID:", this.boardID); + this.snackbarService.queueSnackbar("Board not found."); + this.router.navigate(['/error']); + return; + } + this.board = tempBoard; + + this.project = await this.projectService.get(this.projectID); // Await here + if (!this.project) { + console.error("Project not found for ID:", this.projectID); + this.snackbarService.queueSnackbar("Project not found."); + this.router.navigate(['/error']); + return; + } + + this.intermediateBoardConfig(this.board); + this.postService.getAllByBoard(this.boardID).then((data) => { + data.forEach(async (post) => { + if (post.type == PostType.BOARD) { + const upvotes = await this.upvotesService.getUpvotesByPost( + post.postID + ); + const comments = await this.commentService.getCommentsByPost( + post.postID + ); + this.canvas.add( + new FabricPostComponent(post, { + upvotes: upvotes.length, + comments: comments.length, + }) + ); + } + }); + if ( + !this.isTeacher && + this.board && + !this.board.viewSettings?.allowCanvas + ) { + this.router.navigateByUrl( + `project/${this.projectID}/board/${ + this.boardID + }/${this.board.defaultView?.toLowerCase()}` + ); + } + }); + } catch (error: any) { // *** ADDED ERROR HANDLING *** + console.error("Error configuring board (routed):", error); + this.snackbarService.queueSnackbar("Error configuring board."); + this.router.navigate(['/error']); // Or handle differently + return; // IMPORTANT + } + } else if (map.has('projectID')) { //personal board + this.projectID = + this.activatedRoute.snapshot.paramMap.get('projectID') ?? ''; + try { + const personalBoard = await this.boardService.getPersonal(this.projectID); + if (personalBoard) { + this.boardID = personalBoard.boardID; + this.traceService.setTraceContext(this.projectID, this.boardID); + } else { + console.error("Personal board not found for projectID:", this.projectID); + this.snackbarService.queueSnackbar("Personal board not found"); + this.router.navigate(['/error']); + return; + } + this.project = await this.projectService.get(this.projectID); // Await here + if (!this.project) { + console.error("Project not found for ID:", this.projectID); + this.snackbarService.queueSnackbar("Project not found."); + this.router.navigate(['/error']); + return; + } + + this.postService.getAllByBoard(this.boardID).then((data) => { //get all posts + data.forEach(async (post) => { + if (post.type == PostType.BOARD) { + const upvotes = await this.upvotesService.getUpvotesByPost( + post.postID + ); + const comments = await this.commentService.getCommentsByPost( + post.postID + ); + this.canvas.add( + new FabricPostComponent(post, { + upvotes: upvotes.length, + comments: comments.length, + }) + ); + } + }); + if (personalBoard) this.intermediateBoardConfig(personalBoard); + }); + + } + catch(error: any){ + console.error("Error in configure board, personal board", error); + this.snackbarService.queueSnackbar("Error in configure board"); + this.router.navigate(['/error']); + return; + } + + } else { //no project id or board id + console.error("Missing required route parameters (projectID and/or boardID)"); + this.snackbarService.queueSnackbar("Error in configure board"); + this.router.navigate(['error']); // Or handle differently + return; // IMPORTANT } - this.projectService - .get(this.projectID) - .then((project) => (this.project = project)); - } +} // TODO: handle board update from toolbar-menu configureZoom() { @@ -1134,10 +1260,10 @@ export class CanvasComponent implements OnInit, OnDestroy { private _calcUpvoteCounter() { if (this.board && this.board.upvoteLimit) { this.upvotesService - .getByBoardAndUser(this.boardID, this.user.userID) - .then((votes) => { - this.upvoteCounter = this.board.upvoteLimit - votes.length; - }); + .getByBoardAndUser(this.boardID, this.user.userID) + .then((votes) => { + this.upvoteCounter = this.board.upvoteLimit - votes.length; + }); } } diff --git a/frontend/src/app/components/ck-buckets/ck-buckets.component.ts b/frontend/src/app/components/ck-buckets/ck-buckets.component.ts index 69bbe1cf..2dbb9a1d 100644 --- a/frontend/src/app/components/ck-buckets/ck-buckets.component.ts +++ b/frontend/src/app/components/ck-buckets/ck-buckets.component.ts @@ -29,9 +29,9 @@ export class CkBucketsComponent implements OnInit, OnDestroy { @ViewChild(MatPaginator) paginator: MatPaginator; @Input() isModalView = false; - - boardID: string; - projectID: string; + @Input() projectID: string; + @Input() boardID: string; + @Input() embedded: boolean = false; buckets: any[] = []; user: AuthUser; @@ -69,19 +69,28 @@ export class CkBucketsComponent implements OnInit, OnDestroy { async ngOnInit(): Promise { this.user = this.userService.user!; this.isTeacher = this.user.role === Role.TEACHER; - await this.configureBoard(); - await this.bucketService.getAllByBoard(this.boardID).then((buckets) => { - if (buckets) { - for (const bucket of buckets) { - if (bucket.addedToView) { - this.bucketsOnView.push(bucket); - this.loadBucketPosts(bucket); - } else { - this.buckets.push(bucket); - } + // Prioritize Input properties. If they are provided, use them. + if (this.boardID && this.projectID) { + // We got the IDs from the inputs, proceed as normal. + await this.configureBoard(); // Load board data + this.loadBuckets(); // Load buckets + + } else { + // Fallback to ActivatedRoute *only* if inputs are not provided. + this.activatedRoute.paramMap.subscribe(async params => { // Use paramMap (Observable) + this.boardID = params.get('boardID')!; //use of ! operator + this.projectID = params.get('projectID')!; //use of ! operator + + if (!this.boardID || !this.projectID) { + console.error("Missing boardID or projectID in route parameters"); + this.router.navigate(['/error']); // Redirect to an error page, or handle appropriately + return; // IMPORTANT: Stop execution } - } - }); + + await this.configureBoard(); // Load board data + this.loadBuckets(); // Load buckets + }); + } this.socketService.connect(this.user.userID, this.boardID); } @@ -92,6 +101,21 @@ export class CkBucketsComponent implements OnInit, OnDestroy { } } + async loadBuckets() { + if(!this.boardID) return; + const buckets = await this.bucketService.getAllByBoard(this.boardID); + if(buckets) { + for (const bucket of buckets) { + if (bucket.addedToView) { + this.bucketsOnView.push(bucket); + this.loadBucketPosts(bucket); + } else { + this.buckets.push(bucket); + } + } + } + } + async configureBoard(): Promise { const map = this.activatedRoute.snapshot.paramMap; if (map.has('boardID') && map.has('projectID')) { diff --git a/frontend/src/app/components/ck-monitor/ck-monitor.component.ts b/frontend/src/app/components/ck-monitor/ck-monitor.component.ts index ecd98dc4..22806c68 100644 --- a/frontend/src/app/components/ck-monitor/ck-monitor.component.ts +++ b/frontend/src/app/components/ck-monitor/ck-monitor.component.ts @@ -98,6 +98,9 @@ export class CkMonitorComponent implements OnInit, OnDestroy { } @Input() isModalView = false; + @Input() projectID: string; + @Input() boardID: string; + @Input() embedded: boolean = false; user: AuthUser; group: Group; @@ -171,7 +174,6 @@ export class CkMonitorComponent implements OnInit, OnDestroy { displayColumns: string[] = ['group-name', 'members', 'progress', 'action']; loading: boolean = true; - embedded: boolean = false; showModels = false; @@ -216,38 +218,65 @@ export class CkMonitorComponent implements OnInit, OnDestroy { this.studentView = true; this.loading = false; } - await this.loadWorkspaceData(); - if (this.studentView) this.showModels = true; - } - async loadWorkspaceData(): Promise { - const map = this.activatedRoute.snapshot.paramMap; - let boardID: string, projectID: string; + // Prioritize Input properties. If they are provided, use them. + if (this.projectID && this.boardID) { + await this.loadWorkspaceData(); // Load with Input IDs + this.socketService.connect(this.user.userID, this.boardID); // Moved after to ensure boardID is ready - if (map.has('boardID') && map.has('projectID')) { - boardID = this.activatedRoute.snapshot.paramMap.get('boardID') ?? ''; - projectID = this.activatedRoute.snapshot.paramMap.get('projectID') ?? ''; } else { - return this.router.navigate(['error']); + // Fallback to ActivatedRoute *ONLY* if inputs are not provided. + this.activatedRoute.paramMap.subscribe(async params => { + this.boardID = params.get('boardID')!; + this.projectID = params.get('projectID')!; + + if (!this.boardID || !this.projectID) { + console.error("Missing boardID or projectID in route parameters"); + this.router.navigate(['/error']); // Redirect to an error page + return; // Stop execution + } + + await this.loadWorkspaceData(); + this.socketService.connect(this.user.userID, this.boardID); // Moved after + }); + } + } + + async loadWorkspaceData(): Promise { + // No longer need to get from route since we prioritize inputs + if (!this.boardID || !this.projectID) { + console.error("boardId or projectId is null"); + return false; } - const fetchedBoard = await this.boardService.get(boardID); + const fetchedBoard = await this.boardService.get(this.boardID); if (!fetchedBoard) { + console.error("board not found") this.router.navigate(['error']); - return false; // or true depending on your flow + return false; } this.board = fetchedBoard; - - this.project = await this.projectService.get(projectID); + this.project = await this.projectService.get(this.projectID); + //get group may return undefined. + try { + this.group = await this.groupService.getByProjectUser( //no longer need to await + this.projectID, + this.user.userID + ); + } + catch (error: any) + { + console.error("Could not fetch group"); + } if (!this.isTeacher && !this.board.viewSettings?.allowMonitor) { - this.router.navigateByUrl( - `project/${projectID}/board/${boardID}/${this.board.defaultView?.toLowerCase()}` - ); - } + this.router.navigateByUrl( + `project/${this.projectID}/board/${this.boardID}/${this.board.defaultView?.toLowerCase()}` + ); + } + + if (!this.studentView) await this.updateWorkflowData(this.boardID, this.projectID); - if (!this.studentView) await this.updateWorkflowData(boardID, projectID); - this.socketService.connect(this.user.userID, this.board.boardID); return true; } @@ -329,7 +358,7 @@ export class CkMonitorComponent implements OnInit, OnDestroy { this.activeTaskWorkflows = activeTaskWorkflows; this.loading = false; } - + async updateTodoItemDataSource(): Promise { const data: TodoItemDisplay[] = []; diff --git a/frontend/src/app/components/ck-workspace/ck-workspace.component.ts b/frontend/src/app/components/ck-workspace/ck-workspace.component.ts index 4732ca68..e47cfaf3 100644 --- a/frontend/src/app/components/ck-workspace/ck-workspace.component.ts +++ b/frontend/src/app/components/ck-workspace/ck-workspace.component.ts @@ -62,6 +62,9 @@ export class CkWorkspaceComponent implements OnInit, OnDestroy { @ViewChild(SwiperComponent) swiper: SwiperComponent; @Input() isModalView = false; + @Input() projectID: string; + @Input() boardID: string; + @Input() embedded: boolean = false; loading = false; @@ -93,7 +96,6 @@ export class CkWorkspaceComponent implements OnInit, OnDestroy { TaskActionType: typeof TaskActionType = TaskActionType; TaskWorkflowType: typeof TaskWorkflowType = TaskWorkflowType; GroupTaskStatus: typeof GroupTaskStatus = GroupTaskStatus; - embedded: boolean = false; // If standalone board embed viewType = ViewType.WORKSPACE; isTeacher: boolean = false; @@ -120,42 +122,66 @@ export class CkWorkspaceComponent implements OnInit, OnDestroy { }); } - ngOnInit(): void { + async ngOnInit(): Promise { this.user = this.userService.user!; this.isTeacher = this.user.role === Role.TEACHER; - this.loadWorkspaceData(); + // Prioritize Input properties. If they are provided, use them. + if (this.projectID && this.boardID) { + await this.loadWorkspaceData(); // Load with Input IDs + this.socketService.connect(this.user.userID, this.boardID); //Moved to after board loaded + } else { + // Fallback to ActivatedRoute ONLY if inputs are not provided. + this.activatedRoute.paramMap.subscribe(async params => { + this.boardID = params.get('boardID')!; + this.projectID = params.get('projectID')!; + + if (!this.boardID || !this.projectID) { + console.error("Missing boardID or projectID in route parameters"); + this.router.navigate(['/error']); // Redirect to an error page + return; // Stop execution + } + + await this.loadWorkspaceData(); + this.socketService.connect(this.user.userID, this.boardID); + }); + } } - async loadWorkspaceData(): Promise { - const map = this.activatedRoute.snapshot.paramMap; - let boardID: string, projectID: string; - if (map.has('boardID') && map.has('projectID')) { - boardID = this.activatedRoute.snapshot.paramMap.get('boardID') ?? ''; - projectID = this.activatedRoute.snapshot.paramMap.get('projectID') ?? ''; - } else { - return this.router.navigate(['error']); + async loadWorkspaceData(): Promise { + // No longer need to get from route since we prioritize inputs + if (!this.boardID || !this.projectID) { + console.error("boardId or projectId is null"); + return false; } - const fetchedBoard = await this.boardService.get(boardID); + const fetchedBoard = await this.boardService.get(this.boardID); if (!fetchedBoard) { + console.error("board not found") this.router.navigate(['error']); - return false; // or true depending on your flow + return false; } this.board = fetchedBoard; - this.project = await this.projectService.get(projectID); - this.group = await this.groupService.getByProjectUser( - projectID, - this.user.userID - ); + this.project = await this.projectService.get(this.projectID); + //get group may return undefined. + try { + this.group = await this.groupService.getByProjectUser( //no longer need to await + this.projectID, + this.user.userID + ); + } + catch (error: any) + { + console.error("Could not fetch group"); + } if (!this.isTeacher && !this.board.viewSettings?.allowWorkspace) { this.router.navigateByUrl( - `project/${projectID}/board/${boardID}/${this.board.defaultView?.toLowerCase()}` + `project/\{this\.projectID\}/board/{this.boardID}/${this.board.defaultView?.toLowerCase()}` ); } - const tasks = await this.workflowService.getGroupTasks(boardID, 'expanded'); + const tasks = await this.workflowService.getGroupTasks(this.boardID, 'expanded'); tasks.forEach((t) => { if (t.groupTask.status == GroupTaskStatus.INACTIVE) { this.inactiveGroupTasks.push(t); @@ -165,7 +191,7 @@ export class CkWorkspaceComponent implements OnInit, OnDestroy { this.completeGroupTasks.push(t); } }); - this.socketService.connect(this.user.userID, this.board.boardID); + return true; } @@ -227,7 +253,7 @@ export class CkWorkspaceComponent implements OnInit, OnDestroy { this._startListening(); } - + close(): void { this.runningGroupTask = null; this.currentGroupProgress = 0; diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.html b/frontend/src/app/components/score-authoring/score-authoring.component.html index 59ebd738..53544d64 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.html +++ b/frontend/src/app/components/score-authoring/score-authoring.component.html @@ -103,6 +103,13 @@

Buckets & Workflows

+
diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts index 973fd7b6..e8d7149f 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.ts +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -50,6 +50,7 @@ import { GroupInteractionAgent, WorkflowAgent, } from 'src/app/models/ai-agent'; +import { SocketEvent } from 'src/app/utils/constants'; @Component({ selector: 'app-score-authoring', @@ -325,65 +326,62 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } async dropAiAgentFromAvailable(event: CdkDragDrop) { - const agentType = this.availableAiAgents[event.previousIndex].type; - const agentClass = this.availableAiAgents[event.previousIndex].class; - // Open configuration modal *before* creating in the database - let newAgentData: Partial = { type: agentType }; //for idea agents - if (agentType === 'idea') { - const ideaAgentDialogRef = this.dialog.open( - ConfigureAiAgentModalComponent, - { - width: '800px', - height: '600px', - data: { agentType: 'idea_chat', agentClass: IdeaAgentChat }, - } - ); - const ideaChatResult = await ideaAgentDialogRef - .afterClosed() - .toPromise(); - - if (!ideaChatResult) { - //if cancelled, don't make any agents - return; - } - const ideaAgentAmbientDialogRef = this.dialog.open( - ConfigureAiAgentModalComponent, - { - width: '600px', - data: { - agentType: 'idea_ambient', - agentClass: IdeaAgentAmbient, - topic: ideaChatResult.topic, - enabled: ideaChatResult.enabled, - payloadScope: ideaChatResult.payloadScope, - }, // Pass the *class* - } - ); - const ideaAmbientResult = await ideaAgentAmbientDialogRef - .afterClosed() - .toPromise(); - if (!ideaAmbientResult) { - //if cancelled, don't make any agents - return; + const agentType = this.availableAiAgents[event.previousIndex].type; + const agentClass = this.availableAiAgents[event.previousIndex].class; + // Open configuration modal *before* creating in the database + let newAgentData: Partial = { type: agentType }; //for idea agents + if (agentType === 'idea') { + const ideaAgentDialogRef = this.dialog.open( + ConfigureAiAgentModalComponent, + { + width: '800px', + height: '600px', + data: { agentType: 'idea_chat', agentClass: IdeaAgentChat }, } - //Create and add both agents - await this.createAndAddAiAgent(ideaChatResult); - await this.createAndAddAiAgent(ideaAmbientResult); - } else { - const dialogRef = this.dialog.open(ConfigureAiAgentModalComponent, { + ); + const ideaChatResult = await ideaAgentDialogRef.afterClosed().toPromise(); + + if (!ideaChatResult) { + //if cancelled, don't make any agents + return; + } + const ideaAgentAmbientDialogRef = this.dialog.open( + ConfigureAiAgentModalComponent, + { width: '600px', - data: { agentType, agentClass: agentClass }, // Pass the *class* - }); + data: { + agentType: 'idea_ambient', + agentClass: IdeaAgentAmbient, + topic: ideaChatResult.topic, + enabled: ideaChatResult.enabled, + payloadScope: ideaChatResult.payloadScope, + }, // Pass the *class* + } + ); + const ideaAmbientResult = await ideaAgentAmbientDialogRef + .afterClosed() + .toPromise(); + if (!ideaAmbientResult) { + //if cancelled, don't make any agents + return; + } + //Create and add both agents + await this.createAndAddAiAgent(ideaChatResult); + await this.createAndAddAiAgent(ideaAmbientResult); + } else { + const dialogRef = this.dialog.open(ConfigureAiAgentModalComponent, { + width: '600px', + data: { agentType, agentClass: agentClass }, // Pass the *class* + }); - const result = await dialogRef.afterClosed().toPromise(); + const result = await dialogRef.afterClosed().toPromise(); - if (result) { - // Create the agent in the database and add to the active list - newAgentData = result; - await this.createAndAddAiAgent(newAgentData); - } + if (result) { + // Create the agent in the database and add to the active list + newAgentData = result; + await this.createAndAddAiAgent(newAgentData); } - + } } async createAndAddAiAgent(agentData: Partial) { if (!this.selectedActivity) { @@ -570,7 +568,44 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } start(activity: Activity) { - // ... (Implement logic to start the activity) ... + if (!this.selectedActivity) { + return; + } + // Construct the resource list object. + const resources = this.selectedActivityResources.map((res) => ({ + name: res.name, + order: res.order, + groupIDs: res.groupIDs, + canvas: res.canvas, + bucketView: res.bucketView, + workspace: res.workspace, + monitor: res.monitor, + ideaAgent: res.ideaAgent, + })); + + // *** ADD THESE LOG STATEMENTS *** + console.log("start() called for activity:", activity); + console.log("Emitting RESOURCES_UPDATE with data:", { + eventData: { + projectID: this.project.projectID, + activityID: this.selectedActivity.activityID, // Include activityID + resources: resources, + } + }); + + + // Emit the event with the correct structure: + this.socketService.emit(SocketEvent.RESOURCES_UPDATE, { + eventData: { + // This matches the RoomcastData interface + projectID: this.project.projectID, + resources: resources, + }, + }); + } + + openRoomCasting() { + this.router.navigate(['/roomcast', this.project.projectID]); } editActivity(activity: Activity) { diff --git a/frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.html b/frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.html new file mode 100644 index 00000000..01e33b5b --- /dev/null +++ b/frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.html @@ -0,0 +1,52 @@ + + SCORE Roomcasting - {{ project.name }} - {{board.name}} + + + + + + Canvas + +
+ +
+
+ + + + Bucket View + +
+ +
+
+ + + + Workspace + +
+
+
+ + + + Monitor + +
+
+
+ + + + Idea Agent + +
+ Content3 +
+
+
+ +
+ Loading... +
\ No newline at end of file diff --git a/frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.scss b/frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.scss new file mode 100644 index 00000000..eca0b8f8 --- /dev/null +++ b/frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.scss @@ -0,0 +1,66 @@ +@import '../../../styles.scss'; + +$controls-bg-color: whitesmoke; +$controls-border-radius: 0.75em; + +.top-nav { + padding: 1em; + position: absolute; + top: 1em; + left: 1em; + background-color: $controls-bg-color; + border-radius: $controls-border-radius; + border: 1px solid $accent; +} + +.top-embedded-board-name{ + padding: 0.6em; + position: absolute; + top: 0.5em; + left: 0.5em; + font-weight: bold; + background-color: white; + color: #6c63ff; + border-radius: 0.5em; + font-size: 1em; +} + +.bottom-nav { + padding: 1em; + position: absolute; + bottom: 1em; + left: 1em; + background-color: $controls-bg-color; + border-radius: $controls-border-radius; + border: 1px solid $accent; +} + +.toolbarTitle { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.toolSection { + position: absolute; + padding: 1em; + right: 1em; +} + +.toolField { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + + button { + margin-bottom: 1em; + } +} + +.embeddableLogo{ + height: 36px; + width: auto; + margin-right: 8px; + vertical-align: middle; +} \ No newline at end of file diff --git a/frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.spec.ts b/frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.spec.ts new file mode 100644 index 00000000..e57a3dbc --- /dev/null +++ b/frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ScoreRoomcastingEnvironmentComponent } from './score-roomcasting-environment.component'; + +describe('ScoreRoomcastingEnvironmentComponent', () => { + let component: ScoreRoomcastingEnvironmentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ScoreRoomcastingEnvironmentComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ScoreRoomcastingEnvironmentComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.ts b/frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.ts new file mode 100644 index 00000000..c7974fef --- /dev/null +++ b/frontend/src/app/components/score-roomcasting-environment/score-roomcasting-environment.component.ts @@ -0,0 +1,186 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; // Import Router +import { Subscription } from 'rxjs'; +import { SocketService } from 'src/app/services/socket.service'; +import { UserService } from 'src/app/services/user.service'; +import { AuthUser } from 'src/app/models/user'; +import { Resource } from 'src/app/models/resource'; +import { BoardService } from 'src/app/services/board.service'; +import { Board } from 'src/app/models/board'; +import { ProjectService } from 'src/app/services/project.service'; +import { SocketEvent } from 'src/app/utils/constants'; +import { SnackbarService } from 'src/app/services/snackbar.service'; // Import SnackbarService +import { HttpClient } from '@angular/common/http'; // Import HttpClient +import { Activity } from 'src/app/models/activity'; // Import Activity +import { GroupService } from 'src/app/services/group.service'; // Import GroupService +import { Group } from 'src/app/models/group'; + +@Component({ + selector: 'app-score-roomcasting-environment', + templateUrl: './score-roomcasting-environment.component.html', + styleUrls: ['./score-roomcasting-environment.component.scss'], +}) +export class ScoreRoomcastingEnvironmentComponent implements OnInit, OnDestroy { + projectID: string; + user: AuthUser; + availableTabs: { [key: string]: boolean } = {}; + activeTab: string | null = null; + resources: Resource[] = []; + board: Board | null = null; + boardID: string | null = null; + project: any; + activityId: string; + userGroupIDs: string[] = []; + + private socketSubscription: Subscription; + + constructor( + private route: ActivatedRoute, + private socketService: SocketService, + private userService: UserService, + private boardService: BoardService, + private projectService: ProjectService, + private router: Router, + private snackbarService: SnackbarService, + private http: HttpClient, + private groupService: GroupService + ) {} + + async ngOnInit(): Promise { + this.user = this.userService.user!; + this.projectID = this.route.snapshot.paramMap.get('projectID')!; + + // Listen for resource updates via socket + this.socketSubscription = this.socketService.listen( + SocketEvent.RESOURCES_UPDATE, + (data: any) => { + console.log("Resource Found") + this.resources = data.eventData.resources; + this.activityId = data.eventData.activityID; + this.updateAvailableTabs(); + if (!this.activeTab) { + this.setDefaultTab(); // Await setDefaultTab + } + } + ); + + // Fetch project data and user's groups *before* connecting. + this.project = await this.projectService.get(this.projectID); // Await the project + if ( + !this.project || + !this.project.boards || + this.project.boards.length === 0 + ) { + console.error('Project or boards not found!'); + this.snackbarService.queueSnackbar('Project or board not found.'); + this.router.navigate(['/error']); // Consider redirecting to an error page + return; + } + + // Get the user's groups for this project. + const groups: Group[] = await this.groupService.getByUserAndProject( + this.user.userID, + this.projectID + ); + this.userGroupIDs = groups.map((group) => group.groupID); // Extract the group IDs + } + + ngOnDestroy(): void { + if (this.socketSubscription) { + this.socketSubscription.unsubscribe(); + } + } + + updateAvailableTabs() { + this.availableTabs = {}; // Reset + + for (const resource of this.resources) { + let userInGroup = false; + // Check if ANY of the resource's groupIDs are in the user's groups + for (const groupID of resource.groupIDs) { + if (this.userGroupIDs.includes(groupID)) { + // Use userGroupIDs + userInGroup = true; + break; // Found a match, no need to check further + } + } + + if (userInGroup) { + if (resource.canvas) this.availableTabs['canvas'] = true; + if (resource.bucketView) this.availableTabs['bucketView'] = true; + if (resource.workspace) this.availableTabs['workspace'] = true; + if (resource.monitor) this.availableTabs['monitor'] = true; + if (resource.ideaAgent) this.availableTabs['ideaAgent'] = true; + } + } + } + + //Sets default tab based on lowest available order + async setDefaultTab() { + if (!this.activityId) return; + try { + const activity = await this.http + .get(`activities/${this.activityId}`) + .toPromise(); + if (!activity) { + console.error('Invalid Activity ID'); + return; + } + + // Get the board *before* connecting + const board = await this.boardService.get(activity.boardID); // Use await here + if (!board) { + // Check if board exists + console.error('Board not found for activity:', this.activityId); + this.snackbarService.queueSnackbar( + 'Board not found for this activity.' + ); + // Consider redirecting to an error page or some other fallback + return; + } + this.board = board; // Assign the board *after* the check + this.boardID = board.boardID; + + if (this.resources.length > 0) { + const sortedResources = [...this.resources].sort( + (a, b) => a.order - b.order + ); + for (const resource of sortedResources) { + if (resource.canvas && this.availableTabs['canvas']) { + this.activeTab = 'canvas'; + return; + } + if (resource.bucketView && this.availableTabs['bucketView']) { + this.activeTab = 'bucketView'; + return; + } + if (resource.workspace && this.availableTabs['workspace']) { + this.activeTab = 'workspace'; + return; + } + if (resource.monitor && this.availableTabs['monitor']) { + this.activeTab = 'monitor'; + return; + } + if (resource.ideaAgent && this.availableTabs['ideaAgent']) { + this.activeTab = 'ideaAgent'; + return; + } + } + } + } catch (error) { + this.snackbarService.queueSnackbar(`Error loading activity.`); + } + } + + setActiveTab(tabName: string) { + if (this.availableTabs[tabName]) { + //only change if tab is available + this.activeTab = tabName; + } + } + // Add a method to check if a tab is active + isTabActive(tabName: string): boolean { + return this.activeTab === tabName; + } +} diff --git a/frontend/src/app/utils/constants.ts b/frontend/src/app/utils/constants.ts index 628573ac..9e5658c2 100644 --- a/frontend/src/app/utils/constants.ts +++ b/frontend/src/app/utils/constants.ts @@ -52,6 +52,8 @@ export enum SocketEvent { AI_MESSAGE = 'AI_MESSAGE', // Event for sending AI message AI_RESPONSE = 'AI_RESPONSE', // Event for receiving AI response + + RESOURCES_UPDATE = 'RESOURCES_UPDATE', } export const STUDENT_POST_COLOR = '#FFF7C0'; From 8ba74093a0377ab022ed1623592d063707cb4e97 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Mon, 10 Feb 2025 23:09:06 -0500 Subject: [PATCH 23/29] Included backend files in commit --- backend/src/constants.ts | 2 ++ backend/src/socket/events.ts | 4 ++- backend/src/socket/events/roomcast.events.ts | 35 +++++++++++++++++++ .../score-authoring.component.ts | 14 ++------ 4 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 backend/src/socket/events/roomcast.events.ts diff --git a/backend/src/constants.ts b/backend/src/constants.ts index 847fd0df..38dd6e61 100644 --- a/backend/src/constants.ts +++ b/backend/src/constants.ts @@ -46,6 +46,8 @@ export enum SocketEvent { AI_MESSAGE = 'AI_MESSAGE', // Event for sending AI message AI_RESPONSE = 'AI_RESPONSE', // Event for receiving AI response + + RESOURCES_UPDATE = 'RESOURCES_UPDATE', } export const STUDENT_POST_COLOR = '#FFF7C0'; diff --git a/backend/src/socket/events.ts b/backend/src/socket/events.ts index a75cfbd2..0472a0a7 100644 --- a/backend/src/socket/events.ts +++ b/backend/src/socket/events.ts @@ -3,7 +3,8 @@ import postEvents from './events/post.events'; import workflowEvents from './events/workflow.events'; import notificationEvents from './events/notification.events'; import bucketEvents from './events/bucket.events'; -import aiEvents from './events/ai.events'; // Import the new aiEvents +import aiEvents from './events/ai.events'; +import roomcastEvents from './events/roomcast.events'; const events = [ ...postEvents, @@ -12,6 +13,7 @@ const events = [ ...notificationEvents, ...bucketEvents, ...aiEvents, + ...roomcastEvents, ]; export default events; diff --git a/backend/src/socket/events/roomcast.events.ts b/backend/src/socket/events/roomcast.events.ts new file mode 100644 index 00000000..1a5978f6 --- /dev/null +++ b/backend/src/socket/events/roomcast.events.ts @@ -0,0 +1,35 @@ +// backend/src/socket/events/roomcast.events.ts +import { Server, Socket } from 'socket.io'; +import { SocketEvent } from '../../constants'; +import { SocketPayload } from '../types/event.types'; + +interface RoomcastData { + projectID: string; + resources: any[]; // Use a more specific type if possible +} + + +class RoomcastUpdate { + static type: SocketEvent = SocketEvent.RESOURCES_UPDATE; + + static async handleEvent(data: SocketPayload): Promise { + const { projectID, resources } = data.eventData; + // Add any validation or processing of the data here, if needed. + // For example, you could check if the projectID is valid. + console.log("RoomcastUpdate handleEvent") + return { projectID, resources }; + } + + static async handleResult(io: Server, socket: Socket, result: RoomcastData): Promise { + const { projectID, resources } = result; + // Broadcast to all clients in the room (project) EXCEPT the sender + // socket.to(projectID).emit(SocketEvent.RESOURCES_UPDATE, resources); + // Could also just emit to the room, including sender. Depends on your needs. + console.log("Roomcast Update handleResult") + io.emit(SocketEvent.RESOURCES_UPDATE, resources); + } +} + +const roomcastEvents = [RoomcastUpdate]; + +export default roomcastEvents; \ No newline at end of file diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts index e8d7149f..515d388c 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.ts +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -355,7 +355,7 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { topic: ideaChatResult.topic, enabled: ideaChatResult.enabled, payloadScope: ideaChatResult.payloadScope, - }, // Pass the *class* + }, } ); const ideaAmbientResult = await ideaAgentAmbientDialogRef @@ -371,7 +371,7 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } else { const dialogRef = this.dialog.open(ConfigureAiAgentModalComponent, { width: '600px', - data: { agentType, agentClass: agentClass }, // Pass the *class* + data: { agentType, agentClass: agentClass }, }); const result = await dialogRef.afterClosed().toPromise(); @@ -583,16 +583,6 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { ideaAgent: res.ideaAgent, })); - // *** ADD THESE LOG STATEMENTS *** - console.log("start() called for activity:", activity); - console.log("Emitting RESOURCES_UPDATE with data:", { - eventData: { - projectID: this.project.projectID, - activityID: this.selectedActivity.activityID, // Include activityID - resources: resources, - } - }); - // Emit the event with the correct structure: this.socketService.emit(SocketEvent.RESOURCES_UPDATE, { From 564e848a4f9d2fe764bef486633b11de84d54930 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Mon, 10 Feb 2025 23:11:23 -0500 Subject: [PATCH 24/29] Included additional missing files --- frontend/src/app/components/canvas/canvas.component.ts | 6 +++--- .../src/app/components/ck-monitor/ck-monitor.component.ts | 4 ++-- .../app/components/ck-workspace/ck-workspace.component.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/components/canvas/canvas.component.ts b/frontend/src/app/components/canvas/canvas.component.ts index 736e42c5..3ce08c9c 100644 --- a/frontend/src/app/components/canvas/canvas.component.ts +++ b/frontend/src/app/components/canvas/canvas.component.ts @@ -552,7 +552,7 @@ export class CanvasComponent implements OnInit, OnDestroy { this.activatedRoute.snapshot.paramMap.get('projectID') ?? ''; this.traceService.setTraceContext(this.projectID, this.boardID); - try { // *** ADDED try/catch *** + try { const tempBoard = await this.boardService.get(this.boardID); // Await here if (!tempBoard) { console.error("Board not found for ID:", this.boardID); @@ -600,11 +600,11 @@ export class CanvasComponent implements OnInit, OnDestroy { ); } }); - } catch (error: any) { // *** ADDED ERROR HANDLING *** + } catch (error: any) { console.error("Error configuring board (routed):", error); this.snackbarService.queueSnackbar("Error configuring board."); this.router.navigate(['/error']); // Or handle differently - return; // IMPORTANT + return; } } else if (map.has('projectID')) { //personal board this.projectID = diff --git a/frontend/src/app/components/ck-monitor/ck-monitor.component.ts b/frontend/src/app/components/ck-monitor/ck-monitor.component.ts index 22806c68..35dfe9eb 100644 --- a/frontend/src/app/components/ck-monitor/ck-monitor.component.ts +++ b/frontend/src/app/components/ck-monitor/ck-monitor.component.ts @@ -259,7 +259,7 @@ export class CkMonitorComponent implements OnInit, OnDestroy { this.project = await this.projectService.get(this.projectID); //get group may return undefined. try { - this.group = await this.groupService.getByProjectUser( //no longer need to await + this.group = await this.groupService.getByProjectUser( this.projectID, this.user.userID ); @@ -358,7 +358,7 @@ export class CkMonitorComponent implements OnInit, OnDestroy { this.activeTaskWorkflows = activeTaskWorkflows; this.loading = false; } - + async updateTodoItemDataSource(): Promise { const data: TodoItemDisplay[] = []; diff --git a/frontend/src/app/components/ck-workspace/ck-workspace.component.ts b/frontend/src/app/components/ck-workspace/ck-workspace.component.ts index e47cfaf3..92623252 100644 --- a/frontend/src/app/components/ck-workspace/ck-workspace.component.ts +++ b/frontend/src/app/components/ck-workspace/ck-workspace.component.ts @@ -165,7 +165,7 @@ export class CkWorkspaceComponent implements OnInit, OnDestroy { this.project = await this.projectService.get(this.projectID); //get group may return undefined. try { - this.group = await this.groupService.getByProjectUser( //no longer need to await + this.group = await this.groupService.getByProjectUser( this.projectID, this.user.userID ); @@ -253,7 +253,7 @@ export class CkWorkspaceComponent implements OnInit, OnDestroy { this._startListening(); } - + close(): void { this.runningGroupTask = null; this.currentGroupProgress = 0; From 06d4fc823ac62f4f0a8550212533575faa661245 Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Wed, 12 Feb 2025 19:15:22 -0500 Subject: [PATCH 25/29] Hide SCORE authoring from students and fixed CK Bucket, Workspace, and Monitor projectId and boardID bug --- .../project-dashboard.component.html | 2 +- .../score-authoring.component.ts | 7 +-- .../score-view-modal.component.ts | 53 +++---------------- 3 files changed, 13 insertions(+), 49 deletions(-) diff --git a/frontend/src/app/components/project-dashboard/project-dashboard.component.html b/frontend/src/app/components/project-dashboard/project-dashboard.component.html index 804de668..c4d6e2c2 100644 --- a/frontend/src/app/components/project-dashboard/project-dashboard.component.html +++ b/frontend/src/app/components/project-dashboard/project-dashboard.component.html @@ -84,7 +84,7 @@
+
+ + mat-icon-button + (click)="openRoomCasting()" + matTooltip="Open Roomcasting Environment" + > + cast Open Roomcasting Environment + +
diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.scss b/frontend/src/app/components/score-authoring/score-authoring.component.scss index 5c766883..0e5a04d0 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.scss +++ b/frontend/src/app/components/score-authoring/score-authoring.component.scss @@ -70,7 +70,15 @@ } } } - + .open-tools-buttons { // Add a class to wrap the buttons + margin-top: 1rem; // Add margin above the div + + button { + display: block; // Make buttons stack vertically + width: auto; // Make buttons take full width + margin-bottom: 1rem; // Add space between buttons + } + } } .activities-list { diff --git a/frontend/src/app/components/score-authoring/score-authoring.component.ts b/frontend/src/app/components/score-authoring/score-authoring.component.ts index 461d073f..859927d1 100644 --- a/frontend/src/app/components/score-authoring/score-authoring.component.ts +++ b/frontend/src/app/components/score-authoring/score-authoring.component.ts @@ -1093,6 +1093,20 @@ export class ScoreAuthoringComponent implements OnInit, OnDestroy { } } + openTeacherAgentModal() { + this.getSelectedBoard().then((board) => { + if (board) { + this._openDialog(CreateWorkflowModalComponent, { // Reuse the existing modal component + board: board, + project: this.project, + selectedTabIndex: 3, + }); + } else { + console.error('Selected board is undefined'); + } + }); + } + showJoinCode(task: TeacherTask) { const dialogRef = this.dialog.open(ShowJoinCodeComponent, { width: '800px', From 6e05c646bb376b7ec2adefd022b01d04cf4c873a Mon Sep 17 00:00:00 2001 From: JoelWiebe Date: Wed, 12 Feb 2025 19:55:13 -0500 Subject: [PATCH 29/29] Added server URL to join code pop-up --- .../show-join-code.component.html | 3 +- .../show-join-code.component.scss | 35 ++++++++++++------- .../show-join-code.component.ts | 8 ++++- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/components/show-join-code/show-join-code.component.html b/frontend/src/app/components/show-join-code/show-join-code.component.html index e4f59fba..28e7b49f 100644 --- a/frontend/src/app/components/show-join-code/show-join-code.component.html +++ b/frontend/src/app/components/show-join-code/show-join-code.component.html @@ -1,6 +1,7 @@

Student Join Code

-

{{ data.joinCode }}

+

{{ getServerUrl() }}

+
{{ data.joinCode }}
diff --git a/frontend/src/app/components/show-join-code/show-join-code.component.scss b/frontend/src/app/components/show-join-code/show-join-code.component.scss index 6bdcc97b..17e21636 100644 --- a/frontend/src/app/components/show-join-code/show-join-code.component.scss +++ b/frontend/src/app/components/show-join-code/show-join-code.component.scss @@ -1,20 +1,29 @@ +// src/app/components/show-join-code/show-join-code.component.scss + .join-code { - font-size: 6em; - font-weight: bold; - text-align: center; - word-break: break-all; + font-size: 6em; + font-weight: bold; + text-align: center; + word-break: break-all; + margin-top: 1rem; +} + +.server-url { + // Style for the server URL + font-size: 2em; + text-align: center; + margin-bottom: 3rem; // Add space below the URL } .dialog-content { - display: flex; // Use flexbox - flex-direction: column; // Stack items vertically - justify-content: center; // Center vertically - align-items: center; // Center horizontally (for good measure) - min-height: 200px; // Set a minimum height - IMPORTANT! - padding: 24px 24px 0px 24px; // Add padding around the content + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 200px; + padding: 24px 24px 0px 24px; } -// Style the mat-dialog-actions to push button to bottom and add margin .mat-dialog-actions { - margin-bottom: 12px; // Add some space below buttons. -} \ No newline at end of file + margin-bottom: 12px; +} diff --git a/frontend/src/app/components/show-join-code/show-join-code.component.ts b/frontend/src/app/components/show-join-code/show-join-code.component.ts index 8fffee3d..9fc2ac97 100644 --- a/frontend/src/app/components/show-join-code/show-join-code.component.ts +++ b/frontend/src/app/components/show-join-code/show-join-code.component.ts @@ -14,6 +14,12 @@ export class ShowJoinCodeComponent { ) {} close(): void { - this.dialogRef.close(); + this.dialogRef.close(); + } + + getServerUrl(): string { + const currentUrl = window.location.href; + const url = new URL(currentUrl); + return url.origin; } } \ No newline at end of file