Merge pull request #140 from danny-avila/feat-refactor

Code refactoring
This commit is contained in:
Danny Avila 2023-03-29 11:00:04 -04:00 committed by GitHub
commit 79bb54db9c
68 changed files with 3298 additions and 2196 deletions

View file

@ -18,6 +18,12 @@ MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone"
# API key configuration.
# Leave blank if you don't want them.
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
# _U Cookies Value from bing.com
BING_TOKEN=
# ChatGPT Browser Client (free but use at your own risk)

View file

@ -31,6 +31,8 @@ const browserClient = async ({ text, onProgress, convo, abortController }) => {
options = { ...options, ...convo };
}
console.log('gptBrowser options', options, clientOptions);
/* will error if given a convoId at the start */
if (convo.parentMessageId.startsWith('0000')) {
delete options.conversationId;

View file

@ -1,5 +1,6 @@
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 clientOptions = {
modelOptions: {
@ -9,6 +10,10 @@ const clientOptions = {
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 ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
const store = {

View file

@ -18,22 +18,31 @@ const proxyEnvToAxiosProxy = proxyString => {
const titleConvo = async ({ model, text, response }) => {
let title = 'New Chat';
const messages = [
{
role: 'system',
content:
// `You are a title-generator with one job: giving a conversation, detect the language and titling the conversation provided by a user, using the same language. The requirement are: 1. If possible, generate in 5 words or less, 2. Using title case, 3. must give the title using the language as the user said. 4. Don't refer to the participants of the conversation. 5. Do not include punctuation or quotation marks. 6. Your response should be in title case, exclusively containing the title. 7. don't say anything except the title.
`Detect user language and write in the same language an extremely concise title for this conversation, which you must accurately detect. Write in the detected language. Title in 5 Words or Less. No Punctuation/Quotation. All first letters of every word should be capitalized and complete only the title in User Language only.
||>User:
"${text}"
||>Response:
"${JSON.stringify(response?.text)}"
||>Title:`
}
// {
// role: 'user',
// content: `User:\n "${text}"\n\n${model}: \n"${JSON.stringify(response?.text)}"\n\n`
// }
];
// console.log('Title Prompt', messages[0]);
const request = {
model: 'gpt-3.5-turbo',
messages: [
{
role: 'system',
content:
'You are a title-generator with one job: giving a conversation, detect the language and titling the conversation provided by a user in title case, using the same language.'
},
{
role: 'user',
content: `In 5 words or less, summarize the conversation below with a title in title case using the language the user writes in. Don't refer to the participants of the conversation nor the language. Do not include punctuation or quotation marks. Your response should be in title case, exclusively containing the title. Conversation:\n\nUser: "${text}"\n\n${model}: "${JSON.stringify(
response?.text
)}"\n\nTitle: `
}
],
messages,
temperature: 0,
presence_penalty: 0,
frequency_penalty: 0

View file

@ -43,15 +43,17 @@ module.exports = {
return { message: 'Error saving conversation' };
}
},
updateConvo: async (user, { conversationId, ...update }) => {
updateConvo: async (user, { conversationId, oldConvoId, ...update }) => {
try {
return await Conversation.findOneAndUpdate(
{ conversationId: conversationId, user },
update,
{
new: true
}
).exec();
let convoId = conversationId;
if (oldConvoId) {
convoId = oldConvoId;
update.conversationId = conversationId;
}
return await Conversation.findOneAndUpdate({ conversationId: convoId, user }, update, {
new: true
}).exec();
} catch (error) {
console.log(error);
return { message: 'Error updating conversation' };
@ -89,7 +91,7 @@ module.exports = {
promises.push(
Conversation.findOne({
user,
conversationId: convo.conversationId,
conversationId: convo.conversationId
}).exec()
)
);
@ -143,13 +145,14 @@ module.exports = {
}
} catch (error) {
console.log(error);
return 'Error getting conversation title';
return { message: 'Error getting conversation title' };
}
},
deleteConvos: async (user, filter) => {
let deleteCount = await Conversation.deleteMany({ ...filter, user }).exec();
console.log('deleteCount', deleteCount);
deleteCount.messages = await deleteMessages(filter);
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}});
return deleteCount;
}
};

14
api/package-lock.json generated
View file

@ -10,7 +10,7 @@
"license": "ISC",
"dependencies": {
"@keyv/mongo": "^2.1.8",
"@waylaidwanderer/chatgpt-api": "^1.32.8",
"@waylaidwanderer/chatgpt-api": "^1.33.1",
"axios": "^1.3.4",
"chatgpt-latest": "npm:@waylaidwanderer/chatgpt-api@^1.31.6",
"cors": "^2.8.5",
@ -1626,9 +1626,9 @@
}
},
"node_modules/@waylaidwanderer/chatgpt-api": {
"version": "1.32.8",
"resolved": "https://registry.npmjs.org/@waylaidwanderer/chatgpt-api/-/chatgpt-api-1.32.8.tgz",
"integrity": "sha512-0PZTP+M8tyJa9fT0avDZjGcNVRy4glSKj1dWUIGosCySS2EdOyNt0BZ18zsQYDQP50p2FADtY3b3b6DTZCcldw==",
"version": "1.33.1",
"resolved": "https://registry.npmjs.org/@waylaidwanderer/chatgpt-api/-/chatgpt-api-1.33.1.tgz",
"integrity": "sha512-RUWrwOcm22mV1j0bQUoY1TB3UzmpuQxV7jQurUMsIy/pfSQwS5n8nsJ0erU76t9H5eSiXoRL6/cmMBWbUd+J9w==",
"dependencies": {
"@dqbd/tiktoken": "^1.0.2",
"@fastify/cors": "^8.2.0",
@ -6969,9 +6969,9 @@
}
},
"@waylaidwanderer/chatgpt-api": {
"version": "1.32.8",
"resolved": "https://registry.npmjs.org/@waylaidwanderer/chatgpt-api/-/chatgpt-api-1.32.8.tgz",
"integrity": "sha512-0PZTP+M8tyJa9fT0avDZjGcNVRy4glSKj1dWUIGosCySS2EdOyNt0BZ18zsQYDQP50p2FADtY3b3b6DTZCcldw==",
"version": "1.33.1",
"resolved": "https://registry.npmjs.org/@waylaidwanderer/chatgpt-api/-/chatgpt-api-1.33.1.tgz",
"integrity": "sha512-RUWrwOcm22mV1j0bQUoY1TB3UzmpuQxV7jQurUMsIy/pfSQwS5n8nsJ0erU76t9H5eSiXoRL6/cmMBWbUd+J9w==",
"requires": {
"@dqbd/tiktoken": "^1.0.2",
"@fastify/cors": "^8.2.0",

View file

@ -20,7 +20,7 @@
"homepage": "https://github.com/danny-avila/chatgpt-clone#readme",
"dependencies": {
"@keyv/mongo": "^2.1.8",
"@waylaidwanderer/chatgpt-api": "^1.32.8",
"@waylaidwanderer/chatgpt-api": "^1.33.1",
"axios": "^1.3.4",
"chatgpt-latest": "npm:@waylaidwanderer/chatgpt-api@^1.31.6",
"cors": "^2.8.5",

View file

@ -29,7 +29,8 @@ const projectPath = path.join(__dirname, '..', '..', 'client');
session({
secret: 'chatgpt-clone-random-secrect',
resave: false,
saveUninitialized: true
saveUninitialized: true,
cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 } // 7 days
})
);
@ -67,26 +68,27 @@ const projectPath = path.join(__dirname, '..', '..', 'client');
res.send(JSON.stringify({ hasOpenAI, hasChatGpt, hasBing }));
});
app.get('/*', routes.authenticatedOrRedirect, function (req, res) {
res.sendFile(path.join(projectPath, 'public', 'index.html'));
});
app.listen(port, host, () => {
if (host == '0.0.0.0')
console.log(
`Server listening on all interface at port ${port}. Use http://localhost:${port} to access it`
);
else
console.log(
`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`
);
else console.log(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`);
});
})();
let messageCount = 0;
process.on('uncaughtException', (err) => {
process.on('uncaughtException', err => {
if (!err.message.includes('fetch failed')) {
console.error('There was an uncaught error:', err.message);
}
if (err.message.includes('fetch failed')) {
if (messageCount === 0) {
if (messageCount === 0) {
console.error('Meilisearch error, search will be disabled');
messageCount++;
}

View file

@ -4,26 +4,27 @@ const router = express.Router();
const askBing = require('./askBing');
const askSydney = require('./askSydney');
const { titleConvo, askClient, browserClient, customClient } = require('../../app/');
const { getConvo, saveMessage, getConvoTitle, saveConvo, updateConvo } = require('../../models');
const { saveMessage, getConvoTitle, saveConvo, updateConvo } = require('../../models');
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
const { getMessages } = require('../../models/Message');
router.use('/bing', askBing);
router.use('/sydney', askSydney);
router.post('/', async (req, res) => {
let { model, text, overrideParentMessageId=null, parentMessageId, conversationId: oldConversationId, ...convo } = req.body;
if (text.length === 0) {
return handleError(res, { text: 'Prompt empty or too short' });
}
const {
model,
text,
overrideParentMessageId = null,
parentMessageId,
conversationId: oldConversationId,
...convo
} = req.body;
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
console.log('model:', model, 'oldConvoId:', oldConversationId);
const conversationId = oldConversationId || crypto.randomUUID();
console.log('conversationId after old:', conversationId);
const userMessageId = crypto.randomUUID();
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
let userMessage = {
const userMessage = {
messageId: userMessageId,
sender: 'User',
text,
@ -31,32 +32,18 @@ router.post('/', async (req, res) => {
conversationId,
isCreatedByUser: true
};
console.log('ask log', {
model,
...userMessage,
...convo
});
// Chore: This creates a loose a stranded initial message for chatgptBrowser
if (!overrideParentMessageId) {
await saveMessage(userMessage);
await saveConvo(req?.session?.user?.username, { ...userMessage, model, ...convo });
}
if (!overrideParentMessageId && model !== 'chatgptBrowser') {
await saveConvo(req?.session?.user?.username, { ...userMessage, model, ...convo });
}
return await ask({
userMessage,
model,
convo,
preSendRequest: true,
overrideParentMessageId,
req,
res
});
return await ask({ userMessage, model, convo, preSendRequest: true, overrideParentMessageId, req, res });
});
const ask = async ({
@ -68,22 +55,14 @@ const ask = async ({
req,
res
}) => {
let {
const {
text,
parentMessageId: userParentMessageId,
conversationId,
messageId: userMessageId
} = userMessage;
let client;
if (model === 'chatgpt') {
client = askClient;
} else if (model === 'chatgptCustom') {
client = customClient;
} else {
client = browserClient;
}
const client = model === 'chatgpt' ? askClient : model === 'chatgptCustom' ? customClient : browserClient;
res.writeHead(200, {
Connection: 'keep-alive',
@ -97,79 +76,46 @@ const ask = async ({
try {
const progressCallback = createOnProgress();
const abortController = new AbortController();
res.on('close', () => {
console.log('The client has disconnected.');
// 执行其他操作
abortController.abort();
})
res.on('close', () => abortController.abort());
let gptResponse = await client({
text,
onProgress: progressCallback.call(null, model, { res, text }),
convo: {
parentMessageId: userParentMessageId,
conversationId,
...convo
},
convo: { parentMessageId: userParentMessageId, conversationId, ...convo },
...convo,
abortController
});
console.log('CLIENT RESPONSE', gptResponse);
gptResponse.text = gptResponse.response;
console.log('CLIENT RESPONSE', gptResponse);
if (!gptResponse.parentMessageId) {
// gptResponse.id = gptResponse.messageId;
gptResponse.parentMessageId = overrideParentMessageId || userMessageId;
// userMessage.conversationId = conversationId
// ? conversationId
// : gptResponse.conversationId;
// await saveMessage(userMessage);
delete gptResponse.response;
}
if (
(gptResponse.text.includes('2023') && !gptResponse.text.trim().includes(' ')) ||
gptResponse.text.toLowerCase().includes('no response') ||
gptResponse.text.toLowerCase().includes('no answer')
) {
await saveMessage({
messageId: crypto.randomUUID(),
sender: model,
conversationId,
parentMessageId: overrideParentMessageId || userMessageId,
error: true,
text: 'Prompt empty or too short'
});
return handleError(res, { text: 'Prompt empty or too short' });
}
gptResponse.sender = model === 'chatgptCustom' ? convo.chatGptLabel : model;
gptResponse.model = model;
// gptResponse.final = true;
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;
}
// override the parentMessageId, for the regeneration.
gptResponse.parentMessageId = overrideParentMessageId || userMessageId;
/* this is a hacky solution to get the browserClient working right, will refactor later */
if (model === 'chatgptBrowser' && userParentMessageId.startsWith('000')) {
await saveMessage({ ...userMessage, conversationId: gptResponse.conversationId });
}
await saveMessage(gptResponse);
await updateConvo(req?.session?.user?.username, gptResponse);
await updateConvo(req?.session?.user?.username, {
...gptResponse,
oldConvoId: model === 'chatgptBrowser' && conversationId
});
sendMessage(res, {
title: await getConvoTitle(req?.session?.user?.username, conversationId),
final: true,
@ -180,19 +126,12 @@ const ask = async ({
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
const title = await titleConvo({ model, text, response: gptResponse });
await updateConvo(
req?.session?.user?.username,
{
/* again, for sake of browser client, will soon refactor */
conversationId: model === 'chatgptBrowser' ? gptResponse.conversationId : conversationId,
title
}
);
await updateConvo(req?.session?.user?.username, {
conversationId: model === 'chatgptBrowser' ? gptResponse.conversationId : conversationId,
title
});
}
} catch (error) {
console.log(error);
// await deleteMessages({ messageId: userMessageId });
const errorMessage = {
messageId: crypto.randomUUID(),
sender: model,

View file

@ -13,30 +13,9 @@ router.get('/', async (req, res) => {
router.get('/:conversationId', async (req, res) => {
const { conversationId } = req.params;
const convo = await getConvo(req?.session?.user?.username, conversationId);
res.status(200).send(convo.toObject());
});
router.post('/gen_title', async (req, res) => {
const { conversationId } = req.body.arg;
const convo = await getConvo(req?.session?.user?.username, conversationId);
const firstMessage = (await getMessages({ conversationId }))[0];
const secondMessage = (await getMessages({ conversationId }))[1];
const title = convo.jailbreakConversationId
? await getConvoTitle(req?.session?.user?.username, conversationId)
: await titleConvo({
model: convo?.model,
message: firstMessage?.text,
response: JSON.stringify(secondMessage?.text || '')
});
await saveConvo(req?.session?.user?.username, {
conversationId,
title
});
res.status(200).send(title);
if (convo) res.status(200).send(convo.toObject());
else res.status(404).end();
});
router.post('/clear', async (req, res) => {

View file

@ -42,7 +42,7 @@ router.get('/', async function (req, res) {
},
true
)
).hits.map((message) => {
).hits.map(message => {
const { _formatted, ...rest } = message;
return {
...rest,
@ -64,7 +64,9 @@ router.get('/', async function (req, res) {
message.conversationId = cleanUpPrimaryKeyValue(message.conversationId);
}
if (result.convoMap[message.conversationId] && !message.error) {
message = { ...message, title: result.convoMap[message.conversationId].title };
const convo = result.convoMap[message.conversationId];
const { title, chatGptLabel, model } = convo;
message = { ...message, ...{ title, chatGptLabel, model } };
activeMessages.push(message);
}
}
@ -91,12 +93,12 @@ router.get('/clear', async function (req, res) {
router.get('/test', async function (req, res) {
const { q } = req.query;
const messages = (
await Message.meiliSearch(q, { attributesToHighlight: ['text'] }, true)
).hits.map((message) => {
const { _formatted, ...rest } = message;
return { ...rest, searchResult: true, text: _formatted.text };
});
const messages = (await Message.meiliSearch(q, { attributesToHighlight: ['text'] }, true)).hits.map(
message => {
const { _formatted, ...rest } = message;
return { ...rest, searchResult: true, text: _formatted.text };
}
);
res.send(messages);
});

View file

@ -1,19 +1,21 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './src/store';
// import { Provider } from 'react-redux';
// import { store } from './src/store';
import { RecoilRoot } from 'recoil';
import { ThemeProvider } from './src/hooks/ThemeContext';
import App from './src/App';
import './src/style.css';
import './src/mobile.css'
import './src/mobile.css';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<Provider store={store}>
<RecoilRoot>
<ThemeProvider>
<App />
</ThemeProvider>
</Provider>
);
</RecoilRoot>
);

View file

@ -2,14 +2,14 @@ server {
listen 80;
server_name localhost;
location / {
# Serve your React app
root /usr/share/nginx/html;
index index.html;
}
location /api {
# Proxy requests to the API service
proxy_pass http://api:3080/api;
}
location / {
# Serve your React app
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}

1386
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -24,7 +24,10 @@
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-tabs": "^1.0.3",
"@reduxjs/toolkit": "^1.9.2",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.10",
"@types/react": "^18.0.30",
"@types/react-dom": "^18.0.11",
"axios": "^1.3.4",
"class-variance-authority": "^0.4.0",
"clsx": "^1.2.1",
@ -34,11 +37,12 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-lazy-load": "^4.0.1",
"react-markdown": "^8.0.5",
"react-redux": "^8.0.5",
"react-markdown": "^8.0.6",
"react-router-dom": "^6.9.0",
"react-string-replace": "^1.1.0",
"react-textarea-autosize": "^8.4.0",
"react-transition-group": "^4.4.5",
"recoil": "^0.7.7",
"rehype-highlight": "^6.0.0",
"rehype-katex": "^6.0.2",
"rehype-raw": "^6.1.1",

View file

@ -26,7 +26,7 @@
/>
<script
defer
src="main.js"
src="/main.js"
></script>
</head>
<body>
@ -34,7 +34,7 @@
<script
type="text/javascript"
src="main.js"
src="/main.js"
></script>
</body>
</html>

View file

@ -1,53 +1,88 @@
import React, { useEffect, useState } from 'react';
import Messages from './components/Messages';
import Landing from './components/Main/Landing';
import TextChat from './components/Main/TextChat';
import Nav from './components/Nav';
import MobileNav from './components/Nav/MobileNav';
import useDocumentTitle from '~/hooks/useDocumentTitle';
import { useSelector, useDispatch } from 'react-redux';
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
import Root from './routes/Root';
import Chat from './routes/Chat';
import Search from './routes/Search';
import store from './store';
import userAuth from './utils/userAuth';
import { setUser } from './store/userReducer';
import { setSearchState } from './store/searchSlice';
import { useRecoilState, useSetRecoilState } from 'recoil';
import axios from 'axios';
const App = () => {
const dispatch = useDispatch();
const router = createBrowserRouter([
{
path: '/',
element: <Root />,
children: [
{
index: true,
element: (
<Navigate
to="/chat/new"
replace={true}
/>
)
},
{
path: 'chat/:conversationId?',
element: <Chat />
},
{
path: 'search/:query?',
element: <Search />
}
]
}
]);
const { messages, messageTree } = useSelector((state) => state.messages);
const { user } = useSelector((state) => state.user);
const { title } = useSelector((state) => state.convo);
const [navVisible, setNavVisible] = useState(false);
useDocumentTitle(title);
const App = () => {
const [user, setUser] = useRecoilState(store.user);
const setIsSearchEnabled = useSetRecoilState(store.isSearchEnabled);
const setModelsFilter = useSetRecoilState(store.modelsFilter);
useEffect(() => {
axios.get('/api/search/enable').then((res) => { console.log(res.data); dispatch(setSearchState(res.data))});
// fetch if seatch enabled
axios
.get('/api/search/enable', {
timeout: 1000,
withCredentials: true
})
.then(res => {
setIsSearchEnabled(res.data);
});
// fetch user
userAuth()
.then((user) => dispatch(setUser(user)))
.catch((err) => console.log(err));
.then(user => setUser(user))
.catch(err => console.log(err));
// fetch models
axios
.get('/api/models', {
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);
})
.catch(error => {
console.error(error);
console.log('Not login!');
window.location.href = '/auth/login';
});
}, []);
if (user)
return (
<div className="flex h-screen">
<Nav
navVisible={navVisible}
setNavVisible={setNavVisible}
/>
<div className="flex h-full w-full flex-1 flex-col bg-gray-50 md:pl-[260px]">
<div className="transition-width relative flex h-full w-full flex-1 flex-col items-stretch overflow-hidden bg-white dark:bg-gray-800">
<MobileNav setNavVisible={setNavVisible} />
{messages.length === 0 && title.toLowerCase() === 'chatgpt clone' ? (
<Landing title={title} />
) : (
<Messages
messages={messages}
messageTree={messageTree}
/>
)}
<TextChat messages={messages} />
</div>
</div>
<div>
<RouterProvider router={router} />
</div>
);
else return <div className="flex h-screen"></div>;

View file

@ -1,99 +1,124 @@
import React, { useState, useRef } from 'react';
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
import RenameButton from './RenameButton';
import DeleteButton from './DeleteButton';
import { useSelector, useDispatch } from 'react-redux';
import { setConversation } from '~/store/convoSlice';
import { setSubmission, setStopStream, setCustomGpt, setModel, setCustomModel } from '~/store/submitSlice';
import { setMessages, setEmptyMessage } from '~/store/messageSlice';
import { setText } from '~/store/textSlice';
import manualSWR from '~/utils/fetchers';
import ConvoIcon from '../svg/ConvoIcon';
import { refreshConversation } from '../../store/convoSlice';
import manualSWR from '~/utils/fetchers';
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();
export default function Conversation({
id,
model,
parentMessageId,
conversationId,
title,
chatGptLabel = null,
promptPrefix = null,
bingData,
retainView,
}) {
const [renaming, setRenaming] = useState(false);
const [titleInput, setTitleInput] = useState(title);
const { stopStream } = useSelector((state) => state.submit);
const inputRef = useRef(null);
const dispatch = useDispatch();
const { trigger } = manualSWR(`/api/messages/${id}`, 'get');
const {
model,
parentMessageId,
conversationId,
title,
chatGptLabel = null,
promptPrefix = null,
jailbreakConversationId,
conversationSignature,
clientId,
invocationId,
toneStyle
} = conversation;
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 (conversationId === id) {
if (currentConversation?.conversationId === conversationId) {
return;
}
if (!stopStream) {
dispatch(setStopStream(true));
dispatch(setSubmission({}));
}
dispatch(setEmptyMessage());
// stop existing submission
setSubmission(null);
const convo = { title, error: false, conversationId: id, chatGptLabel, promptPrefix };
// set conversation to the new conversation
switchToConversation(conversation);
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 (!stopStream) {
// dispatch(setStopStream(true));
// dispatch(setSubmission({}));
// }
// dispatch(setEmptyMessage());
if (chatGptLabel) {
dispatch(setModel('chatgptCustom'));
dispatch(setCustomModel(chatGptLabel.toLowerCase()));
} else {
dispatch(setModel(model));
dispatch(setCustomModel(null));
}
// const convo = { title, error: false, conversationId: id, chatGptLabel, promptPrefix };
dispatch(setMessages(data));
dispatch(setCustomGpt(convo));
dispatch(setText(''));
dispatch(setStopStream(false));
// 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) => {
const renameHandler = e => {
e.preventDefault();
setTitleInput(title);
setRenaming(true);
@ -102,24 +127,28 @@ export default function Conversation({
}, 25);
};
const cancelHandler = (e) => {
const cancelHandler = e => {
e.preventDefault();
setRenaming(false);
};
const onRename = (e) => {
const onRename = e => {
e.preventDefault();
setRenaming(false);
if (titleInput === title) {
return;
}
rename.trigger({ conversationId, title: titleInput })
.then(() => {
dispatch(refreshConversation())
});
rename.trigger({ conversationId, title: titleInput }).then(() => {
refreshConversations();
if (conversationId == currentConversation?.conversationId)
setCurrentConversation(prevState => ({
...prevState,
title: titleInput
}));
});
};
const handleKeyDown = (e) => {
const handleKeyDown = e => {
if (e.key === 'Enter') {
onRename(e);
}
@ -130,7 +159,7 @@ export default function Conversation({
'animate-flash group relative flex cursor-pointer items-center gap-3 break-all rounded-md bg-gray-800 py-3 px-3 pr-14 hover:bg-gray-800'
};
if (conversationId !== id) {
if (currentConversation?.conversationId !== conversationId) {
aProps.className =
'group relative flex cursor-pointer items-center gap-3 break-all rounded-md py-3 px-3 hover:bg-[#2A2B32] hover:pr-4';
}
@ -148,7 +177,7 @@ export default function Conversation({
type="text"
className="m-0 mr-0 w-full border border-blue-500 bg-transparent p-0 text-sm leading-tight outline-none"
value={titleInput}
onChange={(e) => setTitleInput(e.target.value)}
onChange={e => setTitleInput(e.target.value)}
onBlur={onRename}
onKeyDown={handleKeyDown}
/>
@ -156,16 +185,16 @@ export default function Conversation({
title
)}
</div>
{conversationId === id ? (
{currentConversation?.conversationId === conversationId ? (
<div className="visible absolute right-1 z-10 flex text-gray-300">
<RenameButton
conversationId={id}
conversationId={conversationId}
renaming={renaming}
renameHandler={renameHandler}
onRename={onRename}
/>
<DeleteButton
conversationId={id}
conversationId={conversationId}
renaming={renaming}
cancelHandler={cancelHandler}
retainView={retainView}

View file

@ -2,24 +2,19 @@ import React from 'react';
import TrashIcon from '../svg/TrashIcon';
import CrossIcon from '../svg/CrossIcon';
import manualSWR from '~/utils/fetchers';
import { useDispatch } from 'react-redux';
import { setNewConvo, removeConvo } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmission } from '~/store/submitSlice';
import { useRecoilValue } from 'recoil';
import store from '~/store';
export default function DeleteButton({ conversationId, renaming, cancelHandler, retainView }) {
const dispatch = useDispatch();
const { trigger } = manualSWR(
`/api/convos/clear`,
'post',
() => {
dispatch(setMessages([]));
dispatch(removeConvo(conversationId));
dispatch(setNewConvo());
dispatch(setSubmission({}));
retainView();
}
);
const currentConversation = useRecoilValue(store.conversation) || {};
const { newConversation } = store.useConversation();
const { refreshConversations } = store.useConversations();
const { trigger } = manualSWR(`/api/convos/clear`, 'post', () => {
if (currentConversation?.conversationId == conversationId) newConversation();
refreshConversations();
retainView();
});
const clickHandler = () => trigger({ conversationId, source: 'button' });
const handler = renaming ? cancelHandler : clickHandler;
@ -29,7 +24,7 @@ export default function DeleteButton({ conversationId, renaming, cancelHandler,
className="p-1 hover:text-white"
onClick={handler}
>
{ renaming ? <CrossIcon/> : <TrashIcon />}
{renaming ? <CrossIcon /> : <TrashIcon />}
</button>
);
}

View file

@ -1,12 +1,12 @@
import React from 'react';
export default function Pages({ pageNumber, pages, nextPage, previousPage }) {
const clickHandler = (func) => async (e) => {
const clickHandler = func => async e => {
e.preventDefault();
await func();
};
return (
return pageNumber == 1 && pages == 1 ? null : (
<div className="m-auto mt-4 mb-2 flex items-center justify-center gap-2">
<button
onClick={clickHandler(previousPage)}

View file

@ -2,34 +2,15 @@ import React from 'react';
import Conversation from './Conversation';
export default function Conversations({ conversations, conversationId, moveToTop }) {
return (
<>
{conversations &&
conversations.length > 0 &&
conversations.map((convo) => {
const bingData = convo.conversationSignature
? {
jailbreakConversationId: convo.jailbreakConversationId,
conversationSignature: convo.conversationSignature,
parentMessageId: convo.parentMessageId || null,
clientId: convo.clientId,
invocationId: convo.invocationId,
toneStyle: convo.toneStyle,
}
: null;
conversations.map(convo => {
return (
<Conversation
key={convo.conversationId}
id={convo.conversationId}
model={convo.model}
parentMessageId={convo.parentMessageId}
title={convo.title}
conversationId={conversationId}
chatGptLabel={convo.chatGptLabel}
promptPrefix={convo.promptPrefix}
bingData={bingData}
conversation={convo}
retainView={moveToTop}
/>
);

View file

@ -1,13 +1,13 @@
import React from 'react';
export default function AdjustButton({ onClick }) {
const clickHandler = (e) => {
const clickHandler = e => {
e.preventDefault();
onClick();
};
return (
<button
onClick={clickHandler}
className="group absolute bottom-11 md:bottom-0 -right-11 flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
className="group absolute bottom-11 -right-11 flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500 md:bottom-0"
>
<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
@ -18,11 +18,13 @@ export default function AdjustButton({ onClick }) {
width="1em"
strokeWidth="2"
stroke="currentColor"
className="mr-1 h-4 w-4">
className="mr-1 h-4 w-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" />
d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75"
/>
</svg>
</div>
</button>

View file

@ -1,36 +1,38 @@
import React, { useState, useEffect, forwardRef } from 'react';
import { Tabs, TabsList, TabsTrigger } from '../ui/Tabs.tsx';
import { useDispatch, useSelector } from 'react-redux';
import { setConversation } from '~/store/convoSlice';
import { useRecoilValue, useRecoilState } from 'recoil';
// import { setConversation } from '~/store/convoSlice';
import store from '~/store';
function BingStyles(props, ref) {
const dispatch = useDispatch();
const [value, setValue] = useState('fast');
const { model } = useSelector((state) => state.submit);
const { conversationId } = useSelector((state) => state.convo);
const { messages } = useSelector((state) => state.messages);
const [conversation, setConversation] = useRecoilState(store.conversation) || {};
const { model, conversationId } = conversation;
const messages = useRecoilValue(store.messages);
const isBing = model === 'bingai' || model === 'sydney';
useEffect(() => {
if (model === 'bingai' && !conversationId || model === 'sydney') {
dispatch(setConversation({ toneStyle: value }));
if ((model === 'bingai' && !conversationId) || model === 'sydney') {
setConversation(prevState => ({ ...prevState, toneStyle: value }));
}
}, [conversationId, model, value, dispatch]);
}, [conversationId, model, value]);
const show = isBing && ((!conversationId || messages?.length === 0) || props.show);
const defaultClasses = 'p-2 rounded-md font-normal bg-white/[.60] dark:bg-gray-700 text-black';
const defaultSelected = defaultClasses + 'font-medium data-[state=active]:text-white';
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 selectedClass = (val) => val + '-tab ' + defaultSelected;
const selectedClass = val => val + '-tab ' + defaultSelected;
const changeHandler = value => {
setValue(value);
dispatch(setConversation({ toneStyle: value }));
setConversation(prevState => ({ ...prevState, toneStyle: value }));
};
return (
<Tabs
defaultValue={value}
className={`shadow-md mb-1 bing-styles ${show ? 'show' : ''}`}
className={`bing-styles mb-1 shadow-md ${show ? 'show' : ''}`}
onValueChange={changeHandler}
ref={ref}
>

View file

@ -4,16 +4,12 @@ import ModelItem from './ModelItem';
export default function MenuItems({ models, onSelect }) {
return (
<>
{models.map((modelItem) => (
{models.map(modelItem => (
<ModelItem
key={modelItem._id}
id={modelItem._id}
modelName={modelItem.name}
value={modelItem.value}
model={modelItem.model || 'chatgptCustom'}
onSelect={onSelect}
chatGptLabel={modelItem.chatGptLabel}
promptPrefix={modelItem.promptPrefix}
model={modelItem}
/>
))}
</>

View file

@ -1,12 +1,9 @@
import React, { useState, useRef } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { useSelector, useDispatch } from 'react-redux';
import { setSubmission, setModel, setCustomGpt } from '~/store/submitSlice';
import { setNewConvo } from '~/store/convoSlice';
import manualSWR from '~/utils/fetchers';
import { Button } from '../ui/Button.tsx';
import { Input } from '../ui/Input.tsx';
import { Label } from '../ui/Label.tsx';
import { Button } from '../../ui/Button.tsx';
import { Input } from '../../ui/Input.tsx';
import { Label } from '../../ui/Label.tsx';
import {
DialogClose,
@ -15,11 +12,13 @@ import {
DialogFooter,
DialogHeader,
DialogTitle
} from '../ui/Dialog.tsx';
} from '../../ui/Dialog.tsx';
import store from '~/store';
export default function ModelDialog({ mutate, setModelSave, handleSaveState }) {
const dispatch = useDispatch();
const { modelMap, initial } = useSelector(state => state.models);
const { newConversation } = store.useConversation();
const [chatGptLabel, setChatGptLabel] = useState('');
const [promptPrefix, setPromptPrefix] = useState('');
const [saveText, setSaveText] = useState('Save');
@ -34,12 +33,15 @@ export default function ModelDialog({ mutate, setModelSave, handleSaveState }) {
inputRef.current.focus();
return;
}
dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
dispatch(setModel('chatgptCustom'));
handleSaveState(chatGptLabel.toLowerCase());
// Set new conversation
dispatch(setNewConvo());
dispatch(setSubmission({}));
newConversation({
model: 'chatgptCustom',
chatGptLabel,
promptPrefix
});
};
const saveHandler = e => {
@ -61,21 +63,25 @@ export default function ModelDialog({ mutate, setModelSave, handleSaveState }) {
setSaveText('Save');
}, 2500);
dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
dispatch(setModel('chatgptCustom'));
// dispatch(setDisabled(false));
// dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
newConversation({
model: 'chatgptCustom',
chatGptLabel,
promptPrefix
});
};
if (
chatGptLabel !== 'chatgptCustom' &&
modelMap[chatGptLabel.toLowerCase()] &&
!initial[chatGptLabel.toLowerCase()] &&
saveText === 'Save'
) {
setSaveText('Update');
} else if (!modelMap[chatGptLabel.toLowerCase()] && saveText === 'Update') {
setSaveText('Save');
}
// 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 } : {};

View file

@ -1,36 +1,61 @@
import React, { useState, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { DropdownMenuRadioItem } from '../ui/DropdownMenu.tsx';
import { setModels } from '~/store/modelSlice';
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 { 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 { 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) || {};
export default function ModelItem({ modelName, value, model, onSelect, id, chatGptLabel, promptPrefix }) {
const dispatch = useDispatch();
const { customModel } = useSelector((state) => state.submit);
const { initial } = useSelector((state) => state.models);
const [isHovering, setIsHovering] = useState(false);
const [renaming, setRenaming] = useState(false);
const [currentName, setCurrentName] = useState(modelName);
const [modelInput, setModelInput] = useState(modelName);
const [currentName, setCurrentName] = useState(name);
const [modelInput, setModelInput] = useState(name);
const inputRef = useRef(null);
const rename = manualSWR(`/api/customGpts`, 'post');
const deleteCustom = manualSWR(`/api/customGpts/delete`, 'post', (res) => {
const fetchedModels = res.data.map((modelItem) => ({
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
name: modelItem.chatGptLabel,
model: 'chatgptCustom'
}));
dispatch(setModels(fetchedModels));
setCustomGPTModels(fetchedModels);
});
const icon = getIconOfModel({ size: 20, sender: modelName, isCreatedByUser: false, model, chatGptLabel, promptPrefix, error: false, className: "mr-2" });
const icon = getIconOfModel({
size: 20,
sender: chatGptLabel || model,
isCreatedByUser: false,
model,
chatGptLabel,
promptPrefix,
error: false,
className: 'mr-2'
});
if (value === 'chatgptCustom') {
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
@ -38,26 +63,13 @@ export default function ModelItem({ modelName, value, model, onSelect, id, chatG
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
>
{icon}
{modelName}
{name}
<sup>$</sup>
</DropdownMenuRadioItem>
</DialogTrigger>
);
}
if (initial[value])
return (
<DropdownMenuRadioItem
value={value}
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
>
{icon}
{modelName}
{value === 'chatgpt' && <sup>$</sup>}
</DropdownMenuRadioItem>
);
// else: a chatgptCustom model
const handleMouseOver = () => {
setIsHovering(true);
};
@ -66,7 +78,7 @@ export default function ModelItem({ modelName, value, model, onSelect, id, chatG
setIsHovering(false);
};
const renameHandler = (e) => {
const renameHandler = e => {
e.preventDefault();
e.stopPropagation();
setRenaming(true);
@ -75,10 +87,10 @@ export default function ModelItem({ modelName, value, model, onSelect, id, chatG
}, 25);
};
const onRename = (e) => {
const onRename = e => {
e.preventDefault();
setRenaming(false);
if (modelInput === modelName) {
if (modelInput === name) {
return;
}
rename.trigger({
@ -89,13 +101,13 @@ export default function ModelItem({ modelName, value, model, onSelect, id, chatG
setCurrentName(modelInput);
};
const onDelete = async (e) => {
const onDelete = async e => {
e.preventDefault();
await deleteCustom.trigger({ _id: id });
onSelect('chatgpt', true);
onSelect('chatgpt');
};
const handleKeyDown = (e) => {
const handleKeyDown = e => {
if (e.key === 'Enter') {
onRename(e);
}
@ -115,14 +127,14 @@ export default function ModelItem({ modelName, value, model, onSelect, id, chatG
<span
value={value}
className={itemClass.className}
onClick={(e) => {
onClick={e => {
if (isHovering) {
return;
}
onSelect(value, true);
onSelect('chatgptCustom', value);
}}
>
{customModel === 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>
@ -137,8 +149,8 @@ export default function ModelItem({ modelName, value, model, onSelect, id, chatG
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)}
onClick={e => e.stopPropagation()}
onChange={e => setModelInput(e.target.value)}
// onBlur={onRename}
onKeyDown={handleKeyDown}
/>

View file

@ -0,0 +1,205 @@
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>
);
}

View file

@ -1,11 +1,7 @@
import React from 'react';
import { useSelector } from 'react-redux';
export default function SubmitButton({ submitMessage, disabled }) {
const { isSubmitting } = useSelector((state) => state.submit);
const { error, latestMessage } = useSelector((state) => state.convo);
const clickHandler = (e) => {
export default function SubmitButton({ submitMessage, disabled, isSubmitting }) {
const clickHandler = e => {
e.preventDefault();
submitMessage();
};
@ -13,13 +9,23 @@ export default function SubmitButton({ submitMessage, disabled }) {
if (isSubmitting) {
return (
<button
className="absolute bottom-0 right-1 h-[100%] w-[30px] 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"
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>·</span>
<span className="blink">·</span>
<span className="blink2">·</span>
<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>
);

View file

@ -0,0 +1,201 @@
import React, { useEffect, useRef, useState } 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 Footer from './Footer';
import TextareaAutosize from 'react-textarea-autosize';
import createPayload from '~/utils/createPayload';
import RegenerateIcon from '../svg/RegenerateIcon';
import StopGeneratingIcon from '../svg/StopGeneratingIcon';
import { useMessageHandler } from '../../utils/handleSubmit';
import store from '~/store';
export default function TextChat({ isSearchView = false }) {
const inputRef = useRef(null);
const isComposing = useRef(false);
const conversation = useRecoilValue(store.conversation);
const latestMessage = useRecoilValue(store.latestMessage);
const messages = useRecoilValue(store.messages);
const isSubmitting = useRecoilValue(store.isSubmitting);
// TODO: do we need this?
const disabled = false;
const [text, setText] = useState('');
const { ask, regenerate, stopGenerating } = useMessageHandler();
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('');
}, [conversation?.conversationId]);
// 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 submitMessage = () => {
ask({ text });
setText('');
};
const handleRegenerate = () => {
if (latestMessage && !latestMessage?.isCreatedByUser) regenerate(latestMessage);
};
const handleStopGenerating = () => {
stopGenerating();
};
const handleKeyDown = e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
}
if (e.key === 'Enter' && !e.shiftKey) {
if (!isComposing?.current) submitMessage();
}
};
const handleKeyUp = e => {
if (e.keyCode === 8 && e.target.value.trim() === '') {
setText(e.target.value);
}
if (e.key === 'Enter' && e.shiftKey) {
return console.log('Enter + Shift');
}
if (isSubmitting) {
return;
}
};
const handleCompositionStart = () => {
isComposing.current = true;
};
const handleCompositionEnd = () => {
isComposing.current = false;
};
const changeHandler = e => {
const { value } = e.target;
setText(value);
};
const getPlaceholderText = () => {
if (isSearchView) {
return 'Click a message title to open its conversation.';
}
if (disabled) {
return 'Choose another model or customize GPT again';
}
if (isNotAppendable) {
return 'Edit your message or Regenerate.';
}
return '';
};
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>
</div>
</form>
<Footer />
</div>
</>
);
}

View file

@ -1,35 +0,0 @@
import React from 'react';
import RegenerateIcon from '../svg/RegenerateIcon';
export default function Regenerate({ submitMessage, tryAgain, errorMessage }) {
const clickHandler = (e) => {
e.preventDefault();
submitMessage();
};
return (
<>
<span className="mb-2 block flex justify-center text-xs text-black dark:text-white/50 md:mb-2">
There was an error generating a response
</span>
<span className="m-auto flex justify-center">
{!errorMessage.includes('short') && (
<button
onClick={clickHandler}
className="btn btn-primary m-auto flex justify-center gap-2"
>
<RegenerateIcon />
Regenerate response
</button>
)}
<button
onClick={tryAgain}
className="btn btn-neutral flex justify-center gap-2 border-0 md:border"
>
<RegenerateIcon />
Try another message
</button>
</span>
</>
);
}

View file

@ -1,446 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { SSE } from '~/utils/sse';
import SubmitButton from './SubmitButton';
// import Regenerate from './Regenerate'; // not used as of Wentao's update
import BingStyles from './BingStyles';
import ModelMenu from '../Models/ModelMenu';
import Footer from './Footer';
import TextareaAutosize from 'react-textarea-autosize';
import createPayload from '~/utils/createPayload';
import RegenerateIcon from '../svg/RegenerateIcon';
import StopGeneratingIcon from '../svg/StopGeneratingIcon';
import { setConversation, setError, refreshConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmitState, toggleCursor } from '~/store/submitSlice';
import { setText } from '~/store/textSlice';
import { useMessageHandler } from '../../utils/handleSubmit';
import AdjustToneButton from './AdjustToneButton';
export default function TextChat({ messages }) {
const inputRef = useRef(null);
const bingStylesRef = useRef(null);
const [showBingToneSetting, setShowBingToneSetting] = useState(false);
const isComposing = useRef(false);
const dispatch = useDispatch();
const convo = useSelector(state => state.convo);
const { isSubmitting, stopStream, submission, disabled, model } = useSelector(state => state.submit);
const { text } = useSelector(state => state.text);
const { latestMessage } = convo;
const { ask, regenerate, stopGenerating } = useMessageHandler();
const isNotAppendable = latestMessage?.cancelled || latestMessage?.error;
// auto focus to input, when enter a conversation.
useEffect(() => {
inputRef.current?.focus();
}, [convo?.conversationId]);
// 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 messageHandler = (data, currentState, currentMsg) => {
const { messages, message, sender, isRegenerate } = currentState;
if (isRegenerate)
dispatch(
setMessages([
...messages,
{
sender,
text: data,
parentMessageId: message?.overrideParentMessageId,
messageId: message?.overrideParentMessageId + '_',
submitting: true
}
])
);
else
dispatch(
setMessages([
...messages,
currentMsg,
{
sender,
text: data,
parentMessageId: currentMsg?.messageId,
messageId: currentMsg?.messageId + '_',
submitting: true
}
])
);
};
const cancelHandler = (data, currentState, currentMsg) => {
const { messages, message, sender, isRegenerate } = currentState;
if (isRegenerate)
dispatch(
setMessages([
...messages,
{
sender,
text: data,
parentMessageId: message?.overrideParentMessageId,
messageId: message?.overrideParentMessageId + '_',
cancelled: true
}
])
);
else
dispatch(
setMessages([
...messages,
currentMsg,
{
sender,
text: data,
parentMessageId: currentMsg?.messageId,
messageId: currentMsg?.messageId + '_',
cancelled: true
}
])
);
};
const createdHandler = (data, currentState, currentMsg) => {
const { conversationId } = currentMsg;
dispatch(
setConversation({
conversationId,
latestMessage: null
})
);
};
const convoHandler = (data, currentState) => {
const { requestMessage, responseMessage } = data;
const { messages, message, isCustomModel, isRegenerate } = currentState;
const { model, chatGptLabel, promptPrefix } = message;
if (isRegenerate) dispatch(setMessages([...messages, responseMessage]));
else dispatch(setMessages([...messages, requestMessage, responseMessage]));
dispatch(setSubmitState(false));
const isBing = model === 'bingai' || model === 'sydney';
// refresh title
if (requestMessage.parentMessageId == '00000000-0000-0000-0000-000000000000') {
setTimeout(() => {
dispatch(refreshConversation());
}, 2000);
// in case it takes too long.
setTimeout(() => {
dispatch(refreshConversation());
}, 5000);
}
if (!isBing && convo.conversationId === null && convo.parentMessageId === null) {
const { title } = data;
const { conversationId, messageId } = responseMessage;
dispatch(
setConversation({
title,
conversationId,
parentMessageId: messageId,
jailbreakConversationId: null,
conversationSignature: null,
clientId: null,
invocationId: null,
chatGptLabel: model === isCustomModel ? chatGptLabel : null,
promptPrefix: model === isCustomModel ? promptPrefix : null,
latestMessage: null
})
);
} else if (model === 'bingai') {
console.log('Bing data:', data);
const { title } = data;
const { conversationSignature, clientId, conversationId, invocationId, parentMessageId } =
responseMessage;
dispatch(
setConversation({
title,
parentMessageId,
conversationSignature,
clientId,
conversationId,
invocationId,
latestMessage: null
})
);
} else if (model === 'sydney') {
const { title } = data;
const {
jailbreakConversationId,
parentMessageId,
conversationSignature,
clientId,
conversationId,
invocationId
} = responseMessage;
dispatch(
setConversation({
title,
jailbreakConversationId,
parentMessageId,
conversationSignature,
clientId,
conversationId,
invocationId,
latestMessage: null
})
);
}
};
const errorHandler = (data, currentState, currentMsg) => {
const { messages, message } = currentState;
console.log('Error:', data);
const errorResponse = {
...data,
error: true,
parentMessageId: currentMsg?.messageId
};
dispatch(setSubmitState(false));
dispatch(setMessages([...messages, currentMsg, errorResponse]));
dispatch(setText(message?.text));
dispatch(setError(true));
return;
};
const submitMessage = () => {
ask({ text });
};
useEffect(() => {
inputRef.current?.focus();
if (Object.keys(submission).length === 0) {
return;
}
const currentState = submission;
let currentMsg = { ...currentState.message };
let latestResponseText = '';
const { server, payload } = createPayload(submission);
const onMessage = e => {
if (stopStream) {
return;
}
const data = JSON.parse(e.data);
if (data.final) {
convoHandler(data, currentState);
dispatch(toggleCursor());
console.log('final', data);
}
if (data.created) {
currentMsg = data.message;
createdHandler(data, currentState, currentMsg);
} else {
let text = data.text || data.response;
if (data.initial) {
dispatch(toggleCursor());
}
if (data.message) {
latestResponseText = text;
messageHandler(text, currentState, currentMsg);
}
// console.log('dataStream', data);
}
};
const events = new SSE(server, {
payload: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' }
});
events.onopen = function () {
console.log('connection is opened');
};
events.onmessage = onMessage;
events.oncancel = () => {
dispatch(toggleCursor(true));
cancelHandler(latestResponseText, currentState, currentMsg);
};
events.onerror = function (e) {
console.log('error in opening conn.');
events.close();
const data = JSON.parse(e.data);
dispatch(toggleCursor(true));
errorHandler(data, currentState, currentMsg);
};
events.stream();
return () => {
events.removeEventListener('message', onMessage);
dispatch(toggleCursor(true));
const isCancelled = events.readyState <= 1;
events.close();
if (isCancelled) {
const e = new Event('cancel');
events.dispatchEvent(e);
}
};
}, [submission]);
const handleRegenerate = () => {
if (latestMessage && !latestMessage?.isCreatedByUser) regenerate(latestMessage);
};
const handleStopGenerating = () => {
stopGenerating();
};
const handleKeyDown = e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
}
if (e.key === 'Enter' && !e.shiftKey) {
if (!isComposing.current) submitMessage();
}
};
const handleKeyUp = e => {
if (e.keyCode === 8 && e.target.value.trim() === '') {
dispatch(setText(e.target.value));
}
if (e.key === 'Enter' && e.shiftKey) {
return console.log('Enter + Shift');
}
if (isSubmitting) {
return;
}
};
const handleCompositionStart = () => {
isComposing.current = true;
};
const handleCompositionEnd = () => {
isComposing.current = false;
};
const changeHandler = e => {
const { value } = e.target;
// if (isSubmitting && (value === '' || value === '\n')) {
// return;
// }
dispatch(setText(value));
};
// const tryAgain = (e) => {
// e.preventDefault();
// dispatch(setError(false));
// };
const isSearchView = messages?.[0]?.searchResult === true;
const getPlaceholderText = () => {
if (isSearchView) {
return 'Click a message title to open its conversation.';
}
if (disabled) {
return 'Choose another model or customize GPT again';
}
if (isNotAppendable) {
return 'Edit your message or Regenerate.';
}
return '';
};
const handleBingToneSetting = () => {
setShowBingToneSetting((show) => !show)
}
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 && !isSearchView ? (
<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 && !isSearchView ? (
<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 && model === 'sydney' ?
<AdjustToneButton onClick={handleBingToneSetting} /> :
null}
</div>
</div>
</form>
<Footer />
</div>
</>
);
}

View file

@ -0,0 +1,274 @@
import React, { useEffect, useRef, useState } from 'react';
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
import { SSE } from '~/utils/sse';
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);
const setMessages = useSetRecoilState(store.messages);
const setConversation = useSetRecoilState(store.conversation);
const resetLatestMessage = useResetRecoilState(store.latestMessage);
const { refreshConversations } = store.useConversations();
const messageHandler = (data, submission) => {
const { messages, message, initialResponse, isRegenerate = false } = submission;
if (isRegenerate)
setMessages([
...messages,
{
...initialResponse,
text: data,
parentMessageId: message?.overrideParentMessageId,
messageId: message?.overrideParentMessageId + '_',
submitting: true
}
]);
else
setMessages([
...messages,
message,
{
...initialResponse,
text: data,
parentMessageId: message?.messageId,
messageId: message?.messageId + '_',
submitting: true
}
]);
};
const cancelHandler = (data, submission) => {
const { messages, message, initialResponse, isRegenerate = false } = submission;
if (isRegenerate)
setMessages([
...messages,
{
...initialResponse,
text: data,
parentMessageId: message?.overrideParentMessageId,
messageId: message?.overrideParentMessageId + '_',
cancelled: true
}
]);
else
setMessages([
...messages,
message,
{
...initialResponse,
text: data,
parentMessageId: message?.messageId,
messageId: message?.messageId + '_',
cancelled: true
}
]);
};
const createdHandler = (data, submission) => {
const { messages, message, initialResponse, isRegenerate = false } = submission;
if (isRegenerate)
setMessages([
...messages,
{
...initialResponse,
parentMessageId: message?.overrideParentMessageId,
messageId: message?.overrideParentMessageId + '_',
submitting: true
}
]);
else
setMessages([
...messages,
message,
{
...initialResponse,
parentMessageId: message?.messageId,
messageId: message?.messageId + '_',
submitting: true
}
]);
const { conversationId } = message;
setConversation(prevState => ({
...prevState,
conversationId
}));
resetLatestMessage();
};
const finalHandler = (data, submission) => {
const { conversation, messages, message, initialResponse, isRegenerate = false } = submission;
const { requestMessage, responseMessage } = data;
const { conversationId } = requestMessage;
// update the messages
if (isRegenerate) setMessages([...messages, responseMessage]);
else setMessages([...messages, requestMessage, responseMessage]);
setIsSubmitting(false);
// refresh title
if (requestMessage.parentMessageId == '00000000-0000-0000-0000-000000000000') {
setTimeout(() => {
refreshConversations();
}, 2000);
// in case it takes too long.
setTimeout(() => {
refreshConversations();
}, 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
}));
}
};
const errorHandler = (data, submission) => {
const { conversation, messages, message, initialResponse, isRegenerate = false } = submission;
console.log('Error:', data);
const errorResponse = {
...data,
error: true,
parentMessageId: message?.messageId
};
setIsSubmitting(false);
setMessages([...messages, message, errorResponse]);
return;
};
useEffect(() => {
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);
const events = new SSE(server, {
payload: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' }
});
let latestResponseText = '';
events.onmessage = e => {
const data = JSON.parse(e.data);
if (data.final) {
finalHandler(data, { ...submission, message });
console.log('final', data);
}
if (data.created) {
message = {
...data.message,
model: message?.model,
chatGptLabel: message?.chatGptLabel,
promptPrefix: message?.promptPrefix,
overrideParentMessageId: message?.overrideParentMessageId
};
createdHandler(data, { ...submission, message });
console.log('created', message);
} else {
let text = data.text || data.response;
if (data.initial) console.log(data);
if (data.message) {
latestResponseText = text;
messageHandler(text, { ...submission, message });
}
// console.log('dataStream', data);
}
};
events.onopen = () => console.log('connection is opened');
events.oncancel = e => cancelHandler(latestResponseText, { ...submission, message });
events.onerror = function (e) {
console.log('error in opening conn.');
events.close();
const data = JSON.parse(e.data);
errorHandler(data, { ...submission, message });
};
setIsSubmitting(true);
events.stream();
return () => {
const isCancelled = events.readyState <= 1;
events.close();
if (isCancelled) {
const e = new Event('cancel');
events.dispatchEvent(e);
}
setIsSubmitting(false);
};
}, [submission]);
return null;
}

View file

@ -1,20 +1,19 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useRecoilValue, useSetRecoilState, useResetRecoilState } from 'recoil';
import SubRow from './Content/SubRow';
import Content from './Content/Content';
import MultiMessage from './MultiMessage';
import HoverButtons from './HoverButtons';
import SiblingSwitch from './SiblingSwitch';
import { setConversation, setLatestMessage } from '~/store/convoSlice';
import { setModel, setCustomModel, setCustomGpt, setDisabled } from '~/store/submitSlice';
import { setMessages } from '~/store/messageSlice';
import { fetchById } from '~/utils/fetchers';
import { getIconOfModel } from '~/utils';
import { useMessageHandler } from '~/utils/handleSubmit';
import store from '~/store';
export default function Message({
conversation,
message,
messages,
scrollToBottom,
currentEditId,
setCurrentEditId,
@ -22,30 +21,34 @@ export default function Message({
siblingCount,
setSiblingIdx
}) {
const { isSubmitting, model, chatGptLabel, cursor, promptPrefix } = useSelector(state => state.submit);
const isSubmitting = useRecoilValue(store.isSubmitting);
const setLatestMessage = useSetRecoilState(store.latestMessage);
const { model, chatGptLabel, promptPrefix } = conversation;
const [abortScroll, setAbort] = useState(false);
const { sender, text, searchResult, isCreatedByUser, error, submitting } = message;
const {
sender,
text,
searchResult,
isCreatedByUser,
error,
submitting,
model: messageModel,
chatGptLabel: messageChatGptLabel,
searchResult: isSearchResult
} = message;
const textEditor = useRef(null);
const last = !message?.children?.length;
const edit = message.messageId == currentEditId;
const { ask } = useMessageHandler();
const dispatch = useDispatch();
// const currentConvo = convoMap[message.conversationId];
// const notUser = !isCreatedByUser; // sender.toLowerCase() !== 'user';
// const blinker = submitting && isSubmitting && last && !isCreatedByUser;
const { switchToConversation } = store.useConversation();
const blinker = submitting && isSubmitting;
const generateCursor = useCallback(() => {
if (!blinker) {
return '';
}
if (!cursor) {
return '';
}
return <span className="result-streaming"></span>;
}, [blinker, cursor]);
}, [blinker]);
useEffect(() => {
if (blinker && !abortScroll) {
@ -55,9 +58,7 @@ export default function Message({
useEffect(() => {
if (last) {
// TODO: stop using conversation.parentMessageId and remove it.
dispatch(setConversation({ parentMessageId: message?.messageId }));
dispatch(setLatestMessage({ ...message }));
setLatestMessage({ ...message });
}
}, [last, message]);
@ -79,9 +80,9 @@ export default function Message({
const icon = getIconOfModel({
sender,
isCreatedByUser,
model,
model: isSearchResult ? messageModel : model,
searchResult,
chatGptLabel,
chatGptLabel: isSearchResult ? messageChatGptLabel : chatGptLabel,
promptPrefix,
error
});
@ -92,7 +93,7 @@ export default function Message({
if (message.bg && searchResult) {
props.className = message.bg.split('hover')[0];
props.titleClass = message.bg.split(props.className)[1] + ' cursor-pointer';
props.titleclass = message.bg.split(props.className)[1] + ' cursor-pointer';
}
const resubmitMessage = () => {
@ -110,22 +111,10 @@ export default function Message({
const clickSearchResult = async () => {
if (!searchResult) return;
dispatch(setMessages([]));
const convoResponse = await fetchById('convos', message.conversationId);
const convo = convoResponse.data;
if (convo?.chatGptLabel) {
dispatch(setModel('chatgptCustom'));
dispatch(setCustomModel(convo.chatGptLabel.toLowerCase()));
} else {
dispatch(setModel(convo.model));
dispatch(setCustomModel(null));
}
dispatch(setCustomGpt(convo));
dispatch(setConversation(convo));
const { data } = await fetchById('messages', message.conversationId);
dispatch(setMessages(data));
dispatch(setDisabled(false));
switchToConversation(convo);
};
return (
@ -133,7 +122,6 @@ export default function Message({
<div
{...props}
onWheel={handleWheel}
// onClick={clickSearchResult}
>
<div className="relative m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
<div className="relative flex h-[30px] w-[30px] flex-col items-end text-right text-xs md:text-sm">
@ -153,7 +141,7 @@ export default function Message({
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 whitespace-pre-wrap md:gap-3 lg:w-[calc(100%-115px)]">
{searchResult && (
<SubRow
classes={props.titleClass + ' rounded'}
classes={props.titleclass + ' rounded'}
subclasses="switch-result pl-2 pb-2"
onClick={clickSearchResult}
>
@ -199,17 +187,13 @@ export default function Message({
<div className="flex min-h-[20px] flex-grow flex-col items-start gap-4 whitespace-pre-wrap">
{/* <div className={`${blinker ? 'result-streaming' : ''} markdown prose dark:prose-invert light w-full break-words`}> */}
<div className="markdown prose dark:prose-invert light w-full break-words">
{!isCreatedByUser ?
<>
<Content
content={text}
/>
{generateCursor()}
</> :
<>
{text}
</>
}
{!isCreatedByUser ? (
<>
<Content content={text} />
</>
) : (
<>{text}</>
)}
</div>
</div>
)}
@ -230,8 +214,8 @@ export default function Message({
</div>
</div>
<MultiMessage
messageList={message.children}
messages={messages}
conversation={conversation}
messagesTree={message.children}
scrollToBottom={scrollToBottom}
currentEditId={currentEditId}
setCurrentEditId={setCurrentEditId}

View file

@ -2,44 +2,65 @@ import React, { useEffect, useState } from 'react';
import Message from './Message';
export default function MultiMessage({
messageList,
messages,
conversation,
messagesTree,
scrollToBottom,
currentEditId,
setCurrentEditId,
isSearchView
}) {
const [siblingIdx, setSiblingIdx] = useState(0);
const setSiblingIdxRev = (value) => {
setSiblingIdx(messageList?.length - value - 1);
const setSiblingIdxRev = value => {
setSiblingIdx(messagesTree?.length - value - 1);
};
useEffect(() => {
// reset siblingIdx when changes, mostly a new message is submitting.
setSiblingIdx(0);
}, [messageList?.length])
}, [messagesTree?.length]);
// if (!messageList?.length) return null;
if (!(messageList && messageList.length)) {
if (!(messagesTree && messagesTree.length)) {
return null;
}
if (siblingIdx >= messageList?.length) {
if (siblingIdx >= messagesTree?.length) {
setSiblingIdx(0);
return null;
}
const message = messageList[messageList.length - siblingIdx - 1];
const message = messagesTree[messagesTree.length - siblingIdx - 1];
if (isSearchView)
return (
<>
{messagesTree
? messagesTree.map(message => (
<Message
key={message.messageId}
conversation={conversation}
message={message}
scrollToBottom={scrollToBottom}
currentEditId={currentEditId}
setCurrentEditId={null}
siblingIdx={1}
siblingCount={1}
setSiblingIdx={null}
/>
))
: null}
</>
);
return (
<Message
key={message.messageId}
conversation={conversation}
message={message}
messages={messages}
scrollToBottom={scrollToBottom}
currentEditId={currentEditId}
setCurrentEditId={setCurrentEditId}
siblingIdx={messageList.length - siblingIdx - 1}
siblingCount={messageList.length}
siblingIdx={messagesTree.length - siblingIdx - 1}
siblingCount={messagesTree.length}
setSiblingIdx={setSiblingIdxRev}
/>
);

View file

@ -1,27 +1,37 @@
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import Spinner from '../svg/Spinner';
import { throttle } from 'lodash';
import { CSSTransition } from 'react-transition-group';
import ScrollToBottom from './ScrollToBottom';
import MultiMessage from './MultiMessage';
import { useSelector } from 'react-redux';
export default function Messages({ messages, messageTree }) {
import store from '~/store';
export default function Messages({ isSearchView = false }) {
const [currentEditId, setCurrentEditId] = useState(-1);
const { conversationId } = useSelector((state) => state.convo);
const { model, customModel } = useSelector((state) => state.submit);
const { models } = useSelector((state) => state.models);
const [showScrollButton, setShowScrollButton] = useState(false);
const scrollableRef = useRef(null);
const messagesEndRef = useRef(null);
const modelName = models.find((element) => element.model == model)?.name;
const messagesTree = useRecoilValue(store.messagesTree);
const searchResultMessagesTree = useRecoilValue(store.searchResultMessagesTree);
const _messagesTree = isSearchView ? searchResultMessagesTree : messagesTree;
const conversation = useRecoilValue(store.conversation) || {};
const { conversationId, model, chatGptLabel } = conversation;
const models = useRecoilValue(store.models) || [];
const modelName = models.find(element => element.model == model)?.name;
const searchQuery = useRecoilValue(store.searchQuery);
useEffect(() => {
const timeoutId = setTimeout(() => {
const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current;
const diff = Math.abs(scrollHeight - scrollTop);
const percent = Math.abs(clientHeight - diff ) / clientHeight;
const percent = Math.abs(clientHeight - diff) / clientHeight;
const hasScrollbar = scrollHeight > clientHeight && percent > 0.2;
setShowScrollButton(hasScrollbar);
}, 650);
@ -33,17 +43,24 @@ export default function Messages({ messages, messageTree }) {
clearTimeout(timeoutId);
window.removeEventListener('scroll', handleScroll);
};
}, [messages]);
}, [_messagesTree]);
const scrollToBottom = useCallback(throttle(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
setShowScrollButton(false);
}, 750, { leading: true }), [messagesEndRef]);
const scrollToBottom = useCallback(
throttle(
() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
setShowScrollButton(false);
},
750,
{ leading: true }
),
[messagesEndRef]
);
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current;
const diff = Math.abs(scrollHeight - scrollTop);
const percent = Math.abs(clientHeight - diff ) / clientHeight;
const percent = Math.abs(clientHeight - diff) / clientHeight;
if (percent <= 0.2) {
setShowScrollButton(false);
} else {
@ -57,7 +74,7 @@ export default function Messages({ messages, messageTree }) {
timeoutId = setTimeout(handleScroll, 100);
};
const scrollHandler = (e) => {
const scrollHandler = e => {
e.preventDefault();
scrollToBottom();
};
@ -68,23 +85,29 @@ export default function Messages({ messages, messageTree }) {
ref={scrollableRef}
onScroll={debouncedHandleScroll}
>
{/* <div className="flex-1 overflow-hidden"> */}
<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">
Model: {modelName} {customModel ? `(${customModel})` : null}
{isSearchView
? `Search: ${searchQuery}`
: `Model: ${modelName} ${chatGptLabel ? `(${chatGptLabel})` : ''}`}
</div>
{(messageTree.length === 0 || !messages) ? (
{_messagesTree === null ? (
<Spinner />
) : _messagesTree?.length == 0 && isSearchView ? (
<div className="flex w-full items-center justify-center gap-1 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-900/50 dark:bg-gray-800 dark:text-gray-300">
Nothing found
</div>
) : (
<>
<MultiMessage
key={conversationId} // avoid internal state mixture
messageList={messageTree}
messages={messages}
conversation={conversation}
messagesTree={_messagesTree}
scrollToBottom={scrollToBottom}
currentEditId={currentEditId}
setCurrentEditId={setCurrentEditId}
isSearchView={isSearchView}
/>
<CSSTransition
in={showScrollButton}
@ -103,7 +126,6 @@ export default function Messages({ messages, messageTree }) {
/>
</div>
</div>
{/* </div> */}
</div>
);
}

View file

@ -1,223 +0,0 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useSelector, useDispatch } from 'react-redux';
import {
setSubmission,
setModel,
setDisabled,
setCustomGpt,
setCustomModel
} from '~/store/submitSlice';
import { setNewConvo } from '~/store/convoSlice';
import ModelDialog from './ModelDialog';
import MenuItems from './MenuItems';
import { swr } from '~/utils/fetchers';
import { setModels, setInitial } from '~/store/modelSlice';
import { setMessages } from '~/store/messageSlice';
import { setText } from '~/store/textSlice';
import { Button } from '../ui/Button.tsx';
import { getIconOfModel } from '../../utils';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '../ui/DropdownMenu.tsx';
import { Dialog } from '../ui/Dialog.tsx';
export default function ModelMenu() {
const dispatch = useDispatch();
const [modelSave, setModelSave] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const { model, customModel, promptPrefix, chatGptLabel } = useSelector((state) => state.submit);
const { models, modelMap, initial } = useSelector((state) => state.models);
const { data, isLoading, mutate } = swr(`/api/customGpts`, (res) => {
const fetchedModels = res.map((modelItem) => ({
...modelItem,
name: modelItem.chatGptLabel,
model: 'chatgptCustom'
}));
dispatch(setModels(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
}, []);
useEffect(() => {
axios.get('/api/models', {
timeout: 1000,
withCredentials: true
}).then((res) => {
return res.data
}).then((data) => {
const initial = {chatgpt: data?.hasOpenAI, chatgptCustom: data?.hasOpenAI, bingai: data?.hasBing, sydney: data?.hasBing, chatgptBrowser: data?.hasChatGpt}
dispatch(setInitial(initial))
// TODO, auto reset default model
if (data?.hasOpenAI) {
dispatch(setModel('chatgpt'));
dispatch(setDisabled(false));
dispatch(setCustomModel(null));
dispatch(setCustomGpt({ chatGptLabel: null, promptPrefix: null }));
} else if (data?.hasBing) {
dispatch(setModel('bingai'));
dispatch(setDisabled(false));
dispatch(setCustomModel(null));
dispatch(setCustomGpt({ chatGptLabel: null, promptPrefix: null }));
} else if (data?.hasChatGpt) {
dispatch(setModel('chatgptBrowser'));
dispatch(setDisabled(false));
dispatch(setCustomModel(null));
dispatch(setCustomGpt({ chatGptLabel: null, promptPrefix: null }));
} else {
dispatch(setDisabled(true));
}
}).catch((error) => {
console.error(error)
console.log('Not login!')
window.location.href = "/auth/login";
})
}, [])
useEffect(() => {
localStorage.setItem('model', JSON.stringify(model));
}, [model]);
const filteredModels = models.filter(({model, _id }) => initial[model] );
const onChange = (value) => {
if (!value) {
return;
} else if (value === model) {
return;
} else if (value === 'chatgptCustom') {
// return;
} else if (initial[value]) {
dispatch(setModel(value));
dispatch(setDisabled(false));
dispatch(setCustomModel(null));
dispatch(setCustomGpt({ chatGptLabel: null, promptPrefix: null }));
} else if (!initial[value]) {
const chatGptLabel = modelMap[value]?.chatGptLabel;
const promptPrefix = modelMap[value]?.promptPrefix;
dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
dispatch(setModel('chatgptCustom'));
dispatch(setCustomModel(value));
setMenuOpen(false);
} else if (!modelMap[value]) {
dispatch(setCustomModel(null));
}
// Set new conversation
dispatch(setText(''));
dispatch(setMessages([]));
dispatch(setNewConvo());
dispatch(setSubmission({}));
};
const onOpenChange = (open) => {
mutate();
if (!open) {
setModelSave(false);
}
};
const handleSaveState = (value) => {
if (!modelSave) {
return;
}
dispatch(setCustomModel(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 isBing = model === 'bingai' || model === 'sydney';
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] items-center mb-0 rounded-md border-0 p-1 ml-1 md:ml-0 outline-none ${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={customModel ? customModel : model}
onValueChange={onChange}
className="overflow-y-auto"
>
{filteredModels.length?
<MenuItems
models={filteredModels}
onSelect={onChange}
/>:<DropdownMenuLabel className="dark:text-gray-300">No model available.</DropdownMenuLabel>
}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<ModelDialog
mutate={mutate}
modelMap={modelMap}
setModelSave={setModelSave}
handleSaveState={handleSaveState}
/>
</Dialog>
);
}

View file

@ -1,51 +1,47 @@
import React from 'react';
import store from '~/store';
import TrashIcon from '../svg/TrashIcon';
import { useSWRConfig } from 'swr';
import manualSWR from '~/utils/fetchers';
import { useDispatch } from 'react-redux';
import { setNewConvo, removeAll } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmission } from '~/store/submitSlice';
import { Dialog, DialogTrigger } from '../ui/Dialog.tsx';
import DialogTemplate from '../ui/DialogTemplate';
export default function ClearConvos() {
const dispatch = useDispatch();
const { newConversation } = store.useConversation();
const { refreshConversations } = store.useConversations();
const { mutate } = useSWRConfig();
const { trigger } = manualSWR(`/api/convos/clear`, 'post', () => {
dispatch(setMessages([]));
dispatch(setNewConvo());
dispatch(setSubmission({}));
newConversation();
refreshConversations();
mutate(`/api/convos`);
});
const clickHandler = () => {
console.log('Clearing conversations...');
dispatch(removeAll());
trigger({});
};
return (
<Dialog>
<DialogTrigger asChild>
<a
className="flex cursor-pointer items-center gap-3 rounded-md py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
// onClick={clickHandler}
>
<TrashIcon />
Clear conversations
</a>
</DialogTrigger>
<DialogTemplate
title="Clear conversations"
description="Are you sure you want to clear all conversations? This is irreversible."
selection={{
selectHandler: clickHandler,
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
selectText: 'Clear',
}}
/>
<a
className="flex cursor-pointer items-center gap-3 rounded-md py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
// onClick={clickHandler}
>
<TrashIcon />
Clear conversations
</a>
</DialogTrigger>
<DialogTemplate
title="Clear conversations"
description="Are you sure you want to clear all conversations? This is irreversible."
selection={{
selectHandler: clickHandler,
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
selectText: 'Clear'
}}
/>
</Dialog>
);
}

View file

@ -1,13 +1,13 @@
import React, { useState, useContext } from 'react';
import { useSelector } from 'react-redux';
import React from 'react';
import LogOutIcon from '../svg/LogOutIcon';
import { useRecoilValue } from 'recoil';
import store from '~/store';
export default function Logout() {
const { user } = useSelector((state) => state.user);
const user = useRecoilValue(store.user);
const clickHandler = () => {
window.location.href = "/auth/logout";
window.location.href = '/auth/logout';
};
return (

View file

@ -1,33 +1,19 @@
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { setNewConvo } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmission } from '~/store/submitSlice';
import { setText } from '~/store/textSlice';
import { useRecoilValue } from 'recoil';
import store from '~/store';
export default function MobileNav({ setNavVisible }) {
const dispatch = useDispatch();
const { conversationId, convos, title } = useSelector((state) => state.convo);
const toggleNavVisible = () => {
setNavVisible((prev) => {
return !prev
})
}
const newConvo = () => {
dispatch(setText(''));
dispatch(setMessages([]));
dispatch(setNewConvo());
dispatch(setSubmission({}));
}
const conversation = useRecoilValue(store.conversation);
const { newConversation } = store.useConversation();
const { title = 'New Chat' } = conversation || {};
return (
<div className="fixed top-0 left-0 right-0 z-10 flex items-center border-b border-white/20 bg-gray-800 pl-1 pt-1 text-gray-200 sm:pl-3 md:hidden">
<button
type="button"
className="-ml-0.5 -mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white dark:hover:text-white"
onClick={toggleNavVisible}
onClick={() => setNavVisible(prev => !prev)}
>
<span className="sr-only">Open sidebar</span>
<svg
@ -66,7 +52,7 @@ export default function MobileNav({ setNavVisible }) {
<button
type="button"
className="px-3"
onClick={newConvo}
onClick={() => newConversation()}
>
<svg
stroke="currentColor"

View file

@ -3,13 +3,17 @@ import SearchBar from './SearchBar';
import ClearConvos from './ClearConvos';
import DarkMode from './DarkMode';
import Logout from './Logout';
import { useSelector } from 'react-redux';
export default function NavLinks({ fetch, onSearchSuccess, clearSearch }) {
const { searchEnabled } = useSelector((state) => state.search);
export default function NavLinks({ fetch, onSearchSuccess, clearSearch, isSearchEnabled }) {
return (
<>
{ !!searchEnabled && <SearchBar fetch={fetch} onSuccess={onSearchSuccess} clearSearch={clearSearch}/>}
{!!isSearchEnabled && (
<SearchBar
fetch={fetch}
onSuccess={onSearchSuccess}
clearSearch={clearSearch}
/>
)}
<DarkMode />
<ClearConvos />
<Logout />

View file

@ -1,23 +1,13 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { setNewConvo, refreshConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmission, setDisabled } from '~/store/submitSlice';
import { setText } from '~/store/textSlice';
import { setInputValue, setQuery } from '~/store/searchSlice';
import store from '~/store';
export default function NewChat() {
const dispatch = useDispatch();
const { newConversation } = store.useConversation();
const clickHandler = () => {
dispatch(setText(''));
dispatch(setMessages([]));
dispatch(setNewConvo());
dispatch(refreshConversation());
dispatch(setSubmission({}));
dispatch(setDisabled(false));
dispatch(setInputValue(''));
dispatch(setQuery(''));
// dispatch(setInputValue(''));
// dispatch(setQuery(''));
newConversation();
};
return (

View file

@ -1,41 +1,48 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { debounce } from 'lodash';
import { useSelector, useDispatch } from 'react-redux';
import { Search } from 'lucide-react';
import { setInputValue, setQuery } from '~/store/searchSlice';
import { useRecoilState } from 'recoil';
import store from '~/store';
export default function SearchBar({ fetch, clearSearch }) {
const dispatch = useDispatch();
const { inputValue } = useSelector((state) => state.search);
// const dispatch = useDispatch();
const [inputValue, setInputValue] = useState('');
const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery);
// const [inputValue, setInputValue] = useState('');
const debouncedChangeHandler = useCallback(
debounce((q) => {
dispatch(setQuery(q));
if (q.length > 0) {
fetch(q, 1);
}
debounce(q => {
setSearchQuery(q);
}, 750),
[dispatch]
[setSearchQuery]
);
const handleKeyUp = (e) => {
useEffect(() => {
if (searchQuery.length > 0) {
fetch(searchQuery, 1);
setInputValue(searchQuery);
}
}, [searchQuery]);
const handleKeyUp = e => {
const { value } = e.target;
if (e.keyCode === 8 && value === '') {
if (e.keyCode === 8 && value === '') {
// Value after clearing input: ""
console.log(`Value after clearing input: "${value}"`);
dispatch(setQuery(''));
setSearchQuery('');
clearSearch();
}
}
};
const changeHandler = (e) => {
const changeHandler = e => {
let q = e.target.value;
dispatch(setInputValue(q));
setInputValue(q);
q = q.trim();
if (q === '') {
dispatch(setQuery(''));
setSearchQuery('');
clearSearch();
} else {
debouncedChangeHandler(q);

View file

@ -6,67 +6,119 @@ import Pages from '../Conversations/Pages';
import Conversations from '../Conversations';
import NavLinks from './NavLinks';
import { searchFetcher, swr } from '~/utils/fetchers';
import { useDispatch, useSelector } from 'react-redux';
import { setConvos, setNewConvo, refreshConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setDisabled } from '~/store/submitSlice';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import store from '~/store';
export default function Nav({ navVisible, setNavVisible }) {
const dispatch = useDispatch();
const [isHovering, setIsHovering] = useState(false);
const [isFetching, setIsFetching] = useState(false);
const containerRef = useRef(null);
const scrollPositionRef = useRef(null);
// const dispatch = useDispatch();
const [conversations, setConversations] = useState([]);
// current page
const [pageNumber, setPageNumber] = useState(1);
// total pages
const [pages, setPages] = useState(1);
const [pageNumber, setPage] = useState(1);
const { search, query } = useSelector((state) => state.search);
const { conversationId, convos, refreshConvoHint } = useSelector((state) => state.convo);
// search
const searchQuery = useRecoilValue(store.searchQuery);
const isSearchEnabled = useRecoilValue(store.isSearchEnabled);
const isSearching = useRecoilValue(store.isSearching);
const { newConversation, searchPlaceholderConversation } = store.useConversation();
// current conversation
const conversation = useRecoilValue(store.conversation);
const { conversationId } = conversation || {};
const setSearchResultMessages = useSetRecoilState(store.searchResultMessages);
// refreshConversationsHint is used for other components to ask refresh of Nav
const refreshConversationsHint = useRecoilValue(store.refreshConversationsHint);
const { refreshConversations } = store.useConversations();
const [isFetching, setIsFetching] = useState(false);
const onSuccess = (data, searchFetch = false) => {
if (search) {
if (isSearching) {
return;
}
const { conversations, pages } = data;
let { conversations, pages } = data;
if (pageNumber > pages) {
setPage(pages);
setPageNumber(pages);
} else {
dispatch(setConvos({ convos: conversations, searchFetch }));
if (!searchFetch)
conversations = conversations.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
setConversations(conversations);
setPages(pages);
}
};
const onSearchSuccess = (data, expectedPage) => {
const res = data;
dispatch(setConvos({ convos: res.conversations, searchFetch: true }));
setConversations(res.conversations);
if (expectedPage) {
setPage(expectedPage);
setPageNumber(expectedPage);
}
setPage(res.pageNumber);
setPageNumber(res.pageNumber);
setPages(res.pages);
setIsFetching(false);
if (res.messages?.length > 0) {
dispatch(setMessages(res.messages));
dispatch(setDisabled(true));
}
searchPlaceholderConversation();
setSearchResultMessages(res.messages);
};
const fetch = useCallback(_.partialRight(searchFetcher.bind(null, () => setIsFetching(true)), onSearchSuccess), [dispatch]);
// TODO: dont need this
const fetch = useCallback(
_.partialRight(
searchFetcher.bind(null, () => setIsFetching(true)),
onSearchSuccess
),
[setIsFetching]
);
const clearSearch = () => {
setPage(1);
dispatch(refreshConversation());
if (!conversationId) {
dispatch(setNewConvo());
dispatch(setMessages([]));
setPageNumber(1);
refreshConversations();
if (conversationId == 'search') {
newConversation();
}
dispatch(setDisabled(false));
// dispatch(setDisabled(false));
};
const { data, isLoading, mutate } = swr(`/api/convos?pageNumber=${pageNumber}`, onSuccess, {
revalidateOnMount: false,
revalidateOnMount: false
});
const containerRef = useRef(null);
const scrollPositionRef = useRef(null);
const nextPage = async () => {
moveToTop();
if (!isSearching) {
setPageNumber(prev => prev + 1);
await mutate();
} else {
await fetch(searchQuery, +pageNumber + 1);
}
};
const previousPage = async () => {
moveToTop();
if (!isSearching) {
setPageNumber(prev => prev - 1);
await mutate();
} else {
await fetch(searchQuery, +pageNumber - 1);
}
};
useEffect(() => {
if (!isSearching) {
mutate();
}
}, [pageNumber, conversationId, refreshConversationsHint]);
const moveToTop = () => {
const container = containerRef.current;
@ -75,35 +127,7 @@ export default function Nav({ navVisible, setNavVisible }) {
}
};
const nextPage = async () => {
moveToTop();
if (!search) {
setPage((prev) => prev + 1);
await mutate();
} else {
await fetch(query, +pageNumber + 1);
}
};
const previousPage = async () => {
moveToTop();
if (!search) {
setPage((prev) => prev - 1);
await mutate();
} else {
await fetch(query, +pageNumber - 1);
}
};
useEffect(() => {
if (!search) {
mutate();
}
}, [pageNumber, conversationId, refreshConvoHint]);
useEffect(() => {
const moveTo = () => {
const container = containerRef.current;
if (container && scrollPositionRef.current !== null) {
@ -112,18 +136,20 @@ export default function Nav({ navVisible, setNavVisible }) {
container.scrollTop = Math.min(maxScrollTop, scrollPositionRef.current);
}
}, [data]);
};
const toggleNavVisible = () => {
setNavVisible(prev => !prev);
};
// useEffect(() => {
// moveTo();
// }, [data]);
useEffect(() => {
setNavVisible(false);
}, [conversationId]);
const toggleNavVisible = () => {
setNavVisible((prev) => {
return !prev;
});
};
const containerClasses =
isLoading && pageNumber === 1
? 'flex flex-col gap-2 text-gray-100 text-sm h-full justify-center items-center'
@ -151,11 +177,11 @@ export default function Nav({ navVisible, setNavVisible }) {
>
<div className={containerClasses}>
{/* {(isLoading && pageNumber === 1) ? ( */}
{(isLoading && pageNumber === 1) || (isFetching) ? (
{(isLoading && pageNumber === 1) || isFetching ? (
<Spinner />
) : (
<Conversations
conversations={convos}
conversations={conversations}
conversationId={conversationId}
moveToTop={moveToTop}
/>
@ -172,6 +198,7 @@ export default function Nav({ navVisible, setNavVisible }) {
fetch={fetch}
onSearchSuccess={onSearchSuccess}
clearSearch={clearSearch}
isSearchEnabled={isSearchEnabled}
/>
</nav>
</div>

View file

@ -1,6 +1,5 @@
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { setText } from '~/store/textSlice';
import { useRecoilValue } from 'recoil';
import useDocumentTitle from '~/hooks/useDocumentTitle';
import Templates from '../Prompts/Templates';
import SunIcon from '../svg/SunIcon';
@ -8,27 +7,31 @@ import LightningIcon from '../svg/LightningIcon';
import CautionIcon from '../svg/CautionIcon';
import ChatIcon from '../svg/ChatIcon';
export default function Landing({ title }) {
import store from '~/store';
export default function Landing() {
const [showingTemplates, setShowingTemplates] = useState(false);
const dispatch = useDispatch();
const conversation = useRecoilValue(store.conversation);
const { title = 'New Chat' } = conversation || {};
useDocumentTitle(title);
const clickHandler = (e) => {
const clickHandler = e => {
e.preventDefault();
const { innerText } = e.target;
const quote = innerText.split('"')[1].trim();
dispatch(setText(quote));
// dispatch(setText(quote));
};
const showTemplates = (e) => {
const showTemplates = e => {
e.preventDefault();
setShowingTemplates(!showingTemplates);
};
return (
<div className="flex pt-10 md:pt-0 h-full flex-col items-center overflow-y-auto text-sm dark:bg-gray-800">
<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="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 md:mt-[20vh] sm:mb-16">
<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]">
ChatGPT Clone
</h1>
<div className="items-start gap-3.5 text-center md:flex">

View file

@ -0,0 +1,75 @@
import React, { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import Landing from '../components/ui/Landing';
import Messages from '../components/Messages';
import TextChat from '../components/Input';
import store from '~/store';
import manualSWR from '~/utils/fetchers';
export default function Chat() {
const searchQuery = useRecoilValue(store.searchQuery);
const [conversation, setConversation] = useRecoilState(store.conversation);
const setMessages = useSetRecoilState(store.messages);
const messagesTree = useRecoilValue(store.messagesTree);
const { newConversation } = store.useConversation();
const { conversationId } = useParams();
const navigate = useNavigate();
const { trigger: messagesTrigger } = manualSWR(`/api/messages/${conversation?.conversationId}`, 'get');
const { trigger: conversationTrigger } = manualSWR(`/api/convos/${conversationId}`, 'get');
// when conversation changed or conversationId (in url) changed
useEffect(() => {
if (conversation === null) {
// no current conversation, we need to do something
if (conversationId === 'new') {
// create new
newConversation();
} else if (conversationId) {
// fetch it from server
conversationTrigger()
.then(setConversation)
.catch(error => {
console.error('failed to fetch the conversation');
console.error(error);
newConversation();
});
setMessages(null);
} else {
navigate(`/chat/new`);
}
} else if (conversation?.conversationId === 'search') {
// jump to search page
navigate(`/search/${searchQuery}`);
} else if (conversation?.conversationId !== conversationId) {
// conversationId (in url) should always follow conversation?.conversationId, unless conversation is null
navigate(`/chat/${conversation?.conversationId}`);
}
}, [conversation, conversationId]);
// when messagesTree is null (<=> messages is null)
// we need to fetch message list from server
useEffect(() => {
if (messagesTree === null) {
messagesTrigger().then(setMessages);
}
}, [conversation?.conversationId]);
// if not a conversation
if (conversation?.conversationId === 'search') return null;
// if conversationId not match
if (conversation?.conversationId !== conversationId) return null;
// if conversationId is null
if (!conversationId) return null;
return (
<>
{conversationId === 'new' && !messagesTree?.length ? <Landing /> : <Messages />}
<TextChat />
</>
);
}

View file

@ -0,0 +1,29 @@
import React, { useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom';
import MessageHandler from '../components/MessageHandler';
import Nav from '../components/Nav';
import MobileNav from '../components/Nav/MobileNav';
export default function Root() {
const [navVisible, setNavVisible] = useState(false);
return (
<>
<div className="flex h-screen">
<Nav
navVisible={navVisible}
setNavVisible={setNavVisible}
/>
<div className="flex h-full w-full flex-1 flex-col bg-gray-50 md:pl-[260px]">
<div className="transition-width relative flex h-full w-full flex-1 flex-col items-stretch overflow-hidden bg-white pt-10 dark:bg-gray-800 md:pt-0">
<MobileNav setNavVisible={setNavVisible} />
<Outlet />
</div>
</div>
</div>
<MessageHandler />
</>
);
}

View file

@ -0,0 +1,50 @@
import React, { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
import Messages from '../components/Messages';
import TextChat from '../components/Input';
import store from '~/store';
export default function Search() {
const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery);
const conversation = useRecoilValue(store.conversation);
const { searchPlaceholderConversation } = store.useConversation();
const { query } = useParams();
const navigate = useNavigate();
// when conversation changed or conversationId (in url) changed
useEffect(() => {
if (conversation === null) {
// no current conversation, we need to do something
if (query) {
// create new
searchPlaceholderConversation();
setSearchQuery(query);
} else {
navigate(`/chat/new`);
}
} else if (conversation?.conversationId === 'search') {
// jump to search page
if (searchQuery !== query) navigate(`/search/${searchQuery}`);
} else {
// conversationId (in url) should always follow conversation?.conversationId, unless conversation is null
navigate(`/chat/${conversation?.conversationId}`);
}
}, [conversation, query, searchQuery]);
// if not a search
if (conversation?.conversationId !== 'search') return null;
// if query not match
if (searchQuery !== query) return null;
// if query is null
if (!query) return null;
return (
<>
<Messages isSearchView={true} />
<TextChat isSearchView={true} />
</>
);
}

View file

@ -0,0 +1,163 @@
import models from './models';
import { atom, selector, useSetRecoilState, useResetRecoilState, useRecoilCallback } from 'recoil';
import buildTree from '~/utils/buildTree';
// current conversation, can be null (need to be fetched from server)
// sample structure
// {
// conversationId: "new",
// title: "New Chat",
// jailbreakConversationId: null,
// conversationSignature: null,
// clientId: null,
// invocationId: null,
// model: "chatgpt",
// chatGptLabel: null,
// promptPrefix: null,
// user: null,
// suggestions: [],
// toneStyle: null,
// }
const conversation = atom({
key: 'conversation',
default: null
});
// current messages of the conversation, must be an array
// sample structure
// [{text, sender, messageId, parentMessageId, isCreatedByUser}]
const messages = atom({
key: 'messages',
default: []
});
const messagesTree = selector({
key: 'messagesTree',
get: ({ get }) => {
return buildTree(get(messages), false);
}
});
const latestMessage = atom({
key: 'latestMessage',
default: null
});
const useConversation = () => {
const setConversation = useSetRecoilState(conversation);
const setMessages = useSetRecoilState(messages);
const resetLatestMessage = useResetRecoilState(latestMessage);
const switchToConversation = useRecoilCallback(
({ snapshot }) =>
async (_conversation, messages = null) => {
const prevConversation = await snapshot.getPromise(conversation);
const prevModelsFilter = await snapshot.getPromise(models.modelsFilter);
_switchToConversation(_conversation, messages, { prevModelsFilter, prevConversation });
},
[]
);
const _switchToConversation = (
conversation,
messages = null,
{ prevModelsFilter = {}, prev_conversation = {} }
) => {
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) {}
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)
// get the default model
getDefaultModel();
setConversation({
...conversation,
model: model,
chatGptLabel: chatGptLabel,
promptPrefix: promptPrefix
});
setMessages(messages);
resetLatestMessage();
};
const newConversation = ({ model = null, chatGptLabel = null, promptPrefix = null } = {}) => {
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
},
[]
);
};
const searchPlaceholderConversation = () => {
switchToConversation(
{
conversationId: 'search',
title: 'Search',
jailbreakConversationId: null,
conversationSignature: null,
clientId: null,
invocationId: null,
model: null,
chatGptLabel: null,
promptPrefix: null,
user: null,
suggestions: [],
toneStyle: null
},
[]
);
};
return { newConversation, switchToConversation, searchPlaceholderConversation };
};
export default {
conversation,
messages,
messagesTree,
latestMessage,
useConversation
};

View file

@ -0,0 +1,27 @@
import React from "react";
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
useSetRecoilState,
} from "recoil";
const refreshConversationsHint = atom({
key: "refreshConversationsHint",
default: 1,
});
const useConversations = () => {
const setRefreshConversationsHint = useSetRecoilState(
refreshConversationsHint
);
const refreshConversations = () =>
setRefreshConversationsHint((prevState) => prevState + 1);
return { refreshConversations };
};
export default { refreshConversationsHint, useConversations };

View file

@ -1,113 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
error: false,
title: 'ChatGPT Clone',
conversationId: null,
parentMessageId: null,
jailbreakConversationId: null,
conversationSignature: null,
clientId: null,
invocationId: null,
toneStyle: null,
chatGptLabel: null,
promptPrefix: null,
convosLoading: false,
pageNumber: 1,
pages: 1,
refreshConvoHint: 0,
search: false,
latestMessage: null,
convos: [],
convoMap: {},
};
const currentSlice = createSlice({
name: 'convo',
initialState,
reducers: {
refreshConversation: (state) => {
state.refreshConvoHint = state.refreshConvoHint + 1;
},
setConversation: (state, action) => {
// return { ...state, ...action.payload };
for (const key in action.payload) {
if (Object.hasOwnProperty.call(action.payload, key)) {
state[key] = action.payload[key];
}
}
},
setError: (state, action) => {
state.error = action.payload;
},
increasePage: (state) => {
state.pageNumber = state.pageNumber + 1;
},
decreasePage: (state) => {
state.pageNumber = state.pageNumber - 1;
},
setPage: (state, action) => {
state.pageNumber = action.payload;
},
setNewConvo: (state) => {
state.error = false;
state.title = 'ChatGPT Clone';
state.conversationId = null;
state.parentMessageId = null;
state.jailbreakConversationId = null;
state.conversationSignature = null;
state.clientId = null;
state.invocationId = null;
state.toneStyle = null;
state.chatGptLabel = null;
state.promptPrefix = null;
state.convosLoading = false;
state.latestMessage = null;
},
setConvos: (state, action) => {
const { convos, searchFetch } = action.payload;
if (searchFetch) {
state.convos = convos;
} else {
state.convos = convos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
}
// state.convoMap = convos.reduce((acc, curr) => {
// acc[curr.conversationId] = { ...curr };
// delete acc[curr.conversationId].conversationId;
// return acc;
// }, {});
},
setPages: (state, action) => {
state.pages = action.payload;
},
removeConvo: (state, action) => {
state.convos = state.convos.filter((convo) => convo.conversationId !== action.payload);
},
removeAll: (state) => {
state.convos = [];
},
setLatestMessage: (state, action) => {
state.latestMessage = action.payload;
}
}
});
export const {
refreshConversation,
setConversation,
setPages,
setConvos,
setNewConvo,
setError,
increasePage,
decreasePage,
setPage,
removeConvo,
removeAll,
setLatestMessage
} = currentSlice.actions;
export default currentSlice.reducer;

View file

@ -1,22 +1,15 @@
import { configureStore } from '@reduxjs/toolkit';
import conversation from './conversation';
import conversations from './conversations';
import models from './models';
import user from './user';
import submission from './submission';
import search from './search';
import convoReducer from './convoSlice.js';
import messageReducer from './messageSlice.js';
import modelReducer from './modelSlice.js';
import submitReducer from './submitSlice.js';
import textReducer from './textSlice.js';
import userReducer from './userReducer.js';
import searchReducer from './searchSlice.js';
export const store = configureStore({
reducer: {
convo: convoReducer,
messages: messageReducer,
models: modelReducer,
text: textReducer,
submit: submitReducer,
user: userReducer,
search: searchReducer
},
devTools: true
});
export default {
...conversation,
...conversations,
...models,
...user,
...submission,
...search
};

View file

@ -1,35 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
import buildTree from '~/utils/buildTree';
const initialState = {
messages: [],
messageTree: []
};
const currentSlice = createSlice({
name: 'messages',
initialState,
reducers: {
setMessages: (state, action) => {
state.messages = action.payload;
const groupAll = action.payload[0]?.searchResult;
if (groupAll) console.log('grouping all messages');
state.messageTree = buildTree(action.payload, groupAll);
},
setEmptyMessage: (state) => {
state.messages = [
{
messageId: '1',
conversationId: '1',
parentMessageId: '1',
sender: '',
text: ''
}
]
},
}
});
export const { setMessages, setEmptyMessage } = currentSlice.actions;
export default currentSlice.reducer;

View file

@ -1,68 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
models: [
{
_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'
},
],
modelMap: {},
initial: { chatgpt: false, chatgptCustom: false, bingai: false, sydney: false, chatgptBrowser: false }
// initial: { chatgpt: true, chatgptCustom: true, bingai: true, }
};
const currentSlice = createSlice({
name: 'models',
initialState,
reducers: {
setModels: (state, action) => {
const models = [...initialState.models, ...action.payload];
state.models = models;
const modelMap = {};
models.slice(initialState.models.length).forEach((modelItem) => {
modelMap[modelItem.value] = {
chatGptLabel: modelItem.chatGptLabel,
promptPrefix: modelItem.promptPrefix,
model: 'chatgptCustom'
};
});
state.modelMap = modelMap;
},
setInitial: (state, action) => {
state.initial = action.payload;
}
}
});
export const { setModels, setInitial } = currentSlice.actions;
export default currentSlice.reducer;

View file

@ -0,0 +1,80 @@
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,
};

View file

@ -0,0 +1,40 @@
import { atom, selector } from 'recoil';
import buildTree from '~/utils/buildTree';
const isSearchEnabled = atom({
key: 'isSearchEnabled',
default: null
});
const searchQuery = atom({
key: 'searchQuery',
default: ''
});
const searchResultMessages = atom({
key: 'searchResultMessages',
default: null
});
const searchResultMessagesTree = selector({
key: 'searchResultMessagesTree',
get: ({ get }) => {
return buildTree(get(searchResultMessages), true);
}
});
const isSearching = selector({
key: 'isSearching',
get: ({ get }) => {
const data = get(searchQuery);
return !!data;
}
});
export default {
isSearchEnabled,
isSearching,
searchResultMessages,
searchResultMessagesTree,
searchQuery
};

View file

@ -1,35 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
searchEnabled: false,
search: false,
query: '',
inputValue: '',
};
const currentSlice = createSlice({
name: 'search',
initialState,
reducers: {
setInputValue: (state, action) => {
state.inputValue = action.payload;
},
setSearchState: (state, action) => {
state.searchEnabled = action.payload;
},
setQuery: (state, action) => {
const q = action.payload;
state.query = q;
if (q === '') {
state.search = false;
} else if (q?.length > 0 && !state.search) {
state.search = true;
}
},
}
});
export const { setInputValue, setSearchState, setQuery } = currentSlice.actions;
export default currentSlice.reducer;

View file

@ -0,0 +1,37 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
useSetRecoilState,
} from "recoil";
import buildTree from "~/utils/buildTree";
// current submission
// submit any new value to this state will cause new message to be send.
// set to null to give up any submission
// {
// conversation, // target submission, must have: model, chatGptLabel, promptPrefix
// messages, // old messages
// message, // request message
// initialResponse, // response message
// isRegenerate=false, // isRegenerate?
// }
const submission = atom({
key: "submission",
default: null,
});
const isSubmitting = atom({
key: "isSubmitting",
default: false,
});
export default {
submission,
isSubmitting,
};

View file

@ -1,57 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
isSubmitting: false,
submission: {},
stopStream: false,
disabled: true,
model: 'chatgpt',
promptPrefix: null,
chatGptLabel: null,
customModel: null,
cursor: true,
};
const currentSlice = createSlice({
name: 'submit',
initialState,
reducers: {
setSubmitState: (state, action) => {
state.isSubmitting = action.payload;
},
setSubmission: (state, action) => {
state.submission = action.payload;
if (Object.keys(action.payload).length === 0) {
state.isSubmitting = false;
}
},
setStopStream: (state, action) => {
state.stopStream = action.payload;
},
setDisabled: (state, action) => {
state.disabled = action.payload;
},
setModel: (state, action) => {
state.model = action.payload;
},
toggleCursor: (state, action) => {
if (action.payload) {
state.cursor = action.payload;
} else {
state.cursor = !state.cursor;
}
},
setCustomGpt: (state, action) => {
state.promptPrefix = action.payload.promptPrefix;
state.chatGptLabel = action.payload.chatGptLabel;
},
setCustomModel: (state, action) => {
state.customModel = action.payload;
}
}
});
export const { toggleCursor, setSubmitState, setSubmission, setStopStream, setDisabled, setModel, setCustomGpt, setCustomModel } =
currentSlice.actions;
export default currentSlice.reducer;

View file

@ -1,19 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
text: '',
};
const currentSlice = createSlice({
name: 'text',
initialState,
reducers: {
setText: (state, action) => {
state.text = action.payload;
},
}
});
export const { setText } = currentSlice.actions;
export default currentSlice.reducer;

17
client/src/store/user.js Normal file
View file

@ -0,0 +1,17 @@
import React from "react";
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from "recoil";
const user = atom({
key: "user",
default: null,
});
export default {
user,
};

View file

@ -1,19 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
user: null,
};
const currentSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser: (state, action) => {
state.user = action.payload;
},
}
});
export const { setUser } = currentSlice.actions;
export default currentSlice.reducer;

View file

@ -37,7 +37,7 @@ bottom: 39px;
z-index: 995;
margin-left: auto;
margin-right: auto;
width: 408px; /* Need a specific value to work */
width: 308px; /* Need a specific value to work */
transition: all 0.5s ease-in-out;
pointer-events: none;
transform: translateY(-60px);

View file

@ -4,9 +4,14 @@ const odd =
'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-[#444654] hover:bg-gray-100/40 hover:text-gray-700 dark:hover:bg-[#3b3d49] dark:hover:text-gray-200';
export default function buildTree(messages, groupAll = false) {
if (messages === null) return null;
let messageMap = {};
let rootMessages = [];
if (groupAll) {
return messages.map((m, idx) => ({ ...m, bg: idx % 2 === 0 ? even : odd }));
}
if (!groupAll) {
// Traverse the messages array and store each element in messageMap.
messages.forEach(message => {
@ -20,18 +25,18 @@ export default function buildTree(messages, groupAll = false) {
return rootMessages;
}
// Group all messages into one tree
let parentId = null;
messages.forEach((message, i) => {
messageMap[message.messageId] = { ...message, bg: i % 2 === 0 ? even : odd, children: [] };
const currentMessage = messageMap[message.messageId];
const parentMessage = messageMap[parentId];
if (parentMessage) parentMessage.children.push(currentMessage);
else rootMessages.push(currentMessage);
parentId = message.messageId;
});
// // Group all messages into one tree
// let parentId = null;
// messages.forEach((message, i) => {
// messageMap[message.messageId] = { ...message, bg: i % 2 === 0 ? even : odd, children: [] };
// const currentMessage = messageMap[message.messageId];
// const parentMessage = messageMap[parentId];
// if (parentMessage) parentMessage.children.push(currentMessage);
// else rootMessages.push(currentMessage);
// parentId = message.messageId;
// });
return rootMessages;
// return rootMessages;
// Group all messages by conversation, doesn't look great
// Traverse the messages array and store each element in messageMap.

View file

@ -1,31 +1,50 @@
export default function createPayload({ convo, message }) {
const endpoint = `/api/ask`;
let payload = { ...message };
const { model } = message;
export default function createPayload(submission) {
const { conversation, messages, message, initialResponse, isRegenerate = false } = submission;
if (!payload.conversationId)
if (convo?.conversationId && convo?.parentMessageId) {
payload = {
...payload,
conversationId: convo.conversationId,
parentMessageId: convo.parentMessageId || '00000000-0000-0000-0000-000000000000'
};
const endpoint = `/api/ask`;
const {
model,
chatGptLabel,
promptPrefix,
jailbreakConversationId,
conversationId,
conversationSignature,
clientId,
invocationId,
toneStyle
} = conversation;
let payload = {
...message,
...{
model,
chatGptLabel,
promptPrefix,
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 && !convo?.conversationId) {
payload.toneStyle = convo.toneStyle || 'fast';
if (isBing && !conversationId) {
payload.toneStyle = toneStyle || 'fast';
}
if (isBing && convo?.conversationId) {
if (isBing && conversationId) {
payload = {
...payload,
jailbreakConversationId: convo.jailbreakConversationId,
conversationId: convo.conversationId,
conversationSignature: convo.conversationSignature,
clientId: convo.clientId,
invocationId: convo.invocationId,
toneStyle: convo.toneStyle,
jailbreakConversationId,
conversationSignature,
clientId,
invocationId
};
}

View file

@ -1,164 +1,123 @@
import resetConvo from './resetConvo';
import { useSelector, useDispatch } from 'react-redux';
import { setNewConvo } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmitState, setSubmission } from '~/store/submitSlice';
import { setText } from '~/store/textSlice';
import { setError } from '~/store/convoSlice';
import {v4} from 'uuid';
// import resetConvo from './resetConvo';
// import { useSelector, useDispatch } from 'react-redux';
// import { setNewConvo } from '~/store/convoSlice';
// import { setMessages } from '~/store/messageSlice';
// import { setSubmitState, setSubmission } from '~/store/submitSlice';
// import { setText } from '~/store/textSlice';
// import { setError } from '~/store/convoSlice';
import { v4 } from 'uuid';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import store from '~/store';
const useMessageHandler = () => {
const dispatch = useDispatch();
const convo = useSelector((state) => state.convo);
const { initial } = useSelector((state) => state.models);
const { messages } = useSelector((state) => state.messages);
const { model, chatGptLabel, promptPrefix, isSubmitting } = useSelector((state) => state.submit);
const { latestMessage, error } = convo;
// const dispatch = useDispatch();
// const convo = useSelector((state) => state.convo);
// const { initial } = useSelector((state) => state.models);
// const { messages } = useSelector((state) => state.messages);
// const { model, chatGptLabel, promptPrefix, isSubmitting } = useSelector((state) => state.submit);
// const { latestMessage, error } = convo;
const ask = ({ text, parentMessageId=null, conversationId=null, messageId=null}, { isRegenerate=false }={}) => {
if (error) {
dispatch(setError(false));
}
const [currentConversation, setCurrentConversation] = useRecoilState(store.conversation) || {};
const setSubmission = useSetRecoilState(store.submission);
const isSubmitting = useRecoilValue(store.isSubmitting);
const latestMessage = useRecoilValue(store.latestMessage);
const { error } = currentConversation;
const [messages, setMessages] = useRecoilState(store.messages);
const ask = (
{ text, parentMessageId = null, conversationId = null, messageId = null },
{ isRegenerate = false } = {}
) => {
if (!!isSubmitting || text === '') {
return;
}
// determine the model to be used
const { model = null, chatGptLabel = null, promptPrefix = null } = currentConversation;
// construct the query message
// this is not a real messageId, it is used as placeholder before real messageId returned
text = text.trim();
const fakeMessageId = v4();
const isCustomModel = model === 'chatgptCustom' || !initial[model];
const sender = model === 'chatgptCustom' ? chatGptLabel : model;
// const isCustomModel = model === 'chatgptCustom' || !initial[model];
// const sender = model === 'chatgptCustom' ? chatGptLabel : model;
parentMessageId = parentMessageId || latestMessage?.messageId || '00000000-0000-0000-0000-000000000000';
let currentMessages = messages;
if (resetConvo(currentMessages, sender)) {
parentMessageId = '00000000-0000-0000-0000-000000000000';
conversationId = null;
dispatch(setNewConvo());
currentMessages = [];
conversationId = conversationId || currentConversation?.conversationId;
if (conversationId == 'search') {
console.error('cannot send any message under search view!');
return;
}
const currentMsg = { sender: 'User', text, current: true, isCreatedByUser: true, parentMessageId, conversationId, messageId: fakeMessageId };
const initialResponse = { sender, text: '', parentMessageId: isRegenerate?messageId:fakeMessageId, messageId: (isRegenerate?messageId:fakeMessageId) + '_', submitting: true };
if (conversationId == 'new') {
parentMessageId = '00000000-0000-0000-0000-000000000000';
currentMessages = [];
conversationId = null;
}
const currentMsg = {
sender: 'User',
text,
current: true,
isCreatedByUser: true,
parentMessageId,
conversationId,
messageId: fakeMessageId
};
// construct the placeholder response message
const initialResponse = {
sender: chatGptLabel || model,
text: '<span className="result-streaming">█</span>',
parentMessageId: isRegenerate ? messageId : fakeMessageId,
messageId: (isRegenerate ? messageId : fakeMessageId) + '_',
conversationId,
submitting: true
};
const submission = {
convo,
isCustomModel,
message: {
conversation: {
...currentConversation,
conversationId,
model,
chatGptLabel,
promptPrefix
},
message: {
...currentMsg,
model,
chatGptLabel,
promptPrefix,
overrideParentMessageId: isRegenerate?messageId:null
overrideParentMessageId: isRegenerate ? messageId : null
},
messages: currentMessages,
isRegenerate,
initialResponse,
sender,
initialResponse
};
console.log('User Input:', text);
if (isRegenerate) {
dispatch(setMessages([...currentMessages, initialResponse]));
setMessages([...currentMessages, initialResponse]);
} else {
dispatch(setMessages([...currentMessages, currentMsg, initialResponse]));
dispatch(setText(''));
setMessages([...currentMessages, currentMsg, initialResponse]);
}
dispatch(setSubmitState(true));
dispatch(setSubmission(submission));
}
setSubmission(submission);
};
const regenerate = ({ parentMessageId }) => {
const parentMessage = messages?.find(element => element.messageId == parentMessageId);
if (parentMessage && parentMessage.isCreatedByUser)
ask({ ...parentMessage }, { isRegenerate: true })
else
console.error('Failed to regenerate the message: parentMessage not found or not created by user.');
}
if (parentMessage && parentMessage.isCreatedByUser) ask({ ...parentMessage }, { isRegenerate: true });
else console.error('Failed to regenerate the message: parentMessage not found or not created by user.');
};
const stopGenerating = () => {
dispatch(setSubmission({}));
}
setSubmission(null);
};
return { ask, regenerate, stopGenerating }
}
return { ask, regenerate, stopGenerating };
};
export { useMessageHandler };
// deprecated
// export default function handleSubmit({
// model,
// text,
// convo,
// messageHandler,
// convoHandler,
// errorHandler,
// chatGptLabel,
// promptPrefix
// }) {
// const endpoint = `/api/ask`;
// let payload = { model, text, chatGptLabel, promptPrefix };
// if (convo.conversationId && convo.parentMessageId) {
// payload = {
// ...payload,
// conversationId: convo.conversationId,
// parentMessageId: convo.parentMessageId
// };
// }
// const isBing = model === 'bingai' || model === 'sydney';
// if (isBing && convo.conversationId) {
// payload = {
// ...payload,
// jailbreakConversationId: convo.jailbreakConversationId,
// conversationId: convo.conversationId,
// conversationSignature: convo.conversationSignature,
// clientId: convo.clientId,
// invocationId: convo.invocationId,
// };
// }
// let server = endpoint;
// server = model === 'bingai' ? server + '/bing' : server;
// server = model === 'sydney' ? server + '/sydney' : server;
// const events = new SSE(server, {
// payload: JSON.stringify(payload),
// headers: { 'Content-Type': 'application/json' }
// });
// events.onopen = function () {
// console.log('connection is opened');
// };
// events.onmessage = function (e) {
// const data = JSON.parse(e.data);
// let text = data.text || data.response;
// if (data.message) {
// messageHandler(text, events);
// }
// if (data.final) {
// convoHandler(data);
// console.log('final', data);
// } else {
// // console.log('dataStream', data);
// }
// };
// events.onerror = function (e) {
// console.log('error in opening conn.');
// events.close();
// errorHandler(e);
// };
// events.addEventListener('stop', () => {
// // Close the SSE stream
// console.log('stop event received');
// events.close();
// });
// events.stream();
// }

View file

@ -38,40 +38,55 @@ export const languages = [
'pascal'
];
export const getIconOfModel = ({ size=30, sender, isCreatedByUser, searchResult, model, chatGptLabel, error, ...props }) => {
// 'ai' is used as 'model' is not accurate for search results
let ai = searchResult ? sender : model;
export const getIconOfModel = ({
size = 30,
sender,
isCreatedByUser,
searchResult,
model,
chatGptLabel,
error,
...props
}) => {
const { button } = props;
const bgColors = {
chatgpt: `rgb(16, 163, 127${ button ? ', 0.75' : ''})`,
chatgptBrowser: `rgb(25, 207, 207${ button ? ', 0.75' : ''})`,
chatgpt: `rgb(16, 163, 127${button ? ', 0.75' : ''})`,
chatgptBrowser: `rgb(25, 207, 207${button ? ', 0.75' : ''})`,
bingai: 'transparent',
sydney: 'radial-gradient(circle at 90% 110%, #F0F0FA, #D0E0F9)',
chatgptCustom: `rgb(0, 163, 255${ button ? ', 0.75' : ''})`,
chatgptCustom: `rgb(0, 163, 255${button ? ', 0.75' : ''})`
};
if (isCreatedByUser)
if (isCreatedByUser)
return (
<div
title='User'
style={{ background: 'radial-gradient(circle at 90% 110%, rgb(1 43 128), rgb(17, 139, 161))', color: 'white', fontSize: 12, width: size, height: size }}
title="User"
style={{
background: 'radial-gradient(circle at 90% 110%, rgb(1 43 128), rgb(17, 139, 161))',
color: 'white',
fontSize: 12,
width: size,
height: size
}}
className={`relative flex items-center justify-center rounded-sm text-white ` + props?.className}
>
User
</div>
)
);
else if (!isCreatedByUser) {
// TODO: use model from convo, rather than submit
// const { model, chatGptLabel, promptPrefix } = convo;
let background = bgColors[ai];
const isBing = ai === 'bingai' || ai === 'sydney';
let background = bgColors[model];
const isBing = model === 'bingai' || model === 'sydney';
return (
<div
title={chatGptLabel || ai}
style={
{ background: background || 'radial-gradient(circle at 90% 110%, #F0F0FA, #D0E0F9)', width: size, height: size }
}
title={chatGptLabel || model}
style={{
background: background || 'radial-gradient(circle at 90% 110%, #F0F0FA, #D0E0F9)',
width: size,
height: size
}}
className={`relative flex items-center justify-center rounded-sm text-white ` + props?.className}
>
{isBing ? <BingIcon size={size * 0.7} /> : <GPTIcon size={size * 0.7} />}
@ -85,11 +100,17 @@ export const getIconOfModel = ({ size=30, sender, isCreatedByUser, searchResult,
} else
return (
<div
title='User'
style={{ background: 'radial-gradient(circle at 90% 110%, rgb(1 43 128), rgb(17, 139, 161))', color: 'white', fontSize: 12, width: size, height: size }}
title="User"
style={{
background: 'radial-gradient(circle at 90% 110%, rgb(1 43 128), rgb(17, 139, 161))',
color: 'white',
fontSize: 12,
width: size,
height: size
}}
className={`relative flex items-center justify-center rounded-sm p-1 text-white ` + props?.className}
>
{chatGptLabel}
</div>
)
}
);
};