diff --git a/.gitignore b/.gitignore index 86e7746a0c..b7bed31cdf 100644 --- a/.gitignore +++ b/.gitignore @@ -40,5 +40,7 @@ bower_components/ # Environment .env cache.json +data/ +.eslintrc.js src/style - official.css \ No newline at end of file diff --git a/README.md b/README.md index bf7a243db6..c9c968e5fd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # ChatGPT Clone # -![chatgpt-clone demo](./demo.gif) +![chatgpt-clone demo](./public/demo.gif) ## Wrap all conversational AIs under one roof. ## Conversational/Utility AIs are the future and OpenAI revolutionized this movement with ChatGPT. While numerous methods exist to integrate conversational AIs, this app commemorates the original styling of ChatGPT, with the ability to integrate any current/future conversational AI models through user-provided APIs, while also having in mind improved client features, such as conversation search, and prompt templates. This project was also built with the anticipation of the official ChatGPT API from OpenAI, though it uses unofficial packages. Through this clone, you can avoid subscription-based models in favor of either free or pay-per-call APIs. I will most likely not deploy this app, as it's mainly a learning experience, but feel free to clone or fork to create your own custom wrapper. @@ -8,6 +8,11 @@ ## Updates
+2023-03-01 +Official ChatGPT API is out! Removed davinci since the official API is extremely fast and 10x less expensive. Since user labeling and prompt prefixing is officially supported, I will add a View feature so you can set this within chat, which gives the UI an added use case. I've kept the BrowserClient, since it's free to use like the official site. + +The Messages UI correctly mirrors code syntax highlighting. The exact replication of the cursor is not 1-to-1 yet, but pretty close. Later on in the project, I'll implement tests for code edge cases and explore the possibility of running code in-browser. Right now, unknown code defaults to javascript, but will detect language as close as possible. +
2023-02-21 BingAI is integrated (although sadly limited by Microsoft with the 5 msg/convo limit, 50 msgs/day). I will need to handle the case when Bing refuses to give more answers on top of the other styling features I have in mind. Official ChatGPT use is back with the new BrowserClient. Brainstorming how to handle the UI when the Ai model changes, since conversations can't be persisted between them (or perhaps build a way to achieve this at some level).
@@ -39,9 +44,11 @@ Currently, this project is only functional with the `text-davinci-003` model. - [x] Remember last selected model - [x] Highlight.js for code blocks - [x] Markdown handling -- [ ] Bing AI Styling (for suggested responses, convo end, etc.) +- [ ] 'Copy to clipboard' button for code and messages +- [ ] Set user/model label and prompt prefix view option - [ ] AI model change handling (whether to pseudo-persist convos or start new convos within existing convo) - [ ] Server convo pagination (limit fetch and load more with 'show more' button) +- [ ] Bing AI Styling (for suggested responses, convo end, etc.) - [ ] Prompt Templates - [ ] Conversation/Prompt Search - [ ] Refactor/clean up code @@ -64,7 +71,7 @@ Currently, this project is only functional with the `text-davinci-003` model. ## Use Cases ## - ![use case example](./use_case.png "GPT is down! Plus is too expensive!") + ![use case example](./public/use_case.png "GPT is down! Plus is too expensive!") - ChatGPT is down ( and don't want to pay for ChatGPT Plus). - ChatGPT/Google Bard/Bing AI conversations are lost in space or cannot be searched past a certain timeframe. diff --git a/app/bingai.js b/app/bingai.js index 2ce2712099..e33278b9a2 100644 --- a/app/bingai.js +++ b/app/bingai.js @@ -10,7 +10,7 @@ const askBing = async ({ text, progressCallback, convo }) => { // If the above doesn't work, provide all your cookies as a string instead // cookies: '', debug: false, - cache: new KeyvFile({ filename: 'bingcache.json' }) + store: new KeyvFile({ filename: './data/cache.json' }) }); let options = { diff --git a/app/chatgpt-browser.js b/app/chatgpt-browser.js new file mode 100644 index 0000000000..c6debc0d91 --- /dev/null +++ b/app/chatgpt-browser.js @@ -0,0 +1,32 @@ +require('dotenv').config(); +const { KeyvFile } = require('keyv-file'); + +const clientOptions = { + // Warning: This will expose your access token to a third party. Consider the risks before using this. + reverseProxyUrl: 'https://chatgpt.duti.tech/api/conversation', + // Access token from https://chat.openai.com/api/auth/session + accessToken: process.env.CHATGPT_TOKEN +}; + +const browserClient = async ({ text, progressCallback, convo }) => { + const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api'); + + const store = { + store: new KeyvFile({ filename: './data/cache.json' }) + }; + + const client = new ChatGPTBrowserClient(clientOptions, store); + + let options = { + onProgress: async (partialRes) => await progressCallback(partialRes) + }; + + if (!!convo.parentMessageId && !!convo.conversationId) { + options = { ...options, ...convo }; + } + + const res = await client.sendMessage(text, options); + return res; +}; + +module.exports = { browserClient }; diff --git a/app/chatgpt-client.js b/app/chatgpt-client.js index 4bc3cf4363..ce3c0e2271 100644 --- a/app/chatgpt-client.js +++ b/app/chatgpt-client.js @@ -1,39 +1,20 @@ require('dotenv').config(); const { KeyvFile } = require('keyv-file'); -const proxyOptions = { - // Warning: This will expose your access token to a third party. Consider the risks before using this. - reverseProxyUrl: 'https://chatgpt.duti.tech/api/conversation', - // Access token from https://chat.openai.com/api/auth/session - accessToken: process.env.CHATGPT_TOKEN -}; - -const davinciOptions = { +const clientOptions = { modelOptions: { - model: 'text-davinci-003' + model: 'gpt-3.5-turbo' }, debug: false }; -const askClient = async ({ model, text, progressCallback, convo }) => { - const davinciClient = (await import('@waylaidwanderer/chatgpt-api')).default; - const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api'); - const clientOptions = model === 'chatgpt' ? proxyOptions : davinciOptions; - const modelClient = model === 'chatgpt' ? ChatGPTBrowserClient : davinciClient; +const askClient = async ({ text, progressCallback, convo }) => { + const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default; const store = { - store: new KeyvFile({ filename: 'cache.json' }) + store: new KeyvFile({ filename: './data/cache.json' }) }; - const params = - model === 'chatgpt' - ? [clientOptions, store] - : [ - process.env.OPENAI_KEY, - clientOptions, - store - ]; - - const client = new modelClient(...params); + const client = new ChatGPTClient(process.env.OPENAI_KEY, clientOptions, store); let options = { onProgress: async (partialRes) => await progressCallback(partialRes) diff --git a/app/index.js b/app/index.js index e81656b400..8616f754ef 100644 --- a/app/index.js +++ b/app/index.js @@ -1,9 +1,11 @@ const { titleConvo } = require('./chatgpt'); const { askClient } = require('./chatgpt-client'); +const { browserClient } = require('./chatgpt-browser'); const { askBing } = require('./bingai'); module.exports = { titleConvo, askClient, askBing, + browserClient, }; \ No newline at end of file diff --git a/nodemon.json b/nodemon.json index a87427c723..69b29e7181 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,5 +1,5 @@ { "ignore": [ - "cache.json" + "data/" ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 636a8c0331..32ff84bddc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4524,9 +4524,9 @@ } }, "node_modules/@waylaidwanderer/chatgpt-api": { - "version": "1.20.8", - "resolved": "https://registry.npmjs.org/@waylaidwanderer/chatgpt-api/-/chatgpt-api-1.20.8.tgz", - "integrity": "sha512-e2F7mEPdypKL3UgQe9rWlqeknXJwXWarTiWFkFG3M5uWWcHdT8S0zPMgrFem+yj0njjlOSmFIs+oJ6jQQpwy5A==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@waylaidwanderer/chatgpt-api/-/chatgpt-api-1.22.2.tgz", + "integrity": "sha512-OjhRBtczNhPn5xIxOWDqIbAel3sC9FLPxh6iAVYOAlIJ+zqyAXI2mxd8P5/Qm8eD1M7YnrIpcWqlPfN8zsNPkA==", "dependencies": { "@fastify/cors": "^8.2.0", "@waylaidwanderer/fastify-sse-v2": "^3.1.0", @@ -18362,9 +18362,9 @@ } }, "@waylaidwanderer/chatgpt-api": { - "version": "1.20.8", - "resolved": "https://registry.npmjs.org/@waylaidwanderer/chatgpt-api/-/chatgpt-api-1.20.8.tgz", - "integrity": "sha512-e2F7mEPdypKL3UgQe9rWlqeknXJwXWarTiWFkFG3M5uWWcHdT8S0zPMgrFem+yj0njjlOSmFIs+oJ6jQQpwy5A==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@waylaidwanderer/chatgpt-api/-/chatgpt-api-1.22.2.tgz", + "integrity": "sha512-OjhRBtczNhPn5xIxOWDqIbAel3sC9FLPxh6iAVYOAlIJ+zqyAXI2mxd8P5/Qm8eD1M7YnrIpcWqlPfN8zsNPkA==", "requires": { "@fastify/cors": "^8.2.0", "@waylaidwanderer/fastify-sse-v2": "^3.1.0", diff --git a/use_case.png b/public/use_case.png similarity index 100% rename from use_case.png rename to public/use_case.png diff --git a/server/routes/ask.js b/server/routes/ask.js index 0846880d92..f29039aa2e 100644 --- a/server/routes/ask.js +++ b/server/routes/ask.js @@ -2,7 +2,7 @@ const express = require('express'); const crypto = require('crypto'); const router = express.Router(); const askBing = require('./askBing'); -const { titleConvo, askClient } = require('../../app/'); +const { titleConvo, askClient, browserClient } = require('../../app/'); const { saveMessage, deleteMessages, saveConvo } = require('../../models'); const { handleError, sendMessage } = require('./handlers'); @@ -19,6 +19,8 @@ router.post('/', async (req, res) => { console.log('ask log', { model, ...userMessage, parentMessageId, conversationId }); + const client = model === 'chatgpt' ? askClient : browserClient; + res.writeHead(200, { Connection: 'keep-alive', 'Content-Type': 'text/event-stream', @@ -53,8 +55,7 @@ router.post('/', async (req, res) => { } }; - let gptResponse = await askClient({ - model, + let gptResponse = await client({ text, progressCallback, convo: { @@ -63,7 +64,7 @@ router.post('/', async (req, res) => { } }); - // console.log('CLIENT RESPONSE', gptResponse); + console.log('CLIENT RESPONSE', gptResponse); if (!gptResponse.parentMessageId) { gptResponse.text = gptResponse.response; diff --git a/src/components/Messages/Message.jsx b/src/components/Messages/Message.jsx index 58c1baa35d..ed880d3b11 100644 --- a/src/components/Messages/Message.jsx +++ b/src/components/Messages/Message.jsx @@ -35,15 +35,21 @@ export default function Message({ 'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group dark:bg-gray-800' }; + const bgColors = { + chatgpt: 'rgb(16, 163, 127)', + chatgptBrowser: 'rgb(25, 207, 207)', + bingai: '', + }; + let icon = `${sender}:`; - const isGPT = sender === 'chatgpt' || sender === 'davinci' || sender === 'GPT'; + let backgroundColor = bgColors[sender]; if (notUser) { props.className = 'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-[#444654]'; icon = (
{sender === 'bingai' ? : } diff --git a/src/components/main/ModelMenu.jsx b/src/components/main/ModelMenu.jsx index ef351f11b7..17b55d53ab 100644 --- a/src/components/main/ModelMenu.jsx +++ b/src/components/main/ModelMenu.jsx @@ -75,10 +75,9 @@ export default function ModelMenu() { value={model} onValueChange={onChange} > - BingAI ChatGPT - Davinci - {/* Right */} + BingAI + {'ChatGPT (free)'}