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() { /> )} - + {attachFiles.length != 0 && ( + + {attachFiles.map((file, index) => { + return ( + + + { + setAttachFiles( + attachFiles.filter((_, i) => i !== index), + ); + }} + /> + + ${file} + + ); + })} + + )} {config.sttConfig.enable ? ( } diff --git a/app/config/server.ts b/app/config/server.ts index 76a14b697..9ee6597b8 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -111,5 +111,7 @@ export const getServerSideConfig = () => { !!process.env.NEXT_PUBLIC_ENABLE_NODEJS_PLUGIN && !process.env.R2_ACCOUNT_ID && !process.env.S3_ENDPOINT, + + isEnableRAG: !!process.env.NEXT_PUBLIC_ENABLE_RAG, }; }; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 0595fa30d..543d7bc37 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -68,6 +68,7 @@ const cn = { EnablePlugins: "开启插件", DisablePlugins: "关闭插件", UploadImage: "上传图片", + UploadFle: "上传文件", }, Rename: "重命名对话", Typing: "正在输入…", diff --git a/app/utils.ts b/app/utils.ts index 1fb3a1649..2d4155501 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -296,3 +296,9 @@ export function isVisionModel(model: string) { return visionKeywords.some((keyword) => model.includes(keyword)); } + +export function isSupportRAGModel(modelName: string) { + return DEFAULT_MODELS.filter((model) => model.provider.id === "openai").some( + (model) => model.name === modelName, + ); +} diff --git a/package.json b/package.json index 362647734..90c75f216 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "https-proxy-agent": "^7.0.2", "langchain": "0.1.20", "mermaid": "^10.6.1", + "mime": "^4.0.1", "nanoid": "^5.0.3", "next": "^13.4.9", "node-fetch": "^3.3.1", @@ -83,4 +84,4 @@ "openai": "4.28.4" }, "packageManager": "yarn@1.22.19" -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 4368ff0c6..e825e351e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6274,6 +6274,11 @@ mime-types@^2.1.12, mime-types@^2.1.27: dependencies: mime-db "1.52.0" +mime@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-4.0.1.tgz#ad7563d1bfe30253ad97dedfae2b1009d01b9470" + integrity sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"