From de70068883e2668fd4d706dcc5e1b20b273e073a Mon Sep 17 00:00:00 2001 From: Eileen Li Date: Wed, 1 May 2024 14:14:27 -0700 Subject: [PATCH] fix: include file names in the message --- .../multimodalInput.stories.tsx | 30 +++++++--- .../multimodalInput/multimodalInput.tsx | 17 ++++-- .../input/multimodal/uploader/uploader.tsx | 59 +++++++++++++++---- src/components/types.ts | 6 +- 4 files changed, 82 insertions(+), 30 deletions(-) diff --git a/src/components/input/multimodal/multimodalInput/multimodalInput.stories.tsx b/src/components/input/multimodal/multimodalInput/multimodalInput.stories.tsx index 8475eba8..cf337c7c 100644 --- a/src/components/input/multimodal/multimodalInput/multimodalInput.stories.tsx +++ b/src/components/input/multimodal/multimodalInput/multimodalInput.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryFn } from '@storybook/react' import React from 'react' +import { v4 as getUUID } from 'uuid' import type { Message } from '../../../types' import MultimodalInput from './multimodalInput' @@ -18,14 +19,14 @@ const meta: Meta> = { }, mockData: [ { - url: 'http://localhost:8080/upload?message-id=1', + url: 'http://localhost:8080/upload?message-id=:messageId', method: 'POST', status: 200, - response: { fileId: '1' }, + response: { fileId: getUUID() }, delay: 1000, }, { - url: 'http://localhost:8080/files?message-id=1&file-id=:fileId', + url: 'http://localhost:8080/files?message-id=:messageId&file-id=:fileId', method: 'DELETE', status: 200, response: { message: 'Delete successfully!' }, @@ -154,13 +155,24 @@ export const Default = { conversationId: '1', placeholder: 'Type your message', ws: { - // eslint-disable-next-line no-console - send: (message: Message) => + send: (message: Message) => { + let fileMessage = '' + let textMessage = '' + if (message.data.files && message.data.files.length > 0) { + const fileNames = message.data.files.join(', ') + fileMessage = `File(s): ${fileNames}` + } + if (message.data.text) { + textMessage = `Text content: ${message.data.text}` + } alert( - message.data.text - ? `Message sent: ${message.data.text}` - : 'File(s) sent!' - ), + 'Message sent!' + + '\n' + + textMessage + + `${textMessage.length > 0 ? '\n' : ''}` + + fileMessage + ) + }, }, uploadFileEndpoint: 'http://localhost:8080/upload', deleteFileEndpoint: 'http://localhost:8080/files', diff --git a/src/components/input/multimodal/multimodalInput/multimodalInput.tsx b/src/components/input/multimodal/multimodalInput/multimodalInput.tsx index 33b88d3e..8328b709 100644 --- a/src/components/input/multimodal/multimodalInput/multimodalInput.tsx +++ b/src/components/input/multimodal/multimodalInput/multimodalInput.tsx @@ -10,17 +10,21 @@ import BaseInput from '../../baseInput/baseInput' import Uploader from '../uploader/uploader' export default function MultimodalInput(props: MultimodalInputProps) { - const [fileCount, setFileCount] = useState(0) + const [fileNames, setFileNames] = useState([]) const [messageId, setMessageId] = useState(getUUID()) const [filePreviewsContainer, setFilePreviewsContainer] = useState() const [errorMessagesContainer, setErrorMessagesContainer] = useState() const inputRef = useRef(null) - const hasAddedFiles = fileCount > 0 + const hasAddedFiles = fileNames.length > 0 - function handleFileCountChange(fileCountChange: 1 | -1) { - setFileCount((prev) => prev + fileCountChange) + function handleFileUpdates(action: 'add' | 'remove', fileName: string) { + if (action === 'add') { + setFileNames((prev) => [...prev, fileName]) + } else { + setFileNames((prev) => prev.filter((file) => file !== fileName)) + } } useEffect(() => { @@ -42,11 +46,12 @@ export default function MultimodalInput(props: MultimodalInputProps) { if (hasAddedFiles) { formattedMessage.id = messageId formattedMessage.format = 'multipart' + formattedMessage.data.files = fileNames } props.ws.send(formattedMessage) setMessageId(getUUID()) - setFileCount(0) + setFileNames([]) } return ( @@ -64,7 +69,7 @@ export default function MultimodalInput(props: MultimodalInputProps) { maxFileSize={props.maxFileSize} uploadFileEndpoint={props.uploadFileEndpoint} deleteFileEndpoint={props.deleteFileEndpoint} - handleFileCountChange={handleFileCountChange} + onFileUpdate={handleFileUpdates} messageId={messageId} filePreviewsContainer={filePreviewsContainer} errorMessagesContainer={errorMessagesContainer} diff --git a/src/components/input/multimodal/uploader/uploader.tsx b/src/components/input/multimodal/uploader/uploader.tsx index 888e8f55..1b712e85 100644 --- a/src/components/input/multimodal/uploader/uploader.tsx +++ b/src/components/input/multimodal/uploader/uploader.tsx @@ -8,7 +8,7 @@ import Tooltip from '@mui/material/Tooltip' import Typography from '@mui/material/Typography' import type { AxiosProgressEvent } from 'axios' import axios from 'axios' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { v4 as getUUID } from 'uuid' @@ -58,11 +58,13 @@ export function getFileSizeAbbrev(bytes: number): string { function Uploader(props: UploaderProps) { const [addedFiles, setAddedFiles] = useState([]) const [errorMessages, setErrorMessages] = useState([]) + const fileNamesRef = useRef<{ [key: string]: number }>({}) const inputId = getUUID() useEffect(() => { setAddedFiles([]) setErrorMessages([]) + fileNamesRef.current = {} }, [props.messageId]) function rejectFile(fileName: string) { @@ -151,22 +153,41 @@ function Uploader(props: UploaderProps) { setAddedFiles((prev) => prev.filter((file) => file.id !== id)) } + function getFileName(file: File) { + let fileName = file.name + const fileNameCount = fileNamesRef.current[fileName] + + if (fileNameCount) { + const newCount = fileNameCount + 1 + const extensionIndex = fileName.lastIndexOf('.') + const baseName = fileName.substring(0, extensionIndex) + const extension = fileName.substring(extensionIndex) + fileName = `${baseName}(${fileNameCount})${extension}` + fileNamesRef.current = { ...fileNamesRef.current, [file.name]: newCount } + } else { + fileNamesRef.current = { ...fileNamesRef.current, [file.name]: 1 } + } + + return fileName + } + function uploadFile(file: File) { const formData = new FormData() const controller = new AbortController() - formData.append('file', file) + const fileName = getFileName(file) + const updatedFile = new File([file], fileName, { type: file.type }) + formData.append('file', updatedFile) const temporaryFileId = getUUID() const newAddedFile = { - name: file.name, + name: fileName, loadingProgress: 0, abortController: controller, id: temporaryFileId, } setAddedFiles((prev) => [...prev, newAddedFile]) - - props.handleFileCountChange(1) + props.onFileUpdate('add', fileName) function handleUploadProgress(progressEvent: AxiosProgressEvent) { const percentageConversionRate = 100 @@ -174,7 +195,7 @@ function Uploader(props: UploaderProps) { const loadedPercentage = (progressEvent.loaded / progressEvent.total) * percentageConversionRate - updateProgress(loadedPercentage, temporaryFileId) + updateProgress(loadedPercentage, newAddedFile.id) } } @@ -185,34 +206,48 @@ function Uploader(props: UploaderProps) { signal: controller.signal, }) .then((response) => { - handleSuccessfulUpload(response.data, temporaryFileId) + handleSuccessfulUpload(response.data, newAddedFile.id) }) .catch((error) => { - props.handleFileCountChange(-1) - handleFailedUpload(file.name, temporaryFileId, error.response?.data) + props.onFileUpdate('remove', fileName) + fileNamesRef.current = { + ...fileNamesRef.current, + [fileName]: fileNamesRef.current[file.name]--, + } + handleFailedUpload(file.name, newAddedFile.id, error.response?.data) }) } function handleDelete(file: FileInfo, index: number) { setErrorMessages([]) - props.handleFileCountChange(-1) + const fileName = file.name + fileNamesRef.current = { + ...fileNamesRef.current, + [fileName]: fileNamesRef.current[fileName]--, + } + props.onFileUpdate('remove', fileName) if (file.loadingProgress === maximumLoadingProgress) { const deleteUrl = `${props.deleteFileEndpoint}?message-id=${props.messageId}&file-id=${file.id}` + axios .delete(deleteUrl) .then(() => { return setAddedFiles((prev) => prev.filter((_, i) => i !== index)) }) .catch(() => { - props.handleFileCountChange(1) + props.onFileUpdate('add', fileName) return setErrorMessages((prevMessages) => [ ...prevMessages, - `Failed to delete ${file.name}. Please try again.`, + `Failed to delete ${fileName}. Please try again.`, ]) }) } else { file.abortController.abort() setAddedFiles((prev) => prev.filter((_file, i) => i !== index)) + fileNamesRef.current = { + ...fileNamesRef.current, + [fileName]: fileNamesRef.current[fileName]++, + } } } diff --git a/src/components/types.ts b/src/components/types.ts index b2bf1f4b..a7b6b555 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -189,8 +189,8 @@ export interface UploaderProps { deleteFileEndpoint: string /** Used in the API request to link the file with the message that's going to be sent. */ messageId: string - /** A function to handle changes in the file count. Parent component should use this to track file count change and handle submit accordingly. */ - handleFileCountChange: (fileCountChange: 1 | -1) => void + /** A function to handle changes in the file list. The parent component should use this to track file names and handle submit accordingly. */ + onFileUpdate: (action: 'add' | 'remove', fileName: string) => void /** Optional HTML div where the errors should be shown. */ errorMessagesContainer?: HTMLDivElement /** Optional HTML div where the filePreviews should be shown. */ @@ -205,7 +205,7 @@ export type MultimodalInputProps = TextInputProps & Omit< UploaderProps, | 'messageId' - | 'handleFileCountChange' + | 'onFileUpdate' | 'filePreviewsContainer' | 'errorMessagesContainer' >