From 88aea81288e4703cd3e89d5c98af78d97fdc8a37 Mon Sep 17 00:00:00 2001 From: Daniel Avila Date: Sat, 8 Apr 2023 23:19:29 -0400 Subject: [PATCH 01/18] WIP: fix: fix abort messages and continue conversation on abort feat(askOpenAI.js): add abort endpoint to cancel requests feat(MessageHandler): add abort functionality to cancel requests feat(submission.js): add lastResponse and source atoms to store feat(handleSubmit.js): add stopGenerating function to cancel requests --- api/server/routes/ask/askOpenAI.js | 26 ++++++++- client/src/components/Input/index.jsx | 3 +- .../src/components/MessageHandler/index.jsx | 56 ++++++++++++++++--- client/src/store/submission.js | 12 ++++ client/src/utils/handleSubmit.js | 3 +- 5 files changed, 88 insertions(+), 12 deletions(-) diff --git a/api/server/routes/ask/askOpenAI.js b/api/server/routes/ask/askOpenAI.js index 9c5d121639..4d95cc4c8d 100644 --- a/api/server/routes/ask/askOpenAI.js +++ b/api/server/routes/ask/askOpenAI.js @@ -6,6 +6,22 @@ const { titleConvo, askClient } = require('../../../app/'); const { saveMessage, getConvoTitle, saveConvo, updateConvo, getConvo } = require('../../../models'); const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); +const abortControllers = new Map(); + +router.get('/abort', (req, res) => { + const requestId = req.query.requestId; + + if (abortControllers.has(requestId)) { + const abortController = abortControllers.get(requestId); + abortController.abort(); + abortControllers.delete(requestId); + console.log('Aborted request', requestId); + res.status(200).send('Aborted'); + } else { + res.status(404).send('Request not found'); + } +}); + router.post('/', async (req, res) => { const { endpoint, @@ -100,7 +116,14 @@ const ask = async ({ try { const progressCallback = createOnProgress(); const abortController = new AbortController(); - res.on('close', () => abortController.abort()); + const abortKey = userMessage.messageId; + abortControllers.set(abortKey, abortController); + + res.on('close', () => { + console.log('stopped message, aborting'); + abortController.abort(); + return res.end(); + }); let response = await askClient({ text, parentMessageId: userParentMessageId, @@ -170,6 +193,7 @@ const ask = async ({ requestMessage: userMessage, responseMessage: responseMessage }); + abortControllers.delete(abortKey); res.end(); if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { diff --git a/client/src/components/Input/index.jsx b/client/src/components/Input/index.jsx index e87f305ac7..067f1bcda9 100644 --- a/client/src/components/Input/index.jsx +++ b/client/src/components/Input/index.jsx @@ -62,7 +62,8 @@ export default function TextChat({ isSearchView = false }) { setText(''); }; - const handleStopGenerating = () => { + const handleStopGenerating = (e) => { + e.preventDefault(); stopGenerating(); }; diff --git a/client/src/components/MessageHandler/index.jsx b/client/src/components/MessageHandler/index.jsx index 24ec7c3dc4..c53b5bcd4c 100644 --- a/client/src/components/MessageHandler/index.jsx +++ b/client/src/components/MessageHandler/index.jsx @@ -1,5 +1,5 @@ -import { useEffect } from 'react'; -import { useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil'; +import { useEffect, useState } from 'react'; +import { useRecoilValue, useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil'; import { SSE } from '~/data-provider/sse.mjs'; import createPayload from '~/data-provider/createPayload'; @@ -11,6 +11,10 @@ export default function MessageHandler() { const setMessages = useSetRecoilState(store.messages); const setConversation = useSetRecoilState(store.conversation); const resetLatestMessage = useResetRecoilState(store.latestMessage); + const [lastResponse, setLastResponse] = useRecoilState(store.lastResponse); + const setSubmission = useSetRecoilState(store.submission); + const [source, setSource] = useState(null); + const [abortKey, setAbortKey] = useState(null); const { refreshConversations } = store.useConversations(); @@ -45,7 +49,7 @@ export default function MessageHandler() { const cancelHandler = (data, submission) => { const { messages, message, initialResponse, isRegenerate = false } = submission; - if (isRegenerate) + if (isRegenerate) { setMessages([ ...messages, { @@ -56,7 +60,7 @@ export default function MessageHandler() { cancelled: true } ]); - else + } else { setMessages([ ...messages, message, @@ -65,9 +69,12 @@ export default function MessageHandler() { text: data, parentMessageId: message?.messageId, messageId: message?.messageId + '_', - cancelled: true + // cancelled: true } ]); + setLastResponse(''); + setSource(null); + } }; const createdHandler = (data, submission) => { @@ -149,7 +156,33 @@ export default function MessageHandler() { if (submission === null) return; if (Object.keys(submission).length === 0) return; - let { message } = submission; + let { message, cancel } = submission; + + if (cancel && source) { + console.log('message aborted', submission); + source.close(); + const { endpoint } = submission.conversation; + const latestMessage = lastResponse.replaceAll('█', ''); + + fetch(`/api/ask/${endpoint}/abort?requestId=${abortKey}`) + .then(response => { + if (response.ok) { + console.log('Request aborted'); + } else { + console.error('Error aborting request'); + } + }) + .catch(error => { + console.error(error); + }); + console.log('source closed, got this far'); + cancelHandler(latestMessage, { ...submission, message }); + setIsSubmitting(false); + setSubmission(null); + return; + } + + // events.oncancel = () => cancelHandler(latestResponseText, { ...submission, message }); const { server, payload } = createPayload(submission); @@ -158,7 +191,9 @@ export default function MessageHandler() { headers: { 'Content-Type': 'application/json' } }); - let latestResponseText = ''; + setSource(events); + + // let latestResponseText = ''; events.onmessage = e => { const data = JSON.parse(e.data); @@ -173,12 +208,14 @@ export default function MessageHandler() { }; createdHandler(data, { ...submission, message }); console.log('created', message); + setAbortKey(message?.messageId); } else { let text = data.text || data.response; if (data.initial) console.log(data); if (data.message) { - latestResponseText = text; + // latestResponseText = text; + setLastResponse(text); messageHandler(text, { ...submission, message }); } // console.log('dataStream', data); @@ -187,7 +224,7 @@ export default function MessageHandler() { events.onopen = () => console.log('connection is opened'); - events.oncancel = () => cancelHandler(latestResponseText, { ...submission, message }); + // events.oncancel = () => cancelHandler(latestResponseText, { ...submission, message }); events.onerror = function (e) { console.log('error in opening conn.'); @@ -204,6 +241,7 @@ export default function MessageHandler() { return () => { const isCancelled = events.readyState <= 1; events.close(); + setSource(null); if (isCancelled) { const e = new Event('cancel'); events.dispatchEvent(e); diff --git a/client/src/store/submission.js b/client/src/store/submission.js index 90e08b6dff..a83a118236 100644 --- a/client/src/store/submission.js +++ b/client/src/store/submission.js @@ -31,7 +31,19 @@ const isSubmitting = atom({ default: false, }); +const lastResponse = atom({ + key: "lastResponse", + default: '', +}); + +const source = atom({ + key: "source", + default: null, +}); + export default { submission, isSubmitting, + lastResponse, + source, }; diff --git a/client/src/utils/handleSubmit.js b/client/src/utils/handleSubmit.js index 3c0db371ef..8514977eef 100644 --- a/client/src/utils/handleSubmit.js +++ b/client/src/utils/handleSubmit.js @@ -138,7 +138,8 @@ const useMessageHandler = () => { }; const stopGenerating = () => { - setSubmission(null); + // setSubmission(null); + setSubmission(prev => ({ ...prev, cancel: true })); }; return { ask, regenerate, stopGenerating }; From 828e438d53104181749013c56c31b670dd4454af Mon Sep 17 00:00:00 2001 From: Daniel Avila Date: Sun, 9 Apr 2023 09:21:04 -0400 Subject: [PATCH 02/18] feat(api-endpoints.ts): add abortRequest endpoint feat(data-service.ts): add abortRequestWithMessage function feat(react-query-service.ts): add useAbortRequestWithMessage hook --- client/src/data-provider/api-endpoints.ts | 4 ++++ client/src/data-provider/data-service.ts | 4 ++++ client/src/data-provider/react-query-service.ts | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/client/src/data-provider/api-endpoints.ts b/client/src/data-provider/api-endpoints.ts index 74568e55a6..2ae4fc55f0 100644 --- a/client/src/data-provider/api-endpoints.ts +++ b/client/src/data-provider/api-endpoints.ts @@ -6,6 +6,10 @@ export const messages = (id: string) => { return `/api/messages/${id}`; }; +export const abortRequest = (endpoint: string) => { + return `/api/ask/${endpoint}/abort`; +}; + export const conversations = (pageNumber: string) => { return `/api/convos?pageNumber=${pageNumber}`; }; diff --git a/client/src/data-provider/data-service.ts b/client/src/data-provider/data-service.ts index 1b717dc888..3a8a6a4ab6 100644 --- a/client/src/data-provider/data-service.ts +++ b/client/src/data-provider/data-service.ts @@ -6,6 +6,10 @@ export function getConversations(pageNumber: string): Promise { + return request.post(endpoints.abortRequest(endpoint), { arg: {abortKey, message} }); +} + export function deleteConversation(payload: t.TDeleteConversationRequest) { //todo: this should be a DELETE request return request.post(endpoints.deleteConversation(), {arg: payload}); diff --git a/client/src/data-provider/react-query-service.ts b/client/src/data-provider/react-query-service.ts index c85660214c..8c8c406a75 100644 --- a/client/src/data-provider/react-query-service.ts +++ b/client/src/data-provider/react-query-service.ts @@ -21,6 +21,10 @@ export enum QueryKeys { tokenCount = "tokenCount", } +export const useAbortRequestWithMessage = (): UseMutationResult => { + return useMutation(({ endpoint, abortKey, message }) => dataService.abortRequestWithMessage(endpoint, abortKey, message)); +}; + export const useGetUserQuery = (): QueryObserverResult => { return useQuery([QueryKeys.user], () => dataService.getUser(), { refetchOnWindowFocus: false, From b59588c6eee143b3b90765c84921a510a41c5486 Mon Sep 17 00:00:00 2001 From: Daniel Avila Date: Sun, 9 Apr 2023 09:22:14 -0400 Subject: [PATCH 03/18] refactor(messageHandler): sends all necessary data to cache/save unfinished response --- .../src/components/MessageHandler/index.jsx | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/client/src/components/MessageHandler/index.jsx b/client/src/components/MessageHandler/index.jsx index c53b5bcd4c..8d95e78294 100644 --- a/client/src/components/MessageHandler/index.jsx +++ b/client/src/components/MessageHandler/index.jsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { useRecoilValue, useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil'; import { SSE } from '~/data-provider/sse.mjs'; import createPayload from '~/data-provider/createPayload'; +import { useAbortRequestWithMessage } from '~/data-provider'; import store from '~/store'; @@ -68,12 +69,14 @@ export default function MessageHandler() { ...initialResponse, text: data, parentMessageId: message?.messageId, - messageId: message?.messageId + '_', + messageId: message?.messageId + '_' // cancelled: true } ]); setLastResponse(''); setSource(null); + setIsSubmitting(false); + setSubmission(null); } }; @@ -162,9 +165,19 @@ export default function MessageHandler() { console.log('message aborted', submission); source.close(); const { endpoint } = submission.conversation; - const latestMessage = lastResponse.replaceAll('█', ''); - fetch(`/api/ask/${endpoint}/abort?requestId=${abortKey}`) + // splitting twice because the cursor may or may not be wrapped in a span + const latestMessage = lastResponse.split('█')[0].split('')[0]; + fetch(`/api/ask/${endpoint}/abort`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + abortKey, + message: latestMessage, + }) + }) .then(response => { if (response.ok) { console.log('Request aborted'); @@ -177,8 +190,6 @@ export default function MessageHandler() { }); console.log('source closed, got this far'); cancelHandler(latestMessage, { ...submission, message }); - setIsSubmitting(false); - setSubmission(null); return; } @@ -208,7 +219,7 @@ export default function MessageHandler() { }; createdHandler(data, { ...submission, message }); console.log('created', message); - setAbortKey(message?.messageId); + setAbortKey(message?.conversationId); } else { let text = data.text || data.response; if (data.initial) console.log(data); From 6246ffff1ec783732bdcc8fec97b18813130c197 Mon Sep 17 00:00:00 2001 From: Daniel Avila Date: Sun, 9 Apr 2023 09:23:03 -0400 Subject: [PATCH 04/18] wip: refactor: new abort message handling --- api/server/routes/ask/addToCache.js | 31 +++++++++++++++++++++++++++++ api/server/routes/ask/askOpenAI.js | 28 +++++++++++++------------- 2 files changed, 45 insertions(+), 14 deletions(-) create mode 100644 api/server/routes/ask/addToCache.js diff --git a/api/server/routes/ask/addToCache.js b/api/server/routes/ask/addToCache.js new file mode 100644 index 0000000000..826e5d74fa --- /dev/null +++ b/api/server/routes/ask/addToCache.js @@ -0,0 +1,31 @@ +const Keyv = require('keyv'); +const { KeyvFile } = require('keyv-file'); +const crypto = require('crypto'); + +const addToCache = async ( { conversationId, parentMessageId }) => { + const conversationsCache = new Keyv({ + store: new KeyvFile({ filename: './data/cache.json' }) + }); + + let conversation = await conversationsCache.get(conversationId); + let isNewConversation = false; + if (!conversation) { + conversation = { + messages: [], + createdAt: Date.now() + }; + isNewConversation = true; + } + + // const shouldGenerateTitle = opts.shouldGenerateTitle && isNewConversation; + + const userMessage = { + id: crypto.randomUUID(), + parentMessageId, + role: 'User', + message + }; + conversation.messages.push(userMessage); +}; + +module.exports = { addToCache }; diff --git a/api/server/routes/ask/askOpenAI.js b/api/server/routes/ask/askOpenAI.js index 4d95cc4c8d..4ad601068b 100644 --- a/api/server/routes/ask/askOpenAI.js +++ b/api/server/routes/ask/askOpenAI.js @@ -8,18 +8,18 @@ const { handleError, sendMessage, createOnProgress, handleText } = require('./ha const abortControllers = new Map(); -router.get('/abort', (req, res) => { - const requestId = req.query.requestId; - - if (abortControllers.has(requestId)) { - const abortController = abortControllers.get(requestId); - abortController.abort(); - abortControllers.delete(requestId); - console.log('Aborted request', requestId); - res.status(200).send('Aborted'); - } else { - res.status(404).send('Request not found'); +router.post('/abort', (req, res) => { + const { abortKey, message } = req.body; + if (!abortControllers.has(abortKey)) { + return res.status(404).send('Request not found'); } + + const { abortController, userMessage } = abortControllers.get(abortKey); + abortController.abort(); + abortControllers.delete(abortKey); + console.log('Aborted request', abortKey, userMessage); + + res.status(200).send('Aborted'); }); router.post('/', async (req, res) => { @@ -116,11 +116,11 @@ const ask = async ({ try { const progressCallback = createOnProgress(); const abortController = new AbortController(); - const abortKey = userMessage.messageId; - abortControllers.set(abortKey, abortController); + const abortKey = conversationId; + console.log('conversationId -----> ', conversationId); + abortControllers.set(abortKey, { abortController, userMessage }); res.on('close', () => { - console.log('stopped message, aborting'); abortController.abort(); return res.end(); }); From a81bd27b39b2957a2619dee002bf38b15998beac Mon Sep 17 00:00:00 2001 From: Daniel Avila Date: Sun, 9 Apr 2023 11:17:08 -0400 Subject: [PATCH 05/18] feat(api): add support for saving messages to database fix(api): change arrowParens prettier option to always fix(api): update addToCache to include endpointOption and latestMessage fix(api): update askOpenAI to include endpointOption in abortControllers fix(client): remove abortKey state and add currentParent state to MessageHandler --- api/.prettierrc | 2 +- api/models/Message.js | 1 + api/server/routes/ask/addToCache.js | 79 +++++++++++++------ api/server/routes/ask/askOpenAI.js | 17 ++-- .../src/components/MessageHandler/index.jsx | 11 ++- 5 files changed, 78 insertions(+), 32 deletions(-) diff --git a/api/.prettierrc b/api/.prettierrc index 34e12e2f49..1c37ff0e5b 100644 --- a/api/.prettierrc +++ b/api/.prettierrc @@ -1,5 +1,5 @@ { - "arrowParens": "avoid", + "arrowParens": "always", "bracketSpacing": true, "endOfLine": "lf", "htmlWhitespaceSensitivity": "css", diff --git a/api/models/Message.js b/api/models/Message.js index c0b280a85b..af3d454980 100644 --- a/api/models/Message.js +++ b/api/models/Message.js @@ -12,6 +12,7 @@ module.exports = { error }) => { try { + // may also need to update the conversation here await Message.findOneAndUpdate( { messageId }, { diff --git a/api/server/routes/ask/addToCache.js b/api/server/routes/ask/addToCache.js index 826e5d74fa..869f47dc7f 100644 --- a/api/server/routes/ask/addToCache.js +++ b/api/server/routes/ask/addToCache.js @@ -1,31 +1,66 @@ const Keyv = require('keyv'); const { KeyvFile } = require('keyv-file'); const crypto = require('crypto'); +const { saveMessage } = require('../../../models'); -const addToCache = async ( { conversationId, parentMessageId }) => { - const conversationsCache = new Keyv({ - store: new KeyvFile({ filename: './data/cache.json' }) - }); +const addToCache = async ({ + endpointOption, + conversationId, + userMessage, + latestMessage, + parentMessageId +}) => { + try { + const conversationsCache = new Keyv({ + store: new KeyvFile({ filename: './data/cache.json' }), + namespace: 'chatgpt', // should be 'bing' for bing/sydney + }); - let conversation = await conversationsCache.get(conversationId); - let isNewConversation = false; - if (!conversation) { - conversation = { - messages: [], - createdAt: Date.now() + let conversation = await conversationsCache.get(conversationId); + // used to generate a title for the conversation if none exists + // let isNewConversation = false; + if (!conversation) { + conversation = { + messages: [], + createdAt: Date.now() + }; + // isNewConversation = true; + } + + // const shouldGenerateTitle = opts.shouldGenerateTitle && isNewConversation; + + const roles = (options) => { + const { endpoint } = options; + if (endpoint === 'openAI') { + return options?.chatGptLabel || 'ChatGPT'; + } else if (endpoint === 'bingAI') { + return options?.jailbreak ? 'Sydney' : 'BingAI'; + } }; - isNewConversation = true; + + const messageId = crypto.randomUUID(); + + let responseMessage = { + id: messageId, + parentMessageId, + role: roles(endpointOption), + message: latestMessage + }; + + await saveMessage({ + ...responseMessage, + conversationId, + messageId, + sender: responseMessage.role, + text: latestMessage + }); + + conversation.messages.push(userMessage, responseMessage); + + await conversationsCache.set(conversationId, conversation); + } catch (error) { + console.error('Trouble adding to cache', error); } - - // const shouldGenerateTitle = opts.shouldGenerateTitle && isNewConversation; - - const userMessage = { - id: crypto.randomUUID(), - parentMessageId, - role: 'User', - message - }; - conversation.messages.push(userMessage); }; -module.exports = { addToCache }; +module.exports = addToCache; diff --git a/api/server/routes/ask/askOpenAI.js b/api/server/routes/ask/askOpenAI.js index 4ad601068b..56f4aee897 100644 --- a/api/server/routes/ask/askOpenAI.js +++ b/api/server/routes/ask/askOpenAI.js @@ -1,6 +1,7 @@ const express = require('express'); const crypto = require('crypto'); const router = express.Router(); +const addToCache = require('./addToCache'); const { getOpenAIModels } = require('../endpoints'); const { titleConvo, askClient } = require('../../../app/'); const { saveMessage, getConvoTitle, saveConvo, updateConvo, getConvo } = require('../../../models'); @@ -8,16 +9,22 @@ const { handleError, sendMessage, createOnProgress, handleText } = require('./ha const abortControllers = new Map(); -router.post('/abort', (req, res) => { - const { abortKey, message } = req.body; +router.post('/abort', async (req, res) => { + const { abortKey, latestMessage, parentMessageId } = req.body; + console.log(`req.body`, req.body); if (!abortControllers.has(abortKey)) { return res.status(404).send('Request not found'); } + + const { abortController, userMessage, endpointOption } = abortControllers.get(abortKey); + if (!endpointOption.endpoint) { + endpointOption.endpoint = req.originalUrl.replace('/api/ask/','').split('/abort')[0]; + } - const { abortController, userMessage } = abortControllers.get(abortKey); abortController.abort(); abortControllers.delete(abortKey); - console.log('Aborted request', abortKey, userMessage); + console.log('Aborted request', abortKey, userMessage, endpointOption); + await addToCache({ endpointOption, conversationId: abortKey, userMessage, latestMessage, parentMessageId }); res.status(200).send('Aborted'); }); @@ -118,7 +125,7 @@ const ask = async ({ const abortController = new AbortController(); const abortKey = conversationId; console.log('conversationId -----> ', conversationId); - abortControllers.set(abortKey, { abortController, userMessage }); + abortControllers.set(abortKey, { abortController, userMessage, endpointOption }); res.on('close', () => { abortController.abort(); diff --git a/client/src/components/MessageHandler/index.jsx b/client/src/components/MessageHandler/index.jsx index 8d95e78294..dd792e7ed0 100644 --- a/client/src/components/MessageHandler/index.jsx +++ b/client/src/components/MessageHandler/index.jsx @@ -15,7 +15,8 @@ export default function MessageHandler() { const [lastResponse, setLastResponse] = useRecoilState(store.lastResponse); const setSubmission = useSetRecoilState(store.submission); const [source, setSource] = useState(null); - const [abortKey, setAbortKey] = useState(null); + // const [abortKey, setAbortKey] = useState(null); + const [currentParent, setCurrentParent] = useState(null); const { refreshConversations } = store.useConversations(); @@ -174,8 +175,9 @@ export default function MessageHandler() { 'Content-Type': 'application/json' }, body: JSON.stringify({ - abortKey, - message: latestMessage, + abortKey: currentParent.conversationId, + latestMessage, + parentMessageId: currentParent.messageId, }) }) .then(response => { @@ -219,7 +221,8 @@ export default function MessageHandler() { }; createdHandler(data, { ...submission, message }); console.log('created', message); - setAbortKey(message?.conversationId); + // setAbortKey(message?.conversationId); + setCurrentParent(message); } else { let text = data.text || data.response; if (data.initial) console.log(data); From a953fc9f2b3022fbf4a20150fd3ffdfef61e14f2 Mon Sep 17 00:00:00 2001 From: Daniel Avila Date: Sun, 9 Apr 2023 22:21:27 -0400 Subject: [PATCH 06/18] wip: feat: abort messages and continue conversation fix(addToCache.js): remove unused variables and parameters feat(addToCache.js): add message to cache with id, parentMessageId, role, and text fix(askOpenAI.js): remove parentMessageId parameter from addToCache call feat(MessageHandler.jsx): add latestMessage to store on cancel of submission, and generate messageId and parentMessageId for latestMessage --- api/server/routes/ask/addToCache.js | 19 ++++++--------- api/server/routes/ask/askOpenAI.js | 4 ++-- .../src/components/MessageHandler/index.jsx | 24 ++++++++++++------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/api/server/routes/ask/addToCache.js b/api/server/routes/ask/addToCache.js index 869f47dc7f..d53e98cbe3 100644 --- a/api/server/routes/ask/addToCache.js +++ b/api/server/routes/ask/addToCache.js @@ -1,21 +1,16 @@ const Keyv = require('keyv'); const { KeyvFile } = require('keyv-file'); -const crypto = require('crypto'); const { saveMessage } = require('../../../models'); -const addToCache = async ({ - endpointOption, - conversationId, - userMessage, - latestMessage, - parentMessageId -}) => { +const addToCache = async ({ endpointOption, userMessage, latestMessage }) => { try { const conversationsCache = new Keyv({ store: new KeyvFile({ filename: './data/cache.json' }), - namespace: 'chatgpt', // should be 'bing' for bing/sydney + namespace: 'chatgpt' // should be 'bing' for bing/sydney }); + const { conversationId, messageId, parentMessageId, text } = latestMessage; + let conversation = await conversationsCache.get(conversationId); // used to generate a title for the conversation if none exists // let isNewConversation = false; @@ -38,13 +33,13 @@ const addToCache = async ({ } }; - const messageId = crypto.randomUUID(); + // const messageId = crypto.randomUUID(); let responseMessage = { id: messageId, parentMessageId, role: roles(endpointOption), - message: latestMessage + message: text }; await saveMessage({ @@ -52,7 +47,7 @@ const addToCache = async ({ conversationId, messageId, sender: responseMessage.role, - text: latestMessage + text }); conversation.messages.push(userMessage, responseMessage); diff --git a/api/server/routes/ask/askOpenAI.js b/api/server/routes/ask/askOpenAI.js index 56f4aee897..b21c7aba1f 100644 --- a/api/server/routes/ask/askOpenAI.js +++ b/api/server/routes/ask/askOpenAI.js @@ -10,7 +10,7 @@ const { handleError, sendMessage, createOnProgress, handleText } = require('./ha const abortControllers = new Map(); router.post('/abort', async (req, res) => { - const { abortKey, latestMessage, parentMessageId } = req.body; + const { abortKey, latestMessage } = req.body; console.log(`req.body`, req.body); if (!abortControllers.has(abortKey)) { return res.status(404).send('Request not found'); @@ -24,7 +24,7 @@ router.post('/abort', async (req, res) => { abortController.abort(); abortControllers.delete(abortKey); console.log('Aborted request', abortKey, userMessage, endpointOption); - await addToCache({ endpointOption, conversationId: abortKey, userMessage, latestMessage, parentMessageId }); + await addToCache({ endpointOption, userMessage, latestMessage }); res.status(200).send('Aborted'); }); diff --git a/client/src/components/MessageHandler/index.jsx b/client/src/components/MessageHandler/index.jsx index dd792e7ed0..b7736a4374 100644 --- a/client/src/components/MessageHandler/index.jsx +++ b/client/src/components/MessageHandler/index.jsx @@ -3,7 +3,7 @@ import { useRecoilValue, useRecoilState, useResetRecoilState, useSetRecoilState import { SSE } from '~/data-provider/sse.mjs'; import createPayload from '~/data-provider/createPayload'; import { useAbortRequestWithMessage } from '~/data-provider'; - +import { v4 } from 'uuid'; import store from '~/store'; export default function MessageHandler() { @@ -13,6 +13,7 @@ export default function MessageHandler() { const setConversation = useSetRecoilState(store.conversation); const resetLatestMessage = useResetRecoilState(store.latestMessage); const [lastResponse, setLastResponse] = useRecoilState(store.lastResponse); + const setLatestMessage = useSetRecoilState(store.latestMessage); const setSubmission = useSetRecoilState(store.submission); const [source, setSource] = useState(null); // const [abortKey, setAbortKey] = useState(null); @@ -50,6 +51,7 @@ export default function MessageHandler() { const cancelHandler = (data, submission) => { const { messages, message, initialResponse, isRegenerate = false } = submission; + const { text, messageId, parentMessageId } = data; if (isRegenerate) { setMessages([ @@ -63,14 +65,15 @@ export default function MessageHandler() { } ]); } else { + console.log('cancelHandler, isRegenerate = false'); setMessages([ ...messages, message, { ...initialResponse, - text: data, + text, parentMessageId: message?.messageId, - messageId: message?.messageId + '_' + messageId, // cancelled: true } ]); @@ -78,6 +81,7 @@ export default function MessageHandler() { setSource(null); setIsSubmitting(false); setSubmission(null); + setLatestMessage(data); } }; @@ -168,7 +172,14 @@ export default function MessageHandler() { const { endpoint } = submission.conversation; // splitting twice because the cursor may or may not be wrapped in a span - const latestMessage = lastResponse.split('█')[0].split('')[0]; + const latestMessageText = lastResponse.split('█')[0].split('')[0]; + const latestMessage = { + text: latestMessageText, + messageId: v4(), + parentMessageId: currentParent.messageId, + conversationId: currentParent.conversationId + }; + fetch(`/api/ask/${endpoint}/abort`, { method: 'POST', headers: { @@ -176,8 +187,7 @@ export default function MessageHandler() { }, body: JSON.stringify({ abortKey: currentParent.conversationId, - latestMessage, - parentMessageId: currentParent.messageId, + latestMessage }) }) .then(response => { @@ -195,8 +205,6 @@ export default function MessageHandler() { return; } - // events.oncancel = () => cancelHandler(latestResponseText, { ...submission, message }); - const { server, payload } = createPayload(submission); const events = new SSE(server, { From bbf2f8a6cadd1c69a2bca7b69ffe55e614c08e31 Mon Sep 17 00:00:00 2001 From: Wentao Lyu <35-wentao.lyu@users.noreply.git.stereye.tech> Date: Mon, 10 Apr 2023 00:41:34 +0800 Subject: [PATCH 07/18] feat: support user-provided token to bingAI and chatgptBrowser --- api/.env.example | 16 +-- api/app/clients/bingai.js | 3 +- api/app/clients/chatgpt-browser.js | 3 +- api/server/routes/ask/askBingAI.js | 6 +- api/server/routes/ask/askChatGPTBrowser.js | 3 +- api/server/routes/endpoints.js | 11 +- .../components/Endpoints/EditPresetDialog.jsx | 8 +- .../Endpoints/EndpointOptionsDialog.jsx | 4 +- .../Endpoints/SaveAsPresetDialog.jsx | 6 +- .../NewConversationMenu/EndpointItem.jsx | 48 +++++++-- .../Input/NewConversationMenu/FileUpload.jsx | 10 +- .../Input/NewConversationMenu/index.jsx | 36 ++++--- .../components/Input/SetTokenDialog/index.jsx | 102 ++++++++++++++++++ client/src/components/Input/SubmitButton.jsx | 45 +++++++- client/src/components/Input/index.jsx | 5 +- .../Nav/ExportConversation/ExportModel.jsx | 8 +- client/src/store/conversation.js | 8 +- client/src/store/index.js | 4 +- client/src/store/token.js | 21 ++++ client/src/utils/cleanupPreset.js | 6 +- client/src/utils/getDefaultConversation.js | 28 ++--- client/src/utils/handleSubmit.js | 14 ++- 22 files changed, 309 insertions(+), 86 deletions(-) create mode 100644 client/src/components/Input/SetTokenDialog/index.jsx create mode 100644 client/src/store/token.js diff --git a/api/.env.example b/api/.env.example index 4fa5a432a1..83da008446 100644 --- a/api/.env.example +++ b/api/.env.example @@ -25,8 +25,9 @@ MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone" OPENAI_KEY= # Identify the available models, sperate by comma, and not space in it +# The first will be default # Leave it blank to use internal settings. -# OPENAI_MODELS=gpt-4,text-davinci-003,gpt-3.5-turbo,gpt-3.5-turbo-0301 +OPENAI_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-0301,text-davinci-003,gpt-4 # Reverse proxy setting for OpenAI # https://github.com/waylaidwanderer/node-chatgpt-api#using-a-reverse-proxy @@ -39,6 +40,8 @@ OPENAI_KEY= # BingAI Tokens: the "_U" cookies value from bing.com # Leave it and BINGAI_USER_TOKEN blank to disable this endpoint. +# Set to "user_providered" to allow user provided token. +# BINGAI_TOKEN="user_providered" BINGAI_TOKEN= # BingAI Host: @@ -46,12 +49,6 @@ BINGAI_TOKEN= # Leave it blank to use default server. # BINGAI_HOST="https://cn.bing.com" -# BingAI User defined Token -# Allow user to set their own token by client -# Uncomment this to enable this feature. -# (Not implemented yet.) -# BINGAI_USER_TOKEN=1 - ############################# # Endpoint chatGPT: @@ -61,11 +58,14 @@ BINGAI_TOKEN= # Access token from https://chat.openai.com/api/auth/session # Exposes your access token to CHATGPT_REVERSE_PROXY # Leave it blank to disable this endpoint +# Set to "user_provide" to allow user provided token. +# CHATGPT_TOKEN="user_provide" CHATGPT_TOKEN= # Identify the available models, sperate by comma, and not space in it +# The first will be default # Leave it blank to use internal settings. -# CHATGPT_MODELS=text-davinci-002-render-sha,text-davinci-002-render-paid,gpt-4 +CHATGPT_MODELS=text-davinci-002-render-sha,text-davinci-002-render-paid,gpt-4 # Reverse proxy setting for OpenAI # https://github.com/waylaidwanderer/node-chatgpt-api#using-a-reverse-proxy diff --git a/api/app/clients/bingai.js b/api/app/clients/bingai.js index ca790ec43d..700c7b72a9 100644 --- a/api/app/clients/bingai.js +++ b/api/app/clients/bingai.js @@ -13,6 +13,7 @@ const askBing = async ({ clientId, invocationId, toneStyle, + token, onProgress }) => { const { BingAIClient } = await import('@waylaidwanderer/chatgpt-api'); @@ -22,7 +23,7 @@ const askBing = async ({ const bingAIClient = new BingAIClient({ // "_U" cookie from bing.com - userToken: process.env.BINGAI_TOKEN, + userToken: process.env.BINGAI_TOKEN == 'user_provide' ? token : process.env.BINGAI_TOKEN ?? null, // If the above doesn't work, provide all your cookies as a string instead // cookies: '', debug: false, diff --git a/api/app/clients/chatgpt-browser.js b/api/app/clients/chatgpt-browser.js index 4eb50a1476..0844e5c173 100644 --- a/api/app/clients/chatgpt-browser.js +++ b/api/app/clients/chatgpt-browser.js @@ -6,6 +6,7 @@ const browserClient = async ({ parentMessageId, conversationId, model, + token, onProgress, abortController }) => { @@ -18,7 +19,7 @@ const browserClient = async ({ // Warning: This will expose your access token to a third party. Consider the risks before using this. reverseProxyUrl: process.env.CHATGPT_REVERSE_PROXY || 'https://bypass.churchless.tech/api/conversation', // Access token from https://chat.openai.com/api/auth/session - accessToken: process.env.CHATGPT_TOKEN, + accessToken: process.env.CHATGPT_TOKEN == 'user_provide' ? token : process.env.CHATGPT_TOKEN ?? null, model: model, // debug: true proxy: process.env.PROXY || null diff --git a/api/server/routes/ask/askBingAI.js b/api/server/routes/ask/askBingAI.js index 05c2220285..9f8d9bc878 100644 --- a/api/server/routes/ask/askBingAI.js +++ b/api/server/routes/ask/askBingAI.js @@ -39,7 +39,8 @@ router.post('/', async (req, res) => { jailbreakConversationId: req.body?.jailbreakConversationId ?? null, systemMessage: req.body?.systemMessage ?? null, context: req.body?.context ?? null, - toneStyle: req.body?.toneStyle ?? 'fast' + toneStyle: req.body?.toneStyle ?? 'fast', + token: req.body?.token ?? null }; else endpointOption = { @@ -49,7 +50,8 @@ router.post('/', async (req, res) => { conversationSignature: req.body?.conversationSignature ?? null, clientId: req.body?.clientId ?? null, invocationId: req.body?.invocationId ?? null, - toneStyle: req.body?.toneStyle ?? 'fast' + toneStyle: req.body?.toneStyle ?? 'fast', + token: req.body?.token ?? null }; console.log('ask log', { diff --git a/api/server/routes/ask/askChatGPTBrowser.js b/api/server/routes/ask/askChatGPTBrowser.js index 4592ab98b4..4e416e7c4a 100644 --- a/api/server/routes/ask/askChatGPTBrowser.js +++ b/api/server/routes/ask/askChatGPTBrowser.js @@ -33,7 +33,8 @@ router.post('/', async (req, res) => { // build endpoint option const endpointOption = { - model: req.body?.model ?? 'text-davinci-002-render-sha' + model: req.body?.model ?? 'text-davinci-002-render-sha', + token: req.body?.token ?? null }; const availableModels = getChatGPTBrowserModels(); diff --git a/api/server/routes/endpoints.js b/api/server/routes/endpoints.js index bcdd051e2f..70ce661428 100644 --- a/api/server/routes/endpoints.js +++ b/api/server/routes/endpoints.js @@ -18,8 +18,15 @@ const getChatGPTBrowserModels = () => { router.get('/', function (req, res) { const azureOpenAI = !!process.env.AZURE_OPENAI_KEY; const openAI = process.env.OPENAI_KEY ? { availableModels: getOpenAIModels() } : false; - const bingAI = !!process.env.BINGAI_TOKEN; - const chatGPTBrowser = process.env.CHATGPT_TOKEN ? { availableModels: getChatGPTBrowserModels() } : false; + const bingAI = process.env.BINGAI_TOKEN + ? { userProvide: process.env.BINGAI_TOKEN == 'user_provide' } + : false; + const chatGPTBrowser = process.env.CHATGPT_TOKEN + ? { + userProvide: process.env.CHATGPT_TOKEN == 'user_provide', + availableModels: getChatGPTBrowserModels() + } + : false; res.send(JSON.stringify({ azureOpenAI, openAI, bingAI, chatGPTBrowser })); }); diff --git a/client/src/components/Endpoints/EditPresetDialog.jsx b/client/src/components/Endpoints/EditPresetDialog.jsx index e264920772..c9d618dbd2 100644 --- a/client/src/components/Endpoints/EditPresetDialog.jsx +++ b/client/src/components/Endpoints/EditPresetDialog.jsx @@ -21,7 +21,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => { const setPresets = useSetRecoilState(store.presets); const availableEndpoints = useRecoilValue(store.availableEndpoints); - const endpointsFilter = useRecoilValue(store.endpointsFilter); + const endpointsConfig = useRecoilValue(store.endpointsConfig); const setOption = param => newValue => { let update = {}; @@ -32,7 +32,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => { ...prevState, ...update }, - endpointsFilter + endpointsConfig }) ); }; @@ -44,7 +44,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => { axios({ method: 'post', url: '/api/presets', - data: cleanupPreset({ preset, endpointsFilter }), + data: cleanupPreset({ preset, endpointsConfig }), withCredentials: true }).then(res => { setPresets(res?.data); @@ -54,7 +54,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => { const exportPreset = () => { const fileName = filenamify(preset?.title || 'preset'); exportFromJSON({ - data: cleanupPreset({ preset, endpointsFilter }), + data: cleanupPreset({ preset, endpointsConfig }), fileName, exportType: exportFromJSON.types.json }); diff --git a/client/src/components/Endpoints/EndpointOptionsDialog.jsx b/client/src/components/Endpoints/EndpointOptionsDialog.jsx index dc62499584..17b9528f64 100644 --- a/client/src/components/Endpoints/EndpointOptionsDialog.jsx +++ b/client/src/components/Endpoints/EndpointOptionsDialog.jsx @@ -16,7 +16,7 @@ const EndpointOptionsDialog = ({ open, onOpenChange, preset: _preset, title }) = const [preset, setPreset] = useState(_preset); const [saveAsDialogShow, setSaveAsDialogShow] = useState(false); - const endpointsFilter = useRecoilValue(store.endpointsFilter); + const endpointsConfig = useRecoilValue(store.endpointsConfig); const setOption = param => newValue => { let update = {}; @@ -33,7 +33,7 @@ const EndpointOptionsDialog = ({ open, onOpenChange, preset: _preset, title }) = const exportPreset = () => { exportFromJSON({ - data: cleanupPreset({ preset, endpointsFilter }), + data: cleanupPreset({ preset, endpointsConfig }), fileName: `${preset?.title}.json`, exportType: exportFromJSON.types.json }); diff --git a/client/src/components/Endpoints/SaveAsPresetDialog.jsx b/client/src/components/Endpoints/SaveAsPresetDialog.jsx index 9fd51153e9..9e85b568cd 100644 --- a/client/src/components/Endpoints/SaveAsPresetDialog.jsx +++ b/client/src/components/Endpoints/SaveAsPresetDialog.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; import DialogTemplate from '../ui/DialogTemplate'; import { Dialog } from '../ui/Dialog.tsx'; @@ -11,7 +11,7 @@ import store from '~/store'; const SaveAsPresetDialog = ({ open, onOpenChange, preset }) => { const [title, setTitle] = useState(preset?.title || 'My Preset'); - const endpointsFilter = useRecoilValue(store.endpointsFilter); + const endpointsConfig = useRecoilValue(store.endpointsConfig); const createPresetMutation = useCreatePresetMutation(); const defaultTextProps = @@ -23,7 +23,7 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }) => { ...preset, title }, - endpointsFilter + endpointsConfig }); createPresetMutation.mutate(_preset); }; diff --git a/client/src/components/Input/NewConversationMenu/EndpointItem.jsx b/client/src/components/Input/NewConversationMenu/EndpointItem.jsx index a783750227..690e120e82 100644 --- a/client/src/components/Input/NewConversationMenu/EndpointItem.jsx +++ b/client/src/components/Input/NewConversationMenu/EndpointItem.jsx @@ -1,8 +1,16 @@ -import React from 'react'; +import React, { useState } from 'react'; import { DropdownMenuRadioItem } from '../../ui/DropdownMenu.tsx'; +import { Settings } from 'lucide-react'; import getIcon from '~/utils/getIcon'; +import { useRecoilValue } from 'recoil'; +import SetTokenDialog from '../SetTokenDialog'; + +import store from '../../../store'; export default function ModelItem({ endpoint, value, onSelect }) { + const [setTokenDialogOpen, setSetTokenDialogOpen] = useState(false); + const endpointsConfig = useRecoilValue(store.endpointsConfig); + const icon = getIcon({ size: 20, endpoint, @@ -10,15 +18,37 @@ export default function ModelItem({ endpoint, value, onSelect }) { className: 'mr-2' }); + const isuserProvide = endpointsConfig?.[endpoint]?.userProvide; + // regular model return ( - - {icon} - {endpoint} - {!!['azureOpenAI', 'openAI'].find(e => e === endpoint) && $} - + <> + + {icon} + {endpoint} + {!!['azureOpenAI', 'openAI'].find(e => e === endpoint) && $} +
+ {isuserProvide ? ( + + ) : null} + + + ); } diff --git a/client/src/components/Input/NewConversationMenu/FileUpload.jsx b/client/src/components/Input/NewConversationMenu/FileUpload.jsx index 69f2bf1dba..15b8e1a743 100644 --- a/client/src/components/Input/NewConversationMenu/FileUpload.jsx +++ b/client/src/components/Input/NewConversationMenu/FileUpload.jsx @@ -7,7 +7,7 @@ import store from '~/store'; const FileUpload = ({ onFileSelected }) => { // const setPresets = useSetRecoilState(store.presets); - const endpointsFilter = useRecoilValue(store.endpointsFilter); + const endpointsConfig = useRecoilValue(store.endpointsConfig); const handleFileChange = event => { const file = event.target.files[0]; @@ -16,7 +16,7 @@ const FileUpload = ({ onFileSelected }) => { const reader = new FileReader(); reader.onload = e => { const jsonData = JSON.parse(e.target.result); - onFileSelected({ ...cleanupPreset({ preset: jsonData, endpointsFilter }), presetId: null }); + onFileSelected({ ...cleanupPreset({ preset: jsonData, endpointsConfig }), presetId: null }); }; reader.readAsText(file); }; @@ -24,10 +24,10 @@ const FileUpload = ({ onFileSelected }) => { return ( { + const [token, setToken] = useState(''); + const { getToken, saveToken } = store.useToken(endpoint); + + const defaultTextProps = + 'rounded-md border border-gray-300 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-400 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0'; + + const submit = () => { + saveToken(token); + onOpenChange(false); + }; + + useEffect(() => { + setToken(getToken() ?? ''); + }, [open]); + + const helpText = { + bingAI: ( + + 'The Bing Access Token is the "_U" cookie from bing.com. Use dev tools or an extension while logged + into the site to view it.' + + ), + chatGPTBrowser: ( + + To get your Access token For ChatGPT 'Free Version', login to{' '} + + https://chat.openai.com + + , then visit{' '} + + https://chat.openai.com/api/auth/session + + . Copy access token. + + ) + }; + + return ( + + + + setToken(e.target.value || '')} + placeholder="Set the token." + className={cn( + defaultTextProps, + 'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0' + )} + /> + + Your token will be send to the server, but we won't save it. + + {helpText?.[endpoint]} +
+ } + selection={{ + selectHandler: submit, + selectClasses: 'bg-green-600 hover:bg-green-700 dark:hover:bg-green-800 text-white', + selectText: 'Submit' + }} + /> + + ); +}; + +export default SetTokenDialog; diff --git a/client/src/components/Input/SubmitButton.jsx b/client/src/components/Input/SubmitButton.jsx index 1198a4eee2..0773208d36 100644 --- a/client/src/components/Input/SubmitButton.jsx +++ b/client/src/components/Input/SubmitButton.jsx @@ -1,12 +1,31 @@ -import React from 'react'; +import React, { useState } from 'react'; import StopGeneratingIcon from '../svg/StopGeneratingIcon'; +import { Settings } from 'lucide-react'; +import SetTokenDialog from './SetTokenDialog'; +import store from '../../store'; + +export default function SubmitButton({ + endpoint, + submitMessage, + handleStopGenerating, + disabled, + isSubmitting, + endpointsConfig +}) { + const [setTokenDialogOpen, setSetTokenDialogOpen] = useState(false); + const { getToken } = store.useToken(endpoint); + + const isTokenProvided = endpointsConfig?.[endpoint]?.userProvide ? !!getToken() : true; -export default function SubmitButton({ submitMessage, handleStopGenerating, disabled, isSubmitting }) { const clickHandler = e => { e.preventDefault(); submitMessage(); }; + const setToken = () => { + setSetTokenDialogOpen(true); + }; + if (isSubmitting) return ( // ); - else + else if (!isTokenProvided) { + return ( + <> + + + + ); + } else return (