From 80a077a3dbaf82a6afb6e1cbb2a9b76cc5afd4b4 Mon Sep 17 00:00:00 2001 From: Hk-Gosuto Date: Sun, 31 Mar 2024 23:07:17 +0800 Subject: [PATCH 1/6] feat: support other type file upload --- app/api/file/[...path]/route.ts | 23 +++++--- app/api/file/upload/route.ts | 18 +++--- app/components/chat.module.scss | 98 ++++++++++++++++++++++++++----- app/components/chat.tsx | 100 +++++++++++++++++++++++++++++++- app/config/server.ts | 2 + app/locales/cn.ts | 1 + app/utils.ts | 6 ++ package.json | 3 +- yarn.lock | 5 ++ 9 files changed, 223 insertions(+), 33 deletions(-) diff --git a/app/api/file/[...path]/route.ts b/app/api/file/[...path]/route.ts index f6fbfd52f..e253389a0 100644 --- a/app/api/file/[...path]/route.ts +++ b/app/api/file/[...path]/route.ts @@ -2,6 +2,7 @@ import { getServerSideConfig } from "@/app/config/server"; import LocalFileStorage from "@/app/utils/local_file_storage"; import S3FileStorage from "@/app/utils/s3_file_storage"; import { NextRequest, NextResponse } from "next/server"; +import mime from "mime"; async function handle( req: NextRequest, @@ -13,19 +14,27 @@ async function handle( try { const serverConfig = getServerSideConfig(); + const fileName = params.path[0]; + const contentType = mime.getType(fileName); + if (serverConfig.isStoreFileToLocal) { - var fileBuffer = await LocalFileStorage.get(params.path[0]); + var fileBuffer = await LocalFileStorage.get(fileName); return new Response(fileBuffer, { headers: { - "Content-Type": "image/png", + "Content-Type": contentType ?? "application/octet-stream", }, }); } else { - var file = await S3FileStorage.get(params.path[0]); - return new Response(file?.transformToWebStream(), { - headers: { - "Content-Type": "image/png", - }, + var file = await S3FileStorage.get(fileName); + if (file) { + return new Response(file?.transformToWebStream(), { + headers: { + "Content-Type": contentType ?? "application/octet-stream", + }, + }); + } + return new Response("not found", { + status: 404, }); } } catch (e) { diff --git a/app/api/file/upload/route.ts b/app/api/file/upload/route.ts index 65991477a..62b3af64f 100644 --- a/app/api/file/upload/route.ts +++ b/app/api/file/upload/route.ts @@ -4,6 +4,7 @@ import { auth } from "@/app/api/auth"; import LocalFileStorage from "@/app/utils/local_file_storage"; import { getServerSideConfig } from "@/app/config/server"; import S3FileStorage from "@/app/utils/s3_file_storage"; +import path from "path"; async function handle(req: NextRequest) { if (req.method === "OPTIONS") { @@ -19,20 +20,21 @@ async function handle(req: NextRequest) { try { const formData = await req.formData(); - const image = formData.get("file") as File; + const file = formData.get("file") as File; + const originalFileName = file?.name; - const imageReader = image.stream().getReader(); - const imageData: number[] = []; + const fileReader = file.stream().getReader(); + const fileData: number[] = []; while (true) { - const { done, value } = await imageReader.read(); + const { done, value } = await fileReader.read(); if (done) break; - imageData.push(...value); + fileData.push(...value); } - const buffer = Buffer.from(imageData); - - var fileName = `${Date.now()}.png`; + const buffer = Buffer.from(fileData); + const fileType = path.extname(originalFileName).slice(1); + var fileName = `${Date.now()}.${fileType}`; var filePath = ""; const serverConfig = getServerSideConfig(); if (serverConfig.isStoreFileToLocal) { diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index d9d97666e..66f254a92 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -1,5 +1,66 @@ @import "../styles/animation.scss"; +.attach-files { + position: absolute; + left: 30px; + bottom: 32px; + display: flex; +} + +.attach-file { + cursor: default; + width: 64px; + height: 64px; + border: rgba($color: #888, $alpha: 0.2) 1px solid; + border-radius: 5px; + margin-right: 10px; + background-size: cover; + background-position: center; + background-color: var(--second); + + .attach-file-mask { + width: 100%; + height: 100%; + opacity: 0; + transition: all ease 0.2s; + } + + .attach-file-mask:hover { + opacity: 1; + } + + .delete-file { + width: 24px; + height: 24px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + float: right; + background-color: var(--white); + } + + .attach-file-name { + font-size: 12px; + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + padding: 4px; + box-sizing: border-box; + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + // line-height: 1.2; + // max-height: 2.4em; + position: absolute; + top: 0; + left: 0; + } +} + .attach-images { position: absolute; left: 30px; @@ -232,10 +293,12 @@ animation: slide-in ease 0.3s; - $linear: linear-gradient(to right, - rgba(0, 0, 0, 0), - rgba(0, 0, 0, 1), - rgba(0, 0, 0, 0)); + $linear: linear-gradient( + to right, + rgba(0, 0, 0, 0), + rgba(0, 0, 0, 1), + rgba(0, 0, 0, 0) + ); mask-image: $linear; @mixin show { @@ -368,7 +431,7 @@ } } -.chat-message-user>.chat-message-container { +.chat-message-user > .chat-message-container { align-items: flex-end; } @@ -482,23 +545,27 @@ border: rgba($color: #888, $alpha: 0.2) 1px solid; } - @media only screen and (max-width: 600px) { - $calc-image-width: calc(100vw/3*2/var(--image-count)); + $calc-image-width: calc(100vw / 3 * 2 / var(--image-count)); .chat-message-item-image-multi { width: $calc-image-width; height: $calc-image-width; } - + .chat-message-item-image { - max-width: calc(100vw/3*2); + max-width: calc(100vw / 3 * 2); } } @media screen and (min-width: 600px) { - $max-image-width: calc(calc(1200px - var(--sidebar-width))/3*2/var(--image-count)); - $image-width: calc(calc(var(--window-width) - var(--sidebar-width))/3*2/var(--image-count)); + $max-image-width: calc( + calc(1200px - var(--sidebar-width)) / 3 * 2 / var(--image-count) + ); + $image-width: calc( + calc(var(--window-width) - var(--sidebar-width)) / 3 * 2 / + var(--image-count) + ); .chat-message-item-image-multi { width: $image-width; @@ -508,7 +575,7 @@ } .chat-message-item-image { - max-width: calc(calc(1200px - var(--sidebar-width))/3*2); + max-width: calc(calc(1200px - var(--sidebar-width)) / 3 * 2); } } @@ -526,7 +593,7 @@ z-index: 1; } -.chat-message-user>.chat-message-container>.chat-message-item { +.chat-message-user > .chat-message-container > .chat-message-item { background-color: var(--second); &:hover { @@ -637,7 +704,8 @@ min-height: 68px; } -.chat-input:focus {} +.chat-input:focus { +} .chat-input-send { background-color: var(--primary); @@ -656,4 +724,4 @@ .chat-input-send { bottom: 30px; } -} \ No newline at end of file +} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 6c33f6bc5..701edf89f 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -69,6 +69,7 @@ import { isVisionModel, compressImage, isFirefox, + isSupportRAGModel, } from "../utils"; import dynamic from "next/dynamic"; @@ -116,6 +117,7 @@ import { SpeechApi, WebTranscriptionApi, } from "../utils/speech"; +import { getServerSideConfig } from "../config/server"; const ttsPlayer = createTTSPlayer(); @@ -460,6 +462,8 @@ function useScrollToBottom( export function ChatActions(props: { uploadImage: () => void; setAttachImages: (images: string[]) => void; + uploadFile: () => void; + setAttachFiles: (files: string[]) => void; setUploading: (uploading: boolean) => void; showPromptModal: () => void; scrollToBottom: () => void; @@ -503,9 +507,15 @@ export function ChatActions(props: { const [showModelSelector, setShowModelSelector] = useState(false); const [showUploadImage, setShowUploadImage] = useState(false); + const [showUploadFile, setShowUploadFile] = useState(false); + useEffect(() => { const show = isVisionModel(currentModel); setShowUploadImage(show); + const serverConfig = getServerSideConfig(); + setShowUploadFile( + serverConfig.isEnableRAG && !show && isSupportRAGModel(currentModel), + ); if (!show) { props.setAttachImages([]); props.setUploading(false); @@ -555,6 +565,14 @@ export function ChatActions(props: { icon={props.uploading ? : } /> )} + + {showUploadFile && ( + : } + /> + )} void }) { ); } +export function DeleteFileButton(props: { deleteFile: () => void }) { + return ( +
+ +
+ ); +} + function _Chat() { type RenderMessage = ChatMessage & { preview?: boolean }; @@ -743,6 +769,7 @@ function _Chat() { const navigate = useNavigate(); const [attachImages, setAttachImages] = useState([]); const [uploading, setUploading] = useState(false); + const [attachFiles, setAttachFiles] = useState([]); // prompt hints const promptStore = usePromptStore(); @@ -851,6 +878,7 @@ function _Chat() { .onUserInput(userInput, attachImages) .then(() => setIsLoading(false)); setAttachImages([]); + setAttachFiles([]); localStorage.setItem(LAST_INPUT_KEY, userInput); setUserInput(""); setPromptHints([]); @@ -1324,6 +1352,53 @@ function _Chat() { setAttachImages(images); } + async function uploadFile() { + const uploadFiles: string[] = []; + uploadFiles.push(...attachFiles); + + uploadFiles.push( + ...(await new Promise((res, rej) => { + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = ".pdf,.txt,.json,.csv,.md"; + fileInput.multiple = true; + fileInput.onchange = (event: any) => { + setUploading(true); + const files = event.target.files; + const api = new ClientApi(); + const fileDatas: string[] = []; + for (let i = 0; i < files.length; i++) { + const file = event.target.files[i]; + api.file + .upload(file) + .then((fileInfo) => { + console.log(fileInfo); + fileDatas.push(fileInfo); + if ( + fileDatas.length === 5 || + fileDatas.length === files.length + ) { + setUploading(false); + res(fileDatas); + } + }) + .catch((e) => { + setUploading(false); + rej(e); + }); + } + }; + fileInput.click(); + })), + ); + + const filesLength = uploadFiles.length; + if (filesLength > 5) { + uploadFiles.splice(5, filesLength - 5); + } + setAttachFiles(uploadFiles); + } + return (
@@ -1632,6 +1707,8 @@ function _Chat() { setShowPromptModal(true)} scrollToBottom={scrollToBottom} @@ -1651,7 +1728,7 @@ function _Chat() { />