Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Directive, Input } from '@angular/core';
import { Directive, inject, Input } from '@angular/core';
import { ComponentContent } from '../../../assets/wise5/common/ComponentContent';
import { Component } from '../../../assets/wise5/common/Component';
import { NotebookService } from '../../../assets/wise5/services/notebookService';
import { TeacherProjectService } from '../../../assets/wise5/services/teacherProjectService';
import { TeacherNodeService } from '../../../assets/wise5/services/teacherNodeService';
import { moveObjectDown, moveObjectUp } from '../../../assets/wise5/common/array/array';
import { ComponentServiceLookupService } from '../../../assets/wise5/services/componentServiceLookupService';

@Directive()
export abstract class EditAdvancedComponentComponent {
protected aiEnabled: boolean;
component: Component;
componentContent: ComponentContent;
protected selectedTabIndex: number = 0;
Expand All @@ -16,6 +18,8 @@ export abstract class EditAdvancedComponentComponent {
@Input() nodeId: string;
@Input() tab: string = 'general';

private componentServiceLookupService = inject(ComponentServiceLookupService);

constructor(
protected nodeService: TeacherNodeService,
protected notebookService: NotebookService,
Expand All @@ -25,6 +29,19 @@ export abstract class EditAdvancedComponentComponent {
ngOnInit(): void {
this.componentContent = this.teacherProjectService.getComponent(this.nodeId, this.componentId);
this.component = new Component(this.componentContent, this.nodeId);
this.aiEnabled = this.teacherProjectService.getProject().ai?.enabled;
if (this.aiEnabled && ['Discussion', 'OpenResponse'].includes(this.component.content.type)) {
if (this.component.content.ai == null) {
const componentService = this.componentServiceLookupService.getService(
this.component.content.type
);
this.component.content.ai = {
teacherSummarySystemPrompt: componentService.getDefaultTeacherSummarySystemPrompt(
this.component.content.prompt
)
};
}
}
this.teacherProjectService.uiChanged();

switch (this.tab) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { FormsModule } from '@angular/forms';
import { EditComponentAddToNotebookButtonComponent } from '../edit-component-add-to-notebook-button/edit-component-add-to-notebook-button.component';
import { EditConnectedComponentsComponent } from '../edit-connected-components/edit-connected-components.component';
import { EditComponentSummarizerSystemPromptComponent } from '../edit-component-summarizer-system-prompt/edit-component-summarizer-system-prompt.component';

@NgModule({
imports: [
Expand All @@ -29,6 +30,7 @@ import { EditConnectedComponentsComponent } from '../edit-connected-components/e
EditComponentExcludeFromTotalScoreComponent,
EditComponentWidthComponent,
EditComponentRubricComponent,
EditComponentSummarizerSystemPromptComponent,
EditComponentTagsComponent,
EditComponentConstraintsComponent,
EditComponentJsonComponent,
Expand All @@ -48,6 +50,7 @@ import { EditConnectedComponentsComponent } from '../edit-connected-components/e
EditComponentExcludeFromTotalScoreComponent,
EditComponentWidthComponent,
EditComponentRubricComponent,
EditComponentSummarizerSystemPromptComponent,
EditComponentTagsComponent,
EditComponentConstraintsComponent,
EditComponentJsonComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { NotificationService } from '../../../assets/wise5/services/notification
import { TeacherNodeService } from '../../../assets/wise5/services/teacherNodeService';
import { TeacherProjectService } from '../../../assets/wise5/services/teacherProjectService';
import { EditComponentAdvancedComponent } from './edit-component-advanced.component';
import { ComponentServiceLookupService } from '../../../assets/wise5/services/componentServiceLookupService';

let component: EditComponentAdvancedComponent;
let fixture: ComponentFixture<EditComponentAdvancedComponent>;
Expand All @@ -34,6 +35,7 @@ describe('EditComponentAdvancedComponent', () => {
}
},
MockProviders(
ComponentServiceLookupService,
TeacherNodeService,
NotebookService,
NotificationService,
Expand All @@ -47,6 +49,8 @@ describe('EditComponentAdvancedComponent', () => {
spyOn(TestBed.inject(TeacherProjectService), 'getComponent').and.returnValue({
type: 'ShowMyWork'
} as ComponentContent);
spyOn(TestBed.inject(TeacherProjectService), 'getProject').and.returnValue({});

fixture.detectChanges();
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Component } from '@angular/core';
import { EditComponentFieldComponent } from '../edit-component-field.component';
import { MatFormFieldModule } from '@angular/material/form-field';
import { FormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';

@Component({
imports: [CdkTextareaAutosize, FormsModule, MatFormFieldModule, MatInputModule],
selector: 'edit-component-summarizer-system-prompt',
template: `<mat-form-field class="w-full">
<mat-label i18n>Teacher Summary System Prompt</mat-label>
<textarea
matInput
cdkTextareaAutosize
[(ngModel)]="componentContent.ai.teacherSummarySystemPrompt"
(ngModelChange)="inputChanged.next($event)"
></textarea>
</mat-form-field> `
})
export class EditComponentSummarizerSystemPromptComponent extends EditComponentFieldComponent {}
3 changes: 3 additions & 0 deletions src/assets/wise5/common/ComponentContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { DynamicPrompt } from '../directives/dynamic-prompt/DynamicPrompt';

export interface ComponentContent {
id: string;
ai?: {
teacherSummarySystemPrompt?: string;
};
anonymizeResponses?: boolean;
connectedComponents?: any[];
constraints?: any[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { NotebookService } from '../../../services/notebookService';
import { EditConnectedComponentsComponent } from '../../../../../app/authoring-tool/edit-connected-components/edit-connected-components.component';
import { EditComponentJsonComponent } from '../../../../../app/authoring-tool/edit-component-json/edit-component-json.component';
import { EditComponentWidthComponent } from '../../../../../app/authoring-tool/edit-component-width/edit-component-width.component';
import { ComponentServiceLookupService } from '../../../services/componentServiceLookupService';

describe('EditAiChatAdvancedComponent', () => {
let component: EditAiChatAdvancedComponent;
Expand All @@ -22,7 +23,14 @@ describe('EditAiChatAdvancedComponent', () => {
EditConnectedComponentsComponent
)
],
providers: [MockProviders(NotebookService, TeacherNodeService, TeacherProjectService)]
providers: [
MockProviders(
ComponentServiceLookupService,
NotebookService,
TeacherNodeService,
TeacherProjectService
)
]
});
fixture = TestBed.createComponent(EditAiChatAdvancedComponent);
component = fixture.componentInstance;
Expand All @@ -31,6 +39,7 @@ describe('EditAiChatAdvancedComponent', () => {
id: 'component1',
type: 'aiChat'
});
spyOn(TestBed.inject(TeacherProjectService), 'getProject').and.returnValue({});
fixture.detectChanges();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('EditConceptMapAdvancedComponent', () => {
spyOn(TestBed.inject(TeacherProjectService), 'getComponent').and.returnValue({
rules: []
} as ConceptMapContent);
spyOn(TestBed.inject(TeacherProjectService), 'getProject').and.returnValue({});
spyOn(TestBed.inject(NotebookService), 'isNotebookEnabled').and.returnValue(true);
fixture = TestBed.createComponent(EditConceptMapAdvancedComponent);
component = fixture.componentInstance;
Expand Down
9 changes: 9 additions & 0 deletions src/assets/wise5/components/discussion/discussionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export class DiscussionService extends ComponentService {
component.isStudentAttachmentEnabled = true;
component.gateClassmateResponses = true;
component.anonymizeResponses = false;
component.ai = {
teacherSummarySystemPrompt: this.getDefaultTeacherSummarySystemPrompt()
};
return component;
}

Expand Down Expand Up @@ -335,4 +338,10 @@ export class DiscussionService extends ComponentService {
col2: evenResponses.reverse()
};
}

getDefaultTeacherSummarySystemPrompt(): string {
return `You are a teacher who is summarizing students' discussion threads, which include posts and replies to the following question: "$QUESTION$".
Each thread is in the format: $RESPONSE_FORMAT$.
In the same language as the question, provide a summary of the threads in 100 words or less.`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
<edit-component-width [componentContent]="componentContent" />
</div>
</mat-tab>
@if (aiEnabled) {
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>auto_awesome</mat-icon>&nbsp;<span i18n>AI</span>
</ng-template>
<edit-component-summarizer-system-prompt [componentContent]="component.content" />
</mat-tab>
}
<mat-tab>
<ng-template mat-tab-label>
<mat-icon>visibility</mat-icon>&nbsp;<span i18n>Visibility</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('EditGraphAdvancedComponent', () => {
xAxis: {},
yAxis: {}
} as GraphContent);
spyOn(TestBed.inject(TeacherProjectService), 'getProject').and.returnValue({});
spyOn(projectService, 'getLocale').and.returnValue(new ProjectLocale({ default: 'en-US' }));
spyOn(projectService, 'isDefaultLocale').and.returnValue(true);
spyOn(TestBed.inject(NotebookService), 'isNotebookEnabled').and.returnValue(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
<ng-template mat-tab-label>
<mat-icon>auto_awesome</mat-icon>&nbsp;<span i18n>AI</span>
</ng-template>
@if (aiEnabled) {
<!-- currently C-Rater can be used without the global AI being turned on, due to legacy reasons. -->
<edit-component-summarizer-system-prompt [componentContent]="component.content" />
}
<div>
<mat-checkbox
color="primary"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe('EditOpenResponseAdvancedComponent', () => {
beforeEach(() => {
const projectService = TestBed.inject(TeacherProjectService);
spyOn(projectService, 'getComponent').and.returnValue({} as ComponentContent);
spyOn(TestBed.inject(TeacherProjectService), 'getProject').and.returnValue({});
spyOn(TestBed.inject(NotebookService), 'isNotebookEnabled').and.returnValue(true);
spyOn(projectService, 'getFlattenedProjectAsNodeIds').and.returnValue([
'node1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export class OpenResponseService extends ComponentService {
component.type = 'OpenResponse';
component.starterSentence = null;
component.isStudentAttachmentEnabled = false;
component.ai = {
teacherSummarySystemPrompt: this.getDefaultTeacherSummarySystemPrompt()
};
return component;
}

Expand Down Expand Up @@ -127,4 +130,10 @@ export class OpenResponseService extends ComponentService {
}
return false;
}

getDefaultTeacherSummarySystemPrompt(): string {
return `You are a teacher who is summarizing student responses to the following question: "$QUESTION$".
Each student response is in the format: $RESPONSE_FORMAT$.
In the same language as the question, provide a summary of the responses in 100 words or less.`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TeacherProjectService } from '../../../services/teacherProjectService';
import { PeerChatContent } from '../PeerChatContent';
import { EditPeerChatAdvancedComponentComponent } from './edit-peer-chat-advanced-component.component';
import { EditComponentJsonComponent } from '../../../../../app/authoring-tool/edit-component-json/edit-component-json.component';
import { ComponentServiceLookupService } from '../../../services/componentServiceLookupService';

describe('EditPeerChatAdvancedComponentComponent', () => {
let component: EditPeerChatAdvancedComponentComponent;
Expand All @@ -16,7 +17,12 @@ describe('EditPeerChatAdvancedComponentComponent', () => {
await TestBed.configureTestingModule({
imports: [EditPeerChatAdvancedComponentComponent, MockComponent(EditComponentJsonComponent)],
providers: [
MockProviders(TeacherNodeService, TeacherProjectService, NotebookService),
MockProviders(
ComponentServiceLookupService,
TeacherNodeService,
TeacherProjectService,
NotebookService
),
provideHttpClient(withInterceptorsFromDi())
]
}).compileComponents();
Expand All @@ -26,6 +32,7 @@ describe('EditPeerChatAdvancedComponentComponent', () => {
spyOn(TestBed.inject(TeacherProjectService), 'getComponent').and.returnValue(
{} as PeerChatContent
);
spyOn(TestBed.inject(TeacherProjectService), 'getProject').and.returnValue({});
fixture = TestBed.createComponent(EditPeerChatAdvancedComponentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('EditTableAdvancedComponent', () => {

beforeEach(() => {
spyOn(TestBed.inject(TeacherProjectService), 'getComponent').and.returnValue(createComponent());
spyOn(TestBed.inject(TeacherProjectService), 'getProject').and.returnValue({});
spyOn(TestBed.inject(NotebookService), 'isNotebookEnabled').and.returnValue(true);
spyOn(TestBed.inject(TeacherProjectService), 'getFlattenedProjectAsNodeIds').and.returnValue([
'node1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DatePipe } from '@angular/common';
import { TeacherProjectService } from '../../../services/teacherProjectService';
import { ChatService } from '../../../../../app/services/chat/chat.service';
import { OpenAiChatService } from '../../../../../app/services/chat/openAiChat.service';
import { ComponentContent } from '../../../common/ComponentContent';

/**
* Abstract base class for components that use an LLM to summarize student responses.
Expand All @@ -25,6 +26,7 @@ export abstract class AiSummaryComponent {
@Input() periodId: number;

private chatService: ChatService = inject(OpenAiChatService);
private component: ComponentContent;
protected dataService: TeacherDataService = inject(TeacherDataService);
protected datePipe: DatePipe = inject(DatePipe);
private localStorageService: LocalStorageService = inject(LocalStorageService);
Expand All @@ -38,6 +40,7 @@ export abstract class AiSummaryComponent {
protected summaryDate: Date;

ngOnInit(): void {
this.component = this.projectService.getComponent(this.nodeId, this.componentId);
this.latestComponentStates = this.getLatestComponentStates();
this.hasStudentResponses = this.latestComponentStates.length > 0;
if (this.hasStudentResponses) {
Expand All @@ -64,9 +67,8 @@ export abstract class AiSummaryComponent {

protected async generateSummary(): Promise<void> {
this.generatingSummary = true;
const prompt = this.projectService.getComponent(this.nodeId, this.componentId).prompt;
this.summary = await this.chatService.sendMessage([
new ChatMessage('system', this.getSystemPrompt(prompt), this.nodeId),
new ChatMessage('system', this.getSystemPrompt(this.component.prompt), this.nodeId),
new ChatMessage('user', this.getStudentResponses(), this.nodeId)
]);
this.localStorageService.setItem(this.getSummaryKey(), this.summary);
Expand All @@ -79,7 +81,17 @@ export abstract class AiSummaryComponent {

protected abstract getStudentResponses(): string;

protected abstract getSystemPrompt(prompt: string): string;
protected getSystemPrompt(prompt: string): string {
const systemPrompt =
this.component.ai?.teacherSummarySystemPrompt ?? this.getDefaultSystemPrompt();
return systemPrompt
.replace('$QUESTION$', prompt)
.replace('$RESPONSE_FORMAT$', this.getResponseFormat());
}

protected abstract getResponseFormat(): string;

protected abstract getDefaultSystemPrompt(): string;

private getSummaryKey(): string {
return `component-summary-${this.periodId}-${this.nodeId}-${this.componentId}`;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { DatePipe } from '@angular/common';
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { MatButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MarkdownComponent } from 'ngx-markdown';
import { AiSummaryComponent } from '../ai-summary/ai-summary.component';
import { DiscussionService } from '../../../components/discussion/discussionService';

interface Thread {
id: number;
Expand All @@ -23,10 +24,14 @@ interface Thread {
templateUrl: '../ai-summary/ai-summary.component.html'
})
export class DiscussionAiSummaryComponent extends AiSummaryComponent {
protected getSystemPrompt(prompt: string): string {
return `You are a teacher who is summarizing students' discussion threads, which include posts and replies to the following question: "${prompt}".
Each thread is in the format: <thread><post>Post</post><replies><reply>Reply 1</reply><reply>Reply 2</reply></replies></thread>.
In the same language as the question, provide a summary of the threads in 100 words or less.`;
private discussionService = inject(DiscussionService);

protected getDefaultSystemPrompt(): string {
return this.discussionService.getDefaultTeacherSummarySystemPrompt();
}

protected getResponseFormat(): string {
return '<thread><post>Post</post><replies><reply>Reply 1</reply><reply>Reply 2</reply></replies></thread>';
}

protected getStudentResponses(): string {
Expand Down
Loading