mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
Merge pull request #142 from danny-avila/feat-endpoint-style-structure
Endpoint style structure and more customize of all model.
This commit is contained in:
commit
b4d451d7ae
106 changed files with 6143 additions and 9455 deletions
113
LOCAL_INSTALL.md
113
LOCAL_INSTALL.md
|
|
@ -1,81 +1,82 @@
|
|||
### Local
|
||||
- **Install the prerequisites**
|
||||
- **Download chatgpt-clone**
|
||||
- Download the latest release here: https://github.com/danny-avila/chatgpt-clone/releases/
|
||||
- Or by clicking on the green code button in the top of the page and selecting "Download ZIP"
|
||||
- Or (Recommended if you have Git installed) pull the latest release from the main branch
|
||||
- If you downloaded a zip file, extract the content in "C:/chatgpt-clone/"
|
||||
-**IMPORTANT : If you install the files somewhere else modify the instructions accordingly**
|
||||
|
||||
- **To enable the Conversation search feature:**
|
||||
-IF YOU DON'T WANT THIS FEATURE YOU CAN SKIP THIS STEP
|
||||
- Download MeileSearch latest release from : https://github.com/meilisearch/meilisearch/releases
|
||||
- Copy it to "C:/chatgpt-clone/"
|
||||
- Rename the file to "meilisearch.exe"
|
||||
- Open it by double clicking on it
|
||||
- Copy the generated Master Key and save it somewhere (You will need it later)
|
||||
|
||||
- **Download and Install Node.js**
|
||||
- Navigate to https://nodejs.org/en/download and to download the latest Node.js version for your OS (The Node.js installer includes the NPM package manager.)
|
||||
|
||||
- **Create a MongoDB database**
|
||||
- Navigate to https://www.mongodb.com/ and Sign In or Create an account
|
||||
- Create a new project
|
||||
- Build a Database using the free plan and name the cluster (example: chatgpt-clone)
|
||||
- Use the "Username and Password" method for authentication
|
||||
- Add your current IP to the access list
|
||||
- Then in the Database Deployment tab click on Connect
|
||||
- In "Choose a connection method" select "Connect your application"
|
||||
- Driver = Node.js / Version = 4.1 or later
|
||||
- Copy the connection string and save it somewhere(you will need it later)
|
||||
|
||||
- **Get your OpenAI API key** here: https://platform.openai.com/account/api-keys and save it somewhere safe (you will need it later)
|
||||
|
||||
- **Get your Bing Access Token**
|
||||
- Using MS Edge, navigate to bing.com
|
||||
- Make sure you are logged in
|
||||
- Open the DevTools by pressing F12 on your keyboard
|
||||
- Click on the tab "Application" (On the left of the DevTools)
|
||||
- Expand the "Cookies" (Under "Storage")
|
||||
- You need to copy the value of the "_U" cookie, save it somewhere, you will need it later
|
||||
|
||||
- **Install the prerequisites**
|
||||
|
||||
- **Download chatgpt-clone**
|
||||
- Download the latest release here: https://github.com/danny-avila/chatgpt-clone/releases/
|
||||
- Or by clicking on the green code button in the top of the page and selecting "Download ZIP"
|
||||
- Or (Recommended if you have Git installed) pull the latest release from the main branch
|
||||
- If you downloaded a zip file, extract the content in "C:/chatgpt-clone/" -**IMPORTANT : If you install the files somewhere else modify the instructions accordingly**
|
||||
- **To enable the Conversation search feature:**
|
||||
-IF YOU DON'T WANT THIS FEATURE YOU CAN SKIP THIS STEP
|
||||
|
||||
- Download MeileSearch latest release from : https://github.com/meilisearch/meilisearch/releases
|
||||
- Copy it to "C:/chatgpt-clone/"
|
||||
- Rename the file to "meilisearch.exe"
|
||||
- Open it by double clicking on it
|
||||
- Copy the generated Master Key and save it somewhere (You will need it later)
|
||||
|
||||
- **Download and Install Node.js**
|
||||
- Navigate to https://nodejs.org/en/download and to download the latest Node.js version for your OS (The Node.js installer includes the NPM package manager.)
|
||||
- **Create a MongoDB database**
|
||||
- Navigate to https://www.mongodb.com/ and Sign In or Create an account
|
||||
- Create a new project
|
||||
- Build a Database using the free plan and name the cluster (example: chatgpt-clone)
|
||||
- Use the "Username and Password" method for authentication
|
||||
- Add your current IP to the access list
|
||||
- Then in the Database Deployment tab click on Connect
|
||||
- In "Choose a connection method" select "Connect your application"
|
||||
- Driver = Node.js / Version = 4.1 or later
|
||||
- Copy the connection string and save it somewhere(you will need it later)
|
||||
- **Get your OpenAI API key** here: https://platform.openai.com/account/api-keys and save it somewhere safe (you will need it later)
|
||||
|
||||
- **Get your Bing Access Token**
|
||||
- Using MS Edge, navigate to bing.com
|
||||
- Make sure you are logged in
|
||||
- Open the DevTools by pressing F12 on your keyboard
|
||||
- Click on the tab "Application" (On the left of the DevTools)
|
||||
- Expand the "Cookies" (Under "Storage")
|
||||
- You need to copy the value of the "\_U" cookie, save it somewhere, you will need it later
|
||||
|
||||
- **Create the ".env" File** You will need all your credentials, (API keys, access tokens, and Mongo Connection String, MeileSearch Master Key)
|
||||
- Open "C:/chatgpt-clone/api/.env.example" in a text editor
|
||||
- At this line **MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone"**
|
||||
Replace mongodb://127.0.0.1:27017/chatgpt-clone with the MondoDB connection string you saved earlier, **remove "&w=majority" at the end**
|
||||
- It should look something like this: "MONGO_URI="mongodb+srv://username:password@chatgpt-clone.lfbcwz3.mongodb.net/?retryWrites=true"
|
||||
- At this line **OPENAI_KEY=** you need to add your openai API key
|
||||
- Add your Bing token to this line **BING_TOKEN=** (needed for BingChat & Sydney)
|
||||
- If you want to enable Search, **SEARCH=TRUE** if you do not want to enable search **SEARCH=FALSE**
|
||||
- Add your previously saved MeiliSearch Master key to this line **MEILI_MASTER_KEY=** (the key is needed if search is enabled even on local install or you may encounter errors)
|
||||
- Save the file as **"C:/chatgpt-clone/api/.env"**
|
||||
- Open "C:/chatgpt-clone/api/.env.example" in a text editor
|
||||
- At this line **MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone"**
|
||||
Replace mongodb://127.0.0.1:27017/chatgpt-clone with the MondoDB connection string you saved earlier, **remove "&w=majority" at the end**
|
||||
- It should look something like this: "MONGO_URI="mongodb+srv://username:password@chatgpt-clone.lfbcwz3.mongodb.net/?retryWrites=true"
|
||||
- At this line **OPENAI_KEY=** you need to add your openai API key
|
||||
- Add your Bing token to this line **BINGAI_TOKEN=** (needed for BingChat & Sydney)
|
||||
- If you want to enable Search, **SEARCH=TRUE** if you do not want to enable search **SEARCH=FALSE**
|
||||
- Add your previously saved MeiliSearch Master key to this line **MEILI_MASTER_KEY=** (the key is needed if search is enabled even on local install or you may encounter errors)
|
||||
- Save the file as **"C:/chatgpt-clone/api/.env"**
|
||||
|
||||
**DO THIS ONCE AFTER EVERY UPDATE**
|
||||
|
||||
- **Run** `npm ci` in the "C:/chatgpt-clone/api" directory
|
||||
- **Run** `npm ci` in the "C:/chatgpt-clone/client" directory
|
||||
- **Run** `npm run build` in the "C:/chatgpt-clone/client"
|
||||
|
||||
**DO THIS EVERY TIME YOU WANT TO START CHATGPT-CLONE**
|
||||
|
||||
- **Run** `"meilisearch --master-key put_your_meilesearch_Master_Key_here"` in the "C:/chatgpt-clone" directory (Only if SEARCH=TRUE)
|
||||
- **Run** `npm start` in the "C:/chatgpt-clone/api" directory
|
||||
- **Run** `npm start` in the "C:/chatgpt-clone/api" directory
|
||||
|
||||
- **Visit** http://localhost:3080 (default port) & enjoy
|
||||
|
||||
|
||||
OPTIONAL BUT RECOMMENDED
|
||||
|
||||
- **Make a batch file to automate the starting process**
|
||||
- Open a text editor
|
||||
- Paste the following code in a new document
|
||||
- Put your MeiliSearch master key instead of "your_master_key_goes_here"
|
||||
- Save the file as "C:/chatgpt-clone/chatgpt-clone.bat"
|
||||
- you can make a shortcut of this batch file and put it anywhere
|
||||
- Open a text editor
|
||||
- Paste the following code in a new document
|
||||
- Put your MeiliSearch master key instead of "your_master_key_goes_here"
|
||||
- Save the file as "C:/chatgpt-clone/chatgpt-clone.bat"
|
||||
- you can make a shortcut of this batch file and put it anywhere
|
||||
|
||||
```
|
||||
REM the meilisearch executable needs to be at the root of the chatgpt-clone directory
|
||||
|
||||
start "MeiliSearch" cmd /k "meilisearch --master-key your_master_key_goes_here
|
||||
|
||||
REM ↑↑↑ meilisearch is the name of the meilisearch executable, put your own master key there
|
||||
REM ↑↑↑ meilisearch is the name of the meilisearch executable, put your own master key there
|
||||
|
||||
start "ChatGPT-Clone" cmd /k "cd api && npm start"
|
||||
|
||||
|
|
|
|||
|
|
@ -15,34 +15,68 @@ NODE_ENV=development
|
|||
# Change this to your MongoDB URI if different and I recommend appending chatgpt-clone
|
||||
MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone"
|
||||
|
||||
# API key configuration.
|
||||
# Leave blank if you don't want them.
|
||||
|
||||
#############################
|
||||
# Endpoint OpenAI:
|
||||
#############################
|
||||
|
||||
# Access key from OpenAI platform
|
||||
# Leave it blank to disable this endpoint
|
||||
OPENAI_KEY=
|
||||
|
||||
# Default ChatGPT API Model, options: 'gpt-4', 'text-davinci-003', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0301'
|
||||
# you will have errors if you don't have access to a model like 'gpt-4', defaults to turbo if left empty/excluded.
|
||||
DEFAULT_API_GPT=gpt-3.5-turbo
|
||||
# Identify the available models, sperate by comma, and not space in it
|
||||
# Leave it blank to use internal settings.
|
||||
# OPENAI_MODELS=gpt-4,text-davinci-003,gpt-3.5-turbo,gpt-3.5-turbo-0301
|
||||
|
||||
# _U Cookies Value from bing.com
|
||||
BING_TOKEN=
|
||||
# Reverse proxy setting for OpenAI
|
||||
# https://github.com/waylaidwanderer/node-chatgpt-api#using-a-reverse-proxy
|
||||
# OPENAI_REVERSE_PROXY=<YOUR REVERSE PROXY>
|
||||
|
||||
|
||||
#############################
|
||||
# Endpoint BingAI (Also jailbreak Sydney):
|
||||
#############################
|
||||
|
||||
# BingAI Tokens: the "_U" cookies value from bing.com
|
||||
# Leave it and BINGAI_USER_TOKEN blank to disable this endpoint.
|
||||
BINGAI_TOKEN=
|
||||
|
||||
# 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:
|
||||
#############################
|
||||
|
||||
# ChatGPT Browser Client (free but use at your own risk)
|
||||
# Access token from https://chat.openai.com/api/auth/session
|
||||
# Exposes your access token to a 3rd party
|
||||
# Exposes your access token to CHATGPT_REVERSE_PROXY
|
||||
# Leave it blank to disable this endpoint
|
||||
CHATGPT_TOKEN=
|
||||
# If you have access to other models on the official site, you can use them here.
|
||||
# Defaults to 'text-davinci-002-render-sha' if left empty.
|
||||
# options: gpt-4, text-davinci-002-render, text-davinci-002-render-paid, or text-davinci-002-render-sha
|
||||
# You cannot use a model that your account does not have access to. You can check
|
||||
# which ones you have access to by opening DevTools and going to the Network tab.
|
||||
# Refresh the page and look at the response body for https://chat.openai.com/backend-api/models.
|
||||
BROWSER_MODEL=
|
||||
|
||||
# Identify the available models, sperate by comma, and not space in it
|
||||
# Leave it blank to use internal settings.
|
||||
# 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
|
||||
# By default it will use the node-chatgpt-api recommended proxy, (it's a third party server)
|
||||
# CHATGPT_REVERSE_PROXY=<YOUR REVERSE PROXY>
|
||||
|
||||
|
||||
#############################
|
||||
# Search:
|
||||
#############################
|
||||
|
||||
# ENABLING SEARCH MESSAGES/CONVOS
|
||||
# Requires installation of free self-hosted Meilisearch or Paid Remote Plan (Remote not tested)
|
||||
# The easiest setup for this is through docker-compose, which takes care of it for you.
|
||||
# SEARCH=TRUE
|
||||
SEARCH=TRUE
|
||||
# SEARCH=1
|
||||
SEARCH=1
|
||||
|
||||
# REQUIRED FOR SEARCH: MeiliSearch Host, mainly for api server to connect to the search server.
|
||||
# must replace '0.0.0.0' with 'meilisearch' if serving meilisearch with docker-compose
|
||||
|
|
@ -63,8 +97,11 @@ MEILI_HTTP_ADDR='0.0.0.0:7700' # <-- local/remote
|
|||
MEILI_MASTER_KEY=JKMW-hGc7v_D1FkJVdbRSDNFLZcUv3S75yrxXP0SmcU # <-- ready made secure key for docker-compose
|
||||
|
||||
|
||||
#############################
|
||||
# User System
|
||||
# global enable/disable the sample user system.
|
||||
#############################
|
||||
|
||||
# Enable the user system.
|
||||
# this is not a ready to use user system.
|
||||
# dont't use it, unless you can write your own code.
|
||||
# ENABLE_USER_SYSTEM= # <-- make sure you don't comment this back in if you're not using your own user system
|
||||
|
|
|
|||
|
|
@ -1,30 +1,67 @@
|
|||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
|
||||
const askBing = async ({ text, onProgress, convo }) => {
|
||||
const askBing = async ({
|
||||
text,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
jailbreak,
|
||||
jailbreakConversationId,
|
||||
context,
|
||||
systemMessage,
|
||||
conversationSignature,
|
||||
clientId,
|
||||
invocationId,
|
||||
toneStyle,
|
||||
onProgress
|
||||
}) => {
|
||||
const { BingAIClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
};
|
||||
|
||||
const bingAIClient = new BingAIClient({
|
||||
// "_U" cookie from bing.com
|
||||
userToken: process.env.BING_TOKEN,
|
||||
userToken: process.env.BINGAI_TOKEN,
|
||||
// If the above doesn't work, provide all your cookies as a string instead
|
||||
// cookies: '',
|
||||
debug: false,
|
||||
cache: { store: new KeyvFile({ filename: './data/cache.json' }) },
|
||||
cache: store,
|
||||
proxy: process.env.PROXY || null
|
||||
});
|
||||
|
||||
let options = { onProgress };
|
||||
if (convo) {
|
||||
options = { ...options, ...convo };
|
||||
let options = {};
|
||||
|
||||
if (jailbreakConversationId == 'false') {
|
||||
jailbreakConversationId = false;
|
||||
}
|
||||
|
||||
if (options?.jailbreakConversationId == 'false') {
|
||||
options.jailbreakConversationId = false;
|
||||
}
|
||||
if (jailbreak)
|
||||
options = {
|
||||
jailbreakConversationId: jailbreakConversationId || jailbreak,
|
||||
context,
|
||||
systemMessage,
|
||||
parentMessageId,
|
||||
toneStyle,
|
||||
onProgress
|
||||
};
|
||||
else {
|
||||
options = {
|
||||
conversationId,
|
||||
context,
|
||||
systemMessage,
|
||||
parentMessageId,
|
||||
toneStyle,
|
||||
onProgress
|
||||
};
|
||||
|
||||
if (convo.toneStyle) {
|
||||
options.toneStyle = convo.toneStyle;
|
||||
// don't give those parameters for new conversation
|
||||
// for new conversation, conversationSignature always is null
|
||||
if (conversationSignature) {
|
||||
options.conversationSignature = conversationSignature;
|
||||
options.clientId = clientId;
|
||||
options.invocationId = invocationId;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('bing options', options);
|
||||
|
|
@ -33,30 +70,8 @@ const askBing = async ({ text, onProgress, convo }) => {
|
|||
|
||||
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] ]
|
||||
// }
|
||||
// }
|
||||
// for reference:
|
||||
// https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/demos/use-bing-client.js
|
||||
};
|
||||
|
||||
module.exports = { askBing };
|
||||
|
|
|
|||
|
|
@ -1,40 +1,39 @@
|
|||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
const set = new Set(["gpt-4", "text-davinci-002-render", "text-davinci-002-render-paid", "text-davinci-002-render-sha"]);
|
||||
|
||||
const clientOptions = {
|
||||
// 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,
|
||||
// debug: true
|
||||
proxy: process.env.PROXY || null,
|
||||
};
|
||||
|
||||
// You can check which models you have access to by opening DevTools and going to the Network tab.
|
||||
// Refresh the page and look at the response body for https://chat.openai.com/backend-api/models.
|
||||
if (set.has(process.env.BROWSER_MODEL)) {
|
||||
clientOptions.model = process.env.BROWSER_MODEL;
|
||||
}
|
||||
|
||||
const browserClient = async ({ text, onProgress, convo, abortController }) => {
|
||||
const browserClient = async ({
|
||||
text,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
model,
|
||||
onProgress,
|
||||
abortController
|
||||
}) => {
|
||||
const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
};
|
||||
|
||||
const clientOptions = {
|
||||
// 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,
|
||||
model: model,
|
||||
// debug: true
|
||||
proxy: process.env.PROXY || null
|
||||
};
|
||||
|
||||
const client = new ChatGPTBrowserClient(clientOptions, store);
|
||||
let options = { onProgress, abortController };
|
||||
|
||||
if (!!convo.parentMessageId && !!convo.conversationId) {
|
||||
options = { ...options, ...convo };
|
||||
if (!!parentMessageId && !!conversationId) {
|
||||
options = { ...options, parentMessageId, conversationId };
|
||||
}
|
||||
|
||||
console.log('gptBrowser options', options, clientOptions);
|
||||
console.log('gptBrowser clientOptions', clientOptions);
|
||||
|
||||
/* will error if given a convoId at the start */
|
||||
if (convo.parentMessageId.startsWith('0000')) {
|
||||
if (parentMessageId === '00000000-0000-0000-0000-000000000000') {
|
||||
delete options.conversationId;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,49 @@
|
|||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
const set = new Set(['gpt-4', 'text-davinci-003', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0301']);
|
||||
// const set = new Set(['gpt-4', 'text-davinci-003', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0301']);
|
||||
|
||||
const clientOptions = {
|
||||
modelOptions: {
|
||||
model: 'gpt-3.5-turbo'
|
||||
},
|
||||
proxy: process.env.PROXY || null,
|
||||
debug: false
|
||||
};
|
||||
|
||||
if (set.has(process.env.DEFAULT_API_GPT)) {
|
||||
clientOptions.modelOptions.model = process.env.DEFAULT_API_GPT;
|
||||
}
|
||||
|
||||
const askClient = async ({ text, onProgress, convo, abortController }) => {
|
||||
const askClient = async ({
|
||||
text,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
model,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
temperature,
|
||||
top_p,
|
||||
presence_penalty,
|
||||
frequency_penalty,
|
||||
onProgress,
|
||||
abortController
|
||||
}) => {
|
||||
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
};
|
||||
|
||||
const clientOptions = {
|
||||
// Warning: This will expose your access token to a third party. Consider the risks before using this.
|
||||
reverseProxyUrl: process.env.OPENAI_REVERSE_PROXY || null,
|
||||
|
||||
modelOptions: {
|
||||
model: model,
|
||||
temperature,
|
||||
top_p,
|
||||
presence_penalty,
|
||||
frequency_penalty
|
||||
},
|
||||
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
proxy: process.env.PROXY || null,
|
||||
debug: false
|
||||
};
|
||||
|
||||
const client = new ChatGPTClient(process.env.OPENAI_KEY, clientOptions, store);
|
||||
let options = { onProgress, abortController };
|
||||
|
||||
if (!!convo.parentMessageId && !!convo.conversationId) {
|
||||
options = { ...options, ...convo };
|
||||
if (!!parentMessageId && !!conversationId) {
|
||||
options = { ...options, parentMessageId, conversationId };
|
||||
}
|
||||
|
||||
const res = await client.sendMessage(text, options);
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
|
||||
const clientOptions = {
|
||||
modelOptions: {
|
||||
model: 'gpt-3.5-turbo'
|
||||
},
|
||||
proxy: process.env.PROXY || null,
|
||||
debug: false
|
||||
};
|
||||
|
||||
const customClient = async ({ text, onProgress, convo, promptPrefix, chatGptLabel, abortController }) => {
|
||||
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
};
|
||||
|
||||
clientOptions.chatGptLabel = chatGptLabel;
|
||||
|
||||
if (promptPrefix?.length > 0) {
|
||||
clientOptions.promptPrefix = promptPrefix;
|
||||
}
|
||||
|
||||
const client = new ChatGPTClient(process.env.OPENAI_KEY, clientOptions, store);
|
||||
|
||||
let options = { onProgress, abortController };
|
||||
if (!!convo.parentMessageId && !!convo.conversationId) {
|
||||
options = { ...options, ...convo };
|
||||
}
|
||||
|
||||
const res = await client.sendMessage(text, options);
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = customClient;
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
|
||||
const askSydney = async ({ text, onProgress, convo }) => {
|
||||
const { BingAIClient } = (await import('@waylaidwanderer/chatgpt-api'));
|
||||
|
||||
const sydneyClient = 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: { store: new KeyvFile({ filename: './data/cache.json' }) }
|
||||
});
|
||||
|
||||
let options = {
|
||||
jailbreakConversationId: true,
|
||||
onProgress,
|
||||
};
|
||||
|
||||
if (convo.jailbreakConversationId) {
|
||||
options = { ...options, jailbreakConversationId: convo.jailbreakConversationId, parentMessageId: convo.parentMessageId };
|
||||
}
|
||||
|
||||
if (convo.toneStyle) {
|
||||
options.toneStyle = convo.toneStyle;
|
||||
}
|
||||
|
||||
console.log('sydney options', options);
|
||||
|
||||
const res = await sydneyClient.sendMessage(text, options
|
||||
);
|
||||
|
||||
return res;
|
||||
|
||||
// for reference:
|
||||
// https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/demos/use-bing-client.js
|
||||
};
|
||||
|
||||
module.exports = { askSydney };
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
const { askClient } = require('./clients/chatgpt-client');
|
||||
const { browserClient } = require('./clients/chatgpt-browser');
|
||||
const { askBing } = require('./clients/bingai');
|
||||
const { askSydney } = require('./clients/sydney');
|
||||
const customClient = require('./clients/chatgpt-custom');
|
||||
const titleConvo = require('./titleConvo');
|
||||
const getCitations = require('../lib/parse/getCitations');
|
||||
const citeText = require('../lib/parse/citeText');
|
||||
|
|
@ -10,10 +8,8 @@ const citeText = require('../lib/parse/citeText');
|
|||
module.exports = {
|
||||
askClient,
|
||||
browserClient,
|
||||
customClient,
|
||||
askBing,
|
||||
askSydney,
|
||||
titleConvo,
|
||||
getCitations,
|
||||
citeText,
|
||||
citeText
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const proxyEnvToAxiosProxy = proxyString => {
|
|||
return proxyConfig;
|
||||
};
|
||||
|
||||
const titleConvo = async ({ model, text, response }) => {
|
||||
const titleConvo = async ({ endpoint, text, response }) => {
|
||||
let title = 'New Chat';
|
||||
const messages = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { Conversation, } = require('../../models/Conversation');
|
||||
const { getMessages, } = require('../../models/');
|
||||
const { Conversation } = require('../../models/Conversation');
|
||||
const { getMessages } = require('../../models/');
|
||||
|
||||
async function migrateDb() {
|
||||
const migrateToStrictFollowParentMessageIdChain = async () => {
|
||||
try {
|
||||
const conversations = await Conversation.find({ model: null }).exec();
|
||||
const conversations = await Conversation.find({ endpoint: null, model: null }).exec();
|
||||
|
||||
if (!conversations || conversations.length === 0)
|
||||
return { message: '[Migrate] No conversations to migrate' };
|
||||
if (!conversations || conversations.length === 0) return { noNeed: true };
|
||||
|
||||
console.log('Migration: To strict follow the parentMessageId chain.');
|
||||
|
||||
for (let convo of conversations) {
|
||||
const messages = await getMessages({
|
||||
|
|
@ -36,7 +37,7 @@ async function migrateDb() {
|
|||
if (message.sender.toLowerCase() === 'user') {
|
||||
message.isCreatedByUser = true;
|
||||
}
|
||||
|
||||
|
||||
promises.push(message.save());
|
||||
});
|
||||
await Promise.all(promises);
|
||||
|
|
@ -57,7 +58,61 @@ async function migrateDb() {
|
|||
console.log(error);
|
||||
return { message: '[Migrate] Error migrating conversations' };
|
||||
}
|
||||
};
|
||||
|
||||
const migrateToSupportBetterCustomization = async () => {
|
||||
try {
|
||||
const conversations = await Conversation.find({ endpoint: null }).exec();
|
||||
|
||||
if (!conversations || conversations.length === 0) return { noNeed: true };
|
||||
|
||||
console.log('Migration: To support better customization.');
|
||||
|
||||
const promises = [];
|
||||
for (let convo of conversations) {
|
||||
const originalModel = convo?.model;
|
||||
|
||||
if (originalModel === 'chatgpt') {
|
||||
convo.endpoint = 'openAI';
|
||||
convo.model = 'gpt-3.5-turbo';
|
||||
} else if (originalModel === 'chatgptCustom') {
|
||||
convo.endpoint = 'openAI';
|
||||
convo.model = 'gpt-3.5-turbo';
|
||||
} else if (originalModel === 'bingai') {
|
||||
convo.endpoint = 'bingAI';
|
||||
convo.model = null;
|
||||
convo.jailbreak = false;
|
||||
} else if (originalModel === 'sydney') {
|
||||
convo.endpoint = 'bingAI';
|
||||
convo.model = null;
|
||||
convo.jailbreak = true;
|
||||
} else if (originalModel === 'chatgptBrowser') {
|
||||
convo.endpoint = 'chatGPTBrowser';
|
||||
convo.model = 'text-davinci-002-render-sha';
|
||||
convo.jailbreak = true;
|
||||
} else {
|
||||
convo.endpoint = 'openAI';
|
||||
convo.model = 'gpt-3.5-turbo';
|
||||
}
|
||||
|
||||
promises.push(convo.save());
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: '[Migrate] Error migrating conversations' };
|
||||
}
|
||||
};
|
||||
|
||||
async function migrateDb() {
|
||||
let ret = [];
|
||||
ret[0] = await migrateToStrictFollowParentMessageIdChain();
|
||||
ret[1] = await migrateToSupportBetterCustomization();
|
||||
|
||||
const isMigrated = !!ret.find(element => !element?.noNeed);
|
||||
|
||||
if (!isMigrated) console.log('[Migrate] Nothing to migrate');
|
||||
}
|
||||
|
||||
|
||||
module.exports = migrateDb;
|
||||
module.exports = migrateDb;
|
||||
|
|
|
|||
|
|
@ -27,11 +27,6 @@ module.exports = {
|
|||
if (!update.jailbreakConversationId) {
|
||||
update.jailbreakConversationId = null;
|
||||
}
|
||||
if (update.model !== 'chatgptCustom' && update.chatGptLabel && update.promptPrefix) {
|
||||
console.log('Validation error: resetting chatgptCustom fields', update);
|
||||
update.chatGptLabel = null;
|
||||
update.promptPrefix = null;
|
||||
}
|
||||
|
||||
return await Conversation.findOneAndUpdate(
|
||||
{ conversationId: conversationId, user },
|
||||
|
|
@ -149,10 +144,10 @@ module.exports = {
|
|||
}
|
||||
},
|
||||
deleteConvos: async (user, filter) => {
|
||||
let toRemove = await Conversation.find({...filter, user}).select('conversationId')
|
||||
let toRemove = await Conversation.find({ ...filter, user }).select('conversationId');
|
||||
const ids = toRemove.map(instance => instance.conversationId);
|
||||
let deleteCount = await Conversation.deleteMany({...filter, user}).exec();
|
||||
deleteCount.messages = await deleteMessages({conversationId: {$in: ids}});
|
||||
let deleteCount = await Conversation.deleteMany({ ...filter, user }).exec();
|
||||
deleteCount.messages = await deleteMessages({ conversationId: { $in: ids } });
|
||||
return deleteCount;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
const mongoose = require('mongoose');
|
||||
|
||||
const customGptSchema = mongoose.Schema({
|
||||
chatGptLabel: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
promptPrefix: {
|
||||
type: String
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: String
|
||||
},
|
||||
}, { timestamps: true });
|
||||
|
||||
const CustomGpt = mongoose.models.CustomGpt || mongoose.model('CustomGpt', customGptSchema);
|
||||
|
||||
const createCustomGpt = async ({ chatGptLabel, promptPrefix, value, user }) => {
|
||||
try {
|
||||
await CustomGpt.create({
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
value,
|
||||
user
|
||||
});
|
||||
return { chatGptLabel, promptPrefix, value };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { customGpt: 'Error saving customGpt' };
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getCustomGpts: async (user, filter) => {
|
||||
try {
|
||||
return await CustomGpt.find({ ...filter, user }).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { customGpt: 'Error getting customGpts' };
|
||||
}
|
||||
},
|
||||
updateCustomGpt: async (user, { value, ...update }) => {
|
||||
try {
|
||||
const customGpt = await CustomGpt.findOne({ value, user }).exec();
|
||||
|
||||
if (!customGpt) {
|
||||
return await createCustomGpt({ value, ...update, user });
|
||||
} else {
|
||||
return await CustomGpt.findOneAndUpdate({ value, user }, update, {
|
||||
new: true,
|
||||
upsert: true
|
||||
}).exec();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error updating customGpt' };
|
||||
}
|
||||
},
|
||||
updateByLabel: async (user, { prevLabel, ...update }) => {
|
||||
try {
|
||||
return await CustomGpt.findOneAndUpdate({ chatGptLabel: prevLabel, user }, update, {
|
||||
new: true,
|
||||
upsert: true
|
||||
}).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error updating customGpt' };
|
||||
}
|
||||
},
|
||||
deleteCustomGpts: async (user, filter) => {
|
||||
try {
|
||||
return await CustomGpt.deleteMany({ ...filter, user }).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { customGpt: 'Error deleting customGpts' };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1,33 +1,30 @@
|
|||
const Message = require('./schema/messageSchema');
|
||||
module.exports = {
|
||||
Message,
|
||||
saveMessage: async ({ messageId, conversationId, parentMessageId, sender, text, isCreatedByUser=false, error }) => {
|
||||
saveMessage: async ({
|
||||
messageId,
|
||||
newMessageId,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender,
|
||||
text,
|
||||
isCreatedByUser = false,
|
||||
error
|
||||
}) => {
|
||||
try {
|
||||
await Message.findOneAndUpdate({ messageId }, {
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender,
|
||||
text,
|
||||
isCreatedByUser,
|
||||
error
|
||||
}, { upsert: true, new: true });
|
||||
return { messageId, conversationId, parentMessageId, sender, text, isCreatedByUser };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error saving message' };
|
||||
}
|
||||
},
|
||||
saveBingMessage: async ({ messageId, oldMessageId = messageId, conversationId, parentMessageId, sender, text, isCreatedByUser=false, error }) => {
|
||||
try {
|
||||
await Message.findOneAndUpdate({ messageId: oldMessageId }, {
|
||||
messageId,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender,
|
||||
text,
|
||||
isCreatedByUser,
|
||||
error
|
||||
}, { upsert: true, new: true });
|
||||
await Message.findOneAndUpdate(
|
||||
{ messageId },
|
||||
{
|
||||
messageId: newMessageId || messageId,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender,
|
||||
text,
|
||||
isCreatedByUser,
|
||||
error
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
return { messageId, conversationId, parentMessageId, sender, text, isCreatedByUser };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
|
@ -36,29 +33,31 @@ module.exports = {
|
|||
},
|
||||
deleteMessagesSince: async ({ messageId, conversationId }) => {
|
||||
try {
|
||||
const message = await Message.findOne({ messageId }).exec()
|
||||
const message = await Message.findOne({ messageId }).exec();
|
||||
|
||||
if (message)
|
||||
return await Message.find({ conversationId }).deleteMany({ createdAt: { $gt: message.createdAt } }).exec();
|
||||
if (message)
|
||||
return await Message.find({ conversationId })
|
||||
.deleteMany({ createdAt: { $gt: message.createdAt } })
|
||||
.exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error deleting messages' };
|
||||
}
|
||||
},
|
||||
getMessages: async (filter) => {
|
||||
getMessages: async filter => {
|
||||
try {
|
||||
return await Message.find(filter).sort({createdAt: 1}).exec()
|
||||
return await Message.find(filter).sort({ createdAt: 1 }).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error getting messages' };
|
||||
}
|
||||
},
|
||||
deleteMessages: async (filter) => {
|
||||
deleteMessages: async filter => {
|
||||
try {
|
||||
return await Message.deleteMany(filter).exec()
|
||||
return await Message.deleteMany(filter).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error deleting messages' };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
46
api/models/Preset.js
Normal file
46
api/models/Preset.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
const Preset = require('./schema/presetSchema');
|
||||
|
||||
const getPreset = async (user, presetId) => {
|
||||
try {
|
||||
return await Preset.findOne({ user, presetId }).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error getting single preset' };
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Preset,
|
||||
getPreset,
|
||||
getPresets: async (user, filter) => {
|
||||
try {
|
||||
return await Preset.find({ ...filter, user }).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error retriving presets' };
|
||||
}
|
||||
},
|
||||
savePreset: async (user, { presetId, newPresetId, ...preset }) => {
|
||||
try {
|
||||
const update = { presetId, ...preset };
|
||||
if (newPresetId) {
|
||||
update.presetId = newPresetId;
|
||||
}
|
||||
|
||||
return await Preset.findOneAndUpdate(
|
||||
{ presetId, user },
|
||||
{ $set: update },
|
||||
{ new: true, upsert: true }
|
||||
).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error saving preset' };
|
||||
}
|
||||
},
|
||||
deletePresets: async (user, filter) => {
|
||||
let toRemove = await Preset.find({ ...filter, user }).select('presetId');
|
||||
const ids = toRemove.map(instance => instance.presetId);
|
||||
let deleteCount = await Preset.deleteMany({ ...filter, user }).exec();
|
||||
return deleteCount;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,19 +1,20 @@
|
|||
const { getMessages, saveMessage, saveBingMessage, deleteMessagesSince, deleteMessages } = require('./Message');
|
||||
const { getCustomGpts, updateCustomGpt, updateByLabel, deleteCustomGpts } = require('./CustomGpt');
|
||||
const { getMessages, saveMessage, deleteMessagesSince, deleteMessages } = require('./Message');
|
||||
const { getConvoTitle, getConvo, saveConvo, updateConvo } = require('./Conversation');
|
||||
const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset');
|
||||
|
||||
module.exports = {
|
||||
getMessages,
|
||||
saveMessage,
|
||||
saveBingMessage,
|
||||
deleteMessagesSince,
|
||||
deleteMessages,
|
||||
|
||||
getConvoTitle,
|
||||
getConvo,
|
||||
saveConvo,
|
||||
updateConvo,
|
||||
getCustomGpts,
|
||||
updateCustomGpt,
|
||||
updateByLabel,
|
||||
deleteCustomGpts
|
||||
|
||||
getPreset,
|
||||
getPresets,
|
||||
savePreset,
|
||||
deletePresets
|
||||
};
|
||||
|
|
|
|||
62
api/models/schema/conversationPreset.js
Normal file
62
api/models/schema/conversationPreset.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
module.exports = {
|
||||
// endpoint: [azureOpenAI, openAI, bingAI, chatGPTBrowser]
|
||||
endpoint: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: true
|
||||
},
|
||||
// for azureOpenAI, openAI, chatGPTBrowser only
|
||||
model: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
// for azureOpenAI, openAI only
|
||||
chatGptLabel: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
promptPrefix: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
temperature: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: false
|
||||
},
|
||||
top_p: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: false
|
||||
},
|
||||
presence_penalty: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: false
|
||||
},
|
||||
frequency_penalty: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: false
|
||||
},
|
||||
// for bingai only
|
||||
jailbreak: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
context: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
systemMessage: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
toneStyle: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
};
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
const mongoose = require('mongoose');
|
||||
const mongoMeili = require('../plugins/mongoMeili');
|
||||
const conversationPreset = require('./conversationPreset');
|
||||
const convoSchema = mongoose.Schema(
|
||||
{
|
||||
conversationId: {
|
||||
|
|
@ -9,15 +10,18 @@ const convoSchema = mongoose.Schema(
|
|||
index: true,
|
||||
meiliIndex: true
|
||||
},
|
||||
parentMessageId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'New Chat',
|
||||
meiliIndex: true
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }],
|
||||
...conversationPreset,
|
||||
// for bingAI only
|
||||
jailbreakConversationId: {
|
||||
type: String,
|
||||
default: null
|
||||
|
|
@ -27,32 +31,13 @@ const convoSchema = mongoose.Schema(
|
|||
default: null
|
||||
},
|
||||
clientId: {
|
||||
type: String
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
invocationId: {
|
||||
type: String
|
||||
},
|
||||
toneStyle: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
chatGptLabel: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
promptPrefix: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: String
|
||||
},
|
||||
suggestions: [{ type: String }],
|
||||
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }]
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
|
|
|||
27
api/models/schema/presetSchema.js
Normal file
27
api/models/schema/presetSchema.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
const mongoose = require('mongoose');
|
||||
const conversationPreset = require('./conversationPreset');
|
||||
const presetSchema = mongoose.Schema(
|
||||
{
|
||||
presetId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'New Chat',
|
||||
meiliIndex: true
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
...conversationPreset
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const Preset = mongoose.models.Preset || mongoose.model('Preset', presetSchema);
|
||||
|
||||
module.exports = Preset;
|
||||
5
api/package-lock.json
generated
5
api/package-lock.json
generated
|
|
@ -1,14 +1,15 @@
|
|||
{
|
||||
"name": "chatgpt-clone",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "chatgpt-clone",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@dqbd/tiktoken": "^1.0.2",
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@waylaidwanderer/chatgpt-api": "^1.33.2",
|
||||
"axios": "^1.3.4",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chatgpt-clone",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"description": "",
|
||||
"main": "server/index.js",
|
||||
"scripts": {
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/danny-avila/chatgpt-clone#readme",
|
||||
"dependencies": {
|
||||
"@dqbd/tiktoken": "^1.0.2",
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@waylaidwanderer/chatgpt-api": "^1.33.2",
|
||||
"axios": "^1.3.4",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ const errorController = require('./controllers/errorController');
|
|||
|
||||
const port = process.env.PORT || 3080;
|
||||
const host = process.env.HOST || 'localhost';
|
||||
const userSystemEnabled = process.env.ENABLE_USER_SYSTEM || false;
|
||||
const projectPath = path.join(__dirname, '..', '..', 'client');
|
||||
|
||||
(async () => {
|
||||
|
|
@ -34,6 +33,8 @@ const projectPath = path.join(__dirname, '..', '..', 'client');
|
|||
})
|
||||
);
|
||||
|
||||
// ROUTES
|
||||
|
||||
/* chore: potential redirect error here, can only comment out this block;
|
||||
comment back in if using auth routes i guess */
|
||||
// app.get('/', routes.authenticatedOrRedirect, function (req, res) {
|
||||
|
|
@ -41,33 +42,21 @@ const projectPath = path.join(__dirname, '..', '..', 'client');
|
|||
// res.sendFile(path.join(projectPath, 'public', 'index.html'));
|
||||
// });
|
||||
|
||||
app.get('/api/me', function (req, res) {
|
||||
if (userSystemEnabled) {
|
||||
const user = req?.session?.user;
|
||||
|
||||
if (user) res.send(JSON.stringify({ username: user?.username, display: user?.display }));
|
||||
else res.send(JSON.stringify(null));
|
||||
} else {
|
||||
res.send(JSON.stringify({ username: 'anonymous_user', display: 'Anonymous User' }));
|
||||
}
|
||||
});
|
||||
|
||||
// api endpoint
|
||||
app.use('/api/search', routes.authenticatedOr401, routes.search);
|
||||
app.use('/api/ask', routes.authenticatedOr401, routes.ask);
|
||||
app.use('/api/messages', routes.authenticatedOr401, routes.messages);
|
||||
app.use('/api/convos', routes.authenticatedOr401, routes.convos);
|
||||
app.use('/api/customGpts', routes.authenticatedOr401, routes.customGpts);
|
||||
app.use('/api/presets', routes.authenticatedOr401, routes.presets);
|
||||
app.use('/api/prompts', routes.authenticatedOr401, routes.prompts);
|
||||
app.use('/api/tokenizer', routes.authenticatedOr401, routes.tokenizer);
|
||||
app.use('/api/endpoints', routes.authenticatedOr401, routes.endpoints);
|
||||
|
||||
// user system
|
||||
app.use('/auth', routes.auth);
|
||||
app.use('/api/me', routes.me);
|
||||
|
||||
app.get('/api/models', function (req, res) {
|
||||
const hasOpenAI = !!process.env.OPENAI_KEY;
|
||||
const hasChatGpt = !!process.env.CHATGPT_TOKEN;
|
||||
const hasBing = !!process.env.BING_TOKEN;
|
||||
|
||||
res.send(JSON.stringify({ hasOpenAI, hasChatGpt, hasBing }));
|
||||
});
|
||||
|
||||
// static files
|
||||
app.get('/*', routes.authenticatedOrRedirect, function (req, res) {
|
||||
res.sendFile(path.join(projectPath, 'dist', 'index.html'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,148 +0,0 @@
|
|||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
const askBing = require('./askBing');
|
||||
const askSydney = require('./askSydney');
|
||||
const { titleConvo, askClient, browserClient, customClient } = require('../../app/');
|
||||
const { saveMessage, getConvoTitle, saveConvo, updateConvo } = require('../../models');
|
||||
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
|
||||
|
||||
router.use('/bing', askBing);
|
||||
router.use('/sydney', askSydney);
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const {
|
||||
model,
|
||||
text,
|
||||
overrideParentMessageId = null,
|
||||
parentMessageId,
|
||||
conversationId: oldConversationId,
|
||||
...convo
|
||||
} = req.body;
|
||||
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
|
||||
|
||||
const conversationId = oldConversationId || crypto.randomUUID();
|
||||
const userMessageId = crypto.randomUUID();
|
||||
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
const userMessage = {
|
||||
messageId: userMessageId,
|
||||
sender: 'User',
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
isCreatedByUser: true
|
||||
};
|
||||
console.log('ask log', {
|
||||
model,
|
||||
...userMessage,
|
||||
...convo
|
||||
});
|
||||
|
||||
if (!overrideParentMessageId) {
|
||||
await saveMessage(userMessage);
|
||||
await saveConvo(req?.session?.user?.username, { ...userMessage, model, ...convo });
|
||||
}
|
||||
|
||||
return await ask({ userMessage, model, convo, preSendRequest: true, overrideParentMessageId, req, res });
|
||||
});
|
||||
|
||||
const ask = async ({
|
||||
userMessage,
|
||||
overrideParentMessageId = null,
|
||||
model,
|
||||
convo,
|
||||
preSendRequest = true,
|
||||
req,
|
||||
res
|
||||
}) => {
|
||||
const {
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
messageId: userMessageId
|
||||
} = userMessage;
|
||||
|
||||
const client = model === 'chatgpt' ? askClient : model === 'chatgptCustom' ? customClient : browserClient;
|
||||
|
||||
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'
|
||||
});
|
||||
|
||||
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
|
||||
|
||||
try {
|
||||
const progressCallback = createOnProgress();
|
||||
const abortController = new AbortController();
|
||||
res.on('close', () => abortController.abort());
|
||||
let gptResponse = await client({
|
||||
text,
|
||||
onProgress: progressCallback.call(null, model, { res, text }),
|
||||
convo: { parentMessageId: userParentMessageId, conversationId, ...convo },
|
||||
...convo,
|
||||
abortController
|
||||
});
|
||||
|
||||
gptResponse.text = gptResponse.response;
|
||||
console.log('CLIENT RESPONSE', gptResponse);
|
||||
|
||||
if (!gptResponse.parentMessageId) {
|
||||
gptResponse.parentMessageId = overrideParentMessageId || userMessageId;
|
||||
delete gptResponse.response;
|
||||
}
|
||||
|
||||
gptResponse.sender = model === 'chatgptCustom' ? convo.chatGptLabel : model;
|
||||
gptResponse.model = model;
|
||||
gptResponse.text = await handleText(gptResponse);
|
||||
if (convo.chatGptLabel?.length > 0 && model === 'chatgptCustom') {
|
||||
gptResponse.chatGptLabel = convo.chatGptLabel;
|
||||
}
|
||||
|
||||
if (convo.promptPrefix?.length > 0 && model === 'chatgptCustom') {
|
||||
gptResponse.promptPrefix = convo.promptPrefix;
|
||||
}
|
||||
|
||||
gptResponse.parentMessageId = overrideParentMessageId || userMessageId;
|
||||
|
||||
if (model === 'chatgptBrowser' && userParentMessageId.startsWith('000')) {
|
||||
await saveMessage({ ...userMessage, conversationId: gptResponse.conversationId });
|
||||
}
|
||||
|
||||
await saveMessage(gptResponse);
|
||||
await updateConvo(req?.session?.user?.username, {
|
||||
...gptResponse,
|
||||
oldConvoId: model === 'chatgptBrowser' && conversationId
|
||||
});
|
||||
sendMessage(res, {
|
||||
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
||||
final: true,
|
||||
requestMessage: userMessage,
|
||||
responseMessage: gptResponse
|
||||
});
|
||||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({ model, text, response: gptResponse });
|
||||
await updateConvo(req?.session?.user?.username, {
|
||||
conversationId: model === 'chatgptBrowser' ? gptResponse.conversationId : conversationId,
|
||||
title
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = {
|
||||
messageId: crypto.randomUUID(),
|
||||
sender: model,
|
||||
conversationId,
|
||||
parentMessageId: overrideParentMessageId || userMessageId,
|
||||
error: true,
|
||||
text: error.message
|
||||
};
|
||||
await saveMessage(errorMessage);
|
||||
handleError(res, errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
226
api/server/routes/ask/askBingAI.js
Normal file
226
api/server/routes/ask/askBingAI.js
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
const { titleConvo, askBing } = require('../../../app');
|
||||
const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models');
|
||||
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const {
|
||||
endpoint,
|
||||
text,
|
||||
messageId,
|
||||
overrideParentMessageId = null,
|
||||
parentMessageId,
|
||||
conversationId: oldConversationId
|
||||
} = req.body;
|
||||
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
|
||||
if (endpoint !== 'bingAI') return handleError(res, { text: 'Illegal request' });
|
||||
|
||||
// build user message
|
||||
const conversationId = oldConversationId || crypto.randomUUID();
|
||||
const isNewConversation = !oldConversationId;
|
||||
const userMessageId = messageId;
|
||||
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
let userMessage = {
|
||||
messageId: userMessageId,
|
||||
sender: 'User',
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
isCreatedByUser: true
|
||||
};
|
||||
|
||||
// build endpoint option
|
||||
let endpointOption = {};
|
||||
if (req.body?.jailbreak)
|
||||
endpointOption = {
|
||||
jailbreak: req.body?.jailbreak || false,
|
||||
jailbreakConversationId: req.body?.jailbreakConversationId || null,
|
||||
systemMessage: req.body?.systemMessage || null,
|
||||
context: req.body?.context || null,
|
||||
toneStyle: req.body?.toneStyle || 'fast'
|
||||
};
|
||||
else
|
||||
endpointOption = {
|
||||
jailbreak: req.body?.jailbreak || false,
|
||||
systemMessage: req.body?.systemMessage || null,
|
||||
context: req.body?.context || null,
|
||||
conversationSignature: req.body?.conversationSignature || null,
|
||||
clientId: req.body?.clientId || null,
|
||||
invocationId: req.body?.invocationId || null,
|
||||
toneStyle: req.body?.toneStyle || 'fast'
|
||||
};
|
||||
|
||||
console.log('ask log', {
|
||||
userMessage,
|
||||
endpointOption,
|
||||
conversationId
|
||||
});
|
||||
|
||||
if (!overrideParentMessageId) {
|
||||
await saveMessage(userMessage);
|
||||
await saveConvo(req?.session?.user?.username, {
|
||||
...userMessage,
|
||||
...endpointOption,
|
||||
conversationId,
|
||||
endpoint
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
return await ask({
|
||||
isNewConversation,
|
||||
userMessage,
|
||||
endpointOption,
|
||||
conversationId,
|
||||
preSendRequest: true,
|
||||
overrideParentMessageId,
|
||||
req,
|
||||
res
|
||||
});
|
||||
});
|
||||
|
||||
const ask = async ({
|
||||
isNewConversation,
|
||||
userMessage,
|
||||
endpointOption,
|
||||
conversationId,
|
||||
preSendRequest = true,
|
||||
overrideParentMessageId = null,
|
||||
req,
|
||||
res
|
||||
}) => {
|
||||
let { text, parentMessageId: userParentMessageId, messageId: userMessageId } = userMessage;
|
||||
|
||||
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'
|
||||
});
|
||||
|
||||
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
|
||||
|
||||
try {
|
||||
const progressCallback = createOnProgress();
|
||||
const abortController = new AbortController();
|
||||
res.on('close', () => abortController.abort());
|
||||
let response = await askBing({
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
...endpointOption,
|
||||
onProgress: progressCallback.call(null, {
|
||||
res,
|
||||
text,
|
||||
parentMessageId: overrideParentMessageId || userMessageId
|
||||
}),
|
||||
abortController
|
||||
});
|
||||
|
||||
console.log('BING RESPONSE', response);
|
||||
|
||||
// STEP1 generate response message
|
||||
response.text = response.response || response.details.spokenText || '**Bing refused to answer.**';
|
||||
|
||||
let responseMessage = {
|
||||
text: await handleText(response, true),
|
||||
suggestions:
|
||||
response.details.suggestedResponses && response.details.suggestedResponses.map(s => s.text),
|
||||
jailbreak: endpointOption?.jailbreak
|
||||
};
|
||||
// // response.text = await handleText(response, true);
|
||||
// response.suggestions =
|
||||
// response.details.suggestedResponses && response.details.suggestedResponses.map(s => s.text);
|
||||
|
||||
if (endpointOption?.jailbreak) {
|
||||
responseMessage.conversationId = response.jailbreakConversationId;
|
||||
responseMessage.messageId = response.messageId || response.details.messageId;
|
||||
responseMessage.parentMessageId = overrideParentMessageId || response.parentMessageId || userMessageId;
|
||||
responseMessage.sender = 'Sydney';
|
||||
} else {
|
||||
responseMessage.conversationId = response.conversationId;
|
||||
responseMessage.messageId = response.messageId || response.details.messageId;
|
||||
responseMessage.parentMessageId =
|
||||
overrideParentMessageId || response.parentMessageId || response.details.requestId || userMessageId;
|
||||
responseMessage.sender = 'BingAI';
|
||||
}
|
||||
|
||||
await saveMessage(responseMessage);
|
||||
|
||||
// STEP2 update the convosation.
|
||||
|
||||
// First update conversationId if needed
|
||||
// Note!
|
||||
// Bing API will not use our conversationId at the first time,
|
||||
// so change the placeholder conversationId to the real one.
|
||||
// Attition: the api will also create new conversationId while using invalid userMessage.parentMessageId,
|
||||
// but in this situation, don't change the conversationId, but create new convo.
|
||||
|
||||
let conversationUpdate = { conversationId, endpoint: 'bingAI' };
|
||||
if (conversationId != responseMessage.conversationId && isNewConversation)
|
||||
conversationUpdate = {
|
||||
...conversationUpdate,
|
||||
conversationId: conversationId,
|
||||
newConversationId: responseMessage.conversationId || conversationId
|
||||
};
|
||||
conversationId = responseMessage.conversationId || conversationId;
|
||||
|
||||
if (endpointOption?.jailbreak) {
|
||||
conversationUpdate.jailbreak = true;
|
||||
conversationUpdate.jailbreakConversationId = response.jailbreakConversationId;
|
||||
} else {
|
||||
conversationUpdate.jailbreak = false;
|
||||
conversationUpdate.conversationSignature = response.conversationSignature;
|
||||
conversationUpdate.clientId = response.clientId;
|
||||
conversationUpdate.invocationId = response.invocationId;
|
||||
}
|
||||
|
||||
await saveConvo(req?.session?.user?.username, conversationUpdate);
|
||||
|
||||
// STEP3 update the user message
|
||||
userMessage.conversationId = conversationId;
|
||||
userMessage.messageId = responseMessage.parentMessageId;
|
||||
|
||||
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
|
||||
if (!overrideParentMessageId) {
|
||||
const oldUserMessageId = userMessageId;
|
||||
await saveMessage({ ...userMessage, messageId: oldUserMessageId, newMessageId: userMessage.messageId });
|
||||
}
|
||||
userMessageId = userMessage.messageId;
|
||||
|
||||
sendMessage(res, {
|
||||
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
||||
final: true,
|
||||
conversation: await getConvo(req?.session?.user?.username, conversationId),
|
||||
requestMessage: userMessage,
|
||||
responseMessage: responseMessage
|
||||
});
|
||||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({ endpoint: endpointOption?.endpoint, text, response: responseMessage });
|
||||
|
||||
await saveConvo(req?.session?.user?.username, {
|
||||
conversationId: conversationId,
|
||||
title
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const errorMessage = {
|
||||
messageId: crypto.randomUUID(),
|
||||
sender: endpointOption?.jailbreak ? 'Sydney' : 'BingAI',
|
||||
conversationId,
|
||||
parentMessageId: overrideParentMessageId || userMessageId,
|
||||
error: true,
|
||||
text: error.message
|
||||
};
|
||||
await saveMessage(errorMessage);
|
||||
handleError(res, errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
177
api/server/routes/ask/askChatGPTBrowser.js
Normal file
177
api/server/routes/ask/askChatGPTBrowser.js
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
const { getChatGPTBrowserModels } = require('../endpoints');
|
||||
const { titleConvo, browserClient } = require('../../../app/');
|
||||
const { saveMessage, getConvoTitle, saveConvo, updateConvo, getConvo } = require('../../../models');
|
||||
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const {
|
||||
endpoint,
|
||||
text,
|
||||
overrideParentMessageId = null,
|
||||
parentMessageId,
|
||||
conversationId: oldConversationId
|
||||
} = req.body;
|
||||
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
|
||||
if (endpoint !== 'chatGPTBrowser') return handleError(res, { text: 'Illegal request' });
|
||||
|
||||
// build user message
|
||||
const conversationId = oldConversationId || crypto.randomUUID();
|
||||
const isNewConversation = !oldConversationId;
|
||||
const userMessageId = crypto.randomUUID();
|
||||
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
const userMessage = {
|
||||
messageId: userMessageId,
|
||||
sender: 'User',
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
isCreatedByUser: true
|
||||
};
|
||||
|
||||
// build endpoint option
|
||||
const endpointOption = {
|
||||
model: req.body?.model || 'text-davinci-002-render-sha'
|
||||
};
|
||||
|
||||
const availableModels = getChatGPTBrowserModels();
|
||||
if (availableModels.find(model => model === endpointOption.model) === undefined)
|
||||
return handleError(res, { text: 'Illegal request: model' });
|
||||
|
||||
console.log('ask log', {
|
||||
userMessage,
|
||||
endpointOption,
|
||||
conversationId
|
||||
});
|
||||
|
||||
if (!overrideParentMessageId) {
|
||||
await saveMessage(userMessage);
|
||||
await saveConvo(req?.session?.user?.username, {
|
||||
...userMessage,
|
||||
...endpointOption,
|
||||
conversationId,
|
||||
endpoint
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
return await ask({
|
||||
isNewConversation,
|
||||
userMessage,
|
||||
endpointOption,
|
||||
conversationId,
|
||||
preSendRequest: true,
|
||||
overrideParentMessageId,
|
||||
req,
|
||||
res
|
||||
});
|
||||
});
|
||||
|
||||
const ask = async ({
|
||||
isNewConversation,
|
||||
userMessage,
|
||||
endpointOption,
|
||||
conversationId,
|
||||
preSendRequest = true,
|
||||
overrideParentMessageId = null,
|
||||
req,
|
||||
res
|
||||
}) => {
|
||||
let { text, parentMessageId: userParentMessageId, messageId: userMessageId } = userMessage;
|
||||
|
||||
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'
|
||||
});
|
||||
|
||||
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
|
||||
|
||||
try {
|
||||
const progressCallback = createOnProgress();
|
||||
const abortController = new AbortController();
|
||||
res.on('close', () => abortController.abort());
|
||||
let response = await browserClient({
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
...endpointOption,
|
||||
onProgress: progressCallback.call(null, { res, text }),
|
||||
abortController
|
||||
});
|
||||
|
||||
console.log('CLIENT RESPONSE', response);
|
||||
|
||||
// STEP1 generate response message
|
||||
response.text = response.response || '**ChatGPT refused to answer.**';
|
||||
|
||||
let responseMessage = {
|
||||
conversationId: response.conversationId,
|
||||
messageId: response.messageId,
|
||||
parentMessageId: overrideParentMessageId || response.parentMessageId || userMessageId,
|
||||
text: await handleText(response),
|
||||
sender: endpointOption?.chatGptLabel || 'ChatGPT'
|
||||
};
|
||||
|
||||
await saveMessage(responseMessage);
|
||||
|
||||
// STEP2 update the conversation
|
||||
|
||||
// First update conversationId if needed
|
||||
let conversationUpdate = { conversationId, endpoint: 'chatGPTBrowser' };
|
||||
if (conversationId != responseMessage.conversationId && isNewConversation)
|
||||
conversationUpdate = {
|
||||
...conversationUpdate,
|
||||
conversationId: conversationId,
|
||||
newConversationId: responseMessage.conversationId || conversationId
|
||||
};
|
||||
conversationId = responseMessage.conversationId || conversationId;
|
||||
|
||||
await saveConvo(req?.session?.user?.username, conversationUpdate);
|
||||
|
||||
// STEP3 update the user message
|
||||
userMessage.conversationId = conversationId;
|
||||
userMessage.messageId = responseMessage.parentMessageId;
|
||||
|
||||
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
|
||||
if (!overrideParentMessageId) {
|
||||
const oldUserMessageId = userMessageId;
|
||||
await saveMessage({ ...userMessage, messageId: oldUserMessageId, newMessageId: userMessage.messageId });
|
||||
}
|
||||
userMessageId = userMessage.messageId;
|
||||
|
||||
sendMessage(res, {
|
||||
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
||||
final: true,
|
||||
conversation: await getConvo(req?.session?.user?.username, conversationId),
|
||||
requestMessage: userMessage,
|
||||
responseMessage: responseMessage
|
||||
});
|
||||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({ endpoint: endpointOption?.endpoint, text, response: responseMessage });
|
||||
await updateConvo(req?.session?.user?.username, {
|
||||
conversationId: conversationId,
|
||||
title
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = {
|
||||
messageId: crypto.randomUUID(),
|
||||
sender: 'ChatGPT',
|
||||
conversationId,
|
||||
parentMessageId: overrideParentMessageId || userMessageId,
|
||||
error: true,
|
||||
text: error.message
|
||||
};
|
||||
await saveMessage(errorMessage);
|
||||
handleError(res, errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
179
api/server/routes/ask/askOpenAI.js
Normal file
179
api/server/routes/ask/askOpenAI.js
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
const { getOpenAIModels } = require('../endpoints');
|
||||
const { titleConvo, askClient } = require('../../../app/');
|
||||
const { saveMessage, getConvoTitle, saveConvo, updateConvo, getConvo } = require('../../../models');
|
||||
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const {
|
||||
endpoint,
|
||||
text,
|
||||
overrideParentMessageId = null,
|
||||
parentMessageId,
|
||||
conversationId: oldConversationId
|
||||
} = req.body;
|
||||
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
|
||||
if (endpoint !== 'openAI') return handleError(res, { text: 'Illegal request' });
|
||||
|
||||
// build user message
|
||||
const conversationId = oldConversationId || crypto.randomUUID();
|
||||
const userMessageId = crypto.randomUUID();
|
||||
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
const userMessage = {
|
||||
messageId: userMessageId,
|
||||
sender: 'User',
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
isCreatedByUser: true
|
||||
};
|
||||
|
||||
// build endpoint option
|
||||
const endpointOption = {
|
||||
model: req.body?.model || 'gpt-3.5-turbo',
|
||||
chatGptLabel: req.body?.chatGptLabel || null,
|
||||
promptPrefix: req.body?.promptPrefix || null,
|
||||
temperature: req.body?.temperature || 1,
|
||||
top_p: req.body?.top_p || 1,
|
||||
presence_penalty: req.body?.presence_penalty || 0,
|
||||
frequency_penalty: req.body?.frequency_penalty || 0
|
||||
};
|
||||
|
||||
const availableModels = getOpenAIModels();
|
||||
if (availableModels.find(model => model === endpointOption.model) === undefined)
|
||||
return handleError(res, { text: 'Illegal request: model' });
|
||||
|
||||
console.log('ask log', {
|
||||
userMessage,
|
||||
endpointOption,
|
||||
conversationId
|
||||
});
|
||||
|
||||
if (!overrideParentMessageId) {
|
||||
await saveMessage(userMessage);
|
||||
await saveConvo(req?.session?.user?.username, {
|
||||
...userMessage,
|
||||
...endpointOption,
|
||||
conversationId,
|
||||
endpoint
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
return await ask({
|
||||
userMessage,
|
||||
endpointOption,
|
||||
conversationId,
|
||||
preSendRequest: true,
|
||||
overrideParentMessageId,
|
||||
req,
|
||||
res
|
||||
});
|
||||
});
|
||||
|
||||
const ask = async ({
|
||||
userMessage,
|
||||
endpointOption,
|
||||
conversationId,
|
||||
preSendRequest = true,
|
||||
overrideParentMessageId = null,
|
||||
req,
|
||||
res
|
||||
}) => {
|
||||
let { text, parentMessageId: userParentMessageId, messageId: userMessageId } = userMessage;
|
||||
|
||||
const client = askClient;
|
||||
|
||||
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'
|
||||
});
|
||||
|
||||
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
|
||||
|
||||
try {
|
||||
const progressCallback = createOnProgress();
|
||||
const abortController = new AbortController();
|
||||
res.on('close', () => abortController.abort());
|
||||
let response = await client({
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
...endpointOption,
|
||||
onProgress: progressCallback.call(null, {
|
||||
res,
|
||||
text,
|
||||
parentMessageId: overrideParentMessageId || userMessageId
|
||||
}),
|
||||
abortController
|
||||
});
|
||||
|
||||
console.log('CLIENT RESPONSE', response);
|
||||
|
||||
// STEP1 generate response message
|
||||
response.text = response.response || '**ChatGPT refused to answer.**';
|
||||
|
||||
let responseMessage = {
|
||||
conversationId: response.conversationId,
|
||||
messageId: response.messageId,
|
||||
parentMessageId: overrideParentMessageId || userMessageId,
|
||||
text: await handleText(response),
|
||||
sender: endpointOption?.chatGptLabel || 'ChatGPT'
|
||||
};
|
||||
|
||||
await saveMessage(responseMessage);
|
||||
|
||||
// STEP2 update the conversation
|
||||
conversationId = responseMessage.conversationId || conversationId;
|
||||
// it seems openAI will not change the conversationId.
|
||||
// let conversationUpdate = { conversationId, endpoint: 'openAI' };
|
||||
// await saveConvo(req?.session?.user?.username, conversationUpdate);
|
||||
|
||||
// STEP3 update the user message
|
||||
userMessage.conversationId = conversationId;
|
||||
userMessage.messageId = responseMessage.parentMessageId;
|
||||
|
||||
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
|
||||
if (!overrideParentMessageId) {
|
||||
const oldUserMessageId = userMessageId;
|
||||
await saveMessage({ ...userMessage, messageId: oldUserMessageId, newMessageId: userMessage.messageId });
|
||||
}
|
||||
userMessageId = userMessage.messageId;
|
||||
|
||||
sendMessage(res, {
|
||||
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
||||
final: true,
|
||||
conversation: await getConvo(req?.session?.user?.username, conversationId),
|
||||
requestMessage: userMessage,
|
||||
responseMessage: responseMessage
|
||||
});
|
||||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({ endpoint: endpointOption?.endpoint, text, response: responseMessage });
|
||||
await updateConvo(req?.session?.user?.username, {
|
||||
conversationId: conversationId,
|
||||
title
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const errorMessage = {
|
||||
messageId: crypto.randomUUID(),
|
||||
sender: endpointOption?.chatGptLabel || 'ChatGPT',
|
||||
conversationId,
|
||||
parentMessageId: overrideParentMessageId || userMessageId,
|
||||
error: true,
|
||||
text: error.message
|
||||
};
|
||||
await saveMessage(errorMessage);
|
||||
handleError(res, errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -3,7 +3,7 @@ const citationRegex = /\[\^\d+?\^]/g;
|
|||
const backtick = /(?<!`)[`](?!`)/g;
|
||||
// const singleBacktick = /(?<!`)[`](?!`)/;
|
||||
const cursorDefault = '<span className="result-streaming">█</span>';
|
||||
const { getCitations, citeText } = require('../../app/');
|
||||
const { getCitations, citeText } = require('../../../app');
|
||||
|
||||
const handleError = (res, message) => {
|
||||
res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`);
|
||||
|
|
@ -68,9 +68,8 @@ const createOnProgress = () => {
|
|||
i++;
|
||||
};
|
||||
|
||||
const onProgress = (model, opts) => {
|
||||
const bingModels = new Set(['bingai', 'sydney']);
|
||||
return _.partialRight(progressCallback, { ...opts, bing: bingModels.has(model) });
|
||||
const onProgress = opts => {
|
||||
return _.partialRight(progressCallback, opts);
|
||||
};
|
||||
|
||||
return onProgress;
|
||||
13
api/server/routes/ask/index.js
Normal file
13
api/server/routes/ask/index.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
// const askAzureOpenAI = require('./askAzureOpenAI';)
|
||||
const askOpenAI = require('./askOpenAI');
|
||||
const askBingAI = require('./askBingAI');
|
||||
const askChatGPTBrowser = require('./askChatGPTBrowser');
|
||||
|
||||
// router.use('/azureOpenAI', askAzureOpenAI);
|
||||
router.use('/openAI', askOpenAI);
|
||||
router.use('/bingAI', askBingAI);
|
||||
router.use('/chatGPTBrowser', askChatGPTBrowser);
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
const { titleConvo, askBing } = require('../../app/');
|
||||
const { saveBingMessage, getConvoTitle, saveConvo } = require('../../models');
|
||||
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const {
|
||||
model,
|
||||
text,
|
||||
overrideParentMessageId=null,
|
||||
parentMessageId,
|
||||
conversationId: oldConversationId,
|
||||
...convo
|
||||
} = req.body;
|
||||
if (text.length === 0) {
|
||||
return handleError(res, { text: 'Prompt empty or too short' });
|
||||
}
|
||||
|
||||
const conversationId = oldConversationId || crypto.randomUUID();
|
||||
const isNewConversation = !oldConversationId;
|
||||
|
||||
const userMessageId = convo.messageId;
|
||||
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
let userMessage = {
|
||||
messageId: userMessageId,
|
||||
sender: 'User',
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
isCreatedByUser: true
|
||||
};
|
||||
|
||||
console.log('ask log', {
|
||||
model,
|
||||
...convo,
|
||||
...userMessage
|
||||
});
|
||||
|
||||
if (!overrideParentMessageId) {
|
||||
await saveBingMessage(userMessage);
|
||||
await saveConvo(req?.session?.user?.username, { model, ...convo, ...userMessage });
|
||||
}
|
||||
|
||||
return await ask({
|
||||
isNewConversation,
|
||||
userMessage,
|
||||
model,
|
||||
convo,
|
||||
preSendRequest: true,
|
||||
overrideParentMessageId,
|
||||
req,
|
||||
res
|
||||
});
|
||||
});
|
||||
|
||||
const ask = async ({
|
||||
isNewConversation,
|
||||
overrideParentMessageId = null,
|
||||
userMessage,
|
||||
model,
|
||||
convo,
|
||||
preSendRequest = true,
|
||||
req,
|
||||
res
|
||||
}) => {
|
||||
let {
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
messageId: userMessageId
|
||||
} = userMessage;
|
||||
|
||||
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'
|
||||
});
|
||||
|
||||
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
|
||||
|
||||
try {
|
||||
const progressCallback = createOnProgress();
|
||||
|
||||
const abortController = new AbortController();
|
||||
res.on('close', () => {
|
||||
console.log('The client has disconnected.');
|
||||
// 执行其他操作
|
||||
abortController.abort();
|
||||
})
|
||||
|
||||
let response = await askBing({
|
||||
text,
|
||||
onProgress: progressCallback.call(null, model, {
|
||||
res,
|
||||
text,
|
||||
parentMessageId: overrideParentMessageId || userMessageId
|
||||
}),
|
||||
convo: {
|
||||
...convo,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId
|
||||
},
|
||||
abortController
|
||||
});
|
||||
|
||||
console.log('BING RESPONSE', response);
|
||||
// console.dir(response, { depth: null });
|
||||
|
||||
userMessage.conversationSignature =
|
||||
convo.conversationSignature || response.conversationSignature;
|
||||
userMessage.conversationId = response.conversationId || conversationId;
|
||||
userMessage.invocationId = response.invocationId;
|
||||
userMessage.messageId = response.details.requestId || userMessageId;
|
||||
if (!overrideParentMessageId)
|
||||
await saveBingMessage({ oldMessageId: userMessageId, ...userMessage });
|
||||
|
||||
// Bing API will not use our conversationId at the first time,
|
||||
// so change the placeholder conversationId to the real one.
|
||||
// Attition: the api will also create new conversationId while using invalid userMessage.parentMessageId,
|
||||
// but in this situation, don't change the conversationId, but create new convo.
|
||||
if (conversationId != userMessage.conversationId && isNewConversation)
|
||||
await saveConvo(
|
||||
req?.session?.user?.username,
|
||||
{
|
||||
conversationId: conversationId,
|
||||
newConversationId: userMessage.conversationId
|
||||
}
|
||||
);
|
||||
conversationId = userMessage.conversationId;
|
||||
|
||||
response.text = response.response || response.details.spokenText || '**Bing refused to answer.**';
|
||||
// delete response.response;
|
||||
// response.id = response.details.messageId;
|
||||
response.suggestions =
|
||||
response.details.suggestedResponses &&
|
||||
response.details.suggestedResponses.map((s) => s.text);
|
||||
response.sender = model;
|
||||
// response.final = true;
|
||||
|
||||
response.messageId = response.details.messageId;
|
||||
// override the parentMessageId, for the regeneration.
|
||||
response.parentMessageId =
|
||||
overrideParentMessageId || response.details.requestId || userMessageId;
|
||||
|
||||
response.text = await handleText(response, true);
|
||||
await saveBingMessage(response);
|
||||
await saveConvo(req?.session?.user?.username, { model, chatGptLabel: null, promptPrefix: null, ...convo, ...response });
|
||||
|
||||
sendMessage(res, {
|
||||
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
||||
final: true,
|
||||
requestMessage: userMessage,
|
||||
responseMessage: response
|
||||
});
|
||||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({ model, text, response });
|
||||
|
||||
await saveConvo(
|
||||
req?.session?.user?.username,
|
||||
{
|
||||
...convo,
|
||||
...response,
|
||||
conversationId,
|
||||
title
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// await deleteMessages({ messageId: userMessageId });
|
||||
const errorMessage = {
|
||||
messageId: crypto.randomUUID(),
|
||||
sender: model,
|
||||
conversationId,
|
||||
parentMessageId: overrideParentMessageId || userMessageId,
|
||||
error: true,
|
||||
text: error.message
|
||||
};
|
||||
await saveBingMessage(errorMessage);
|
||||
handleError(res, errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
const { titleConvo, askSydney } = require('../../app/');
|
||||
const { saveBingMessage, saveConvo, updateConvo, getConvoTitle } = require('../../models');
|
||||
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const {
|
||||
model,
|
||||
text,
|
||||
overrideParentMessageId=null,
|
||||
parentMessageId,
|
||||
conversationId: oldConversationId,
|
||||
...convo
|
||||
} = req.body;
|
||||
if (text.length === 0) {
|
||||
return handleError(res, { text: 'Prompt empty or too short' });
|
||||
}
|
||||
|
||||
const conversationId = oldConversationId || crypto.randomUUID();
|
||||
const isNewConversation = !oldConversationId;
|
||||
|
||||
const userMessageId = convo.messageId;
|
||||
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
let userMessage = {
|
||||
messageId: userMessageId,
|
||||
sender: 'User',
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
isCreatedByUser: true
|
||||
};
|
||||
|
||||
console.log('ask log', {
|
||||
model,
|
||||
...convo,
|
||||
...userMessage
|
||||
});
|
||||
|
||||
if (!overrideParentMessageId) {
|
||||
await saveBingMessage(userMessage);
|
||||
await saveConvo(req?.session?.user?.username, { model, ...convo, ...userMessage });
|
||||
}
|
||||
|
||||
return await ask({
|
||||
isNewConversation,
|
||||
userMessage,
|
||||
model,
|
||||
convo,
|
||||
preSendRequest: true,
|
||||
overrideParentMessageId,
|
||||
req,
|
||||
res
|
||||
});
|
||||
});
|
||||
|
||||
const ask = async ({
|
||||
isNewConversation,
|
||||
overrideParentMessageId = null,
|
||||
userMessage,
|
||||
model,
|
||||
convo,
|
||||
preSendRequest = true,
|
||||
req,
|
||||
res
|
||||
}) => {
|
||||
let {
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
messageId: userMessageId
|
||||
} = userMessage;
|
||||
|
||||
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'
|
||||
});
|
||||
|
||||
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
|
||||
|
||||
try {
|
||||
const progressCallback = createOnProgress();
|
||||
|
||||
const abortController = new AbortController();
|
||||
res.on('close', () => {
|
||||
console.log('The client has disconnected.');
|
||||
// 执行其他操作
|
||||
abortController.abort();
|
||||
})
|
||||
|
||||
let response = await askSydney({
|
||||
text,
|
||||
onProgress: progressCallback.call(null, model, {
|
||||
res,
|
||||
text,
|
||||
parentMessageId: overrideParentMessageId || userMessageId
|
||||
}),
|
||||
convo: {
|
||||
...convo,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId
|
||||
},
|
||||
abortController
|
||||
});
|
||||
|
||||
console.log('SYDNEY RESPONSE', response);
|
||||
// console.dir(response, { depth: null });
|
||||
|
||||
userMessage.conversationSignature =
|
||||
convo.conversationSignature || response.conversationSignature;
|
||||
userMessage.conversationId = response.conversationId || conversationId;
|
||||
userMessage.invocationId = response.invocationId;
|
||||
// Unlike gpt and bing, Sydney will never accept our given userMessage.messageId, it will generate its own one.
|
||||
userMessage.messageId = response.parentMessageId || userMessageId;
|
||||
|
||||
// Save sydney response
|
||||
// response.id = response.messageId;
|
||||
response.invocationId = convo.invocationId ? convo.invocationId + 1 : 1;
|
||||
response.conversationId = conversationId ? conversationId : crypto.randomUUID();
|
||||
response.conversationSignature = convo.conversationSignature
|
||||
? convo.conversationSignature
|
||||
: crypto.randomUUID();
|
||||
response.text = response.response || response.details.spokenText || '**Bing refused to answer.**';
|
||||
// delete response.response;
|
||||
response.suggestions =
|
||||
response.details.suggestedResponses &&
|
||||
response.details.suggestedResponses.map((s) => s.text);
|
||||
response.sender = model;
|
||||
// response.final = true;
|
||||
|
||||
// override the parentMessageId, for the regeneration.
|
||||
response.parentMessageId =
|
||||
overrideParentMessageId || response.parentMessageId || userMessageId;
|
||||
|
||||
// Save user message
|
||||
userMessage.conversationId = response.conversationId || conversationId;
|
||||
if (!overrideParentMessageId)
|
||||
await saveBingMessage({ oldMessageId: userMessageId, ...userMessage });
|
||||
|
||||
// Bing API will not use our conversationId at the first time,
|
||||
// so change the placeholder conversationId to the real one.
|
||||
// Attition: the api will also create new conversationId while using invalid userMessage.parentMessageId,
|
||||
// but in this situation, don't change the conversationId, but create new convo.
|
||||
if (conversationId != userMessage.conversationId && isNewConversation)
|
||||
await updateConvo(
|
||||
req?.session?.user?.username,
|
||||
{
|
||||
conversationId: conversationId,
|
||||
newConversationId: userMessage.conversationId
|
||||
}
|
||||
);
|
||||
conversationId = userMessage.conversationId;
|
||||
|
||||
response.text = await handleText(response, true);
|
||||
// Save sydney response & convo, then send
|
||||
await saveBingMessage(response);
|
||||
await updateConvo(req?.session?.user?.username, { model, chatGptLabel: null, promptPrefix: null, ...convo, ...response });
|
||||
|
||||
sendMessage(res, {
|
||||
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
||||
final: true,
|
||||
requestMessage: userMessage,
|
||||
responseMessage: response
|
||||
});
|
||||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({ model, text, response });
|
||||
|
||||
await updateConvo(
|
||||
req?.session?.user?.username,
|
||||
{
|
||||
conversationId,
|
||||
title
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// await deleteMessages({ messageId: userMessageId });
|
||||
const errorMessage = {
|
||||
messageId: crypto.randomUUID(),
|
||||
sender: model,
|
||||
conversationId,
|
||||
parentMessageId: overrideParentMessageId || userMessageId,
|
||||
error: true,
|
||||
text: error.message
|
||||
};
|
||||
await saveBingMessage(errorMessage);
|
||||
handleError(res, errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -45,7 +45,7 @@ const authenticatedOrRedirect = (req, res, next) => {
|
|||
if (user) {
|
||||
next();
|
||||
} else {
|
||||
res.redirect('/auth/login').end();
|
||||
res.redirect('/auth/login');
|
||||
}
|
||||
} else next();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
getCustomGpts,
|
||||
updateCustomGpt,
|
||||
updateByLabel,
|
||||
deleteCustomGpts
|
||||
} = require('../../models');
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const models = (await getCustomGpts(req?.session?.user?.username)).map((model) => {
|
||||
model = model.toObject();
|
||||
model._id = model._id.toString();
|
||||
return model;
|
||||
});
|
||||
res.status(200).send(models);
|
||||
});
|
||||
|
||||
router.post('/delete', async (req, res) => {
|
||||
const { arg } = req.body;
|
||||
|
||||
try {
|
||||
await deleteCustomGpts(req?.session?.user?.username, arg);
|
||||
const models = (await getCustomGpts(req?.session?.user?.username)).map((model) => {
|
||||
model = model.toObject();
|
||||
model._id = model._id.toString();
|
||||
return model;
|
||||
});
|
||||
res.status(201).send(models);
|
||||
// res.status(201).send(dbResponse);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
// router.post('/create', async (req, res) => {
|
||||
// const payload = req.body.arg;
|
||||
|
||||
// try {
|
||||
// const dbResponse = await createCustomGpt(payload);
|
||||
// res.status(201).send(dbResponse);
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// res.status(500).send(error);
|
||||
// }
|
||||
// });
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const update = req.body.arg;
|
||||
|
||||
let setter = updateCustomGpt;
|
||||
|
||||
if (update.prevLabel) {
|
||||
setter = updateByLabel;
|
||||
}
|
||||
|
||||
try {
|
||||
const dbResponse = await setter(req?.session?.user?.username, update);
|
||||
res.status(201).send(dbResponse);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
27
api/server/routes/endpoints.js
Normal file
27
api/server/routes/endpoints.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const getOpenAIModels = () => {
|
||||
let models = ['gpt-4', 'text-davinci-003', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0301'];
|
||||
if (process.env.OPENAI_MODELS) models = String(process.env.OPENAI_MODELS).split(',');
|
||||
|
||||
return models;
|
||||
};
|
||||
|
||||
const getChatGPTBrowserModels = () => {
|
||||
let models = ['text-davinci-002-render-sha', 'text-davinci-002-render-paid', 'gpt-4'];
|
||||
if (process.env.CHATGPT_MODELS) models = String(process.env.CHATGPT_MODELS).split(',');
|
||||
|
||||
return models;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
res.send(JSON.stringify({ azureOpenAI, openAI, bingAI, chatGPTBrowser }));
|
||||
});
|
||||
|
||||
module.exports = { router, getOpenAIModels, getChatGPTBrowserModels };
|
||||
|
|
@ -1,9 +1,25 @@
|
|||
const ask = require('./ask');
|
||||
const messages = require('./messages');
|
||||
const convos = require('./convos');
|
||||
const customGpts = require('./customGpts');
|
||||
const prompts = require('./prompts');
|
||||
const presets = require('./presets');
|
||||
const prompts = require('./prompts');
|
||||
const search = require('./search');
|
||||
const tokenizer = require('./tokenizer');
|
||||
const me = require('./me');
|
||||
const { router: endpoints } = require('./endpoints');
|
||||
const { router: auth, authenticatedOr401, authenticatedOrRedirect } = require('./auth');
|
||||
|
||||
module.exports = { search, ask, messages, convos, customGpts, prompts, auth, authenticatedOr401, authenticatedOrRedirect };
|
||||
module.exports = {
|
||||
search,
|
||||
ask,
|
||||
messages,
|
||||
convos,
|
||||
presets,
|
||||
prompts,
|
||||
auth,
|
||||
tokenizer,
|
||||
me,
|
||||
endpoints,
|
||||
authenticatedOr401,
|
||||
authenticatedOrRedirect
|
||||
};
|
||||
|
|
|
|||
16
api/server/routes/me.js
Normal file
16
api/server/routes/me.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const userSystemEnabled = !!process.env.ENABLE_USER_SYSTEM || false;
|
||||
|
||||
router.get('/', function (req, res) {
|
||||
if (userSystemEnabled) {
|
||||
const user = req?.session?.user;
|
||||
|
||||
if (user) res.send(JSON.stringify({ username: user?.username, display: user?.display }));
|
||||
else res.send(JSON.stringify(null));
|
||||
} else {
|
||||
res.send(JSON.stringify({ username: 'anonymous_user', display: 'Anonymous User' }));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
53
api/server/routes/presets.js
Normal file
53
api/server/routes/presets.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getPresets, savePreset, deletePresets } = require('../../models');
|
||||
const crypto = require('crypto');
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const presets = (await getPresets(req?.session?.user?.username)).map((preset) => {
|
||||
return preset.toObject();
|
||||
});
|
||||
res.status(200).send(presets);
|
||||
});
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const update = req.body || {};
|
||||
|
||||
update.presetId = update?.presetId || crypto.randomUUID();
|
||||
|
||||
try {
|
||||
await savePreset(req?.session?.user?.username, update);
|
||||
|
||||
const presets = (await getPresets(req?.session?.user?.username)).map((preset) => {
|
||||
return preset.toObject();
|
||||
});
|
||||
res.status(201).send(presets);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/delete', async (req, res) => {
|
||||
let filter = {};
|
||||
const { presetId } = req.body.arg || {};
|
||||
|
||||
if (presetId) filter = { presetId };
|
||||
|
||||
console.log('delete preset filter', filter);
|
||||
|
||||
try {
|
||||
await deletePresets(req?.session?.user?.username, filter);
|
||||
|
||||
const presets = (await getPresets(req?.session?.user?.username)).map(preset => preset.toObject());
|
||||
|
||||
// console.log('delete preset response', presets);
|
||||
res.status(201).send(presets);
|
||||
// res.status(201).send(dbResponse);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
18
api/server/routes/tokenizer.js
Normal file
18
api/server/routes/tokenizer.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Tiktoken } = require('@dqbd/tiktoken/lite');
|
||||
const { load } = require('@dqbd/tiktoken/load');
|
||||
const registry = require('@dqbd/tiktoken/registry.json');
|
||||
const models = require('@dqbd/tiktoken/model_to_encoding.json');
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const { arg } = req.body;
|
||||
// console.log(typeof req.body === 'object' ? { ...req.body, ...req.query } : req.query);
|
||||
const model = await load(registry[models['gpt-3.5-turbo']]);
|
||||
const encoder = new Tiktoken(model.bpe_ranks, model.special_tokens, model.pat_str);
|
||||
const tokens = encoder.encode(arg.text);
|
||||
encoder.free();
|
||||
res.send({ count: tokens.length });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
6663
client/package-lock.json
generated
6663
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chatgpt-clone",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
@ -21,21 +21,30 @@
|
|||
},
|
||||
"homepage": "https://github.com/danny-avila/chatgpt-clone#readme",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.13",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.2",
|
||||
"@radix-ui/react-hover-card": "^1.0.5",
|
||||
"@radix-ui/react-label": "^2.0.0",
|
||||
"@radix-ui/react-slider": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.0.3",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/node": "^18.15.10",
|
||||
"@types/react": "^18.0.30",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@zattoo/use-double-click": "1.2.0",
|
||||
"axios": "^1.3.4",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"clsx": "^1.2.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"esbuild": "0.17.15",
|
||||
"export-from-json": "^1.7.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.113.0",
|
||||
"rc-input-number": "^7.4.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-lazy-load": "^4.0.1",
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ const router = createBrowserRouter([
|
|||
const App = () => {
|
||||
const [user, setUser] = useRecoilState(store.user);
|
||||
const setIsSearchEnabled = useSetRecoilState(store.isSearchEnabled);
|
||||
const setModelsFilter = useSetRecoilState(store.modelsFilter);
|
||||
const setEndpointsConfig = useSetRecoilState(store.endpointsConfig);
|
||||
const setPresets = useSetRecoilState(store.presets);
|
||||
|
||||
useEffect(() => {
|
||||
// fetch if seatch enabled
|
||||
|
|
@ -58,19 +59,27 @@ const App = () => {
|
|||
|
||||
// fetch models
|
||||
axios
|
||||
.get('/api/models', {
|
||||
.get('/api/endpoints', {
|
||||
timeout: 1000,
|
||||
withCredentials: true
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const filter = {
|
||||
chatgpt: data?.hasOpenAI,
|
||||
chatgptCustom: data?.hasOpenAI,
|
||||
bingai: data?.hasBing,
|
||||
sydney: data?.hasBing,
|
||||
chatgptBrowser: data?.hasChatGpt
|
||||
};
|
||||
setModelsFilter(filter);
|
||||
setEndpointsConfig(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
console.log('Not login!');
|
||||
window.location.href = '/auth/login';
|
||||
});
|
||||
|
||||
// fetch presets
|
||||
axios
|
||||
.get('/api/presets', {
|
||||
timeout: 1000,
|
||||
withCredentials: true
|
||||
})
|
||||
.then(({ data }) => {
|
||||
setPresets(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import RenameButton from './RenameButton';
|
||||
import DeleteButton from './DeleteButton';
|
||||
|
|
@ -10,9 +10,7 @@ import store from '~/store';
|
|||
|
||||
export default function Conversation({ conversation, retainView }) {
|
||||
const [currentConversation, setCurrentConversation] = useRecoilState(store.conversation);
|
||||
const setMessages = useSetRecoilState(store.messages);
|
||||
const setSubmission = useSetRecoilState(store.submission);
|
||||
const resetLatestMessage = useResetRecoilState(store.latestMessage);
|
||||
|
||||
const { refreshConversations } = store.useConversations();
|
||||
const { switchToConversation } = store.useConversation();
|
||||
|
|
@ -20,35 +18,12 @@ export default function Conversation({ conversation, retainView }) {
|
|||
const [renaming, setRenaming] = useState(false);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const {
|
||||
model,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
title,
|
||||
chatGptLabel = null,
|
||||
promptPrefix = null,
|
||||
jailbreakConversationId,
|
||||
conversationSignature,
|
||||
clientId,
|
||||
invocationId,
|
||||
toneStyle
|
||||
} = conversation;
|
||||
const { conversationId, title } = conversation;
|
||||
|
||||
const [titleInput, setTitleInput] = useState(title);
|
||||
|
||||
const rename = manualSWR(`/api/convos/update`, 'post');
|
||||
|
||||
const bingData = conversationSignature
|
||||
? {
|
||||
jailbreakConversationId: jailbreakConversationId,
|
||||
conversationSignature: conversationSignature,
|
||||
parentMessageId: parentMessageId || null,
|
||||
clientId: clientId,
|
||||
invocationId: invocationId,
|
||||
toneStyle: toneStyle
|
||||
}
|
||||
: null;
|
||||
|
||||
const clickHandler = async () => {
|
||||
if (currentConversation?.conversationId === conversationId) {
|
||||
return;
|
||||
|
|
@ -59,64 +34,6 @@ export default function Conversation({ conversation, retainView }) {
|
|||
|
||||
// set conversation to the new conversation
|
||||
switchToConversation(conversation);
|
||||
|
||||
// if (!stopStream) {
|
||||
// dispatch(setStopStream(true));
|
||||
// dispatch(setSubmission({}));
|
||||
// }
|
||||
// dispatch(setEmptyMessage());
|
||||
|
||||
// const convo = { title, error: false, conversationId: id, chatGptLabel, promptPrefix };
|
||||
|
||||
// if (bingData) {
|
||||
// const {
|
||||
// parentMessageId,
|
||||
// conversationSignature,
|
||||
// jailbreakConversationId,
|
||||
// clientId,
|
||||
// invocationId,
|
||||
// toneStyle
|
||||
// } = bingData;
|
||||
// dispatch(
|
||||
// setConversation({
|
||||
// ...convo,
|
||||
// parentMessageId,
|
||||
// jailbreakConversationId,
|
||||
// conversationSignature,
|
||||
// clientId,
|
||||
// invocationId,
|
||||
// toneStyle,
|
||||
// latestMessage: null
|
||||
// })
|
||||
// );
|
||||
// } else {
|
||||
// dispatch(
|
||||
// setConversation({
|
||||
// ...convo,
|
||||
// parentMessageId,
|
||||
// jailbreakConversationId: null,
|
||||
// conversationSignature: null,
|
||||
// clientId: null,
|
||||
// invocationId: null,
|
||||
// toneStyle: null,
|
||||
// latestMessage: null
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
// const data = await trigger();
|
||||
|
||||
// if (chatGptLabel) {
|
||||
// dispatch(setModel('chatgptCustom'));
|
||||
// dispatch(setCustomModel(chatGptLabel.toLowerCase()));
|
||||
// } else {
|
||||
// dispatch(setModel(model));
|
||||
// dispatch(setCustomModel(null));
|
||||
// }
|
||||
|
||||
// dispatch(setMessages(data));
|
||||
// dispatch(setCustomGpt(convo));
|
||||
// dispatch(setText(''));
|
||||
// dispatch(setStopStream(false));
|
||||
};
|
||||
|
||||
const renameHandler = e => {
|
||||
|
|
|
|||
168
client/src/components/Endpoints/BingAI/Settings.jsx
Normal file
168
client/src/components/Endpoints/BingAI/Settings.jsx
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { Input } from '~/components/ui/Input.tsx';
|
||||
import { Label } from '~/components/ui/Label.tsx';
|
||||
import { Checkbox } from '~/components/ui/Checkbox.tsx';
|
||||
import SelectDropdown from '../../ui/SelectDropDown';
|
||||
import { axiosPost } from '~/utils/fetchers.js';
|
||||
import { cn } from '~/utils/';
|
||||
import debounce from 'lodash/debounce';
|
||||
// import ModelDropDown from '../../ui/ModelDropDown';
|
||||
// import { Slider } from '~/components/ui/Slider.tsx';
|
||||
// import OptionHover from './OptionHover';
|
||||
// import { HoverCard, HoverCardTrigger } from '~/components/ui/HoverCard.tsx';
|
||||
const defaultTextProps =
|
||||
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] 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-500 dark:bg-gray-700 focus:dark:bg-gray-600 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 optionText =
|
||||
'p-0 shadow-none text-right pr-1 h-8 border-transparent focus:ring-[#10a37f] focus:ring-offset-0 focus:ring-opacity-100 hover:bg-gray-800/10 dark:hover:bg-white/10 focus:bg-gray-800/10 dark:focus:bg-white/10 transition-colors';
|
||||
|
||||
function Settings(props) {
|
||||
const { readonly, context, systemMessage, jailbreak, toneStyle, setOption } = props;
|
||||
const [tokenCount, setTokenCount] = useState(0);
|
||||
const showSystemMessage = jailbreak;
|
||||
const setContext = setOption('context');
|
||||
const setSystemMessage = setOption('systemMessage');
|
||||
const setJailbreak = setOption('jailbreak');
|
||||
const setToneStyle = value => setOption('toneStyle')(value.toLowerCase());
|
||||
|
||||
// useEffect to update token count
|
||||
|
||||
useEffect(() => {
|
||||
if (!context || context.trim() === '') {
|
||||
setTokenCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const debouncedPost = debounce(axiosPost, 250);
|
||||
const handleTextChange = context => {
|
||||
debouncedPost({
|
||||
url: '/api/tokenizer',
|
||||
arg: { text: context },
|
||||
callback: data => {
|
||||
setTokenCount(data.count);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleTextChange(context);
|
||||
return () => debouncedPost.cancel();
|
||||
}, [context]);
|
||||
|
||||
// console.log('data', data);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="toneStyle-dropdown"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Tone Style <small className="opacity-40">(default: fast)</small>
|
||||
</Label>
|
||||
<SelectDropdown
|
||||
id="toneStyle-dropdown"
|
||||
title={null}
|
||||
value={`${toneStyle.charAt(0).toUpperCase()}${toneStyle.slice(1)}`}
|
||||
setValue={setToneStyle}
|
||||
availableValues={['Creative', 'Fast', 'Balanced', 'Precise']}
|
||||
disabled={readonly}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex w-full resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
|
||||
)}
|
||||
containerClassName="flex w-full resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="context"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Context <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="context"
|
||||
disabled={readonly}
|
||||
value={context || ''}
|
||||
onChange={e => setContext(e.target.value || null)}
|
||||
placeholder="Bing can use up to 7k tokens for 'context', which it can reference for the conversation. The specific limit is not known but may run into errors exceeding 7k tokens"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[100px] w-full resize-none px-3 py-2'
|
||||
)}
|
||||
/>
|
||||
<small className="mb-5 text-black dark:text-white">{`Token count: ${tokenCount}`}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="jailbreak"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Enable Sydney <small className="opacity-40">(default: false)</small>
|
||||
</Label>
|
||||
<div className="flex h-[40px] w-full items-center space-x-3">
|
||||
<Checkbox
|
||||
id="jailbreak"
|
||||
disabled={readonly}
|
||||
checked={jailbreak}
|
||||
className="focus:ring-opacity-20 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:focus:ring-gray-600 dark:focus:ring-opacity-50 dark:focus:ring-offset-0"
|
||||
onCheckedChange={setJailbreak}
|
||||
/>
|
||||
<label
|
||||
htmlFor="jailbreak"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||
>
|
||||
Jailbreak <small>To enable Sydney</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{showSystemMessage && (
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="systemMessage"
|
||||
className="text-left text-sm font-medium"
|
||||
style={{ opacity: showSystemMessage ? '1' : '0' }}
|
||||
>
|
||||
<a
|
||||
href="https://github.com/danny-avila/chatgpt-clone/blob/main/client/defaultSystemMessage.md"
|
||||
target="_blank"
|
||||
className="text-blue-500 transition-colors duration-200 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-500"
|
||||
>
|
||||
System Message
|
||||
</a>{' '}
|
||||
<small className="opacity-40 dark:text-gray-50">(default: blank)</small>
|
||||
</Label>
|
||||
|
||||
<TextareaAutosize
|
||||
id="systemMessage"
|
||||
disabled={readonly}
|
||||
value={systemMessage || ''}
|
||||
onChange={e => setSystemMessage(e.target.value || null)}
|
||||
placeholder="WARNING: Misuse of this feature can get you BANNED from using Bing! Click on 'System Message' for full instructions and the default message if omitted, which is the 'Sydney' preset that is considered safe."
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[100px] w-full resize-none px-3 py-2 placeholder:text-red-400'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* <HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
type="temp"
|
||||
side="left"
|
||||
/>
|
||||
</HoverCard> */}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
148
client/src/components/Endpoints/EditPresetDialog.jsx
Normal file
148
client/src/components/Endpoints/EditPresetDialog.jsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import axios from 'axios';
|
||||
import exportFromJSON from 'export-from-json';
|
||||
import DialogTemplate from '../ui/DialogTemplate';
|
||||
import { Dialog, DialogClose, DialogButton } from '../ui/Dialog.tsx';
|
||||
import { Input } from '../ui/Input.tsx';
|
||||
import { Label } from '../ui/Label.tsx';
|
||||
import Dropdown from '../ui/Dropdown';
|
||||
import { cn } from '~/utils/';
|
||||
import cleanupPreset from '~/utils/cleanupPreset';
|
||||
|
||||
import Settings from './Settings';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
// const [title, setTitle] = useState('My Preset');
|
||||
const [preset, setPreset] = useState(_preset);
|
||||
const setPresets = useSetRecoilState(store.presets);
|
||||
|
||||
const availableEndpoints = useRecoilValue(store.availableEndpoints);
|
||||
const endpointsFilter = useRecoilValue(store.endpointsFilter);
|
||||
|
||||
const setOption = param => newValue => {
|
||||
let update = {};
|
||||
update[param] = newValue;
|
||||
setPreset(prevState =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update
|
||||
},
|
||||
endpointsFilter
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const defaultTextProps =
|
||||
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] 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-500 dark:bg-gray-700 focus:dark:bg-gray-600 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 submitPreset = () => {
|
||||
axios({
|
||||
method: 'post',
|
||||
url: '/api/presets',
|
||||
data: cleanupPreset({ preset, endpointsFilter }),
|
||||
withCredentials: true
|
||||
}).then(res => {
|
||||
setPresets(res?.data);
|
||||
});
|
||||
};
|
||||
|
||||
const exportPreset = () => {
|
||||
exportFromJSON({
|
||||
data: cleanupPreset({ preset, endpointsFilter }),
|
||||
fileName: `${preset?.title}.json`,
|
||||
exportType: exportFromJSON.types.json
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPreset(_preset);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<DialogTemplate
|
||||
title={`${title || 'Edit Preset'} - ${preset?.title}`}
|
||||
className="max-w-full sm:max-w-4xl"
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="grid w-full gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||
<Label
|
||||
htmlFor="chatGptLabel"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Preset Name
|
||||
</Label>
|
||||
<Input
|
||||
id="chatGptLabel"
|
||||
value={preset?.title || ''}
|
||||
onChange={e => setOption('title')(e.target.value || '')}
|
||||
placeholder="Set a custom name, in case you can find this preset"
|
||||
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'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||
<Label
|
||||
htmlFor="endpoint"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Endpoint
|
||||
</Label>
|
||||
<Dropdown
|
||||
id="endpoint"
|
||||
value={preset?.endpoint || ''}
|
||||
onChange={setOption('endpoint')}
|
||||
options={availableEndpoints}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
|
||||
)}
|
||||
containerClassName="flex w-full resize-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-4 w-full border-t border-gray-300 dark:border-gray-500" />
|
||||
<div className="w-full p-0">
|
||||
<Settings
|
||||
preset={preset}
|
||||
setOption={setOption}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
buttons={
|
||||
<>
|
||||
<DialogClose
|
||||
onClick={submitPreset}
|
||||
className="dark:hover:gray-400 border-gray-700 bg-green-600 text-white hover:bg-green-700 dark:hover:bg-green-800"
|
||||
>
|
||||
Save
|
||||
</DialogClose>
|
||||
</>
|
||||
}
|
||||
leftButtons={
|
||||
<>
|
||||
<DialogButton
|
||||
onClick={exportPreset}
|
||||
className="dark:hover:gray-400 border-gray-700"
|
||||
>
|
||||
Export
|
||||
</DialogButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditPresetDialog;
|
||||
97
client/src/components/Endpoints/EndpointOptionsDialog.jsx
Normal file
97
client/src/components/Endpoints/EndpointOptionsDialog.jsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import exportFromJSON from 'export-from-json';
|
||||
import DialogTemplate from '../ui/DialogTemplate.jsx';
|
||||
import { Dialog, DialogButton } from '../ui/Dialog.tsx';
|
||||
import SaveAsPresetDialog from './SaveAsPresetDialog';
|
||||
import cleanupPreset from '~/utils/cleanupPreset';
|
||||
|
||||
import Settings from './Settings';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
// A preset dialog to show readonly preset values.
|
||||
const EndpointOptionsDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
// const [title, setTitle] = useState('My Preset');
|
||||
const [preset, setPreset] = useState(_preset);
|
||||
|
||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState(false);
|
||||
const endpointsFilter = useRecoilValue(store.endpointsFilter);
|
||||
|
||||
const setOption = param => newValue => {
|
||||
let update = {};
|
||||
update[param] = newValue;
|
||||
setPreset(prevState => ({
|
||||
...prevState,
|
||||
...update
|
||||
}));
|
||||
};
|
||||
|
||||
const saveAsPreset = () => {
|
||||
setSaveAsDialogShow(true);
|
||||
};
|
||||
|
||||
const exportPreset = () => {
|
||||
exportFromJSON({
|
||||
data: cleanupPreset({ preset, endpointsFilter }),
|
||||
fileName: `${preset?.title}.json`,
|
||||
exportType: exportFromJSON.types.json
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPreset(_preset);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<DialogTemplate
|
||||
title={`${title || 'View Options'} - ${preset?.endpoint}`}
|
||||
className="max-w-full sm:max-w-4xl"
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="w-full p-0">
|
||||
<Settings
|
||||
preset={preset}
|
||||
readonly={true}
|
||||
setOption={setOption}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
buttons={
|
||||
<>
|
||||
<DialogButton
|
||||
onClick={saveAsPreset}
|
||||
className="dark:hover:gray-400 border-gray-700 bg-green-600 text-white hover:bg-green-700 dark:hover:bg-green-800"
|
||||
>
|
||||
Save As Preset
|
||||
</DialogButton>
|
||||
</>
|
||||
}
|
||||
leftButtons={
|
||||
<>
|
||||
<DialogButton
|
||||
onClick={exportPreset}
|
||||
className="dark:hover:gray-400 border-gray-700"
|
||||
>
|
||||
Export
|
||||
</DialogButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Dialog>
|
||||
<SaveAsPresetDialog
|
||||
open={saveAsDialogShow}
|
||||
onOpenChange={setSaveAsDialogShow}
|
||||
preset={preset}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EndpointOptionsDialog;
|
||||
51
client/src/components/Endpoints/EndpointOptionsPopover.jsx
Normal file
51
client/src/components/Endpoints/EndpointOptionsPopover.jsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import { Button } from '../ui/Button.tsx';
|
||||
import CrossIcon from '../svg/CrossIcon';
|
||||
// import SaveIcon from '../svg/SaveIcon';
|
||||
import { Save } from 'lucide-react';
|
||||
|
||||
function EndpointOptionsPopover({ content, visible, saveAsPreset, switchToSimpleMode }) {
|
||||
const cardStyle =
|
||||
'shadow-md rounded-md min-w-[75px] font-normal bg-white border-black/10 border dark:bg-gray-700 text-black dark:text-white';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
' endpointOptionsPopover-container absolute bottom-[-10px] flex w-full flex-col items-center justify-center md:px-4' +
|
||||
(visible ? ' show' : '')
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
cardStyle +
|
||||
' border-s-0 border-d-0 flex w-full flex-col overflow-hidden rounded-none border-t bg-slate-200 px-0 pb-[10px] dark:border-white/10 md:rounded-md md:border lg:w-[736px]'
|
||||
}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between bg-slate-100 px-2 py-2 dark:bg-gray-800/60">
|
||||
{/* <span className="text-xs font-medium font-normal">Advanced settings for OpenAI endpoint</span> */}
|
||||
<Button
|
||||
type="button"
|
||||
className="h-auto bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
onClick={saveAsPreset}
|
||||
>
|
||||
<Save className="mr-1 w-[14px]" />
|
||||
Save as preset
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="h-auto bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
onClick={switchToSimpleMode}
|
||||
>
|
||||
<CrossIcon className="mr-1" />
|
||||
{/* Switch to simple mode */}
|
||||
</Button>
|
||||
</div>
|
||||
<div>{content}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default EndpointOptionsPopover;
|
||||
33
client/src/components/Endpoints/OpenAI/OptionHover.jsx
Normal file
33
client/src/components/Endpoints/OpenAI/OptionHover.jsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import { HoverCardPortal, HoverCardContent } from '~/components/ui/HoverCard.tsx';
|
||||
|
||||
const types = {
|
||||
temp: 'Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.',
|
||||
max: "The max tokens to generate. The total length of input tokens and generated tokens is limited by the model's context length.",
|
||||
topp: 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We recommend altering this or temperature but not both.',
|
||||
freq: "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.",
|
||||
pres: "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics."
|
||||
};
|
||||
|
||||
function OptionHover({ type, side }) {
|
||||
// const options = {};
|
||||
// if (type === 'pres') {
|
||||
// options.sideOffset = 45;
|
||||
// }
|
||||
|
||||
return (
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent
|
||||
side={side}
|
||||
className="w-80 "
|
||||
// {...options}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">{types[type]}</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
);
|
||||
}
|
||||
|
||||
export default OptionHover;
|
||||
337
client/src/components/Endpoints/OpenAI/Settings.jsx
Normal file
337
client/src/components/Endpoints/OpenAI/Settings.jsx
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import SelectDropdown from '../../ui/SelectDropDown';
|
||||
import { Input } from '~/components/ui/Input.tsx';
|
||||
import { Label } from '~/components/ui/Label.tsx';
|
||||
import { Slider } from '~/components/ui/Slider.tsx';
|
||||
import { InputNumber } from '~/components/ui/InputNumber.tsx';
|
||||
// import { InputNumber } from '../../ui/InputNumber';
|
||||
import OptionHover from './OptionHover';
|
||||
import { HoverCard, HoverCardTrigger } from '~/components/ui/HoverCard.tsx';
|
||||
import { cn } from '~/utils/';
|
||||
const defaultTextProps =
|
||||
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] 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-500 dark:bg-gray-700 focus:dark:bg-gray-600 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 optionText =
|
||||
'p-0 shadow-none text-right pr-1 h-8 border-transparent focus:ring-[#10a37f] focus:ring-offset-0 focus:ring-opacity-100 hover:bg-gray-800/10 dark:hover:bg-white/10 focus:bg-gray-800/10 dark:focus:bg-white/10 transition-colors';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
function Settings(props) {
|
||||
const { readonly, model, chatGptLabel, promptPrefix, temperature, topP, freqP, presP, setOption } = props;
|
||||
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
|
||||
const setModel = setOption('model');
|
||||
const setChatGptLabel = setOption('chatGptLabel');
|
||||
const setPromptPrefix = setOption('promptPrefix');
|
||||
const setTemperature = setOption('temperature');
|
||||
const setTopP = setOption('top_p');
|
||||
const setFreqP = setOption('presence_penalty');
|
||||
const setPresP = setOption('frequency_penalty');
|
||||
|
||||
const models = endpointsConfig?.['openAI']?.['availableModels'] || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<SelectDropdown
|
||||
value={model}
|
||||
setValue={setModel}
|
||||
availableValues={models}
|
||||
disabled={readonly}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex w-full resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
|
||||
)}
|
||||
containerClassName="flex w-full resize-none"
|
||||
/>
|
||||
{/* <Label
|
||||
htmlFor="model"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Model
|
||||
</Label>
|
||||
<Input
|
||||
id="model"
|
||||
value={model}
|
||||
// ref={inputRef}
|
||||
onChange={e => setModel(e.target.value)}
|
||||
placeholder="Set a custom name for ChatGPT"
|
||||
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'
|
||||
)}
|
||||
/> */}
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="chatGptLabel"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Custom Name <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<Input
|
||||
id="chatGptLabel"
|
||||
disabled={readonly}
|
||||
value={chatGptLabel || ''}
|
||||
// ref={inputRef}
|
||||
onChange={e => setChatGptLabel(e.target.value || null)}
|
||||
placeholder="Set a custom name for ChatGPT"
|
||||
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'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="promptPrefix"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Prompt Prefix <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="promptPrefix"
|
||||
disabled={readonly}
|
||||
value={promptPrefix || ''}
|
||||
onChange={e => setPromptPrefix(e.target.value || null)}
|
||||
placeholder="Set custom instructions. Defaults to: 'You are ChatGPT, a large language model trained by OpenAI.'"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[100px] w-full resize-none px-3 py-2 '
|
||||
)}
|
||||
// onFocus={() => {
|
||||
// textareaRef.current.classList.remove('max-h-10');
|
||||
// textareaRef.current.classList.add('max-h-52');
|
||||
// }}
|
||||
// onBlur={() => {
|
||||
// textareaRef.current.classList.remove('max-h-52');
|
||||
// textareaRef.current.classList.add('max-h-10');
|
||||
// }}
|
||||
// ref={textareaRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label
|
||||
htmlFor="temp-int"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Temperature <small className="opacity-40">(default: 1)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="temp-int"
|
||||
disabled={readonly}
|
||||
value={temperature}
|
||||
onChange={value => setTemperature(value)}
|
||||
max={2}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[temperature]}
|
||||
onValueChange={value => setTemperature(value[0])}
|
||||
doubleClickHandler={() => setTemperature(1)}
|
||||
max={2}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
type="temp"
|
||||
side="left"
|
||||
/>
|
||||
</HoverCard>
|
||||
|
||||
{/* <HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label
|
||||
htmlFor="chatGptLabel"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Max tokens
|
||||
</Label>
|
||||
<Input
|
||||
id="max-tokens-int"
|
||||
disabled
|
||||
value={maxTokens}
|
||||
onChange={e => setMaxTokens(e.target.value)}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(optionText, 'h-auto w-12 border-0 group-hover/temp:border-gray-200')
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[maxTokens]}
|
||||
onValueChange={value => setMaxTokens(value[0])}
|
||||
max={2048} // should be dynamic to the currently selected model
|
||||
min={1}
|
||||
step={1}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
type="max"
|
||||
side="left"
|
||||
/>
|
||||
</HoverCard> */}
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label
|
||||
htmlFor="top-p-int"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Top P <small className="opacity-40">(default: 1)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-p-int"
|
||||
disabled={readonly}
|
||||
value={topP}
|
||||
onChange={value => setTopP(value)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topP]}
|
||||
onValueChange={value => setTopP(value[0])}
|
||||
doubleClickHandler={() => setTopP(1)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
type="topp"
|
||||
side="left"
|
||||
/>
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label
|
||||
htmlFor="freq-penalty-int"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Frequency Penalty <small className="opacity-40">(default: 0)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="freq-penalty-int"
|
||||
disabled={readonly}
|
||||
value={freqP}
|
||||
onChange={value => setFreqP(value)}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[freqP]}
|
||||
onValueChange={value => setFreqP(value[0])}
|
||||
doubleClickHandler={() => setFreqP(0)}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
type="freq"
|
||||
side="left"
|
||||
/>
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label
|
||||
htmlFor="pres-penalty-int"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Presence Penalty <small className="opacity-40">(default: 0)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="pres-penalty-int"
|
||||
disabled={readonly}
|
||||
value={presP}
|
||||
onChange={value => setPresP(value)}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[presP]}
|
||||
onValueChange={value => setPresP(value[0])}
|
||||
doubleClickHandler={() => setPresP(0)}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
type="pres"
|
||||
side="left"
|
||||
/>
|
||||
</HoverCard>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
81
client/src/components/Endpoints/SaveAsPresetDialog.jsx
Normal file
81
client/src/components/Endpoints/SaveAsPresetDialog.jsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import axios from 'axios';
|
||||
import DialogTemplate from '../ui/DialogTemplate';
|
||||
import { Dialog } from '../ui/Dialog.tsx';
|
||||
import { Input } from '../ui/Input.tsx';
|
||||
import { Label } from '../ui/Label.tsx';
|
||||
import { cn } from '~/utils/';
|
||||
import cleanupPreset from '~/utils/cleanupPreset';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
const SaveAsPresetDialog = ({ open, onOpenChange, preset }) => {
|
||||
const [title, setTitle] = useState(preset?.title || 'My Preset');
|
||||
const setPresets = useSetRecoilState(store.presets);
|
||||
const endpointsFilter = useRecoilValue(store.endpointsFilter);
|
||||
|
||||
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 submitPreset = () => {
|
||||
const _preset = cleanupPreset({
|
||||
preset: {
|
||||
...preset,
|
||||
title
|
||||
},
|
||||
endpointsFilter
|
||||
});
|
||||
|
||||
axios({
|
||||
method: 'post',
|
||||
url: '/api/presets',
|
||||
data: _preset,
|
||||
withCredentials: true
|
||||
}).then(res => {
|
||||
setPresets(res?.data);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(preset?.title || 'My Preset');
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<DialogTemplate
|
||||
title="Save As Preset"
|
||||
main={
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="chatGptLabel"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
Preset Name
|
||||
</Label>
|
||||
<Input
|
||||
id="chatGptLabel"
|
||||
value={title || ''}
|
||||
onChange={e => setTitle(e.target.value || '')}
|
||||
placeholder="Set a custom name, in case you can find this preset"
|
||||
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'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: submitPreset,
|
||||
selectClasses: 'bg-green-600 hover:bg-green-700 dark:hover:bg-green-800 text-white',
|
||||
selectText: 'Save'
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveAsPresetDialog;
|
||||
40
client/src/components/Endpoints/Settings.jsx
Normal file
40
client/src/components/Endpoints/Settings.jsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
|
||||
import OpenAISettings from './OpenAI/Settings.jsx';
|
||||
import BingAISettings from './BingAI/Settings.jsx';
|
||||
|
||||
// A preset dialog to show readonly preset values.
|
||||
const Settings = ({ preset, ...props }) => {
|
||||
const renderSettings = () => {
|
||||
const { endpoint } = preset || {};
|
||||
|
||||
if (endpoint === 'openAI')
|
||||
return (
|
||||
<OpenAISettings
|
||||
model={preset?.model}
|
||||
chatGptLabel={preset?.chatGptLabel}
|
||||
promptPrefix={preset?.promptPrefix}
|
||||
temperature={preset?.temperature}
|
||||
topP={preset?.top_p}
|
||||
freqP={preset?.presence_penalty}
|
||||
presP={preset?.frequency_penalty}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
else if (endpoint === 'bingAI')
|
||||
return (
|
||||
<BingAISettings
|
||||
toneStyle={preset?.toneStyle}
|
||||
context={preset?.context}
|
||||
systemMessage={preset?.systemMessage}
|
||||
jailbreak={preset?.jailbreak}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
else return <div className="text-black dark:text-white">Not implemented</div>;
|
||||
};
|
||||
|
||||
return renderSettings();
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
155
client/src/components/Input/BingAIOptions/index.jsx
Normal file
155
client/src/components/Input/BingAIOptions/index.jsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useRecoilValue, useRecoilState } from 'recoil';
|
||||
import { cn } from '~/utils';
|
||||
import { Button } from '../../ui/Button.tsx';
|
||||
import { Settings2 } from 'lucide-react';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../../ui/Tabs.tsx';
|
||||
import SelectDropdown from '../../ui/SelectDropDown';
|
||||
import Settings from '../../Endpoints/BingAI/Settings.jsx';
|
||||
import EndpointOptionsPopover from '../../Endpoints/EndpointOptionsPopover';
|
||||
import SaveAsPresetDialog from '../../Endpoints/SaveAsPresetDialog';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
function BingAIOptions() {
|
||||
const [conversation, setConversation] = useRecoilState(store.conversation) || {};
|
||||
const [advancedMode, setAdvancedMode] = useState(false);
|
||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState(false);
|
||||
const { endpoint, conversationId } = conversation;
|
||||
const { toneStyle, context, systemMessage, jailbreak } = conversation;
|
||||
|
||||
// useEffect(() => {
|
||||
// if (endpoint !== 'bingAI') return;
|
||||
|
||||
// const mustInAdvancedMode = context !== null || systemMessage !== null;
|
||||
|
||||
// if (mustInAdvancedMode && !advancedMode) setAdvancedMode(true);
|
||||
// }, [conversation, advancedMode]);
|
||||
|
||||
if (endpoint !== 'bingAI') return null;
|
||||
if (conversationId !== 'new') return null;
|
||||
|
||||
const triggerAdvancedMode = () => setAdvancedMode(prev => !prev);
|
||||
|
||||
const switchToSimpleMode = () => {
|
||||
// setConversation(prevState => ({
|
||||
// ...prevState,
|
||||
// context: null,
|
||||
// systemMessage: null
|
||||
// }));
|
||||
setAdvancedMode(false);
|
||||
};
|
||||
|
||||
const saveAsPreset = () => {
|
||||
setSaveAsDialogShow(true);
|
||||
};
|
||||
|
||||
const setOption = param => newValue => {
|
||||
let update = {};
|
||||
update[param] = newValue;
|
||||
setConversation(prevState => ({
|
||||
...prevState,
|
||||
...update
|
||||
}));
|
||||
};
|
||||
|
||||
const cardStyle =
|
||||
'transition-colors shadow-md rounded-md min-w-[75px] font-normal bg-white border-black/10 hover:border-black/10 focus:border-black/10 dark:border-black/10 dark:hover:border-black/10 dark:focus:border-black/10 border dark:bg-gray-700 text-black dark:text-white';
|
||||
const defaultClasses =
|
||||
'p-2 rounded-md min-w-[75px] font-normal bg-white/[.60] dark:bg-gray-700 text-black text-xs';
|
||||
const defaultSelected = cn(defaultClasses, 'font-medium data-[state=active]:text-white text-xs text-white');
|
||||
const selectedClass = val => val + '-tab ' + defaultSelected;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
'openAIOptions-simple-container flex w-full flex-wrap items-center justify-center gap-2' +
|
||||
(!advancedMode ? ' show' : '')
|
||||
}
|
||||
>
|
||||
<SelectDropdown
|
||||
title="Mode"
|
||||
value={jailbreak ? 'Sydney' : 'BingAI'}
|
||||
setValue={value => setOption('jailbreak')(value === 'Sydney')}
|
||||
availableValues={['BingAI', 'Sydney']}
|
||||
showAbove={true}
|
||||
showLabel={false}
|
||||
className={cn(
|
||||
cardStyle,
|
||||
'min-w-36 z-50 flex h-[40px] w-36 flex-none items-center justify-center px-4 ring-0 hover:cursor-pointer hover:bg-slate-50 focus:ring-0 focus:ring-offset-0 data-[state=open]:bg-slate-50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:data-[state=open]:bg-gray-600'
|
||||
)}
|
||||
/>
|
||||
|
||||
<Tabs
|
||||
value={toneStyle}
|
||||
className={
|
||||
cardStyle +
|
||||
' z-50 flex h-[40px] flex-none items-center justify-center px-0 hover:bg-slate-50 dark:hover:bg-gray-600'
|
||||
}
|
||||
onValueChange={value => setOption('toneStyle')(value.toLowerCase())}
|
||||
>
|
||||
<TabsList className="bg-white/[.60] dark:bg-gray-700">
|
||||
<TabsTrigger
|
||||
value="creative"
|
||||
className={`${toneStyle === 'creative' ? selectedClass('creative') : defaultClasses}`}
|
||||
>
|
||||
{'Creative'}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="fast"
|
||||
className={`${toneStyle === 'fast' ? selectedClass('fast') : defaultClasses}`}
|
||||
>
|
||||
{'Fast'}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="balanced"
|
||||
className={`${toneStyle === 'balanced' ? selectedClass('balanced') : defaultClasses}`}
|
||||
>
|
||||
{'Balanced'}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="precise"
|
||||
className={`${toneStyle === 'precise' ? selectedClass('precise') : defaultClasses}`}
|
||||
>
|
||||
{'Precise'}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<Button
|
||||
type="button"
|
||||
className={cn(
|
||||
cardStyle,
|
||||
'min-w-4 z-50 flex h-[40px] flex-none items-center justify-center px-4 hover:bg-slate-50 focus:ring-0 focus:ring-offset-0 dark:hover:bg-gray-600'
|
||||
)}
|
||||
onClick={triggerAdvancedMode}
|
||||
>
|
||||
<Settings2 className="w-4 text-gray-600 dark:text-white" />
|
||||
</Button>
|
||||
</div>
|
||||
<EndpointOptionsPopover
|
||||
content={
|
||||
<div className="z-50 px-4 py-4">
|
||||
<Settings
|
||||
context={context}
|
||||
systemMessage={systemMessage}
|
||||
jailbreak={jailbreak}
|
||||
toneStyle={toneStyle}
|
||||
setOption={setOption}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
visible={advancedMode}
|
||||
saveAsPreset={saveAsPreset}
|
||||
switchToSimpleMode={switchToSimpleMode}
|
||||
/>
|
||||
<SaveAsPresetDialog
|
||||
open={saveAsDialogShow}
|
||||
onOpenChange={setSaveAsDialogShow}
|
||||
preset={conversation}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default BingAIOptions;
|
||||
|
|
@ -1,37 +1,31 @@
|
|||
import React, { useState, useEffect, forwardRef } from 'react';
|
||||
import { Tabs, TabsList, TabsTrigger } from '../ui/Tabs.tsx';
|
||||
import { useRecoilValue, useRecoilState } from 'recoil';
|
||||
// import { setConversation } from '~/store/convoSlice';
|
||||
import { cn } from '../../utils';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
function BingStyles(props, ref) {
|
||||
const [value, setValue] = useState('fast');
|
||||
|
||||
const [conversation, setConversation] = useRecoilState(store.conversation) || {};
|
||||
const { model, conversationId } = conversation;
|
||||
const { endpoint, conversationId, jailbreak, toneStyle } = conversation;
|
||||
const messages = useRecoilValue(store.messages);
|
||||
|
||||
const isBing = model === 'bingai' || model === 'sydney';
|
||||
useEffect(() => {
|
||||
if ((model === 'bingai' && !conversationId) || model === 'sydney') {
|
||||
setConversation(prevState => ({ ...prevState, toneStyle: value }));
|
||||
}
|
||||
}, [conversationId, model, value]);
|
||||
const isBing = endpoint === 'bingAI';
|
||||
|
||||
const show = isBing && (!conversationId || messages?.length === 0 || props.show);
|
||||
const defaultClasses = 'p-2 rounded-md min-w-[75px] font-normal bg-white/[.60] dark:bg-gray-700 text-black text-xs';
|
||||
const defaultSelected = defaultClasses + 'font-medium data-[state=active]:text-white text-xs';
|
||||
const defaultClasses =
|
||||
'p-2 rounded-md min-w-[75px] font-normal bg-white/[.60] dark:bg-gray-700 text-black text-xs';
|
||||
const defaultSelected = cn(defaultClasses, 'font-medium data-[state=active]:text-white text-xs text-white');
|
||||
|
||||
const selectedClass = val => val + '-tab ' + defaultSelected;
|
||||
|
||||
const changeHandler = value => {
|
||||
setValue(value);
|
||||
setConversation(prevState => ({ ...prevState, toneStyle: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultValue={value}
|
||||
value={toneStyle}
|
||||
className={`bing-styles mb-1 shadow-md ${show ? 'show' : ''}`}
|
||||
onValueChange={changeHandler}
|
||||
ref={ref}
|
||||
|
|
@ -39,25 +33,25 @@ function BingStyles(props, ref) {
|
|||
<TabsList className="bg-white/[.60] dark:bg-gray-700">
|
||||
<TabsTrigger
|
||||
value="creative"
|
||||
className={`${value === 'creative' ? selectedClass(value) : defaultClasses}`}
|
||||
className={`${toneStyle === 'creative' ? selectedClass('creative') : defaultClasses}`}
|
||||
>
|
||||
{'Creative'}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="fast"
|
||||
className={`${value === 'fast' ? selectedClass(value) : defaultClasses}`}
|
||||
className={`${toneStyle === 'fast' ? selectedClass('fast') : defaultClasses}`}
|
||||
>
|
||||
{'Fast'}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="balanced"
|
||||
className={`${value === 'balanced' ? selectedClass(value) : defaultClasses}`}
|
||||
className={`${toneStyle === 'balanced' ? selectedClass('balanced') : defaultClasses}`}
|
||||
>
|
||||
{'Balanced'}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="precise"
|
||||
className={`${value === 'precise' ? selectedClass(value) : defaultClasses}`}
|
||||
className={`${toneStyle === 'precise' ? selectedClass('precise') : defaultClasses}`}
|
||||
>
|
||||
{'Precise'}
|
||||
</TabsTrigger>
|
||||
|
|
|
|||
59
client/src/components/Input/ChatGPTOptions/index.jsx
Normal file
59
client/src/components/Input/ChatGPTOptions/index.jsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import SelectDropdown from '../../ui/SelectDropDown.jsx';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
function ChatGPTOptions() {
|
||||
const [conversation, setConversation] = useRecoilState(store.conversation) || {};
|
||||
const { endpoint, conversationId } = conversation;
|
||||
const { model } = conversation;
|
||||
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (endpoint !== 'chatGPTBrowser') return;
|
||||
// }, [conversation]);
|
||||
|
||||
if (endpoint !== 'chatGPTBrowser') return null;
|
||||
if (conversationId !== 'new') return null;
|
||||
|
||||
const models = endpointsConfig?.['chatGPTBrowser']?.['availableModels'] || [];
|
||||
|
||||
// const modelMap = new Map([
|
||||
// ['Default (GPT-3.5)', 'text-davinci-002-render-sha'],
|
||||
// ['Legacy (GPT-3.5)', 'text-davinci-002-render-paid'],
|
||||
// ['GPT-4', 'gpt-4']
|
||||
// ]);
|
||||
|
||||
const setOption = param => newValue => {
|
||||
let update = {};
|
||||
update[param] = newValue;
|
||||
setConversation(prevState => ({
|
||||
...prevState,
|
||||
...update
|
||||
}));
|
||||
};
|
||||
|
||||
const cardStyle =
|
||||
'transition-colors shadow-md rounded-md min-w-[75px] font-normal bg-white border-black/10 hover:border-black/10 focus:border-black/10 dark:border-black/10 dark:hover:border-black/10 dark:focus:border-black/10 border dark:bg-gray-700 text-black dark:text-white';
|
||||
|
||||
return (
|
||||
<div className="openAIOptions-simple-container show flex w-full flex-wrap items-center justify-center gap-2">
|
||||
<SelectDropdown
|
||||
value={model}
|
||||
setValue={setOption('model')}
|
||||
availableValues={models}
|
||||
showAbove={true}
|
||||
showLabel={false}
|
||||
className={cn(
|
||||
cardStyle,
|
||||
'z-50 flex h-[40px] w-[260px] min-w-[260px] flex-none items-center justify-center px-4 ring-0 hover:cursor-pointer hover:bg-slate-50 focus:ring-0 focus:ring-offset-0 data-[state=open]:bg-slate-50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:data-[state=open]:bg-gray-600'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChatGPTOptions;
|
||||
24
client/src/components/Input/Endpoints/EndpointItem.jsx
Normal file
24
client/src/components/Input/Endpoints/EndpointItem.jsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import { DropdownMenuRadioItem } from '../../ui/DropdownMenu.tsx';
|
||||
import getIcon from '~/utils/getIcon';
|
||||
|
||||
export default function ModelItem({ endpoint, value, onSelect }) {
|
||||
const icon = getIcon({
|
||||
size: 20,
|
||||
endpoint,
|
||||
error: false,
|
||||
className: 'mr-2'
|
||||
});
|
||||
|
||||
// regular model
|
||||
return (
|
||||
<DropdownMenuRadioItem
|
||||
value={value}
|
||||
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
{icon}
|
||||
{endpoint}
|
||||
{!!['azureOpenAI', 'openAI'].find(e => e === endpoint) && <sup>$</sup>}
|
||||
</DropdownMenuRadioItem>
|
||||
);
|
||||
}
|
||||
17
client/src/components/Input/Endpoints/EndpointItems.jsx
Normal file
17
client/src/components/Input/Endpoints/EndpointItems.jsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import EndpointItem from './EndpointItem';
|
||||
|
||||
export default function EndpointItems({ endpoints, onSelect }) {
|
||||
return (
|
||||
<>
|
||||
{endpoints.map(endpoint => (
|
||||
<EndpointItem
|
||||
key={endpoint}
|
||||
value={endpoint}
|
||||
onSelect={onSelect}
|
||||
endpoint={endpoint}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
43
client/src/components/Input/Endpoints/FileUpload.jsx
Normal file
43
client/src/components/Input/Endpoints/FileUpload.jsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import { FileUp } from 'lucide-react';
|
||||
import cleanupPreset from '~/utils/cleanupPreset.js';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
const FileUpload = ({ onFileSelected }) => {
|
||||
// const setPresets = useSetRecoilState(store.presets);
|
||||
const endpointsFilter = useRecoilValue(store.endpointsFilter);
|
||||
|
||||
const handleFileChange = event => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const jsonData = JSON.parse(e.target.result);
|
||||
onFileSelected({ ...cleanupPreset({ preset: jsonData, endpointsFilter }), presetId: null });
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
className=" mr-1 flex h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal text-gray-600 hover:bg-slate-200 hover:text-green-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-green-500"
|
||||
>
|
||||
<FileUp className="flex w-[22px] items-center stroke-1" />
|
||||
<span className="ml-1 flex text-xs ">Import</span>
|
||||
<input
|
||||
id="file-upload"
|
||||
value=""
|
||||
type="file"
|
||||
className="hidden "
|
||||
accept=".json"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileUpload;
|
||||
190
client/src/components/Input/Endpoints/NewConversationMenu.jsx
Normal file
190
client/src/components/Input/Endpoints/NewConversationMenu.jsx
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useRecoilValue, useRecoilState } from 'recoil';
|
||||
import EditPresetDialog from '../../Endpoints/EditPresetDialog';
|
||||
import EndpointItems from './EndpointItems';
|
||||
import PresetItems from './PresetItems';
|
||||
import FileUpload from './FileUpload';
|
||||
import getIcon from '~/utils/getIcon';
|
||||
import manualSWR, { handleFileSelected } from '~/utils/fetchers';
|
||||
|
||||
import { Button } from '../../ui/Button.tsx';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '../../ui/DropdownMenu.tsx';
|
||||
import { Dialog, DialogTrigger } from '../../ui/Dialog.tsx';
|
||||
import DialogTemplate from '../../ui/DialogTemplate';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
export default function NewConversationMenu() {
|
||||
// const [modelSave, setModelSave] = useState(false);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [presetModelVisible, setPresetModelVisible] = useState(false);
|
||||
const [preset, setPreset] = useState(false);
|
||||
|
||||
// const models = useRecoilValue(store.models);
|
||||
const availableEndpoints = useRecoilValue(store.availableEndpoints);
|
||||
// const setCustomGPTModels = useSetRecoilState(store.customGPTModels);
|
||||
const [presets, setPresets] = useRecoilState(store.presets);
|
||||
|
||||
const conversation = useRecoilValue(store.conversation) || {};
|
||||
const { endpoint, conversationId } = conversation;
|
||||
// const { model, promptPrefix, chatGptLabel, conversationId } = conversation;
|
||||
const { newConversation } = store.useConversation();
|
||||
|
||||
const { trigger: clearPresetsTrigger } = manualSWR(`/api/presets/delete`, 'post', res => {
|
||||
console.log(res);
|
||||
setPresets(res.data);
|
||||
});
|
||||
|
||||
const importPreset = jsonData => {
|
||||
handleFileSelected(jsonData).then(setPresets);
|
||||
};
|
||||
|
||||
// update the default model when availableModels changes
|
||||
// typically, availableModels changes => modelsFilter or customGPTModels changes
|
||||
useEffect(() => {
|
||||
if (conversationId == 'new') {
|
||||
newConversation();
|
||||
}
|
||||
}, [availableEndpoints]);
|
||||
|
||||
// save selected model to localstoreage
|
||||
useEffect(() => {
|
||||
if (endpoint) localStorage.setItem('lastConversationSetup', JSON.stringify(conversation));
|
||||
}, [conversation]);
|
||||
|
||||
// set the current model
|
||||
const onSelectEndpoint = newEndpoint => {
|
||||
setMenuOpen(false);
|
||||
|
||||
if (!newEndpoint) return;
|
||||
// else if (newEndpoint === endpoint) return;
|
||||
else {
|
||||
newConversation({}, { endpoint: newEndpoint });
|
||||
}
|
||||
};
|
||||
|
||||
// set the current model
|
||||
const onSelectPreset = newPreset => {
|
||||
setMenuOpen(false);
|
||||
if (!newPreset) return;
|
||||
// else if (newEndpoint === endpoint) return;
|
||||
else {
|
||||
newConversation({}, newPreset);
|
||||
}
|
||||
};
|
||||
|
||||
const onChangePreset = preset => {
|
||||
setPresetModelVisible(true);
|
||||
setPreset(preset);
|
||||
};
|
||||
|
||||
const clearPreset = () => {
|
||||
clearPresetsTrigger({});
|
||||
};
|
||||
|
||||
const icon = getIcon({
|
||||
size: 32,
|
||||
...conversation,
|
||||
isCreatedByUser: false,
|
||||
error: false,
|
||||
button: true
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
// onOpenChange={onOpenChange}
|
||||
>
|
||||
<DropdownMenu
|
||||
open={menuOpen}
|
||||
onOpenChange={setMenuOpen}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
// style={{backgroundColor: 'rgb(16, 163, 127)'}}
|
||||
className={`absolute top-[0.25px] mb-0 ml-1 items-center rounded-md border-0 p-1 outline-none focus:ring-0 focus:ring-offset-0 disabled:top-[0.25px] dark:data-[state=open]:bg-opacity-50 md:top-1 md:left-1 md:ml-0 md:pl-1 md:disabled:top-1`}
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="min-w-[300px] dark:bg-gray-700"
|
||||
onCloseAutoFocus={event => event.preventDefault()}
|
||||
>
|
||||
<DropdownMenuLabel className="dark:text-gray-300">Select an Endpoint</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuRadioGroup
|
||||
value={endpoint}
|
||||
onValueChange={onSelectEndpoint}
|
||||
className="overflow-y-auto"
|
||||
>
|
||||
{availableEndpoints.length ? (
|
||||
<EndpointItems
|
||||
endpoints={availableEndpoints}
|
||||
onSelect={onSelectEndpoint}
|
||||
/>
|
||||
) : (
|
||||
<DropdownMenuLabel className="dark:text-gray-300">No endpoint available.</DropdownMenuLabel>
|
||||
)}
|
||||
</DropdownMenuRadioGroup>
|
||||
|
||||
<div className="mt-6 w-full" />
|
||||
|
||||
<DropdownMenuLabel className="flex items-center dark:text-gray-300">
|
||||
<span>Select a Preset</span>
|
||||
<div className="flex-1" />
|
||||
<FileUpload onFileSelected={importPreset} />
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
className="h-auto bg-transparent px-2 py-1 text-xs font-medium font-normal text-red-700 hover:bg-slate-200 hover:text-red-700 dark:bg-transparent dark:text-red-400 dark:hover:bg-gray-800 dark:hover:text-red-400"
|
||||
// onClick={clearPreset}
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogTemplate
|
||||
title="Clear presets"
|
||||
description="Are you sure you want to clear all presets? This is irreversible."
|
||||
selection={{
|
||||
selectHandler: clearPreset,
|
||||
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
|
||||
selectText: 'Clear'
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuRadioGroup
|
||||
onValueChange={onSelectPreset}
|
||||
className="overflow-y-auto"
|
||||
>
|
||||
{presets.length ? (
|
||||
<PresetItems
|
||||
presets={presets}
|
||||
onSelect={onSelectPreset}
|
||||
onChangePreset={onChangePreset}
|
||||
onDeletePreset={clearPresetsTrigger}
|
||||
/>
|
||||
) : (
|
||||
<DropdownMenuLabel className="dark:text-gray-300">No preset yet.</DropdownMenuLabel>
|
||||
)}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<EditPresetDialog
|
||||
open={presetModelVisible}
|
||||
onOpenChange={setPresetModelVisible}
|
||||
preset={preset}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
78
client/src/components/Input/Endpoints/PresetItem.jsx
Normal file
78
client/src/components/Input/Endpoints/PresetItem.jsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import React from 'react';
|
||||
import { DropdownMenuRadioItem } from '../../ui/DropdownMenu.tsx';
|
||||
import EditIcon from '../../svg/EditIcon';
|
||||
import TrashIcon from '../../svg/TrashIcon';
|
||||
import getIcon from '~/utils/getIcon';
|
||||
|
||||
export default function PresetItem({ preset = {}, value, onSelect, onChangePreset, onDeletePreset }) {
|
||||
const { endpoint } = preset;
|
||||
|
||||
const icon = getIcon({
|
||||
size: 20,
|
||||
endpoint: preset?.endpoint,
|
||||
error: false,
|
||||
className: 'mr-2'
|
||||
});
|
||||
|
||||
const getPresetTitle = () => {
|
||||
let _title = `${endpoint}`;
|
||||
|
||||
if (endpoint === 'azureOpenAI' || endpoint === 'openAI') {
|
||||
const { chatGptLabel, model } = preset;
|
||||
if (model) _title += `: ${model}`;
|
||||
if (chatGptLabel) _title += ` as ${chatGptLabel}`;
|
||||
} else if (endpoint === 'bingAI') {
|
||||
const { jailbreak, toneStyle } = preset;
|
||||
if (toneStyle) _title += `: ${toneStyle}`;
|
||||
if (jailbreak) _title += ` as Sydney`;
|
||||
} else if (endpoint === 'chatGPTBrowser') {
|
||||
const { model } = preset;
|
||||
if (model) _title += `: ${model}`;
|
||||
} else if (endpoint === null) {
|
||||
null;
|
||||
} else {
|
||||
null;
|
||||
}
|
||||
return _title;
|
||||
};
|
||||
|
||||
// regular model
|
||||
return (
|
||||
<DropdownMenuRadioItem
|
||||
value={value}
|
||||
className="group dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
{icon}
|
||||
{preset?.title}
|
||||
<small className="ml-2">({getPresetTitle()})</small>
|
||||
|
||||
{/* <RenameButton
|
||||
twcss={`ml-auto mr-2 ${buttonClass.className}`}
|
||||
onRename={onRename}
|
||||
renaming={renaming}
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
renameHandler={renameHandler}
|
||||
/> */}
|
||||
<div className="flex w-4 flex-1" />
|
||||
<button
|
||||
className="invisible m-0 mr-1 rounded-md text-gray-400 hover:text-gray-700 group-hover:visible dark:text-gray-400 dark:hover:text-gray-200 "
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
onDeletePreset(preset);
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
<button
|
||||
className="invisible m-0 p-2 rounded-md text-gray-400 hover:text-gray-700 group-hover:visible dark:text-gray-400 dark:hover:text-gray-200 "
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
onChangePreset(preset);
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
</DropdownMenuRadioItem>
|
||||
);
|
||||
}
|
||||
19
client/src/components/Input/Endpoints/PresetItems.jsx
Normal file
19
client/src/components/Input/Endpoints/PresetItems.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import PresetItem from './PresetItem';
|
||||
|
||||
export default function PresetItems({ presets, onSelect, onChangePreset, onDeletePreset }) {
|
||||
return (
|
||||
<>
|
||||
{presets.map(preset => (
|
||||
<PresetItem
|
||||
key={preset?.presetId}
|
||||
value={preset}
|
||||
onSelect={onSelect}
|
||||
onChangePreset={onChangePreset}
|
||||
onDeletePreset={onDeletePreset}
|
||||
preset={preset}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<div className="hidden md:block px-3 pt-2 pb-1 text-center text-xs text-black/50 dark:text-white/50 md:px-4 md:pt-3 md:pb-4">
|
||||
<div className="hidden px-3 pt-2 pb-1 text-center text-xs text-black/50 dark:text-white/50 md:block md:px-4 md:pt-3 md:pb-4">
|
||||
<a
|
||||
href="https://github.com/danny-avila/chatgpt-clone"
|
||||
target="_blank"
|
||||
|
|
@ -11,8 +11,8 @@ export default function Footer() {
|
|||
>
|
||||
ChatGPT Clone
|
||||
</a>
|
||||
. Serves and searches all conversations reliably. All AI convos under one house. Pay per
|
||||
call and not per month (cents compared to dollars).
|
||||
. Serves and searches all conversations reliably. All AI convos under one house. Pay per call and not
|
||||
per month (cents compared to dollars).
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import ModelItem from './ModelItem';
|
||||
|
||||
export default function MenuItems({ models, onSelect }) {
|
||||
return (
|
||||
<>
|
||||
{models.map(modelItem => (
|
||||
<ModelItem
|
||||
key={modelItem._id}
|
||||
value={modelItem.value}
|
||||
onSelect={onSelect}
|
||||
model={modelItem}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import manualSWR from '~/utils/fetchers';
|
||||
import { Button } from '../../ui/Button.tsx';
|
||||
import { Input } from '../../ui/Input.tsx';
|
||||
import { Label } from '../../ui/Label.tsx';
|
||||
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '../../ui/Dialog.tsx';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
export default function ModelDialog({ mutate, setModelSave, handleSaveState }) {
|
||||
const { newConversation } = store.useConversation();
|
||||
|
||||
const [chatGptLabel, setChatGptLabel] = useState('');
|
||||
const [promptPrefix, setPromptPrefix] = useState('');
|
||||
const [saveText, setSaveText] = useState('Save');
|
||||
const [required, setRequired] = useState(false);
|
||||
const inputRef = useRef(null);
|
||||
const updateCustomGpt = manualSWR(`/api/customGpts/`, 'post');
|
||||
|
||||
const selectHandler = e => {
|
||||
if (chatGptLabel.length === 0) {
|
||||
e.preventDefault();
|
||||
setRequired(true);
|
||||
inputRef.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
handleSaveState(chatGptLabel.toLowerCase());
|
||||
|
||||
// Set new conversation
|
||||
newConversation({
|
||||
model: 'chatgptCustom',
|
||||
chatGptLabel,
|
||||
promptPrefix
|
||||
});
|
||||
};
|
||||
|
||||
const saveHandler = e => {
|
||||
e.preventDefault();
|
||||
setModelSave(true);
|
||||
const value = chatGptLabel.toLowerCase();
|
||||
|
||||
if (chatGptLabel.length === 0) {
|
||||
setRequired(true);
|
||||
inputRef.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
updateCustomGpt.trigger({ value, chatGptLabel, promptPrefix });
|
||||
|
||||
mutate();
|
||||
setSaveText(prev => prev + 'd!');
|
||||
setTimeout(() => {
|
||||
setSaveText('Save');
|
||||
}, 2500);
|
||||
|
||||
// dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
|
||||
newConversation({
|
||||
model: 'chatgptCustom',
|
||||
chatGptLabel,
|
||||
promptPrefix
|
||||
});
|
||||
};
|
||||
|
||||
// Commented by wtlyu
|
||||
// if (
|
||||
// chatGptLabel !== 'chatgptCustom' &&
|
||||
// modelMap[chatGptLabel.toLowerCase()] &&
|
||||
// !initial[chatGptLabel.toLowerCase()] &&
|
||||
// saveText === 'Save'
|
||||
// ) {
|
||||
// setSaveText('Update');
|
||||
// } else if (!modelMap[chatGptLabel.toLowerCase()] && saveText === 'Update') {
|
||||
// setSaveText('Save');
|
||||
// }
|
||||
|
||||
const requiredProp = required ? { required: true } : {};
|
||||
|
||||
return (
|
||||
<DialogContent className="shadow-2xl dark:bg-gray-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-gray-800 dark:text-white">Customize ChatGPT</DialogTitle>
|
||||
<DialogDescription className="text-gray-600 dark:text-gray-300">
|
||||
Note: important instructions are often better placed in your message rather than the prefix.{' '}
|
||||
<a
|
||||
href="https://platform.openai.com/docs/guides/chat/instructing-chat-models"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<u>More info here</u>
|
||||
</a>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="chatGptLabel"
|
||||
className="text-right"
|
||||
>
|
||||
Custom Name
|
||||
</Label>
|
||||
<Input
|
||||
id="chatGptLabel"
|
||||
value={chatGptLabel}
|
||||
ref={inputRef}
|
||||
onChange={e => setChatGptLabel(e.target.value)}
|
||||
placeholder="Set a custom name for ChatGPT"
|
||||
className=" col-span-3 shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none placeholder:text-gray-400 invalid:border-red-400 invalid:text-red-600 invalid:placeholder-red-600 invalid:placeholder-opacity-70 invalid:ring-opacity-10 focus:ring-0 focus:invalid:border-red-400 focus:invalid:ring-red-300 dark:border-none dark:bg-gray-700
|
||||
dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:invalid:border-red-600 dark:invalid:text-red-300 dark:invalid:placeholder-opacity-80 dark:focus:border-none dark:focus:border-transparent dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0 dark:focus:invalid:ring-red-600 dark:focus:invalid:ring-opacity-50"
|
||||
{...requiredProp}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="promptPrefix"
|
||||
className="text-right"
|
||||
>
|
||||
Prompt Prefix
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="promptPrefix"
|
||||
value={promptPrefix}
|
||||
onChange={e => setPromptPrefix(e.target.value)}
|
||||
placeholder="Set custom instructions. Defaults to: 'You are ChatGPT, a large language model trained by OpenAI.'"
|
||||
className="col-span-3 flex h-20 max-h-52 w-full resize-none rounded-md border border-gray-300 bg-transparent py-2 px-3 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-none dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-none dark:focus:border-transparent dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose className="dark:hover:gray-400 border-gray-700">Cancel</DialogClose>
|
||||
<Button
|
||||
style={{ backgroundColor: 'rgb(16, 163, 127)' }}
|
||||
onClick={saveHandler}
|
||||
className="inline-flex h-10 items-center justify-center rounded-md border-none py-2 px-4 text-sm font-semibold text-white transition-colors dark:text-gray-200"
|
||||
>
|
||||
{saveText}
|
||||
</Button>
|
||||
<DialogClose
|
||||
onClick={selectHandler}
|
||||
className="inline-flex h-10 items-center justify-center rounded-md border-none bg-gray-900 py-2 px-4 text-sm font-semibold text-white transition-colors hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900"
|
||||
>
|
||||
Select
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { DropdownMenuRadioItem } from '../../ui/DropdownMenu.tsx';
|
||||
import { Circle } from 'lucide-react';
|
||||
import { DialogTrigger } from '../../ui/Dialog.tsx';
|
||||
import RenameButton from '../../Conversations/RenameButton';
|
||||
import TrashIcon from '../../svg/TrashIcon';
|
||||
import manualSWR from '~/utils/fetchers';
|
||||
import { getIconOfModel } from '~/utils';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
export default function ModelItem({ model: _model, value, onSelect }) {
|
||||
const { name, model, _id: id, chatGptLabel = null, promptPrefix = null } = _model;
|
||||
const setCustomGPTModels = useSetRecoilState(store.customGPTModels);
|
||||
const currentConversation = useRecoilValue(store.conversation) || {};
|
||||
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const [currentName, setCurrentName] = useState(name);
|
||||
const [modelInput, setModelInput] = useState(name);
|
||||
const inputRef = useRef(null);
|
||||
const rename = manualSWR(`/api/customGpts`, 'post', res => {});
|
||||
const deleteCustom = manualSWR(`/api/customGpts/delete`, 'post', res => {
|
||||
const fetchedModels = res.data.map(modelItem => ({
|
||||
...modelItem,
|
||||
name: modelItem.chatGptLabel,
|
||||
model: 'chatgptCustom'
|
||||
}));
|
||||
|
||||
setCustomGPTModels(fetchedModels);
|
||||
});
|
||||
|
||||
const icon = getIconOfModel({
|
||||
size: 20,
|
||||
sender: chatGptLabel || model,
|
||||
isCreatedByUser: false,
|
||||
model,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
error: false,
|
||||
className: 'mr-2'
|
||||
});
|
||||
|
||||
if (model !== 'chatgptCustom')
|
||||
// regular model
|
||||
return (
|
||||
<DropdownMenuRadioItem
|
||||
value={value}
|
||||
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
{icon}
|
||||
{name}
|
||||
{model === 'chatgpt' && <sup>$</sup>}
|
||||
</DropdownMenuRadioItem>
|
||||
);
|
||||
else if (model === 'chatgptCustom' && chatGptLabel === null && promptPrefix === null)
|
||||
// base chatgptCustom model, click to add new chatgptCustom.
|
||||
return (
|
||||
<DialogTrigger className="w-full">
|
||||
<DropdownMenuRadioItem
|
||||
value={value}
|
||||
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
{icon}
|
||||
{name}
|
||||
<sup>$</sup>
|
||||
</DropdownMenuRadioItem>
|
||||
</DialogTrigger>
|
||||
);
|
||||
|
||||
// else: a chatgptCustom model
|
||||
const handleMouseOver = () => {
|
||||
setIsHovering(true);
|
||||
};
|
||||
|
||||
const handleMouseOut = () => {
|
||||
setIsHovering(false);
|
||||
};
|
||||
|
||||
const renameHandler = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setRenaming(true);
|
||||
setTimeout(() => {
|
||||
inputRef.current.focus();
|
||||
}, 25);
|
||||
};
|
||||
|
||||
const onRename = e => {
|
||||
e.preventDefault();
|
||||
setRenaming(false);
|
||||
if (modelInput === name) {
|
||||
return;
|
||||
}
|
||||
rename.trigger({
|
||||
prevLabel: currentName,
|
||||
chatGptLabel: modelInput,
|
||||
value: modelInput.toLowerCase()
|
||||
});
|
||||
setCurrentName(modelInput);
|
||||
};
|
||||
|
||||
const onDelete = async e => {
|
||||
e.preventDefault();
|
||||
await deleteCustom.trigger({ _id: id });
|
||||
onSelect('chatgpt');
|
||||
};
|
||||
|
||||
const handleKeyDown = e => {
|
||||
if (e.key === 'Enter') {
|
||||
onRename(e);
|
||||
}
|
||||
};
|
||||
|
||||
const buttonClass = {
|
||||
className:
|
||||
'invisible group-hover:visible z-50 rounded-md m-0 text-gray-400 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
|
||||
};
|
||||
|
||||
const itemClass = {
|
||||
className:
|
||||
'relative flex group cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none hover:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:hover:bg-slate-700 dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800'
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
value={value}
|
||||
className={itemClass.className}
|
||||
onClick={e => {
|
||||
if (isHovering) {
|
||||
return;
|
||||
}
|
||||
onSelect('chatgptCustom', value);
|
||||
}}
|
||||
>
|
||||
{currentConversation?.chatGptLabel === value && (
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{icon}
|
||||
|
||||
{renaming === true ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
key={id}
|
||||
type="text"
|
||||
className="pointer-events-auto z-50 m-0 mr-2 w-3/4 border border-blue-500 bg-transparent p-0 text-sm leading-tight outline-none"
|
||||
value={modelInput}
|
||||
onClick={e => e.stopPropagation()}
|
||||
onChange={e => setModelInput(e.target.value)}
|
||||
// onBlur={onRename}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
) : (
|
||||
<div className=" overflow-hidden">{modelInput}</div>
|
||||
)}
|
||||
|
||||
{value === 'chatgpt' && <sup>$</sup>}
|
||||
<RenameButton
|
||||
twcss={`ml-auto mr-2 ${buttonClass.className}`}
|
||||
onRename={onRename}
|
||||
renaming={renaming}
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
renameHandler={renameHandler}
|
||||
/>
|
||||
<button
|
||||
{...buttonClass}
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
onClick={onDelete}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import axios from 'axios';
|
||||
import ModelDialog from './ModelDialog';
|
||||
import MenuItems from './MenuItems';
|
||||
import { swr } from '~/utils/fetchers';
|
||||
import { getIconOfModel } from '~/utils';
|
||||
|
||||
import { Button } from '../../ui/Button.tsx';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '../../ui/DropdownMenu.tsx';
|
||||
import { Dialog } from '../../ui/Dialog.tsx';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
export default function ModelMenu() {
|
||||
const [modelSave, setModelSave] = useState(false);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const models = useRecoilValue(store.models);
|
||||
const availableModels = useRecoilValue(store.availableModels);
|
||||
const setCustomGPTModels = useSetRecoilState(store.customGPTModels);
|
||||
|
||||
const conversation = useRecoilValue(store.conversation) || {};
|
||||
const { model, promptPrefix, chatGptLabel, conversationId } = conversation;
|
||||
const { newConversation } = store.useConversation();
|
||||
|
||||
// fetch the list of saved chatgptCustom
|
||||
const { data, isLoading, mutate } = swr(`/api/customGpts`, res => {
|
||||
const fetchedModels = res.map(modelItem => ({
|
||||
...modelItem,
|
||||
name: modelItem.chatGptLabel,
|
||||
model: 'chatgptCustom'
|
||||
}));
|
||||
|
||||
setCustomGPTModels(fetchedModels);
|
||||
});
|
||||
|
||||
// useEffect(() => {
|
||||
// mutate();
|
||||
// try {
|
||||
// const lastSelected = JSON.parse(localStorage.getItem('model'));
|
||||
|
||||
// if (lastSelected === 'chatgptCustom') {
|
||||
// return;
|
||||
// } else if (initial[lastSelected]) {
|
||||
// dispatch(setModel(lastSelected));
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.log(err);
|
||||
// }
|
||||
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, []);
|
||||
|
||||
// update the default model when availableModels changes
|
||||
// typically, availableModels changes => modelsFilter or customGPTModels changes
|
||||
useEffect(() => {
|
||||
if (conversationId == 'new') {
|
||||
newConversation();
|
||||
}
|
||||
}, [availableModels]);
|
||||
|
||||
// save selected model to localstoreage
|
||||
useEffect(() => {
|
||||
if (model) localStorage.setItem('model', JSON.stringify({ model, chatGptLabel, promptPrefix }));
|
||||
}, [model]);
|
||||
|
||||
// set the current model
|
||||
const onChange = (newModel, value = null) => {
|
||||
setMenuOpen(false);
|
||||
|
||||
if (!newModel) {
|
||||
return;
|
||||
} else if (newModel === model && value === chatGptLabel) {
|
||||
// bypass if not changed
|
||||
return;
|
||||
} else if (newModel === 'chatgptCustom' && value === null) {
|
||||
// return;
|
||||
} else if (newModel !== 'chatgptCustom') {
|
||||
newConversation({
|
||||
model: newModel,
|
||||
chatGptLabel: null,
|
||||
promptPrefix: null
|
||||
});
|
||||
} else if (newModel === 'chatgptCustom') {
|
||||
const targetModel = models.find(element => element.value == value);
|
||||
if (targetModel) {
|
||||
const chatGptLabel = targetModel?.chatGptLabel;
|
||||
const promptPrefix = targetModel?.promptPrefix;
|
||||
newConversation({
|
||||
model: newModel,
|
||||
chatGptLabel,
|
||||
promptPrefix
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onOpenChange = open => {
|
||||
mutate();
|
||||
if (!open) {
|
||||
setModelSave(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveState = value => {
|
||||
if (!modelSave) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCustomGPTModels(value);
|
||||
setModelSave(false);
|
||||
};
|
||||
|
||||
const defaultColorProps = [
|
||||
'text-gray-500',
|
||||
'hover:bg-gray-100',
|
||||
'hover:bg-opacity-20',
|
||||
'disabled:hover:bg-transparent',
|
||||
'dark:data-[state=open]:bg-gray-800',
|
||||
'dark:hover:bg-opacity-20',
|
||||
'dark:hover:bg-gray-900',
|
||||
'dark:hover:text-gray-400',
|
||||
'dark:disabled:hover:bg-transparent'
|
||||
];
|
||||
|
||||
const chatgptColorProps = [
|
||||
'text-green-700',
|
||||
'data-[state=open]:bg-green-100',
|
||||
'dark:text-emerald-300',
|
||||
'hover:bg-green-100',
|
||||
'disabled:hover:bg-transparent',
|
||||
'dark:data-[state=open]:bg-green-900',
|
||||
'dark:hover:bg-opacity-50',
|
||||
'dark:hover:bg-green-900',
|
||||
'dark:hover:text-gray-100',
|
||||
'dark:disabled:hover:bg-transparent'
|
||||
];
|
||||
|
||||
const colorProps = model === 'chatgpt' ? chatgptColorProps : defaultColorProps;
|
||||
const icon = getIconOfModel({
|
||||
size: 32,
|
||||
sender: chatGptLabel || model,
|
||||
isCreatedByUser: false,
|
||||
model,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
error: false,
|
||||
button: true
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={onOpenChange}>
|
||||
<DropdownMenu
|
||||
open={menuOpen}
|
||||
onOpenChange={setMenuOpen}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
// style={{backgroundColor: 'rgb(16, 163, 127)'}}
|
||||
className={`absolute top-[0.25px] mb-0 ml-1 items-center rounded-md border-0 p-1 outline-none md:ml-0 ${colorProps.join(
|
||||
' '
|
||||
)} focus:ring-0 focus:ring-offset-0 disabled:top-[0.25px] dark:data-[state=open]:bg-opacity-50 md:top-1 md:left-1 md:pl-1 md:disabled:top-1`}
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-56 dark:bg-gray-700"
|
||||
onCloseAutoFocus={event => event.preventDefault()}
|
||||
>
|
||||
<DropdownMenuLabel className="dark:text-gray-300">Select a Model</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuRadioGroup
|
||||
value={chatGptLabel || model}
|
||||
onValueChange={onChange}
|
||||
className="overflow-y-auto"
|
||||
>
|
||||
{availableModels.length ? (
|
||||
<MenuItems
|
||||
models={availableModels}
|
||||
onSelect={onChange}
|
||||
/>
|
||||
) : (
|
||||
<DropdownMenuLabel className="dark:text-gray-300">No model available.</DropdownMenuLabel>
|
||||
)}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ModelDialog
|
||||
mutate={mutate}
|
||||
setModelSave={setModelSave}
|
||||
handleSaveState={handleSaveState}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
142
client/src/components/Input/OpenAIOptions/index.jsx
Normal file
142
client/src/components/Input/OpenAIOptions/index.jsx
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Settings2 } from 'lucide-react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import SelectDropdown from '../../ui/SelectDropDown';
|
||||
import EndpointOptionsPopover from '../../Endpoints/EndpointOptionsPopover';
|
||||
import SaveAsPresetDialog from '../../Endpoints/SaveAsPresetDialog';
|
||||
import { Button } from '../../ui/Button.tsx';
|
||||
import Settings from '../../Endpoints/OpenAI/Settings.jsx';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
function OpenAIOptions() {
|
||||
const [advancedMode, setAdvancedMode] = useState(false);
|
||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState(false);
|
||||
|
||||
const [conversation, setConversation] = useRecoilState(store.conversation) || {};
|
||||
const { endpoint, conversationId } = conversation;
|
||||
const { model, chatGptLabel, promptPrefix, temperature, top_p, presence_penalty, frequency_penalty } =
|
||||
conversation;
|
||||
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (endpoint !== 'openAI') return;
|
||||
|
||||
// const mustInAdvancedMode =
|
||||
// chatGptLabel !== null ||
|
||||
// promptPrefix !== null ||
|
||||
// temperature !== 1 ||
|
||||
// top_p !== 1 ||
|
||||
// presence_penalty !== 0 ||
|
||||
// frequency_penalty !== 0;
|
||||
|
||||
// if (mustInAdvancedMode && !advancedMode) setAdvancedMode(true);
|
||||
// }, [conversation, advancedMode]);
|
||||
|
||||
if (endpoint !== 'openAI') return null;
|
||||
if (conversationId !== 'new') return null;
|
||||
|
||||
const models = endpointsConfig?.['openAI']?.['availableModels'] || [];
|
||||
|
||||
const triggerAdvancedMode = () => setAdvancedMode(prev => !prev);
|
||||
|
||||
const switchToSimpleMode = () => {
|
||||
// setConversation(prevState => ({
|
||||
// ...prevState,
|
||||
// chatGptLabel: null,
|
||||
// promptPrefix: null,
|
||||
// temperature: 1,
|
||||
// top_p: 1,
|
||||
// presence_penalty: 0,
|
||||
// frequency_penalty: 0
|
||||
// }));
|
||||
setAdvancedMode(false);
|
||||
};
|
||||
|
||||
const saveAsPreset = () => {
|
||||
setSaveAsDialogShow(true);
|
||||
};
|
||||
|
||||
const setOption = param => newValue => {
|
||||
let update = {};
|
||||
update[param] = newValue;
|
||||
setConversation(prevState => ({
|
||||
...prevState,
|
||||
...update
|
||||
}));
|
||||
};
|
||||
|
||||
const cardStyle =
|
||||
'transition-colors shadow-md rounded-md min-w-[75px] font-normal bg-white border-black/10 hover:border-black/10 focus:border-black/10 dark:border-black/10 dark:hover:border-black/10 dark:focus:border-black/10 border dark:bg-gray-700 text-black dark:text-white';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
'openAIOptions-simple-container flex w-full flex-wrap items-center justify-center gap-2' +
|
||||
(!advancedMode ? ' show' : '')
|
||||
}
|
||||
>
|
||||
{/* <ModelSelect
|
||||
model={model}
|
||||
availableModels={availableModels}
|
||||
onChange={setOption('model')}
|
||||
type="button"
|
||||
className={cn(
|
||||
cardStyle,
|
||||
' z-50 flex h-[40px] items-center justify-center px-4 hover:bg-slate-50 data-[state=open]:bg-slate-50 dark:hover:bg-gray-600 dark:data-[state=open]:bg-gray-600'
|
||||
)}
|
||||
/> */}
|
||||
<SelectDropdown
|
||||
value={model}
|
||||
setValue={setOption('model')}
|
||||
availableValues={models}
|
||||
showAbove={true}
|
||||
showLabel={false}
|
||||
className={cn(
|
||||
cardStyle,
|
||||
'min-w-48 z-50 flex h-[40px] w-48 flex-none items-center justify-center px-4 ring-0 hover:cursor-pointer hover:bg-slate-50 focus:ring-0 focus:ring-offset-0 data-[state=open]:bg-slate-50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:data-[state=open]:bg-gray-600'
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
className={cn(
|
||||
cardStyle,
|
||||
'min-w-4 z-50 flex h-[40px] flex-none items-center justify-center px-4 hover:bg-slate-50 focus:ring-0 focus:ring-offset-0 dark:hover:bg-gray-600'
|
||||
)}
|
||||
onClick={triggerAdvancedMode}
|
||||
>
|
||||
<Settings2 className="w-4 text-gray-600 dark:text-white" />
|
||||
</Button>
|
||||
</div>
|
||||
<EndpointOptionsPopover
|
||||
content={
|
||||
<div className="px-4 py-4">
|
||||
<Settings
|
||||
model={model}
|
||||
chatGptLabel={chatGptLabel}
|
||||
promptPrefix={promptPrefix}
|
||||
temperature={temperature}
|
||||
topP={top_p}
|
||||
freqP={presence_penalty}
|
||||
presP={frequency_penalty}
|
||||
setOption={setOption}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
visible={advancedMode}
|
||||
saveAsPreset={saveAsPreset}
|
||||
switchToSimpleMode={switchToSimpleMode}
|
||||
/>
|
||||
<SaveAsPresetDialog
|
||||
open={saveAsDialogShow}
|
||||
onOpenChange={setSaveAsDialogShow}
|
||||
preset={conversation}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default OpenAIOptions;
|
||||
|
|
@ -1,65 +1,78 @@
|
|||
import React from 'react';
|
||||
import StopGeneratingIcon from '../svg/StopGeneratingIcon';
|
||||
|
||||
export default function SubmitButton({ submitMessage, disabled, isSubmitting }) {
|
||||
export default function SubmitButton({ submitMessage, handleStopGenerating, disabled, isSubmitting }) {
|
||||
const clickHandler = e => {
|
||||
e.preventDefault();
|
||||
submitMessage();
|
||||
};
|
||||
|
||||
if (isSubmitting) {
|
||||
if (isSubmitting)
|
||||
return (
|
||||
<button
|
||||
className="absolute bottom-0 right-1 h-[100%] w-[40px] rounded-md p-1 text-gray-500 hover:bg-gray-100 disabled:hover:bg-transparent dark:hover:bg-gray-900 dark:hover:text-gray-400 dark:disabled:hover:bg-transparent md:right-2"
|
||||
disabled
|
||||
onClick={handleStopGenerating}
|
||||
type="button"
|
||||
className="group absolute bottom-0 right-0 flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
|
||||
>
|
||||
<div className="text-2xl">
|
||||
<span style={{ maxWidth: 5.5, display: 'inline-grid' }}>·</span>
|
||||
<span
|
||||
className="blink"
|
||||
style={{ maxWidth: 5.5, display: 'inline-grid' }}
|
||||
>
|
||||
·
|
||||
</span>
|
||||
<span
|
||||
className="blink2"
|
||||
style={{ maxWidth: 5.5, display: 'inline-grid' }}
|
||||
>
|
||||
·
|
||||
</span>
|
||||
<div className="m-1 mr-0 rounded-md p-2 pt-[10px] pb-[10px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
|
||||
<StopGeneratingIcon />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
// // previous three dot animation
|
||||
// return (
|
||||
// <button
|
||||
// className="absolute bottom-0 right-1 h-[100%] w-[40px] rounded-md p-1 text-gray-500 hover:bg-gray-100 disabled:hover:bg-transparent dark:hover:bg-gray-900 dark:hover:text-gray-400 dark:disabled:hover:bg-transparent md:right-2"
|
||||
// disabled
|
||||
// >
|
||||
// <div className="text-2xl">
|
||||
// <span style={{ maxWidth: 5.5, display: 'inline-grid' }}>·</span>
|
||||
// <span
|
||||
// className="blink"
|
||||
// style={{ maxWidth: 5.5, display: 'inline-grid' }}
|
||||
// >
|
||||
// ·
|
||||
// </span>
|
||||
// <span
|
||||
// className="blink2"
|
||||
// style={{ maxWidth: 5.5, display: 'inline-grid' }}
|
||||
// >
|
||||
// ·
|
||||
// </span>
|
||||
// </div>
|
||||
// </button>
|
||||
// );
|
||||
else
|
||||
return (
|
||||
<button
|
||||
onClick={clickHandler}
|
||||
disabled={disabled}
|
||||
className="group absolute bottom-0 right-0 flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
|
||||
>
|
||||
<div className="m-1 mr-0 rounded-md p-2 pt-[10px] pb-[10px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-1 h-4 w-4 "
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line
|
||||
x1="22"
|
||||
y1="2"
|
||||
x2="11"
|
||||
y2="13"
|
||||
/>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
onClick={clickHandler}
|
||||
disabled={disabled}
|
||||
className="group absolute bottom-0 right-0 flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
|
||||
>
|
||||
<div className="m-1 mr-0 rounded-md p-2 pt-[10px] pb-[10px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-1 h-4 w-4 "
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line
|
||||
x1="22"
|
||||
y1="2"
|
||||
x2="11"
|
||||
y2="13"
|
||||
/>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useRecoilValue, useRecoilState } from 'recoil';
|
||||
import SubmitButton from './SubmitButton';
|
||||
import AdjustToneButton from './AdjustToneButton';
|
||||
import BingStyles from './BingStyles';
|
||||
import ModelMenu from './Models/ModelMenu';
|
||||
import OpenAIOptions from './OpenAIOptions';
|
||||
import ChatGPTOptions from './ChatGPTOptions';
|
||||
import BingAIOptions from './BingAIOptions';
|
||||
// import BingStyles from './BingStyles';
|
||||
import EndpointMenu from './Endpoints/NewConversationMenu';
|
||||
import Footer from './Footer';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import RegenerateIcon from '../svg/RegenerateIcon';
|
||||
import StopGeneratingIcon from '../svg/StopGeneratingIcon';
|
||||
import { useMessageHandler } from '../../utils/handleSubmit';
|
||||
|
||||
import store from '~/store';
|
||||
|
|
@ -18,7 +18,6 @@ export default function TextChat({ isSearchView = false }) {
|
|||
|
||||
const conversation = useRecoilValue(store.conversation);
|
||||
const latestMessage = useRecoilValue(store.latestMessage);
|
||||
const messages = useRecoilValue(store.messages);
|
||||
const [text, setText] = useRecoilState(store.text);
|
||||
// const [text, setText] = useState('');
|
||||
|
||||
|
|
@ -27,45 +26,41 @@ export default function TextChat({ isSearchView = false }) {
|
|||
// TODO: do we need this?
|
||||
const disabled = false;
|
||||
|
||||
const { ask, regenerate, stopGenerating } = useMessageHandler();
|
||||
const { ask, stopGenerating } = useMessageHandler();
|
||||
|
||||
const bingStylesRef = useRef(null);
|
||||
const [showBingToneSetting, setShowBingToneSetting] = useState(false);
|
||||
// const bingStylesRef = useRef(null);
|
||||
// const [showBingToneSetting, setShowBingToneSetting] = useState(false);
|
||||
|
||||
const isNotAppendable = latestMessage?.cancelled || latestMessage?.error;
|
||||
|
||||
// auto focus to input, when enter a conversation.
|
||||
useEffect(() => {
|
||||
if (conversation?.conversationId !== 'search') inputRef.current?.focus();
|
||||
setText('');
|
||||
// setText('');
|
||||
}, [conversation?.conversationId]);
|
||||
|
||||
// controls the height of Bing tone style tabs
|
||||
useEffect(() => {
|
||||
if (!inputRef.current) {
|
||||
return; // wait for the ref to be available
|
||||
}
|
||||
// // controls the height of Bing tone style tabs
|
||||
// useEffect(() => {
|
||||
// if (!inputRef.current) {
|
||||
// return; // wait for the ref to be available
|
||||
// }
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const newHeight = inputRef.current.clientHeight;
|
||||
if (newHeight >= 24) {
|
||||
// 24 is the default height of the input
|
||||
bingStylesRef.current.style.bottom = 15 + newHeight + 'px';
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(inputRef.current);
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [inputRef]);
|
||||
// const resizeObserver = new ResizeObserver(() => {
|
||||
// const newHeight = inputRef.current.clientHeight;
|
||||
// if (newHeight >= 24) {
|
||||
// // 24 is the default height of the input
|
||||
// // bingStylesRef.current.style.bottom = 15 + newHeight + 'px';
|
||||
// }
|
||||
// });
|
||||
// resizeObserver.observe(inputRef.current);
|
||||
// return () => resizeObserver.disconnect();
|
||||
// }, [inputRef]);
|
||||
|
||||
const submitMessage = () => {
|
||||
ask({ text });
|
||||
setText('');
|
||||
};
|
||||
|
||||
const handleRegenerate = () => {
|
||||
if (latestMessage && !latestMessage?.isCreatedByUser) regenerate(latestMessage);
|
||||
};
|
||||
|
||||
const handleStopGenerating = () => {
|
||||
stopGenerating();
|
||||
};
|
||||
|
|
@ -124,77 +119,63 @@ export default function TextChat({ isSearchView = false }) {
|
|||
return '';
|
||||
};
|
||||
|
||||
const handleBingToneSetting = () => {
|
||||
setShowBingToneSetting(show => !show);
|
||||
};
|
||||
// const handleBingToneSetting = () => {
|
||||
// setShowBingToneSetting(show => !show);
|
||||
// };
|
||||
|
||||
if (isSearchView) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="input-panel md:bg-vert-light-gradient dark:md:bg-vert-dark-gradient fixed bottom-0 left-0 w-full border-t bg-white py-2 dark:border-white/20 dark:bg-gray-800 md:absolute md:border-t-0 md:border-transparent md:bg-transparent md:dark:border-transparent md:dark:bg-transparent">
|
||||
<form className="stretch mx-2 flex flex-row gap-3 last:mb-2 md:pt-2 md:last:mb-6 lg:mx-auto lg:max-w-3xl lg:pt-6">
|
||||
<div className="relative flex h-full flex-1 md:flex-col">
|
||||
<span className="order-last ml-1 flex justify-center gap-0 md:order-none md:m-auto md:mb-2 md:w-full md:gap-2">
|
||||
<BingStyles
|
||||
ref={bingStylesRef}
|
||||
show={showBingToneSetting}
|
||||
/>
|
||||
{isSubmitting ? (
|
||||
<button
|
||||
onClick={handleStopGenerating}
|
||||
className="input-panel-button btn btn-neutral flex justify-center gap-2 border-0 md:border"
|
||||
type="button"
|
||||
>
|
||||
<StopGeneratingIcon />
|
||||
<span className="hidden md:block">Stop generating</span>
|
||||
</button>
|
||||
) : latestMessage && !latestMessage?.isCreatedByUser ? (
|
||||
<button
|
||||
onClick={handleRegenerate}
|
||||
className="input-panel-button btn btn-neutral flex justify-center gap-2 border-0 md:border"
|
||||
type="button"
|
||||
>
|
||||
<RegenerateIcon />
|
||||
<span className="hidden md:block">Regenerate response</span>
|
||||
</button>
|
||||
) : null}
|
||||
</span>
|
||||
<div
|
||||
className={`relative flex flex-grow flex-col rounded-md border border-black/10 ${
|
||||
disabled ? 'bg-gray-100' : 'bg-white'
|
||||
} py-2 shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 ${
|
||||
disabled ? 'dark:bg-gray-900' : 'dark:bg-gray-700'
|
||||
} dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] md:py-3 md:pl-4`}
|
||||
>
|
||||
<ModelMenu />
|
||||
<TextareaAutosize
|
||||
tabIndex="0"
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
// style={{maxHeight: '200px', height: '24px', overflowY: 'hidden'}}
|
||||
rows="1"
|
||||
value={disabled || isNotAppendable ? '' : text}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={changeHandler}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
placeholder={getPlaceholderText()}
|
||||
disabled={disabled || isNotAppendable}
|
||||
className="m-0 h-auto max-h-52 resize-none overflow-auto border-0 bg-transparent p-0 pl-12 pr-8 leading-6 placeholder:text-sm placeholder:text-gray-600 focus:outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder:text-gray-500 md:pl-8"
|
||||
/>
|
||||
<SubmitButton
|
||||
submitMessage={submitMessage}
|
||||
disabled={disabled || isNotAppendable}
|
||||
/>
|
||||
{messages?.length && conversation?.model === 'sydney' ? (
|
||||
<AdjustToneButton onClick={handleBingToneSetting} />
|
||||
) : null}
|
||||
<div className="fixed bottom-0 left-0 w-full md:absolute">
|
||||
<div className="relative py-2 md:mb-[-16px] md:py-4 lg:mb-[-32px]">
|
||||
<span className="flex w-full flex-col items-center justify-center gap-0 md:order-none md:m-auto md:gap-2">
|
||||
<OpenAIOptions />
|
||||
<ChatGPTOptions />
|
||||
<BingAIOptions />
|
||||
</span>
|
||||
</div>
|
||||
<div className="input-panel md:bg-vert-light-gradient dark:md:bg-vert-dark-gradient relative w-full border-t bg-white py-2 dark:border-white/20 dark:bg-gray-800 md:border-t-0 md:border-transparent md:bg-transparent md:dark:border-transparent md:dark:bg-transparent">
|
||||
<form className="stretch mx-2 flex flex-row gap-3 last:mb-2 md:pt-2 md:last:mb-6 lg:mx-auto lg:max-w-3xl lg:pt-6">
|
||||
<div className="relative flex h-full flex-1 md:flex-col">
|
||||
<div
|
||||
className={`relative flex flex-grow flex-col rounded-md border border-black/10 ${
|
||||
disabled ? 'bg-gray-100' : 'bg-white'
|
||||
} py-2 shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 ${
|
||||
disabled ? 'dark:bg-gray-900' : 'dark:bg-gray-700'
|
||||
} dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] md:py-3 md:pl-4`}
|
||||
>
|
||||
<EndpointMenu />
|
||||
<TextareaAutosize
|
||||
tabIndex="0"
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
// style={{maxHeight: '200px', height: '24px', overflowY: 'hidden'}}
|
||||
rows="1"
|
||||
value={disabled || isNotAppendable ? '' : text}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={changeHandler}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
placeholder={getPlaceholderText()}
|
||||
disabled={disabled || isNotAppendable}
|
||||
className="m-0 h-auto max-h-52 resize-none overflow-auto border-0 bg-transparent p-0 pl-12 pr-8 leading-6 placeholder:text-sm placeholder:text-gray-600 focus:outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder:text-gray-500 md:pl-8"
|
||||
/>
|
||||
<SubmitButton
|
||||
submitMessage={submitMessage}
|
||||
handleStopGenerating={handleStopGenerating}
|
||||
disabled={disabled || isNotAppendable}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
{/* {messages?.length && conversation?.model === 'sydney' ? (
|
||||
<AdjustToneButton onClick={handleBingToneSetting} />
|
||||
) : null} */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<Footer />
|
||||
</form>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { SSE } from '~/utils/sse.mjs';
|
||||
import { useMessageHandler } from '../../utils/handleSubmit';
|
||||
import createPayload from '~/utils/createPayload';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
export default function MessageHandler({ messages }) {
|
||||
const [submission, setSubmission] = useRecoilState(store.submission);
|
||||
const [isSubmitting, setIsSubmitting] = useRecoilState(store.isSubmitting);
|
||||
export default function MessageHandler() {
|
||||
const submission = useRecoilValue(store.submission);
|
||||
const setIsSubmitting = useSetRecoilState(store.isSubmitting);
|
||||
const setMessages = useSetRecoilState(store.messages);
|
||||
const setConversation = useSetRecoilState(store.conversation);
|
||||
const resetLatestMessage = useResetRecoilState(store.latestMessage);
|
||||
|
|
@ -105,10 +104,9 @@ export default function MessageHandler({ messages }) {
|
|||
};
|
||||
|
||||
const finalHandler = (data, submission) => {
|
||||
const { conversation, messages, message, initialResponse, isRegenerate = false } = submission;
|
||||
const { messages, isRegenerate = false } = submission;
|
||||
|
||||
const { requestMessage, responseMessage } = data;
|
||||
const { conversationId } = requestMessage;
|
||||
const { requestMessage, responseMessage, conversation } = data;
|
||||
|
||||
// update the messages
|
||||
if (isRegenerate) setMessages([...messages, responseMessage]);
|
||||
|
|
@ -127,66 +125,14 @@ export default function MessageHandler({ messages }) {
|
|||
}, 5000);
|
||||
}
|
||||
|
||||
const { model, chatGptLabel, promptPrefix } = conversation;
|
||||
const isBing = model === 'bingai' || model === 'sydney';
|
||||
|
||||
if (!isBing) {
|
||||
const { title } = data;
|
||||
const { conversationId } = responseMessage;
|
||||
setConversation(prevState => ({
|
||||
...prevState,
|
||||
title,
|
||||
conversationId,
|
||||
jailbreakConversationId: null,
|
||||
conversationSignature: null,
|
||||
clientId: null,
|
||||
invocationId: null,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
latestMessage: null
|
||||
}));
|
||||
} else if (model === 'bingai') {
|
||||
const { title } = data;
|
||||
const { conversationSignature, clientId, conversationId, invocationId } = responseMessage;
|
||||
setConversation(prevState => ({
|
||||
...prevState,
|
||||
title,
|
||||
conversationId,
|
||||
jailbreakConversationId: null,
|
||||
conversationSignature,
|
||||
clientId,
|
||||
invocationId,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
latestMessage: null
|
||||
}));
|
||||
} else if (model === 'sydney') {
|
||||
const { title } = data;
|
||||
const {
|
||||
jailbreakConversationId,
|
||||
parentMessageId,
|
||||
conversationSignature,
|
||||
clientId,
|
||||
conversationId,
|
||||
invocationId
|
||||
} = responseMessage;
|
||||
setConversation(prevState => ({
|
||||
...prevState,
|
||||
title,
|
||||
conversationId,
|
||||
jailbreakConversationId,
|
||||
conversationSignature,
|
||||
clientId,
|
||||
invocationId,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
latestMessage: null
|
||||
}));
|
||||
}
|
||||
setConversation(prevState => ({
|
||||
...prevState,
|
||||
...conversation
|
||||
}));
|
||||
};
|
||||
|
||||
const errorHandler = (data, submission) => {
|
||||
const { conversation, messages, message, initialResponse, isRegenerate = false } = submission;
|
||||
const { messages, message } = submission;
|
||||
|
||||
console.log('Error:', data);
|
||||
const errorResponse = {
|
||||
|
|
@ -203,7 +149,6 @@ export default function MessageHandler({ messages }) {
|
|||
if (submission === null) return;
|
||||
if (Object.keys(submission).length === 0) return;
|
||||
|
||||
const { messages, initialResponse, isRegenerate = false } = submission;
|
||||
let { message } = submission;
|
||||
|
||||
const { server, payload } = createPayload(submission);
|
||||
|
|
@ -224,9 +169,6 @@ export default function MessageHandler({ messages }) {
|
|||
if (data.created) {
|
||||
message = {
|
||||
...data.message,
|
||||
model: message?.model,
|
||||
chatGptLabel: message?.chatGptLabel,
|
||||
promptPrefix: message?.promptPrefix,
|
||||
overrideParentMessageId: message?.overrideParentMessageId
|
||||
};
|
||||
createdHandler(data, { ...submission, message });
|
||||
|
|
@ -245,7 +187,7 @@ export default function MessageHandler({ messages }) {
|
|||
|
||||
events.onopen = () => console.log('connection is opened');
|
||||
|
||||
events.oncancel = e => cancelHandler(latestResponseText, { ...submission, message });
|
||||
events.oncancel = () => cancelHandler(latestResponseText, { ...submission, message });
|
||||
|
||||
events.onerror = function (e) {
|
||||
console.log('error in opening conn.');
|
||||
|
|
|
|||
|
|
@ -1,25 +1,70 @@
|
|||
import React from 'react';
|
||||
// import Clipboard from '../svg/Clipboard';
|
||||
import Clipboard from '../svg/Clipboard';
|
||||
import EditIcon from '../svg/EditIcon';
|
||||
import RegenerateIcon from '../svg/RegenerateIcon';
|
||||
|
||||
export default function HoverButtons({ visible, onClick, model }) {
|
||||
const isBing = model === 'bingai';
|
||||
const enabled = !isBing;
|
||||
export default function HoverButtons({
|
||||
isEditting,
|
||||
enterEdit,
|
||||
copyToClipboard,
|
||||
conversation,
|
||||
isSubmitting,
|
||||
message,
|
||||
regenerate
|
||||
}) {
|
||||
const { endpoint, jailbreak = false } = conversation;
|
||||
|
||||
const branchingSupported =
|
||||
// azureOpenAI, openAI, chatGPTBrowser support branching, so edit enabled
|
||||
!!['azureOpenAI', 'openAI', 'chatGPTBrowser'].find(e => e === endpoint) ||
|
||||
// Sydney in bingAI supports branching, so edit enabled
|
||||
(endpoint === 'bingAI' && jailbreak);
|
||||
|
||||
const editEnabled =
|
||||
!message?.error &&
|
||||
message?.isCreatedByUser &&
|
||||
!message?.searchResult &&
|
||||
!isEditting &&
|
||||
branchingSupported;
|
||||
|
||||
// for now, once branching is supported, regerate will be enabled
|
||||
const regenerateEnabled =
|
||||
// !message?.error &&
|
||||
!message?.isCreatedByUser && !message?.searchResult && !isEditting && !isSubmitting && branchingSupported;
|
||||
|
||||
return (
|
||||
<div className="visible mt-2 flex justify-center gap-3 self-end text-gray-400 md:gap-4 lg:absolute lg:top-0 lg:right-0 lg:mt-0 lg:translate-x-full lg:gap-1 lg:self-center lg:pl-2">
|
||||
{(visible&&enabled)?(
|
||||
<>
|
||||
<button className="resubmit-edit-button rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible"
|
||||
onClick={onClick}>
|
||||
{/* <button className="rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400"> */}
|
||||
<EditIcon />
|
||||
</button>
|
||||
</>
|
||||
):null}
|
||||
{/* <button className="rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400">
|
||||
<Clipboard />
|
||||
</button> */}
|
||||
{editEnabled ? (
|
||||
<button
|
||||
className="hover-button rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible"
|
||||
onClick={enterEdit}
|
||||
type="button"
|
||||
title="edit"
|
||||
>
|
||||
{/* <button className="rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400"> */}
|
||||
<EditIcon />
|
||||
</button>
|
||||
) : null}
|
||||
{regenerateEnabled ? (
|
||||
<button
|
||||
className="hover-button rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible"
|
||||
onClick={regenerate}
|
||||
type="button"
|
||||
title="regenerate"
|
||||
>
|
||||
{/* <button className="rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400"> */}
|
||||
<RegenerateIcon />
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
className="hover-button rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible"
|
||||
onClick={copyToClipboard}
|
||||
type="button"
|
||||
title="copy to clipboard"
|
||||
>
|
||||
<Clipboard />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState, useResetRecoilState } from 'recoil';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import SubRow from './Content/SubRow';
|
||||
import Content from './Content/Content';
|
||||
import MultiMessage from './MultiMessage';
|
||||
import HoverButtons from './HoverButtons';
|
||||
import SiblingSwitch from './SiblingSwitch';
|
||||
import { fetchById } from '~/utils/fetchers';
|
||||
import { getIconOfModel } from '~/utils';
|
||||
import getIcon from '~/utils/getIcon';
|
||||
import { useMessageHandler } from '~/utils/handleSubmit';
|
||||
|
||||
import store from '~/store';
|
||||
|
|
@ -23,32 +24,15 @@ export default function Message({
|
|||
}) {
|
||||
const isSubmitting = useRecoilValue(store.isSubmitting);
|
||||
const setLatestMessage = useSetRecoilState(store.latestMessage);
|
||||
const { model, chatGptLabel, promptPrefix } = conversation;
|
||||
// const { model, chatGptLabel, promptPrefix } = conversation;
|
||||
const [abortScroll, setAbort] = useState(false);
|
||||
const {
|
||||
sender,
|
||||
text,
|
||||
searchResult,
|
||||
isCreatedByUser,
|
||||
error,
|
||||
submitting,
|
||||
model: messageModel,
|
||||
chatGptLabel: messageChatGptLabel,
|
||||
searchResult: isSearchResult
|
||||
} = message;
|
||||
const { text, searchResult, isCreatedByUser, error, submitting } = message;
|
||||
const textEditor = useRef(null);
|
||||
const last = !message?.children?.length;
|
||||
const edit = message.messageId == currentEditId;
|
||||
const { ask } = useMessageHandler();
|
||||
const { ask, regenerate } = useMessageHandler();
|
||||
const { switchToConversation } = store.useConversation();
|
||||
const blinker = submitting && isSubmitting;
|
||||
const generateCursor = useCallback(() => {
|
||||
if (!blinker) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return <span className="result-streaming">█</span>;
|
||||
}, [blinker]);
|
||||
|
||||
useEffect(() => {
|
||||
if (blinker && !abortScroll) {
|
||||
|
|
@ -77,14 +61,9 @@ export default function Message({
|
|||
'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800'
|
||||
};
|
||||
|
||||
const icon = getIconOfModel({
|
||||
sender,
|
||||
isCreatedByUser,
|
||||
model: isSearchResult ? messageModel : model,
|
||||
searchResult,
|
||||
chatGptLabel: isSearchResult ? messageChatGptLabel : chatGptLabel,
|
||||
promptPrefix,
|
||||
error
|
||||
const icon = getIcon({
|
||||
...conversation,
|
||||
...message
|
||||
});
|
||||
|
||||
if (!isCreatedByUser)
|
||||
|
|
@ -109,6 +88,14 @@ export default function Message({
|
|||
enterEdit(true);
|
||||
};
|
||||
|
||||
const regenerateMessage = () => {
|
||||
if (!isSubmitting && !message?.isCreatedByUser) regenerate(message);
|
||||
};
|
||||
|
||||
const copyToClipboard = () => {
|
||||
copy(message?.text);
|
||||
};
|
||||
|
||||
const clickSearchResult = async () => {
|
||||
if (!searchResult) return;
|
||||
const convoResponse = await fetchById('convos', message.conversationId);
|
||||
|
|
@ -199,9 +186,13 @@ export default function Message({
|
|||
)}
|
||||
</div>
|
||||
<HoverButtons
|
||||
model={model}
|
||||
visible={!error && isCreatedByUser && !edit && !searchResult}
|
||||
onClick={() => enterEdit()}
|
||||
isEditting={edit}
|
||||
isSubmitting={isSubmitting}
|
||||
message={message}
|
||||
conversation={conversation}
|
||||
enterEdit={() => enterEdit()}
|
||||
regenerate={() => regenerateMessage()}
|
||||
copyToClipboard={() => copyToClipboard()}
|
||||
/>
|
||||
<SubRow subclasses="switch-container">
|
||||
<SiblingSwitch
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import React, { useRef, useEffect, useState } from 'react';
|
||||
|
||||
const MessageBar = ({ children, dynamicProps, handleWheel, clickSearchResult }) => {
|
||||
const ref = useRef(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
observer.unobserve(ref.current);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
observer.observe(ref.current);
|
||||
|
||||
return () => {
|
||||
observer.unobserve(ref.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...dynamicProps}
|
||||
onWheel={handleWheel}
|
||||
// onClick={clickSearchResult}
|
||||
|
||||
ref={ref}
|
||||
>
|
||||
{isVisible ? children : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageBar;
|
||||
71
client/src/components/Messages/MessageHeader.jsx
Normal file
71
client/src/components/Messages/MessageHeader.jsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import EndpointOptionsDialog from '../Endpoints/EndpointOptionsDialog';
|
||||
import { cn } from '~/utils/';
|
||||
import { Button } from '../ui/Button.tsx';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
const clipPromptPrefix = str => {
|
||||
if (typeof str !== 'string') {
|
||||
return null;
|
||||
} else if (str.length > 10) {
|
||||
return str.slice(0, 10) + '...';
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
const MessageHeader = ({ isSearchView = false }) => {
|
||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState(false);
|
||||
const conversation = useRecoilValue(store.conversation);
|
||||
const searchQuery = useRecoilValue(store.searchQuery);
|
||||
const { endpoint } = conversation;
|
||||
|
||||
const getConversationTitle = () => {
|
||||
if (isSearchView) return `Search: ${searchQuery}`;
|
||||
else {
|
||||
let _title = `${endpoint}`;
|
||||
|
||||
if (endpoint === 'azureOpenAI' || endpoint === 'openAI') {
|
||||
const { chatGptLabel, model } = conversation;
|
||||
if (model) _title += `: ${model}`;
|
||||
if (chatGptLabel) _title += ` as ${chatGptLabel}`;
|
||||
} else if (endpoint === 'bingAI') {
|
||||
const { jailbreak, toneStyle } = conversation;
|
||||
if (toneStyle) _title += `: ${toneStyle}`;
|
||||
if (jailbreak) _title += ` as Sydney`;
|
||||
} else if (endpoint === 'chatGPTBrowser') {
|
||||
const { model } = conversation;
|
||||
if (model) _title += `: ${model}`;
|
||||
} else if (endpoint === null) {
|
||||
null;
|
||||
} else {
|
||||
null;
|
||||
}
|
||||
return _title;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'dark:text-gray-450 w-full gap-1 border-b border-black/10 bg-gray-50 text-sm text-gray-500 transition-all hover:bg-gray-100 hover:bg-opacity-30 dark:border-gray-900/50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:hover:bg-opacity-100',
|
||||
endpoint === 'chatGPTBrowser' ? '' : 'cursor-pointer '
|
||||
)}
|
||||
onClick={() => (endpoint === 'chatGPTBrowser' ? null : setSaveAsDialogShow(true))}
|
||||
>
|
||||
<div className="d-block flex w-full items-center justify-center p-3">{getConversationTitle()}</div>
|
||||
</div>
|
||||
|
||||
<EndpointOptionsDialog
|
||||
open={saveAsDialogShow}
|
||||
onOpenChange={setSaveAsDialogShow}
|
||||
preset={conversation}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageHeader;
|
||||
|
|
@ -5,6 +5,7 @@ import { throttle } from 'lodash';
|
|||
import { CSSTransition } from 'react-transition-group';
|
||||
import ScrollToBottom from './ScrollToBottom';
|
||||
import MultiMessage from './MultiMessage';
|
||||
import MessageHeader from './MessageHeader';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
|
|
@ -20,12 +21,10 @@ export default function Messages({ isSearchView = false }) {
|
|||
const _messagesTree = isSearchView ? searchResultMessagesTree : messagesTree;
|
||||
|
||||
const conversation = useRecoilValue(store.conversation) || {};
|
||||
const { conversationId, model, chatGptLabel } = conversation;
|
||||
const { conversationId } = conversation;
|
||||
|
||||
const models = useRecoilValue(store.models) || [];
|
||||
const modelName = models.find(element => element.model == model)?.name;
|
||||
|
||||
const searchQuery = useRecoilValue(store.searchQuery);
|
||||
// const models = useRecoilValue(store.models) || [];
|
||||
// const modelName = models.find(element => element.model == model)?.name;
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
|
|
@ -81,17 +80,13 @@ export default function Messages({ isSearchView = false }) {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="flex-1 overflow-y-auto pt-10 md:pt-0"
|
||||
className="flex-1 overflow-y-auto pt-0"
|
||||
ref={scrollableRef}
|
||||
onScroll={debouncedHandleScroll}
|
||||
>
|
||||
<div className="dark:gpt-dark-gray h-full">
|
||||
<div className="dark:gpt-dark-gray flex h-full flex-col items-center text-sm">
|
||||
<div className="flex w-full items-center justify-center gap-1 border-b border-black/10 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-900/50 dark:bg-gray-700 dark:text-gray-300">
|
||||
{isSearchView
|
||||
? `Search: ${searchQuery}`
|
||||
: `Model: ${modelName} ${chatGptLabel ? `(${chatGptLabel})` : ''}`}
|
||||
</div>
|
||||
<MessageHeader isSearchView={isSearchView} />
|
||||
{_messagesTree === null ? (
|
||||
<Spinner />
|
||||
) : _messagesTree?.length == 0 && isSearchView ? (
|
||||
|
|
|
|||
|
|
@ -3,20 +3,20 @@ import React from 'react';
|
|||
export default function Regenerate() {
|
||||
return (
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-3 w-3"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline points="1 4 1 10 7 10" />
|
||||
<polyline points="23 20 23 14 17 14" />
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" />
|
||||
</svg>
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-4 w-4"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline points="1 4 1 10 7 10" />
|
||||
<polyline points="23 20 23 14 17 14" />
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
19
client/src/components/svg/SaveIcon.jsx
Normal file
19
client/src/components/svg/SaveIcon.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function SaveIcon({ size = '1em', className }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="64 64 896 896"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
width={size}
|
||||
height={size}
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M893.3 293.3L730.7 130.7c-7.5-7.5-16.7-13-26.7-16V112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V338.5c0-17-6.7-33.2-18.7-45.2zM384 184h256v104H384V184zm456 656H184V184h136v136c0 17.7 14.3 32 32 32h320c17.7 0 32-14.3 32-32V205.8l136 136V840zM512 442c-79.5 0-144 64.5-144 144s64.5 144 144 144 144-64.5 144-144-64.5-144-144-144zm0 224c-44.2 0-80-35.8-80-80s35.8-80 80-80 80 35.8 80 80-35.8 80-80 80z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,6 +2,26 @@ import React from 'react';
|
|||
|
||||
export default function StopGeneratingIcon() {
|
||||
return (
|
||||
<svg stroke="currentColor" fill="none" strokeWidth="1.5" viewBox="0 0 24 24" strokeLinecap="round" strokeLinejoin="round" className="h-3 w-3" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="2.5"
|
||||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-3 w-3"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
ry="2"
|
||||
></rect>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
19
client/src/components/svg/SwitchIcon.jsx
Normal file
19
client/src/components/svg/SwitchIcon.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function SwitchIcon({ size = '1em', className }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="64 64 896 896"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
width={size}
|
||||
height={size}
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M847.9 592H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h605.2L612.9 851c-4.1 5.2-.4 13 6.3 13h72.5c4.9 0 9.5-2.2 12.6-6.1l168.8-214.1c16.5-21 1.6-51.8-25.2-51.8zM872 356H266.8l144.3-183c4.1-5.2.4-13-6.3-13h-72.5c-4.9 0-9.5 2.2-12.6 6.1L150.9 380.2c-16.5 21-1.6 51.8 25.1 51.8h696c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
29
client/src/components/ui/Checkbox.tsx
Normal file
29
client/src/components/ui/Checkbox.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
import { cn } from "../../utils"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-slate-300 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { Button } from '../ui/Button';
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "../../utils"
|
||||
|
|
@ -53,7 +54,7 @@ const DialogContent = React.forwardRef<
|
|||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=open]:bg-slate-800">
|
||||
<X className="h-4 w-4" />
|
||||
<X className="h-4 w-4 text-black dark:text-white" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
|
|
@ -81,7 +82,7 @@ const DialogFooter = ({
|
|||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-between sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -132,6 +133,22 @@ const DialogClose = React.forwardRef<
|
|||
))
|
||||
DialogClose.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogButton = React.forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
React.ComponentPropsWithoutRef<typeof Button>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"mt-2 inline-flex h-10 items-center justify-center rounded-md border border-slate-200 bg-transparent py-2 px-4 text-sm font-semibold text-slate-900 transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-slate-700 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogButton.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
|
|
@ -141,4 +158,5 @@ export {
|
|||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogClose,
|
||||
DialogButton,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,18 +8,26 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle
|
||||
} from './Dialog.tsx';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
export default function DialogTemplate({ title, description, main, buttons, selection }) {
|
||||
const { selectHandler, selectClasses, selectText } = selection;
|
||||
export default function DialogTemplate({
|
||||
title,
|
||||
description,
|
||||
main,
|
||||
buttons,
|
||||
leftButtons,
|
||||
selection,
|
||||
className
|
||||
}) {
|
||||
const { selectHandler, selectClasses, selectText } = selection || {};
|
||||
|
||||
const defaultSelect = "bg-gray-900 text-white transition-colors hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900"
|
||||
const defaultSelect =
|
||||
'bg-gray-900 text-white transition-colors hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900';
|
||||
return (
|
||||
<DialogContent className="shadow-2xl dark:bg-gray-800">
|
||||
<DialogContent className={cn('shadow-2xl dark:bg-gray-800', className || '')}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-gray-800 dark:text-white">{title}</DialogTitle>
|
||||
<DialogDescription className="text-gray-600 dark:text-gray-300">
|
||||
{description}
|
||||
</DialogDescription>
|
||||
<DialogDescription className="text-gray-600 dark:text-gray-300">{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{/* <div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4"> //input template
|
||||
|
|
@ -43,14 +51,21 @@ export default function DialogTemplate({ title, description, main, buttons, sele
|
|||
</div> */}
|
||||
{main ? main : null}
|
||||
<DialogFooter>
|
||||
<DialogClose className="dark:hover:gray-400 border-gray-700">Cancel</DialogClose>
|
||||
{ buttons ? buttons : null}
|
||||
<DialogClose
|
||||
onClick={selectHandler}
|
||||
className={`${selectClasses || defaultSelect} inline-flex h-10 items-center justify-center rounded-md border-none py-2 px-4 text-sm font-semibold`}
|
||||
>
|
||||
{selectText}
|
||||
</DialogClose>
|
||||
<div>{leftButtons ? leftButtons : null}</div>
|
||||
<div className="flex gap-2">
|
||||
<DialogClose className="dark:hover:gray-400 border-gray-700">Cancel</DialogClose>
|
||||
{buttons ? buttons : null}
|
||||
{selection ? (
|
||||
<DialogClose
|
||||
onClick={selectHandler}
|
||||
className={`${
|
||||
selectClasses || defaultSelect
|
||||
} inline-flex h-10 items-center justify-center rounded-md border-none py-2 px-4 text-sm font-semibold`}
|
||||
>
|
||||
{selectText}
|
||||
</DialogClose>
|
||||
) : null}
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
);
|
||||
|
|
|
|||
73
client/src/components/ui/Dropdown.jsx
Normal file
73
client/src/components/ui/Dropdown.jsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import CheckMark from '../svg/CheckMark';
|
||||
import { Listbox } from '@headlessui/react';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
function Dropdown({ value, onChange, options, className, containerClassName }) {
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center gap-2', containerClassName)}>
|
||||
<div className="relative w-full">
|
||||
<Listbox
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
>
|
||||
<Listbox.Button
|
||||
className={cn(
|
||||
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:border-green-600 focus:outline-none focus:ring-1 focus:ring-green-600 dark:border-white/20 dark:bg-gray-800 sm:text-sm',
|
||||
className || ''
|
||||
)}
|
||||
>
|
||||
<span className="inline-flex w-full truncate">
|
||||
<span className="flex h-6 items-center gap-1 truncate text-sm text-black dark:text-white">
|
||||
{value}
|
||||
</span>
|
||||
</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-4 w-4 text-gray-400"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Listbox.Options className="absolute z-50 mt-2 max-h-60 w-full overflow-auto rounded bg-white text-base text-xs ring-1 ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:last:border-0 md:w-[100%] ">
|
||||
{options.map((item, i) => (
|
||||
<Listbox.Option
|
||||
key={i}
|
||||
value={item}
|
||||
className="group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden border-b border-black/10 pl-3 pr-9 text-gray-900 last:border-0 hover:bg-[#ECECF1] dark:border-white/20 dark:text-white dark:hover:bg-gray-700"
|
||||
>
|
||||
<span className="flex items-center gap-1.5 truncate">
|
||||
<span
|
||||
className={cn(
|
||||
'flex h-6 items-center gap-1 text-gray-800 dark:text-gray-100',
|
||||
value === item ? 'font-semibold' : ''
|
||||
)}
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
{value === item && (
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-800 dark:text-gray-100">
|
||||
<CheckMark />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Listbox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dropdown;
|
||||
31
client/src/components/ui/HoverCard.tsx
Normal file
31
client/src/components/ui/HoverCard.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
|
||||
import { cn } from "../../utils"
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
const HoverCardPortal = HoverCardPrimitive.Portal
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 6, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"animate-in fade-in-0 z-50 w-64 rounded-md border border-gray-100 bg-white p-4 shadow-md outline-none dark:border-gray-800 dark:bg-gray-800",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent, HoverCardPortal }
|
||||
47
client/src/components/ui/InputNumber.tsx
Normal file
47
client/src/components/ui/InputNumber.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
// import { NumericFormat } from 'react-number-format';
|
||||
|
||||
import RCInputNumber from 'rc-input-number';
|
||||
import * as InputNumberPrimitive from 'rc-input-number';
|
||||
|
||||
import { cn } from '../../utils/index.jsx';
|
||||
|
||||
// TODO help needed
|
||||
// React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
// React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
|
||||
const InputNumber = React.forwardRef< React.ElementRef<typeof RCInputNumber>, InputNumberPrimitive.InputNumberProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RCInputNumber
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
InputNumber.displayName = "Input"
|
||||
|
||||
// console.log(_InputNumber);
|
||||
|
||||
// const InputNumber = React.forwardRef(({ className, ...props }, ref) => {
|
||||
// return (
|
||||
// <NumericFormat
|
||||
// className={cn(
|
||||
// 'flex h-10 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900',
|
||||
// className
|
||||
// )}
|
||||
// ref={ref}
|
||||
// {...props}
|
||||
// />
|
||||
// );
|
||||
// });
|
||||
|
||||
export { InputNumber };
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import useDocumentTitle from '~/hooks/useDocumentTitle';
|
||||
import Templates from '../Prompts/Templates';
|
||||
import Templates from '../ui/Templates';
|
||||
import SunIcon from '../svg/SunIcon';
|
||||
import LightningIcon from '../svg/LightningIcon';
|
||||
import CautionIcon from '../svg/CautionIcon';
|
||||
|
|
@ -30,9 +30,9 @@ export default function Landing() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center overflow-y-auto pt-10 text-sm dark:bg-gray-800 md:pt-0">
|
||||
<div className="flex h-full flex-col items-center overflow-y-auto pt-0 text-sm dark:bg-gray-800">
|
||||
<div className="w-full px-6 text-gray-800 dark:text-gray-100 md:flex md:max-w-2xl md:flex-col lg:max-w-3xl">
|
||||
<h1 className="mt-6 ml-auto mr-auto mb-10 flex items-center justify-center gap-2 text-center text-4xl font-semibold sm:mb-16 md:mt-[20vh]">
|
||||
<h1 className="mt-6 ml-auto mr-auto mb-10 flex items-center justify-center gap-2 text-center text-4xl font-semibold sm:mb-16 md:mt-[10vh]">
|
||||
ChatGPT Clone
|
||||
</h1>
|
||||
<div className="items-start gap-3.5 text-center md:flex">
|
||||
|
|
|
|||
52
client/src/components/ui/ModelSelect.jsx
Normal file
52
client/src/components/ui/ModelSelect.jsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button } from './Button.tsx';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuRadioItem
|
||||
} from './DropdownMenu.tsx';
|
||||
|
||||
const ModelSelect = ({ model, onChange, availableModels, ...props }) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
open={menuOpen}
|
||||
onOpenChange={setMenuOpen}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button {...props}>
|
||||
<span className="w-full text-center text-xs font-medium font-normal">Model: {model}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-56 dark:bg-gray-700"
|
||||
onCloseAutoFocus={event => event.preventDefault()}
|
||||
>
|
||||
<DropdownMenuLabel className="dark:text-gray-300">Select a model</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuRadioGroup
|
||||
value={model}
|
||||
onValueChange={onChange}
|
||||
className="overflow-y-auto"
|
||||
>
|
||||
{availableModels.map(model => (
|
||||
<DropdownMenuRadioItem
|
||||
key={model}
|
||||
value={model}
|
||||
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
{model}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelSelect;
|
||||
114
client/src/components/ui/SelectDropDown.jsx
Normal file
114
client/src/components/ui/SelectDropDown.jsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import React from 'react';
|
||||
import CheckMark from '../svg/CheckMark.jsx';
|
||||
import { Listbox, Transition } from '@headlessui/react';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
function SelectDropdown({
|
||||
title = 'Model',
|
||||
value,
|
||||
disabled,
|
||||
setValue,
|
||||
availableValues,
|
||||
showAbove = false,
|
||||
showLabel = true,
|
||||
containerClassName,
|
||||
className
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center gap-2', containerClassName)}>
|
||||
<div className="relative w-full">
|
||||
<Listbox
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Listbox.Button
|
||||
className={cn(
|
||||
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:border-green-600 focus:outline-none focus:ring-1 focus:ring-green-600 dark:border-white/20 dark:bg-gray-800 sm:text-sm',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{' '}
|
||||
{showLabel && (
|
||||
<Listbox.Label
|
||||
className="block text-xs text-gray-700 dark:text-gray-500"
|
||||
id="headlessui-listbox-label-:r1:"
|
||||
data-headlessui-state=""
|
||||
>
|
||||
{title}
|
||||
</Listbox.Label>
|
||||
)}
|
||||
<span className="inline-flex w-full truncate">
|
||||
<span
|
||||
className={cn(
|
||||
'flex h-6 items-center gap-1 truncate text-sm text-gray-900 dark:text-white',
|
||||
!showLabel ? 'text-xs' : ''
|
||||
)}
|
||||
>
|
||||
{!showLabel && <span className="text-xs text-gray-700 dark:text-gray-500">{title}:</span>}
|
||||
{value}
|
||||
</span>
|
||||
</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-4 w-4 text-gray-400"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={showAbove ? { transform: 'scaleY(-1)' } : {}}
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
className={showAbove ? 'bottom-full mb-3' : 'top-full mt-3'}
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 mt-2 max-h-60 w-full overflow-auto rounded bg-white text-base text-xs ring-1 ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:last:border-0 md:w-[100%]">
|
||||
{availableValues.map((option, i) => (
|
||||
<Listbox.Option
|
||||
key={i}
|
||||
value={option}
|
||||
className="group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden border-b border-black/10 pl-3 pr-9 text-gray-900 last:border-0 hover:bg-[#ECECF1] dark:border-white/20 dark:text-white dark:hover:bg-gray-700"
|
||||
>
|
||||
<span className="flex items-center gap-1.5 truncate">
|
||||
<span
|
||||
className={cn(
|
||||
'flex h-6 items-center gap-1 text-gray-800 dark:text-gray-100',
|
||||
option === value ? 'font-semibold' : ''
|
||||
)}
|
||||
>
|
||||
{option}
|
||||
</span>
|
||||
{option === value && (
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-800 dark:text-gray-100">
|
||||
<CheckMark />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectDropdown;
|
||||
34
client/src/components/ui/Slider.tsx
Normal file
34
client/src/components/ui/Slider.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
import { useDoubleClick } from "@zattoo/use-double-click"
|
||||
import { cn } from "../../utils"
|
||||
|
||||
type clickEvent = (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
|
||||
interface SliderProps extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
|
||||
doubleClickHandler?: clickEvent;
|
||||
}
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
SliderProps
|
||||
>(({ className, doubleClickHandler, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-gray-100 dark:bg-gray-900">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-gray-400 dark:bg-gray-400" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb onClick={useDoubleClick(doubleClickHandler) ?? (() => {})} className="block h-4 w-4 rounded-full border-2 border-gray-400 bg-white transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:border-gray-100 dark:bg-gray-400 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900" />
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.resubmit-edit-button {
|
||||
.hover-button {
|
||||
display: block;
|
||||
visibility: visible;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,36 @@
|
|||
import models from './models';
|
||||
import endpoints from './endpoints';
|
||||
import { atom, selector, useSetRecoilState, useResetRecoilState, useRecoilCallback } from 'recoil';
|
||||
import buildTree from '~/utils/buildTree';
|
||||
import getDefaultConversation from '~/utils/getDefaultConversation';
|
||||
|
||||
// current conversation, can be null (need to be fetched from server)
|
||||
// sample structure
|
||||
// {
|
||||
// conversationId: "new",
|
||||
// title: "New Chat",
|
||||
// conversationId: 'new',
|
||||
// title: 'New Chat',
|
||||
// user: null,
|
||||
// // endpoint: [azureOpenAI, openAI, bingAI, chatGPTBrowser]
|
||||
// endpoint: 'azureOpenAI',
|
||||
// // for azureOpenAI, openAI, chatGPTBrowser only
|
||||
// model: 'gpt-3.5-turbo',
|
||||
// // for azureOpenAI, openAI only
|
||||
// chatGptLabel: null,
|
||||
// promptPrefix: null,
|
||||
// temperature: 1,
|
||||
// top_p: 1,
|
||||
// presence_penalty: 0,
|
||||
// frequency_penalty: 0,
|
||||
// // for bingAI only
|
||||
// jailbreak: false,
|
||||
// context: null,
|
||||
// systemMessage: null,
|
||||
// jailbreakConversationId: null,
|
||||
// conversationSignature: null,
|
||||
// clientId: null,
|
||||
// invocationId: null,
|
||||
// model: "chatgpt",
|
||||
// chatGptLabel: null,
|
||||
// promptPrefix: null,
|
||||
// user: null,
|
||||
// suggestions: [],
|
||||
// invocationId: 1,
|
||||
// toneStyle: null,
|
||||
// }
|
||||
// };
|
||||
|
||||
const conversation = atom({
|
||||
key: 'conversation',
|
||||
default: null
|
||||
|
|
@ -50,10 +63,13 @@ const useConversation = () => {
|
|||
|
||||
const switchToConversation = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async (_conversation, messages = null) => {
|
||||
async (_conversation, messages = null, preset = null) => {
|
||||
const prevConversation = await snapshot.getPromise(conversation);
|
||||
const prevModelsFilter = await snapshot.getPromise(models.modelsFilter);
|
||||
_switchToConversation(_conversation, messages, { prevModelsFilter, prevConversation });
|
||||
const endpointsFilter = await snapshot.getPromise(endpoints.endpointsFilter);
|
||||
_switchToConversation(_conversation, messages, preset, {
|
||||
endpointsFilter,
|
||||
prevConversation
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
|
@ -61,73 +77,34 @@ const useConversation = () => {
|
|||
const _switchToConversation = (
|
||||
conversation,
|
||||
messages = null,
|
||||
{ prevModelsFilter = {}, prev_conversation = {} }
|
||||
preset = null,
|
||||
{ endpointsFilter = {}, prevConversation = {} }
|
||||
) => {
|
||||
let { model = null, chatGptLabel = null, promptPrefix = null } = conversation;
|
||||
const getDefaultModel = () => {
|
||||
try {
|
||||
// try to use current model
|
||||
const { _model = null, _chatGptLabel = null, _promptPrefix = null } = prev_conversation || {};
|
||||
if (prevModelsFilter[_model]) {
|
||||
model = _model;
|
||||
chatGptLabel = _chatGptLabel;
|
||||
promptPrefix = _promptPrefix;
|
||||
return;
|
||||
}
|
||||
} catch (error) {}
|
||||
let { endpoint = null } = conversation;
|
||||
|
||||
try {
|
||||
// try to read latest selected model from local storage
|
||||
const lastSelected = JSON.parse(localStorage.getItem('model'));
|
||||
const { model: _model, chatGptLabel: _chatGptLabel, promptPrefix: _promptPrefix } = lastSelected;
|
||||
|
||||
if (prevModelsFilter[_model]) {
|
||||
model = _model;
|
||||
chatGptLabel = _chatGptLabel;
|
||||
promptPrefix = _promptPrefix;
|
||||
return;
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
// if anything happens, reset to default model
|
||||
if (prevModelsFilter?.chatgpt) model = 'chatgpt';
|
||||
else if (prevModelsFilter?.bingai) model = 'bingai';
|
||||
else if (prevModelsFilter?.chatgptBrowser) model = 'chatgptBrowser';
|
||||
chatGptLabel = null;
|
||||
promptPrefix = null;
|
||||
};
|
||||
|
||||
if (model === null)
|
||||
if (endpoint === null)
|
||||
// get the default model
|
||||
getDefaultModel();
|
||||
conversation = getDefaultConversation({
|
||||
conversation,
|
||||
endpointsFilter,
|
||||
prevConversation,
|
||||
preset
|
||||
});
|
||||
|
||||
setConversation({
|
||||
...conversation,
|
||||
model: model,
|
||||
chatGptLabel: chatGptLabel,
|
||||
promptPrefix: promptPrefix
|
||||
});
|
||||
setConversation(conversation);
|
||||
setMessages(messages);
|
||||
resetLatestMessage();
|
||||
};
|
||||
|
||||
const newConversation = ({ model = null, chatGptLabel = null, promptPrefix = null } = {}) => {
|
||||
const newConversation = (template = {}, preset) => {
|
||||
switchToConversation(
|
||||
{
|
||||
conversationId: 'new',
|
||||
title: 'New Chat',
|
||||
jailbreakConversationId: null,
|
||||
conversationSignature: null,
|
||||
clientId: null,
|
||||
invocationId: null,
|
||||
model: model,
|
||||
chatGptLabel: chatGptLabel,
|
||||
promptPrefix: promptPrefix,
|
||||
user: null,
|
||||
suggestions: [],
|
||||
toneStyle: null
|
||||
...template
|
||||
},
|
||||
[]
|
||||
[],
|
||||
preset
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -135,17 +112,7 @@ const useConversation = () => {
|
|||
switchToConversation(
|
||||
{
|
||||
conversationId: 'search',
|
||||
title: 'Search',
|
||||
jailbreakConversationId: null,
|
||||
conversationSignature: null,
|
||||
clientId: null,
|
||||
invocationId: null,
|
||||
model: null,
|
||||
chatGptLabel: null,
|
||||
promptPrefix: null,
|
||||
user: null,
|
||||
suggestions: [],
|
||||
toneStyle: null
|
||||
title: 'Search'
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
|
|
|||
38
client/src/store/endpoints.js
Normal file
38
client/src/store/endpoints.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { atom, selector } from 'recoil';
|
||||
|
||||
const endpointsConfig = atom({
|
||||
key: 'endpointsConfig',
|
||||
default: {
|
||||
azureOpenAI: null,
|
||||
openAI: null,
|
||||
bingAI: null,
|
||||
chatGPTBrowser: null
|
||||
}
|
||||
});
|
||||
|
||||
const endpointsFilter = selector({
|
||||
key: 'endpointsFilter',
|
||||
get: ({ get }) => {
|
||||
const config = get(endpointsConfig) || {};
|
||||
|
||||
let filter = {};
|
||||
for (const key of Object.keys(config)) filter[key] = !!config[key];
|
||||
return filter;
|
||||
}
|
||||
});
|
||||
|
||||
const availableEndpoints = selector({
|
||||
key: 'availableEndpoints',
|
||||
get: ({ get }) => {
|
||||
const endpoints = ['azureOpenAI', 'openAI', 'bingAI', 'chatGPTBrowser'];
|
||||
const f = get(endpointsFilter);
|
||||
return endpoints.filter(endpoint => f[endpoint]);
|
||||
}
|
||||
});
|
||||
// const modelAvailable
|
||||
|
||||
export default {
|
||||
endpointsConfig,
|
||||
endpointsFilter,
|
||||
availableEndpoints
|
||||
};
|
||||
|
|
@ -1,17 +1,19 @@
|
|||
import conversation from './conversation';
|
||||
import conversations from './conversations';
|
||||
import models from './models';
|
||||
import endpoints from './endpoints';
|
||||
import user from './user';
|
||||
import text from './text';
|
||||
import submission from './submission';
|
||||
import search from './search';
|
||||
import preset from './preset';
|
||||
|
||||
export default {
|
||||
...conversation,
|
||||
...conversations,
|
||||
...models,
|
||||
...endpoints,
|
||||
...user,
|
||||
text,
|
||||
...text,
|
||||
...submission,
|
||||
...search
|
||||
...search,
|
||||
...preset
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
import React from "react";
|
||||
import {
|
||||
RecoilRoot,
|
||||
atom,
|
||||
selector,
|
||||
useRecoilState,
|
||||
useRecoilValue,
|
||||
} from "recoil";
|
||||
|
||||
const customGPTModels = atom({
|
||||
key: "customGPTModels",
|
||||
default: [],
|
||||
});
|
||||
|
||||
const models = selector({
|
||||
key: "models",
|
||||
get: ({ get }) => {
|
||||
return [
|
||||
{
|
||||
_id: "0",
|
||||
name: "ChatGPT",
|
||||
value: "chatgpt",
|
||||
model: "chatgpt",
|
||||
},
|
||||
{
|
||||
_id: "1",
|
||||
name: "CustomGPT",
|
||||
value: "chatgptCustom",
|
||||
model: "chatgptCustom",
|
||||
},
|
||||
{
|
||||
_id: "2",
|
||||
name: "BingAI",
|
||||
value: "bingai",
|
||||
model: "bingai",
|
||||
},
|
||||
{
|
||||
_id: "3",
|
||||
name: "Sydney",
|
||||
value: "sydney",
|
||||
model: "sydney",
|
||||
},
|
||||
{
|
||||
_id: "4",
|
||||
name: "ChatGPT",
|
||||
value: "chatgptBrowser",
|
||||
model: "chatgptBrowser",
|
||||
},
|
||||
...get(customGPTModels),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
const modelsFilter = atom({
|
||||
key: "modelsFilter",
|
||||
default: {
|
||||
chatgpt: false,
|
||||
chatgptCustom: false,
|
||||
bingai: false,
|
||||
sydney: false,
|
||||
chatgptBrowser: false,
|
||||
},
|
||||
});
|
||||
|
||||
const availableModels = selector({
|
||||
key: "availableModels",
|
||||
get: ({ get }) => {
|
||||
const m = get(models);
|
||||
const f = get(modelsFilter);
|
||||
return m.filter(({ model }) => f[model]);
|
||||
},
|
||||
});
|
||||
// const modelAvailable
|
||||
|
||||
export default {
|
||||
customGPTModels,
|
||||
models,
|
||||
modelsFilter,
|
||||
availableModels,
|
||||
};
|
||||
37
client/src/store/preset.js
Normal file
37
client/src/store/preset.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { atom } from 'recoil';
|
||||
|
||||
// preset structure is as same defination as conversation
|
||||
// sample structure
|
||||
// {
|
||||
// presetId: 'new',
|
||||
// title: 'New Chat',
|
||||
// user: null,
|
||||
// // endpoint: [azureOpenAI, openAI, bingAI, chatGPTBrowser]
|
||||
// endpoint: 'azureOpenAI',
|
||||
// // for azureOpenAI, openAI, chatGPTBrowser only
|
||||
// model: 'gpt-3.5-turbo',
|
||||
// // for azureOpenAI, openAI only
|
||||
// chatGptLabel: null,
|
||||
// promptPrefix: null,
|
||||
// temperature: 1,
|
||||
// top_p: 1,
|
||||
// presence_penalty: 0,
|
||||
// frequency_penalty: 0,
|
||||
// // for bingAI only
|
||||
// jailbreak: false,
|
||||
// toneStyle: null,
|
||||
// context: null,
|
||||
// systemMessage: null,
|
||||
// };
|
||||
|
||||
// an array of saved presets.
|
||||
// sample structure
|
||||
// [preset1, preset2, preset3]
|
||||
const presets = atom({
|
||||
key: 'presets',
|
||||
default: []
|
||||
});
|
||||
|
||||
export default {
|
||||
presets
|
||||
};
|
||||
|
|
@ -5,4 +5,4 @@ const text = atom({
|
|||
default: ''
|
||||
});
|
||||
|
||||
export default text;
|
||||
export default { text };
|
||||
|
|
|
|||
|
|
@ -28,26 +28,47 @@
|
|||
transition: all 1s ease-in-out;
|
||||
} */
|
||||
|
||||
.bing-styles {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
bottom: 39px;
|
||||
z-index: 995;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 308px; /* Need a specific value to work */
|
||||
transition: all 0.5s ease-in-out;
|
||||
pointer-events: none;
|
||||
transform: translateY(-60px);
|
||||
.overflow-y-auto {
|
||||
overflow-y: overlay;
|
||||
}
|
||||
|
||||
.bing-styles.show {
|
||||
/* bottom: -20px; */
|
||||
.overflow-x-auto {
|
||||
overflow-x: overlay;
|
||||
}
|
||||
|
||||
.reset-rc-number-input input {
|
||||
width: 100%;
|
||||
border: none !important;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.reset-rc-number-input-text-right input {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.openAIOptions-simple-container {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.openAIOptions-simple-container.show {
|
||||
pointer-events: fill;
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
.endpointOptionsPopover-container {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease-in-out;
|
||||
transform: scaleY(0);
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
.endpointOptionsPopover-container.show {
|
||||
pointer-events: fill;
|
||||
opacity: 1;
|
||||
transform: scaleY(1)
|
||||
}
|
||||
|
||||
.creative-tab {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
54
client/src/utils/cleanupPreset.js
Normal file
54
client/src/utils/cleanupPreset.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
const cleanupPreset = ({ preset: _preset, endpointsFilter = {} }) => {
|
||||
const { endpoint } = _preset;
|
||||
|
||||
let preset = {};
|
||||
if (endpoint === 'azureOpenAI' || endpoint === 'openAI') {
|
||||
preset = {
|
||||
endpoint,
|
||||
presetId: _preset?.presetId ?? null,
|
||||
model: _preset?.model ?? endpointsFilter[endpoint]?.availableModels?.[0] ?? 'gpt-3.5-turbo',
|
||||
chatGptLabel: _preset?.chatGptLabel ?? null,
|
||||
promptPrefix: _preset?.promptPrefix ?? null,
|
||||
temperature: _preset?.temperature ?? 1,
|
||||
top_p: _preset?.top_p ?? 1,
|
||||
presence_penalty: _preset?.presence_penalty ?? 0,
|
||||
frequency_penalty: _preset?.frequency_penalty ?? 0,
|
||||
title: _preset?.title ?? 'New Preset'
|
||||
};
|
||||
} else if (endpoint === 'bingAI') {
|
||||
preset = {
|
||||
endpoint,
|
||||
presetId: _preset?.presetId ?? null,
|
||||
jailbreak: _preset?.jailbreak ?? false,
|
||||
context: _preset?.context ?? null,
|
||||
systemMessage: _preset?.systemMessage ?? null,
|
||||
toneStyle: _preset?.toneStyle ?? 'fast',
|
||||
title: _preset?.title ?? 'New Preset'
|
||||
};
|
||||
} else if (endpoint === 'chatGPTBrowser') {
|
||||
preset = {
|
||||
endpoint,
|
||||
presetId: _preset?.presetId ?? null,
|
||||
model:
|
||||
_preset?.model ?? endpointsFilter[endpoint]?.availableModels?.[0] ?? 'text-davinci-002-render-sha',
|
||||
title: _preset?.title ?? 'New Preset'
|
||||
};
|
||||
} else if (endpoint === null) {
|
||||
preset = {
|
||||
endpoint,
|
||||
presetId: _preset?.presetId || null,
|
||||
title: _preset?.title ?? 'New Preset'
|
||||
};
|
||||
} else {
|
||||
console.error(`Unknown endpoint ${endpoint}`);
|
||||
preset = {
|
||||
endpoint: null,
|
||||
presetId: _preset?.presetId ?? null,
|
||||
title: _preset?.title ?? 'New Preset'
|
||||
};
|
||||
}
|
||||
|
||||
return preset;
|
||||
};
|
||||
|
||||
export default cleanupPreset;
|
||||
|
|
@ -1,55 +1,22 @@
|
|||
export default function createPayload(submission) {
|
||||
const { conversation, messages, message, initialResponse, isRegenerate = false } = submission;
|
||||
const { conversation, message, endpointOption } = submission;
|
||||
const { conversationId } = conversation;
|
||||
const { endpoint } = endpointOption;
|
||||
|
||||
const endpoint = `/api/ask`;
|
||||
const {
|
||||
model,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
jailbreakConversationId,
|
||||
conversationId,
|
||||
conversationSignature,
|
||||
clientId,
|
||||
invocationId,
|
||||
toneStyle
|
||||
} = conversation;
|
||||
const endpointUrlMap = {
|
||||
azureOpenAI: '/api/ask/azureOpenAI',
|
||||
openAI: '/api/ask/openAI',
|
||||
bingAI: '/api/ask/bingAI',
|
||||
chatGPTBrowser: '/api/ask/chatGPTBrowser'
|
||||
};
|
||||
|
||||
const server = endpointUrlMap[endpoint];
|
||||
|
||||
let payload = {
|
||||
...message,
|
||||
...{
|
||||
model,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
conversationId
|
||||
}
|
||||
...endpointOption,
|
||||
conversationId
|
||||
};
|
||||
|
||||
// if (!payload.conversationId)
|
||||
// if (convo?.conversationId && convo?.parentMessageId) {
|
||||
// payload = {
|
||||
// ...payload,
|
||||
// conversationId: convo.conversationId,
|
||||
// parentMessageId: convo.parentMessageId || '00000000-0000-0000-0000-000000000000'
|
||||
// };
|
||||
// }
|
||||
|
||||
const isBing = model === 'bingai' || model === 'sydney';
|
||||
if (isBing && !conversationId) {
|
||||
payload.toneStyle = toneStyle || 'fast';
|
||||
}
|
||||
|
||||
if (isBing && conversationId) {
|
||||
payload = {
|
||||
...payload,
|
||||
jailbreakConversationId,
|
||||
conversationSignature,
|
||||
clientId,
|
||||
invocationId
|
||||
};
|
||||
}
|
||||
|
||||
let server = endpoint;
|
||||
server = model === 'bingai' ? server + '/bing' : server;
|
||||
server = model === 'sydney' ? server + '/sydney' : server;
|
||||
return { server, payload };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,28 @@ import axios from 'axios';
|
|||
import useSWR from 'swr';
|
||||
import useSWRMutation from 'swr/mutation';
|
||||
|
||||
const fetcher = (url) => fetch(url, { credentials: 'include' }).then((res) => res.json());
|
||||
const fetcher = url => fetch(url, { credentials: 'include' }).then(res => res.json());
|
||||
const axiosFetcher = async (url, params) => {
|
||||
console.log(params, 'params');
|
||||
return axios.get(url, params);
|
||||
};
|
||||
|
||||
const postRequest = async (url, { arg }) => {
|
||||
return await axios.post(url, { withCredentials: true, arg });
|
||||
export const postRequest = async (url, { arg }) => {
|
||||
return await axios({
|
||||
method: 'post',
|
||||
url: url,
|
||||
withCredentials: true,
|
||||
data: { arg }
|
||||
});
|
||||
};
|
||||
|
||||
export const axiosPost = async ({ url, arg, callback }) => {
|
||||
try {
|
||||
const response = await axios.post(url, { arg }, { withCredentials: true });
|
||||
callback(response.data);
|
||||
} catch (error) {
|
||||
console.error('An error occurred while making the axios post request:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const searchFetcher = async (pre, q, pageNumber, callback) => {
|
||||
|
|
@ -58,3 +72,25 @@ export function useManualSWR({ path, params, type, onSuccess }) {
|
|||
const fetchFunction = type === 'get' ? _.partialRight(axiosFetcher, params) : postRequest;
|
||||
return useSWRMutation(path, fetchFunction, options);
|
||||
}
|
||||
|
||||
export const handleFileSelected = async jsonData => {
|
||||
try {
|
||||
const response = await axios({
|
||||
url: '/api/presets',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
withCredentials: true,
|
||||
data: JSON.stringify(jsonData)
|
||||
});
|
||||
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`Error: ${response.statusText}`);
|
||||
// }
|
||||
console.log(response);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error uploading the preset:', error);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
115
client/src/utils/getDefaultConversation.js
Normal file
115
client/src/utils/getDefaultConversation.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
const buildDefaultConversation = ({
|
||||
conversation,
|
||||
endpoint,
|
||||
endpointsFilter = {},
|
||||
lastConversationSetup = {}
|
||||
}) => {
|
||||
if (endpoint === 'azureOpenAI' || endpoint === 'openAI') {
|
||||
conversation = {
|
||||
...conversation,
|
||||
endpoint,
|
||||
model:
|
||||
lastConversationSetup?.model ?? endpointsFilter[endpoint]?.availableModels?.[0] ?? 'gpt-3.5-turbo',
|
||||
chatGptLabel: lastConversationSetup?.chatGptLabel ?? null,
|
||||
promptPrefix: lastConversationSetup?.promptPrefix ?? null,
|
||||
temperature: lastConversationSetup?.temperature ?? 1,
|
||||
top_p: lastConversationSetup?.top_p ?? 1,
|
||||
presence_penalty: lastConversationSetup?.presence_penalty ?? 0,
|
||||
frequency_penalty: lastConversationSetup?.frequency_penalty ?? 0
|
||||
};
|
||||
} else if (endpoint === 'bingAI') {
|
||||
conversation = {
|
||||
...conversation,
|
||||
endpoint,
|
||||
jailbreak: lastConversationSetup?.jailbreak ?? false,
|
||||
context: lastConversationSetup?.context ?? null,
|
||||
systemMessage: lastConversationSetup?.systemMessage ?? null,
|
||||
toneStyle: lastConversationSetup?.toneStyle ?? 'fast',
|
||||
jailbreakConversationId: lastConversationSetup?.jailbreakConversationId ?? null,
|
||||
conversationSignature: null,
|
||||
clientId: null,
|
||||
invocationId: 1
|
||||
};
|
||||
} else if (endpoint === 'chatGPTBrowser') {
|
||||
conversation = {
|
||||
...conversation,
|
||||
endpoint,
|
||||
model:
|
||||
lastConversationSetup?.model ??
|
||||
endpointsFilter[endpoint]?.availableModels?.[0] ??
|
||||
'text-davinci-002-render-sha'
|
||||
};
|
||||
} else if (endpoint === null) {
|
||||
conversation = {
|
||||
...conversation,
|
||||
endpoint
|
||||
};
|
||||
} else {
|
||||
console.error(`Unknown endpoint ${endpoint}`);
|
||||
conversation = {
|
||||
...conversation,
|
||||
endpoint: null
|
||||
};
|
||||
}
|
||||
|
||||
return conversation;
|
||||
};
|
||||
|
||||
const getDefaultConversation = ({ conversation, prevConversation, endpointsFilter, preset }) => {
|
||||
const { endpoint: targetEndpoint } = preset || {};
|
||||
|
||||
if (targetEndpoint) {
|
||||
// try to use preset
|
||||
const endpoint = targetEndpoint;
|
||||
if (endpointsFilter?.[endpoint]) {
|
||||
conversation = buildDefaultConversation({
|
||||
conversation,
|
||||
endpoint,
|
||||
lastConversationSetup: preset,
|
||||
endpointsFilter
|
||||
});
|
||||
return conversation;
|
||||
} else {
|
||||
console.log(endpoint);
|
||||
console.warn(`Illegal target endpoint ${targetEndpoint} ${endpointsFilter}`);
|
||||
}
|
||||
}
|
||||
|
||||
// try {
|
||||
// // try to use current model
|
||||
// const { endpoint = null } = prevConversation || {};
|
||||
// if (endpointsFilter?.[endpoint]) {
|
||||
// conversation = buildDefaultConversation({
|
||||
// conversation,
|
||||
// endpoint,
|
||||
// lastConversationSetup: prevConversation,
|
||||
// endpointsFilter
|
||||
// });
|
||||
// return conversation;
|
||||
// }
|
||||
// } catch (error) {}
|
||||
|
||||
try {
|
||||
// try to read latest selected model from local storage
|
||||
const lastConversationSetup = JSON.parse(localStorage.getItem('lastConversationSetup'));
|
||||
const { endpoint = null } = lastConversationSetup;
|
||||
|
||||
if (endpointsFilter?.[endpoint]) {
|
||||
conversation = buildDefaultConversation({ conversation, endpoint, endpointsFilter });
|
||||
return conversation;
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
// if anything happens, reset to default model
|
||||
|
||||
const endpoint = ['openAI', 'azureOpenAI', 'bingAI', 'chatGPTBrowser'].find(e => endpointsFilter?.[e]);
|
||||
if (endpoint) {
|
||||
conversation = buildDefaultConversation({ conversation, endpoint, endpointsFilter });
|
||||
return conversation;
|
||||
} else {
|
||||
conversation = buildDefaultConversation({ conversation, endpoint: null, endpointsFilter });
|
||||
return conversation;
|
||||
}
|
||||
};
|
||||
|
||||
export default getDefaultConversation;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue