Skip to content
Merged
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
5 changes: 5 additions & 0 deletions packages/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,11 @@ export type AsyncQuestion = {
votesSum: number
}

export type GetAsyncQuestionsResponse = {
questions: AsyncQuestion[]
hiddenPrivateQuestionsCount: number
}

/**
* An async question is created when a student wants help from a TA.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React, {
import {
Button,
Checkbox,
Empty,
Pagination,
Popover,
Segmented,
Expand Down Expand Up @@ -65,7 +66,11 @@ export default function AsyncCentrePage(

const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [asyncQuestions, mutateAsyncQuestions] = useAsyncQuestions(courseId)
const [asyncQuestionsResponse, mutateAsyncQuestions] =
useAsyncQuestions(courseId)
const asyncQuestions = asyncQuestionsResponse?.questions
const hiddenPrivateQuestionsCount =
asyncQuestionsResponse?.hiddenPrivateQuestionsCount ?? 0

const [createAsyncQuestionModalOpen, setCreateAsyncQuestionModalOpen] =
useState(false)
Expand Down Expand Up @@ -207,6 +212,8 @@ export default function AsyncCentrePage(
const displayedQuestions = useMemo(() => applySort, [applySort])

const totalQuestions = displayedQuestions.length // total length after all filters applied
const totalPages = Math.max(1, Math.ceil(totalQuestions / pageSize))
const isLastPage = page >= totalPages

// reset to page 1 whenever the filtered question count changes.
useEffect(() => {
Expand Down Expand Up @@ -344,7 +351,11 @@ export default function AsyncCentrePage(

if (!userInfo) {
return <CenteredSpinner tip="Loading User Info..." />
} else if (asyncQuestions === undefined || asyncQuestions === null) {
} else if (
asyncQuestionsResponse === undefined ||
asyncQuestionsResponse === null ||
asyncQuestions === undefined // should always be defined beyond this point, just for type safety
) {
return <CenteredSpinner tip="Loading Questions..." />
} else {
return (
Expand Down Expand Up @@ -450,25 +461,76 @@ export default function AsyncCentrePage(
showStudents={showStudents}
/>
))}

{asyncQuestions.length === 0 &&
hiddenPrivateQuestionsCount === 0 ? (
<div className="flex flex-grow items-center justify-center">
<Empty description="No questions have been posted here yet" />
</div>
) : (
asyncQuestions.length === 0 &&
hiddenPrivateQuestionsCount > 0 && (
<div className="flex flex-grow items-center justify-center">
<Empty
description={
<div className="text-center">
<p className="mb-1">
No public questions or questions you created found.
Try posting a course question!
</p>
{hiddenPrivateQuestionsCount > 0 && (
<Tooltip title="These are questions other students have asked that have not been made public by the Professor/TA (they're all private by default, meaning only the Professor/TA can see them)">
<p className="text-sm text-gray-500">
+{hiddenPrivateQuestionsCount} additional
private question
{hiddenPrivateQuestionsCount === 1 ? '' : 's'}
</p>
</Tooltip>
)}
</div>
}
/>
</div>
)
)}
{
// Show how many total private questions that the student can't see so that the student has
// a better way to know that this system is being used
// (and students usually ask questions to the most-popular system the prof set up)
!isStaff &&
hiddenPrivateQuestionsCount > 0 &&
paginatedQuestions.length > 0 &&
isLastPage && (
<Tooltip title="These are questions other students have asked that have not been made public by the Professor/TA (they're all private by default, meaning only the Professor/TA can see them)">
<p className="mt-1 self-center pl-2 text-sm text-gray-500">
+{hiddenPrivateQuestionsCount} additional private
question
{hiddenPrivateQuestionsCount === 1 ? '' : 's'}
</p>
</Tooltip>
)
}
</div>

<Pagination
current={page}
pageSize={pageSize}
total={totalQuestions}
onChange={(newPage, newPageSize) => {
setPage(newPage)
if (newPageSize !== pageSize) {
setPageSize(newPageSize)
setPage(1) // reset to page 1 when page size changes so you don't end up on a page that doesnt exist anymore
{totalQuestions > 0 && (
<Pagination
current={page}
pageSize={pageSize}
total={totalQuestions}
onChange={(newPage, newPageSize) => {
setPage(newPage)
if (newPageSize !== pageSize) {
setPageSize(newPageSize)
setPage(1) // reset to page 1 when page size changes so you don't end up on a page that doesnt exist anymore
}
}}
showSizeChanger
showTotal={(total, range) =>
`${range[0]}-${range[1]} of ${total} questions`
}
}}
showSizeChanger
showTotal={(total, range) =>
`${range[0]}-${range[1]} of ${total} questions`
}
className="mb-2 mt-4 text-center"
/>
className="mb-2 mt-4 text-center"
/>
)}
</div>
</div>
<ConvertChatbotQToAnytimeQModal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
import { API } from '@/app/api'
import MarkdownCustom from '@/app/components/Markdown'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { usePathname, useRouter } from 'next/navigation'
import {
Message,
parseThinkBlock,
Expand Down Expand Up @@ -99,6 +99,7 @@ const Chatbot: React.FC<ChatbotProps> = ({
const messagesEndRef = useRef<HTMLDivElement>(null)
const hasAskedQuestion = useRef(false) // to track if the user has asked a question
const pathname = usePathname()
const router = useRouter()
const currentPageTitle = convertPathnameToPageName(pathname)
const [popResetOpen, setPopResetOpen] = useState(false)
// used to temporarily store what question type the user is trying to change to
Expand Down Expand Up @@ -558,16 +559,31 @@ const Chatbot: React.FC<ChatbotProps> = ({
courseFeatures.asyncQueueEnabled &&
chatbotQuestionType === 'Course' &&
messages.length > 1 && (
<div>
Unhappy with your answer?{' '}
<Link
href={{
pathname: `/course/${cid}/async_centre`,
query: { convertChatbotQ: true },
<div className="text-sm">
Want to discuss or verify this with your Professor/TA?{' '}
<Popconfirm
title="Continue Navigation?"
getPopupContainer={(trigger) =>
trigger.parentNode as HTMLElement
}
onConfirm={() => {
router.push(
`/course/${cid}/async_centre?convertChatbotQ=true`,
)
}}
>
Convert to anytime question
</Link>
<Link
href={{
pathname: `/course/${cid}/async_centre`,
query: { convertChatbotQ: true },
}}
onNavigate={(e) => {
e.preventDefault()
}}
>
Convert to Anytime Question
</Link>
</Popconfirm>
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ const PromptStudentToLeaveQueueModal: React.FC<
icon={<ArrowRightLeft />}
size="large"
>
Convert to anytime question
Convert to Anytime Question
</Button>
</span>
</Tooltip>
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/app/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
AddChatbotQuestionParams,
AddDocumentChunkParams,
AllStudentAssignmentProgress,
AsyncQuestion,
AsyncQuestionComment,
AsyncQuestionCommentParams,
AsyncQuestionParams,
Expand Down Expand Up @@ -40,6 +39,7 @@ import {
EditCourseInfoParams,
ExtraTAStatus,
GetAlertsResponse,
GetAsyncQuestionsResponse,
GetAvailableModelsBody,
GetChatbotHistoryResponse,
GetCourseResponse,
Expand Down Expand Up @@ -808,7 +808,7 @@ export class APIClient {
),
}
asyncQuestions = {
get: async (cid: number): Promise<AsyncQuestion[]> =>
get: async (cid: number): Promise<GetAsyncQuestionsResponse> =>
this.req('GET', `/api/v1/asyncQuestions/${cid}`, undefined),
create: async (body: CreateAsyncQuestions, cid: number) =>
this.req(
Expand Down
10 changes: 6 additions & 4 deletions packages/frontend/app/hooks/useAsyncQuestions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { AsyncQuestion } from '@koh/common'
import { GetAsyncQuestionsResponse } from '@koh/common'
import useSWR from 'swr'
import { API } from '../api'

export function useAsyncQuestions(
cid: number,
): [
AsyncQuestion[] | undefined,
GetAsyncQuestionsResponse | undefined,
(
data?: AsyncQuestion[] | Promise<AsyncQuestion[]>,
data?:
| GetAsyncQuestionsResponse
| Promise<GetAsyncQuestionsResponse>,
shouldRevalidate?: boolean,
) => Promise<AsyncQuestion[] | undefined>,
) => Promise<GetAsyncQuestionsResponse | undefined>,
] {
const key = `/api/v1/courses/${cid}/asyncQuestions`

Expand Down
56 changes: 37 additions & 19 deletions packages/server/src/asyncQuestion/asyncQuestion.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
asyncQuestionStatus,
CreateAsyncQuestions,
ERROR_MESSAGES,
GetAsyncQuestionsResponse,
nameToRGB,
Role,
UnreadAsyncQuestionResponse,
Expand Down Expand Up @@ -743,7 +744,7 @@ export class asyncQuestionController {
async getAsyncQuestions(
@Param('courseId', ParseIntPipe) courseId: number,
@UserId() userId: number,
): Promise<AsyncQuestion[]> {
): Promise<GetAsyncQuestionsResponse> {
const userCourse = await UserCourseModel.findOne({
where: {
userId,
Expand Down Expand Up @@ -794,6 +795,7 @@ export class asyncQuestionController {
}

let questions: Partial<AsyncQuestionModel>[];
let hiddenPrivateQuestionsCount = 0;

const isStaff: boolean =
userCourse.role === Role.TA || userCourse.role === Role.PROFESSOR;
Expand All @@ -808,23 +810,36 @@ export class asyncQuestionController {
);
} else {
// Students see their own questions and questions that are visible
questions = (
await Promise.all(
all.map(async (question) => {
if (
question.creatorId === userId ||
(await this.asyncQuestionService.isVisible(
question,
courseSettings,
))
) {
return question;
} else {
return undefined;
}
}),
)
).filter((s) => s != undefined);
const visibilityResults = await Promise.all(
all.map(async (question) => {
if (question.creatorId === userId) {
return { question, isHiddenPrivate: false };
}

const isVisible = await this.asyncQuestionService.isVisible(
question,
courseSettings,
);

if (isVisible) {
return { question, isHiddenPrivate: false };
}

return {
question: undefined,
isHiddenPrivate:
question.status !== asyncQuestionStatus.TADeleted &&
question.status !== asyncQuestionStatus.StudentDeleted,
};
}),
);

questions = visibilityResults
.map((result) => result.question)
.filter((question): question is AsyncQuestionModel => !!question);
hiddenPrivateQuestionsCount = visibilityResults.filter(
(result) => result.isHiddenPrivate,
).length;
}

questions = questions.map((question: AsyncQuestionModel) => {
Expand Down Expand Up @@ -930,7 +945,10 @@ export class asyncQuestionController {
return temp;
});

return questions as unknown as AsyncQuestion[];
return {
questions: questions as unknown as AsyncQuestion[],
hiddenPrivateQuestionsCount,
};
}

// Moved from userInfo context endpoint as this updates too frequently to make sense caching it with userInfo data
Expand Down
Loading