From 89832ff0e225d9d5d9d5bfa05f4a2599657574d7 Mon Sep 17 00:00:00 2001 From: bhunt02 Date: Tue, 14 Oct 2025 15:51:48 -0700 Subject: [PATCH 1/8] big old commit with all the frontend/backend changes i've made so far to the chatbot, will be written up later --- packages/common/chatbot-api-types.ts | 714 +++++++++++++++++ packages/common/index.ts | 335 +++----- packages/common/tsconfig.json | 2 +- .../components/CourseCloneForm.tsx | 14 +- .../components/CourseCloneFormModal.tsx | 8 +- .../components/ImageCropperModal.tsx | 56 +- .../components/DashboardPresetComponent.tsx | 1 + .../components/filters/QueueFilter.tsx | 3 +- .../components/filters/StaffFilter.tsx | 3 +- .../components/filters/StudentFilter.tsx | 3 +- .../EditChatbotDocumentChunkModal.tsx | 112 --- .../components/UpsertDocumentChunkModal.tsx | 195 +++++ .../settings/chatbot_knowledge_base/page.tsx | 379 +++++---- .../components/AddChatbotQuestionModal.tsx | 179 ----- .../components/EditChatbotQuestionModal.tsx | 457 ----------- .../components/UpsertChatbotQuestionModal.tsx | 429 +++++++++++ .../settings/chatbot_questions/page.tsx | 716 ++++++++---------- .../components/AddChatbotDocumentModal.tsx | 22 +- .../components/ChatbotSettingsModal.tsx | 58 +- .../components/EditChatbotDocumentModal.tsx | 142 ++++ .../components/LegacyChatbotSettingsModal.tsx | 10 +- .../settings/chatbot_settings/page.tsx | 298 +++++--- .../ChatbotDocumentContentTooltip.tsx | 39 + .../components/ChatbotListDocumentItem.tsx | 189 +++++ .../components/ChatbotSelectCitations.tsx | 201 +++++ .../settings/components/CustomPagination.tsx | 265 +++++++ .../components/QuizContentPreviewModal.tsx | 5 +- .../components/UpsertIntegrationModal.tsx | 1 + .../settings/lms_integrations/page.tsx | 4 +- .../course/[cid]/(settings)/settings/util.ts | 82 ++ .../modals/ConvertChatbotQToAnytimeQModal.tsx | 11 +- .../modals/ConvertQueueQToAnytimeQModal.tsx | 8 +- .../modals/CreateAsyncQuestionModal.tsx | 11 +- .../modals/EditAsyncCentreModal.tsx | 9 +- .../components/modals/PostResponseModal.tsx | 1 + .../course/[cid]/components/CheckInModal.tsx | 1 + .../[cid]/components/CreateQueueModal.tsx | 1 + .../[cid]/components/chatbot/Chatbot.tsx | 402 +++++----- .../components/chatbot/ChatbotCitation.tsx | 150 ++++ .../components/chatbot/ChatbotProvider.tsx | 36 +- .../app/(dashboard)/course/[cid]/page.tsx | 17 +- .../modals/AddStudentsToQueueModal.tsx | 6 +- .../modals/AssignmentReportModal.tsx | 3 +- .../[qid]/components/modals/CantFindModal.tsx | 1 + .../components/modals/CreateDemoModal.tsx | 1 + .../components/modals/CreateQuestionModal.tsx | 1 + .../components/modals/EditQueueModal.tsx | 27 +- .../modals/EventEndedCheckoutStaffModal.tsx | 1 + .../components/modals/JoinZoomNowModal.tsx | 1 + .../modals/PromptStudentToLeaveQueueModal.tsx | 3 +- .../modals/StudentRemovedFromQueueModal.tsx | 1 + .../modals/StudentRephraseModal.tsx | 1 + .../schedule/components/CreateEventModal.tsx | 70 +- .../schedule/components/EditEventModal.tsx | 55 +- .../ai/components/AddModelModal.tsx | 6 +- .../components/BatchCourseCloneModal.tsx | 11 +- .../organization/lms_integrations/page.tsx | 2 + .../components/DeleteConfirmationModal.tsx | 5 +- .../settings/components/SemesterModal.tsx | 3 +- .../profile/components/UserChatbotHistory.tsx | 28 +- packages/frontend/app/api/index.ts | 212 +++--- .../app/components/ChangeLogModal.tsx | 7 +- packages/frontend/app/globals.css | 45 +- packages/frontend/app/typings/chatbot.ts | 4 +- packages/frontend/app/utils/generalUtils.ts | 29 + packages/server/ormconfig.ts | 4 +- .../server/src/chatbot/chatbot-api.service.ts | 543 +++++++------ .../chatbot-provider.entity.ts | 7 +- .../chatbot-settings.subscriber.ts | 51 +- .../course-chatbot-settings.entity.ts | 4 +- .../llm-type.entity.ts | 4 +- .../organization-chatbot-settings.entity.ts | 4 +- ...n.entity.ts => chatbot-question.entity.ts} | 36 +- .../server/src/chatbot/chatbot.controller.ts | 485 ++++++------ .../src/chatbot/chatbot.service.spec.ts | 117 ++- .../server/src/chatbot/chatbot.service.ts | 395 ++++++++-- .../server/src/chatbot/interaction.entity.ts | 16 +- .../src/chatbot/questionDocument.entity.ts | 27 - .../server/src/course/course.service.spec.ts | 6 +- packages/server/src/course/course.service.ts | 26 +- .../server/src/insights/insight-objects.ts | 2 +- .../lmsIntegration/lmsIntegration.service.ts | 148 +--- packages/server/src/seed/seed.controller.ts | 2 +- packages/server/test/chatbot.integration.ts | 10 +- 84 files changed, 4916 insertions(+), 3063 deletions(-) create mode 100644 packages/common/chatbot-api-types.ts delete mode 100644 packages/frontend/app/(dashboard)/course/[cid]/(settings)/settings/chatbot_knowledge_base/components/EditChatbotDocumentChunkModal.tsx create mode 100644 packages/frontend/app/(dashboard)/course/[cid]/(settings)/settings/chatbot_knowledge_base/components/UpsertDocumentChunkModal.tsx delete mode 100644 packages/frontend/app/(dashboard)/course/[cid]/(settings)/settings/chatbot_questions/components/AddChatbotQuestionModal.tsx delete mode 100644 packages/frontend/app/(dashboard)/course/[cid]/(settings)/settings/chatbot_questions/components/EditChatbotQuestionModal.tsx create mode 100644 packages/frontend/app/(dashboard)/course/[cid]/(settings)/settings/chatbot_questions/components/UpsertChatbotQuestionModal.tsx create mode 100644 packages/frontend/app/(dashboard)/course/[cid]/(settings)/settings/chatbot_settings/components/EditChatbotDocumentModal.tsx create mode 100644 packages/frontend/app/(dashboard)/course/[cid]/(settings)/settings/components/ChatbotDocumentContentTooltip.tsx create mode 100644 packages/frontend/app/(dashboard)/course/[cid]/(settings)/settings/components/ChatbotListDocumentItem.tsx create mode 100644 packages/frontend/app/(dashboard)/course/[cid]/(settings)/settings/components/ChatbotSelectCitations.tsx create mode 100644 packages/frontend/app/(dashboard)/course/[cid]/(settings)/settings/components/CustomPagination.tsx create mode 100644 packages/frontend/app/(dashboard)/course/[cid]/(settings)/settings/util.ts create mode 100644 packages/frontend/app/(dashboard)/course/[cid]/components/chatbot/ChatbotCitation.tsx rename packages/server/src/chatbot/{question.entity.ts => chatbot-question.entity.ts} (62%) delete mode 100644 packages/server/src/chatbot/questionDocument.entity.ts diff --git a/packages/common/chatbot-api-types.ts b/packages/common/chatbot-api-types.ts new file mode 100644 index 000000000..5a191a660 --- /dev/null +++ b/packages/common/chatbot-api-types.ts @@ -0,0 +1,714 @@ +import { Transform, Type } from 'class-transformer' +import { + IsArray, + IsBoolean, + IsDate, + IsEnum, + IsInstance, + IsInt, + IsNumber, + IsObject, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator' + +/* KEEP UP TO DATE */ + +export enum DocumentType { + Inserted = 'inserted_document', + InsertedLMS = 'inserted_lms_document', + InsertedQuestion = 'inserted_question', + PDF = 'pdf', + PPTX = 'pptx', + DOCX = 'docx', + MD = 'md', + TXT = 'txt', + CSV = 'csv', + TSV = 'tsv', +} + +export const DocumentTypeDisplayMap = { + [DocumentType.Inserted]: 'Inserted', + [DocumentType.InsertedLMS]: 'LMS', + [DocumentType.InsertedQuestion]: 'Inserted Question', + [DocumentType.PDF]: 'PDF', + [DocumentType.PPTX]: 'PPTX', + [DocumentType.DOCX]: 'DOCX', + [DocumentType.MD]: 'MD', + [DocumentType.TXT]: 'TXT', + [DocumentType.CSV]: 'CSV', + [DocumentType.TSV]: 'TSV', +} + +export const DocumentTypeColorMap = { + [DocumentType.Inserted]: '#5E60CE', + [DocumentType.InsertedLMS]: '#4EA8DE', + [DocumentType.InsertedQuestion]: '#56CFE1', + [DocumentType.PDF]: '#E63946', + [DocumentType.PPTX]: '#F77F00', + [DocumentType.DOCX]: '#457B9D', + [DocumentType.MD]: '#CA00CA', + [DocumentType.TXT]: '#00D844', + [DocumentType.CSV]: '#FFB703', + [DocumentType.TSV]: '#8338EC', +} + +export enum ChatbotQueryTypeEnum { + DEFAULT = 'default', + ABSTRACT = 'abstract', +} + +export class ChatMessage { + @IsString() + type!: string + + @IsString() + message!: string +} + +export class ImageDescription { + @IsInt() + imageId!: number + + @IsString() + description!: string +} + +export class Citation { + @IsString() + docName!: string + + @IsEnum(DocumentType) + type!: DocumentType + + @IsString() + @IsOptional() + sourceLink?: string + + @IsObject() + @IsOptional() + confidences?: Record + + @IsArray() + @Type(() => Number) + @IsOptional() + pageNumbers?: number[] + + @IsString() + documentId!: string + + @IsString() + questionId!: string + + @IsString() + @IsOptional() + aggregateId?: string +} + +export class ChatbotAskBody { + @IsString() + question!: string + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ChatMessage) + history!: ChatMessage[] +} + +export class ChatbotAskResponse { + @IsString() + question!: string + + @IsString() + answer!: string + + @IsInt() + courseId!: number + + @IsString() + questionId!: string + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Citation) + citations!: Citation[] + + @IsBoolean() + verified!: boolean + + @IsBoolean() + isPreviousQuestion!: boolean + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ImageDescription) + imageDescriptions?: ImageDescription[] +} + +export class ChatbotQueryBody { + @IsString() + query!: string + + @IsEnum(ChatbotQueryTypeEnum) + type!: ChatbotQueryTypeEnum + + @IsObject() + @IsOptional() + params?: Record + + @IsInt() + @IsOptional() + courseId?: number +} + +export class ChatbotProviderResponse { + @IsString() + type!: string + + @IsOptional() + @IsString() + baseUrl!: string + + @IsString() + defaultModelName!: string + + @IsString() + defaultVisionModelName!: string + + @IsOptional() + @IsObject() + headers?: Record +} + +export class ChatbotModelResponse { + @IsInstance(ChatbotProviderResponse) + @ValidateNested() + @Type(() => ChatbotProviderResponse) + provider!: ChatbotProviderResponse + + @IsString() + modelName!: string +} + +export class ChatbotOrganizationSettings { + @IsInstance(ChatbotProviderResponse) + @ValidateNested() + @Type(() => ChatbotProviderResponse) + defaultProvider!: ChatbotProviderResponse +} + +export class ChatbotCourseSettingsProperties { + @IsOptional() + @IsInstance(ChatbotOrganizationSettings) + @ValidateNested() + organizationSettings?: ChatbotOrganizationSettings + + @IsOptional() + @IsInstance(ChatbotModelResponse) + @ValidateNested() + model?: ChatbotModelResponse + + @IsOptional() + @IsString() + modelName?: string + + @IsString() + prompt!: string + + @IsNumber() + similarityThresholdDocuments!: number + + @IsNumber() + similarityThresholdQuestions!: number + + @IsNumber() + temperature!: number + + @IsInt() + topK!: number +} + +export class CreateChatbotCourseSettingsBody extends ChatbotCourseSettingsProperties {} + +export class UpdateChatbotCourseSettingsBody { + @IsOptional() + @IsInstance(ChatbotOrganizationSettings) + @ValidateNested() + organizationSettings?: ChatbotOrganizationSettings + + @IsOptional() + @IsInstance(ChatbotModelResponse) + @ValidateNested() + model?: ChatbotModelResponse + + @IsOptional() + @IsString() + modelName?: string + + @IsOptional() + @IsString() + prompt?: string + + @IsOptional() + @IsNumber() + similarityThresholdDocuments?: number + + @IsOptional() + @IsNumber() + similarityThresholdQuestions?: number + + @IsOptional() + @IsNumber() + temperature?: number + + @IsOptional() + @IsInt() + topK?: number +} + +export class CreateDocumentAggregateBody { + @IsString() + title!: string + + @IsString() + source!: string + + @IsString() + documentText!: string + + @IsString() + @IsOptional() + lmsDocumentId?: string + + @IsString() + @IsOptional() + prefix?: string +} + +export class UpdateDocumentAggregateBody { + @IsString() + @IsOptional() + title?: string + + @IsString() + @IsOptional() + source?: string + + @IsString() + @IsOptional() + documentText?: string + + @IsString() + @IsOptional() + lmsDocumentId?: string + + @IsString() + @IsOptional() + prefix?: string +} + +export class UploadDocumentAggregateBody { + @IsString() + source!: string + + @Transform((params) => + params.value === 'true' + ? true + : params.value === 'false' + ? false + : undefined, + ) + @IsBoolean() + @IsOptional() + parseAsPng?: boolean + + @IsString() + @IsOptional() + lmsDocumentId?: string + + @IsString() + @IsOptional() + prefix?: string +} + +export class UploadURLDocumentAggregateBody { + @IsString() + url!: string + + @IsString() + @IsOptional() + source?: string + + @Transform((params) => + params.value === 'true' + ? true + : params.value === 'false' + ? false + : undefined, + ) + @IsBoolean() + @IsOptional() + parseAsPng?: boolean + + @IsString() + @IsOptional() + lmsDocumentId?: string + + @IsString() + @IsOptional() + prefix?: string +} + +export class CloneCourseDocumentsBody { + @IsOptional() + @IsBoolean() + includeDocuments?: boolean + + @IsOptional() + @IsBoolean() + includeInsertedQuestions?: boolean + @IsOptional() + @IsBoolean() + includeInsertedDocuments?: boolean + + @IsObject() + docIdMap!: Record +} + +export class CreateDocumentChunkBody { + @IsString() + content!: string + + @IsEnum(DocumentType) + type!: DocumentType + + @IsBoolean() + @IsOptional() + disabled?: boolean + + @IsString() + @IsOptional() + title?: string + + @IsString() + @IsOptional() + source?: string + + @IsArray({ each: true }) + @Type(() => Number) + @IsOptional() + lines?: [number, number] + + @IsInt() + @IsOptional() + pageNumber?: number + + @IsInt() + @IsOptional() + asyncQuestionId?: number + + @IsString() + @IsOptional() + aggregateId?: string + + @IsString() + @IsOptional() + questionId?: string + + @IsString() + @IsOptional() + prefix?: string +} + +export class UpdateDocumentChunkBody { + @IsString() + @IsOptional() + content?: string + + @IsEnum(DocumentType) + @IsOptional() + type?: DocumentType + + @IsBoolean() + @IsOptional() + disabled?: boolean + + @IsString() + @IsOptional() + title?: string + + @IsString() + @IsOptional() + source?: string + + @IsArray({ each: true }) + @Type(() => Number) + @IsOptional() + lines?: [number, number] + + @IsInt() + @IsOptional() + pageNumber?: number + + @IsInt() + @IsOptional() + asyncQuestionId?: number + + @IsString() + @IsOptional() + aggregateId?: string + + @IsString() + @IsOptional() + questionId?: string + + @IsString() + @IsOptional() + prefix?: string +} + +export class CreateQuestionBody { + @IsString() + question!: string + + @IsString() + answer!: string + + @IsArray() + @Type(() => String) + @IsOptional() + @ValidateNested({ each: true }) + sourceDocumentIds?: string[] + + @IsBoolean() + @IsOptional() + verified?: boolean + + @IsBoolean() + @IsOptional() + suggested?: boolean +} + +export class UpdateQuestionBody extends CreateQuestionBody { + @IsString() + @IsOptional() + question!: string + + @IsString() + @IsOptional() + answer!: string +} + +export class SuggestedQuestionResponse { + @IsString() + id!: string + + @IsInt() + courseId!: number + + @IsString() + question!: string + + @IsString() + answer!: string + + @IsDate() + askedAt!: Date + + @IsBoolean() + verified!: boolean + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Citation) + citations!: Citation[] +} + +export class ChatbotCourseSettingsResponse extends ChatbotCourseSettingsProperties { + @IsInt() + courseId!: number +} + +export class ChatbotDocumentAggregateResponse { + @IsString() + id!: string + + @IsInt() + courseId!: number + + @IsString() + title!: string + + @IsEnum(DocumentType) + type!: DocumentType + + @IsString() + source!: string + + @IsString() + @IsOptional() + lmsDocumentId?: string + + @IsArray() + @Type(() => ChatbotDocumentResponse) + @ValidateNested({ each: true }) + subDocuments!: ChatbotDocumentResponse[] +} + +export class ChatbotQuestionResponse { + @IsString() + id!: string + + @IsInt() + courseId!: number + + @IsString() + question!: string + + @IsString() + answer!: string + + @IsDate() + @Transform((params) => new Date(params.value)) + askedAt!: Date + + @IsBoolean() + inserted!: boolean + + @IsBoolean() + suggested!: boolean + + @IsBoolean() + verified!: boolean + + @IsArray() + @Type(() => ChatbotDocumentResponse) + insertedDocuments!: ChatbotDocumentResponse[] + + @IsArray() + @Type(() => ChatbotCitationResponse) + @ValidateNested({ each: true }) + citations!: ChatbotCitationResponse[] +} + +export class ChatbotDocumentResponse { + @IsString() + id!: string + + @IsInt() + courseId!: number + + @IsString() + content!: string + + @IsEnum(DocumentType) + type!: DocumentType + + @IsBoolean() + disabled!: boolean + + @IsString() + @IsOptional() + title?: string + + @IsString() + @IsOptional() + source?: string + + @IsArray() + @Type(() => Number) + @ValidateNested({ each: true }) + lines?: [number, number] + + @IsInt() + @IsOptional() + pageNumber?: number + + @IsInt() + @IsOptional() + asyncQuestionId?: number + + @IsDate() + @Transform((params) => new Date(params.value)) + firstInsertedAt!: Date + + @IsArray() + @Type(() => ChatbotDocumentQueryResponse) + @ValidateNested({ each: true }) + queries!: ChatbotDocumentQueryResponse[] + + @IsArray() + @Type(() => ChatbotCitationResponse) + @ValidateNested({ each: true }) + citations!: ChatbotCitationResponse[] + + @IsString() + @IsOptional() + aggregateId?: string + + @IsString() + @IsOptional() + questionId?: string + + @IsInstance(ChatbotQuestionResponse) + @IsOptional() + parentQuestion?: ChatbotQuestionResponse + + @IsInstance(ChatbotDocumentAggregateResponse) + @IsOptional() + aggregate?: ChatbotDocumentAggregateResponse +} + +export class ChatbotDocumentQueryResponse { + @IsString() + id!: string + + @IsString() + documentId!: string + + @IsString() + query!: string + + @IsInstance(ChatbotDocumentResponse) + document!: ChatbotDocumentResponse +} + +export class ChatbotCitationResponse { + @IsString() + documentId!: string + + @IsString() + questionId!: string + + @IsDate() + citedAt!: Date + + @IsNumber() + @IsOptional() + confidence?: number + + @IsInstance(ChatbotQuestionResponse) + question!: ChatbotQuestionResponse + + @IsInstance(ChatbotDocumentResponse) + document!: ChatbotDocumentResponse +} + +export class ChatbotDocumentListResponse { + @IsString() + @IsOptional() + aggregateId?: string + + @IsString() + type!: 'aggregate' | 'chunk' + + @IsString() + title!: string + + @IsArray() + @Type(() => ChatbotDocumentResponse) + @ValidateNested({ each: true }) + documents!: ChatbotDocumentResponse[] +} diff --git a/packages/common/index.ts b/packages/common/index.ts index f39b4a0ee..14a57af58 100644 --- a/packages/common/index.ts +++ b/packages/common/index.ts @@ -1,4 +1,4 @@ -import { Exclude, Type } from 'class-transformer' +import { Exclude, Expose, Type } from 'class-transformer' import { IsArray, IsBoolean, @@ -19,6 +19,15 @@ import { import 'reflect-metadata' import { Cache } from 'cache-manager' import { Ajv } from 'ajv' +import { + ChatbotAskBody, + ChatbotAskResponse, + ChatbotQuestionResponse, + ChatMessage, + Citation, +} from './chatbot-api-types' + +export * from './chatbot-api-types' export const PROD_URL = 'https://coursehelp.ubc.ca' @@ -215,9 +224,8 @@ export type CourseCloneAttributes = { chatbot?: { settings?: boolean documents?: boolean - manuallyCreatedChunks?: boolean insertedQuestions?: boolean - insertedLMSData?: boolean + insertedDocuments?: boolean } } } @@ -238,9 +246,8 @@ export const defaultCourseCloneAttributes: CourseCloneAttributes = { chatbot: { settings: true, documents: true, - manuallyCreatedChunks: true, insertedQuestions: true, - insertedLMSData: false, + insertedDocuments: false, }, }, } @@ -334,175 +341,119 @@ export enum AccountType { // chatbot questions and interactions -export interface UpdateDocumentChunkParams { - documentText: string - metadata: { - name: string - source: string - } -} - -// comes from helpme db -export interface ChatbotQuestionResponseHelpMeDB { - id: number - vectorStoreId: string - interactionId: number - questionText: string - responseText: string - timestamp: Date - userScore: number - suggested: boolean - isPreviousQuestion: boolean - correspondingChatbotQuestion?: ChatbotQuestionResponseChatbotDB // used by chatbot_questions page on frontend - timesAsked?: number // same as above -} - -// comes from chatbot db -export interface ChatbotQuestionResponseChatbotDB { - id: string - pageContent: string // this is the question - metadata: { - answer: string - timestamp?: string // i found a chatbot question without a timestamp 😭 - courseId: string - verified: boolean - sourceDocuments: SourceDocument[] - suggested: boolean - inserted?: boolean - } - userScoreTotal?: number // NOT returned from db, it's calculated and used by chatbot_questions page on frontend - timesAsked?: number // same as above - interactionsWithThisQuestion?: InteractionResponse[] // same as above - mostRecentlyAskedHelpMeVersion?: ChatbotQuestionResponseHelpMeDB | null // same as above -} +export class PaginatedResponse { + @IsInt() + total!: number -interface Loc { - pageNumber: number + @IsArray() + items!: T[] } -// source document return type (from chatbot db) -export interface SourceDocument { - id?: string - metadata?: { - loc?: Loc - name: string - type?: string - source?: string - courseId?: string - fromLMS?: boolean - apiDocId?: number - } - type?: string - // TODO: is it content or pageContent? since this file uses both. EDIT: It seems to be both/either. Gross. - content?: string - pageContent: string - docName: string - docId?: string // no idea if this exists in the actual data EDIT: yes it does, sometimes - pageNumbers?: number[] // same with this, but this might only be for the edit question modal - pageNumbersString?: string // used only for the edit question modal - sourceLink?: string - pageNumber?: number - key?: string // used for front-end rendering -} +export class HelpMeChatMessage extends ChatMessage { + @IsBoolean() + @IsOptional() + verified?: boolean -export interface PreDeterminedQuestion { - id: string - pageContent: string - metadata: { - answer: string - courseId: string - inserted: boolean - sourceDocuments: SourceDocument[] - suggested: boolean - verified: boolean - } -} + @IsArray() + @IsOptional() + @Type(() => Citation) + @ValidateNested({ each: true }) + citations?: Citation[] -export interface Message { - type: 'apiMessage' | 'userMessage' - message: string | void - verified?: boolean - sourceDocuments?: SourceDocument[] + @IsString() + @IsOptional() questionId?: string - thinkText?: string | null // used on frontend only -} -export interface ChatbotQueryParams { - query: string - type: 'default' | 'abstract' + @IsString() + @IsOptional() + thinkText?: string // used on frontend only } -export interface ChatbotAskParams { - question: string - history: Message[] +export class HelpMeChatbotAskBody extends ChatbotAskBody { + @IsString() + question!: string + + @IsArray() + @Type(() => HelpMeChatMessage) + @ValidateNested({ each: true }) + history!: HelpMeChatMessage[] + + @IsInt() + @IsOptional() interactionId?: number - onlySaveInChatbotDB?: boolean + + @IsBoolean() + @IsOptional() + save?: boolean } -export interface ChatbotAskSuggestedParams { - question: string - responseText: string - vectorStoreId: string +export class ChatbotAskSuggestedBody { + @IsString() + vectorStoreId!: string } -export interface AddDocumentChunkParams { - documentText: string - metadata: { - name: string - type: string - source?: string - loc?: Loc - id?: string - courseId?: number - } - prefix?: string +export class HelpMeChatbotQuestionResponse { + @IsInt() + id!: number + + @IsString() + vectorStoreId!: string + + @IsInt() + interactionId!: number + + @IsInt() + userScore!: number + + @IsBoolean() + isPreviousQuestion!: boolean + + @IsDate() + @IsOptional() + timestamp?: Date + + @IsInstance(ChatbotQuestionResponse) + @IsOptional() + chatbotQuestion?: ChatbotQuestionResponse } -export interface AddDocumentAggregateParams { - name: string - source: string - documentText: string - metadata?: any - prefix?: string +export class HelpMeChatbotQuestionTableResponse extends HelpMeChatbotQuestionResponse { + @IsArray() + @ValidateNested({ each: true }) + @IsOptional() + children?: HelpMeChatbotQuestionTableResponse[] + + @IsBoolean() + @IsOptional() + isChild?: boolean + + @IsInt() + @IsOptional() + userScoreTotal?: number + + @IsInt() + @IsOptional() + timesAsked?: number } -export interface UpdateDocumentAggregateParams { - documentText: string - metadata?: any - prefix?: string +export class HelpMeChatbotAskResponse extends ChatbotAskResponse { + internal!: Omit } -export interface UpdateChatbotQuestionParams { - id: string - inserted?: boolean - sourceDocuments?: SourceDocument[] - question?: string - answer?: string - verified?: boolean - suggested?: boolean - selectedDocuments?: { - docId: string - pageNumbersString: 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 { - chatbotRepoVersion: ChatbotAskResponseChatbotDB - helpmeRepoVersion: ChatbotQuestionResponseHelpMeDB | null -} - -// comes from /ask from chatbot db -export interface ChatbotAskResponseChatbotDB { - question: string - answer: string - questionId: string - interactionId: number - sourceDocuments?: SourceDocument[] - verified: boolean - courseId: string - isPreviousQuestion: boolean +export class InteractionResponse { + @Expose() + @IsInt() + id!: number + + @Expose() + @IsDate() + timestamp!: Date + + @IsArray() + @Type(() => HelpMeChatbotQuestionResponse) + @ValidateNested({ each: true }) + @IsOptional() + questions?: HelpMeChatbotQuestionResponse[] } export enum ChatbotServiceType { @@ -510,20 +461,10 @@ export enum ChatbotServiceType { LATEST = 'latest', } -export interface AddChatbotQuestionParams { - question: string - answer: string - verified: boolean - suggested: boolean - sourceDocuments: SourceDocument[] -} - export interface OrganizationChatbotSettings { id: number - defaultProvider: ChatbotProvider providers: ChatbotProvider[] - default_prompt?: string default_temperature?: number default_topK?: number @@ -880,41 +821,6 @@ export class UpdateLLMTypeBody { additionalNotes?: string[] } -export interface ChatbotSettings { - id: string - pageContent: string - metadata: ChatbotSettingsMetadata -} - -export interface ProviderMetadata { - type: ChatbotServiceProvider - baseUrl: string - apiKey: string - defaultModelName: string - defaultVisionModelName: string - headers: ChatbotAllowedHeaders -} - -export interface ModelMetadata { - provider: ProviderMetadata - modelName: string -} - -export interface OrganizationChatbotSettingsMetadata { - defaultProvider: ProviderMetadata -} - -export interface ChatbotSettingsMetadata { - organizationSettings?: OrganizationChatbotSettingsMetadata - model?: ModelMetadata - modelName?: string - prompt: string - similarityThresholdDocuments: number - similarityThresholdQuestions: number - temperature: number - topK: number -} - export class UpsertCourseChatbotSettings { @IsInt() @IsOptional() @@ -937,30 +843,6 @@ export class UpsertCourseChatbotSettings { topK?: number } -export type ChatbotSettingsUpdateParams = Partial - -export interface InteractionResponse { - id: number - timestamp: Date - questions?: ChatbotQuestionResponseHelpMeDB[] -} - -export class ChatbotDocument { - id!: number - name!: number - type!: string - subDocumentIds!: string[] -} - -export type GetInteractionsAndQuestionsResponse = { - helpmeDB: InteractionResponse[] - chatbotDB: ChatbotQuestionResponseChatbotDB[] -} - -export type GetChatbotHistoryResponse = { - history: InteractionResponse[] -} - /** * A Queue that students can join with their tickets. * @param id - The unique id number for a Queue. @@ -3498,7 +3380,7 @@ export function parseThinkBlock(answer: string) { if (!match) { // No block, return the text unchanged - return { thinkText: null, cleanAnswer: answer } + return { thinkText: undefined, cleanAnswer: answer } } const thinkText = match[1].trim() @@ -3560,6 +3442,8 @@ export const ERROR_MESSAGES = { 'Organization settings could not be created; organization not found.', }, chatbotController: { + textFileTooBig: + 'Text-only files (.txt, .csv, .md) must be less than 2 MB in size.', organizationSettingsAlreadyExists: 'Chatbot settings for this organization already exists.', organizationSettingsNotFound: @@ -3575,6 +3459,9 @@ export const ERROR_MESSAGES = { `Specified chatbot provider is not an ${provider} provider.`, }, chatbotService: { + missingVectorStoreId: + 'Cannot create question, corresponding vector store ID for question not specified', + interactionNotFound: 'Interaction with specified ID was not found.', defaultModelNotFound: 'Specified default model was not found in list of models', courseSettingsNotFound: diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json index 4fe490521..b45b4e4c8 100644 --- a/packages/common/tsconfig.json +++ b/packages/common/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.json", - "include": ["index.ts"] + "include": ["index.ts","chatbot-api-types.ts"] } diff --git a/packages/frontend/app/(dashboard)/components/CourseCloneForm.tsx b/packages/frontend/app/(dashboard)/components/CourseCloneForm.tsx index ae98bbe70..aa46b7ebb 100644 --- a/packages/frontend/app/(dashboard)/components/CourseCloneForm.tsx +++ b/packages/frontend/app/(dashboard)/components/CourseCloneForm.tsx @@ -275,9 +275,9 @@ const CourseCloneForm: React.FC = ({ = ({ > - - - ) diff --git a/packages/frontend/app/(dashboard)/components/CourseCloneFormModal.tsx b/packages/frontend/app/(dashboard)/components/CourseCloneFormModal.tsx index ce9246f22..a2c6e6613 100644 --- a/packages/frontend/app/(dashboard)/components/CourseCloneFormModal.tsx +++ b/packages/frontend/app/(dashboard)/components/CourseCloneFormModal.tsx @@ -100,6 +100,7 @@ const CourseCloneFormModal: React.FC = ({ = ({ form.getFieldValue([ 'toClone', 'chatbot', - 'manuallyCreatedChunks', + 'insertedDocuments', ]) || form.getFieldValue([ 'toClone', 'chatbot', 'insertedQuestions', - ]) || - form.getFieldValue([ - 'toClone', - 'chatbot', - 'insertedLMSData', ])) && (

Note that you may want to review and remove any out-of-date diff --git a/packages/frontend/app/(dashboard)/components/ImageCropperModal.tsx b/packages/frontend/app/(dashboard)/components/ImageCropperModal.tsx index 25f8e8918..73b170c4c 100644 --- a/packages/frontend/app/(dashboard)/components/ImageCropperModal.tsx +++ b/packages/frontend/app/(dashboard)/components/ImageCropperModal.tsx @@ -1,6 +1,6 @@ 'use client' -import { Button, Modal, Upload, message } from 'antd' +import { Button, message, Modal, Upload } from 'antd' import { UploadOutlined } from '@ant-design/icons' import React, { useCallback, useState } from 'react' import Cropper from 'react-easy-crop' @@ -128,6 +128,7 @@ const ImageCropperModal: React.FC = ({ return ( = ({ xl: '70%', xxl: '65%', }} - className="flex flex-col items-center justify-center" > - await handleUpload(file)} - beforeUpload={beforeUpload} - className="mb-2" - showUploadList={false} - maxCount={1} - accept=".jpg,.jpeg,.png,.webp,.avif,.gif,.svg,.tiff" - > - - -

- +
+ await handleUpload(file)} + beforeUpload={beforeUpload} + className="mb-2" + showUploadList={false} + maxCount={1} + accept=".jpg,.jpeg,.png,.webp,.avif,.gif,.svg,.tiff" + > + + +
+ +
) diff --git a/packages/frontend/app/(dashboard)/course/[cid]/(insights)/components/DashboardPresetComponent.tsx b/packages/frontend/app/(dashboard)/course/[cid]/(insights)/components/DashboardPresetComponent.tsx index b96187a9e..069cee2c5 100644 --- a/packages/frontend/app/(dashboard)/course/[cid]/(insights)/components/DashboardPresetComponent.tsx +++ b/packages/frontend/app/(dashboard)/course/[cid]/(insights)/components/DashboardPresetComponent.tsx @@ -139,6 +139,7 @@ const DashboardPresetComponent: React.FC = ({ <> {insightsList != undefined && ( = ({ queueDetails != undefined && ( <> = ({ staffDetails != undefined && ( <> = ({ studentDetails != undefined && ( <> void - onCancel: () => void -} - -const EditDocumentChunkModal: React.FC = ({ - editingRecord, - open, - courseId, - onSuccessfulUpdate, - onCancel, -}): ReactElement => { - const [form] = Form.useForm() - - const onFinish = async (values: FormValues) => { - await API.chatbot.staffOnly - .updateDocumentChunk(courseId, editingRecord.id || '', { - documentText: values.content, - metadata: { - name: values.documentName, - source: values.source, - }, - }) - .then((updatedDocs) => { - message.success('Document updated successfully.') - onSuccessfulUpdate(updatedDocs) - }) - .catch((e) => { - const errorMessage = getErrorMessage(e) - message.error('Failed to update document: ' + errorMessage) - }) - } - - return ( - -
Edit Document Chunk
-
- } - okText="Save Changes" - cancelText="Cancel" - okButtonProps={{ - autoFocus: true, - htmlType: 'submit', - }} - onCancel={onCancel} - width={800} - destroyOnHidden - modalRender={(dom) => ( -
onFinish(values)} - > - {dom} -
- )} - > - - - - -