diff --git a/README.md b/README.md index d25fa02af3..309abe1968 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,10 @@ Currently, this project is only functional with the `text-davinci-003` model. ### Technologies used -- Utilizes waylaidwanderer's [node-chatgpt-api package](https://github.com/waylaidwanderer/node-chatgpt-api) +- Utilizes [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api) - Response streaming identical to ChatGPT through server-sent events - Use of Tailwind CSS (like the official site) and [shadcn/ui](https://github.com/shadcn/ui) components -- Backend: Node.js, Redux Toolkit, Express, MongoDB, Keyv +- useSWR, Redux Toolkit, Express, MongoDB, [Keyv](https://www.npmjs.com/package/keyv) ## Use Cases ## diff --git a/app/bingai.js b/app/bingai.js new file mode 100644 index 0000000000..86e88f11fb --- /dev/null +++ b/app/bingai.js @@ -0,0 +1,66 @@ +// import { BingAIClient } from '@waylaidwanderer/chatgpt-api'; +require('dotenv').config(); +const { KeyvFile } = require('keyv-file'); + +const askBing = async ({ text, progressCallback, convo }) => { + const { BingAIClient } = (await import('@waylaidwanderer/chatgpt-api')); + + const bingAIClient = new BingAIClient({ + // "_U" cookie from bing.com + userToken: process.env.BING_TOKEN, + // If the above doesn't work, provide all your cookies as a string instead + // cookies: '', + debug: false, + cache: new KeyvFile({ filename: 'bingcache.json' }) + }); + + let options = { + onProgress: async (partialRes) => await progressCallback(partialRes), + }; + + if (!!convo) { + options = { ...options, ...convo }; + } + + const res = await bingAIClient.sendMessage(text, options + // Options for reference + // { + // conversationSignature: response.conversationSignature, + // conversationId: response.conversationId, + // clientId: response.clientId, + // invocationId: response.invocationId, + // onProgress: (token) => { + // process.stdout.write(token); + // }, + // } + ); + + return res; + + // Example response for reference + // { + // conversationSignature: 'wwZ2GC/qRgEqP3VSNIhbPGwtno5RcuBhzZFASOM+Sxg=', + // conversationId: '51D|BingProd|026D3A4017554DE6C446798144B6337F4D47D5B76E62A31F31D0B1D0A95ED868', + // clientId: '914800201536527', + // invocationId: 1, + // conversationExpiryTime: '2023-02-15T21:48:46.2892088Z', + // response: 'Hello, this is Bing. Nice to meet you. 😊', + // details: { + // text: 'Hello, this is Bing. Nice to meet you. 😊', + // author: 'bot', + // createdAt: '2023-02-15T15:48:43.0631898+00:00', + // timestamp: '2023-02-15T15:48:43.0631898+00:00', + // messageId: '9d0c9a80-91b1-49ab-b9b1-b457dc3fe247', + // requestId: '5b252ef8-4f09-4c08-b6f5-4499d2e12fba', + // offense: 'None', + // adaptiveCards: [ [Object] ], + // sourceAttributions: [], + // feedback: { tag: null, updatedOn: null, type: 'None' }, + // contentOrigin: 'DeepLeo', + // privacy: null, + // suggestedResponses: [ [Object], [Object], [Object] ] + // } + // } +}; + +module.exports = { askBing }; diff --git a/models/Conversation.js b/models/Conversation.js index 329bd82d80..f7adce638b 100644 --- a/models/Conversation.js +++ b/models/Conversation.js @@ -15,6 +15,15 @@ const convoSchema = mongoose.Schema({ type: String, default: 'New conversation', }, + conversationSignature: { + type: String, + }, + clientId: { + type: String, + }, + invocationId: { + type: String, + }, messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }], created: { type: Date, diff --git a/server/routes/ask.js b/server/routes/ask.js index 1509a5818d..1e078ef0ec 100644 --- a/server/routes/ask.js +++ b/server/routes/ask.js @@ -3,6 +3,7 @@ const crypto = require('crypto'); const router = express.Router(); const { ask, titleConvo } = require('../../app/chatgpt'); const { askClient } = require('../../app/chatgpt-client'); +const { askBing } = require('../../app/bingai'); const { saveMessage, deleteMessages } = require('../../models/Message'); const { saveConvo } = require('../../models/Conversation'); @@ -15,6 +16,82 @@ const sendMessage = (res, message) => { res.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`); }; +router.post('/bing', async (req, res) => { + const { model, text, parentMessageId, conversationId } = req.body; + if (!text.trim().includes(' ') && text.length < 5) { + return handleError(res, 'Prompt empty or too short'); + } + + const userMessageId = crypto.randomUUID(); + let userMessage = { id: userMessageId, sender: 'User', text }; + + console.log('ask log', { model, ...userMessage, parentMessageId, conversationId }); + + res.writeHead(200, { + Connection: 'keep-alive', + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache, no-transform', + 'Access-Control-Allow-Origin': '*', + 'X-Accel-Buffering': 'no' + }); + + try { + let i = 0; + let tokens = ''; + const progressCallback = async (partial) => { + tokens += partial; + sendMessage(res, { text: tokens, message: true }); + }; + + let response = await askBing({ + text, + progressCallback, + // convo: { + // parentMessageId, + // conversationId + // } + }); + + console.log('CLIENT RESPONSE'); + console.dir(response, {depth: null}); + + // if (!parentMessageId) { + // response.title = await titleConvo(text, response.text); + // } + + // if (!response.parentMessageId) { + // response.text = response.response; + // response.id = response.messageId; + // response.parentMessageId = response.messageId; + // userMessage.parentMessageId = parentMessageId ? parentMessageId : response.messageId; + // userMessage.conversationId = conversationId + // ? conversationId + // : response.conversationId; + // await saveMessage(userMessage); + // delete response.response; + // } + + // if ( + // (response.text.includes('2023') && !response.text.trim().includes(' ')) || + // response.text.toLowerCase().includes('no response') || + // response.text.toLowerCase().includes('no answer') + // ) { + // return handleError(res, 'Prompt empty or too short'); + // } + + response.sender = 'Bing'; + response.final = true; + // await saveMessage(response); + // await saveConvo(response); + sendMessage(res, response); + res.end(); + } catch (error) { + console.log(error); + // await deleteMessages({ id: userMessageId }); + handleError(res, error.message); + } +}); + router.post('/', async (req, res) => { const { model, text, parentMessageId, conversationId } = req.body; if (!text.trim().includes(' ') && text.length < 5) { @@ -90,7 +167,7 @@ router.post('/', async (req, res) => { return handleError(res, 'Prompt empty or too short'); } - gptResponse.sender = 'GPT'; + gptResponse.sender = model; gptResponse.final = true; await saveMessage(gptResponse); await saveConvo(gptResponse); diff --git a/src/components/main/Message.jsx b/src/components/main/Message.jsx index 85593519b2..16ed2838f2 100644 --- a/src/components/main/Message.jsx +++ b/src/components/main/Message.jsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; import GPTIcon from '../svg/GPTIcon'; +import BingIcon from '../svg/BingIcon'; // import useCustomEffect from '~/hooks/useCustomEffect'; export default function Message({ @@ -11,7 +12,7 @@ export default function Message({ scrollToBottom }) { const { isSubmitting } = useSelector((state) => state.submit); - const blinker = isSubmitting && last && sender === 'GPT'; + const blinker = isSubmitting && last && sender.toLowerCase() !== 'user'; useEffect(() => { if (blinker) { @@ -24,25 +25,30 @@ 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' }; - if (sender === 'GPT') { + if (sender.toLowerCase() !== 'user') { 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]'; } + let icon = `${sender}:`; + const isGPT = sender === 'chatgpt' || sender === 'davinci'; + + if (sender.toLowerCase() !== 'user') { + icon = ( +