+ {modelTypes[model] == 'image' && (
+
+
+ {_content.slice(1).map((image, index) => (
+
+
+
+
+
+
+
+ ))}
- {sticky || (
-
- )}
-
-
- {sticky || (
+ {/* Hidden file input */}
+
+
+ )}
+
+
+ {sticky && (
+
+ )}
+
+ {sticky || (
+
+ )}
+
- )}
+
+ {sticky || (
+
+ )}
+
+ {sticky && advancedMode &&
}
+
- {sticky && advancedMode &&
}
-
);
}
diff --git a/src/constants/chat.ts b/src/constants/chat.ts
index f806ef1b6..1191bed54 100644
--- a/src/constants/chat.ts
+++ b/src/constants/chat.ts
@@ -1,5 +1,12 @@
import { v4 as uuidv4 } from 'uuid';
-import { ChatInterface, ConfigInterface, ModelOptions } from '@type/chat';
+import {
+ ChatInterface,
+ ConfigInterface,
+ ModelCost,
+ ModelOptions,
+ ModelType,
+ TextContentInterface,
+} from '@type/chat';
import useStore from '@store/store';
const date = new Date();
@@ -35,9 +42,10 @@ export const modelOptions: ModelOptions[] = [
// 'gpt-4-32k-0314',
];
+export const defaultApiVersion = '2024-04-01-preview';
export const defaultModel = 'gpt-3.5-turbo';
-export const modelMaxToken = {
+export const modelMaxToken: { [key: string]: number } = {
'gpt-3.5-turbo': 4096,
'gpt-3.5-turbo-0301': 4096,
'gpt-3.5-turbo-0613': 4096,
@@ -59,7 +67,7 @@ export const modelMaxToken = {
'gpt-4o-2024-05-13': 128000,
};
-export const modelCost = {
+export const modelCost: ModelCost = {
'gpt-3.5-turbo': {
prompt: { price: 0.0015, unit: 1000 },
completion: { price: 0.002, unit: 1000 },
@@ -157,7 +165,17 @@ export const generateDefaultChat = (
title: title ? title : 'New Chat',
messages:
useStore.getState().defaultSystemMessage.length > 0
- ? [{ role: 'system', content: useStore.getState().defaultSystemMessage }]
+ ? [
+ {
+ role: 'system',
+ content: [
+ {
+ type: 'text',
+ text: useStore.getState().defaultSystemMessage,
+ } as TextContentInterface,
+ ],
+ },
+ ]
: [],
config: { ...useStore.getState().defaultChatConfig },
titleSet: false,
@@ -201,3 +219,9 @@ export const codeLanguageSubset = [
'xml',
'yaml',
];
+
+export const modelTypes: { [key: string]: string } = {
+ 'gpt-4o': 'image',
+ 'gpt-4o-2024-05-13': 'image',
+ 'gpt-4-vision-preview': 'image',
+};
diff --git a/src/hooks/useSubmit.ts b/src/hooks/useSubmit.ts
index cd6efc5aa..f6315f181 100644
--- a/src/hooks/useSubmit.ts
+++ b/src/hooks/useSubmit.ts
@@ -1,7 +1,10 @@
-import React from 'react';
import useStore from '@store/store';
import { useTranslation } from 'react-i18next';
-import { ChatInterface, MessageInterface } from '@type/chat';
+import {
+ ChatInterface,
+ MessageInterface,
+ TextContentInterface,
+} from '@type/chat';
import { getChatCompletion, getChatCompletionStream } from '@api/api';
import { parseEventSource } from '@api/helper';
import { limitMessageTokens, updateTotalTokenUsed } from '@utils/messageUtils';
@@ -34,7 +37,10 @@ const useSubmit = () => {
data = await getChatCompletion(
useStore.getState().apiEndpoint,
message,
- _defaultChatConfig
+ _defaultChatConfig,
+ undefined,
+ undefined,
+ useStore.getState().apiVersion
);
} else if (apiKey) {
// own apikey
@@ -42,7 +48,9 @@ const useSubmit = () => {
useStore.getState().apiEndpoint,
message,
_defaultChatConfig,
- apiKey
+ apiKey,
+ undefined,
+ useStore.getState().apiVersion
);
}
} catch (error: unknown) {
@@ -59,7 +67,12 @@ const useSubmit = () => {
updatedChats[currentChatIndex].messages.push({
role: 'assistant',
- content: '',
+ content: [
+ {
+ type: 'text',
+ text: '',
+ } as TextContentInterface,
+ ],
});
setChats(updatedChats);
@@ -88,7 +101,10 @@ const useSubmit = () => {
stream = await getChatCompletionStream(
useStore.getState().apiEndpoint,
messages,
- chats[currentChatIndex].config
+ chats[currentChatIndex].config,
+ undefined,
+ undefined,
+ useStore.getState().apiVersion
);
} else if (apiKey) {
// own apikey
@@ -96,7 +112,9 @@ const useSubmit = () => {
useStore.getState().apiEndpoint,
messages,
chats[currentChatIndex].config,
- apiKey
+ apiKey,
+ undefined,
+ useStore.getState().apiVersion
);
}
@@ -132,7 +150,10 @@ const useSubmit = () => {
JSON.stringify(useStore.getState().chats)
);
const updatedMessages = updatedChats[currentChatIndex].messages;
- updatedMessages[updatedMessages.length - 1].content += resultString;
+ (
+ updatedMessages[updatedMessages.length - 1]
+ .content[0] as TextContentInterface
+ ).text += resultString;
setChats(updatedChats);
}
}
@@ -173,7 +194,12 @@ const useSubmit = () => {
const message: MessageInterface = {
role: 'user',
- content: `Generate a title in less than 6 words for the following message (language: ${i18n.language}):\n"""\nUser: ${user_message}\nAssistant: ${assistant_message}\n"""`,
+ content: [
+ {
+ type: 'text',
+ text: `Generate a title in less than 6 words for the following message (language: ${i18n.language}):\n"""\nUser: ${user_message}\nAssistant: ${assistant_message}\n"""`,
+ } as TextContentInterface,
+ ],
};
let title = (await generateTitle([message])).trim();
@@ -192,7 +218,7 @@ const useSubmit = () => {
const model = _defaultChatConfig.model;
updateTotalTokenUsed(model, [message], {
role: 'assistant',
- content: title,
+ content: [{ type: 'text', text: title } as TextContentInterface],
});
}
}
diff --git a/src/store/auth-slice.ts b/src/store/auth-slice.ts
index 52088906a..b76affe94 100644
--- a/src/store/auth-slice.ts
+++ b/src/store/auth-slice.ts
@@ -4,15 +4,18 @@ import { StoreSlice } from './store';
export interface AuthSlice {
apiKey?: string;
apiEndpoint: string;
+ apiVersion?: string;
firstVisit: boolean;
setApiKey: (apiKey: string) => void;
setApiEndpoint: (apiEndpoint: string) => void;
+ setApiVersion: (apiVersion: string) => void;
setFirstVisit: (firstVisit: boolean) => void;
}
export const createAuthSlice: StoreSlice
= (set, get) => ({
apiKey: import.meta.env.VITE_OPENAI_API_KEY || undefined,
apiEndpoint: defaultAPIEndpoint,
+ apiVersion: undefined,
firstVisit: true,
setApiKey: (apiKey: string) => {
set((prev: AuthSlice) => ({
@@ -26,6 +29,12 @@ export const createAuthSlice: StoreSlice = (set, get) => ({
apiEndpoint: apiEndpoint,
}));
},
+ setApiVersion: (apiVersion: string) => {
+ set((prev: AuthSlice) => ({
+ ...prev,
+ apiVersion: apiVersion,
+ }));
+ },
setFirstVisit: (firstVisit: boolean) => {
set((prev: AuthSlice) => ({
...prev,
diff --git a/src/store/chat-slice.ts b/src/store/chat-slice.ts
index 0a3b682dc..f46ab3925 100644
--- a/src/store/chat-slice.ts
+++ b/src/store/chat-slice.ts
@@ -1,5 +1,6 @@
import { StoreSlice } from './store';
import { ChatInterface, FolderCollection, MessageInterface } from '@type/chat';
+import { toast } from 'react-toastify';
export interface ChatSlice {
messages: MessageInterface[];
@@ -16,46 +17,54 @@ export interface ChatSlice {
setFolders: (folders: FolderCollection) => void;
}
-export const createChatSlice: StoreSlice = (set, get) => ({
- messages: [],
- currentChatIndex: -1,
- generating: false,
- error: '',
- folders: {},
- setMessages: (messages: MessageInterface[]) => {
- set((prev: ChatSlice) => ({
- ...prev,
- messages: messages,
- }));
- },
- setChats: (chats: ChatInterface[]) => {
- set((prev: ChatSlice) => ({
- ...prev,
- chats: chats,
- }));
- },
- setCurrentChatIndex: (currentChatIndex: number) => {
- set((prev: ChatSlice) => ({
- ...prev,
- currentChatIndex: currentChatIndex,
- }));
- },
- setGenerating: (generating: boolean) => {
- set((prev: ChatSlice) => ({
- ...prev,
- generating: generating,
- }));
- },
- setError: (error: string) => {
- set((prev: ChatSlice) => ({
- ...prev,
- error: error,
- }));
- },
- setFolders: (folders: FolderCollection) => {
- set((prev: ChatSlice) => ({
- ...prev,
- folders: folders,
- }));
- },
-});
+export const createChatSlice: StoreSlice = (set, get) => {
+ return {
+ messages: [],
+ currentChatIndex: -1,
+ generating: false,
+ error: '',
+ folders: {},
+ setMessages: (messages: MessageInterface[]) => {
+ set((prev: ChatSlice) => ({
+ ...prev,
+ messages: messages,
+ }));
+ },
+ setChats: (chats: ChatInterface[]) => {
+ try {
+ set((prev: ChatSlice) => ({
+ ...prev,
+ chats: chats,
+ }));
+ } catch (e: unknown) {
+ // Notify if storage quota exceeded
+ toast((e as Error).message);
+ throw e;
+ }
+ },
+ setCurrentChatIndex: (currentChatIndex: number) => {
+ set((prev: ChatSlice) => ({
+ ...prev,
+ currentChatIndex: currentChatIndex,
+ }));
+ },
+ setGenerating: (generating: boolean) => {
+ set((prev: ChatSlice) => ({
+ ...prev,
+ generating: generating,
+ }));
+ },
+ setError: (error: string) => {
+ set((prev: ChatSlice) => ({
+ ...prev,
+ error: error,
+ }));
+ },
+ setFolders: (folders: FolderCollection) => {
+ set((prev: ChatSlice) => ({
+ ...prev,
+ folders: folders,
+ }));
+ },
+ };
+};
diff --git a/src/store/migrate.ts b/src/store/migrate.ts
index b96ba6011..a87c57fb5 100644
--- a/src/store/migrate.ts
+++ b/src/store/migrate.ts
@@ -11,9 +11,12 @@ import {
LocalStorageInterfaceV5ToV6,
LocalStorageInterfaceV6ToV7,
LocalStorageInterfaceV7oV8,
+ LocalStorageInterfaceV8oV8_1,
+ TextContentInterface,
} from '@type/chat';
import {
_defaultChatConfig,
+ defaultApiVersion,
defaultModel,
defaultUserMaxToken,
} from '@constants/chat';
@@ -104,3 +107,17 @@ export const migrateV7 = (persistedState: LocalStorageInterfaceV7oV8) => {
chat.id = uuidv4();
});
};
+
+export const migrateV8_1 = (persistedState: LocalStorageInterfaceV8oV8_1) => {
+ persistedState.chats.forEach((chat) => {
+ persistedState.apiVersion = defaultApiVersion;
+ chat.messages.forEach((msg) => {
+ if (typeof msg.content === 'string') {
+ const content: TextContentInterface[] = [
+ { type: 'text', text: msg.content },
+ ];
+ msg.content = content;
+ }
+ });
+ });
+};
diff --git a/src/store/store.ts b/src/store/store.ts
index 69aba7de0..153c0f0d7 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -15,6 +15,7 @@ import {
LocalStorageInterfaceV5ToV6,
LocalStorageInterfaceV6ToV7,
LocalStorageInterfaceV7oV8,
+ LocalStorageInterfaceV8oV8_1,
} from '@type/chat';
import {
migrateV0,
@@ -25,6 +26,7 @@ import {
migrateV5,
migrateV6,
migrateV7,
+ migrateV8_1,
} from './migrate';
export type StoreState = ChatSlice &
@@ -43,6 +45,7 @@ export const createPartializedState = (state: StoreState) => ({
chats: state.chats,
currentChatIndex: state.currentChatIndex,
apiKey: state.apiKey,
+ apiVersion: state.apiVersion,
apiEndpoint: state.apiEndpoint,
theme: state.theme,
autoTitle: state.autoTitle,
@@ -74,7 +77,7 @@ const useStore = create()(
{
name: 'free-chat-gpt',
partialize: (state) => createPartializedState(state),
- version: 8,
+ version: 8.1,
migrate: (persistedState, version) => {
switch (version) {
case 0:
@@ -93,6 +96,8 @@ const useStore = create()(
migrateV6(persistedState as LocalStorageInterfaceV6ToV7);
case 7:
migrateV7(persistedState as LocalStorageInterfaceV7oV8);
+ case 8:
+ migrateV8_1(persistedState as LocalStorageInterfaceV8oV8_1);
break;
}
return persistedState as StoreState;
diff --git a/src/types/chat.ts b/src/types/chat.ts
index 5b706952a..fbc73f6b1 100644
--- a/src/types/chat.ts
+++ b/src/types/chat.ts
@@ -1,12 +1,35 @@
import { Prompt } from './prompt';
import { Theme } from './theme';
+// The types in this file must mimick the structure of the the API request
+
+export type Content = 'text' | 'image_url';
+export type ImageDetail = 'low' | 'high' | 'auto';
+export const imageDetails: ImageDetail[] = ['low', 'high', 'auto'];
export type Role = 'user' | 'assistant' | 'system';
export const roles: Role[] = ['user', 'assistant', 'system'];
+export interface ImageContentInterface extends ContentInterface {
+ type: 'image_url';
+ image_url: {
+ url: string; // base64 or image URL
+ detail: ImageDetail;
+ };
+}
+
+export interface TextContentInterface extends ContentInterface {
+ type: 'text';
+ text: string;
+}
+
+export interface ContentInterface {
+ [x: string]: any;
+ type: Content;
+}
+
export interface MessageInterface {
role: Role;
- content: string;
+ content: ContentInterface[];
}
export interface ChatInterface {
@@ -61,11 +84,27 @@ export type ModelOptions =
| 'gpt-3.5-turbo'
| 'gpt-3.5-turbo-16k'
| 'gpt-3.5-turbo-1106'
- | 'gpt-3.5-turbo-0125';
+ | 'gpt-3.5-turbo-0125'
+ | 'gpt-4-vision-preview';
// | 'gpt-3.5-turbo-0301';
// | 'gpt-4-0314'
// | 'gpt-4-32k-0314'
+export type ModelType = 'text' | 'image';
+interface Pricing {
+ price: number;
+ unit: number;
+}
+
+interface CostDetails {
+ prompt: Pricing;
+ completion: Pricing;
+}
+
+export interface ModelCost {
+ [modelName: string]: CostDetails;
+}
+
export type TotalTokenUsed = {
[model in ModelOptions]?: {
promptTokens: number;
@@ -159,3 +198,10 @@ export interface LocalStorageInterfaceV7oV8
foldersExpanded: boolean[];
folders: FolderCollection;
}
+export interface LocalStorageInterfaceV8oV8_1
+ extends LocalStorageInterfaceV7oV8 {
+ apiVersion: string;
+}
+
+// export interface LocalStorageInterfaceV8_1ToV9
+// extends LocalStorageInterfaceV8oV8_1 {
diff --git a/src/types/export.ts b/src/types/export.ts
index f275ee7a6..5a42a03ee 100644
--- a/src/types/export.ts
+++ b/src/types/export.ts
@@ -1,4 +1,9 @@
-import { ChatInterface, FolderCollection, Role } from './chat';
+import {
+ ChatInterface,
+ ContentInterface,
+ FolderCollection,
+ Role,
+} from './chat';
export interface ExportBase {
version: number;
@@ -8,7 +13,6 @@ export interface ExportV1 extends ExportBase {
chats?: ChatInterface[];
folders: FolderCollection;
}
-
export type OpenAIChat = {
title: string;
mapping: {
@@ -18,9 +22,11 @@ export type OpenAIChat = {
author: {
role: Role;
};
- content: {
- parts?: string[];
- };
+ content:
+ | {
+ parts?: string[];
+ }
+ | ContentInterface;
} | null;
parent: string | null;
children: string[];
diff --git a/src/utils/import.ts b/src/utils/import.ts
index 4224a3d38..d24f52ce8 100644
--- a/src/utils/import.ts
+++ b/src/utils/import.ts
@@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
import {
ChatInterface,
ConfigInterface,
+ ContentInterface,
FolderCollection,
MessageInterface,
} from '@type/chat';
@@ -88,6 +89,10 @@ export const validateFolders = (
export const validateExportV1 = (data: ExportV1): data is ExportV1 => {
return validateAndFixChats(data.chats) && validateFolders(data.folders);
};
+// Type guard to check if content is ContentInterface
+const isContentInterface = (content: any): content is ContentInterface => {
+ return typeof content === 'object' && 'type' in content;
+};
// Convert OpenAI chat format to BetterChatGPT format
export const convertOpenAIToBetterChatGPTFormat = (
@@ -102,8 +107,21 @@ export const convertOpenAIToBetterChatGPTFormat = (
// Extract message if it exists
if (node.message) {
const { role } = node.message.author;
- const content = node.message.content.parts?.join('') || '';
- if (content.length > 0) messages.push({ role, content });
+ const content = node.message.content;
+ if (Array.isArray(content.parts)) {
+ const textContent = content.parts.join('') || '';
+ if (textContent.length > 0) {
+ messages.push({
+ role,
+ content: [{ type: 'text', text: textContent }],
+ });
+ }
+ } else if (isContentInterface(content)) {
+ messages.push({ role, content: [content] });
+ }
+ // TODO: Remove this after stable build
+ // const content = node.message.content.parts?.join('') || '';
+ // if (content.length > 0) messages.push({ role, content });
}
// Traverse the last child node if any children exist
diff --git a/src/utils/messageUtils.ts b/src/utils/messageUtils.ts
index e4aacde0c..25e3dc000 100644
--- a/src/utils/messageUtils.ts
+++ b/src/utils/messageUtils.ts
@@ -1,4 +1,4 @@
-import { MessageInterface, ModelOptions, TotalTokenUsed } from '@type/chat';
+import { MessageInterface, ModelOptions, TextContentInterface, TotalTokenUsed } from '@type/chat';
import useStore from '@store/store';
@@ -29,7 +29,7 @@ export const getChatGPTEncoding = (
const serialized = [
messages
.map(({ role, content }) => {
- return `<|im_start|>${role}${roleSep}${content}<|im_end|>`;
+ return `<|im_start|>${role}${roleSep}${(content[0] as TextContentInterface).text}<|im_end|>`;
})
.join(msgSep),
`<|im_start|>assistant${roleSep}`,
diff --git a/yarn.lock b/yarn.lock
index 717c9659e..adc08d197 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1205,6 +1205,11 @@ clone-response@^1.0.2:
dependencies:
mimic-response "^1.0.0"
+clsx@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
+ integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
+
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -1336,6 +1341,13 @@ crc@^3.8.0:
dependencies:
buffer "^5.1.0"
+cross-env@^7.0.3:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
+ integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
+ dependencies:
+ cross-spawn "^7.0.1"
+
cross-fetch@3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
@@ -3305,6 +3317,13 @@ react-scroll-to-bottom@^4.2.0:
prop-types "15.7.2"
simple-update-in "2.2.0"
+react-toastify@^10.0.5:
+ version "10.0.5"
+ resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-10.0.5.tgz#6b8f8386060c5c856239f3036d1e76874ce3bd1e"
+ integrity sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==
+ dependencies:
+ clsx "^2.1.0"
+
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"