- {attachImages.map((image, index) => {
- return (
-
-
-
{
- setAttachImages(
- attachImages.filter((_, i) => i !== index),
- );
- }}
- />
+
+ {attachImages.length != 0 && (
+
+ {attachImages.map((image, index) => {
+ return (
+
+
+ {
+ setAttachImages(
+ attachImages.filter((_, i) => i !== index),
+ );
+ }}
+ />
+
-
- );
- })}
-
- )}
+ );
+ })}
+
+ )}
+ {attachFiles.length != 0 && (
+
+ {attachFiles.map((file, index) => {
+ const extension: DefaultExtensionType = file.name
+ .split(".")
+ .pop()
+ ?.toLowerCase() as DefaultExtensionType;
+ const style = defaultStyles[extension];
+ return (
+
+
+
+
+ {attachImages.length == 0 && (
+
+ {file.name} {file.tokenCount}K
+
+ )}
+ {attachImages.length == 1 && (
+
+ {file.name} {file.tokenCount}K
+
+ )}
+ {attachImages.length == 2 && (
+
+ {file.name} {file.tokenCount}K
+
+ )}
+ {attachImages.length == 3 && (
+
+ {file.name} {file.tokenCount}K
+
+ )}
+
+
+ {
+ setAttachFiles(
+ attachFiles.filter((_, i) => i !== index),
+ );
+ }}
+ />
+
+
+ );
+ })}
+
+ )}
+
}
text={Locale.Chat.Send}
diff --git a/app/icons/upload-doc.svg b/app/icons/upload-doc.svg
new file mode 100644
index 00000000000..4a58c92b97e
--- /dev/null
+++ b/app/icons/upload-doc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/store/chat.ts b/app/store/chat.ts
index 968d8cb6422..2fbd56c5e17 100644
--- a/app/store/chat.ts
+++ b/app/store/chat.ts
@@ -6,6 +6,7 @@ import type {
ClientApi,
MultimodalContent,
RequestMessage,
+ UploadFile,
} from "../client/api";
import { getClientApi } from "../client/api";
import { ChatControllerPool } from "../client/controller";
@@ -18,7 +19,7 @@ import {
StoreKey,
} from "../constant";
import Locale, { getLang } from "../locales";
-import { isDalle3, safeLocalStorage } from "../utils";
+import { isDalle3, safeLocalStorage, readFileContent } from "../utils";
import { prettyObject } from "../utils/format";
import { createPersistStore } from "../utils/store";
import { estimateTokenLength } from "../utils/token";
@@ -326,16 +327,75 @@ export const useChatStore = createPersistStore(
get().summarizeSession();
},
- async onUserInput(content: string, attachImages?: string[]) {
+ async onUserInput(
+ content: string,
+ attachImages?: string[],
+ attachFiles?: UploadFile[],
+ ) {
const session = get().currentSession();
const modelConfig = session.mask.modelConfig;
+ //read file content from the url
const userContent = fillTemplateWith(content, modelConfig);
console.log("[User Input] after template: ", userContent);
let mContent: string | MultimodalContent[] = userContent;
+ let displayContent: string | MultimodalContent[] = userContent;
+ displayContent = [
+ {
+ type: "text",
+ text: userContent,
+ },
+ ];
+
+ if (attachFiles && attachFiles.length > 0) {
+ let fileContent = userContent + " Here are the files: \n";
+ for (let i = 0; i < attachFiles.length; i++) {
+ fileContent += attachFiles[i].name + "\n";
+ fileContent += await readFileContent(attachFiles[i]);
+ }
+ mContent = [
+ {
+ type: "text",
+ text: fileContent,
+ },
+ ];
+ displayContent = displayContent.concat(
+ attachFiles.map((file) => {
+ return {
+ type: "file_url",
+ file_url: {
+ url: file.url,
+ name: file.name,
+ tokenCount: file.tokenCount,
+ },
+ };
+ }),
+ );
- if (attachImages && attachImages.length > 0) {
+ if (attachImages && attachImages.length > 0) {
+ mContent = mContent.concat(
+ attachImages.map((url) => {
+ return {
+ type: "image_url",
+ image_url: {
+ url: url,
+ },
+ };
+ }),
+ );
+ displayContent = displayContent.concat(
+ attachImages.map((url) => {
+ return {
+ type: "image_url",
+ image_url: {
+ url: url,
+ },
+ };
+ }),
+ );
+ }
+ } else if (attachImages && attachImages.length > 0) {
mContent = [
{
type: "text",
@@ -352,6 +412,19 @@ export const useChatStore = createPersistStore(
};
}),
);
+ displayContent = displayContent.concat(
+ attachImages.map((url) => {
+ return {
+ type: "image_url",
+ image_url: {
+ url: url,
+ },
+ };
+ }),
+ );
+ } else {
+ mContent = userContent;
+ displayContent = userContent;
}
let userMessage: ChatMessage = createMessage({
role: "user",
@@ -373,7 +446,8 @@ export const useChatStore = createPersistStore(
get().updateCurrentSession((session) => {
const savedUserMessage = {
...userMessage,
- content: mContent,
+ //content: mContent,
+ content: displayContent,
};
session.messages = session.messages.concat([
savedUserMessage,
diff --git a/app/utils.ts b/app/utils.ts
index 6b2f65952c7..c7664f17149 100644
--- a/app/utils.ts
+++ b/app/utils.ts
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { showToast } from "./components/ui-lib";
import Locale from "./locales";
-import { RequestMessage } from "./client/api";
+import { RequestMessage, UploadFile } from "./client/api";
import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant";
import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
@@ -17,6 +17,58 @@ export function trimTopic(topic: string) {
);
}
+export const readFileContent = async (file: UploadFile): Promise
=> {
+ const host_url = new URL(window.location.href);
+ if (!file.url.includes(host_url.host)) {
+ throw new Error(`The URL ${file.url} is not allowed to access.`);
+ }
+ try {
+ const response = await fetch(file.url);
+ if (!response.ok) {
+ throw new Error(
+ `Failed to fetch content from ${file.url}: ${response.statusText}`,
+ );
+ }
+ //const content = await response.text();
+ //const result = file.name + "\n" + content;
+ //return result;
+ return await response.text();
+ } catch (error) {
+ console.error("Error reading file content:", error);
+ throw error;
+ }
+};
+
+export const countTokens = async (file: UploadFile) => {
+ const text = await readFileContent(file);
+ let totalTokens = 0;
+
+ for (let i = 0; i < text.length; i++) {
+ const char = text[i];
+ const nextChar = text[i + 1];
+
+ if (char === " " && nextChar === " ") {
+ totalTokens += 0.081;
+ } else if ("NORabcdefghilnopqrstuvy ".includes(char)) {
+ totalTokens += 0.202;
+ } else if ("CHLMPQSTUVfkmspwx".includes(char)) {
+ totalTokens += 0.237;
+ } else if ("-.ABDEFGIKWY_\\r\\tz{ü".includes(char)) {
+ totalTokens += 0.304;
+ } else if ("!{{input}}(/;=JX`j\\n}ö".includes(char)) {
+ totalTokens += 0.416;
+ } else if ('"#%)*+56789<>?@Z[\\]^|§«äç’'.includes(char)) {
+ totalTokens += 0.479;
+ } else if (",01234:~Üß".includes(char) || char.charCodeAt(0) > 255) {
+ totalTokens += 0.658;
+ } else {
+ totalTokens += 0.98;
+ }
+ }
+ const totalTokenCount: number = +(totalTokens / 1000).toFixed(2);
+ return totalTokenCount;
+};
+
export async function copyToClipboard(text: string) {
try {
if (window.__TAURI__) {
@@ -250,6 +302,19 @@ export function getMessageImages(message: RequestMessage): string[] {
return urls;
}
+export function getMessageFiles(message: RequestMessage): UploadFile[] {
+ if (typeof message.content === "string") {
+ return [];
+ }
+ const files: UploadFile[] = [];
+ for (const c of message.content) {
+ if (c.type === "file_url" && c.file_url) {
+ files.push(c.file_url);
+ }
+ }
+ return files;
+}
+
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)
diff --git a/package.json b/package.json
index 8696f83b558..dba7fdb496d 100644
--- a/package.json
+++ b/package.json
@@ -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"
-}
\ No newline at end of file
+}
diff --git a/yarn.lock b/yarn.lock
index 7e7dd3484f1..d9edacf0a37 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"