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 = ( +
+ { sender === 'bingai' ? : } +
+ ); + } + return (
- {sender === 'GPT' ? ( -
- -
- ) : ( - `${sender}:` - )} + {icon}
diff --git a/src/components/main/ModelMenu.jsx b/src/components/main/ModelMenu.jsx index ac661683f8..5b24d004ae 100644 --- a/src/components/main/ModelMenu.jsx +++ b/src/components/main/ModelMenu.jsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { setModel } from '~/store/submitSlice'; import GPTIcon from '../svg/GPTIcon'; +import BingIcon from '../svg/BingIcon'; import { DropdownMenuCheckboxItemProps } from '@radix-ui/react-dropdown-menu'; import { Button } from '../ui/Button.tsx'; @@ -44,6 +45,7 @@ export default function ModelMenu() { ]; const colorProps = model === 'chatgpt' ? chatgptColorProps : defaultColorProps; + const icon = model === 'bingai' ? : ; return ( @@ -53,7 +55,7 @@ export default function ModelMenu() { // style={{backgroundColor: 'rgb(16, 163, 127)'}} className={`absolute bottom-0.5 rounded-md border-0 p-1 pl-2 outline-none ${colorProps.join(' ')} focus:ring-0 focus:ring-offset-0 disabled:bottom-0.5 md:pl-1 md:bottom-1 md:left-2 md:disabled:bottom-1`} > - + {icon} @@ -63,6 +65,7 @@ export default function ModelMenu() { value={model} onValueChange={onChange} > + BingAI ChatGPT Davinci {/* Right */} diff --git a/src/components/main/TextChat.jsx b/src/components/main/TextChat.jsx index cf1ca8003e..4c9491d525 100644 --- a/src/components/main/TextChat.jsx +++ b/src/components/main/TextChat.jsx @@ -30,11 +30,11 @@ export default function TextChat({ messages }) { dispatch(setSubmitState(true)); const message = text.trim(); const currentMsg = { sender: 'User', text: message, current: true }; - const initialResponse = { sender: 'GPT', text: '' }; + const initialResponse = { sender: model, text: '' }; dispatch(setMessages([...messages, currentMsg, initialResponse])); dispatch(setText('')); const messageHandler = (data) => { - dispatch(setMessages([...messages, currentMsg, { sender: 'GPT', text: data }])); + dispatch(setMessages([...messages, currentMsg, { sender: model, text: data }])); }; const convoHandler = (data) => { console.log('in convo handler'); diff --git a/src/components/svg/BingChatIcon.jsx b/src/components/svg/BingChatIcon.jsx new file mode 100644 index 0000000000..ab8c97db56 --- /dev/null +++ b/src/components/svg/BingChatIcon.jsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export default function BingChatIcon() { + return ( + + + + ); +} \ No newline at end of file diff --git a/src/components/svg/BingIcon.jsx b/src/components/svg/BingIcon.jsx new file mode 100644 index 0000000000..6da92e462e --- /dev/null +++ b/src/components/svg/BingIcon.jsx @@ -0,0 +1,260 @@ +import React from 'react'; + +export default function BingIcon() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/store/submitSlice.js b/src/store/submitSlice.js index 0260e95796..72721d92eb 100644 --- a/src/store/submitSlice.js +++ b/src/store/submitSlice.js @@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'; const initialState = { isSubmitting: false, - model: 'chatgpt' + model: 'bingai' }; const currentSlice = createSlice({ diff --git a/src/utils/handleSubmit.js b/src/utils/handleSubmit.js index 444b83106b..cc86b708f3 100644 --- a/src/utils/handleSubmit.js +++ b/src/utils/handleSubmit.js @@ -1,4 +1,5 @@ import { SSE } from '../../app/sse'; +const endpoint = 'http://localhost:3050/ask'; export default function handleSubmit({ model, @@ -17,7 +18,8 @@ export default function handleSubmit({ }; } - const events = new SSE('http://localhost:3050/ask', { + const server = model === 'bingai' ? endpoint + '/bing' : endpoint; + const events = new SSE(server, { payload: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' } }); @@ -28,8 +30,9 @@ export default function handleSubmit({ events.onmessage = function (e) { const data = JSON.parse(e.data); + const text = data.text || data.response; if (!!data.message) { - messageHandler(data.text.replace(/^\n/, '')); + messageHandler(text.replace(/^\n/, '')); } else if (!!data.final) { console.log(data); convoHandler(data);