Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
91908f5
frontend for SaveToChatbot for anytime questions. Also re-arranged so…
AdamFipke Apr 9, 2025
aec67c4
added backend for upserting async questions as chatbot doc chunks. Al…
AdamFipke Apr 9, 2025
c460f50
hovering over chatbot source links now shows the chunk content
AdamFipke Apr 9, 2025
6a600d4
improved file interceptors for upload pfp endpoint
AdamFipke Apr 10, 2025
72c82f3
added a space check for chatbot document upload
AdamFipke Apr 12, 2025
58b72c0
can now upload images when creating async questions. Also moved the c…
AdamFipke Apr 12, 2025
3b126c4
now displays async question images. Also made it so async questions s…
AdamFipke Apr 12, 2025
914ca5a
Merge branch 'getUser-better-error-checking' into 149-async-question-…
AdamFipke Apr 14, 2025
fa471f1
now saves generated ai descriptions of uploaded images and turns them…
AdamFipke Apr 15, 2025
ef037df
move tags for asyncquestioncard to be above answer rather than below …
AdamFipke Apr 15, 2025
b665bc6
Merge branch 'main' into 149-async-question-imageattachments-support
AdamFipke Apr 15, 2025
11e1df7
Merge branch 'main' into 149-async-question-imageattachments-support
AdamFipke Apr 15, 2025
b960911
store ai image descriptions in async question chatbot chunk
AdamFipke Apr 15, 2025
7fd05f5
finally fixed issue with AsyncQuestionCards expanding to take the who…
AdamFipke Apr 15, 2025
aae323f
can now edit the images of async questions. It will only re-upload an…
AdamFipke Apr 15, 2025
cbbc657
fixed issue with image descriptions not getting updated in redis afte…
AdamFipke Apr 15, 2025
4ffa626
got async image update (deleting old images, not changing others, and…
AdamFipke Apr 15, 2025
272c993
fix issue with importing sharp and checkDiskSpace
AdamFipke Apr 15, 2025
53d7569
added ability for users to clear their own profile cache (clearing bo…
AdamFipke Apr 15, 2025
c93dca6
fixed issue with course preference table overflowing on mobile due to…
AdamFipke Apr 15, 2025
99057ab
added async question source document support (I called them 'citation…
AdamFipke Apr 16, 2025
fc95c75
added ability for staff to delete the source document citations off o…
AdamFipke Apr 16, 2025
bb04a3e
added ability to paste images inside of the question text to upload
AdamFipke Apr 16, 2025
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
89 changes: 61 additions & 28 deletions packages/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,16 +298,25 @@ interface Loc {
pageNumber: number
}

export interface ChatbotChunkMetadata {
name: string // name when cited (e.g. "L2 Slides")
type: string // "inserted_question", "inserted_async_question", etc.
source?: string // source url
loc?: Loc
id?: string
courseId?: number
// below only added in chunks from April 2025 onwards
firstInsertedAt?: Date
lastUpdatedAt?: Date
shouldProbablyKeepWhenCloning?: boolean
asyncQuestionId?: number // inserted async questions only
}

// source document return type (from chatbot db)
// Actually this is kinda messy. It's supposed to just be a chunk, not a full document, but it's also used for the full documents on the frontend. TODO: maybe refactor that a little
export interface SourceDocument {
id?: string
metadata?: {
loc?: Loc
name: string
type?: string
source?: string
courseId?: string
}
metadata?: ChatbotChunkMetadata
type?: string
// TODO: is it content or pageContent? since this file uses both. EDIT: It seems to be both/either. Gross.
content?: string
Expand Down Expand Up @@ -358,14 +367,7 @@ export interface ChatbotAskSuggestedParams {

export interface AddDocumentChunkParams {
documentText: string
metadata: {
name: string
type: string
source?: string
loc?: Loc
id?: string
courseId?: number
}
metadata: ChatbotChunkMetadata
}

export interface UpdateChatbotQuestionParams {
Expand Down Expand Up @@ -399,6 +401,10 @@ export interface ChatbotAskResponseChatbotDB {
verified: boolean
courseId: string
isPreviousQuestion: boolean
imageDescriptions?: {
imageId: string // should be a number but I don't trust it
description: string
}[]
}

export interface AddChatbotQuestionParams {
Expand Down Expand Up @@ -786,6 +792,17 @@ export type AsyncQuestion = {
votes?: AsyncQuestionVotes[]
comments: AsyncQuestionComment[]
votesSum: number
images: AsyncQuestionImage[]
citations: SourceDocument[]
}

export type AsyncQuestionImage = {
imageId: number
originalFileName: string
newFileName: string
imageSizeBytes: number
previewImageSizeBytes: number
aiSummary: string
}

/**
Expand Down Expand Up @@ -828,10 +845,6 @@ export class AsyncQuestionParams {
@IsString()
answerText?: string

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

@Type(() => Date)
closedAt?: Date

Expand All @@ -848,6 +861,14 @@ export class AsyncQuestionParams {
@IsOptional()
@IsInt()
votesSum?: number

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

@IsOptional()
@IsBoolean()
refreshAIAnswer?: boolean
}
export class AsyncQuestionVotes {
@IsOptional()
Expand Down Expand Up @@ -1536,9 +1557,26 @@ export class ResolveGroupParams {
queueId!: number
}

export class CreateAsyncQuestions extends AsyncQuestionParams {}
export class CreateAsyncQuestions extends AsyncQuestionParams {
@IsOptional()
@IsArray()
images?: any // This will be handled by FormData, so we don't need to specify the type here
}

export class UpdateAsyncQuestions extends AsyncQuestionParams {}
export class UpdateAsyncQuestions extends AsyncQuestionParams {
@IsOptional()
@IsArray()
newImages?: any // This will be handled by FormData, so we don't need to specify the type here

@IsOptional()
@IsArray()
deletedImageIds?: number[]

// used with staff to delete citations when posting a response
@IsOptional()
@IsBoolean()
deleteCitations?: boolean
}

export type TAUpdateStatusResponse = QueuePartial
export type QueueNotePayloadType = {
Expand Down Expand Up @@ -2731,6 +2769,8 @@ export enum LMSIntegrationPlatform {
export const ERROR_MESSAGES = {
common: {
pageOutOfBounds: "Can't retrieve out of bounds page.",
noDiskSpace:
'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 @@ -2946,8 +2986,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 Khoury Office Hours team as soon as possible.',
},
alertController: {
duplicateAlert: 'This alert has already been sent',
Expand All @@ -2970,11 +3008,6 @@ export const ERROR_MESSAGES = {
publish: 'Publisher client is unable to publish',
clientIdNotFound: 'Client ID not found during subscribing to client',
},
resourcesService: {
noDiskSpace:
'There is no disk space left to store a iCal file. Please immediately contact your course staff and let them know. They will contact the Khoury Office Hours team as soon as possible.',
saveCalError: 'There was an error saving an iCal to disk',
},
questionType: {
questionTypeNotFound: 'Question type not found',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ const EditCourseForm: React.FC<EditCourseFormProps> = ({
<Form.Item
label="Coordinator Email"
name="coordinatorEmail"
tooltip="Email of the coordinator of the course"
tooltip="Email of the coordinator/instructor of the course"
className="flex-1"
>
<Input allowClear={true} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,22 @@ import EditDocumentChunkModal from './components/EditChatbotDocumentChunkModal'
import { AddDocumentChunkParams, SourceDocument } from '@koh/common'
import { API } from '@/app/api'
import ChunkHelpTooltip from './components/ChunkHelpTooltip'
import { formatDateAndTimeForExcel } from '@/app/utils/timeFormatUtils'

interface FormValues {
content: string
name: string
source: string
pageNumber: string
}

interface ChatbotDocumentsProps {
interface ChatbotDocumentChunksProps {
params: { cid: string }
}

export default function ChatbotDocuments({
export default function ChatbotDocumentChunks({
params,
}: ChatbotDocumentsProps): ReactElement {
}: ChatbotDocumentChunksProps): ReactElement {
const courseId = Number(params.cid)
const [documents, setDocuments] = useState<SourceDocument[]>([])
const [filteredDocuments, setFilteredDocuments] = useState<SourceDocument[]>(
Expand All @@ -36,17 +38,23 @@ export default function ChatbotDocuments({
const [editRecordModalOpen, setEditRecordModalOpen] = useState(false)
const [form] = Form.useForm()
const [addDocChunkPopupVisible, setAddDocChunkPopupVisible] = useState(false)
const [dataLoading, setDataLoading] = useState(false)

const addDocument = async (values: FormValues) => {
const now = new Date()
const body: AddDocumentChunkParams = {
documentText: values.content,
metadata: {
name: 'Manually Inserted Information',
name: values.name ?? 'Manually Inserted Info',
type: 'inserted_document',
source: values.source ?? undefined,
loc: values.pageNumber
? { pageNumber: parseInt(values.pageNumber) }
: undefined,
shouldProbablyKeepWhenCloning: true,
courseId: courseId,
firstInsertedAt: now,
lastUpdatedAt: now,
},
}
await API.chatbot.staffOnly
Expand All @@ -62,6 +70,7 @@ export default function ChatbotDocuments({
}

const fetchDocuments = useCallback(async () => {
setDataLoading(true)
await API.chatbot.staffOnly
.getAllDocumentChunks(courseId)
.then((response) => {
Expand All @@ -76,15 +85,18 @@ export default function ChatbotDocuments({
const errorMessage = getErrorMessage(e)
message.error('Failed to load documents: ' + errorMessage)
})
}, [courseId, setDocuments, setFilteredDocuments])
.finally(() => {
setDataLoading(false)
})
}, [courseId, setDocuments, setFilteredDocuments, setDataLoading])

useEffect(() => {
if (courseId) {
fetchDocuments()
}
}, [courseId, fetchDocuments])

const columns = [
const columns: any[] = [
{
title: 'Name',
dataIndex: ['metadata', 'name'],
Expand All @@ -105,7 +117,12 @@ export default function ChatbotDocuments({
prefetch={false}
rel="noopener noreferrer"
>
{text}
<Highlighter
highlightStyle={{ backgroundColor: '#ffc069', padding: 0 }}
searchWords={[search]}
autoEscape
textToHighlight={text ? text.toString() : ''}
/>
</Link>
</ExpandableText>
),
Expand All @@ -131,6 +148,24 @@ export default function ChatbotDocuments({
key: 'pageNumber',
width: 40,
},
{
title: 'Created At',
dataIndex: ['metadata', 'firstInsertedAt'],
key: 'firstInsertedAt',
defaultSortOrder: 'descend',
width: 90,
sorter: (a: SourceDocument, b: SourceDocument) => {
const A = a.metadata?.firstInsertedAt
? new Date(a.metadata.firstInsertedAt).getTime()
: 0
const B = b.metadata?.firstInsertedAt
? new Date(b.metadata.firstInsertedAt).getTime()
: 0
return A - B
},
render: (firstInsertedAt: Date) =>
formatDateAndTimeForExcel(firstInsertedAt),
},
{
title: 'Actions',
key: 'actions',
Expand Down Expand Up @@ -190,7 +225,8 @@ export default function ChatbotDocuments({
const searchTerm = e.target.value.toLowerCase()
const filtered = documents.filter((doc) => {
const isNameMatch = doc.pageContent
? doc.pageContent.toLowerCase().includes(searchTerm)
? doc.pageContent.toLowerCase().includes(searchTerm) ||
doc.metadata?.name?.toLowerCase().includes(searchTerm)
: false
return isNameMatch
})
Expand Down Expand Up @@ -258,25 +294,33 @@ export default function ChatbotDocuments({
>
<Input.TextArea />
</Form.Item>
<Form.Item
label="Name"
name="name"
tooltip={`When this chunk is cited, it will show this name. Defaults to "Manually Inserted Info" if not specified.`}
>
<Input placeholder="Manually Inserted Info" />
</Form.Item>
{/* <Form.Item label="Edited Chunk" name="editedChunk">
<Input.TextArea />
</Form.Item> */}
<Form.Item
label="Source"
label="Source URL"
name="source"
rules={[
{
type: 'url',
message: 'Please enter a valid URL',
},
]}
tooltip="When a student clicks on the citation, they will be redirected to this link"
tooltip="When a student clicks on the citation, they will be redirected to this link. Can be a link to anything."
>
<Input />
<Input placeholder="https://canvas.ubc.ca/courses/.../pages/..." />
</Form.Item>
<Form.Item
label="Page Number"
name="pageNumber"
tooltip="If the document in the Source URL is multi-page (e.g. a PDF), the content of the chunk should be found on this page. This is only for display purposes so that the citation says 'My doc p.3' for example. Feel free to leave this as 0 or blank."
rules={[
{
type: 'number',
Expand Down Expand Up @@ -319,7 +363,13 @@ export default function ChatbotDocuments({
onPressEnter={fetchDocuments}
/>
<div className="flex justify-between">
<Table columns={columns} dataSource={filteredDocuments} size="small" />
<Table
columns={columns}
dataSource={filteredDocuments}
size="small"
bordered
loading={documents.length === 0 && dataLoading}
/>
</div>
{editingRecord && (
<EditDocumentChunkModal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,17 @@ const EditChatbotQuestionModal: React.FC<EditChatbotQuestionModalProps> = ({
const handleOkInsert = async () => {
const values = await form.validateFields()
setSaveLoading(true)
const now = new Date()
const newChunk: AddDocumentChunkParams = {
documentText: values.question + '\nAnswer:' + values.answer,
metadata: {
name: 'inserted Q&A',
name: 'Previously Asked Question',
type: 'inserted_question',
id: editingRecord.vectorStoreId,
courseId: cid,
firstInsertedAt: now,
lastUpdatedAt: now,
shouldProbablyKeepWhenCloning: true,
},
}
await API.chatbot.staffOnly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export default function ChatbotSettings({
.getAllAggregateDocuments(courseId)
.then((response) => {
const formattedDocuments = response.map((doc) => ({
...doc,
key: doc.id,
docId: doc.id,
docName: doc.pageContent,
Expand Down
Loading
Loading