Skip to content

Commit

Permalink
Merge pull request #1 from animalnots/dev
Browse files Browse the repository at this point in the history
Added vision capabilities / image support
  • Loading branch information
animalnots authored Jul 7, 2024
2 parents fbc47eb + eb74dcb commit 177a8a8
Show file tree
Hide file tree
Showing 24 changed files with 575 additions and 159 deletions.
29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,20 +144,23 @@ One click deploy with Vercel
5. Launch the app by running `yarn dev` or `npm run dev`

### Running it locally using docker compose

1. Ensure that you have the following installed:

- [docker](https://www.docker.com/) (v24.0.7 or above)
```bash
curl https://get.docker.com | sh \
&& sudo usermod -aG docker $USER
```
```bash
curl https://get.docker.com | sh \
&& sudo usermod -aG docker $USER
```

2. Build the docker image

```
docker compose build
```

3. Build and start the container using docker compose

```
docker compose build
docker compose up -d
Expand All @@ -168,6 +171,24 @@ One click deploy with Vercel
docker compose down
```

### Running it locally via desktop app

1. Ensure that you have the following installed:

- [yarn](https://yarnpkg.com/) or [npm](https://www.npmjs.com/) (6.14.15 or above)

2. Build the executable (Windows)

```
yarn make --win
```

3. Build for other OS
```
yarn make _ADD_BUILD_ARGS_HERE
```
To find out available building arguments, go to [electron-builder reference](https://www.electron.build/cli.html)

# ⭐️ Star History

[![Star History Chart](https://api.star-history.com/svg?repos=ztjhz/BetterChatGPT&type=Date)](https://github.com/ztjhz/BetterChatGPT/stargazers)
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"preview": "vite preview",
"electron": "concurrently -k \"BROWSER=none yarn dev\" \"wait-on tcp:5173 && electron .\"",
"pack": "yarn build && electron-builder --dir",
"make": "yarn build && electron-builder"
"make": "yarn build && electron-builder",
"debug": "concurrently -k \"cross-env BROWSER=none yarn dev\" \"wait-on tcp:5173 && electron --inspect=5858 .\""
},
"build": {
"appId": "better-chatgpt",
Expand Down Expand Up @@ -62,6 +63,7 @@
"react-i18next": "^12.2.0",
"react-markdown": "^8.0.5",
"react-scroll-to-bottom": "^4.2.0",
"react-toastify": "^10.0.5",
"rehype-highlight": "^6.0.0",
"rehype-katex": "^6.0.2",
"remark-gfm": "^3.0.1",
Expand All @@ -80,6 +82,7 @@
"@vitejs/plugin-react-swc": "^3.0.0",
"autoprefixer": "^10.4.13",
"concurrently": "^8.0.1",
"cross-env": "^7.0.3",
"electron": "^23.2.0",
"electron-builder": "^23.6.0",
"postcss": "^8.4.21",
Expand Down
4 changes: 4 additions & 0 deletions public/locales/en/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
"howTo": "Get your personal API key <0>here</0>.",
"inputLabel": "API Key"
},
"apiVersion": {
"inputLabel": "Api Version",
"description": "Api Version e.g. 2024-04-01-preview"
},
"customEndpoint": "Use custom API endpoint",
"advancedConfig": "View advanced API configuration <0>here</0>",
"noApiKeyWarning": "No API key supplied! Please check your API settings."
Expand Down
3 changes: 3 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { ChatInterface } from '@type/chat';
import { Theme } from '@type/theme';
import ApiPopup from '@components/ApiPopup';
import Toast from '@components/Toast';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

function App() {
const initialiseNewChat = useInitialiseNewChat();
Expand Down Expand Up @@ -80,6 +82,7 @@ function App() {
<Chat />
<ApiPopup />
<Toast />
<ToastContainer />
</div>
);
}
Expand Down
26 changes: 18 additions & 8 deletions src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { ShareGPTSubmitBodyInterface } from '@type/api';
import { ConfigInterface, MessageInterface, ModelOptions } from '@type/chat';
import {
ConfigInterface,
ImageContentInterface,
MessageInterface,
ModelOptions,
} from '@type/chat';
import { isAzureEndpoint } from '@utils/api';

export const getChatCompletion = async (
endpoint: string,
messages: MessageInterface[],
config: ConfigInterface,
apiKey?: string,
customHeaders?: Record<string, string>
customHeaders?: Record<string, string>,
apiVersionToUse?: string
) => {
const headers: HeadersInit = {
'Content-Type': 'application/json',
Expand All @@ -29,9 +35,10 @@ export const getChatCompletion = async (

// set api version to 2023-07-01-preview for gpt-4 and gpt-4-32k, otherwise use 2023-03-15-preview
const apiVersion =
model === 'gpt-4' || model === 'gpt-4-32k'
apiVersionToUse ??
(model === 'gpt-4' || model === 'gpt-4-32k'
? '2023-07-01-preview'
: '2023-03-15-preview';
: '2023-03-15-preview');

const path = `openai/deployments/${model}/chat/completions?api-version=${apiVersion}`;

Expand All @@ -42,6 +49,7 @@ export const getChatCompletion = async (
endpoint += path;
}
}
endpoint = endpoint.trim();

const response = await fetch(endpoint, {
method: 'POST',
Expand All @@ -63,7 +71,8 @@ export const getChatCompletionStream = async (
messages: MessageInterface[],
config: ConfigInterface,
apiKey?: string,
customHeaders?: Record<string, string>
customHeaders?: Record<string, string>,
apiVersionToUse?: string
) => {
const headers: HeadersInit = {
'Content-Type': 'application/json',
Expand All @@ -83,9 +92,10 @@ export const getChatCompletionStream = async (

// set api version to 2023-07-01-preview for gpt-4 and gpt-4-32k, otherwise use 2023-03-15-preview
const apiVersion =
model === 'gpt-4' || model === 'gpt-4-32k'
apiVersionToUse ??
(model === 'gpt-4' || model === 'gpt-4-32k'
? '2023-07-01-preview'
: '2023-03-15-preview';
: '2023-03-15-preview');
const path = `openai/deployments/${model}/chat/completions?api-version=${apiVersion}`;

if (!endpoint.endsWith(path)) {
Expand All @@ -95,7 +105,7 @@ export const getChatCompletionStream = async (
endpoint += path;
}
}

endpoint = endpoint.trim();
const response = await fetch(endpoint, {
method: 'POST',
headers,
Expand Down
18 changes: 18 additions & 0 deletions src/components/ApiMenu/ApiMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@ const ApiMenu = ({
const setApiKey = useStore((state) => state.setApiKey);
const apiEndpoint = useStore((state) => state.apiEndpoint);
const setApiEndpoint = useStore((state) => state.setApiEndpoint);
const apiVersion = useStore((state) => state.apiVersion);
const setApiVersion = useStore((state) => state.setApiVersion);

const [_apiKey, _setApiKey] = useState<string>(apiKey || '');
const [_apiEndpoint, _setApiEndpoint] = useState<string>(apiEndpoint);
const [_customEndpoint, _setCustomEndpoint] = useState<boolean>(
!availableEndpoints.includes(apiEndpoint)
);
const [_apiVersion, _setApiVersion] = useState<string>(apiVersion || '');

const handleSave = () => {
setApiKey(_apiKey);
setApiEndpoint(_apiEndpoint);
setApiVersion(_apiVersion);
setIsModalOpen(false);
};

Expand Down Expand Up @@ -92,6 +96,20 @@ const ApiMenu = ({
/>
</div>

<div className='flex gap-2 items-center justify-center mt-2'>
<div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm'>
{t('apiVersion.inputLabel', { ns: 'api' })}
</div>
<input
type='text'
placeholder={t('apiVersion.description', { ns: 'api' }) ?? ''}
className='text-gray-800 dark:text-white p-3 text-sm border-none bg-gray-200 dark:bg-gray-600 rounded-md m-0 w-full mr-0 h-8 focus:outline-none'
value={_apiVersion}
onChange={(e) => {
_setApiVersion(e.target.value);
}}
/>
</div>
<div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm flex flex-col gap-3 leading-relaxed'>
<p className='mt-4'>
<Trans
Expand Down
6 changes: 5 additions & 1 deletion src/components/Chat/ChatContent/ChatContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useSubmit from '@hooks/useSubmit';
import DownloadChat from './DownloadChat';
import CloneChat from './CloneChat';
import ShareGPT from '@components/ShareGPT';
import { ImageContentInterface, TextContentInterface } from '@type/chat';

const ChatContent = () => {
const inputRole = useStore((state) => state.inputRole);
Expand Down Expand Up @@ -79,7 +80,10 @@ const ChatContent = () => {

<Message
role={inputRole}
content=''
// For now we always initizlize a new message with an empty text content.
// It is possible to send a message to the API without a TextContentInterface,
// but the UI would need to be modified to allow the user to control the order of text and image content
content={[{type: 'text', text: ''} as TextContentInterface]}
messageIndex={stickyIndex}
sticky
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { matchSorter } from 'match-sorter';
import { Prompt } from '@type/prompt';

import useHideOnOutsideClick from '@hooks/useHideOnOutsideClick';
import { ContentInterface } from '@type/chat';

const CommandPrompt = ({
_setContent,
}: {
_setContent: React.Dispatch<React.SetStateAction<string>>;
_setContent: React.Dispatch<React.SetStateAction<ContentInterface[]>>;
}) => {
const { t } = useTranslation();
const prompts = useStore((state) => state.prompts);
Expand Down Expand Up @@ -69,7 +70,7 @@ const CommandPrompt = ({
<li
className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer text-start w-full'
onClick={() => {
_setContent((prev) => prev + cp.prompt);
_setContent((prev) => [{type: 'text', text: prev + cp.prompt}, ...prev.slice(1)]);
setDropDown(false);
}}
key={cp.id}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Chat/ChatContent/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import useStore from '@store/store';
import Avatar from './Avatar';
import MessageContent from './MessageContent';

import { Role } from '@type/chat';
import { ContentInterface, Role } from '@type/chat';
import RoleSelector from './RoleSelector';

// const backgroundStyle: { [role in Role]: string } = {
Expand All @@ -22,7 +22,7 @@ const Message = React.memo(
sticky = false,
}: {
role: Role;
content: string;
content: ContentInterface[],
messageIndex: number;
sticky?: boolean;
}) => {
Expand Down
3 changes: 2 additions & 1 deletion src/components/Chat/ChatContent/Message/MessageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import useStore from '@store/store';

import ContentView from './View/ContentView';
import EditView from './View/EditView';
import { ContentInterface } from '@type/chat';

const MessageContent = ({
role,
Expand All @@ -11,7 +12,7 @@ const MessageContent = ({
sticky = false,
}: {
role: string;
content: string;
content: ContentInterface[];
messageIndex: number;
sticky?: boolean;
}) => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/Chat/ChatContent/Message/NewMessageButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import useStore from '@store/store';

import PlusIcon from '@icon/PlusIcon';

import { ChatInterface } from '@type/chat';
import { ChatInterface, TextContentInterface } from '@type/chat';
import { generateDefaultChat } from '@constants/chat';

const NewMessageButton = React.memo(
Expand Down Expand Up @@ -38,7 +38,7 @@ const NewMessageButton = React.memo(
JSON.stringify(useStore.getState().chats)
);
updatedChats[currentChatIndex].messages.splice(messageIndex + 1, 0, {
content: '',
content: [{ type: 'text', text: '' } as TextContentInterface],
role: 'user',
});
setChats(updatedChats);
Expand Down
17 changes: 12 additions & 5 deletions src/components/Chat/ChatContent/Message/View/ContentView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import CrossIcon from '@icon/CrossIcon';

import useSubmit from '@hooks/useSubmit';

import { ChatInterface } from '@type/chat';
import { ChatInterface, ContentInterface, ImageContentInterface, TextContentInterface } from '@type/chat';

import { codeLanguageSubset } from '@constants/chat';

Expand All @@ -41,7 +41,7 @@ const ContentView = memo(
messageIndex,
}: {
role: string;
content: string;
content: ContentInterface[],
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
messageIndex: number;
}) => {
Expand Down Expand Up @@ -100,7 +100,7 @@ const ContentView = memo(
};

const handleCopy = () => {
navigator.clipboard.writeText(content);
navigator.clipboard.writeText((content[0] as TextContentInterface).text);
};

return (
Expand Down Expand Up @@ -129,12 +129,19 @@ const ContentView = memo(
p,
}}
>
{content}
{(content[0] as TextContentInterface).text}
</ReactMarkdown>
) : (
<span className='whitespace-pre-wrap'>{content}</span>
<span className='whitespace-pre-wrap'>{(content[0] as TextContentInterface).text}</span>
)}
</div>
<div className="flex gap-4">
{(content.slice(1) as ImageContentInterface[]).map((image, index) => (
<div key={index} className="image-container">
<img src={image.image_url.url} alt={`uploaded-${index}`} className="h-20" />
</div>
))}
</div>
<div className='flex justify-end gap-2 w-full mt-2'>
{isDelete || (
<>
Expand Down
Loading

0 comments on commit 177a8a8

Please sign in to comment.