feat: add functionality to upload documents in chat

This commit is contained in:
dakai 2024-10-05 10:27:22 +08:00
parent 2474d5b6d2
commit 8878e238d2
5 changed files with 193 additions and 30 deletions

View File

@ -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,53 @@
}
}
.attach-files {
//position: absolute;
//left: 30px;
//bottom: 32px;
display: flex;
flex-direction: column;
row-gap: 11px;
}
.attach-file {
cursor: default;
//width: 64px;
width: 14px;
height: 14px;
//border: rgba($color: #888, $alpha: 0.2) 1px solid;
border-radius: 5px;
margin-right: 10px;
background-size: cover;
background-position: center;
background-color: var(--white);
.attach-image-mask {
width: 100%;
height: 100%;
opacity: 0;
transition: all ease 0.2s;
}
.attach-image-mask:hover {
opacity: 1;
}
.delete-image {
width: 24px;
height: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 5px;
float: right;
background-color: var(--white);
}
}
.chat-input-actions {
display: flex;
flex-wrap: wrap;
@ -693,4 +748,4 @@
.shortcut-key span {
font-size: 12px;
color: var(--black);
}
}

View File

@ -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,
@ -96,6 +97,7 @@ import {
showToast,
} from "./ui-lib";
import { useNavigate } from "react-router-dom";
import { FileIcon, defaultStyles } from "react-file-icon";
import {
CHAT_PAGE_SIZE,
DEFAULT_TTS_ENGINE,
@ -442,8 +444,10 @@ function useScrollToBottom(
}
export function ChatActions(props: {
uploadDocument: () => void;
uploadImage: () => void;
setAttachImages: (images: string[]) => void;
setAttachFiles: (files: string[]) => void;
setUploading: (uploading: boolean) => void;
showPromptModal: () => void;
scrollToBottom: () => void;
@ -502,7 +506,8 @@ export function ChatActions(props: {
}, [models, currentModel, currentProviderName]);
const [showModelSelector, setShowModelSelector] = useState(false);
const [showPluginSelector, setShowPluginSelector] = useState(false);
const [showUploadImage, setShowUploadImage] = useState(false);
// TODO: remember to make it false
const [showUploadImage, setShowUploadImage] = useState(true);
const [showSizeSelector, setShowSizeSelector] = useState(false);
const [showQualitySelector, setShowQualitySelector] = useState(false);
@ -521,7 +526,8 @@ export function ChatActions(props: {
useEffect(() => {
const show = isVisionModel(currentModel);
setShowUploadImage(show);
//NOTE: temporary disable upload image
//setShowUploadImage(show);
if (!show) {
props.setAttachImages([]);
props.setUploading(false);
@ -577,6 +583,11 @@ export function ChatActions(props: {
icon={props.uploading ? <LoadingButtonIcon /> : <ImageIcon />}
/>
)}
<ChatAction
onClick={props.uploadDocument}
text={"Upload Document"}
icon={props.uploading ? <LoadingButtonIcon /> : <UploadDocIcon />}
/>
<ChatAction
onClick={nextTheme}
text={Locale.Chat.InputActions.Theme[theme]}
@ -945,6 +956,7 @@ function _Chat() {
const isMobileScreen = useMobileScreen();
const navigate = useNavigate();
const [attachImages, setAttachImages] = useState<string[]>([]);
const [attachFiles, setAttachFiles] = useState<string[]>([]);
const [uploading, setUploading] = useState(false);
// prompt hints
@ -1460,6 +1472,51 @@ function _Chat() {
[attachImages, chatStore],
);
async function uploadDocument() {
const files: string[] = [];
files.push(...attachFiles);
files.push(
...(await new Promise<string[]>((res, rej) => {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = "text/*";
fileInput.multiple = true;
fileInput.onchange = (event: any) => {
setUploading(true);
const files = event.target.files;
const imagesData: string[] = [];
for (let i = 0; i < files.length; i++) {
const file = event.target.files[i];
uploadImageRemote(file)
.then((dataUrl) => {
imagesData.push(dataUrl);
if (
imagesData.length === 3 ||
imagesData.length === files.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);
console.log("upload files: ", files);
}
async function uploadImage() {
const images: string[] = [];
images.push(...attachImages);
@ -1897,8 +1954,10 @@ function _Chat() {
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
<ChatActions
uploadDocument={uploadDocument}
uploadImage={uploadImage}
setAttachImages={setAttachImages}
setAttachFiles={setAttachFiles}
setUploading={setUploading}
showPromptModal={() => setShowPromptModal(true)}
scrollToBottom={scrollToBottom}
@ -1920,7 +1979,7 @@ function _Chat() {
/>
<label
className={`${styles["chat-input-panel-inner"]} ${
attachImages.length != 0
attachImages.length != 0 || attachFiles.length != 0
? styles["chat-input-panel-inner-attach"]
: ""
}`}
@ -1944,29 +2003,55 @@ function _Chat() {
fontFamily: config.fontFamily,
}}
/>
{attachImages.length != 0 && (
<div className={styles["attach-images"]}>
{attachImages.map((image, index) => {
return (
<div
key={index}
className={styles["attach-image"]}
style={{ backgroundImage: `url("${image}")` }}
>
<div className={styles["attach-image-mask"]}>
<DeleteImageButton
deleteImage={() => {
setAttachImages(
attachImages.filter((_, i) => i !== index),
);
}}
/>
<div className={styles["attachments"]}>
{attachImages.length != 0 && (
<div className={styles["attach-images"]}>
{attachImages.map((image, index) => {
return (
<div
key={index}
className={styles["attach-image"]}
style={{ backgroundImage: `url("${image}")` }}
>
<div className={styles["attach-image-mask"]}>
<DeleteImageButton
deleteImage={() => {
setAttachImages(
attachImages.filter((_, i) => i !== index),
);
}}
/>
</div>
</div>
</div>
);
})}
</div>
)}
);
})}
</div>
)}
{attachFiles.length != 0 && (
<div className={styles["attach-files"]}>
{attachFiles.map((file, index) => {
return (
<div
key={index}
className={styles["attach-file"]}
style={{ backgroundImage: `url("${file}")` }}
>
<FileIcon extension="csv" {...defaultStyles["csv"]} />
<div className={styles["attach-image-mask"]}>
<DeleteImageButton
deleteImage={() => {
setAttachFiles(
attachFiles.filter((_, i) => i !== index),
);
}}
/>
</div>
</div>
);
})}
</div>
)}
</div>
<IconButton
icon={<SendWhiteIcon />}
text={Locale.Chat.Send}

1
app/icons/upload-doc.svg Normal file
View File

@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><defs><image width="32" height="40" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAoCAMAAACo9wirAAAAAXNSR0IB2cksfwAAAIpQTFRFAAAAVVVVUVFRUVFRUFBQUVFRUFBQUVFRf39/UFBQUVFRUFBQZmZmVVVVUVFRAAAAUVFRUFBQUVFRUVFRUFBQX19fUFBQUVFRUVFRU1NTT09PUVFRUVFRUVFRUlJSUFBQTU1NT09PUVFRUlJSUFBQUVFRUVFRUVFRUFBQUFBQUFBQUFBQUFBQUVFReUEd1wAAAC50Uk5TAAmB3Pz/+ZACxJx1BQadAZaed4CRCH7bMjEw7O81Iu0hdq8l4NXWhveF83jawwNJtaIAAAEdSURBVHicpdRrSwJBFAbg8+7WskarlIqhtBAS1f//LwaFIJkWiX3wgotbTrNIeubMBaL32+x5dnYunAWhCh2jR4UxTL9I5HTHBKJY1k2BOKIEK1YUAiegdEkCMAH9oCQT1DZMuMAZ1lptCy8oG5U43xReQEy4ARMecBQW+N12muz3YgF9cFnJhAWaCz7Sn5JAXB4sQG1gzq7fBmJLAlzNWtFbAHTxQR2MvaCqExcC7OtcmCDH9CBHLnDzSt14TNffU0rWLtAHVtsF1ZNsp4a+beqDrs9D5/B/cLmk7DME8gn1XkJA5E/gDkNW6eNRgEbv2Xj3diBneFDhGfxrkO3PkxWkcOgDRz1/0msXfSBz4fqJsdy/Q/YBi2oqNfsBQGqACm/REAgAAAAASUVORK5CYII="/></defs><style></style><use href="#img1" transform="matrix(.333,0,0,.333,2.667,1.333)"/></svg>

After

Width:  |  Height:  |  Size: 1020 B

View File

@ -22,6 +22,7 @@
"@hello-pangea/dnd": "^16.5.0",
"@next/third-parties": "^14.1.0",
"@svgr/webpack": "^6.5.1",
"@types/react-file-icon": "^1.0.4",
"@vercel/analytics": "^0.1.11",
"@vercel/speed-insights": "^1.0.2",
"axios": "^1.7.5",
@ -31,14 +32,15 @@
"html-to-image": "^1.11.11",
"idb-keyval": "^6.2.1",
"lodash-es": "^4.17.21",
"mermaid": "^10.6.1",
"markdown-to-txt": "^2.0.1",
"mermaid": "^10.6.1",
"nanoid": "^5.0.3",
"next": "^14.1.1",
"node-fetch": "^3.3.1",
"openapi-client-axios": "^7.5.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-file-icon": "^1.5.0",
"react-markdown": "^8.0.7",
"react-router-dom": "^6.15.0",
"rehype-highlight": "^6.0.0",
@ -80,4 +82,4 @@
"lint-staged/yaml": "^2.2.2"
},
"packageManager": "yarn@1.22.19"
}
}

View File

@ -1762,6 +1762,13 @@
dependencies:
"@types/react" "*"
"@types/react-file-icon@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@types/react-file-icon/-/react-file-icon-1.0.4.tgz#6825b0e6b8ab639f7f25a6cd52499650d3afcd89"
integrity sha512-c1mIklUDaxm9odxf8RTiy/EAxsblZliJ86EKIOAyuafP9eK3iudyn4ATv53DX6ZvgGymc7IttVNm97LTGnTiYA==
dependencies:
"@types/react" "*"
"@types/react-katex@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/react-katex/-/react-katex-3.0.0.tgz#119a902bff10eb52f449fac744aaed8c4909391f"
@ -2416,6 +2423,11 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colord@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
colorette@^2.0.19:
version "2.0.19"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
@ -5404,7 +5416,7 @@ prettier@^3.0.2:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.2.tgz#78fcecd6d870551aa5547437cdae39d4701dca5b"
integrity sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==
prop-types@^15.0.0, prop-types@^15.8.1:
prop-types@^15.0.0, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -5453,6 +5465,14 @@ react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-file-icon@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/react-file-icon/-/react-file-icon-1.5.0.tgz#cccc8827d927291b8a52fab41afbe5b3625ddbf4"
integrity sha512-6K2/nAI69CS838HOS+4S95MLXwf1neWywek1FgqcTFPTYjnM8XT7aBLz4gkjoqQKY9qPhu3A2tu+lvxhmZYY9w==
dependencies:
colord "^2.9.3"
prop-types "^15.7.2"
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"