feat: add UploadFile interface for handling file uploads

This commit is contained in:
dakai 2024-10-06 01:01:02 +08:00
parent 10f6ef0fb1
commit f9e4f02d53
5 changed files with 103 additions and 16 deletions

View File

@ -36,9 +36,15 @@ export interface MultimodalContent {
}; };
file_url?: { file_url?: {
url: string; url: string;
name: string;
}; };
} }
export interface UploadFile {
name: string;
url: string;
}
export interface RequestMessage { export interface RequestMessage {
role: MessageRole; role: MessageRole;
content: string | MultimodalContent[]; content: string | MultimodalContent[];

View File

@ -71,16 +71,52 @@
border-radius: 5px; border-radius: 5px;
margin-right: 10px; margin-right: 10px;
.attach-file-name-full {
max-width:calc(62vw);
display:flex;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.attach-file-name-half {
max-width:calc(45vw);
display:flex;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.attach-file-name-less {
max-width:calc(28vw);
display:flex;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.attach-file-name-min {
max-width:calc(12vw);
display:flex;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.attach-file-icon { .attach-file-icon {
min-width: 16px; min-width: 16px;
max-width: 16px; max-width: 16px;
} }
.attach-file-icon:hover {
opacity: 0;
}
.attach-image-mask { .attach-image-mask {
width: 100%; width: 100%;
height: 100%; height: 100%;
opacity: 0; opacity: 0;
transition: all ease 0.2s; transition: all ease 0.2s;
position: absolute;
} }
.attach-image-mask:hover { .attach-image-mask:hover {
@ -88,8 +124,8 @@
} }
.delete-image { .delete-image {
width: 24px; width: 16px;
height: 24px; height: 20px;
cursor: pointer; cursor: pointer;
border-radius: 5px; border-radius: 5px;
float: left; float: left;

View File

@ -69,12 +69,15 @@ import {
useMobileScreen, useMobileScreen,
getMessageTextContent, getMessageTextContent,
getMessageImages, getMessageImages,
getMessageFiles,
isVisionModel, isVisionModel,
isDalle3, isDalle3,
showPlugins, showPlugins,
safeLocalStorage, safeLocalStorage,
} from "../utils"; } from "../utils";
import type { UploadFile } from "../client/api";
import { uploadImage as uploadImageRemote } from "@/app/utils/chat"; import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
@ -448,7 +451,7 @@ export function ChatActions(props: {
uploadDocument: () => void; uploadDocument: () => void;
uploadImage: () => void; uploadImage: () => void;
setAttachImages: (images: string[]) => void; setAttachImages: (images: string[]) => void;
setAttachFiles: (files: string[]) => void; setAttachFiles: (files: UploadFile[]) => void;
setUploading: (uploading: boolean) => void; setUploading: (uploading: boolean) => void;
showPromptModal: () => void; showPromptModal: () => void;
scrollToBottom: () => void; scrollToBottom: () => void;
@ -957,7 +960,7 @@ function _Chat() {
const isMobileScreen = useMobileScreen(); const isMobileScreen = useMobileScreen();
const navigate = useNavigate(); const navigate = useNavigate();
const [attachImages, setAttachImages] = useState<string[]>([]); const [attachImages, setAttachImages] = useState<string[]>([]);
const [attachFiles, setAttachFiles] = useState<string[]>([]); const [attachFiles, setAttachFiles] = useState<UploadFile[]>([]);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
// prompt hints // prompt hints
@ -1475,11 +1478,11 @@ function _Chat() {
); );
async function uploadDocument() { async function uploadDocument() {
const files: string[] = []; const files: UploadFile[] = [];
files.push(...attachFiles); files.push(...attachFiles);
files.push( files.push(
...(await new Promise<string[]>((res, rej) => { ...(await new Promise<UploadFile[]>((res, rej) => {
const fileInput = document.createElement("input"); const fileInput = document.createElement("input");
fileInput.type = "file"; fileInput.type = "file";
fileInput.accept = "text/*"; fileInput.accept = "text/*";
@ -1487,12 +1490,12 @@ function _Chat() {
fileInput.onchange = (event: any) => { fileInput.onchange = (event: any) => {
setUploading(true); setUploading(true);
const files = event.target.files; const files = event.target.files;
const imagesData: string[] = []; const imagesData: UploadFile[] = [];
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const file = event.target.files[i]; const file = event.target.files[i];
uploadImageRemote(file) uploadImageRemote(file)
.then((dataUrl) => { .then((dataUrl) => {
imagesData.push(dataUrl); imagesData.push({ name: file.name, url: dataUrl });
if ( if (
imagesData.length === 3 || imagesData.length === 3 ||
imagesData.length === files.length imagesData.length === files.length
@ -1937,6 +1940,13 @@ function _Chat() {
})} })}
</div> </div>
)} )}
{getMessageFiles(message).length > 0 && (
<div>
{getMessageFiles(message).map((file, index) => {
return <div key={index}></div>;
})}
</div>
)}
</div> </div>
<div className={styles["chat-message-action-date"]}> <div className={styles["chat-message-action-date"]}>
@ -2032,7 +2042,7 @@ function _Chat() {
{attachFiles.length != 0 && ( {attachFiles.length != 0 && (
<div className={styles["attach-files"]}> <div className={styles["attach-files"]}>
{attachFiles.map((file, index) => { {attachFiles.map((file, index) => {
const extension: DefaultExtensionType = file const extension: DefaultExtensionType = file.url
.split(".") .split(".")
.pop() .pop()
?.toLowerCase() as DefaultExtensionType; ?.toLowerCase() as DefaultExtensionType;
@ -2045,7 +2055,27 @@ function _Chat() {
> >
<FileIcon {...style} /> <FileIcon {...style} />
</div> </div>
<span>{extension}</span> {attachImages.length == 0 && (
<div className={styles["attach-file-name-full"]}>
{file.name}
</div>
)}
{attachImages.length == 1 && (
<div className={styles["attach-file-name-half"]}>
{file.name}
</div>
)}
{attachImages.length == 2 && (
<div className={styles["attach-file-name-less"]}>
{file.name}
</div>
)}
{attachImages.length == 3 && (
<div className={styles["attach-file-name-min"]}>
{file.name}
</div>
)}
<div className={styles["attach-image-mask"]}> <div className={styles["attach-image-mask"]}>
<DeleteImageButton <DeleteImageButton
deleteImage={() => { deleteImage={() => {

View File

@ -6,6 +6,7 @@ import type {
ClientApi, ClientApi,
MultimodalContent, MultimodalContent,
RequestMessage, RequestMessage,
UploadFile,
} from "../client/api"; } from "../client/api";
import { getClientApi } from "../client/api"; import { getClientApi } from "../client/api";
import { ChatControllerPool } from "../client/controller"; import { ChatControllerPool } from "../client/controller";
@ -153,12 +154,12 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
return output; return output;
} }
const readFileContent = async (url: string): Promise<string> => { const readFileContent = async (file: UploadFile): Promise<string> => {
try { try {
const response = await fetch(url); const response = await fetch(file.url);
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(
`Failed to fetch content from ${url}: ${response.statusText}`, `Failed to fetch content from ${file.url}: ${response.statusText}`,
); );
} }
return await response.text(); return await response.text();
@ -344,7 +345,7 @@ export const useChatStore = createPersistStore(
async onUserInput( async onUserInput(
content: string, content: string,
attachImages?: string[], attachImages?: string[],
attachFiles?: string[], attachFiles?: UploadFile[],
) { ) {
const session = get().currentSession(); const session = get().currentSession();
const modelConfig = session.mask.modelConfig; const modelConfig = session.mask.modelConfig;
@ -386,11 +387,12 @@ export const useChatStore = createPersistStore(
}), }),
); );
displayContent = displayContent.concat( displayContent = displayContent.concat(
attachFiles.map((url) => { attachFiles.map((file) => {
return { return {
type: "file_url", type: "file_url",
file_url: { file_url: {
url: url, url: file.url,
name: file.name,
}, },
}; };
}), }),

View File

@ -250,6 +250,19 @@ export function getMessageImages(message: RequestMessage): string[] {
return urls; return urls;
} }
export function getMessageFiles(message: RequestMessage): string[] {
if (typeof message.content === "string") {
return [];
}
const urls: string[] = [];
for (const c of message.content) {
if (c.type === "file_url") {
urls.push(c.file_url?.url ?? "");
}
}
return urls;
}
export function isVisionModel(model: string) { export function isVisionModel(model: string) {
// Note: This is a better way using the TypeScript feature instead of `&&` or `||` (ts v5.5.0-dev.20240314 I've been using) // Note: This is a better way using the TypeScript feature instead of `&&` or `||` (ts v5.5.0-dev.20240314 I've been using)