diff --git a/src/app/authoring-tool/edit-component-advanced/edit-component-advanced.component.html b/src/app/authoring-tool/edit-component-advanced/edit-component-advanced.component.html index 4aec98bc4cc..7585bbc3179 100644 --- a/src/app/authoring-tool/edit-component-advanced/edit-component-advanced.component.html +++ b/src/app/authoring-tool/edit-component-advanced/edit-component-advanced.component.html @@ -2,6 +2,11 @@

Advanced Settings

+ { ], imports: [ ClassroomMonitorTestingModule, + ComponentTypeServiceModule, MatCardModule, MatDialogModule, MatIconModule, diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts index 6d16cc6899a..993ff48218d 100644 --- a/src/app/services/user.service.ts +++ b/src/app/services/user.service.ts @@ -38,11 +38,27 @@ export class UserService { } isStudent(): boolean { - return this.isAuthenticated && this.getRoles().includes('student'); + return this.isRole('student'); } isTeacher(): boolean { - return this.isAuthenticated && this.getRoles().includes('teacher'); + return this.isRole('teacher'); + } + + isTrustedAuthor(): boolean { + return this.isRole('trustedAuthor'); + } + + isResearcher(): boolean { + return this.isRole('researcher'); + } + + isAdmin(): boolean { + return this.isRole('admin'); + } + + private isRole(role: string): boolean { + return this.isAuthenticated && this.getRoles().includes(role); } getRoles(): string[] { diff --git a/src/app/student-teacher-common-services.module.ts b/src/app/student-teacher-common-services.module.ts index 5f76f54b8a6..49de92ff787 100644 --- a/src/app/student-teacher-common-services.module.ts +++ b/src/app/student-teacher-common-services.module.ts @@ -53,10 +53,12 @@ import { PeerGroupService } from '../assets/wise5/services/peerGroupService'; import { NodeProgressService } from '../assets/wise5/services/nodeProgressService'; import { CompletionService } from '../assets/wise5/services/completionService'; import { StudentNodeService } from '../assets/wise5/services/studentNodeService'; +import { AiChatService } from '../assets/wise5/components/aiChat/aiChatService'; @NgModule({ providers: [ AchievementService, + AiChatService, AnimationService, AnnotationService, AudioOscillatorService, diff --git a/src/app/teacher/component-authoring.module.ts b/src/app/teacher/component-authoring.module.ts index db9cf189ad2..3c59424b37d 100644 --- a/src/app/teacher/component-authoring.module.ts +++ b/src/app/teacher/component-authoring.module.ts @@ -84,9 +84,12 @@ import { ComponentAuthoringComponent } from '../../assets/wise5/authoringTool/co import { WiseTinymceEditorModule } from '../../assets/wise5/directives/wise-tinymce-editor/wise-tinymce-editor.module'; import { WiseLinkAuthoringDialogComponent } from '../../assets/wise5/authoringTool/wise-link-authoring-dialog/wise-link-authoring-dialog.component'; import { EditComponentAdvancedButtonComponent } from '../../assets/wise5/authoringTool/components/edit-component-advanced-button/edit-component-advanced-button.component'; +import { AiChatAuthoringComponent } from '../../assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component'; +import { EditAiChatAdvancedComponent } from '../../assets/wise5/components/aiChat/edit-ai-chat-advanced/edit-ai-chat-advanced.component'; @NgModule({ declarations: [ + AiChatAuthoringComponent, AnimationAuthoring, AudioOscillatorAuthoring, AuthorUrlParametersComponent, @@ -97,6 +100,7 @@ import { EditComponentAdvancedButtonComponent } from '../../assets/wise5/authori DrawAuthoring, DialogGuidanceAuthoringComponent, DiscussionAuthoring, + EditAiChatAdvancedComponent, EditAnimationAdvancedComponent, EditAudioOscillatorAdvancedComponent, EditCommonAdvancedComponent, @@ -176,13 +180,14 @@ import { EditComponentAdvancedButtonComponent } from '../../assets/wise5/authori WiseTinymceEditorModule ], exports: [ - AnimationAuthoring, + AiChatAuthoringComponent, AudioOscillatorAuthoring, ComponentAuthoringComponent, ConceptMapAuthoring, DialogGuidanceAuthoringComponent, DiscussionAuthoring, DrawAuthoring, + EditAiChatAdvancedComponent, EditAnimationAdvancedComponent, EditAudioOscillatorAdvancedComponent, EditCommonAdvancedComponent, diff --git a/src/app/teacher/component-grading.module.ts b/src/app/teacher/component-grading.module.ts index c23cf0e6aca..251b0808543 100644 --- a/src/app/teacher/component-grading.module.ts +++ b/src/app/teacher/component-grading.module.ts @@ -17,10 +17,12 @@ import { ShowGroupWorkGradingModule } from '../../assets/wise5/components/showGr import { TableGradingModule } from '../../assets/wise5/components/table/table-grading/table-grading.module'; import { ComponentGradingComponent } from '../../assets/wise5/classroomMonitor/classroomMonitorComponents/component-grading.component'; import { ShowMyWorkGradingModule } from '../../assets/wise5/components/showMyWork/show-my-work-grading/show-my-work-grading.module'; +import { AiChatGradingModule } from '../../assets/wise5/components/aiChat/ai-chat-grading/ai-chat-grading.module'; @NgModule({ declarations: [ComponentGradingComponent], imports: [ + AiChatGradingModule, AnimationGradingModule, AudioOscillatorGradingModule, ComponentStateInfoModule, @@ -40,6 +42,7 @@ import { ShowMyWorkGradingModule } from '../../assets/wise5/components/showMyWor TableGradingModule ], exports: [ + AiChatGradingModule, AnimationGradingModule, AudioOscillatorGradingModule, ComponentGradingComponent, diff --git a/src/assets/wise5/authoringTool/components/component-info-dialog/component-info-dialog.component.spec.ts b/src/assets/wise5/authoringTool/components/component-info-dialog/component-info-dialog.component.spec.ts index e86998a5ce0..24a4553f3aa 100644 --- a/src/assets/wise5/authoringTool/components/component-info-dialog/component-info-dialog.component.spec.ts +++ b/src/assets/wise5/authoringTool/components/component-info-dialog/component-info-dialog.component.spec.ts @@ -21,6 +21,7 @@ import { OutsideUrlInfo } from '../../../components/outsideURL/OutsideUrlInfo'; import { OpenResponseInfo } from '../../../components/openResponse/OpenResponseInfo'; import { ComponentInfo } from '../../../components/ComponentInfo'; import { MatCardModule } from '@angular/material/card'; +import { ComponentTypeServiceModule } from '../../../services/componentTypeService.module'; let component: ComponentInfoDialogComponent; let fixture: ComponentFixture; @@ -39,6 +40,7 @@ describe('ComponentInfoDialogComponent', () => { ], imports: [ BrowserAnimationsModule, + ComponentTypeServiceModule, HttpClientTestingModule, MatButtonModule, MatCardModule, diff --git a/src/assets/wise5/authoringTool/components/component-type-selector/component-type-selector.component.spec.ts b/src/assets/wise5/authoringTool/components/component-type-selector/component-type-selector.component.spec.ts index 86a1d5519eb..711af346a26 100644 --- a/src/assets/wise5/authoringTool/components/component-type-selector/component-type-selector.component.spec.ts +++ b/src/assets/wise5/authoringTool/components/component-type-selector/component-type-selector.component.spec.ts @@ -8,10 +8,15 @@ import { MatSelectModule } from '@angular/material/select'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ComponentTypeSelectorHarness } from './component-type-selector.harness'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentTypeServiceModule } from '../../../services/componentTypeService.module'; +import { UserService } from '../../../../../app/services/user.service'; +import { ConfigService } from '../../../services/configService'; let component: ComponentTypeSelectorComponent; let componentTypeSelectorHarness: ComponentTypeSelectorHarness; +let configService: ConfigService; let fixture: ComponentFixture; +let userService: UserService; describe('ComponentTypeSelectorComponent', () => { beforeEach(async () => { @@ -19,6 +24,7 @@ describe('ComponentTypeSelectorComponent', () => { declarations: [ComponentTypeSelectorComponent], imports: [ BrowserAnimationsModule, + ComponentTypeServiceModule, HttpClientTestingModule, MatFormFieldModule, MatIconModule, @@ -28,6 +34,11 @@ describe('ComponentTypeSelectorComponent', () => { providers: [] }); fixture = TestBed.createComponent(ComponentTypeSelectorComponent); + configService = TestBed.inject(ConfigService); + spyOn(configService, 'getConfigParam').and.returnValue(true); + userService = TestBed.inject(UserService); + userService.isAuthenticated = true; + spyOn(userService, 'getRoles').and.returnValue(['researcher', 'teacher']); component = fixture.componentInstance; component.componentType = 'OpenResponse'; fixture.detectChanges(); @@ -65,9 +76,9 @@ function selectComponent() { describe('select first component type', () => { it('changes to the first component type and the previous button becomes disabled', async () => { await (await componentTypeSelectorHarness.getComponentTypeSelect()).clickOptions({ - text: 'Animation' + text: 'AI Chat' }); - expect(component.componentType).toEqual('Animation'); + expect(component.componentType).toEqual('AiChat'); expect( await (await componentTypeSelectorHarness.getPreviousComponentTypeButton()).isDisabled() ).toBeTrue(); diff --git a/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.spec.ts b/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.spec.ts index 739825083f2..7ba7d79b61d 100644 --- a/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.spec.ts +++ b/src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.spec.ts @@ -25,6 +25,7 @@ import { TeacherNodeService } from '../../../services/teacherNodeService'; import { EditNodeTitleComponent } from '../edit-node-title/edit-node-title.component'; import { AddComponentButtonComponent } from '../add-component-button/add-component-button.component'; import { CopyComponentButtonComponent } from '../copy-component-button/copy-component-button.component'; +import { ComponentTypeServiceModule } from '../../../services/componentTypeService.module'; let component: NodeAuthoringComponent; let component1: any; @@ -50,6 +51,7 @@ describe('NodeAuthoringComponent', () => { imports: [ BrowserAnimationsModule, ComponentAuthoringModule, + ComponentTypeServiceModule, DragDropModule, FormsModule, HttpClientTestingModule, diff --git a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/node-grading-view/node-grading-view.component.spec.ts b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/node-grading-view/node-grading-view.component.spec.ts index d5b614d0a73..95b184b2c55 100644 --- a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/node-grading-view/node-grading-view.component.spec.ts +++ b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/node-grading-view/node-grading-view.component.spec.ts @@ -19,6 +19,7 @@ import { CommonModule } from '@angular/common'; import { NodeGradingViewComponentTestHelper } from '../../nodeGrading/node-grading-view/node-grading-view.component.test.helper'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { of } from 'rxjs'; +import { ComponentTypeServiceModule } from '../../../../services/componentTypeService.module'; let component: NodeGradingViewComponent; let fixture: ComponentFixture; @@ -36,6 +37,7 @@ describe('NodeGradingViewComponent', () => { BrowserAnimationsModule, ClassroomMonitorTestingModule, CommonModule, + ComponentTypeServiceModule, FlexLayoutModule, FormsModule, MatAutocompleteModule, diff --git a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/workgroup-item/workgroup-item.component.spec.ts b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/workgroup-item/workgroup-item.component.spec.ts index 7c09c949b46..3ae9af6d290 100644 --- a/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/workgroup-item/workgroup-item.component.spec.ts +++ b/src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/workgroup-item/workgroup-item.component.spec.ts @@ -3,6 +3,7 @@ import { ClassroomMonitorTestingModule } from '../../../classroom-monitor-testin import { WorkgroupItemComponent } from './workgroup-item.component'; import { TeacherProjectService } from '../../../../services/teacherProjectService'; import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentTypeServiceModule } from '../../../../services/componentTypeService.module'; let component: WorkgroupItemComponent; let fixture: ComponentFixture; @@ -13,7 +14,7 @@ describe('WorkgroupItemComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [WorkgroupItemComponent], - imports: [ClassroomMonitorTestingModule], + imports: [ClassroomMonitorTestingModule, ComponentTypeServiceModule], providers: [TeacherProjectService], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/assets/wise5/common/ComponentFactory.ts b/src/assets/wise5/common/ComponentFactory.ts index 3cb9e339600..9bba888a6ed 100644 --- a/src/assets/wise5/common/ComponentFactory.ts +++ b/src/assets/wise5/common/ComponentFactory.ts @@ -1,3 +1,4 @@ +import { AiChatComponent } from '../components/aiChat/AiChatComponent'; import { DialogGuidanceComponent } from '../components/dialogGuidance/DialogGuidanceComponent'; import { MultipleChoiceComponent } from '../components/multipleChoice/MultipleChoiceComponent'; import { PeerChatComponent } from '../components/peerChat/PeerChatComponent'; @@ -7,7 +8,9 @@ import { ComponentContent } from './ComponentContent'; export class ComponentFactory { getComponent(content: ComponentContent, nodeId: string): Component { - if (content.type === 'DialogGuidance') { + if (content.type === 'AiChat') { + return new AiChatComponent(content, nodeId); + } else if (content.type === 'DialogGuidance') { return new DialogGuidanceComponent(content, nodeId); } else if (content.type === 'MultipleChoice') { return new MultipleChoiceComponent(content, nodeId); diff --git a/src/assets/wise5/common/apply-mixins.ts b/src/assets/wise5/common/apply-mixins.ts new file mode 100644 index 00000000000..1481794c526 --- /dev/null +++ b/src/assets/wise5/common/apply-mixins.ts @@ -0,0 +1,11 @@ +export function applyMixins(derivedCtor: any, constructors: any[]) { + constructors.forEach((baseCtor) => { + Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { + Object.defineProperty( + derivedCtor.prototype, + name, + Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null) + ); + }); + }); +} diff --git a/src/assets/wise5/common/chat-input/chat-input.component.html b/src/assets/wise5/common/chat-input/chat-input.component.html new file mode 100644 index 00000000000..d284b34aee1 --- /dev/null +++ b/src/assets/wise5/common/chat-input/chat-input.component.html @@ -0,0 +1,21 @@ +
+ + + + +
diff --git a/src/assets/wise5/common/chat-input/chat-input.component.scss b/src/assets/wise5/common/chat-input/chat-input.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/assets/wise5/common/chat-input/chat-input.component.spec.ts b/src/assets/wise5/common/chat-input/chat-input.component.spec.ts new file mode 100644 index 00000000000..0c11e989d4e --- /dev/null +++ b/src/assets/wise5/common/chat-input/chat-input.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ChatInputComponent } from './chat-input.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('ChatInputComponent', () => { + let component: ChatInputComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [BrowserAnimationsModule, ChatInputComponent] + }); + fixture = TestBed.createComponent(ChatInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/assets/wise5/common/chat-input/chat-input.component.ts b/src/assets/wise5/common/chat-input/chat-input.component.ts new file mode 100644 index 00000000000..ada2c7fbc3e --- /dev/null +++ b/src/assets/wise5/common/chat-input/chat-input.component.ts @@ -0,0 +1,33 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; + +@Component({ + selector: 'chat-input', + templateUrl: './chat-input.component.html', + styleUrls: ['./chat-input.component.scss'], + standalone: true, + imports: [FormsModule, FlexLayoutModule, MatButtonModule, MatFormFieldModule, MatInputModule] +}) +export class ChatInputComponent { + protected response: string = ''; + @Input() submitDisabled: boolean = false; + @Output() submitEvent: EventEmitter = new EventEmitter(); + + protected keyPressed(event: KeyboardEvent): void { + if (event.key === 'Enter') { + event.preventDefault(); + if (this.response.length > 0 && !this.submitDisabled) { + this.submit(); + } + } + } + + protected submit(): void { + this.submitEvent.emit(this.response); + this.response = ''; + } +} diff --git a/src/assets/wise5/common/ComputerAvatar.ts b/src/assets/wise5/common/computer-avatar/ComputerAvatar.ts similarity index 100% rename from src/assets/wise5/common/ComputerAvatar.ts rename to src/assets/wise5/common/computer-avatar/ComputerAvatar.ts diff --git a/src/assets/wise5/components/dialogGuidance/ComputerAvatarSettings.ts b/src/assets/wise5/common/computer-avatar/ComputerAvatarSettings.ts similarity index 100% rename from src/assets/wise5/components/dialogGuidance/ComputerAvatarSettings.ts rename to src/assets/wise5/common/computer-avatar/ComputerAvatarSettings.ts diff --git a/src/assets/wise5/common/computer-avatar/computer-avatar-component-content.ts b/src/assets/wise5/common/computer-avatar/computer-avatar-component-content.ts new file mode 100644 index 00000000000..34b237e0f0b --- /dev/null +++ b/src/assets/wise5/common/computer-avatar/computer-avatar-component-content.ts @@ -0,0 +1,6 @@ +import { ComputerAvatarSettings } from './ComputerAvatarSettings'; + +export interface ComputerAvatarComponentContent { + computerAvatarSettings: ComputerAvatarSettings; + isComputerAvatarEnabled: boolean; +} diff --git a/src/assets/wise5/common/computer-avatar/computer-avatar-component.ts b/src/assets/wise5/common/computer-avatar/computer-avatar-component.ts new file mode 100644 index 00000000000..a66efebe1b5 --- /dev/null +++ b/src/assets/wise5/common/computer-avatar/computer-avatar-component.ts @@ -0,0 +1,26 @@ +import { ComputerAvatarComponentContent } from './computer-avatar-component-content'; + +export class ComputerAvatarComponent { + content: ComputerAvatarComponentContent; + + isComputerAvatarEnabled(): boolean { + return this.content.isComputerAvatarEnabled; + } + + isComputerAvatarPromptAvailable(): boolean { + const computerAvatarPrompt = this.content.computerAvatarSettings.prompt; + return computerAvatarPrompt != null && computerAvatarPrompt !== ''; + } + + isOnlyOneComputerAvatarAvailable(): boolean { + return this.content.computerAvatarSettings.ids.length === 1; + } + + isUseGlobalComputerAvatar(): boolean { + return this.content.computerAvatarSettings.useGlobalComputerAvatar; + } + + getComputerAvatarInitialResponse(): string { + return this.content.computerAvatarSettings.initialResponse; + } +} diff --git a/src/assets/wise5/common/computer-avatar/computer-avatar-initializer.ts b/src/assets/wise5/common/computer-avatar/computer-avatar-initializer.ts new file mode 100644 index 00000000000..a54b67e333e --- /dev/null +++ b/src/assets/wise5/common/computer-avatar/computer-avatar-initializer.ts @@ -0,0 +1,94 @@ +import { ComputerAvatar } from './ComputerAvatar'; +import { ComputerAvatarComponent } from './computer-avatar-component'; +import { ComputerAvatarService } from '../../services/computerAvatarService'; +import { StudentStatusService } from '../../services/studentStatusService'; + +export abstract class ComputerAvatarInitializer { + component: ComputerAvatarComponent; + componentState: any; + protected computerAvatar: ComputerAvatar; + protected computerAvatarSelectorVisible: boolean = false; + + constructor( + protected computerAvatarService: ComputerAvatarService, + protected studentStatusService: StudentStatusService + ) {} + + protected initializeComputerAvatar(): void { + if (this.component.isComputerAvatarEnabled()) { + this.tryToRepopulateComputerAvatar(); + if (this.hasStudentPreviouslyChosenComputerAvatar()) { + this.hideComputerAvatarSelector(); + } else if ( + this.component.isOnlyOneComputerAvatarAvailable() && + !this.component.isComputerAvatarPromptAvailable() + ) { + this.selectComputerAvatar(this.getFirstComputerAvatar()); + } else { + this.showComputerAvatarSelector(); + } + } else { + this.computerAvatar = this.computerAvatarService.getDefaultAvatar(); + } + } + + private tryToRepopulateComputerAvatar(): void { + if (this.includesComputerAvatar(this.componentState)) { + this.repopulateComputerAvatarFromComponentState(this.componentState); + } else if ( + this.component.isUseGlobalComputerAvatar() && + this.studentStatusService.isGlobalComputerAvatarAvailable() + ) { + this.repopulateGlobalComputerAvatar(); + } + } + + private includesComputerAvatar(componentState: any): boolean { + return componentState?.studentData?.computerAvatarId != null; + } + + private repopulateComputerAvatarFromComponentState(componentState: any): void { + this.computerAvatar = this.computerAvatarService.getAvatar( + componentState?.studentData?.computerAvatarId + ); + } + + private repopulateGlobalComputerAvatar(): void { + const computerAvatarId = this.studentStatusService.getComputerAvatarId(); + if (computerAvatarId != null) { + this.selectComputerAvatar(this.computerAvatarService.getAvatar(computerAvatarId)); + } + } + + private hasStudentPreviouslyChosenComputerAvatar(): boolean { + return this.computerAvatar != null; + } + + private getFirstComputerAvatar(): ComputerAvatar { + return this.computerAvatarService.getAvatar( + this.component.content.computerAvatarSettings.ids[0] + ); + } + + private showComputerAvatarSelector(): void { + this.computerAvatarSelectorVisible = true; + } + + private hideComputerAvatarSelector(): void { + this.computerAvatarSelectorVisible = false; + } + + protected selectComputerAvatar(computerAvatar: ComputerAvatar): void { + this.computerAvatar = computerAvatar; + if (this.component.isUseGlobalComputerAvatar()) { + this.studentStatusService.setComputerAvatarId(computerAvatar.id); + } + this.hideComputerAvatarSelector(); + const computerAvatarInitialResponse = this.component.getComputerAvatarInitialResponse(); + if (computerAvatarInitialResponse != null && computerAvatarInitialResponse !== '') { + this.showInitialMessage(); + } + } + + abstract showInitialMessage(): void; +} diff --git a/src/assets/wise5/components/Components.ts b/src/assets/wise5/components/Components.ts index 241ba87ebec..43075616b3f 100644 --- a/src/assets/wise5/components/Components.ts +++ b/src/assets/wise5/components/Components.ts @@ -1,3 +1,6 @@ +import { AiChatAuthoringComponent } from './aiChat/ai-chat-authoring/ai-chat-authoring.component'; +import { AiChatGradingComponent } from './aiChat/ai-chat-grading/ai-chat-grading.component'; +import { AiChatStudentComponent } from './aiChat/ai-chat-student/ai-chat-student.component'; import { AnimationAuthoring } from './animation/animation-authoring/animation-authoring.component'; import { AnimationGradingComponent } from './animation/animation-grading/animation-grading.component'; import { AnimationStudent } from './animation/animation-student/animation-student.component'; @@ -54,6 +57,11 @@ import { TableGradingComponent } from './table/table-grading/table-grading.compo import { TableStudent } from './table/table-student/table-student.component'; export const components = { + AiChat: { + authoring: AiChatAuthoringComponent, + grading: AiChatGradingComponent, + student: AiChatStudentComponent + }, Animation: { authoring: AnimationAuthoring, grading: AnimationGradingComponent, diff --git a/src/assets/wise5/components/aiChat/AiChatComponent.ts b/src/assets/wise5/components/aiChat/AiChatComponent.ts new file mode 100644 index 00000000000..612c097f3c4 --- /dev/null +++ b/src/assets/wise5/components/aiChat/AiChatComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '../../common/Component'; +import { ComputerAvatarComponent } from '../../common/computer-avatar/computer-avatar-component'; +import { AiChatContent } from './AiChatContent'; +import { applyMixins } from '../../common/apply-mixins'; + +export class AiChatComponent extends Component implements ComputerAvatarComponent { + content: AiChatContent; + isComputerAvatarEnabled: () => boolean; + isComputerAvatarPromptAvailable: () => boolean; + isOnlyOneComputerAvatarAvailable: () => boolean; + isUseGlobalComputerAvatar: () => boolean; + getComputerAvatarInitialResponse: () => string; +} + +applyMixins(AiChatComponent, [ComputerAvatarComponent]); diff --git a/src/assets/wise5/components/aiChat/AiChatContent.ts b/src/assets/wise5/components/aiChat/AiChatContent.ts new file mode 100644 index 00000000000..73070f6041f --- /dev/null +++ b/src/assets/wise5/components/aiChat/AiChatContent.ts @@ -0,0 +1,7 @@ +import { ComponentContent } from '../../common/ComponentContent'; +import { ComputerAvatarComponentContent } from '../../common/computer-avatar/computer-avatar-component-content'; + +export interface AiChatContent extends ComponentContent, ComputerAvatarComponentContent { + model: 'gpt-3.5-turbo' | 'gpt-4'; + systemPrompt: string; +} diff --git a/src/assets/wise5/components/aiChat/AiChatInfo.ts b/src/assets/wise5/components/aiChat/AiChatInfo.ts new file mode 100644 index 00000000000..aa8942c6b8a --- /dev/null +++ b/src/assets/wise5/components/aiChat/AiChatInfo.ts @@ -0,0 +1,38 @@ +import { ComponentInfo } from '../ComponentInfo'; + +export class AiChatInfo extends ComponentInfo { + protected description: string = $localize`Students chat with an AI bot.`; + protected label: string = $localize`AI Chat`; + protected previewExamples: any[] = [ + { + label: $localize`AI Chat`, + content: { + id: 'abcde12345', + type: 'AiChat', + prompt: + "Let's think about how global warming happens. At the end of the project you will revise your answer to this question. On a cold winter day, Akbar is walking to his car that is parked in the sun. His car has not been driven for one week. How will the temperature inside the car feel? Hint: Akbar wonders if what happens in his car is similar to the greenhouse effect.", + model: 'gpt-4', + systemPrompt: + 'You are a teacher helping a student understand the greenhouse effect by using the example of the inside of a car heating up from the sun on a cold day. You do not tell them the correct answer but you do guide them to the correct answer. Also make sure they explain their reasoning. Limit your response to 100 words or less.', + isComputerAvatarEnabled: true, + computerAvatarSettings: { + ids: [ + 'person1', + 'person2', + 'person3', + 'person4', + 'person5', + 'person6', + 'person7', + 'person8', + 'robot1', + 'robot2' + ], + label: 'Thought Buddy', + prompt: 'Discuss your answer with a thought buddy!', + initialResponse: 'Can you explain what happens to the temperature inside the car?' + } + } + } + ]; +} diff --git a/src/assets/wise5/components/aiChat/AiChatMessage.ts b/src/assets/wise5/components/aiChat/AiChatMessage.ts new file mode 100644 index 00000000000..c1ed8c06cdc --- /dev/null +++ b/src/assets/wise5/components/aiChat/AiChatMessage.ts @@ -0,0 +1,9 @@ +export class AiChatMessage { + content: string; + role: 'assistant' | 'system' | 'user'; + + constructor(role: 'assistant' | 'system' | 'user', content: string) { + this.content = content; + this.role = role; + } +} diff --git a/src/assets/wise5/components/aiChat/AiChatMessageResponse.ts b/src/assets/wise5/components/aiChat/AiChatMessageResponse.ts new file mode 100644 index 00000000000..e598c1b07da --- /dev/null +++ b/src/assets/wise5/components/aiChat/AiChatMessageResponse.ts @@ -0,0 +1,5 @@ +import { AiChatMessageResponseChoice } from './AiChatMessageResponseChoice'; + +export class AiChatMessageReponse { + choices: AiChatMessageResponseChoice[]; +} diff --git a/src/assets/wise5/components/aiChat/AiChatMessageResponseChoice.ts b/src/assets/wise5/components/aiChat/AiChatMessageResponseChoice.ts new file mode 100644 index 00000000000..0e7d716350d --- /dev/null +++ b/src/assets/wise5/components/aiChat/AiChatMessageResponseChoice.ts @@ -0,0 +1,5 @@ +import { AiChatMessage } from './AiChatMessage'; + +export class AiChatMessageResponseChoice { + message: AiChatMessage; +} diff --git a/src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.html b/src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.html new file mode 100644 index 00000000000..75d936b3443 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.html @@ -0,0 +1,62 @@ +
+
+ + Use the system prompt to instruct the chat bot how to behave. Students will not see the system + prompt. + + +
+
+ System Prompt Instructions +

+ Provide context, instructions, and other relevant information to help the chat bot act the way + you want it to. Be as specific as possible. +

+ Example System Prompt +

+ You are a teacher helping a student understand the greenhouse effect by using the example of a + car that has been sitting in the sun on a cold day. The student is asked how the temperature + inside the car will feel. Do not tell them the correct answer, but guide them to better + understand the science by asking questions. Also make sure they explain their reasoning. Limit + your response to 100 words or less. +

+
+ + System Prompt + + +

Use the prompt to introduce the chatbot activity. Students will see the prompt.

+ + Enable Computer Avatar + + +
diff --git a/src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.scss b/src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.scss new file mode 100644 index 00000000000..4735afa35b2 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.scss @@ -0,0 +1,10 @@ +.system-prompt { + width: 100%; +} + +.system-prompt-help { + margin-bottom: 16px; + padding: 12px 16px; + border: 2px solid #dddddd; + border-radius: 6px; +} \ No newline at end of file diff --git a/src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.spec.ts b/src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.spec.ts new file mode 100644 index 00000000000..9f75dd0c09f --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.spec.ts @@ -0,0 +1,57 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AiChatAuthoringComponent } from './ai-chat-authoring.component'; +import { StudentTeacherCommonServicesModule } from '../../../../../app/student-teacher-common-services.module'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TeacherNodeService } from '../../../services/teacherNodeService'; +import { MatDialogModule } from '@angular/material/dialog'; +import { ProjectAssetService } from '../../../../../app/services/projectAssetService'; +import { TeacherProjectService } from '../../../services/teacherProjectService'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { EditComponentPrompt } from '../../../../../app/authoring-tool/edit-component-prompt/edit-component-prompt.component'; +import { FormsModule } from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { EditDialogGuidanceComputerAvatarComponent } from '../../dialogGuidance/edit-dialog-guidance-computer-avatar/edit-dialog-guidance-computer-avatar.component'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; + +describe('AiChatAuthoringComponent', () => { + let component: AiChatAuthoringComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ + AiChatAuthoringComponent, + EditComponentPrompt, + EditDialogGuidanceComputerAvatarComponent + ], + imports: [ + BrowserAnimationsModule, + FormsModule, + HttpClientTestingModule, + MatButtonToggleModule, + MatCheckboxModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + StudentTeacherCommonServicesModule + ], + providers: [ProjectAssetService, TeacherNodeService, TeacherProjectService] + }); + fixture = TestBed.createComponent(AiChatAuthoringComponent); + component = fixture.componentInstance; + component.componentContent = { + id: 'component1', + type: 'aiChat', + computerAvatarSettings: {} + }; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.ts b/src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.ts new file mode 100644 index 00000000000..43d15f6daf8 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { AbstractComponentAuthoring } from '../../../authoringTool/components/AbstractComponentAuthoring'; +import { ProjectAssetService } from '../../../../../app/services/projectAssetService'; +import { ConfigService } from '../../../services/configService'; +import { TeacherNodeService } from '../../../services/teacherNodeService'; +import { TeacherProjectService } from '../../../services/teacherProjectService'; + +@Component({ + templateUrl: './ai-chat-authoring.component.html', + styleUrls: ['./ai-chat-authoring.component.scss'] +}) +export class AiChatAuthoringComponent extends AbstractComponentAuthoring { + protected showSystemPromptHelp = false; + + constructor( + protected configService: ConfigService, + protected nodeService: TeacherNodeService, + protected projectAssetService: ProjectAssetService, + protected projectService: TeacherProjectService + ) { + super(configService, nodeService, projectAssetService, projectService); + } +} diff --git a/src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.html b/src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.html new file mode 100644 index 00000000000..b47d5b95ad4 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.html @@ -0,0 +1,14 @@ +
+ +
+
+
{{ computerAvatar.name }}
+
+
+
+
diff --git a/src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.scss b/src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.scss new file mode 100644 index 00000000000..8c64283fa11 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.scss @@ -0,0 +1,12 @@ +@import 'style/abstracts/variables'; + +.response-text { + padding: 4px 8px; + display: inline-block; + border-radius: $card-border-radius; +} + +.computer-avatar { + width: 36px; + border-radius: 50%; +} \ No newline at end of file diff --git a/src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.spec.ts b/src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.spec.ts new file mode 100644 index 00000000000..2de9b16d6af --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AiChatBotMessageComponent } from './ai-chat-bot-message.component'; +import { MatIconModule } from '@angular/material/icon'; +import { ComputerAvatarService } from '../../../services/computerAvatarService'; + +describe('AiChatBotMessageComponent', () => { + let component: AiChatBotMessageComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [AiChatBotMessageComponent], + imports: [MatIconModule], + providers: [ComputerAvatarService] + }); + fixture = TestBed.createComponent(AiChatBotMessageComponent); + component = fixture.componentInstance; + component.message = { content: 'Hello', role: 'assistant' }; + component.computerAvatar = { + id: 'robot1', + name: 'Robot', + image: 'robot-1.png', + isSelected: true + }; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.ts b/src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.ts new file mode 100644 index 00000000000..02a2c22dc79 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.ts @@ -0,0 +1,23 @@ +import { Component, Input } from '@angular/core'; +import { AiChatMessage } from '../AiChatMessage'; +import { ComputerAvatar } from '../../../common/computer-avatar/ComputerAvatar'; +import { ComputerAvatarService } from '../../../services/computerAvatarService'; + +@Component({ + selector: 'ai-chat-bot-message', + templateUrl: './ai-chat-bot-message.component.html', + styleUrls: ['./ai-chat-bot-message.component.scss'] +}) +export class AiChatBotMessageComponent { + @Input() computerAvatar: ComputerAvatar; + protected computerAvatarImageSrc: string; + @Input() message: AiChatMessage; + + constructor(private computerAvatarService: ComputerAvatarService) {} + + ngOnInit(): void { + this.computerAvatarImageSrc = + this.computerAvatarService.getAvatarsPath() + + this.computerAvatarService.getAvatar(this.computerAvatar.id).image; + } +} diff --git a/src/assets/wise5/components/aiChat/ai-chat-grading/ai-chat-grading.component.html b/src/assets/wise5/components/aiChat/ai-chat-grading/ai-chat-grading.component.html new file mode 100644 index 00000000000..d0645f0b10b --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-grading/ai-chat-grading.component.html @@ -0,0 +1,6 @@ + diff --git a/src/assets/wise5/components/aiChat/ai-chat-grading/ai-chat-grading.component.spec.ts b/src/assets/wise5/components/aiChat/ai-chat-grading/ai-chat-grading.component.spec.ts new file mode 100644 index 00000000000..fc510f43878 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-grading/ai-chat-grading.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AiChatGradingComponent } from './ai-chat-grading.component'; +import { StudentTeacherCommonServicesModule } from '../../../../../app/student-teacher-common-services.module'; +import { MatDialogModule } from '@angular/material/dialog'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { AiChatGradingModule } from './ai-chat-grading.module'; +import { ProjectService } from '../../../services/projectService'; + +describe('AiChatGradingComponent', () => { + let component: AiChatGradingComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [AiChatGradingComponent], + imports: [ + AiChatGradingModule, + HttpClientTestingModule, + MatDialogModule, + StudentTeacherCommonServicesModule + ] + }); + fixture = TestBed.createComponent(AiChatGradingComponent); + component = fixture.componentInstance; + component.componentState = { + studentData: { + messages: [] + } + }; + spyOn(TestBed.inject(ProjectService), 'getComponent').and.returnValue({ + id: 'component1', + type: 'aiChat' + }); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/assets/wise5/components/aiChat/ai-chat-grading/ai-chat-grading.component.ts b/src/assets/wise5/components/aiChat/ai-chat-grading/ai-chat-grading.component.ts new file mode 100644 index 00000000000..e4892156a5d --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-grading/ai-chat-grading.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; +import { AiChatShowWorkComponent } from '../ai-chat-show-work/ai-chat-show-work.component'; + +@Component({ + selector: 'ai-chat-grading', + templateUrl: './ai-chat-grading.component.html' +}) +export class AiChatGradingComponent extends AiChatShowWorkComponent {} diff --git a/src/assets/wise5/components/aiChat/ai-chat-grading/ai-chat-grading.module.ts b/src/assets/wise5/components/aiChat/ai-chat-grading/ai-chat-grading.module.ts new file mode 100644 index 00000000000..b136d00ff42 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-grading/ai-chat-grading.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { AiChatShowWorkModule } from '../ai-chat-show-work/ai-chat-show-work.module'; +import { AiChatGradingComponent } from './ai-chat-grading.component'; + +@NgModule({ + declarations: [AiChatGradingComponent], + imports: [AiChatShowWorkModule], + exports: [AiChatGradingComponent] +}) +export class AiChatGradingModule {} diff --git a/src/assets/wise5/components/aiChat/ai-chat-messages/ai-chat-messages.component.html b/src/assets/wise5/components/aiChat/ai-chat-messages/ai-chat-messages.component.html new file mode 100644 index 00000000000..81e8dacce79 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-messages/ai-chat-messages.component.html @@ -0,0 +1,21 @@ + + + + + + diff --git a/src/assets/wise5/components/aiChat/ai-chat-messages/ai-chat-messages.component.scss b/src/assets/wise5/components/aiChat/ai-chat-messages/ai-chat-messages.component.scss new file mode 100644 index 00000000000..d29c099ac22 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-messages/ai-chat-messages.component.scss @@ -0,0 +1,7 @@ +.ai-chat-message { + display: block; + + &:not(:last-child) { + margin-bottom: 12px; + } +} diff --git a/src/assets/wise5/components/aiChat/ai-chat-messages/ai-chat-messages.component.spec.ts b/src/assets/wise5/components/aiChat/ai-chat-messages/ai-chat-messages.component.spec.ts new file mode 100644 index 00000000000..7ff38f17cbd --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-messages/ai-chat-messages.component.spec.ts @@ -0,0 +1,20 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AiChatMessagesComponent } from './ai-chat-messages.component'; + +describe('AiChatMessagesComponent', () => { + let component: AiChatMessagesComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [AiChatMessagesComponent] + }); + fixture = TestBed.createComponent(AiChatMessagesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/assets/wise5/components/aiChat/ai-chat-messages/ai-chat-messages.component.ts b/src/assets/wise5/components/aiChat/ai-chat-messages/ai-chat-messages.component.ts new file mode 100644 index 00000000000..862ad0a1188 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-messages/ai-chat-messages.component.ts @@ -0,0 +1,15 @@ +import { Component, Input } from '@angular/core'; +import { AiChatMessage } from '../AiChatMessage'; +import { ComputerAvatar } from '../../../common/computer-avatar/ComputerAvatar'; + +@Component({ + selector: 'ai-chat-messages', + templateUrl: './ai-chat-messages.component.html', + styleUrls: ['./ai-chat-messages.component.scss'] +}) +export class AiChatMessagesComponent { + @Input() computerAvatar: ComputerAvatar; + @Input() messages: AiChatMessage[]; + @Input() waitingForComputerResponse: boolean; + @Input() workgroupId: number; +} diff --git a/src/assets/wise5/components/aiChat/ai-chat-show-work/ai-chat-show-work.component.html b/src/assets/wise5/components/aiChat/ai-chat-show-work/ai-chat-show-work.component.html new file mode 100644 index 00000000000..48d5aeee7fe --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-show-work/ai-chat-show-work.component.html @@ -0,0 +1,7 @@ +
+ +
diff --git a/src/assets/wise5/components/aiChat/ai-chat-show-work/ai-chat-show-work.component.spec.ts b/src/assets/wise5/components/aiChat/ai-chat-show-work/ai-chat-show-work.component.spec.ts new file mode 100644 index 00000000000..9b308a17510 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-show-work/ai-chat-show-work.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AiChatShowWorkComponent } from './ai-chat-show-work.component'; +import { StudentTeacherCommonServicesModule } from '../../../../../app/student-teacher-common-services.module'; +import { MatDialogModule } from '@angular/material/dialog'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { AiChatModule } from '../ai-chat.module'; +import { ProjectService } from '../../../services/projectService'; + +describe('AiChatShowWorkComponent', () => { + let component: AiChatShowWorkComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [AiChatShowWorkComponent], + imports: [ + AiChatModule, + HttpClientTestingModule, + MatDialogModule, + StudentTeacherCommonServicesModule + ] + }); + fixture = TestBed.createComponent(AiChatShowWorkComponent); + component = fixture.componentInstance; + component.componentState = { + studentData: {} + }; + component.componentContent = {}; + spyOn(TestBed.inject(ProjectService), 'getComponent').and.returnValue({ + id: 'component1', + type: 'aiChat' + }); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/assets/wise5/components/aiChat/ai-chat-show-work/ai-chat-show-work.component.ts b/src/assets/wise5/components/aiChat/ai-chat-show-work/ai-chat-show-work.component.ts new file mode 100644 index 00000000000..3ec6e7d7e77 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-show-work/ai-chat-show-work.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { ComponentShowWorkDirective } from '../../component-show-work.directive'; +import { ComputerAvatarService } from '../../../services/computerAvatarService'; +import { NodeService } from '../../../services/nodeService'; +import { ProjectService } from '../../../services/projectService'; +import { ComputerAvatar } from '../../../common/computer-avatar/ComputerAvatar'; + +@Component({ + selector: 'ai-chat-show-work', + templateUrl: './ai-chat-show-work.component.html' +}) +export class AiChatShowWorkComponent extends ComponentShowWorkDirective { + protected computerAvatar: ComputerAvatar; + protected messages: any[] = []; + protected workgroupId: number; + + constructor( + private computerAvatarService: ComputerAvatarService, + protected nodeService: NodeService, + protected projectService: ProjectService + ) { + super(nodeService, projectService); + } + + ngOnInit(): void { + super.ngOnInit(); + this.computerAvatar = this.computerAvatarService.getAvatar( + this.componentState.studentData.computerAvatarId + ); + this.messages = this.componentState.studentData.messages; + this.workgroupId = this.componentState.workgroupId; + } +} diff --git a/src/assets/wise5/components/aiChat/ai-chat-show-work/ai-chat-show-work.module.ts b/src/assets/wise5/components/aiChat/ai-chat-show-work/ai-chat-show-work.module.ts new file mode 100644 index 00000000000..e04b979cec9 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-show-work/ai-chat-show-work.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AiChatShowWorkComponent } from './ai-chat-show-work.component'; +import { AiChatModule } from '../ai-chat.module'; + +@NgModule({ + declarations: [AiChatShowWorkComponent], + imports: [AiChatModule, CommonModule], + exports: [AiChatShowWorkComponent] +}) +export class AiChatShowWorkModule {} diff --git a/src/assets/wise5/components/aiChat/ai-chat-student-message/ai-chat-student-message.component.html b/src/assets/wise5/components/aiChat/ai-chat-student-message/ai-chat-student-message.component.html new file mode 100644 index 00000000000..b7541f54c22 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-student-message/ai-chat-student-message.component.html @@ -0,0 +1,16 @@ +
+ account_circle +
+
+
{{ displayNames }}
+
+
+
+
diff --git a/src/assets/wise5/components/aiChat/ai-chat-student-message/ai-chat-student-message.component.scss b/src/assets/wise5/components/aiChat/ai-chat-student-message/ai-chat-student-message.component.scss new file mode 100644 index 00000000000..44d91a0b0a5 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-student-message/ai-chat-student-message.component.scss @@ -0,0 +1,7 @@ +@import 'style/abstracts/variables'; + +.response-text { + padding: 4px 8px; + display: inline-block; + border-radius: $card-border-radius; +} diff --git a/src/assets/wise5/components/aiChat/ai-chat-student-message/ai-chat-student-message.component.spec.ts b/src/assets/wise5/components/aiChat/ai-chat-student-message/ai-chat-student-message.component.spec.ts new file mode 100644 index 00000000000..e1d25b5069a --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-student-message/ai-chat-student-message.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AiChatStudentMessageComponent } from './ai-chat-student-message.component'; +import { ConfigService } from '../../../services/configService'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('AiChatStudentMessageComponent', () => { + let component: AiChatStudentMessageComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [AiChatStudentMessageComponent], + imports: [HttpClientTestingModule], + providers: [ConfigService] + }); + fixture = TestBed.createComponent(AiChatStudentMessageComponent); + component = fixture.componentInstance; + component.message = { content: 'Hello', role: 'user' }; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/assets/wise5/components/aiChat/ai-chat-student-message/ai-chat-student-message.component.ts b/src/assets/wise5/components/aiChat/ai-chat-student-message/ai-chat-student-message.component.ts new file mode 100644 index 00000000000..c9f693279b2 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-student-message/ai-chat-student-message.component.ts @@ -0,0 +1,24 @@ +import { Component, Input } from '@angular/core'; +import { AiChatMessage } from '../AiChatMessage'; +import { ConfigService } from '../../../services/configService'; +import { getAvatarColorForWorkgroupId } from '../../../common/workgroup/workgroup'; + +@Component({ + selector: 'ai-chat-student-message', + templateUrl: './ai-chat-student-message.component.html', + styleUrls: ['./ai-chat-student-message.component.scss'] +}) +export class AiChatStudentMessageComponent { + protected avatarColor: string; + protected displayNames: string; + @Input() message: AiChatMessage; + @Input() workgroupId: number; + + constructor(private configService: ConfigService) {} + + ngOnInit(): void { + const firstNames = this.configService.getStudentFirstNamesByWorkgroupId(this.workgroupId); + this.displayNames = firstNames.join(', '); + this.avatarColor = getAvatarColorForWorkgroupId(this.workgroupId); + } +} diff --git a/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.component.html b/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.component.html new file mode 100644 index 00000000000..9b7d343523a --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.component.html @@ -0,0 +1,19 @@ + + + + + + + diff --git a/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.component.scss b/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.component.scss new file mode 100644 index 00000000000..8f7c48e3577 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.component.scss @@ -0,0 +1,16 @@ +@import 'style/abstracts/variables', 'style/abstracts/functions'; + +.mat-mdc-card { + padding: 12px; + margin-top: 16px; + max-width: breakpoint('sm.min'); +} + +ai-chat-messages { + display: block; + margin-bottom: 12px; +} + +.add-response { + margin-top: 12px; +} diff --git a/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.component.spec.ts b/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.component.spec.ts new file mode 100644 index 00000000000..26a64e9c1ea --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.component.spec.ts @@ -0,0 +1,59 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AiChatStudentComponent } from './ai-chat-student.component'; +import { AiChatService } from '../aiChatService'; +import { StudentTeacherCommonServicesModule } from '../../../../../app/student-teacher-common-services.module'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatCardModule } from '@angular/material/card'; +import { ComponentHeader } from '../../../directives/component-header/component-header.component'; +import { AiChatModule } from '../ai-chat.module'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { PromptComponent } from '../../../directives/prompt/prompt.component'; +import { PossibleScoreComponent } from '../../../../../app/possible-score/possible-score.component'; +import { AiChatComponent } from '../AiChatComponent'; +import { FormsModule } from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { ProjectService } from '../../../services/projectService'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { ChatInputComponent } from '../../../common/chat-input/chat-input.component'; + +describe('AiChatStudentComponent', () => { + let component: AiChatStudentComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ + AiChatStudentComponent, + ComponentHeader, + PossibleScoreComponent, + PromptComponent + ], + imports: [ + AiChatModule, + BrowserAnimationsModule, + ChatInputComponent, + FormsModule, + HttpClientTestingModule, + MatCardModule, + MatDialogModule, + MatFormFieldModule, + MatInputModule, + MatSnackBarModule, + StudentTeacherCommonServicesModule + ], + providers: [AiChatService] + }); + fixture = TestBed.createComponent(AiChatStudentComponent); + component = fixture.componentInstance; + component.component = new AiChatComponent({ id: 'component1', type: 'aiChat' }, 'node1'); + spyOn(component, 'isNotebookEnabled').and.returnValue(false); + spyOn(TestBed.inject(ProjectService), 'getThemeSettings').and.returnValue({}); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.component.ts b/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.component.ts new file mode 100644 index 00000000000..c97d186f272 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.component.ts @@ -0,0 +1,124 @@ +import { Component } from '@angular/core'; +import { ComponentStudent } from '../../component-student.component'; +import { ConfigService } from '../../../services/configService'; +import { AnnotationService } from '../../../services/annotationService'; +import { ComponentService } from '../../componentService'; +import { MatDialog } from '@angular/material/dialog'; +import { NodeService } from '../../../services/nodeService'; +import { NotebookService } from '../../../services/notebookService'; +import { StudentAssetService } from '../../../services/studentAssetService'; +import { StudentDataService } from '../../../services/studentDataService'; +import { AiChatMessage } from '../AiChatMessage'; +import { AiChatService } from '../aiChatService'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { AiChatComponent } from '../AiChatComponent'; +import { ComputerAvatar } from '../../../common/computer-avatar/ComputerAvatar'; +import { applyMixins } from '../../../common/apply-mixins'; +import { ComputerAvatarInitializer } from '../../../common/computer-avatar/computer-avatar-initializer'; +import { ComputerAvatarService } from '../../../services/computerAvatarService'; +import { StudentStatusService } from '../../../services/studentStatusService'; + +@Component({ + selector: 'ai-chat-student', + templateUrl: './ai-chat-student.component.html', + styleUrls: ['./ai-chat-student.component.scss'] +}) +export class AiChatStudentComponent extends ComponentStudent { + component: AiChatComponent; + protected computerAvatar: ComputerAvatar; + protected computerAvatarSelectorVisible: boolean = false; + protected messages: AiChatMessage[] = []; + protected studentResponse: string = ''; + protected submitEnabled: boolean = false; + protected waitingForComputerResponse: boolean = false; + + constructor( + private aiChatService: AiChatService, + protected annotationService: AnnotationService, + protected computerAvatarService: ComputerAvatarService, + protected componentService: ComponentService, + protected configService: ConfigService, + protected dataService: StudentDataService, + protected dialog: MatDialog, + protected nodeService: NodeService, + protected notebookService: NotebookService, + private snackBar: MatSnackBar, + protected studentAssetService: StudentAssetService, + protected studentStatusService: StudentStatusService + ) { + super( + annotationService, + componentService, + configService, + dialog, + nodeService, + notebookService, + studentAssetService, + dataService + ); + } + + ngOnInit(): void { + super.ngOnInit(); + this.initializeComputerAvatar(); + if (this.componentState != null) { + this.messages = this.componentState.studentData.messages; + } + this.initializeMessages(); + } + + showInitialMessage(): void { + this.messages.push( + new AiChatMessage('assistant', this.component.getComputerAvatarInitialResponse()) + ); + } + + private initializeMessages(): void { + if (this.messages.length === 0) { + this.messages.push(new AiChatMessage('system', this.componentContent.systemPrompt)); + } + } + + protected async submitStudentResponse(response: string): Promise { + this.waitingForComputerResponse = true; + this.messages.push(new AiChatMessage('user', response)); + try { + const response = await this.aiChatService.sendChatMessage( + this.messages, + this.componentContent.model + ); + this.waitingForComputerResponse = false; + this.messages.push(new AiChatMessage('assistant', response.choices[0].message.content)); + this.emitComponentSubmitTriggered(); + } catch (error) { + this.waitingForComputerResponse = false; + this.snackBar.open($localize`An error occurred.`); + } + } + + createComponentState(action: any): any { + const componentState: any = this.createNewComponentState(); + componentState.studentData = { + messages: this.messages, + model: this.componentContent.model + }; + if (this.computerAvatar != null) { + componentState.studentData.computerAvatarId = this.computerAvatar.id; + } + componentState.componentType = 'AiChat'; + componentState.nodeId = this.nodeId; + componentState.componentId = this.component.id; + const promise = new Promise((resolve, reject) => { + return this.createComponentStateAdditionalProcessing( + { resolve: resolve, reject: reject }, + componentState, + action + ); + }); + return promise; + } + + initializeComputerAvatar: () => void; +} + +applyMixins(AiChatStudentComponent, [ComputerAvatarInitializer]); diff --git a/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.module.ts b/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.module.ts new file mode 100644 index 00000000000..39c8e2b3098 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { AiChatStudentComponent } from './ai-chat-student.component'; +import { StudentComponentModule } from '../../../../../app/student/student.component.module'; +import { StudentTeacherCommonModule } from '../../../../../app/student-teacher-common.module'; +import { AiChatModule } from '../ai-chat.module'; +import { ComputerAvatarSelectorModule } from '../../../vle/computer-avatar-selector/computer-avatar-selector.module'; +import { ChatInputComponent } from '../../../common/chat-input/chat-input.component'; + +@NgModule({ + declarations: [AiChatStudentComponent], + imports: [ + AiChatModule, + ChatInputComponent, + ComputerAvatarSelectorModule, + StudentComponentModule, + StudentTeacherCommonModule + ], + exports: [AiChatStudentComponent] +}) +export class AiChatStudentModule {} diff --git a/src/assets/wise5/components/aiChat/ai-chat.module.ts b/src/assets/wise5/components/aiChat/ai-chat.module.ts new file mode 100644 index 00000000000..f78ad211248 --- /dev/null +++ b/src/assets/wise5/components/aiChat/ai-chat.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { AiChatMessagesComponent } from './ai-chat-messages/ai-chat-messages.component'; +import { StudentTeacherCommonModule } from '../../../../app/student-teacher-common.module'; +import { AiChatBotMessageComponent } from './ai-chat-bot-message/ai-chat-bot-message.component'; +import { AiChatStudentMessageComponent } from './ai-chat-student-message/ai-chat-student-message.component'; + +@NgModule({ + declarations: [AiChatBotMessageComponent, AiChatStudentMessageComponent, AiChatMessagesComponent], + imports: [StudentTeacherCommonModule], + exports: [AiChatBotMessageComponent, AiChatStudentMessageComponent, AiChatMessagesComponent] +}) +export class AiChatModule {} diff --git a/src/assets/wise5/components/aiChat/aiChatService.ts b/src/assets/wise5/components/aiChat/aiChatService.ts new file mode 100644 index 00000000000..6c36db0cc3c --- /dev/null +++ b/src/assets/wise5/components/aiChat/aiChatService.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { ComponentService } from '../componentService'; +import { AiChatMessage } from './AiChatMessage'; +import { AiChatMessageReponse } from './AiChatMessageResponse'; +import { ComputerAvatarService } from '../../services/computerAvatarService'; + +@Injectable() +export class AiChatService extends ComponentService { + constructor(protected computerAvatarService: ComputerAvatarService) { + super(); + } + + getComponentTypeLabel() { + return $localize`AI Chat`; + } + + createComponent(): any { + const component: any = super.createComponent(); + component.type = 'AiChat'; + component.computerAvatarSettings = this.computerAvatarService.getDefaultComputerAvatarSettings(); + component.isComputerAvatarEnabled = false; + component.model = 'gpt-4'; + component.systemPrompt = ''; + return component; + } + + async sendChatMessage(messages: AiChatMessage[], model: string): Promise { + const response = await fetch('/api/chat-gpt', { + method: 'POST', + body: JSON.stringify({ + messages: messages, + model: model + }) + }); + return response.json(); + } +} diff --git a/src/assets/wise5/components/aiChat/edit-ai-chat-advanced/edit-ai-chat-advanced.component.html b/src/assets/wise5/components/aiChat/edit-ai-chat-advanced/edit-ai-chat-advanced.component.html new file mode 100644 index 00000000000..51248c42cca --- /dev/null +++ b/src/assets/wise5/components/aiChat/edit-ai-chat-advanced/edit-ai-chat-advanced.component.html @@ -0,0 +1,7 @@ + + Model + + {{ model }} + + + diff --git a/src/assets/wise5/components/aiChat/edit-ai-chat-advanced/edit-ai-chat-advanced.component.spec.ts b/src/assets/wise5/components/aiChat/edit-ai-chat-advanced/edit-ai-chat-advanced.component.spec.ts new file mode 100644 index 00000000000..69cc4914618 --- /dev/null +++ b/src/assets/wise5/components/aiChat/edit-ai-chat-advanced/edit-ai-chat-advanced.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { EditAiChatAdvancedComponent } from './edit-ai-chat-advanced.component'; +import { TeacherNodeService } from '../../../services/teacherNodeService'; +import { MatDialogModule } from '@angular/material/dialog'; +import { StudentTeacherCommonServicesModule } from '../../../../../app/student-teacher-common-services.module'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TeacherProjectService } from '../../../services/teacherProjectService'; +import { ComponentAuthoringModule } from '../../../../../app/teacher/component-authoring.module'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('EditAiChatAdvancedComponent', () => { + let component: EditAiChatAdvancedComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [EditAiChatAdvancedComponent], + imports: [ + BrowserAnimationsModule, + ComponentAuthoringModule, + HttpClientTestingModule, + MatDialogModule, + StudentTeacherCommonServicesModule + ], + providers: [TeacherNodeService, TeacherProjectService] + }); + fixture = TestBed.createComponent(EditAiChatAdvancedComponent); + component = fixture.componentInstance; + component.nodeId = 'node1'; + spyOn(TestBed.inject(TeacherProjectService), 'getComponent').and.returnValue({ + id: 'component1', + type: 'aiChat' + }); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/assets/wise5/components/aiChat/edit-ai-chat-advanced/edit-ai-chat-advanced.component.ts b/src/assets/wise5/components/aiChat/edit-ai-chat-advanced/edit-ai-chat-advanced.component.ts new file mode 100644 index 00000000000..1c196943771 --- /dev/null +++ b/src/assets/wise5/components/aiChat/edit-ai-chat-advanced/edit-ai-chat-advanced.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { EditAdvancedComponentComponent } from '../../../../../app/authoring-tool/edit-advanced-component/edit-advanced-component.component'; +import { AiChatContent } from '../AiChatContent'; + +@Component({ + selector: 'edit-ai-chat-advanced', + templateUrl: './edit-ai-chat-advanced.component.html' +}) +export class EditAiChatAdvancedComponent extends EditAdvancedComponentComponent { + componentContent: AiChatContent; + protected models: string[] = ['gpt-3.5-turbo', 'gpt-4']; +} diff --git a/src/assets/wise5/components/component/component-student.module.ts b/src/assets/wise5/components/component/component-student.module.ts index dafefb6a278..28e1701da57 100644 --- a/src/assets/wise5/components/component/component-student.module.ts +++ b/src/assets/wise5/components/component/component-student.module.ts @@ -25,10 +25,12 @@ import { ShowMyWorkStudentModule } from '../showMyWork/show-my-work-student/show import { SummaryStudentModule } from '../summary/summary-student/summary-student.module'; import { TableStudentModule } from '../table/table-student/table-student.module'; import { ComponentComponent } from './component.component'; +import { AiChatStudentModule } from '../aiChat/ai-chat-student/ai-chat-student.module'; @NgModule({ declarations: [ComponentComponent, PreviewComponentComponent], imports: [ + AiChatStudentModule, AnimationStudentModule, AudioOscillatorStudentModule, CommonModule, diff --git a/src/assets/wise5/components/dialogGuidance/DialogGuidanceComponent.ts b/src/assets/wise5/components/dialogGuidance/DialogGuidanceComponent.ts index f956bbe6ac8..5621c51a6fc 100644 --- a/src/assets/wise5/components/dialogGuidance/DialogGuidanceComponent.ts +++ b/src/assets/wise5/components/dialogGuidance/DialogGuidanceComponent.ts @@ -1,43 +1,24 @@ import { Component } from '../../common/Component'; +import { ComputerAvatarComponent } from '../../common/computer-avatar/computer-avatar-component'; +import { applyMixins } from '../../common/apply-mixins'; import { FeedbackRule } from '../common/feedbackRule/FeedbackRule'; import { DialogGuidanceContent } from './DialogGuidanceContent'; -export class DialogGuidanceComponent extends Component { +export class DialogGuidanceComponent extends Component implements ComputerAvatarComponent { content: DialogGuidanceContent; getFeedbackRules(): FeedbackRule[] { return this.content.feedbackRules; } - getComputerAvatarInitialResponse(): string { - return this.content.computerAvatarSettings.initialResponse; - } - - isComputerAvatarEnabled(): boolean { - return this.content.isComputerAvatarEnabled; - } - getItemId(): string { return this.content.itemId; } - isComputerAvatarPromptAvailable(): boolean { - const computerAvatarPrompt = this.content.computerAvatarSettings.prompt; - return computerAvatarPrompt != null && computerAvatarPrompt !== ''; - } - isMultipleFeedbackTextsForSameRuleAllowed(): boolean { return !this.isVersion1(); } - isOnlyOneComputerAvatarAvailable(): boolean { - return this.content.computerAvatarSettings.ids.length === 1; - } - - isUseGlobalComputerAvatar(): boolean { - return this.content.computerAvatarSettings.useGlobalComputerAvatar; - } - isVersion1(): boolean { return this.content.version == null; } @@ -45,4 +26,12 @@ export class DialogGuidanceComponent extends Component { isVersion2(): boolean { return this.content.version === 2; } + + isComputerAvatarEnabled: () => boolean; + isComputerAvatarPromptAvailable: () => boolean; + isOnlyOneComputerAvatarAvailable: () => boolean; + isUseGlobalComputerAvatar: () => boolean; + getComputerAvatarInitialResponse: () => string; } + +applyMixins(DialogGuidanceComponent, [ComputerAvatarComponent]); diff --git a/src/assets/wise5/components/dialogGuidance/DialogGuidanceContent.ts b/src/assets/wise5/components/dialogGuidance/DialogGuidanceContent.ts index cf33f5baa54..1659f2f57b9 100644 --- a/src/assets/wise5/components/dialogGuidance/DialogGuidanceContent.ts +++ b/src/assets/wise5/components/dialogGuidance/DialogGuidanceContent.ts @@ -1,11 +1,9 @@ import { ComponentContent } from '../../common/ComponentContent'; +import { ComputerAvatarComponentContent } from '../../common/computer-avatar/computer-avatar-component-content'; import { FeedbackRule } from '../common/feedbackRule/FeedbackRule'; -import { ComputerAvatarSettings } from './ComputerAvatarSettings'; -export interface DialogGuidanceContent extends ComponentContent { - computerAvatarSettings?: ComputerAvatarSettings; +export interface DialogGuidanceContent extends ComponentContent, ComputerAvatarComponentContent { feedbackRules: FeedbackRule[]; - isComputerAvatarEnabled?: boolean; itemId: string; version?: number; } diff --git a/src/assets/wise5/components/dialogGuidance/dialog-guidance-authoring/dialog-guidance-authoring.component.ts b/src/assets/wise5/components/dialogGuidance/dialog-guidance-authoring/dialog-guidance-authoring.component.ts index d3ba57b1e68..2bc15b37557 100644 --- a/src/assets/wise5/components/dialogGuidance/dialog-guidance-authoring/dialog-guidance-authoring.component.ts +++ b/src/assets/wise5/components/dialogGuidance/dialog-guidance-authoring/dialog-guidance-authoring.component.ts @@ -3,8 +3,8 @@ import { AbstractComponentAuthoring } from '../../../authoringTool/components/Ab import { ConfigService } from '../../../services/configService'; import { ProjectAssetService } from '../../../../../app/services/projectAssetService'; import { TeacherProjectService } from '../../../services/teacherProjectService'; -import { DialogGuidanceService } from '../dialogGuidanceService'; import { TeacherNodeService } from '../../../services/teacherNodeService'; +import { ComputerAvatarService } from '../../../services/computerAvatarService'; @Component({ selector: 'dialog-guidance-authoring', @@ -13,8 +13,8 @@ import { TeacherNodeService } from '../../../services/teacherNodeService'; }) export class DialogGuidanceAuthoringComponent extends AbstractComponentAuthoring { constructor( + private computerAvatarService: ComputerAvatarService, protected configService: ConfigService, - private dialogGuidanceService: DialogGuidanceService, protected nodeService: TeacherNodeService, protected projectAssetService: ProjectAssetService, protected projectService: TeacherProjectService @@ -25,7 +25,7 @@ export class DialogGuidanceAuthoringComponent extends AbstractComponentAuthoring ngOnInit() { super.ngOnInit(); if (this.componentContent.computerAvatarSettings == null) { - this.componentContent.computerAvatarSettings = this.dialogGuidanceService.getDefaultComputerAvatarSettings(); + this.componentContent.computerAvatarSettings = this.computerAvatarService.getDefaultComputerAvatarSettings(); } } } diff --git a/src/assets/wise5/components/dialogGuidance/dialog-guidance-show-work/dialog-guidance-show-work.component.ts b/src/assets/wise5/components/dialogGuidance/dialog-guidance-show-work/dialog-guidance-show-work.component.ts index 271582fa6b0..97858bde2f8 100644 --- a/src/assets/wise5/components/dialogGuidance/dialog-guidance-show-work/dialog-guidance-show-work.component.ts +++ b/src/assets/wise5/components/dialogGuidance/dialog-guidance-show-work/dialog-guidance-show-work.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { ComputerAvatar } from '../../../common/ComputerAvatar'; +import { ComputerAvatar } from '../../../common/computer-avatar/ComputerAvatar'; import { ComputerAvatarService } from '../../../services/computerAvatarService'; import { NodeService } from '../../../services/nodeService'; import { ProjectService } from '../../../services/projectService'; diff --git a/src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.html b/src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.html index 2aa3e92bdb0..68b78a0f695 100644 --- a/src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.html +++ b/src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.html @@ -1,10 +1,10 @@ - + -
- - - - -
+ [submitDisabled]="isWaitingForComputerResponse" + (submitEvent)="submitStudentResponse($event)" + >
diff --git a/src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.spec.ts b/src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.spec.ts index 5de52cc5d49..058ef27a986 100644 --- a/src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.spec.ts +++ b/src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.spec.ts @@ -9,7 +9,7 @@ import { MatInputModule } from '@angular/material/input'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { PossibleScoreComponent } from '../../../../../app/possible-score/possible-score.component'; import { StudentTeacherCommonServicesModule } from '../../../../../app/student-teacher-common-services.module'; -import { ComputerAvatar } from '../../../common/ComputerAvatar'; +import { ComputerAvatar } from '../../../common/computer-avatar/ComputerAvatar'; import { ComponentHeader } from '../../../directives/component-header/component-header.component'; import { ComputerAvatarService } from '../../../services/computerAvatarService'; import { DialogGuidanceFeedbackService } from '../../../services/dialogGuidanceFeedbackService'; @@ -26,11 +26,27 @@ import { DialogGuidanceStudentComponent } from './dialog-guidance-student.compon import { DialogGuidanceComponent } from '../DialogGuidanceComponent'; import { RawCRaterResponse } from '../../common/cRater/RawCRaterResponse'; import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChatInputComponent } from '../../../common/chat-input/chat-input.component'; let component: DialogGuidanceStudentComponent; let fixture: ComponentFixture; const robotAvatar = new ComputerAvatar('robot', 'Robot', 'robot.png'); +function initializeComponent(isComputerAvatarEnabled: boolean): void { + fixture = TestBed.createComponent(DialogGuidanceStudentComponent); + component = fixture.componentInstance; + component.component = createDialogGuidanceComponent(isComputerAvatarEnabled); + spyOn(component, 'subscribeToSubscriptions').and.callFake(() => {}); + spyOn(component, 'isNotebookEnabled').and.returnValue(false); + fixture.detectChanges(); +} + +function createDialogGuidanceComponent(isComputerAvatarEnabled: boolean): DialogGuidanceComponent { + const componentContent = TestBed.inject(DialogGuidanceService).createComponent(); + componentContent.isComputerAvatarEnabled = isComputerAvatarEnabled; + return new DialogGuidanceComponent(componentContent, null); +} + describe('DialogGuidanceStudentComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ @@ -42,6 +58,7 @@ describe('DialogGuidanceStudentComponent', () => { ], imports: [ BrowserAnimationsModule, + ChatInputComponent, FormsModule, HttpClientTestingModule, MatCardModule, @@ -57,20 +74,8 @@ describe('DialogGuidanceStudentComponent', () => { }); beforeEach(() => { - fixture = TestBed.createComponent(DialogGuidanceStudentComponent); spyOn(TestBed.inject(ProjectService), 'getThemeSettings').and.returnValue({}); - component = fixture.componentInstance; - const componentContent = TestBed.inject(DialogGuidanceService).createComponent(); - componentContent.feedbackRules = [ - { - expression: 'isDefault', - feedback: 'Default Feedback' - } - ]; - component.component = new DialogGuidanceComponent(componentContent, null); - spyOn(component, 'subscribeToSubscriptions').and.callFake(() => {}); - spyOn(component, 'isNotebookEnabled').and.returnValue(false); - fixture.detectChanges(); + initializeComponent(true); }); it('should create computer dialog response with single score', () => { @@ -122,8 +127,7 @@ describe('DialogGuidanceStudentComponent', () => { }); it('should initialize computer avatar to default computer avatar', () => { - clearComputerAvatar(component); - component.ngOnInit(); + initializeComponent(false); expectComputerAvatarSelectorNotToBeShown(component); expect(component.computerAvatar).not.toBeNull(); }); @@ -220,18 +224,18 @@ function initializeComponentStateWithNoComputerAvatarId(component: any) { } function expectComputerAvatarSelectorToBeShown(component: any) { - expectIsShowComputerAvatarSelector(component, true); + expectComputerAvatarSelectorVisible(component, true); } function expectComputerAvatarSelectorNotToBeShown(component: any) { - expectIsShowComputerAvatarSelector(component, false); + expectComputerAvatarSelectorVisible(component, false); } -function expectIsShowComputerAvatarSelector( +function expectComputerAvatarSelectorVisible( component: any, expectedIsShowComputerAvatarSelector: boolean ) { - expect(component.isShowComputerAvatarSelector).toEqual(expectedIsShowComputerAvatarSelector); + expect(component.computerAvatarSelectorVisible).toEqual(expectedIsShowComputerAvatarSelector); } function createDummyScoringResponse(): RawCRaterResponse { diff --git a/src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.ts b/src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.ts index 7ba31c49ca2..d44e370fc1f 100644 --- a/src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.ts +++ b/src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.ts @@ -17,7 +17,7 @@ import { FeedbackRuleEvaluator } from '../../common/feedbackRule/FeedbackRuleEva import { ComputerDialogResponseMultipleScores } from '../ComputerDialogResponseMultipleScores'; import { ComputerDialogResponseSingleScore } from '../ComputerDialogResponseSingleScore'; import { MatDialog } from '@angular/material/dialog'; -import { ComputerAvatar } from '../../../common/ComputerAvatar'; +import { ComputerAvatar } from '../../../common/computer-avatar/ComputerAvatar'; import { ComputerAvatarService } from '../../../services/computerAvatarService'; import { StudentStatusService } from '../../../services/studentStatusService'; import { DialogGuidanceFeedbackService } from '../../../services/dialogGuidanceFeedbackService'; @@ -27,6 +27,8 @@ import { DialogGuidanceComponent } from '../DialogGuidanceComponent'; import { copy } from '../../../common/object/object'; import { RawCRaterResponse } from '../../common/cRater/RawCRaterResponse'; import { ConstraintService } from '../../../services/constraintService'; +import { applyMixins } from '../../../common/apply-mixins'; +import { ComputerAvatarInitializer } from '../../../common/computer-avatar/computer-avatar-initializer'; @Component({ selector: 'dialog-guidance-student', @@ -36,14 +38,12 @@ import { ConstraintService } from '../../../services/constraintService'; export class DialogGuidanceStudentComponent extends ComponentStudent { component: DialogGuidanceComponent; computerAvatar: ComputerAvatar; + protected computerAvatarSelectorVisible: boolean = false; cRaterTimeout: number = 40000; feedbackRuleEvaluator: FeedbackRuleEvaluator; - isShowComputerAvatarSelector: boolean = false; - isSubmitEnabled: boolean = false; isWaitingForComputerResponse: boolean = false; responses: DialogResponse[] = []; studentCanRespond: boolean = true; - studentResponse: string; workgroupId: number; constructor( @@ -92,106 +92,24 @@ export class DialogGuidanceStudentComponent extends ComponentStudent { this.configService, this.constraintService ); - if (this.component.isComputerAvatarEnabled()) { - this.initializeComputerAvatar(); - } else { - this.computerAvatar = this.computerAvatarService.getDefaultAvatar(); - } - } - - initializeComputerAvatar(): void { - this.tryToRepopulateComputerAvatar(); - if (this.hasStudentPreviouslyChosenComputerAvatar()) { - this.hideComputerAvatarSelector(); - } else if ( - this.component.isOnlyOneComputerAvatarAvailable() && - !this.component.isComputerAvatarPromptAvailable() - ) { - this.hideComputerAvatarSelector(); - this.selectComputerAvatar(this.getTheOnlyComputerAvatarAvailable()); - } else { - this.showComputerAvatarSelector(); - } - } - - private tryToRepopulateComputerAvatar(): void { - if (this.includesComputerAvatar(this.componentState)) { - this.repopulateComputerAvatarFromComponentState(this.componentState); - } else if ( - this.component.isUseGlobalComputerAvatar() && - this.isGlobalComputerAvatarAvailable() - ) { - this.repopulateGlobalComputerAvatar(); - } - } - - private includesComputerAvatar(componentState: any): boolean { - return componentState?.studentData?.computerAvatarId != null; - } - - private isGlobalComputerAvatarAvailable(): boolean { - return this.studentStatusService.getComputerAvatarId() != null; - } - - private repopulateComputerAvatarFromComponentState(componentState: any): void { - this.computerAvatar = this.computerAvatarService.getAvatar( - componentState?.studentData?.computerAvatarId - ); - } - - private repopulateGlobalComputerAvatar(): void { - const computerAvatarId = this.studentStatusService.getComputerAvatarId(); - if (computerAvatarId != null) { - this.selectComputerAvatar(this.computerAvatarService.getAvatar(computerAvatarId)); - } - } - - private hasStudentPreviouslyChosenComputerAvatar(): boolean { - return this.computerAvatar != null; + this.initializeComputerAvatar(); } - private getTheOnlyComputerAvatarAvailable(): ComputerAvatar { - return this.computerAvatarService.getAvatar( - this.component.content.computerAvatarSettings.ids[0] + showInitialMessage(): void { + this.addDialogResponse( + new ComputerDialogResponse( + this.component.getComputerAvatarInitialResponse(), + [], + new Date().getTime(), + true + ) ); } - private showComputerAvatarSelector(): void { - this.isShowComputerAvatarSelector = true; - } - - private hideComputerAvatarSelector(): void { - this.isShowComputerAvatarSelector = false; - } - - selectComputerAvatar(computerAvatar: ComputerAvatar): void { - this.computerAvatar = computerAvatar; - if (this.component.isUseGlobalComputerAvatar()) { - this.studentStatusService.setComputerAvatarId(computerAvatar.id); - } - this.hideComputerAvatarSelector(); - const computerAvatarInitialResponse = this.component.getComputerAvatarInitialResponse(); - if (computerAvatarInitialResponse != null && computerAvatarInitialResponse !== '') { - this.addDialogResponse( - new ComputerDialogResponse(computerAvatarInitialResponse, [], new Date().getTime(), true) - ); - } - } - - submitStudentResponse(): void { - this.disableInput(); - const response = this.studentResponse; + protected submitStudentResponse(response: string): void { this.addStudentDialogResponse(response); - this.clearStudentResponse(); - setTimeout(() => { - this.submitToCRater(response); - this.studentDataChanged(); - }, 500); - } - - private clearStudentResponse(): void { - this.studentResponse = ''; - this.studentResponseChanged(); + this.submitToCRater(response); + this.studentDataChanged(); } private addStudentDialogResponse(text: string): void { @@ -225,14 +143,6 @@ export class DialogGuidanceStudentComponent extends ComponentStudent { this.isWaitingForComputerResponse = false; } - private disableInput(): void { - this.isDisabled = true; - } - - private enableInput(): void { - this.isDisabled = false; - } - private disableStudentResponse(): void { this.studentCanRespond = false; } @@ -244,8 +154,6 @@ export class DialogGuidanceStudentComponent extends ComponentStudent { this.addDialogResponse(this.createComputerDialogResponse(cRaterResponse)); if (this.hasMaxSubmitCountAndUsedAllSubmits()) { this.disableStudentResponse(); - } else { - this.enableInput(); } } @@ -292,7 +200,6 @@ export class DialogGuidanceStudentComponent extends ComponentStudent { cRaterErrorResponse() { this.hideWaitingForComputerResponse(); - this.enableInput(); this.saveButtonClicked(); } @@ -319,8 +226,8 @@ export class DialogGuidanceStudentComponent extends ComponentStudent { return promise; } - studentResponseChanged(): void { - this.isSubmitEnabled = this.studentResponse.length > 0; - this.setIsSubmitDirty(this.isSubmitDirty || this.isSubmitEnabled); - } + initializeComputerAvatar: () => void; + selectComputerAvatar: (computerAvatar: ComputerAvatar) => void; } + +applyMixins(DialogGuidanceStudentComponent, [ComputerAvatarInitializer]); diff --git a/src/assets/wise5/components/dialogGuidance/dialog-response/dialog-response.component.ts b/src/assets/wise5/components/dialogGuidance/dialog-response/dialog-response.component.ts index 1d9fa70c54f..af309977649 100644 --- a/src/assets/wise5/components/dialogGuidance/dialog-response/dialog-response.component.ts +++ b/src/assets/wise5/components/dialogGuidance/dialog-response/dialog-response.component.ts @@ -1,7 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { SafeHtml } from '@angular/platform-browser'; import { WiseLinkService } from '../../../../../app/services/wiseLinkService'; -import { ComputerAvatar } from '../../../common/ComputerAvatar'; +import { ComputerAvatar } from '../../../common/computer-avatar/ComputerAvatar'; import { ComputerAvatarService } from '../../../services/computerAvatarService'; import { ConfigService } from '../../../services/configService'; import { DialogResponse } from '../DialogResponse'; diff --git a/src/assets/wise5/components/dialogGuidance/dialog-responses/dialog-responses.component.ts b/src/assets/wise5/components/dialogGuidance/dialog-responses/dialog-responses.component.ts index 51f876c97ae..6c1290c8c5f 100644 --- a/src/assets/wise5/components/dialogGuidance/dialog-responses/dialog-responses.component.ts +++ b/src/assets/wise5/components/dialogGuidance/dialog-responses/dialog-responses.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnInit } from '@angular/core'; -import { ComputerAvatar } from '../../../common/ComputerAvatar'; +import { ComputerAvatar } from '../../../common/computer-avatar/ComputerAvatar'; import { DialogResponse } from '../DialogResponse'; @Component({ diff --git a/src/assets/wise5/components/dialogGuidance/dialogGuidanceService.ts b/src/assets/wise5/components/dialogGuidance/dialogGuidanceService.ts index 2ffa73add72..811321c52cf 100644 --- a/src/assets/wise5/components/dialogGuidance/dialogGuidanceService.ts +++ b/src/assets/wise5/components/dialogGuidance/dialogGuidanceService.ts @@ -18,24 +18,11 @@ export class DialogGuidanceService extends ComponentService { component.itemId = ''; component.feedbackRules = []; component.isComputerAvatarEnabled = false; - component.computerAvatarSettings = this.getDefaultComputerAvatarSettings(); + component.computerAvatarSettings = this.computerAvatarService.getDefaultComputerAvatarSettings(); component.version = 2; return component; } - getDefaultComputerAvatarSettings(): any { - return { - ids: this.computerAvatarService.getAvatars().map((avatar) => avatar.id), - label: this.getDefaultComputerAvatarLabel(), - prompt: $localize`Discuss your answer with a thought buddy!`, - initialResponse: $localize`Hi there! It's nice to meet you. What do you think about...` - }; - } - - getDefaultComputerAvatarLabel(): string { - return $localize`Thought Buddy`; - } - isCompleted(component: any, componentStates: any[], nodeEvents: any[], node: any) { return componentStates.length > 0; } diff --git a/src/assets/wise5/components/dialogGuidance/dialogGuidanceStudentModule.ts b/src/assets/wise5/components/dialogGuidance/dialogGuidanceStudentModule.ts index 5d48e881d0a..bbc5c755b96 100644 --- a/src/assets/wise5/components/dialogGuidance/dialogGuidanceStudentModule.ts +++ b/src/assets/wise5/components/dialogGuidance/dialogGuidanceStudentModule.ts @@ -1,14 +1,21 @@ import { NgModule } from '@angular/core'; import { StudentComponentModule } from '../../../../app/student/student.component.module'; -import { ComputerAvatarSelectorComponent } from '../../vle/computer-avatar-selector/computer-avatar-selector.component'; import { DialogGuidanceStudentComponent } from './dialog-guidance-student/dialog-guidance-student.component'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { DialogGuidanceFeedbackService } from '../../services/dialogGuidanceFeedbackService'; import { StudentTeacherCommonModule } from '../../../../app/student-teacher-common.module'; +import { ComputerAvatarSelectorModule } from '../../vle/computer-avatar-selector/computer-avatar-selector.module'; +import { ChatInputComponent } from '../../common/chat-input/chat-input.component'; @NgModule({ - declarations: [ComputerAvatarSelectorComponent, DialogGuidanceStudentComponent], - imports: [StudentTeacherCommonModule, MatButtonToggleModule, StudentComponentModule], + declarations: [DialogGuidanceStudentComponent], + imports: [ + ChatInputComponent, + ComputerAvatarSelectorModule, + MatButtonToggleModule, + StudentComponentModule, + StudentTeacherCommonModule + ], providers: [DialogGuidanceFeedbackService], exports: [DialogGuidanceStudentComponent] }) diff --git a/src/assets/wise5/components/dialogGuidance/edit-dialog-guidance-computer-avatar/edit-dialog-guidance-computer-avatar.component.spec.ts b/src/assets/wise5/components/dialogGuidance/edit-dialog-guidance-computer-avatar/edit-dialog-guidance-computer-avatar.component.spec.ts index 5bd0159bc35..97d671f2073 100644 --- a/src/assets/wise5/components/dialogGuidance/edit-dialog-guidance-computer-avatar/edit-dialog-guidance-computer-avatar.component.spec.ts +++ b/src/assets/wise5/components/dialogGuidance/edit-dialog-guidance-computer-avatar/edit-dialog-guidance-computer-avatar.component.spec.ts @@ -12,7 +12,7 @@ import { MatInputModule } from '@angular/material/input'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ProjectAssetService } from '../../../../../app/services/projectAssetService'; import { StudentTeacherCommonServicesModule } from '../../../../../app/student-teacher-common-services.module'; -import { ComputerAvatar } from '../../../common/ComputerAvatar'; +import { ComputerAvatar } from '../../../common/computer-avatar/ComputerAvatar'; import { copy } from '../../../common/object/object'; import { ComputerAvatarService } from '../../../services/computerAvatarService'; import { TeacherProjectService } from '../../../services/teacherProjectService'; diff --git a/src/assets/wise5/components/dialogGuidance/edit-dialog-guidance-computer-avatar/edit-dialog-guidance-computer-avatar.component.ts b/src/assets/wise5/components/dialogGuidance/edit-dialog-guidance-computer-avatar/edit-dialog-guidance-computer-avatar.component.ts index 1a734448367..af341660f42 100644 --- a/src/assets/wise5/components/dialogGuidance/edit-dialog-guidance-computer-avatar/edit-dialog-guidance-computer-avatar.component.ts +++ b/src/assets/wise5/components/dialogGuidance/edit-dialog-guidance-computer-avatar/edit-dialog-guidance-computer-avatar.component.ts @@ -1,8 +1,8 @@ import { Component, Input, OnInit } from '@angular/core'; -import { ComputerAvatar } from '../../../common/ComputerAvatar'; +import { ComputerAvatar } from '../../../common/computer-avatar/ComputerAvatar'; import { ComputerAvatarService } from '../../../services/computerAvatarService'; import { TeacherProjectService } from '../../../services/teacherProjectService'; -import { ComputerAvatarSettings } from '../ComputerAvatarSettings'; +import { ComputerAvatarSettings } from '../../../common/computer-avatar/ComputerAvatarSettings'; @Component({ selector: 'edit-dialog-guidance-computer-avatar', diff --git a/src/assets/wise5/services/componentInfoService.ts b/src/assets/wise5/services/componentInfoService.ts index ef48513d286..650eab4dcff 100644 --- a/src/assets/wise5/services/componentInfoService.ts +++ b/src/assets/wise5/services/componentInfoService.ts @@ -19,10 +19,12 @@ import { ShowMyWorkInfo } from '../components/showMyWork/ShowMyWorkInfo'; import { SummaryInfo } from '../components/summary/SummaryInfo'; import { TableInfo } from '../components/table/TableInfo'; import { ComponentInfo } from '../components/ComponentInfo'; +import { AiChatInfo } from '../components/aiChat/AiChatInfo'; @Injectable() export class ComponentInfoService { private componentInfo = { + AiChat: new AiChatInfo(), Animation: new AnimationInfo(), AudioOscillator: new AudioOscillatorInfo(), ConceptMap: new ConceptMapInfo(), diff --git a/src/assets/wise5/services/componentServiceLookupService.ts b/src/assets/wise5/services/componentServiceLookupService.ts index 6f4f59bd361..fff47c45282 100644 --- a/src/assets/wise5/services/componentServiceLookupService.ts +++ b/src/assets/wise5/services/componentServiceLookupService.ts @@ -18,12 +18,14 @@ import { ShowGroupWorkService } from '../components/showGroupWork/showGroupWorkS import { ShowMyWorkService } from '../components/showMyWork/showMyWorkService'; import { SummaryService } from '../components/summary/summaryService'; import { TableService } from '../components/table/tableService'; +import { AiChatService } from '../components/aiChat/aiChatService'; @Injectable() export class ComponentServiceLookupService { services = new Map(); constructor( + private aiChatService: AiChatService, private animationService: AnimationService, private audioOscillatorService: AudioOscillatorService, private conceptMapService: ConceptMapService, @@ -44,6 +46,7 @@ export class ComponentServiceLookupService { private summaryService: SummaryService, private tableService: TableService ) { + this.services.set('AiChat', this.aiChatService); this.services.set('Animation', this.animationService); this.services.set('AudioOscillator', this.audioOscillatorService); this.services.set('ConceptMap', this.conceptMapService); diff --git a/src/assets/wise5/services/componentServiceLookupServiceModule.ts b/src/assets/wise5/services/componentServiceLookupServiceModule.ts index e0e4a9dd463..0cd05813ad5 100644 --- a/src/assets/wise5/services/componentServiceLookupServiceModule.ts +++ b/src/assets/wise5/services/componentServiceLookupServiceModule.ts @@ -23,11 +23,13 @@ import { ComponentServiceLookupService } from './componentServiceLookupService'; import { ComputerAvatarService } from './computerAvatarService'; import { ConfigService } from './configService'; import { StudentAssetService } from './studentAssetService'; +import { AiChatService } from '../components/aiChat/aiChatService'; @NgModule({ declarations: [], imports: [], providers: [ + AiChatService, AnimationService, AudioOscillatorService, ComputerAvatarService, diff --git a/src/assets/wise5/services/componentTypeService.module.ts b/src/assets/wise5/services/componentTypeService.module.ts new file mode 100644 index 00000000000..2b1ec0038f1 --- /dev/null +++ b/src/assets/wise5/services/componentTypeService.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { ComponentServiceLookupService } from './componentServiceLookupService'; +import { UserService } from '../../../app/services/user.service'; +import { ComponentTypeService } from './componentTypeService'; +import { ConfigService } from '../../../app/services/config.service'; + +@NgModule({ + providers: [ComponentServiceLookupService, ComponentTypeService, ConfigService, UserService] +}) +export class ComponentTypeServiceModule {} diff --git a/src/assets/wise5/services/componentTypeService.ts b/src/assets/wise5/services/componentTypeService.ts index 78daab01aa6..38bb59c5cbb 100644 --- a/src/assets/wise5/services/componentTypeService.ts +++ b/src/assets/wise5/services/componentTypeService.ts @@ -1,12 +1,18 @@ import { Injectable } from '@angular/core'; import { ComponentServiceLookupService } from './componentServiceLookupService'; +import { UserService } from '../../../app/services/user.service'; +import { ConfigService } from './configService'; @Injectable() export class ComponentTypeService { - constructor(private componentServiceLookupService: ComponentServiceLookupService) {} + constructor( + private componentServiceLookupService: ComponentServiceLookupService, + private configService: ConfigService, + private userService: UserService + ) {} getComponentTypes(): any[] { - return [ + const componentTypes = [ { type: 'Animation', name: this.getComponentTypeLabel('Animation') }, { type: 'AudioOscillator', name: this.getComponentTypeLabel('AudioOscillator') }, { type: 'ConceptMap', name: this.getComponentTypeLabel('ConceptMap') }, @@ -27,9 +33,22 @@ export class ComponentTypeService { { type: 'Summary', name: this.getComponentTypeLabel('Summary') }, { type: 'Table', name: this.getComponentTypeLabel('Table') } ]; + if (this.isAiChatAllowed()) { + componentTypes.unshift({ type: 'AiChat', name: this.getComponentTypeLabel('AiChat') }); + } + return componentTypes; } getComponentTypeLabel(componentType: string): string { return this.componentServiceLookupService.getService(componentType).getComponentTypeLabel(); } + + private isAiChatAllowed(): boolean { + return ( + this.configService.getConfigParam('chatGptEnabled') && + (this.userService.isAdmin() || + this.userService.isResearcher() || + this.userService.isTrustedAuthor()) + ); + } } diff --git a/src/assets/wise5/services/computerAvatarService.ts b/src/assets/wise5/services/computerAvatarService.ts index 15a7806df0f..c87ba3ee210 100644 --- a/src/assets/wise5/services/computerAvatarService.ts +++ b/src/assets/wise5/services/computerAvatarService.ts @@ -1,8 +1,9 @@ 'use strict'; import { Injectable } from '@angular/core'; -import { ComputerAvatar } from '../common/ComputerAvatar'; +import { ComputerAvatar } from '../common/computer-avatar/ComputerAvatar'; import { copy } from '../common/object/object'; +import { ComputerAvatarSettings } from '../common/computer-avatar/ComputerAvatarSettings'; @Injectable() export class ComputerAvatarService { @@ -35,4 +36,17 @@ export class ComputerAvatarService { getDefaultAvatar(): ComputerAvatar { return this.getAvatar('robot1'); } + + getDefaultComputerAvatarSettings(): ComputerAvatarSettings { + return { + ids: this.getAvatars().map((avatar) => avatar.id), + label: this.getDefaultComputerAvatarLabel(), + prompt: $localize`Discuss your answer with a thought buddy!`, + initialResponse: $localize`Hi there! It's nice to meet you. What do you think about...` + }; + } + + getDefaultComputerAvatarLabel(): string { + return $localize`Thought Buddy`; + } } diff --git a/src/assets/wise5/services/studentStatusService.ts b/src/assets/wise5/services/studentStatusService.ts index f6462968494..915c741a6bc 100644 --- a/src/assets/wise5/services/studentStatusService.ts +++ b/src/assets/wise5/services/studentStatusService.ts @@ -48,6 +48,10 @@ export class StudentStatusService { return this.studentStatus.computerAvatarId; } + isGlobalComputerAvatarAvailable(): boolean { + return this.getComputerAvatarId() != null; + } + private saveStudentStatus(): void { const runId = this.configService.getRunId(); const periodId = this.configService.getPeriodId(); diff --git a/src/assets/wise5/vle/computer-avatar-selector/computer-avatar-selector.component.spec.ts b/src/assets/wise5/vle/computer-avatar-selector/computer-avatar-selector.component.spec.ts index 7c212dbd270..3dbc43cdf10 100644 --- a/src/assets/wise5/vle/computer-avatar-selector/computer-avatar-selector.component.spec.ts +++ b/src/assets/wise5/vle/computer-avatar-selector/computer-avatar-selector.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ComputerAvatar } from '../../common/ComputerAvatar'; +import { ComputerAvatar } from '../../common/computer-avatar/ComputerAvatar'; import { DialogGuidanceService } from '../../components/dialogGuidance/dialogGuidanceService'; import { AnnotationService } from '../../services/annotationService'; import { ComputerAvatarService } from '../../services/computerAvatarService'; diff --git a/src/assets/wise5/vle/computer-avatar-selector/computer-avatar-selector.component.ts b/src/assets/wise5/vle/computer-avatar-selector/computer-avatar-selector.component.ts index 2b6628cd687..c80865c2df3 100644 --- a/src/assets/wise5/vle/computer-avatar-selector/computer-avatar-selector.component.ts +++ b/src/assets/wise5/vle/computer-avatar-selector/computer-avatar-selector.component.ts @@ -1,8 +1,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { ComputerAvatar } from '../../common/ComputerAvatar'; +import { ComputerAvatar } from '../../common/computer-avatar/ComputerAvatar'; import { ComputerAvatarService } from '../../services/computerAvatarService'; -import { ComputerAvatarSettings } from '../../components/dialogGuidance/ComputerAvatarSettings'; -import { DialogGuidanceService } from '../../components/dialogGuidance/dialogGuidanceService'; +import { ComputerAvatarSettings } from '../../common/computer-avatar/ComputerAvatarSettings'; @Component({ selector: 'computer-avatar-selector', @@ -21,10 +20,7 @@ export class ComputerAvatarSelectorComponent implements OnInit { avatarsPath: string; label: string; - constructor( - private computerAvatarService: ComputerAvatarService, - private dialogGuidanceService: DialogGuidanceService - ) {} + constructor(private computerAvatarService: ComputerAvatarService) {} ngOnInit(): void { this.avatars = this.filterAvatars( @@ -45,7 +41,7 @@ export class ComputerAvatarSelectorComponent implements OnInit { initializeLabel(): void { const computerAvatarSettingsLabel = this.computerAvatarSettings.label; if (computerAvatarSettingsLabel == null || computerAvatarSettingsLabel === '') { - this.label = this.dialogGuidanceService.getDefaultComputerAvatarLabel(); + this.label = this.computerAvatarService.getDefaultComputerAvatarLabel(); } else { this.label = computerAvatarSettingsLabel; } diff --git a/src/assets/wise5/vle/computer-avatar-selector/computer-avatar-selector.module.ts b/src/assets/wise5/vle/computer-avatar-selector/computer-avatar-selector.module.ts new file mode 100644 index 00000000000..bd79c64ee96 --- /dev/null +++ b/src/assets/wise5/vle/computer-avatar-selector/computer-avatar-selector.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { ComputerAvatarSelectorComponent } from './computer-avatar-selector.component'; +import { StudentTeacherCommonModule } from '../../../../app/student-teacher-common.module'; + +@NgModule({ + declarations: [ComputerAvatarSelectorComponent], + exports: [ComputerAvatarSelectorComponent], + imports: [StudentTeacherCommonModule] +}) +export class ComputerAvatarSelectorModule {} diff --git a/src/messages.xlf b/src/messages.xlf index f3efd3619f1..27390cff6b8 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -267,7 +267,7 @@ src/app/authoring-tool/edit-component-advanced/edit-component-advanced.component.html - 106 + 111 src/app/classroom-monitor/show-node-info-dialog/show-node-info-dialog.component.html @@ -1135,6 +1135,10 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/assets/wise5/authoringTool/components/top-bar/top-bar.component.html 39 + + src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.html + 10 + src/assets/wise5/components/common/feedbackRule/edit-feedback-rules/edit-feedback-rules.component.html 15 @@ -14909,6 +14913,28 @@ Are you sure you want to proceed? 49 + + Add response... + + src/assets/wise5/common/chat-input/chat-input.component.html + 6 + + + src/assets/wise5/components/peerChat/peer-chat-message-input/peer-chat-message-input.component.html + 6 + + + + Send + + src/assets/wise5/common/chat-input/chat-input.component.html + 18,20 + + + src/assets/wise5/components/peerChat/peer-chat-message-input/peer-chat-message-input.component.html + 21,23 + + @@ -15153,6 +15179,142 @@ Are you sure you want to proceed? 58 + + Students chat with an AI bot. + + src/assets/wise5/components/aiChat/AiChatInfo.ts + 4 + + + + AI Chat + + src/assets/wise5/components/aiChat/AiChatInfo.ts + 5 + + + src/assets/wise5/components/aiChat/AiChatInfo.ts + 8 + + + src/assets/wise5/components/aiChat/aiChatService.ts + 14 + + + + Use the system prompt to instruct the chat bot how to behave. Students will not see the system prompt. + + src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.html + 3,6 + + + + Toggle system prompt instructions + + src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.html + 13 + + + + System Prompt Instructions + + src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.html + 20 + + + + Provide context, instructions, and other relevant information to help the chat bot act the way you want it to. Be as specific as possible. + + src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.html + 21,24 + + + + Example System Prompt + + src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.html + 25 + + + + You are a teacher helping a student understand the greenhouse effect by using the example of a car that has been sitting in the sun on a cold day. The student is asked how the temperature inside the car will feel. Do not tell them the correct answer, but guide them to better understand the science by asking questions. Also make sure they explain their reasoning. Limit your response to 100 words or less. + + src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.html + 26,32 + + + + System Prompt + + src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.html + 35 + + + + Use the prompt to introduce the chatbot activity. Students will see the prompt. + + src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.html + 44 + + + + Enable Computer Avatar + + src/assets/wise5/components/aiChat/ai-chat-authoring/ai-chat-authoring.component.html + 55 + + + src/assets/wise5/components/dialogGuidance/dialog-guidance-authoring/dialog-guidance-authoring.component.html + 21 + + + + Automated guidance response + + src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.html + 5 + + + src/assets/wise5/components/dialogGuidance/dialog-response/dialog-response.component.html + 17 + + + src/assets/wise5/components/dialogGuidance/dialog-response/dialog-response.component.html + 26 + + + + + + src/assets/wise5/components/aiChat/ai-chat-bot-message/ai-chat-bot-message.component.html + 10 + + + + Student response + + src/assets/wise5/components/aiChat/ai-chat-student-message/ai-chat-student-message.component.html + 5 + + + src/assets/wise5/components/dialogGuidance/dialog-response/dialog-response.component.html + 6 + + + + An error occurred. + + src/assets/wise5/components/aiChat/ai-chat-student/ai-chat-student.component.ts + 95 + + + + Model + + src/assets/wise5/components/aiChat/edit-ai-chat-advanced/edit-ai-chat-advanced.component.html + 2 + + Students watch an animation. @@ -16970,70 +17132,6 @@ Category Name: 7 - - Enable Computer Avatar - - src/assets/wise5/components/dialogGuidance/dialog-guidance-authoring/dialog-guidance-authoring.component.html - 21 - - - - Add response... - - src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.html - 26 - - - src/assets/wise5/components/peerChat/peer-chat-message-input/peer-chat-message-input.component.html - 6 - - - - Send - - src/assets/wise5/components/dialogGuidance/dialog-guidance-student/dialog-guidance-student.component.html - 39,41 - - - - Student response - - src/assets/wise5/components/dialogGuidance/dialog-response/dialog-response.component.html - 6 - - - - Automated guidance response - - src/assets/wise5/components/dialogGuidance/dialog-response/dialog-response.component.html - 17 - - - src/assets/wise5/components/dialogGuidance/dialog-response/dialog-response.component.html - 26 - - - - Discuss your answer with a thought buddy! - - src/assets/wise5/components/dialogGuidance/dialogGuidanceService.ts - 30 - - - - Hi there! It's nice to meet you. What do you think about... - - src/assets/wise5/components/dialogGuidance/dialogGuidanceService.ts - 31 - - - - Thought Buddy - - src/assets/wise5/components/dialogGuidance/dialogGuidanceService.ts - 36 - - Use Global Computer Avatar @@ -20064,13 +20162,6 @@ If this problem continues, let your teacher know and move on to the next activit 7 - - Send - - src/assets/wise5/components/peerChat/peer-chat-message-input/peer-chat-message-input.component.html - 21,23 - - Make this message viewable to students again @@ -21164,7 +21255,7 @@ If this problem continues, let your teacher know and move on to the next activit Ermina src/assets/wise5/services/computerAvatarService.ts - 10 + 11 A name for a computer avatar @@ -21172,7 +21263,7 @@ If this problem continues, let your teacher know and move on to the next activit Alyx src/assets/wise5/services/computerAvatarService.ts - 11 + 12 A name for a computer avatar @@ -21180,7 +21271,7 @@ If this problem continues, let your teacher know and move on to the next activit Kai src/assets/wise5/services/computerAvatarService.ts - 12 + 13 A name for a computer avatar @@ -21188,7 +21279,7 @@ If this problem continues, let your teacher know and move on to the next activit Morgan src/assets/wise5/services/computerAvatarService.ts - 13 + 14 A name for a computer avatar @@ -21196,7 +21287,7 @@ If this problem continues, let your teacher know and move on to the next activit Parker src/assets/wise5/services/computerAvatarService.ts - 14 + 15 A name for a computer avatar @@ -21204,7 +21295,7 @@ If this problem continues, let your teacher know and move on to the next activit Milan src/assets/wise5/services/computerAvatarService.ts - 15 + 16 A name for a computer avatar @@ -21212,7 +21303,7 @@ If this problem continues, let your teacher know and move on to the next activit Emery src/assets/wise5/services/computerAvatarService.ts - 16 + 17 A name for a computer avatar @@ -21220,7 +21311,7 @@ If this problem continues, let your teacher know and move on to the next activit Yuna src/assets/wise5/services/computerAvatarService.ts - 17 + 18 A name for a computer avatar @@ -21228,7 +21319,7 @@ If this problem continues, let your teacher know and move on to the next activit Ada src/assets/wise5/services/computerAvatarService.ts - 18 + 19 A name for a computer avatar @@ -21236,10 +21327,31 @@ If this problem continues, let your teacher know and move on to the next activit Nico src/assets/wise5/services/computerAvatarService.ts - 19 + 20 A name for a computer avatar + + Discuss your answer with a thought buddy! + + src/assets/wise5/services/computerAvatarService.ts + 44 + + + + Hi there! It's nice to meet you. What do you think about... + + src/assets/wise5/services/computerAvatarService.ts + 45 + + + + Thought Buddy + + src/assets/wise5/services/computerAvatarService.ts + 50 + + Student