diff --git a/backend/app/bedrock.py b/backend/app/bedrock.py index b0a41d1c..96d69275 100644 --- a/backend/app/bedrock.py +++ b/backend/app/bedrock.py @@ -2,6 +2,7 @@ import json import logging import os +import re from pathlib import Path from typing import TypedDict, no_type_check @@ -94,6 +95,17 @@ def _get_converse_supported_format(ext: str) -> str: return supported_formats.get(ext, "txt") +def _convert_to_valid_file_name(file_name: str) -> str: + # Note: The document file name can only contain alphanumeric characters, + # whitespace characters, hyphens, parentheses, and square brackets. + # The name can't contain more than one consecutive whitespace character. + file_name = re.sub(r"[^a-zA-Z0-9\s\-\(\)\[\]]", "", file_name) + file_name = re.sub(r"\s+", " ", file_name) + file_name = file_name.strip() + + return file_name + + @no_type_check def compose_args_for_converse_api( messages: list[MessageModel], @@ -124,7 +136,7 @@ def compose_args_for_converse_api( } } ) - elif c.content_type == "textAttachment": + elif c.content_type == "attachment": content_blocks.append( { "document": { @@ -134,10 +146,10 @@ def compose_args_for_converse_api( ], # e.g. "document.txt" -> "txt" ), "name": Path( - c.file_name + _convert_to_valid_file_name(c.file_name) ).stem, # e.g. "document.txt" -> "document" # encode text attachment body - "source": {"bytes": c.body.encode("utf-8")}, + "source": {"bytes": base64.b64decode(c.body)}, } } ) diff --git a/backend/app/repositories/models/conversation.py b/backend/app/repositories/models/conversation.py index bbb953b6..95953754 100644 --- a/backend/app/repositories/models/conversation.py +++ b/backend/app/repositories/models/conversation.py @@ -6,7 +6,7 @@ class ContentModel(BaseModel): - content_type: Literal["text", "image", "textAttachment"] + content_type: Literal["text", "image", "attachment"] media_type: str | None body: str = Field( ..., diff --git a/backend/app/routes/schemas/conversation.py b/backend/app/routes/schemas/conversation.py index db224cbe..63cbfcd3 100644 --- a/backend/app/routes/schemas/conversation.py +++ b/backend/app/routes/schemas/conversation.py @@ -18,7 +18,7 @@ class Content(BaseSchema): - content_type: Literal["text", "image", "textAttachment"] = Field( + content_type: Literal["text", "image", "attachment"] = Field( ..., description="Content type. Note that image is only available for claude 3." ) media_type: str | None = Field( @@ -27,7 +27,7 @@ class Content(BaseSchema): ) file_name: str | None = Field( None, - description="File name of the attachment. Must be specified if `content_type` is `textAttachment`.", + description="File name of the attachment. Must be specified if `content_type` is `attachment`.", ) body: str = Field(..., description="Content body.") @@ -42,18 +42,18 @@ def check_media_type(cls, v, values): def check_body(cls, v, values): content_type = values.get("content_type") - # if content_type in ["image", "textAttachment"]: - # try: - # # Check if the body is a valid base64 string - # base64.b64decode(v, validate=True) - # except Exception: - # raise ValueError( - # "body must be a valid base64 string if `content_type` is `image` or `textAttachment`" - # ) - if content_type == "text" and not isinstance(v, str): raise ValueError("body must be str if `content_type` is `text`") + if content_type in ["image", "attachment"]: + try: + # Check if the body is a valid base64 string + base64.b64decode(v, validate=True) + except Exception: + raise ValueError( + "body must be a valid base64 string if `content_type` is `image` or `attachment`" + ) + return v diff --git a/backend/tests/test_stream/test_stream.py b/backend/tests/test_stream/test_stream.py index 463028c9..92e08b14 100644 --- a/backend/tests/test_stream/test_stream.py +++ b/backend/tests/test_stream/test_stream.py @@ -1,3 +1,4 @@ +import base64 import sys sys.path.append(".") @@ -151,16 +152,16 @@ def test_run_with_image(self): self._run(message) def test_run_with_attachment(self): - # _, aws_pdf_body = get_aws_overview() - # aws_pdf_filename = "aws_arch_overview.pdf" - body = get_test_markdown() + file_name, body = get_aws_overview() + body = base64.b64encode(body).decode("utf-8") + # body = get_test_markdown() file_name = "test.md" message = MessageModel( role="user", content=[ ContentModel( - content_type="textAttachment", + content_type="attachment", media_type=None, body=body, file_name=file_name, diff --git a/backend/tests/test_usecases/test_chat.py b/backend/tests/test_usecases/test_chat.py index 29dc8889..71bd0b2f 100644 --- a/backend/tests/test_usecases/test_chat.py +++ b/backend/tests/test_usecases/test_chat.py @@ -1,3 +1,4 @@ +import base64 import sys sys.path.insert(0, ".") @@ -39,6 +40,7 @@ ) from app.vector_search import SearchResult from tests.test_stream.get_aws_logo import get_aws_logo +from tests.test_stream.get_pdf import get_aws_overview from tests.test_usecases.utils.bot_factory import ( create_test_instruction_template, create_test_private_bot, @@ -271,6 +273,42 @@ def tearDown(self) -> None: delete_conversation_by_id("user1", self.output.conversation_id) +class TestAttachmentChat(unittest.TestCase): + def tearDown(self) -> None: + delete_conversation_by_id("user1", self.output.conversation_id) + + def test_chat(self): + file_name, body = get_aws_overview() + chat_input = ChatInput( + conversation_id="test_conversation_id", + message=MessageInput( + role="user", + content=[ + Content( + content_type="attachment", + body=base64.b64encode(body).decode("utf-8"), + media_type=None, + file_name=file_name, + ), + Content( + content_type="text", + body="Summarize the document.", + media_type=None, + file_name=None, + ), + ], + model=MODEL, + parent_message_id=None, + message_id=None, + ), + bot_id=None, + continue_generate=False, + ) + output: ChatOutput = chat(user_id="user1", chat_input=chat_input) + pprint(output.model_dump()) + self.output = output + + class TestMultimodalChat(unittest.TestCase): def tearDown(self) -> None: delete_conversation_by_id("user1", self.output.conversation_id) diff --git a/cdk/lib/bedrock-chat-stack.ts b/cdk/lib/bedrock-chat-stack.ts index a93f085f..8f35aadc 100644 --- a/cdk/lib/bedrock-chat-stack.ts +++ b/cdk/lib/bedrock-chat-stack.ts @@ -197,6 +197,7 @@ export class BedrockChatStack extends cdk.Stack { bedrockRegion: props.bedrockRegion, largeMessageBucket, documentBucket, + enableMistral: props.enableMistral, }); frontend.buildViteApp({ backendApiEndpoint: backendApi.api.apiEndpoint, diff --git a/cdk/lib/constructs/websocket.ts b/cdk/lib/constructs/websocket.ts index ec410507..0c850954 100644 --- a/cdk/lib/constructs/websocket.ts +++ b/cdk/lib/constructs/websocket.ts @@ -31,6 +31,7 @@ export interface WebSocketProps { readonly websocketSessionTable: ITable; readonly largeMessageBucket: s3.IBucket; readonly accessLogBucket?: s3.Bucket; + readonly enableMistral: boolean; } export class WebSocket extends Construct { @@ -110,6 +111,7 @@ export class WebSocket extends Construct { DB_SECRETS_ARN: props.dbSecrets.secretArn, LARGE_PAYLOAD_SUPPORT_BUCKET: largePayloadSupportBucket.bucketName, WEBSOCKET_SESSION_TABLE_NAME: props.websocketSessionTable.tableName, + ENABLE_MISTRAL: props.enableMistral.toString(), }, role: handlerRole, }); diff --git a/frontend/src/@types/conversation.d.ts b/frontend/src/@types/conversation.d.ts index 3540f39f..4398b027 100644 --- a/frontend/src/@types/conversation.d.ts +++ b/frontend/src/@types/conversation.d.ts @@ -10,7 +10,7 @@ export type Model = | 'mixtral-8x7b-instruct' | 'mistral-large'; export type Content = { - contentType: 'text' | 'image' | 'textAttachment'; + contentType: 'text' | 'image' | 'attachment'; mediaType?: string; fileName?: string; body: string; diff --git a/frontend/src/components/ChatMessage.tsx b/frontend/src/components/ChatMessage.tsx index 3df32517..c2e7ee7c 100644 --- a/frontend/src/components/ChatMessage.tsx +++ b/frontend/src/components/ChatMessage.tsx @@ -21,7 +21,8 @@ import ModalDialog from './ModalDialog'; import { useTranslation } from 'react-i18next'; import useChat from '../hooks/useChat'; import DialogFeedback from './DialogFeedback'; -import UploadedFileText from './UploadedFileText'; +import UploadedAttachedFile from './UploadedAttachedFile'; +import { TEXT_FILE_EXTENSIONS } from '../constants/supportedAttachedFiles'; type Props = BaseProps & { chatContent?: DisplayMessageContent; @@ -177,20 +178,35 @@ const ChatMessage: React.FC = (props) => { )} {chatContent.content.some( - (content) => content.contentType === 'textAttachment' + (content) => content.contentType === 'attachment' ) && (
{chatContent.content.map((content, idx) => { - if (content.contentType === 'textAttachment') { + if (content.contentType === 'attachment') { + const isTextFile = TEXT_FILE_EXTENSIONS.some( + (ext) => content.fileName?.toLowerCase().endsWith(ext) + ); return ( - { - setDialogFileName(content.fileName ?? ''); - setDialogFileContent(content.body); - setIsFileModalOpen(true); - }} + onClick={ + // Only text file can be previewed + isTextFile + ? () => { + const textContent = new TextDecoder( + 'utf-8' + ).decode( + Uint8Array.from(atob(content.body), (c) => + c.charCodeAt(0) + ) + ); // base64 encoded text to be decoded string + setDialogFileName(content.fileName ?? ''); + setDialogFileContent(textContent); + setIsFileModalOpen(true); + } + : undefined + } /> ); } diff --git a/frontend/src/components/InputChatContent.tsx b/frontend/src/components/InputChatContent.tsx index bc677c9d..8a2fca83 100644 --- a/frontend/src/components/InputChatContent.tsx +++ b/frontend/src/components/InputChatContent.tsx @@ -8,7 +8,7 @@ import React, { import ButtonSend from './ButtonSend'; import Textarea from './Textarea'; import useChat from '../hooks/useChat'; -import { TextAttachmentType } from '../hooks/useChat'; +import { AttachmentType } from '../hooks/useChat'; import Button from './Button'; import { PiArrowsCounterClockwise, @@ -25,8 +25,14 @@ import { create } from 'zustand'; import ButtonFileChoose from './ButtonFileChoose'; import { BaseProps } from '../@types/common'; import ModalDialog from './ModalDialog'; -import UploadedFileText from './UploadedFileText'; +import UploadedAttachedFile from './UploadedAttachedFile'; import useSnackbar from '../hooks/useSnackbar'; +import { + MAX_FILE_SIZE_BYTES, + MAX_FILE_SIZE_MB, + SUPPORTED_FILE_EXTENSIONS, + MAX_ATTACHED_FILES, +} from '../constants/supportedAttachedFiles'; type Props = BaseProps & { disabledSend?: boolean; @@ -36,76 +42,34 @@ type Props = BaseProps & { onSend: ( content: string, base64EncodedImages?: string[], - textAttachments?: TextAttachmentType[] + attachments?: AttachmentType[] ) => void; onRegenerate: () => void; continueGenerate: () => void; }; - -const MAX_IMAGE_WIDTH = 800; -const MAX_IMAGE_HEIGHT = 800; -// To change the supported text format files, change the extension list below. -const TEXT_FILE_EXTENSIONS = [ - '.txt', - '.py', - '.ipynb', - '.js', - '.jsx', - '.html', - '.css', - '.java', - '.cs', - '.php', - '.c', - '.cpp', - '.cxx', - '.h', - '.hpp', - '.rs', - '.R', - '.Rmd', - '.swift', - '.go', - '.rb', - '.kt', - '.kts', - '.ts', - '.tsx', - '.m', - '.scala', - '.rs', - '.dart', - '.lua', - '.pl', - '.pm', - '.t', - '.sh', - '.bash', - '.zsh', - '.csv', - '.log', - '.ini', - '.config', - '.json', - '.proto', - '.yaml', - '.yml', - '.toml', - '.lua', - '.sql', - '.bat', - '.md', - '.coffee', - '.tex', - '.latex', -]; +// Image size +// Ref: https://docs.anthropic.com/en/docs/build-with-claude/vision#evaluate-image-size +const MAX_IMAGE_WIDTH = 1568; +const MAX_IMAGE_HEIGHT = 1568; +// 6 MB (Lambda response size limit is 6 MB) +// Ref: https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html +// Converse API can handle 4.5 MB x 5 files, but the API to fetch conversation history is based on the lambda, +// so we limit the size to 6 MB to prevent the error. +// Need to refactor if want to increase the limit by using s3 presigned URL. +const MAX_FILE_SIZE_TO_SEND_MB = 6; +const MAX_FILE_SIZE_TO_SEND_BYTES = MAX_FILE_SIZE_TO_SEND_MB * 1024 * 1024; const useInputChatContentState = create<{ base64EncodedImages: string[]; pushBase64EncodedImage: (encodedImage: string) => void; removeBase64EncodedImage: (index: number) => void; clearBase64EncodedImages: () => void; - textFiles: { name: string; type: string; size: number; content: string }[]; + attachedFiles: { + name: string; + type: string; + size: number; + content: string; + }[]; pushTextFile: (file: { name: string; type: string; @@ -113,11 +77,13 @@ const useInputChatContentState = create<{ content: string; }) => void; removeTextFile: (index: number) => void; - clearTextFiles: () => void; + clearAttachedFiles: () => void; previewImageUrl: string | null; setPreviewImageUrl: (url: string | null) => void; isOpenPreviewImage: boolean; setIsOpenPreviewImage: (isOpen: boolean) => void; + totalFileSizeToSend: number; + setTotalFileSizeToSend: (size: number) => void; }>((set, get) => ({ base64EncodedImages: [], pushBase64EncodedImage: (encodedImage) => { @@ -147,26 +113,31 @@ const useInputChatContentState = create<{ setIsOpenPreviewImage: (isOpen) => { set({ isOpenPreviewImage: isOpen }); }, - textFiles: [], + attachedFiles: [], pushTextFile: (file) => { set({ - textFiles: produce(get().textFiles, (draft) => { + attachedFiles: produce(get().attachedFiles, (draft) => { draft.push(file); }), }); }, removeTextFile: (index) => { set({ - textFiles: produce(get().textFiles, (draft) => { + attachedFiles: produce(get().attachedFiles, (draft) => { draft.splice(index, 1); }), }); }, - clearTextFiles: () => { + clearAttachedFiles: () => { set({ - textFiles: [], + attachedFiles: [], }); }, + totalFileSizeToSend: 0, + setTotalFileSizeToSend: (size) => + set({ + totalFileSizeToSend: size, + }), })); const InputChatContent: React.FC = (props) => { @@ -175,7 +146,7 @@ const InputChatContent: React.FC = (props) => { const { disabledImageUpload, model, acceptMediaType } = useModel(); const extendedAcceptMediaType = useMemo(() => { - return [...acceptMediaType, ...TEXT_FILE_EXTENSIONS]; + return [...acceptMediaType, ...SUPPORTED_FILE_EXTENSIONS]; }, [acceptMediaType]); const [shouldContinue, setShouldContinue] = useState(false); @@ -190,15 +161,17 @@ const InputChatContent: React.FC = (props) => { setPreviewImageUrl, isOpenPreviewImage, setIsOpenPreviewImage, - textFiles, + attachedFiles, pushTextFile, removeTextFile, - clearTextFiles, + clearAttachedFiles, + totalFileSizeToSend, + setTotalFileSizeToSend, } = useInputChatContentState(); useEffect(() => { clearBase64EncodedImages(); - clearTextFiles(); + clearAttachedFiles(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -227,7 +200,7 @@ const InputChatContent: React.FC = (props) => { const inputRef = useRef(null); const sendContent = useCallback(() => { - const textAttachments = textFiles.map((file) => ({ + const attachments = attachedFiles.map((file) => ({ fileName: file.name, fileType: file.type, extractedContent: file.content, @@ -238,16 +211,16 @@ const InputChatContent: React.FC = (props) => { !disabledImageUpload && base64EncodedImages.length > 0 ? base64EncodedImages : undefined, - textAttachments.length > 0 ? textAttachments : undefined + attachments.length > 0 ? attachments : undefined ); setContent(''); clearBase64EncodedImages(); - clearTextFiles(); + clearAttachedFiles(); }, [ base64EncodedImages, - textFiles, + attachedFiles, clearBase64EncodedImages, - clearTextFiles, + clearAttachedFiles, content, disabledImageUpload, props, @@ -293,29 +266,83 @@ const InputChatContent: React.FC = (props) => { const resizedImageData = canvas.toDataURL('image/png'); + // Total file size check + if ( + totalFileSizeToSend + resizedImageData.length > + MAX_FILE_SIZE_TO_SEND_BYTES + ) { + open( + t('error.totalFileSizeToSendExceeded', { + maxSize: `${MAX_FILE_SIZE_TO_SEND_MB} MB`, + }) + ); + return; + } + pushBase64EncodedImage(resizedImageData); + setTotalFileSizeToSend(totalFileSizeToSend + resizedImageData.length); }; }; }, - [pushBase64EncodedImage] + [ + pushBase64EncodedImage, + totalFileSizeToSend, + setTotalFileSizeToSend, + open, + t, + ] ); - const handleFileRead = useCallback( + const handleAttachedFileRead = useCallback( (file: File) => { + if (file.size > MAX_FILE_SIZE_BYTES) { + open( + t('error.attachment.fileSizeExceeded', { + maxSize: `${MAX_FILE_SIZE_MB} MB`, + }) + ); + return; + } + const reader = new FileReader(); reader.onload = () => { - if (typeof reader.result === 'string') { + if (reader.result instanceof ArrayBuffer) { + // Convert from byte to base64 encoded string + const byteArray = new Uint8Array(reader.result); + let binaryString = ''; + const chunkSize = 8192; + + for (let i = 0; i < byteArray.length; i += chunkSize) { + const chunk = byteArray.slice(i, i + chunkSize); + // To avoid `Maximum call stack size exceeded` error, split into smaller chunks + binaryString += String.fromCharCode(...chunk); + } + const base64String = btoa(binaryString); + + // Total file size check + if ( + totalFileSizeToSend + base64String.length > + MAX_FILE_SIZE_TO_SEND_BYTES + ) { + open( + t('error.totalFileSizeToSendExceeded', { + maxSize: `${MAX_FILE_SIZE_TO_SEND_MB} MB`, + }) + ); + return; + } pushTextFile({ name: file.name, type: file.type, size: file.size, - content: reader.result, + content: base64String, }); + setTotalFileSizeToSend(totalFileSizeToSend + base64String.length); } }; - reader.readAsText(file); + reader.readAsArrayBuffer(file); }, - [pushTextFile] + [pushTextFile, totalFileSizeToSend, setTotalFileSizeToSend, open, t] ); useEffect(() => { @@ -357,15 +384,50 @@ const InputChatContent: React.FC = (props) => { const onChangeFile = useCallback( (fileList: FileList) => { + // Check if the total number of attached files exceeds the limit + const currentAttachedFiles = + useInputChatContentState.getState().attachedFiles; + const currentAttachedFilesCount = currentAttachedFiles.filter((file) => + SUPPORTED_FILE_EXTENSIONS.some((extension) => + file.name.endsWith(extension) + ) + ).length; + + let newAttachedFilesCount = 0; + for (let i = 0; i < fileList.length; i++) { + const file = fileList.item(i); + if (file) { + if ( + SUPPORTED_FILE_EXTENSIONS.some((extension) => + file.name.endsWith(extension) + ) + ) { + newAttachedFilesCount++; + } + } + } + + if ( + currentAttachedFilesCount + newAttachedFilesCount > + MAX_ATTACHED_FILES + ) { + open( + t('error.attachment.fileCountExceeded', { + maxCount: MAX_ATTACHED_FILES, + }) + ); + return; + } + for (let i = 0; i < fileList.length; i++) { const file = fileList.item(i); if (file) { if ( - TEXT_FILE_EXTENSIONS.some((extension) => + SUPPORTED_FILE_EXTENSIONS.some((extension) => file.name.endsWith(extension) ) ) { - handleFileRead(file); + handleAttachedFileRead(file); } else if ( acceptMediaType.some((extension) => file.name.endsWith(extension)) ) { @@ -376,7 +438,7 @@ const InputChatContent: React.FC = (props) => { } } }, - [encodeAndPushImage, handleFileRead, open, t, acceptMediaType] + [encodeAndPushImage, handleAttachedFileRead, open, t, acceptMediaType] ); const onDragOver: React.DragEventHandler = useCallback( @@ -470,11 +532,11 @@ const InputChatContent: React.FC = (props) => {
)} - {textFiles.length > 0 && ( + {attachedFiles.length > 0 && (
- {textFiles.map((file, idx) => ( + {attachedFiles.map((file, idx) => (
- + { diff --git a/frontend/src/components/UploadedFileText.tsx b/frontend/src/components/UploadedAttachedFile.tsx similarity index 100% rename from frontend/src/components/UploadedFileText.tsx rename to frontend/src/components/UploadedAttachedFile.tsx diff --git a/frontend/src/constants/supportedAttachedFiles.ts b/frontend/src/constants/supportedAttachedFiles.ts new file mode 100644 index 00000000..be7ff850 --- /dev/null +++ b/frontend/src/constants/supportedAttachedFiles.ts @@ -0,0 +1,77 @@ +// To change the supported text format files, change the extension list below. +export const TEXT_FILE_EXTENSIONS = [ + '.txt', + '.py', + '.ipynb', + '.js', + '.jsx', + '.html', + '.css', + '.java', + '.cs', + '.php', + '.c', + '.cpp', + '.cxx', + '.h', + '.hpp', + '.rs', + '.R', + '.Rmd', + '.swift', + '.go', + '.rb', + '.kt', + '.kts', + '.ts', + '.tsx', + '.m', + '.scala', + '.rs', + '.dart', + '.lua', + '.pl', + '.pm', + '.t', + '.sh', + '.bash', + '.zsh', + '.csv', + '.log', + '.ini', + '.config', + '.json', + '.proto', + '.yaml', + '.yml', + '.toml', + '.lua', + '.sql', + '.bat', + '.md', + '.coffee', + '.tex', + '.latex', +]; + +// Supported non-text file extensions which can be handled on Converse API. +// Ref: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_DocumentBlock.html +export const NON_TEXT_FILE_EXTENSIONS = [ + '.pdf', + '.doc', + '.docx', + '.xls', + '.xlsx', +]; + +export const SUPPORTED_FILE_EXTENSIONS = [ + ...TEXT_FILE_EXTENSIONS, + ...NON_TEXT_FILE_EXTENSIONS, +]; + +// Converse API limitations: +// You can include up to five documents. Each document’s size must be no more than 4.5 MB. +// Ref: https://awscli.amazonaws.com/v2/documentation/api/latest/reference/bedrock-runtime/converse.html +export const MAX_FILE_SIZE_MB = 4.5; +export const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; +export const MAX_ATTACHED_FILES = 5; diff --git a/frontend/src/features/helper/components/BottomHelper.tsx b/frontend/src/features/helper/components/BottomHelper.tsx index 4238aca0..cb805ec7 100644 --- a/frontend/src/features/helper/components/BottomHelper.tsx +++ b/frontend/src/features/helper/components/BottomHelper.tsx @@ -18,12 +18,14 @@ export const BottomHelper = () => { />
setIsOpen(() => false)}>
-
{t('heler.shortcuts.items.newChat')}
+
+ {t('helper.shortcuts.items.newChat')} +
@@ -40,7 +42,7 @@ export const BottomHelper = () => {
- {t('heler.shortcuts.items.focusInput')} + {t('helper.shortcuts.items.focusInput')}
diff --git a/frontend/src/hooks/useChat.ts b/frontend/src/hooks/useChat.ts index 89d6b5b4..24fe3426 100644 --- a/frontend/src/hooks/useChat.ts +++ b/frontend/src/hooks/useChat.ts @@ -34,7 +34,7 @@ type BotInputType = { hasAgent: boolean; }; -export type TextAttachmentType = { +export type AttachmentType = { fileName: string; fileType: string; extractedContent: string; @@ -364,10 +364,10 @@ const useChat = () => { const postChat = (params: { content: string; base64EncodedImages?: string[]; - textAttachments?: TextAttachmentType[]; + attachments?: AttachmentType[]; bot?: BotInputType; }) => { - const { content, bot, base64EncodedImages, textAttachments } = params; + const { content, bot, base64EncodedImages, attachments } = params; const isNewChat = conversationId ? false : true; const newConversationId = ulid(); @@ -397,20 +397,20 @@ const useChat = () => { }; }); - const textAttachContents: MessageContent['content'] = ( - textAttachments ?? [] - ).map((attachment) => { - return { - body: attachment.extractedContent, - contentType: 'textAttachment', - mediaType: attachment.fileType, - fileName: attachment.fileName, - }; - }); + const attachContents: MessageContent['content'] = (attachments ?? []).map( + (attachment) => { + return { + body: attachment.extractedContent, + contentType: 'attachment', + mediaType: attachment.fileType, + fileName: attachment.fileName, + }; + } + ); const messageContent: MessageContent = { content: [ - ...textAttachContents, + ...attachContents, ...imageContents, { body: content, diff --git a/frontend/src/i18n/en/index.ts b/frontend/src/i18n/en/index.ts index 1cbfafb7..f45c5ddc 100644 --- a/frontend/src/i18n/en/index.ts +++ b/frontend/src/i18n/en/index.ts @@ -506,6 +506,13 @@ How would you categorize this email?`, }, notSupportedImage: 'The selected model does not support images.', unsupportedFileFormat: 'The selected file format is not supported.', + totalFileSizeToSendExceeded: + 'The total file size must be no more than {{maxSize}}.', + attachment: { + fileSizeExceeded: + 'Each document size must be no more than {{maxSize}}.', + fileCountExceeded: 'Could not upload more than {{maxCount}} files.', + }, }, validation: { title: 'Validation Error', @@ -522,7 +529,7 @@ How would you categorize this email?`, message: 'Please input both Title and Conversation Example.', }, }, - heler: { + helper: { shortcuts: { title: 'Shortcut Keys', items: { diff --git a/frontend/src/i18n/ja/index.ts b/frontend/src/i18n/ja/index.ts index 0d8ce1d9..ff7deff4 100644 --- a/frontend/src/i18n/ja/index.ts +++ b/frontend/src/i18n/ja/index.ts @@ -509,6 +509,12 @@ const translation = { }, notSupportedImage: '選択しているモデルは、画像を利用できません。', unsupportedFileFormat: '選択したファイル形式はサポートされていません。', + totalFileSizeToSendExceeded: + 'ファイルサイズの合計が{{maxSize}}を超えています。', + attachment: { + fileSizeExceeded: 'ファイルサイズは{{maxSize}}以下にしてください。', + fileCountExceeded: 'ファイル数は{{maxCount}}以下にしてください。', + }, }, validation: { title: 'バリデーションエラー', @@ -526,7 +532,7 @@ const translation = { message: 'タイトルと入力例は、どちらも入力してください。', }, }, - heler: { + helper: { shortcuts: { title: 'ショートカットキー', items: { diff --git a/frontend/src/pages/ChatPage.tsx b/frontend/src/pages/ChatPage.tsx index 9d8b898e..61311568 100644 --- a/frontend/src/pages/ChatPage.tsx +++ b/frontend/src/pages/ChatPage.tsx @@ -7,7 +7,7 @@ import React, { } from 'react'; import InputChatContent from '../components/InputChatContent'; import useChat from '../hooks/useChat'; -import { TextAttachmentType } from '../hooks/useChat'; +import { AttachmentType } from '../hooks/useChat'; import ChatMessage from '../components/ChatMessage'; import useScroll from '../hooks/useScroll'; import { useNavigate, useParams } from 'react-router-dom'; @@ -137,12 +137,12 @@ const ChatPage: React.FC = () => { ( content: string, base64EncodedImages?: string[], - textAttachments?: TextAttachmentType[] + attachments?: AttachmentType[] ) => { postChat({ content, base64EncodedImages, - textAttachments, + attachments, bot: inputBotParams, }); }, diff --git a/lefthook.yml b/lefthook.yml index 758d5ab1..68b8058b 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -19,3 +19,4 @@ pre-commit: root: "frontend/" glob: "**/*.{js,jsx,ts,tsx,vue}" run: npx eslint --fix --max-warnings=0 {staged_files} +