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
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?"
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
},
): 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;
}> {
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
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