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] 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 (