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
14 changes: 12 additions & 2 deletions packages/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ export interface SourceDocument {
courseId?: string
fromLMS?: boolean
apiDocId?: number
asyncQuestionId?: number // inserted async questions only
}
type?: string
// TODO: is it content or pageContent? since this file uses both. EDIT: It seems to be both/either. Gross.
Expand Down Expand Up @@ -490,6 +491,7 @@ export interface AddDocumentChunkParams {
name: string
type: string
source?: string
asyncQuestionId?: number
loc?: Loc
id?: string
courseId?: number
Expand Down Expand Up @@ -1397,6 +1399,14 @@ export class AsyncQuestionParams {
@IsOptional()
@IsInt()
votesSum?: number

@IsOptional()
@IsBoolean()
saveToChatbot?: boolean

@IsOptional()
@IsBoolean()
refreshAIAnswer?: boolean
}
export class AsyncQuestionVotes {
@IsOptional()
Expand Down Expand Up @@ -3964,6 +3974,8 @@ export interface ToolUsageExportData {
export const ERROR_MESSAGES = {
common: {
pageOutOfBounds: "Can't retrieve out of bounds page.",
noDiskSpace:
Copy link
Collaborator

Choose a reason for hiding this comment

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

hm, we might need to apply this across a few different routes in the future...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah for sure, I sprinkled it everywhere for the async question images, but it should probably also be added to like the chatbot document upload and stuff.

'There is not enough disk space left to store an image (<1GB). Please immediately contact your course staff and let them know. They will contact the HelpMe team as soon as possible.',
},
questionService: {
getDBClient: 'Error getting DB client',
Expand Down Expand Up @@ -4243,8 +4255,6 @@ export const ERROR_MESSAGES = {
noProfilePicture: "User doesn't have a profile picture",
noCoursesToDelete: "User doesn't have any courses to delete",
emailInUse: 'Email is already in use',
noDiskSpace:
'There is no disk space left to store an image. Please immediately contact your course staff and let them know. They will contact the HelpMe team as soon as possible.',
},
alertController: {
duplicateAlert: 'This alert has already been sent',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ const CourseCloneForm: React.FC<CourseCloneFormProps> = ({
valuePropName="checked"
label="Documents"
layout="horizontal"
tooltip="Clone the documents you uploaded to the chatbot. Note that after you clone these, you may want to review them and remove any that contain out-of-date information"
tooltip="Clone the documents you uploaded to the chatbot knowledge base. Note that after you clone these, you may want to review them and remove any that contain out-of-date information"
className={`${formItemClassNames}`}
>
<Checkbox />
Expand All @@ -289,7 +289,7 @@ const CourseCloneForm: React.FC<CourseCloneFormProps> = ({
valuePropName="checked"
label="Inserted Questions"
layout="horizontal"
tooltip="Clone over any chatbot questions that were inserted as a source into the chatbot."
tooltip="Clone over any Chatbot Questions and Anytime Questions that were inserted into the chatbot knowledge base."
className={`${formItemClassNames}`}
>
<Checkbox />
Expand All @@ -299,7 +299,7 @@ const CourseCloneForm: React.FC<CourseCloneFormProps> = ({
valuePropName="checked"
label="Inserted LMS Data"
layout="horizontal"
tooltip="Clone over any LMS data (e.g. assignment descriptions, announcements) that was inserted as a source into the chatbot. Defaulted to false since announcements usually have outdated information."
tooltip="Clone over any LMS data (e.g. assignment descriptions, announcements) that was inserted into the chatbot knowledge base. Defaulted to false since announcements usually have outdated information."
className={`${formItemClassNames}`}
>
<Checkbox />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const PostResponseModal: React.FC<PostResponseModalProps> = ({
useState<boolean>(false)
const courseFeatures = useCourseFeatures(courseId)
const authorCanSetVisible = courseFeatures?.asyncCentreAuthorPublic ?? false
const [saveToChatbot, setSaveToChatbot] = useState(true)

const [hasCheckedPopconfirm, setHasCheckedPopconfirm] =
useState<boolean>(!authorCanSetVisible)
Expand Down Expand Up @@ -88,6 +89,7 @@ const PostResponseModal: React.FC<PostResponseModalProps> = ({
staffSetVisible: staffSetVisible,
status: newStatus,
verified: values.verified,
saveToChatbot: saveToChatbot,
})
.then(() => {
message.success('Response Successfully Posted/Edited')
Expand All @@ -108,6 +110,17 @@ const PostResponseModal: React.FC<PostResponseModalProps> = ({
title="Post/Edit response to Student question"
okText="Finish"
cancelText="Cancel"
cancelButtonProps={{
className: 'md:w-24',
}}
width={{
xs: '100%',
sm: '100%',
md: '100%',
lg: '60%',
xl: '50%',
xxl: '35%',
}}
okButtonProps={{
autoFocus: true,
htmlType: 'submit',
Expand All @@ -129,11 +142,10 @@ const PostResponseModal: React.FC<PostResponseModalProps> = ({
onCancel={onCancel}
// display delete button for mobile in footer
footer={(_, { OkBtn, CancelBtn }) => (
<div className={'flex flex-col gap-1'}>
{question.creator.id == userInfo.id && (
<div className={'flex flex-col gap-1 md:hidden'}>
<Divider className={'text-gray-500'}>Actions</Divider>
<div className={'flex flex-row justify-between gap-1'}>
<div className={'flex justify-between gap-1'}>
<div className={'flex flex-col justify-center gap-2'}>
{question.creator.id === userInfo.id ? ( // special case where a TA created their own question
<>
<DeleteButton
question={question}
deleteLoading={deleteLoading}
Expand All @@ -149,51 +161,70 @@ const PostResponseModal: React.FC<PostResponseModalProps> = ({
onClick={() => setCreateAsyncQuestionModalOpen(true)}
>
{' '}
Edit
Edit Question
</Button>
)}
</div>
<Divider className={'text-gray-500'} orientation={'right'}>
Post Response
</Divider>
</div>
)}
<div className={'flex justify-between gap-2'}>
{question.creator.id != userInfo.id ? (
<div className={'w-min'}>
<DeleteButton
question={question}
deleteLoading={deleteLoading}
setDeleteLoading={setDeleteLoading}
deleteAsyncQuestion={deleteAsyncQuestion}
onPostResponse={onPostResponse}
/>
</div>
</>
) : (
<div></div>
// Standard case
<DeleteButton
question={question}
deleteLoading={deleteLoading}
setDeleteLoading={setDeleteLoading}
deleteAsyncQuestion={deleteAsyncQuestion}
onPostResponse={onPostResponse}
/>
)}
<div className="flex justify-end gap-2">
<CancelBtn />
<OkBtn />
<Popconfirm
className={'max-w-32 md:max-w-48'}
title="Are you sure you want to override visibility?"
description={
question.authorSetVisible
? 'The student who created this question wanted it to be visible to other students.'
: 'The student who created this question did not want for it to be visible to other students.'
<CancelBtn />
</div>
<div className="flex flex-col items-center justify-center gap-1 rounded-md bg-blue-100 p-1 px-2">
<OkBtn />
<Popconfirm
className={'max-w-32 md:max-w-48'}
title="Are you sure you want to override visibility?"
description={
question.authorSetVisible
? 'The student who created this question wanted it to be visible to other students.'
: 'The student who created this question did not want for it to be visible to other students.'
}
open={confirmPopoverOpen}
arrow={false}
okText="Yes"
cancelText="No"
onConfirm={() => {
onFinish().then()
setConfirmPopoverOpen(false)
}}
onCancel={() => setConfirmPopoverOpen(false)}
/>
<Checkbox
checked={saveToChatbot}
onChange={(e) => setSaveToChatbot(e.target.checked)}
// Antd checkboxes will automatically put its children into a span with some padding, so this targets it to get rid of the padding
className="[&>span]:!px-0 [&>span]:text-center"
>
<Tooltip
placement="bottom"
title={
<div className="flex flex-col gap-1">
<p>
Keeping this enabled will insert this Q&A into the
chatbot&apos;s knowledge base, allowing the chatbot to
reference it in future answers.
</p>
<p>
Please consider disabling this if the question contains
private information.
</p>
</div>
}
open={confirmPopoverOpen}
arrow={false}
okText="Yes"
cancelText="No"
onConfirm={() => {
onFinish().then()
setConfirmPopoverOpen(false)
}}
onCancel={() => setConfirmPopoverOpen(false)}
></Popconfirm>
</div>
>
<span className="pb-2 pl-1.5">
Save to Chatbot
<QuestionCircleOutlined className="ml-1 text-gray-500" />
</span>
</Tooltip>
</Checkbox>
</div>
</div>
)}
Expand Down Expand Up @@ -311,7 +342,7 @@ const DeleteButton: React.FC<DeleteButtonProps> = ({
return (
<Popconfirm
className={'inline-flex flex-auto md:hidden'}
title="Are you sure you want to delete the question?"
title="Are you sure you want to delete this question?"
okText="Yes"
cancelText="No"
getPopupContainer={(trigger) => trigger.parentNode as HTMLElement}
Expand Down
36 changes: 27 additions & 9 deletions packages/server/src/asyncQuestion/asyncQuestion.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { CourseRolesGuard } from 'guards/course-roles.guard';
import { AsyncQuestionRolesGuard } from 'guards/async-question-roles.guard';
import { pick } from 'lodash';
import { UserModel } from 'profile/user.entity';
import { Not } from 'typeorm';
import { In, Not } from 'typeorm';
import { ApplicationConfigService } from '../config/application_config.service';
import { AsyncQuestionService } from './asyncQuestion.service';
import { UnreadAsyncQuestionModel } from './unread-async-question.entity';
Expand Down Expand Up @@ -323,10 +323,10 @@ export class asyncQuestionController {
@Patch('faculty/:questionId')
@UseGuards(AsyncQuestionRolesGuard)
@Roles(Role.TA, Role.PROFESSOR)
async updateTAQuestion(
async updateQuestionStaff(
@Param('questionId', ParseIntPipe) questionId: number,
@Body() body: UpdateAsyncQuestions,
@UserId() userId: number,
@User({ chat_token: true }) user: UserModel,
): Promise<AsyncQuestionParams> {
const question = await AsyncQuestionModel.findOne({
where: { id: questionId },
Expand Down Expand Up @@ -354,7 +354,7 @@ export class asyncQuestionController {
// Verify if user is TA/PROF of the course
const requester = await UserCourseModel.findOne({
where: {
userId: userId,
userId: user.id,
courseId: courseId,
},
});
Expand All @@ -373,7 +373,7 @@ export class asyncQuestionController {

if (body.status === asyncQuestionStatus.HumanAnswered) {
question.closedAt = new Date();
question.taHelpedId = userId;
question.taHelpedId = user.id;
await this.asyncQuestionService.sendQuestionAnsweredEmails(question);
} else if (
body.status !== asyncQuestionStatus.TADeleted &&
Expand All @@ -390,6 +390,14 @@ export class asyncQuestionController {

const updatedQuestion = await question.save();

if (body.saveToChatbot) {
await this.asyncQuestionService.upsertQAToChatbotChunk(
updatedQuestion,
courseId,
user.chat_token.token,
);
}

// Mark as new unread for all students if the question is marked as visible
const courseSettings = await CourseSettingsModel.findOne({
where: { courseId: courseId },
Expand All @@ -406,7 +414,7 @@ export class asyncQuestionController {
await this.asyncQuestionService.markUnreadForRoles(
updatedQuestion,
[Role.STUDENT],
userId,
user.id,
);
}
// When the question creator gets their question human verified, notify them
Expand Down Expand Up @@ -760,11 +768,19 @@ export class asyncQuestionController {
let all: AsyncQuestionModel[];

if (!asyncQuestionKeys || Object.keys(asyncQuestionKeys).length === 0) {
console.log('Fetching from Database');
console.log(
`Fetching async questions from Database for courseId ${courseId}`,
);
all = await AsyncQuestionModel.find({
where: {
courseId,
status: Not(asyncQuestionStatus.StudentDeleted),
// don't include studentDeleted or TADeleted questions
status: Not(
In([
asyncQuestionStatus.StudentDeleted,
asyncQuestionStatus.TADeleted,
]),
),
Comment on lines +777 to +783
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oh yeah this was a thing, Apparently all this time TADeleted async questions were being fetched I guess

},
relations: [
'creator',
Expand All @@ -783,7 +799,9 @@ export class asyncQuestionController {
if (all)
await this.redisQueueService.setAsyncQuestions(`c:${courseId}:aq`, all);
} else {
console.log('Fetching from Redis');
console.log(
`Fetching async questions from Redis for courseId ${courseId}`,
);
all = Object.values(asyncQuestionKeys).map(
(question) => question as AsyncQuestionModel,
);
Expand Down
13 changes: 10 additions & 3 deletions packages/server/src/asyncQuestion/asyncQuestion.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,30 @@ import { MailModule, MailTestingModule } from 'mail/mail.module';
import { RedisQueueService } from '../redisQueue/redis-queue.service';
import { ApplicationConfigService } from '../config/application_config.service';
import { RedisQueueModule } from '../redisQueue/redis-queue.module';
import { ChatbotModule } from 'chatbot/chatbot.module';
import { ChatbotApiService } from 'chatbot/chatbot-api.service';

@Module({
controllers: [asyncQuestionController],
providers: [
AsyncQuestionService,
RedisQueueService,
ApplicationConfigService,
ChatbotApiService,
],
imports: [NotificationModule, MailModule, RedisQueueModule],
imports: [NotificationModule, MailModule, RedisQueueModule, ChatbotModule],
exports: [AsyncQuestionService],
})
export class asyncQuestionModule {}

@Module({
controllers: [asyncQuestionController],
providers: [AsyncQuestionService, ApplicationConfigService],
imports: [NotificationModule, MailTestingModule],
providers: [
AsyncQuestionService,
ChatbotApiService,
ApplicationConfigService,
],
imports: [NotificationModule, MailTestingModule, ChatbotModule],
exports: [AsyncQuestionService],
})
export class asyncQuestionTestingModule {}
Loading