diff --git a/app/client/api.ts b/app/client/api.ts index 7a242ea99dd..f38c20330ec 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -29,11 +29,22 @@ export const TTSModels = ["tts-1", "tts-1-hd"] as const; export type ChatModel = ModelType; export interface MultimodalContent { - type: "text" | "image_url"; + type: "text" | "image_url" | "file_url"; text?: string; image_url?: { url: string; }; + file_url?: { + url: string; + name: string; + tokenCount?: number; + }; +} + +export interface UploadFile { + name: string; + url: string; + tokenCount?: number; } export interface RequestMessage { diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index 73542fc67f1..9399ece2a6a 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -1,10 +1,18 @@ @import "../styles/animation.scss"; -.attach-images { +.attachments { position: absolute; left: 30px; bottom: 32px; display: flex; + flex-direction: row; +} + +.attach-images { + //position: absolute; + //left: 30px; + //bottom: 32px; + display: flex; } .attach-image { @@ -42,6 +50,86 @@ } } +.attach-files { + //position: absolute; + //left: 30px; + //bottom: 32px; + display: flex; + flex-direction: column; + //row-gap: 11px; + max-height: 64px; +} + +.attach-file { + cursor: default; + display: flex; + flex-direction: row; + column-gap: 4px; + justify-content: flex-start; + color: var(--black); + font-size: 14px; + border-radius: 5px; + margin-right: 10px; + + %attach-file-name-common { + display:flex; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + } + .attach-file-name-full { + @extend %attach-file-name-common; + max-width:calc(62vw); + } + + .attach-file-name-half { + @extend %attach-file-name-common; + max-width:calc(45vw); + } + + .attach-file-name-less { + @extend %attach-file-name-common; + max-width:calc(28vw); + } + + .attach-file-name-min { + @extend %attach-file-name-common; + max-width:calc(12vw); + } + .attach-file-icon { + min-width: 16px; + max-width: 16px; + } + + .attach-file-icon:hover { + opacity: 0; + } + + .attach-image-mask { + width: 100%; + height: 100%; + opacity: 0; + transition: all ease 0.2s; + position: absolute; + } + + .attach-image-mask:hover { + opacity: 1; + } + + .delete-image { + width: 16px; + height: 20px; + cursor: pointer; + border-radius: 5px; + float: left; + background-color: var(--white); + } +} + + + .chat-input-actions { display: flex; flex-wrap: wrap; @@ -471,6 +559,32 @@ border: rgba($color: #888, $alpha: 0.2) 1px solid; } +.chat-message-item-files { + width: 100%; + display: flex; + flex-direction: column; + row-gap: 10px; + flex-wrap: wrap; + margin-top: 10px; +} + +.chat-message-item-file { + display: flex; + flex-direction: row; + column-gap: 6px; + +} +.chat-message-item-file-icon { + max-width: 16px; +} + +.chat-message-item-file-name { + max-width:100%; +} + + + + @media only screen and (max-width: 600px) { $calc-image-width: calc(100vw/3*2/var(--image-count)); @@ -693,4 +807,4 @@ .shortcut-key span { font-size: 12px; color: var(--black); -} \ No newline at end of file +} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 3d519dee722..b128f1e7a91 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -46,6 +46,7 @@ import StyleIcon from "../icons/palette.svg"; import PluginIcon from "../icons/plugin.svg"; import ShortcutkeyIcon from "../icons/shortcutkey.svg"; import ReloadIcon from "../icons/reload.svg"; +import UploadDocIcon from "../icons/upload-doc.svg"; import { ChatMessage, @@ -68,13 +69,18 @@ import { useMobileScreen, getMessageTextContent, getMessageImages, + getMessageFiles, isVisionModel, isDalle3, showPlugins, safeLocalStorage, + countTokens, } from "../utils"; +import type { UploadFile } from "../client/api"; + import { uploadImage as uploadImageRemote } from "@/app/utils/chat"; +import { uploadImage as uploadFileRemote } from "@/app/utils/chat"; import dynamic from "next/dynamic"; @@ -96,6 +102,8 @@ import { showToast, } from "./ui-lib"; import { useNavigate } from "react-router-dom"; +import { FileIcon, defaultStyles } from "react-file-icon"; +import type { DefaultExtensionType } from "react-file-icon"; import { CHAT_PAGE_SIZE, DEFAULT_TTS_ENGINE, @@ -442,8 +450,10 @@ function useScrollToBottom( } export function ChatActions(props: { + uploadDocument: () => void; uploadImage: () => void; setAttachImages: (images: string[]) => void; + setAttachFiles: (files: UploadFile[]) => void; setUploading: (uploading: boolean) => void; showPromptModal: () => void; scrollToBottom: () => void; @@ -577,6 +587,11 @@ export function ChatActions(props: { icon={props.uploading ? : } /> )} + : } + /> ([]); + const [attachFiles, setAttachFiles] = useState([]); const [uploading, setUploading] = useState(false); // prompt hints @@ -1025,9 +1041,10 @@ function _Chat() { } setIsLoading(true); chatStore - .onUserInput(userInput, attachImages) + .onUserInput(userInput, attachImages, attachFiles) .then(() => setIsLoading(false)); setAttachImages([]); + setAttachFiles([]); chatStore.setLastInput(userInput); setUserInput(""); setPromptHints([]); @@ -1177,7 +1194,9 @@ function _Chat() { setIsLoading(true); const textContent = getMessageTextContent(userMessage); const images = getMessageImages(userMessage); - chatStore.onUserInput(textContent, images).then(() => setIsLoading(false)); + chatStore + .onUserInput(textContent, images, attachFiles) + .then(() => setIsLoading(false)); inputRef.current?.focus(); }; @@ -1460,6 +1479,54 @@ function _Chat() { [attachImages, chatStore], ); + async function uploadDocument() { + const files: UploadFile[] = []; + files.push(...attachFiles); + + files.push( + ...(await new Promise((res, rej) => { + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = "text/*"; + fileInput.multiple = true; + fileInput.onchange = (event: any) => { + setUploading(true); + const inputFiles = event.target.files; + const imagesData: UploadFile[] = []; + (async () => { + for (let i = 0; i < inputFiles.length; i++) { + const file = inputFiles[i]; + try { + const dataUrl = await uploadFileRemote(file); + const fileData: UploadFile = { name: file.name, url: dataUrl }; + const tokenCount: number = await countTokens(fileData); + fileData.tokenCount = tokenCount; + imagesData.push(fileData); + if ( + imagesData.length === 3 || + imagesData.length === inputFiles.length + ) { + setUploading(false); + res(imagesData); + } + } catch (e) { + setUploading(false); + rej(e); + } + } + })(); + }; + fileInput.click(); + })), + ); + + const filesLength = files.length; + if (filesLength > 3) { + files.splice(3, filesLength - 3); + } + setAttachFiles(files); + } + async function uploadImage() { const images: string[] = []; images.push(...attachImages); @@ -1878,6 +1945,41 @@ function _Chat() { })} )} + {getMessageFiles(message).length > 0 && ( +
+ {getMessageFiles(message).map((file, index) => { + const extension: DefaultExtensionType = file.name + .split(".") + .pop() + ?.toLowerCase() as DefaultExtensionType; + const style = defaultStyles[extension]; + return ( + +
+ +
+
+ {file.name} {file.tokenCount}K +
+
+ ); + })} +
+ )}
@@ -1897,8 +1999,10 @@ function _Chat() { setShowPromptModal(true)} scrollToBottom={scrollToBottom} @@ -1920,7 +2024,7 @@ function _Chat() { />