feat: add UploadFile interface for handling file uploads
This commit is contained in:
parent
10f6ef0fb1
commit
f9e4f02d53
|
@ -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[];
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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={() => {
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
13
app/utils.ts
13
app/utils.ts
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue