mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-28 14:18:51 +01:00
Merge pull request #140 from danny-avila/feat-refactor
Code refactoring
This commit is contained in:
commit
79bb54db9c
68 changed files with 3298 additions and 2196 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
14
api/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
1386
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
>
|
||||
|
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
@ -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 } : {};
|
||||
|
||||
|
|
@ -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}
|
||||
/>
|
||||
205
client/src/components/Input/Models/ModelMenu.jsx
Normal file
205
client/src/components/Input/Models/ModelMenu.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
201
client/src/components/Input/index.jsx
Normal file
201
client/src/components/Input/index.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
274
client/src/components/MessageHandler/index.jsx
Normal file
274
client/src/components/MessageHandler/index.jsx
Normal 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;
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
75
client/src/routes/Chat.jsx
Normal file
75
client/src/routes/Chat.jsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
29
client/src/routes/Root.jsx
Normal file
29
client/src/routes/Root.jsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
50
client/src/routes/Search.jsx
Normal file
50
client/src/routes/Search.jsx
Normal 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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
163
client/src/store/conversation.js
Normal file
163
client/src/store/conversation.js
Normal 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
|
||||
};
|
||||
27
client/src/store/conversations.js
Normal file
27
client/src/store/conversations.js
Normal 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 };
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
80
client/src/store/models.js
Normal file
80
client/src/store/models.js
Normal 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,
|
||||
};
|
||||
40
client/src/store/search.js
Normal file
40
client/src/store/search.js
Normal 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
|
||||
};
|
||||
|
|
@ -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;
|
||||
37
client/src/store/submission.js
Normal file
37
client/src/store/submission.js
Normal 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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
17
client/src/store/user.js
Normal 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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue