Skip to content
Open
17 changes: 17 additions & 0 deletions packages/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ export enum MailServiceType {
ASYNC_QUESTION_NEW_COMMENT_ON_MY_POST = 'async_question_new_comment_on_my_post',
ASYNC_QUESTION_NEW_COMMENT_ON_OTHERS_POST = 'async_question_new_comment_on_others_post',
COURSE_CLONE_SUMMARY = 'course_clone_summary',
CHATBOT_ANSWER_UPDATED = 'chatbot_answer_updated',
}
/**
* Represents one of three possible user roles in a course.
Expand Down Expand Up @@ -525,6 +526,22 @@ export interface UpdateChatbotQuestionParams {
}[]
}

export class NotifyUpdatedChatbotAnswerParams {
@IsString()
oldAnswer!: string

@IsString()
newAnswer!: string

@IsString()
@IsOptional()
oldQuestion?: string

@IsString()
@IsOptional()
newQuestion?: string
}

// this is the response from the backend when new questions are asked
// if question is I don't know, only answer and questionId are returned
export interface ChatbotAskResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface FormValues {
answer: string
verified: boolean
suggested: boolean
emailNotifyOnAnswerUpdate?: boolean
sourceDocuments: SourceDocument[]
selectedDocuments: {
docId: string
Expand All @@ -46,6 +47,23 @@ interface EditChatbotQuestionModalProps {
cid: number
deleteQuestion: (id: string) => void
}
type AnswerUpdateCheckboxProps = {
form: any
originalAnswer: string
checked?: boolean
onChange?: (e: any) => void
}

const AnswerUpdateCheckbox: React.FC<AnswerUpdateCheckboxProps> = ({
form,
originalAnswer,
checked,
onChange,
}) => {
const currentAnswer = Form.useWatch('answer', form)
const changed = (currentAnswer ?? '') !== (originalAnswer ?? '')
return <Checkbox disabled={!changed} checked={checked} onChange={onChange} />
}

const EditChatbotQuestionModal: React.FC<EditChatbotQuestionModalProps> = ({
open,
Expand Down Expand Up @@ -150,16 +168,49 @@ const EditChatbotQuestionModal: React.FC<EditChatbotQuestionModalProps> = ({
})
}

const { emailNotifyOnAnswerUpdate, ...sanitizedValues } = values
const valuesWithId = {
...values,
...sanitizedValues,
Comment on lines +171 to +173
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh neat I didn't know you could use the spread operator when deconstructing an object

id: editingRecord.vectorStoreId,
sourceDocuments: values.sourceDocuments || [],
}

await API.chatbot.staffOnly
.updateQuestion(cid, valuesWithId)
.then(() => {
.then(async () => {
message.success('Question updated successfully')
if (emailNotifyOnAnswerUpdate) {
try {
const resp = await API.chatbot.staffOnly.notifyAnswerUpdate(
cid,
editingRecord.vectorStoreId,
{
oldAnswer: editingRecord.answer,
newAnswer: values.answer,
oldQuestion: editingRecord.question,
newQuestion: values.question,
},
)
if (resp?.recipients != undefined) {
if (resp.totalRecipients === 5) {
message.success(
`Notification email sent to ${resp.recipients} user${
resp.recipients === 1 ? '' : 's'
} (max 5).`,
)
} else {
message.success(
`Notification email sent to ${resp.recipients} user${
resp.recipients === 1 ? '' : 's'
}`,
)
}
}
} catch (e) {
const errorMessage = getErrorMessage(e)
message.error('Failed to send notification email: ' + errorMessage)
}
}
onSuccessfulUpdate()
})
.catch((e) => {
Expand Down Expand Up @@ -227,6 +278,7 @@ const EditChatbotQuestionModal: React.FC<EditChatbotQuestionModalProps> = ({
question: editingRecord.question,
verified: editingRecord.verified,
suggested: editingRecord.suggested,
emailNotifyOnAnswerUpdate: false,
sourceDocuments: editingRecord.sourceDocuments,
}}
clearOnDestroy
Expand All @@ -245,6 +297,7 @@ const EditChatbotQuestionModal: React.FC<EditChatbotQuestionModalProps> = ({
</Form.Item>
<Form.Item
name="answer"
className="mb-1"
tooltip={{
title: <MarkdownGuideTooltipBody />,
classNames: {
Expand All @@ -256,6 +309,25 @@ const EditChatbotQuestionModal: React.FC<EditChatbotQuestionModalProps> = ({
>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 8 }} />
</Form.Item>
<Form.Item
label="Email notify student(s) of updated answer?"
Comment thread
AdamFipke marked this conversation as resolved.
layout="horizontal"
name="emailNotifyOnAnswerUpdate"
valuePropName="checked"
tooltip={
<div className="flex flex-col gap-y-2">
<p>
Sends an email to the student(s) who previously asked this
question with a before/after of the answer.
</p>
</div>
}
>
<AnswerUpdateCheckbox
form={form}
originalAnswer={editingRecord.answer}
/>
</Form.Item>
<Form.Item
label="Mark Q&A as Verified by Human"
layout="horizontal"
Expand Down
20 changes: 20 additions & 0 deletions packages/frontend/app/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,26 @@ export class APIClient {
docId: string,
): Promise<{ success: boolean }> =>
this.req('DELETE', `/api/v1/chatbot/document/${courseId}/${docId}`),
notifyAnswerUpdate: async (
courseId: number,
vectorStoreId: string,
body: {
oldAnswer: string
newAnswer: string
oldQuestion?: string
newQuestion?: string
},
Comment on lines +401 to +406
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace with NotifyUpdatedChatbotAnswerParams that you've created

): Promise<{
recipients: number
totalRecipients: number
unsubscribedRecipients: number
}> =>
this.req(
'POST',
`/api/v1/chatbot/question/${courseId}/${vectorStoreId}/notify`,
undefined,
body,
),
uploadDocument: async (
courseId: number,
body: FormData,
Expand Down
24 changes: 24 additions & 0 deletions packages/server/src/chatbot/chatbot.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
OrganizationChatbotSettings,
OrganizationChatbotSettingsDefaults,
OrganizationRole,
NotifyUpdatedChatbotAnswerParams,
Role,
UpdateChatbotProviderBody,
UpdateChatbotQuestionParams,
Expand Down Expand Up @@ -378,6 +379,29 @@ export class ChatbotController {
// }

// resets all chatbot data for the course. Unused

// staff-only: send notification email to all students who asked this question
// Body must include oldAnswer and newAnswer, and can optionally include question changes for context
@Post('question/:courseId/:vectorStoreId/notify')
@UseGuards(CourseRolesGuard)
@Roles(Role.PROFESSOR, Role.TA)
async notifyUpdatedAnswer(
@Param('courseId', ParseIntPipe) courseId: number,
@Param('vectorStoreId') vectorStoreId: string,
@Body() body: NotifyUpdatedChatbotAnswerParams,
@User({ courses: true }) user: UserModel,
): Promise<{
recipients: number;
totalRecipients: number;
unsubscribedRecipients: number;
}> {
Comment on lines +393 to +397
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turn this return type into an object NotifyUpdatedAnswerResponse and place it inside common/index.ts. Modify the notifyAnswerUpdate endpoint wrapper function inside app/api/index.ts to use this return type as well

return await this.chatbotService.notifyUpdatedAnswer(
courseId,
vectorStoreId,
body,
user,
);
}
// @Get('resetCourse/:courseId')
// @UseGuards(CourseRolesGuard)
// @Roles(Role.PROFESSOR, Role.TA)
Expand Down
8 changes: 7 additions & 1 deletion packages/server/src/chatbot/chatbot.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CacheModule } from '@nestjs/cache-manager';
import { ChatbotDataSourceService } from './chatbot-datasource/chatbot-datasource.service';
import { ChatbotDataSourceModule } from './chatbot-datasource/chatbot-datasource.module';
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
import { MailService } from 'mail/mail.service';

@Module({
controllers: [ChatbotController],
Expand All @@ -21,7 +22,12 @@ export class ChatbotModule {
CacheModule.register(),
ChatbotDataSourceModule.forRoot(connectionOptions),
],
providers: [ChatbotService, ChatbotApiService, ChatbotSettingsSubscriber],
providers: [
ChatbotService,
ChatbotApiService,
ChatbotSettingsSubscriber,
MailService,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huh interesting, i guess you didn't need to add MailModule to the imports array for some reason?

],
};
}
}
Loading