mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-01 15:46:39 +08:00
Merge e86a39047c
into 8ad63a6c25
This commit is contained in:
commit
23d299c4ab
@ -29,11 +29,22 @@ export const TTSModels = ["tts-1", "tts-1-hd"] as const;
|
|||||||
export type ChatModel = ModelType;
|
export type ChatModel = ModelType;
|
||||||
|
|
||||||
export interface MultimodalContent {
|
export interface MultimodalContent {
|
||||||
type: "text" | "image_url";
|
type: "text" | "image_url" | "file_url";
|
||||||
text?: string;
|
text?: string;
|
||||||
image_url?: {
|
image_url?: {
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
file_url?: {
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
tokenCount?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadFile {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
tokenCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestMessage {
|
export interface RequestMessage {
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
@import "../styles/animation.scss";
|
@import "../styles/animation.scss";
|
||||||
|
|
||||||
.attach-images {
|
.attachments {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 30px;
|
left: 30px;
|
||||||
bottom: 32px;
|
bottom: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-images {
|
||||||
|
//position: absolute;
|
||||||
|
//left: 30px;
|
||||||
|
//bottom: 32px;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attach-image {
|
.attach-image {
|
||||||
@ -42,6 +50,86 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attach-files {
|
||||||
|
//position: absolute;
|
||||||
|
//left: 30px;
|
||||||
|
//bottom: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
//row-gap: 11px;
|
||||||
|
max-height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-file {
|
||||||
|
cursor: default;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
column-gap: 4px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
color: var(--black);
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
%attach-file-name-common {
|
||||||
|
display:flex;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
}
|
||||||
|
.attach-file-name-full {
|
||||||
|
@extend %attach-file-name-common;
|
||||||
|
max-width:calc(62vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-file-name-half {
|
||||||
|
@extend %attach-file-name-common;
|
||||||
|
max-width:calc(45vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-file-name-less {
|
||||||
|
@extend %attach-file-name-common;
|
||||||
|
max-width:calc(28vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-file-name-min {
|
||||||
|
@extend %attach-file-name-common;
|
||||||
|
max-width:calc(12vw);
|
||||||
|
}
|
||||||
|
.attach-file-icon {
|
||||||
|
min-width: 16px;
|
||||||
|
max-width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-file-icon:hover {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-image-mask {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-image-mask:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-image {
|
||||||
|
width: 16px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 5px;
|
||||||
|
float: left;
|
||||||
|
background-color: var(--white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.chat-input-actions {
|
.chat-input-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -471,6 +559,32 @@
|
|||||||
border: rgba($color: #888, $alpha: 0.2) 1px solid;
|
border: rgba($color: #888, $alpha: 0.2) 1px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-message-item-files {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-item-file {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
column-gap: 6px;
|
||||||
|
|
||||||
|
}
|
||||||
|
.chat-message-item-file-icon {
|
||||||
|
max-width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-item-file-name {
|
||||||
|
max-width:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@media only screen and (max-width: 600px) {
|
@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));
|
||||||
@ -693,4 +807,4 @@
|
|||||||
.shortcut-key span {
|
.shortcut-key span {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--black);
|
color: var(--black);
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,7 @@ import StyleIcon from "../icons/palette.svg";
|
|||||||
import PluginIcon from "../icons/plugin.svg";
|
import PluginIcon from "../icons/plugin.svg";
|
||||||
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
||||||
import ReloadIcon from "../icons/reload.svg";
|
import ReloadIcon from "../icons/reload.svg";
|
||||||
|
import UploadDocIcon from "../icons/upload-doc.svg";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
@ -68,13 +69,18 @@ import {
|
|||||||
useMobileScreen,
|
useMobileScreen,
|
||||||
getMessageTextContent,
|
getMessageTextContent,
|
||||||
getMessageImages,
|
getMessageImages,
|
||||||
|
getMessageFiles,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
isDalle3,
|
isDalle3,
|
||||||
showPlugins,
|
showPlugins,
|
||||||
safeLocalStorage,
|
safeLocalStorage,
|
||||||
|
countTokens,
|
||||||
} 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 { uploadImage as uploadFileRemote } from "@/app/utils/chat";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
@ -96,6 +102,8 @@ import {
|
|||||||
showToast,
|
showToast,
|
||||||
} from "./ui-lib";
|
} from "./ui-lib";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { FileIcon, defaultStyles } from "react-file-icon";
|
||||||
|
import type { DefaultExtensionType } from "react-file-icon";
|
||||||
import {
|
import {
|
||||||
CHAT_PAGE_SIZE,
|
CHAT_PAGE_SIZE,
|
||||||
DEFAULT_TTS_ENGINE,
|
DEFAULT_TTS_ENGINE,
|
||||||
@ -442,8 +450,10 @@ function useScrollToBottom(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ChatActions(props: {
|
export function ChatActions(props: {
|
||||||
|
uploadDocument: () => void;
|
||||||
uploadImage: () => void;
|
uploadImage: () => void;
|
||||||
setAttachImages: (images: string[]) => void;
|
setAttachImages: (images: string[]) => void;
|
||||||
|
setAttachFiles: (files: UploadFile[]) => void;
|
||||||
setUploading: (uploading: boolean) => void;
|
setUploading: (uploading: boolean) => void;
|
||||||
showPromptModal: () => void;
|
showPromptModal: () => void;
|
||||||
scrollToBottom: () => void;
|
scrollToBottom: () => void;
|
||||||
@ -577,6 +587,11 @@ export function ChatActions(props: {
|
|||||||
icon={props.uploading ? <LoadingButtonIcon /> : <ImageIcon />}
|
icon={props.uploading ? <LoadingButtonIcon /> : <ImageIcon />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<ChatAction
|
||||||
|
onClick={props.uploadDocument}
|
||||||
|
text={"Upload Plain Text File"}
|
||||||
|
icon={props.uploading ? <LoadingButtonIcon /> : <UploadDocIcon />}
|
||||||
|
/>
|
||||||
<ChatAction
|
<ChatAction
|
||||||
onClick={nextTheme}
|
onClick={nextTheme}
|
||||||
text={Locale.Chat.InputActions.Theme[theme]}
|
text={Locale.Chat.InputActions.Theme[theme]}
|
||||||
@ -945,6 +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<UploadFile[]>([]);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
// prompt hints
|
// prompt hints
|
||||||
@ -1025,9 +1041,10 @@ function _Chat() {
|
|||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
chatStore
|
chatStore
|
||||||
.onUserInput(userInput, attachImages)
|
.onUserInput(userInput, attachImages, attachFiles)
|
||||||
.then(() => setIsLoading(false));
|
.then(() => setIsLoading(false));
|
||||||
setAttachImages([]);
|
setAttachImages([]);
|
||||||
|
setAttachFiles([]);
|
||||||
chatStore.setLastInput(userInput);
|
chatStore.setLastInput(userInput);
|
||||||
setUserInput("");
|
setUserInput("");
|
||||||
setPromptHints([]);
|
setPromptHints([]);
|
||||||
@ -1177,7 +1194,9 @@ function _Chat() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const textContent = getMessageTextContent(userMessage);
|
const textContent = getMessageTextContent(userMessage);
|
||||||
const images = getMessageImages(userMessage);
|
const images = getMessageImages(userMessage);
|
||||||
chatStore.onUserInput(textContent, images).then(() => setIsLoading(false));
|
chatStore
|
||||||
|
.onUserInput(textContent, images, attachFiles)
|
||||||
|
.then(() => setIsLoading(false));
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1460,6 +1479,54 @@ function _Chat() {
|
|||||||
[attachImages, chatStore],
|
[attachImages, chatStore],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async function uploadDocument() {
|
||||||
|
const files: UploadFile[] = [];
|
||||||
|
files.push(...attachFiles);
|
||||||
|
|
||||||
|
files.push(
|
||||||
|
...(await new Promise<UploadFile[]>((res, rej) => {
|
||||||
|
const fileInput = document.createElement("input");
|
||||||
|
fileInput.type = "file";
|
||||||
|
fileInput.accept = "text/*";
|
||||||
|
fileInput.multiple = true;
|
||||||
|
fileInput.onchange = (event: any) => {
|
||||||
|
setUploading(true);
|
||||||
|
const inputFiles = event.target.files;
|
||||||
|
const imagesData: UploadFile[] = [];
|
||||||
|
(async () => {
|
||||||
|
for (let i = 0; i < inputFiles.length; i++) {
|
||||||
|
const file = inputFiles[i];
|
||||||
|
try {
|
||||||
|
const dataUrl = await uploadFileRemote(file);
|
||||||
|
const fileData: UploadFile = { name: file.name, url: dataUrl };
|
||||||
|
const tokenCount: number = await countTokens(fileData);
|
||||||
|
fileData.tokenCount = tokenCount;
|
||||||
|
imagesData.push(fileData);
|
||||||
|
if (
|
||||||
|
imagesData.length === 3 ||
|
||||||
|
imagesData.length === inputFiles.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);
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadImage() {
|
async function uploadImage() {
|
||||||
const images: string[] = [];
|
const images: string[] = [];
|
||||||
images.push(...attachImages);
|
images.push(...attachImages);
|
||||||
@ -1878,6 +1945,41 @@ function _Chat() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{getMessageFiles(message).length > 0 && (
|
||||||
|
<div className={styles["chat-message-item-files"]}>
|
||||||
|
{getMessageFiles(message).map((file, index) => {
|
||||||
|
const extension: DefaultExtensionType = file.name
|
||||||
|
.split(".")
|
||||||
|
.pop()
|
||||||
|
?.toLowerCase() as DefaultExtensionType;
|
||||||
|
const style = defaultStyles[extension];
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={file.url}
|
||||||
|
target="_blank"
|
||||||
|
key={index}
|
||||||
|
className={styles["chat-message-item-file"]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
styles["chat-message-item-file-icon"] +
|
||||||
|
" no-dark"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FileIcon {...style} glyphColor="#303030" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
styles["chat-message-item-file-name"]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{file.name} {file.tokenCount}K
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles["chat-message-action-date"]}>
|
<div className={styles["chat-message-action-date"]}>
|
||||||
@ -1897,8 +1999,10 @@ function _Chat() {
|
|||||||
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
||||||
|
|
||||||
<ChatActions
|
<ChatActions
|
||||||
|
uploadDocument={uploadDocument}
|
||||||
uploadImage={uploadImage}
|
uploadImage={uploadImage}
|
||||||
setAttachImages={setAttachImages}
|
setAttachImages={setAttachImages}
|
||||||
|
setAttachFiles={setAttachFiles}
|
||||||
setUploading={setUploading}
|
setUploading={setUploading}
|
||||||
showPromptModal={() => setShowPromptModal(true)}
|
showPromptModal={() => setShowPromptModal(true)}
|
||||||
scrollToBottom={scrollToBottom}
|
scrollToBottom={scrollToBottom}
|
||||||
@ -1920,7 +2024,7 @@ function _Chat() {
|
|||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
className={`${styles["chat-input-panel-inner"]} ${
|
className={`${styles["chat-input-panel-inner"]} ${
|
||||||
attachImages.length != 0
|
attachImages.length != 0 || attachFiles.length != 0
|
||||||
? styles["chat-input-panel-inner-attach"]
|
? styles["chat-input-panel-inner-attach"]
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
@ -1944,29 +2048,82 @@ function _Chat() {
|
|||||||
fontFamily: config.fontFamily,
|
fontFamily: config.fontFamily,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{attachImages.length != 0 && (
|
<div className={styles["attachments"]}>
|
||||||
<div className={styles["attach-images"]}>
|
{attachImages.length != 0 && (
|
||||||
{attachImages.map((image, index) => {
|
<div className={styles["attach-images"]}>
|
||||||
return (
|
{attachImages.map((image, index) => {
|
||||||
<div
|
return (
|
||||||
key={index}
|
<div
|
||||||
className={styles["attach-image"]}
|
key={index}
|
||||||
style={{ backgroundImage: `url("${image}")` }}
|
className={styles["attach-image"]}
|
||||||
>
|
style={{ backgroundImage: `url("${image}")` }}
|
||||||
<div className={styles["attach-image-mask"]}>
|
>
|
||||||
<DeleteImageButton
|
<div className={styles["attach-image-mask"]}>
|
||||||
deleteImage={() => {
|
<DeleteImageButton
|
||||||
setAttachImages(
|
deleteImage={() => {
|
||||||
attachImages.filter((_, i) => i !== index),
|
setAttachImages(
|
||||||
);
|
attachImages.filter((_, i) => i !== index),
|
||||||
}}
|
);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{attachFiles.length != 0 && (
|
||||||
|
<div className={styles["attach-files"]}>
|
||||||
|
{attachFiles.map((file, index) => {
|
||||||
|
const extension: DefaultExtensionType = file.name
|
||||||
|
.split(".")
|
||||||
|
.pop()
|
||||||
|
?.toLowerCase() as DefaultExtensionType;
|
||||||
|
const style = defaultStyles[extension];
|
||||||
|
return (
|
||||||
|
<div key={index} className={styles["attach-file"]}>
|
||||||
|
<div
|
||||||
|
className={styles["attach-file-icon"] + " no-dark"}
|
||||||
|
key={extension}
|
||||||
|
>
|
||||||
|
<FileIcon {...style} glyphColor="#303030" />
|
||||||
|
</div>
|
||||||
|
{attachImages.length == 0 && (
|
||||||
|
<div className={styles["attach-file-name-full"]}>
|
||||||
|
{file.name} {file.tokenCount}K
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{attachImages.length == 1 && (
|
||||||
|
<div className={styles["attach-file-name-half"]}>
|
||||||
|
{file.name} {file.tokenCount}K
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{attachImages.length == 2 && (
|
||||||
|
<div className={styles["attach-file-name-less"]}>
|
||||||
|
{file.name} {file.tokenCount}K
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{attachImages.length == 3 && (
|
||||||
|
<div className={styles["attach-file-name-min"]}>
|
||||||
|
{file.name} {file.tokenCount}K
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles["attach-image-mask"]}>
|
||||||
|
<DeleteImageButton
|
||||||
|
deleteImage={() => {
|
||||||
|
setAttachFiles(
|
||||||
|
attachFiles.filter((_, i) => i !== index),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<SendWhiteIcon />}
|
icon={<SendWhiteIcon />}
|
||||||
text={Locale.Chat.Send}
|
text={Locale.Chat.Send}
|
||||||
|
1
app/icons/upload-doc.svg
Normal file
1
app/icons/upload-doc.svg
Normal 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 |
@ -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";
|
||||||
@ -18,7 +19,7 @@ import {
|
|||||||
StoreKey,
|
StoreKey,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import Locale, { getLang } from "../locales";
|
import Locale, { getLang } from "../locales";
|
||||||
import { isDalle3, safeLocalStorage } from "../utils";
|
import { isDalle3, safeLocalStorage, readFileContent } from "../utils";
|
||||||
import { prettyObject } from "../utils/format";
|
import { prettyObject } from "../utils/format";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
import { estimateTokenLength } from "../utils/token";
|
import { estimateTokenLength } from "../utils/token";
|
||||||
@ -326,16 +327,75 @@ export const useChatStore = createPersistStore(
|
|||||||
get().summarizeSession();
|
get().summarizeSession();
|
||||||
},
|
},
|
||||||
|
|
||||||
async onUserInput(content: string, attachImages?: string[]) {
|
async onUserInput(
|
||||||
|
content: string,
|
||||||
|
attachImages?: string[],
|
||||||
|
attachFiles?: UploadFile[],
|
||||||
|
) {
|
||||||
const session = get().currentSession();
|
const session = get().currentSession();
|
||||||
const modelConfig = session.mask.modelConfig;
|
const modelConfig = session.mask.modelConfig;
|
||||||
|
|
||||||
|
//read file content from the url
|
||||||
const userContent = fillTemplateWith(content, modelConfig);
|
const userContent = fillTemplateWith(content, modelConfig);
|
||||||
console.log("[User Input] after template: ", userContent);
|
console.log("[User Input] after template: ", userContent);
|
||||||
|
|
||||||
let mContent: string | MultimodalContent[] = userContent;
|
let mContent: string | MultimodalContent[] = userContent;
|
||||||
|
let displayContent: string | MultimodalContent[] = userContent;
|
||||||
|
displayContent = [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: userContent,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (attachImages && attachImages.length > 0) {
|
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) {
|
||||||
|
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 = [
|
mContent = [
|
||||||
{
|
{
|
||||||
type: "text",
|
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({
|
let userMessage: ChatMessage = createMessage({
|
||||||
role: "user",
|
role: "user",
|
||||||
@ -373,7 +446,8 @@ export const useChatStore = createPersistStore(
|
|||||||
get().updateCurrentSession((session) => {
|
get().updateCurrentSession((session) => {
|
||||||
const savedUserMessage = {
|
const savedUserMessage = {
|
||||||
...userMessage,
|
...userMessage,
|
||||||
content: mContent,
|
//content: mContent,
|
||||||
|
content: displayContent,
|
||||||
};
|
};
|
||||||
session.messages = session.messages.concat([
|
session.messages = session.messages.concat([
|
||||||
savedUserMessage,
|
savedUserMessage,
|
||||||
|
67
app/utils.ts
67
app/utils.ts
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { showToast } from "./components/ui-lib";
|
import { showToast } from "./components/ui-lib";
|
||||||
import Locale from "./locales";
|
import Locale from "./locales";
|
||||||
import { RequestMessage } from "./client/api";
|
import { RequestMessage, UploadFile } from "./client/api";
|
||||||
import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant";
|
import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant";
|
||||||
import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
|
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<string> => {
|
||||||
|
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) {
|
export async function copyToClipboard(text: string) {
|
||||||
try {
|
try {
|
||||||
if (window.__TAURI__) {
|
if (window.__TAURI__) {
|
||||||
@ -250,6 +302,19 @@ export function getMessageImages(message: RequestMessage): string[] {
|
|||||||
return urls;
|
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) {
|
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)
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"@hello-pangea/dnd": "^16.5.0",
|
"@hello-pangea/dnd": "^16.5.0",
|
||||||
"@next/third-parties": "^14.1.0",
|
"@next/third-parties": "^14.1.0",
|
||||||
"@svgr/webpack": "^6.5.1",
|
"@svgr/webpack": "^6.5.1",
|
||||||
|
"@types/react-file-icon": "^1.0.4",
|
||||||
"@vercel/analytics": "^0.1.11",
|
"@vercel/analytics": "^0.1.11",
|
||||||
"@vercel/speed-insights": "^1.0.2",
|
"@vercel/speed-insights": "^1.0.2",
|
||||||
"axios": "^1.7.5",
|
"axios": "^1.7.5",
|
||||||
@ -31,14 +32,15 @@
|
|||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mermaid": "^10.6.1",
|
|
||||||
"markdown-to-txt": "^2.0.1",
|
"markdown-to-txt": "^2.0.1",
|
||||||
|
"mermaid": "^10.6.1",
|
||||||
"nanoid": "^5.0.3",
|
"nanoid": "^5.0.3",
|
||||||
"next": "^14.1.1",
|
"next": "^14.1.1",
|
||||||
"node-fetch": "^3.3.1",
|
"node-fetch": "^3.3.1",
|
||||||
"openapi-client-axios": "^7.5.5",
|
"openapi-client-axios": "^7.5.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-file-icon": "^1.5.0",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
"rehype-highlight": "^6.0.0",
|
"rehype-highlight": "^6.0.0",
|
||||||
@ -80,4 +82,4 @@
|
|||||||
"lint-staged/yaml": "^2.2.2"
|
"lint-staged/yaml": "^2.2.2"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.19"
|
"packageManager": "yarn@1.22.19"
|
||||||
}
|
}
|
||||||
|
22
yarn.lock
22
yarn.lock
@ -1762,6 +1762,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@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":
|
"@types/react-katex@^3.0.0":
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-katex/-/react-katex-3.0.0.tgz#119a902bff10eb52f449fac744aaed8c4909391f"
|
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"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
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:
|
colorette@^2.0.19:
|
||||||
version "2.0.19"
|
version "2.0.19"
|
||||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
|
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"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.2.tgz#78fcecd6d870551aa5547437cdae39d4701dca5b"
|
||||||
integrity sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==
|
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"
|
version "15.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||||
@ -5453,6 +5465,14 @@ react-dom@^18.2.0:
|
|||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
scheduler "^0.23.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:
|
react-is@^16.13.1, react-is@^16.7.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
|
Loading…
Reference in New Issue
Block a user