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
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -18,14 +19,14 @@ const meta: Meta<React.ComponentProps<typeof MultimodalInput>> = {
},
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!' },
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>([])
const [messageId, setMessageId] = useState(getUUID())
const [filePreviewsContainer, setFilePreviewsContainer] =
useState<HTMLDivElement>()
const [errorMessagesContainer, setErrorMessagesContainer] =
useState<HTMLDivElement>()
const inputRef = useRef<HTMLDivElement>(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(() => {
Expand All @@ -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 (
Expand All @@ -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}
Expand Down
59 changes: 47 additions & 12 deletions src/components/input/multimodal/uploader/uploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -58,11 +58,13 @@ export function getFileSizeAbbrev(bytes: number): string {
function Uploader(props: UploaderProps) {
const [addedFiles, setAddedFiles] = useState<FileInfo[]>([])
const [errorMessages, setErrorMessages] = useState<string[]>([])
const fileNamesRef = useRef<{ [key: string]: number }>({})
const inputId = getUUID()

useEffect(() => {
setAddedFiles([])
setErrorMessages([])
fileNamesRef.current = {}
}, [props.messageId])

function rejectFile(fileName: string) {
Expand Down Expand Up @@ -151,30 +153,49 @@ 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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const newCount = fileNameCount + 1
const newCount = fileNameCount + 1
const extensionIndex = fileName.lastIndexOf('.')
const baseName = fileName.substring(0, extensionIndex)

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
if (progressEvent.total) {
const loadedPercentage =
(progressEvent.loaded / progressEvent.total) *
percentageConversionRate
updateProgress(loadedPercentage, temporaryFileId)
updateProgress(loadedPercentage, newAddedFile.id)
}
}

Expand All @@ -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]++,
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -205,7 +205,7 @@ export type MultimodalInputProps = TextInputProps &
Omit<
UploaderProps,
| 'messageId'
| 'handleFileCountChange'
| 'onFileUpdate'
| 'filePreviewsContainer'
| 'errorMessagesContainer'
>