mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
commit
c6fb3018e7
40 changed files with 990 additions and 364 deletions
|
|
@ -18,12 +18,47 @@ MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone"
|
||||||
# API key configuration.
|
# API key configuration.
|
||||||
# Leave blank if you don't want them.
|
# Leave blank if you don't want them.
|
||||||
OPENAI_KEY=
|
OPENAI_KEY=
|
||||||
CHATGPT_TOKEN=
|
|
||||||
BING_TOKEN=
|
BING_TOKEN=
|
||||||
|
|
||||||
# User System
|
# ChatGPT Browser Client (free but use at your own risk)
|
||||||
|
# Access token from https://chat.openai.com/api/auth/session
|
||||||
|
# Exposes your access token to a 3rd party
|
||||||
|
CHATGPT_TOKEN=
|
||||||
|
# If you have access to other models on the official site, you can use them here.
|
||||||
|
# Defaults to 'text-davinci-002-render-sha' if left empty.
|
||||||
|
# options: gpt-4, text-davinci-002-render, text-davinci-002-render-paid, or text-davinci-002-render-sha
|
||||||
|
# You cannot use a model that your account does not have access to. You can check
|
||||||
|
# which ones you have access to by opening DevTools and going to the Network tab.
|
||||||
|
# Refresh the page and look at the response body for https://chat.openai.com/backend-api/models.
|
||||||
|
BROWSER_MODEL=
|
||||||
|
|
||||||
|
# ENABLING SEARCH MESSAGES/CONVOS
|
||||||
|
# Requires installation of free self-hosted Meilisearch or Paid Remote Plan (Remote not tested)
|
||||||
|
# The easiest setup for this is through docker-compose, which takes care of it for you.
|
||||||
|
# SEARCH=TRUE
|
||||||
|
SEARCH=TRUE
|
||||||
|
|
||||||
|
# REQUIRED FOR SEARCH: MeiliSearch Host, mainly for api server to connect to the search server.
|
||||||
|
# must replace '0.0.0.0' with 'meilisearch' if serving meilisearch with docker-compose
|
||||||
|
# MEILI_HOST='http://0.0.0.0:7700' # <-- local/remote
|
||||||
|
MEILI_HOST='http://meilisearch:7700' # <-- docker-compose
|
||||||
|
|
||||||
|
# REQUIRED FOR SEARCH: MeiliSearch HTTP Address, mainly for docker-compose to expose the search server.
|
||||||
|
# must replace '0.0.0.0' with 'meilisearch' if serving meilisearch with docker-compose
|
||||||
|
# MEILI_HTTP_ADDR='0.0.0.0:7700' # <-- local/remote
|
||||||
|
MEILI_HTTP_ADDR='meilisearch:7700' # <-- docker-compose
|
||||||
|
|
||||||
|
# REQUIRED FOR SEARCH: In production env., needs a secure key, feel free to generate your own.
|
||||||
|
# This master key must be at least 16 bytes, composed of valid UTF-8 characters.
|
||||||
|
# Meilisearch will throw an error and refuse to launch if no master key is provided or if it is under 16 bytes,
|
||||||
|
# Meilisearch will suggest a secure autogenerated master key.
|
||||||
|
# Using docker, it seems recognized as production so use a secure key.
|
||||||
|
# MEILI_MASTER_KEY= # <-- no/insecure key for local/remote
|
||||||
|
MEILI_MASTER_KEY=JKMW-hGc7v_D1FkJVdbRSDNFLZcUv3S75yrxXP0SmcU # <-- ready made secure key for docker-compose
|
||||||
|
|
||||||
|
|
||||||
|
# User System
|
||||||
# global enable/disable the sample user system.
|
# global enable/disable the sample user system.
|
||||||
# this is not a ready to use user system.
|
# this is not a ready to use user system.
|
||||||
# dont't use it, unless you can write your own code.
|
# dont't use it, unless you can write your own code.
|
||||||
ENABLE_USER_SYSTEM=
|
ENABLE_USER_SYSTEM=FALSE
|
||||||
22
api/.prettierrc
Normal file
22
api/.prettierrc
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"insertPragma": false,
|
||||||
|
"singleAttributePerLine": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"printWidth": 110,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"requirePragma": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"useTabs": false,
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"parser": "babel"
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const { KeyvFile } = require('keyv-file');
|
const { KeyvFile } = require('keyv-file');
|
||||||
|
const set = new Set(["gpt-4", "text-davinci-002-render", "text-davinci-002-render-paid", "text-davinci-002-render-sha"]);
|
||||||
|
|
||||||
const clientOptions = {
|
const clientOptions = {
|
||||||
// Warning: This will expose your access token to a third party. Consider the risks before using this.
|
// Warning: This will expose your access token to a third party. Consider the risks before using this.
|
||||||
|
|
@ -10,6 +11,12 @@ const clientOptions = {
|
||||||
proxy: process.env.PROXY || null,
|
proxy: process.env.PROXY || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// You can check which models you have access to by opening DevTools and going to the Network tab.
|
||||||
|
// Refresh the page and look at the response body for https://chat.openai.com/backend-api/models.
|
||||||
|
if (set.has(process.env.BROWSER_MODEL)) {
|
||||||
|
clientOptions.model = process.env.BROWSER_MODEL;
|
||||||
|
}
|
||||||
|
|
||||||
const browserClient = async ({ text, onProgress, convo, abortController }) => {
|
const browserClient = async ({ text, onProgress, convo, abortController }) => {
|
||||||
const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api');
|
const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const { Configuration, OpenAIApi } = require('openai');
|
const { Configuration, OpenAIApi } = require('openai');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const proxyEnvToAxiosProxy = (proxyString) => {
|
const proxyEnvToAxiosProxy = proxyString => {
|
||||||
if (!proxyString) return null;
|
if (!proxyString) return null;
|
||||||
|
|
||||||
const regex = /^([^:]+):\/\/(?:([^:@]*):?([^:@]*)@)?([^:]+)(?::(\d+))?/;
|
const regex = /^([^:]+):\/\/(?:([^:@]*):?([^:@]*)@)?([^:]+)(?::(\d+))?/;
|
||||||
|
|
@ -18,33 +18,37 @@ const proxyEnvToAxiosProxy = (proxyString) => {
|
||||||
|
|
||||||
const titleConvo = async ({ model, text, response }) => {
|
const titleConvo = async ({ model, text, response }) => {
|
||||||
let title = 'New Chat';
|
let title = 'New Chat';
|
||||||
|
|
||||||
|
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: `
|
||||||
|
}
|
||||||
|
],
|
||||||
|
temperature: 0,
|
||||||
|
presence_penalty: 0,
|
||||||
|
frequency_penalty: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.log('REQUEST', request);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const configuration = new Configuration({
|
const configuration = new Configuration({
|
||||||
apiKey: process.env.OPENAI_KEY
|
apiKey: process.env.OPENAI_KEY
|
||||||
});
|
});
|
||||||
const openai = new OpenAIApi(configuration);
|
const openai = new OpenAIApi(configuration);
|
||||||
const completion = await openai.createChatCompletion(
|
const completion = await openai.createChatCompletion(request, {
|
||||||
{
|
proxy: proxyEnvToAxiosProxy(process.env.PROXY || null)
|
||||||
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: `
|
|
||||||
}
|
|
||||||
],
|
|
||||||
temperature: 0,
|
|
||||||
presence_penalty: 0,
|
|
||||||
frequency_penalty: 0,
|
|
||||||
},
|
|
||||||
{ proxy: proxyEnvToAxiosProxy(process.env.PROXY || null) }
|
|
||||||
);
|
|
||||||
|
|
||||||
//eslint-disable-next-line
|
//eslint-disable-next-line
|
||||||
title = completion.data.choices[0].message.content.replace(/["\.]/g, '');
|
title = completion.data.choices[0].message.content.replace(/["\.]/g, '');
|
||||||
|
|
|
||||||
70
api/lib/db/indexSync.js
Normal file
70
api/lib/db/indexSync.js
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Conversation = mongoose.models.Conversation;
|
||||||
|
const Message = mongoose.models.Message;
|
||||||
|
const { MeiliSearch } = require('meilisearch');
|
||||||
|
let currentTimeout = null;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
async function indexSync(req, res, next) {
|
||||||
|
try {
|
||||||
|
if (!process.env.MEILI_HOST || !process.env.MEILI_MASTER_KEY || !process.env.SEARCH) {
|
||||||
|
throw new Error('Meilisearch not configured, search will be disabled.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new MeiliSearch({
|
||||||
|
host: process.env.MEILI_HOST,
|
||||||
|
apiKey: process.env.MEILI_MASTER_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const { status } = await client.health();
|
||||||
|
// console.log(`Meilisearch: ${status}`);
|
||||||
|
const result = status === 'available' && !!process.env.SEARCH;
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Meilisearch not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageCount = await Message.countDocuments();
|
||||||
|
const convoCount = await Conversation.countDocuments();
|
||||||
|
const messages = await client.index('messages').getStats();
|
||||||
|
const convos = await client.index('convos').getStats();
|
||||||
|
const messagesIndexed = messages.numberOfDocuments;
|
||||||
|
const convosIndexed = convos.numberOfDocuments;
|
||||||
|
|
||||||
|
console.log(`There are ${messageCount} messages in the database, ${messagesIndexed} indexed`);
|
||||||
|
console.log(`There are ${convoCount} convos in the database, ${convosIndexed} indexed`);
|
||||||
|
|
||||||
|
if (messageCount !== messagesIndexed) {
|
||||||
|
console.log('Messages out of sync, indexing');
|
||||||
|
await Message.syncWithMeili();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (convoCount !== convosIndexed) {
|
||||||
|
console.log('Convos out of sync, indexing');
|
||||||
|
await Conversation.syncWithMeili();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// console.log('in index sync');
|
||||||
|
if (err.message.includes('not found')) {
|
||||||
|
console.log('Creating indices...');
|
||||||
|
currentTimeout = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await Message.syncWithMeili();
|
||||||
|
await Conversation.syncWithMeili();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Trouble creating indices, try restarting the server.');
|
||||||
|
}
|
||||||
|
}, 750);
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
// res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
console.log('Clearing sync timeouts before exiting...');
|
||||||
|
clearTimeout(currentTimeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = indexSync;
|
||||||
15
api/lib/utils/misc.js
Normal file
15
api/lib/utils/misc.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
const cleanUpPrimaryKeyValue = (value) => {
|
||||||
|
// For Bing convoId handling
|
||||||
|
return value.replace(/--/g, '-');
|
||||||
|
};
|
||||||
|
|
||||||
|
function replaceSup(text) {
|
||||||
|
if (!text.includes('<sup>')) return text;
|
||||||
|
const replacedText = text.replace(/<sup>/g, '^').replace(/\s+<\/sup>/g, '^');
|
||||||
|
return replacedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
cleanUpPrimaryKeyValue,
|
||||||
|
replaceSup
|
||||||
|
};
|
||||||
|
|
@ -49,16 +49,16 @@ const configSchema = mongoose.Schema(
|
||||||
);
|
);
|
||||||
|
|
||||||
// Instance method
|
// Instance method
|
||||||
ConfigSchema.methods.incrementCount = function () {
|
configSchema.methods.incrementCount = function () {
|
||||||
this.startupCounts += 1;
|
this.startupCounts += 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Static methods
|
// Static methods
|
||||||
ConfigSchema.statics.findByTag = async function (tag) {
|
configSchema.statics.findByTag = async function (tag) {
|
||||||
return await this.findOne({ tag });
|
return await this.findOne({ tag });
|
||||||
};
|
};
|
||||||
|
|
||||||
ConfigSchema.statics.updateByTag = async function (tag, update) {
|
configSchema.statics.updateByTag = async function (tag, update) {
|
||||||
return await this.findOneAndUpdate({ tag }, update, { new: true });
|
return await this.findOneAndUpdate({ tag }, update, { new: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,8 @@
|
||||||
const mongoose = require('mongoose');
|
// const { Conversation } = require('./plugins');
|
||||||
const mongoMeili = require('../lib/db/mongoMeili');
|
const Conversation = require('./schema/convoSchema');
|
||||||
|
const { cleanUpPrimaryKeyValue } = require('../lib/utils/misc');
|
||||||
const { getMessages, deleteMessages } = require('./Message');
|
const { getMessages, deleteMessages } = require('./Message');
|
||||||
|
|
||||||
const convoSchema = mongoose.Schema(
|
|
||||||
{
|
|
||||||
conversationId: {
|
|
||||||
type: String,
|
|
||||||
unique: true,
|
|
||||||
required: true,
|
|
||||||
index: true,
|
|
||||||
meiliIndex: true
|
|
||||||
},
|
|
||||||
parentMessageId: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: 'New Chat',
|
|
||||||
meiliIndex: true
|
|
||||||
},
|
|
||||||
jailbreakConversationId: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
conversationSignature: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
clientId: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
invocationId: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
chatGptLabel: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
promptPrefix: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
suggestions: [{ type: String }],
|
|
||||||
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }]
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// convoSchema.plugin(mongoMeili, {
|
|
||||||
// host: process.env.MEILI_HOST,
|
|
||||||
// apiKey: process.env.MEILI_KEY,
|
|
||||||
// indexName: 'convos', // Will get created automatically if it doesn't exist already
|
|
||||||
// primaryKey: 'conversationId'
|
|
||||||
// });
|
|
||||||
|
|
||||||
const Conversation =
|
|
||||||
mongoose.models.Conversation || mongoose.model('Conversation', convoSchema);
|
|
||||||
|
|
||||||
const getConvo = async (user, conversationId) => {
|
const getConvo = async (user, conversationId) => {
|
||||||
try {
|
try {
|
||||||
return await Conversation.findOne({ user, conversationId }).exec();
|
return await Conversation.findOne({ user, conversationId }).exec();
|
||||||
|
|
@ -143,15 +81,16 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const cache = {};
|
const cache = {};
|
||||||
|
const convoMap = {};
|
||||||
const promises = [];
|
const promises = [];
|
||||||
// will handle a syncing solution soon
|
// will handle a syncing solution soon
|
||||||
const deletedConvoIds = [];
|
const deletedConvoIds = [];
|
||||||
|
|
||||||
convoIds.forEach((convo) =>
|
convoIds.forEach(convo =>
|
||||||
promises.push(
|
promises.push(
|
||||||
Conversation.findOne({
|
Conversation.findOne({
|
||||||
user,
|
user,
|
||||||
conversationId: convo.conversationId
|
conversationId: cleanUpPrimaryKeyValue(convo.conversationId)
|
||||||
}).exec()
|
}).exec()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -166,6 +105,7 @@ module.exports = {
|
||||||
cache[page] = [];
|
cache[page] = [];
|
||||||
}
|
}
|
||||||
cache[page].push(convo);
|
cache[page].push(convo);
|
||||||
|
convoMap[convo.conversationId] = convo;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -183,6 +123,7 @@ module.exports = {
|
||||||
pageSize,
|
pageSize,
|
||||||
// will handle a syncing solution soon
|
// will handle a syncing solution soon
|
||||||
filter: new Set(deletedConvoIds),
|
filter: new Set(deletedConvoIds),
|
||||||
|
convoMap
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
@ -208,6 +149,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
deleteConvos: async (user, filter) => {
|
deleteConvos: async (user, filter) => {
|
||||||
let deleteCount = await Conversation.deleteMany({ ...filter, user }).exec();
|
let deleteCount = await Conversation.deleteMany({ ...filter, user }).exec();
|
||||||
|
console.log('deleteCount', deleteCount);
|
||||||
deleteCount.messages = await deleteMessages(filter);
|
deleteCount.messages = await deleteMessages(filter);
|
||||||
return deleteCount;
|
return deleteCount;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,5 @@
|
||||||
const mongoose = require('mongoose');
|
const Message = require('./schema/messageSchema');
|
||||||
const mongoMeili = require('../lib/db/mongoMeili');
|
|
||||||
|
|
||||||
const messageSchema = mongoose.Schema({
|
|
||||||
messageId: {
|
|
||||||
type: String,
|
|
||||||
unique: true,
|
|
||||||
required: true,
|
|
||||||
index: true,
|
|
||||||
meiliIndex: true
|
|
||||||
},
|
|
||||||
conversationId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
meiliIndex: true
|
|
||||||
},
|
|
||||||
conversationSignature: {
|
|
||||||
type: String,
|
|
||||||
// required: true
|
|
||||||
},
|
|
||||||
clientId: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
invocationId: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
parentMessageId: {
|
|
||||||
type: String,
|
|
||||||
// required: true
|
|
||||||
},
|
|
||||||
sender: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
meiliIndex: true
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
meiliIndex: true
|
|
||||||
},
|
|
||||||
isCreatedByUser: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
_meiliIndex: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
select: false,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
// messageSchema.plugin(mongoMeili, {
|
|
||||||
// host: process.env.MEILI_HOST,
|
|
||||||
// apiKey: process.env.MEILI_KEY,
|
|
||||||
// indexName: 'messages', // Will get created automatically if it doesn't exist already
|
|
||||||
// primaryKey: 'messageId',
|
|
||||||
// });
|
|
||||||
|
|
||||||
const Message = mongoose.models.Message || mongoose.model('Message', messageSchema);
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
messageSchema,
|
|
||||||
Message,
|
Message,
|
||||||
saveMessage: async ({ messageId, conversationId, parentMessageId, sender, text, isCreatedByUser=false, error }) => {
|
saveMessage: async ({ messageId, conversationId, parentMessageId, sender, text, isCreatedByUser=false, error }) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
const { MeiliSearch } = require('meilisearch');
|
const { MeiliSearch } = require('meilisearch');
|
||||||
|
const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const validateOptions = function (options) {
|
const validateOptions = function (options) {
|
||||||
|
|
@ -54,7 +56,9 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
|
||||||
if (populate) {
|
if (populate) {
|
||||||
// Find objects into mongodb matching `objectID` from Meili search
|
// Find objects into mongodb matching `objectID` from Meili search
|
||||||
const query = {};
|
const query = {};
|
||||||
query[primaryKey] = { $in: _.map(data.hits, primaryKey) };
|
// query[primaryKey] = { $in: _.map(data.hits, primaryKey) };
|
||||||
|
query[primaryKey] = _.map(data.hits, hit => cleanUpPrimaryKeyValue(hit[primaryKey]));
|
||||||
|
// console.log('query', query);
|
||||||
const hitsFromMongoose = await this.find(
|
const hitsFromMongoose = await this.find(
|
||||||
query,
|
query,
|
||||||
_.reduce(
|
_.reduce(
|
||||||
|
|
@ -86,13 +90,18 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
|
||||||
// Push new document to Meili
|
// Push new document to Meili
|
||||||
async addObjectToMeili() {
|
async addObjectToMeili() {
|
||||||
const object = _.pick(this.toJSON(), attributesToIndex);
|
const object = _.pick(this.toJSON(), attributesToIndex);
|
||||||
|
// NOTE: MeiliSearch does not allow | in primary key, so we replace it with - for Bing convoIds
|
||||||
|
// object.conversationId = object.conversationId.replace(/\|/g, '-');
|
||||||
|
if (object.conversationId && object.conversationId.includes('|')) {
|
||||||
|
object.conversationId = object.conversationId.replace(/\|/g, '--');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// console.log('Adding document to Meili', object);
|
// console.log('Adding document to Meili', object);
|
||||||
await index.addDocuments([object]);
|
await index.addDocuments([object]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error adding document to Meili');
|
// console.log('Error adding document to Meili');
|
||||||
console.error(error);
|
// console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.collection.updateMany({ _id: this._id }, { $set: { _meiliIndex: true } });
|
await this.collection.updateMany({ _id: this._id }, { $set: { _meiliIndex: true } });
|
||||||
|
|
@ -100,7 +109,7 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
|
||||||
|
|
||||||
// Update an existing document in Meili
|
// Update an existing document in Meili
|
||||||
async updateObjectToMeili() {
|
async updateObjectToMeili() {
|
||||||
const object = pick(this.toJSON(), attributesToIndex);
|
const object = _.pick(this.toJSON(), attributesToIndex);
|
||||||
await index.updateDocuments([object]);
|
await index.updateDocuments([object]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,6 +193,18 @@ module.exports = function mongoMeili(schema, options) {
|
||||||
schema.post('remove', function (doc) {
|
schema.post('remove', function (doc) {
|
||||||
doc.postRemoveHook();
|
doc.postRemoveHook();
|
||||||
});
|
});
|
||||||
|
schema.post('deleteMany', function () {
|
||||||
|
// console.log('deleteMany hook', doc);
|
||||||
|
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messages')) {
|
||||||
|
console.log('Syncing convos...');
|
||||||
|
mongoose.model('Conversation').syncWithMeili();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messageId')) {
|
||||||
|
console.log('Syncing messages...');
|
||||||
|
mongoose.model('Message').syncWithMeili();
|
||||||
|
}
|
||||||
|
});
|
||||||
schema.post('findOneAndUpdate', function (doc) {
|
schema.post('findOneAndUpdate', function (doc) {
|
||||||
doc.postSaveHook();
|
doc.postSaveHook();
|
||||||
});
|
});
|
||||||
67
api/models/schema/convoSchema.js
Normal file
67
api/models/schema/convoSchema.js
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const mongoMeili = require('../plugins/mongoMeili');
|
||||||
|
const convoSchema = mongoose.Schema(
|
||||||
|
{
|
||||||
|
conversationId: {
|
||||||
|
type: String,
|
||||||
|
unique: true,
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
meiliIndex: true
|
||||||
|
},
|
||||||
|
parentMessageId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: 'New Chat',
|
||||||
|
meiliIndex: true
|
||||||
|
},
|
||||||
|
jailbreakConversationId: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
conversationSignature: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
clientId: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
invocationId: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
chatGptLabel: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
promptPrefix: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
suggestions: [{ type: String }],
|
||||||
|
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }]
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
||||||
|
convoSchema.plugin(mongoMeili, {
|
||||||
|
host: process.env.MEILI_HOST,
|
||||||
|
apiKey: process.env.MEILI_MASTER_KEY,
|
||||||
|
indexName: 'convos', // Will get created automatically if it doesn't exist already
|
||||||
|
primaryKey: 'conversationId'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const Conversation = mongoose.models.Conversation || mongoose.model('Conversation', convoSchema);
|
||||||
|
|
||||||
|
module.exports = Conversation;
|
||||||
71
api/models/schema/messageSchema.js
Normal file
71
api/models/schema/messageSchema.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const mongoMeili = require('../plugins/mongoMeili');
|
||||||
|
const messageSchema = mongoose.Schema(
|
||||||
|
{
|
||||||
|
messageId: {
|
||||||
|
type: String,
|
||||||
|
unique: true,
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
meiliIndex: true
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
meiliIndex: true
|
||||||
|
},
|
||||||
|
conversationSignature: {
|
||||||
|
type: String
|
||||||
|
// required: true
|
||||||
|
},
|
||||||
|
clientId: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
invocationId: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
parentMessageId: {
|
||||||
|
type: String
|
||||||
|
// required: true
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
meiliIndex: true
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
meiliIndex: true
|
||||||
|
},
|
||||||
|
isCreatedByUser: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
_meiliIndex: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
select: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
||||||
|
messageSchema.plugin(mongoMeili, {
|
||||||
|
host: process.env.MEILI_HOST,
|
||||||
|
apiKey: process.env.MEILI_MASTER_KEY,
|
||||||
|
indexName: 'messages',
|
||||||
|
primaryKey: 'messageId'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const Message = mongoose.models.Message || mongoose.model('Message', messageSchema);
|
||||||
|
|
||||||
|
module.exports = Message;
|
||||||
33
api/server/controllers/errorController.js
Normal file
33
api/server/controllers/errorController.js
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
//handle duplicates
|
||||||
|
const handleDuplicateKeyError = (err, res) => {
|
||||||
|
const field = Object.keys(err.keyValue);
|
||||||
|
const code = 409;
|
||||||
|
const error = `An document with that ${field} already exists.`;
|
||||||
|
console.log('congrats you hit the duped keys error');
|
||||||
|
res.status(code).send({ messages: error, fields: field });
|
||||||
|
};
|
||||||
|
|
||||||
|
//handle validation errors
|
||||||
|
const handleValidationError = (err, res) => {
|
||||||
|
console.log('congrats you hit the validation middleware');
|
||||||
|
let errors = Object.values(err.errors).map(el => el.message);
|
||||||
|
let fields = Object.values(err.errors).map(el => el.path);
|
||||||
|
let code = 400;
|
||||||
|
if (errors.length > 1) {
|
||||||
|
const formattedErrors = errors.join(' ');
|
||||||
|
res.status(code).send({ messages: formattedErrors, fields: fields });
|
||||||
|
} else {
|
||||||
|
res.status(code).send({ messages: errors, fields: fields });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
module.exports = (err, req, res, next) => {
|
||||||
|
try {
|
||||||
|
console.log('congrats you hit the error middleware');
|
||||||
|
if (err.name === 'ValidationError') return (err = handleValidationError(err, res));
|
||||||
|
if (err.code && err.code == 11000) return (err = handleDuplicateKeyError(err, res));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).send('An unknown error occurred.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,70 +1,96 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const session = require('express-session')
|
const session = require('express-session');
|
||||||
const connectDb = require('../lib/db/connectDb');
|
const connectDb = require('../lib/db/connectDb');
|
||||||
const migrateDb = require('../lib/db/migrateDb');
|
const migrateDb = require('../lib/db/migrateDb');
|
||||||
|
const indexSync = require('../lib/db/indexSync');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const routes = require('./routes');
|
const routes = require('./routes');
|
||||||
const app = express();
|
const errorController = require('./controllers/errorController');
|
||||||
|
|
||||||
const port = process.env.PORT || 3080;
|
const port = process.env.PORT || 3080;
|
||||||
const host = process.env.HOST || 'localhost'
|
const host = process.env.HOST || 'localhost';
|
||||||
const userSystemEnabled = process.env.ENABLE_USER_SYSTEM || false
|
const userSystemEnabled = process.env.ENABLE_USER_SYSTEM || false;
|
||||||
const projectPath = path.join(__dirname, '..', '..', 'client');
|
const projectPath = path.join(__dirname, '..', '..', 'client');
|
||||||
connectDb().then(() => {
|
|
||||||
|
(async () => {
|
||||||
|
await connectDb();
|
||||||
console.log('Connected to MongoDB');
|
console.log('Connected to MongoDB');
|
||||||
migrateDb();
|
await migrateDb();
|
||||||
});
|
await indexSync();
|
||||||
|
|
||||||
app.use(cors());
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(errorController);
|
||||||
app.use(express.static(path.join(projectPath, 'public')));
|
app.use(cors());
|
||||||
app.set('trust proxy', 1) // trust first proxy
|
app.use(express.json());
|
||||||
app.use(session({
|
app.use(express.static(path.join(projectPath, 'public')));
|
||||||
secret: 'chatgpt-clone-random-secrect',
|
app.set('trust proxy', 1); // trust first proxy
|
||||||
resave: false,
|
app.use(
|
||||||
saveUninitialized: true,
|
session({
|
||||||
}))
|
secret: 'chatgpt-clone-random-secrect',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
/* chore: potential redirect error here, can only comment out this block;
|
/* chore: potential redirect error here, can only comment out this block;
|
||||||
comment back in if using auth routes i guess */
|
comment back in if using auth routes i guess */
|
||||||
// app.get('/', routes.authenticatedOrRedirect, function (req, res) {
|
// app.get('/', routes.authenticatedOrRedirect, function (req, res) {
|
||||||
// console.log(path.join(projectPath, 'public', 'index.html'));
|
// console.log(path.join(projectPath, 'public', 'index.html'));
|
||||||
// res.sendFile(path.join(projectPath, 'public', 'index.html'));
|
// res.sendFile(path.join(projectPath, 'public', 'index.html'));
|
||||||
// });
|
// });
|
||||||
|
|
||||||
app.get('/api/me', function (req, res) {
|
app.get('/api/me', function (req, res) {
|
||||||
if (userSystemEnabled) {
|
if (userSystemEnabled) {
|
||||||
const user = req?.session?.user
|
const user = req?.session?.user;
|
||||||
|
|
||||||
if (user)
|
if (user) res.send(JSON.stringify({ username: user?.username, display: user?.display }));
|
||||||
res.send(JSON.stringify({username: user?.username, display: user?.display}));
|
else res.send(JSON.stringify(null));
|
||||||
|
} else {
|
||||||
|
res.send(JSON.stringify({ username: 'anonymous_user', display: 'Anonymous User' }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/api/search', routes.authenticatedOr401, routes.search);
|
||||||
|
app.use('/api/ask', routes.authenticatedOr401, routes.ask);
|
||||||
|
app.use('/api/messages', routes.authenticatedOr401, routes.messages);
|
||||||
|
app.use('/api/convos', routes.authenticatedOr401, routes.convos);
|
||||||
|
app.use('/api/customGpts', routes.authenticatedOr401, routes.customGpts);
|
||||||
|
app.use('/api/prompts', routes.authenticatedOr401, routes.prompts);
|
||||||
|
app.use('/auth', routes.auth);
|
||||||
|
|
||||||
|
app.get('/api/models', function (req, res) {
|
||||||
|
const hasOpenAI = !!process.env.OPENAI_KEY;
|
||||||
|
const hasChatGpt = !!process.env.CHATGPT_TOKEN;
|
||||||
|
const hasBing = !!process.env.BING_TOKEN;
|
||||||
|
|
||||||
|
res.send(JSON.stringify({ hasOpenAI, hasChatGpt, hasBing }));
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
else
|
||||||
res.send(JSON.stringify(null));
|
console.log(
|
||||||
|
`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
let messageCount = 0;
|
||||||
|
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) {
|
||||||
|
console.error('Meilisearch error, search will be disabled');
|
||||||
|
messageCount++;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
res.send(JSON.stringify({username: 'anonymous_user', display: 'Anonymous User'}));
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use('/api/search', routes.authenticatedOr401, routes.search);
|
|
||||||
app.use('/api/ask', routes.authenticatedOr401, routes.ask);
|
|
||||||
app.use('/api/messages', routes.authenticatedOr401, routes.messages);
|
|
||||||
app.use('/api/convos', routes.authenticatedOr401, routes.convos);
|
|
||||||
app.use('/api/customGpts', routes.authenticatedOr401, routes.customGpts);
|
|
||||||
app.use('/api/prompts', routes.authenticatedOr401, routes.prompts);
|
|
||||||
app.use('/auth', routes.auth);
|
|
||||||
|
|
||||||
|
|
||||||
app.get('/api/models', function (req, res) {
|
|
||||||
const hasOpenAI = !!process.env.OPENAI_KEY;
|
|
||||||
const hasChatGpt = !!process.env.CHATGPT_TOKEN;
|
|
||||||
const hasBing = !!process.env.BING_TOKEN;
|
|
||||||
|
|
||||||
res.send(JSON.stringify({ hasOpenAI, hasChatGpt, hasBing }));
|
|
||||||
});
|
|
||||||
|
|
||||||
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}`);
|
|
||||||
});
|
|
||||||
|
|
@ -19,25 +19,22 @@ router.get('/:conversationId', async (req, res) => {
|
||||||
router.post('/gen_title', async (req, res) => {
|
router.post('/gen_title', async (req, res) => {
|
||||||
const { conversationId } = req.body.arg;
|
const { conversationId } = req.body.arg;
|
||||||
|
|
||||||
const convo = await getConvo(req?.session?.user?.username, conversationId)
|
const convo = await getConvo(req?.session?.user?.username, conversationId);
|
||||||
const firstMessage = (await getMessages({ conversationId }))[0]
|
const firstMessage = (await getMessages({ conversationId }))[0];
|
||||||
const secondMessage = (await getMessages({ conversationId }))[1]
|
const secondMessage = (await getMessages({ conversationId }))[1];
|
||||||
|
|
||||||
const title = convo.jailbreakConversationId
|
const title = convo.jailbreakConversationId
|
||||||
? await getConvoTitle(req?.session?.user?.username, conversationId)
|
? await getConvoTitle(req?.session?.user?.username, conversationId)
|
||||||
: await titleConvo({
|
: await titleConvo({
|
||||||
model: convo?.model,
|
model: convo?.model,
|
||||||
message: firstMessage?.text,
|
message: firstMessage?.text,
|
||||||
response: JSON.stringify(secondMessage?.text || '')
|
response: JSON.stringify(secondMessage?.text || '')
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveConvo(
|
await saveConvo(req?.session?.user?.username, {
|
||||||
req?.session?.user?.username,
|
conversationId,
|
||||||
{
|
title
|
||||||
conversationId,
|
});
|
||||||
title
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
res.status(200).send(title);
|
res.status(200).send(title);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const { MeiliSearch } = require('meilisearch');
|
||||||
const { Message } = require('../../models/Message');
|
const { Message } = require('../../models/Message');
|
||||||
const { Conversation, getConvosQueried } = require('../../models/Conversation');
|
const { Conversation, getConvosQueried } = require('../../models/Conversation');
|
||||||
const { reduceMessages, reduceHits } = require('../../lib/utils/reduceHits');
|
const { reduceHits } = require('../../lib/utils/reduceHits');
|
||||||
// const { MeiliSearch } = require('meilisearch');
|
const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc');
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
|
|
||||||
router.get('/sync', async function (req, res) {
|
router.get('/sync', async function (req, res) {
|
||||||
|
|
@ -22,8 +23,8 @@ router.get('/', async function (req, res) {
|
||||||
if (cache.has(key)) {
|
if (cache.has(key)) {
|
||||||
console.log('cache hit', key);
|
console.log('cache hit', key);
|
||||||
const cached = cache.get(key);
|
const cached = cache.get(key);
|
||||||
const { pages, pageSize } = cached;
|
const { pages, pageSize, messages } = cached;
|
||||||
res.status(200).send({ conversations: cached[pageNumber], pages, pageNumber, pageSize });
|
res.status(200).send({ conversations: cached[pageNumber], pages, pageNumber, pageSize, messages });
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
cache.clear();
|
cache.clear();
|
||||||
|
|
@ -49,11 +50,29 @@ router.get('/', async function (req, res) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const titles = (await Conversation.meiliSearch(q)).hits;
|
const titles = (await Conversation.meiliSearch(q)).hits;
|
||||||
|
console.log('message hits:', messages.length, 'convo hits:', titles.length);
|
||||||
const sortedHits = reduceHits(messages, titles);
|
const sortedHits = reduceHits(messages, titles);
|
||||||
const result = await getConvosQueried(user, sortedHits, pageNumber);
|
const result = await getConvosQueried(user, sortedHits, pageNumber);
|
||||||
cache.set(q, result.cache);
|
|
||||||
delete result.cache;
|
const activeMessages = [];
|
||||||
result.messages = messages.filter((message) => !result.filter.has(message.conversationId));
|
for (let i = 0; i < messages.length; i++) {
|
||||||
|
let message = messages[i];
|
||||||
|
if (message.conversationId.includes('--')) {
|
||||||
|
message.conversationId = cleanUpPrimaryKeyValue(message.conversationId);
|
||||||
|
}
|
||||||
|
if (result.convoMap[message.conversationId] && !message.error) {
|
||||||
|
message = { ...message, title: result.convoMap[message.conversationId].title };
|
||||||
|
activeMessages.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.messages = activeMessages;
|
||||||
|
if (result.cache) {
|
||||||
|
result.cache.messages = activeMessages;
|
||||||
|
cache.set(key, result.cache);
|
||||||
|
delete result.cache;
|
||||||
|
}
|
||||||
|
delete result.convoMap;
|
||||||
|
// for debugging
|
||||||
// console.log(result, messages.length);
|
// console.log(result, messages.length);
|
||||||
res.status(200).send(result);
|
res.status(200).send(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -78,4 +97,22 @@ router.get('/test', async function (req, res) {
|
||||||
res.send(messages);
|
res.send(messages);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/enable', async function (req, res) {
|
||||||
|
let result = false;
|
||||||
|
try {
|
||||||
|
const client = new MeiliSearch({
|
||||||
|
host: process.env.MEILI_HOST,
|
||||||
|
apiKey: process.env.MEILI_MASTER_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const { status } = await client.health();
|
||||||
|
// console.log(`Meilisearch: ${status}`);
|
||||||
|
result = status === 'available' && !!process.env.SEARCH;
|
||||||
|
return res.send(result);
|
||||||
|
} catch (error) {
|
||||||
|
// console.error(error);
|
||||||
|
return res.send(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
22
client/.prettierrc
Normal file
22
client/.prettierrc
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"insertPragma": false,
|
||||||
|
"singleAttributePerLine": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"printWidth": 110,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"requirePragma": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"useTabs": false,
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"parser": "babel"
|
||||||
|
}
|
||||||
136
client/package-lock.json
generated
136
client/package-lock.json
generated
|
|
@ -34,8 +34,10 @@
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"rehype-highlight": "^6.0.0",
|
"rehype-highlight": "^6.0.0",
|
||||||
"rehype-katex": "^6.0.2",
|
"rehype-katex": "^6.0.2",
|
||||||
|
"rehype-raw": "^6.1.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
|
"remark-supersub": "^1.0.0",
|
||||||
"swr": "^2.0.3",
|
"swr": "^2.0.3",
|
||||||
"tailwind-merge": "^1.9.1",
|
"tailwind-merge": "^1.9.1",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
|
|
@ -3185,6 +3187,11 @@
|
||||||
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
|
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/parse5": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g=="
|
||||||
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.5",
|
"version": "15.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||||
|
|
@ -6812,6 +6819,45 @@
|
||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hast-util-raw": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/hast": "^2.0.0",
|
||||||
|
"@types/parse5": "^6.0.0",
|
||||||
|
"hast-util-from-parse5": "^7.0.0",
|
||||||
|
"hast-util-to-parse5": "^7.0.0",
|
||||||
|
"html-void-elements": "^2.0.0",
|
||||||
|
"parse5": "^6.0.0",
|
||||||
|
"unist-util-position": "^4.0.0",
|
||||||
|
"unist-util-visit": "^4.0.0",
|
||||||
|
"vfile": "^5.0.0",
|
||||||
|
"web-namespaces": "^2.0.0",
|
||||||
|
"zwitch": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/unified"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hast-util-to-parse5": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/hast": "^2.0.0",
|
||||||
|
"comma-separated-tokens": "^2.0.0",
|
||||||
|
"property-information": "^6.0.0",
|
||||||
|
"space-separated-tokens": "^2.0.0",
|
||||||
|
"web-namespaces": "^2.0.0",
|
||||||
|
"zwitch": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/unified"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hast-util-to-text": {
|
"node_modules/hast-util-to-text": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz",
|
||||||
|
|
@ -6926,6 +6972,15 @@
|
||||||
"integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
|
"integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/html-void-elements": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-deceiver": {
|
"node_modules/http-deceiver": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
|
||||||
|
|
@ -10916,6 +10971,20 @@
|
||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rehype-raw": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/hast": "^2.0.0",
|
||||||
|
"hast-util-raw": "^7.2.0",
|
||||||
|
"unified": "^10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/unified"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/remark-gfm": {
|
"node_modules/remark-gfm": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz",
|
||||||
|
|
@ -10975,6 +11044,14 @@
|
||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/remark-supersub": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/remark-supersub/-/remark-supersub-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-3SYsphMqpAWbr8AZozdcypozinl/lly3e7BEwPG3YT5J9uZQaDcELBF6/sr/OZoAlFxy2nhNFWSrZBu/ZPRT3Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"unist-util-visit": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/require-from-string": {
|
"node_modules/require-from-string": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
|
@ -15214,6 +15291,11 @@
|
||||||
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
|
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/parse5": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g=="
|
||||||
|
},
|
||||||
"@types/prop-types": {
|
"@types/prop-types": {
|
||||||
"version": "15.7.5",
|
"version": "15.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||||
|
|
@ -18033,6 +18115,37 @@
|
||||||
"@types/hast": "^2.0.0"
|
"@types/hast": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"hast-util-raw": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/hast": "^2.0.0",
|
||||||
|
"@types/parse5": "^6.0.0",
|
||||||
|
"hast-util-from-parse5": "^7.0.0",
|
||||||
|
"hast-util-to-parse5": "^7.0.0",
|
||||||
|
"html-void-elements": "^2.0.0",
|
||||||
|
"parse5": "^6.0.0",
|
||||||
|
"unist-util-position": "^4.0.0",
|
||||||
|
"unist-util-visit": "^4.0.0",
|
||||||
|
"vfile": "^5.0.0",
|
||||||
|
"web-namespaces": "^2.0.0",
|
||||||
|
"zwitch": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hast-util-to-parse5": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==",
|
||||||
|
"requires": {
|
||||||
|
"@types/hast": "^2.0.0",
|
||||||
|
"comma-separated-tokens": "^2.0.0",
|
||||||
|
"property-information": "^6.0.0",
|
||||||
|
"space-separated-tokens": "^2.0.0",
|
||||||
|
"web-namespaces": "^2.0.0",
|
||||||
|
"zwitch": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"hast-util-to-text": {
|
"hast-util-to-text": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz",
|
||||||
|
|
@ -18134,6 +18247,11 @@
|
||||||
"integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
|
"integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"html-void-elements": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A=="
|
||||||
|
},
|
||||||
"http-deceiver": {
|
"http-deceiver": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
|
||||||
|
|
@ -20727,6 +20845,16 @@
|
||||||
"unified": "^10.0.0"
|
"unified": "^10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"rehype-raw": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==",
|
||||||
|
"requires": {
|
||||||
|
"@types/hast": "^2.0.0",
|
||||||
|
"hast-util-raw": "^7.2.0",
|
||||||
|
"unified": "^10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"remark-gfm": {
|
"remark-gfm": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz",
|
||||||
|
|
@ -20770,6 +20898,14 @@
|
||||||
"unified": "^10.0.0"
|
"unified": "^10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"remark-supersub": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/remark-supersub/-/remark-supersub-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-3SYsphMqpAWbr8AZozdcypozinl/lly3e7BEwPG3YT5J9uZQaDcELBF6/sr/OZoAlFxy2nhNFWSrZBu/ZPRT3Q==",
|
||||||
|
"requires": {
|
||||||
|
"unist-util-visit": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"require-from-string": {
|
"require-from-string": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,10 @@
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"rehype-highlight": "^6.0.0",
|
"rehype-highlight": "^6.0.0",
|
||||||
"rehype-katex": "^6.0.2",
|
"rehype-katex": "^6.0.2",
|
||||||
|
"rehype-raw": "^6.1.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
|
"remark-supersub": "^1.0.0",
|
||||||
"swr": "^2.0.3",
|
"swr": "^2.0.3",
|
||||||
"tailwind-merge": "^1.9.1",
|
"tailwind-merge": "^1.9.1",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
|
|
|
||||||
|
|
@ -6,42 +6,34 @@ import Nav from './components/Nav';
|
||||||
import MobileNav from './components/Nav/MobileNav';
|
import MobileNav from './components/Nav/MobileNav';
|
||||||
import useDocumentTitle from '~/hooks/useDocumentTitle';
|
import useDocumentTitle from '~/hooks/useDocumentTitle';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import userAuth from './utils/userAuth';
|
||||||
import { setUser } from './store/userReducer';
|
import { setUser } from './store/userReducer';
|
||||||
|
import { setSearchState } from './store/searchSlice';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { messages, messageTree } = useSelector((state) => state.messages);
|
const { messages, messageTree } = useSelector((state) => state.messages);
|
||||||
const { user } = useSelector((state) => state.user);
|
const { user } = useSelector((state) => state.user);
|
||||||
const { title } = useSelector((state) => state.convo);
|
const { title } = useSelector((state) => state.convo);
|
||||||
const [ navVisible, setNavVisible ]= useState(false)
|
const [navVisible, setNavVisible] = useState(false);
|
||||||
useDocumentTitle(title);
|
useDocumentTitle(title);
|
||||||
|
|
||||||
useEffect(async () => {
|
useEffect(() => {
|
||||||
try {
|
axios.get('/api/search/enable').then((res) => { console.log(res.data); dispatch(setSearchState(res.data))});
|
||||||
const response = await axios.get('/api/me', {
|
userAuth()
|
||||||
timeout: 1000,
|
.then((user) => dispatch(setUser(user)))
|
||||||
withCredentials: true
|
.catch((err) => console.log(err));
|
||||||
});
|
}, []);
|
||||||
const user = response.data;
|
|
||||||
if (user) {
|
|
||||||
dispatch(setUser(user));
|
|
||||||
} else {
|
|
||||||
console.log('Not login!');
|
|
||||||
window.location.href = '/auth/login';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
console.log('Not login!');
|
|
||||||
window.location.href = '/auth/login';
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (user)
|
if (user)
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
<Nav navVisible={navVisible} setNavVisible={setNavVisible} />
|
<Nav
|
||||||
|
navVisible={navVisible}
|
||||||
|
setNavVisible={setNavVisible}
|
||||||
|
/>
|
||||||
<div className="flex h-full w-full flex-1 flex-col bg-gray-50 md:pl-[260px]">
|
<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">
|
<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} />
|
<MobileNav setNavVisible={setNavVisible} />
|
||||||
|
|
@ -58,12 +50,7 @@ const App = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
else
|
else return <div className="flex h-screen"></div>;
|
||||||
return (
|
|
||||||
<div className="flex h-screen">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,6 @@ export default function TextChat({ messages }) {
|
||||||
} else {
|
} else {
|
||||||
let text = data.text || data.response;
|
let text = data.text || data.response;
|
||||||
if (data.initial) {
|
if (data.initial) {
|
||||||
console.log(data);
|
|
||||||
dispatch(toggleCursor());
|
dispatch(toggleCursor());
|
||||||
}
|
}
|
||||||
if (data.message) {
|
if (data.message) {
|
||||||
|
|
@ -350,7 +349,7 @@ export default function TextChat({ messages }) {
|
||||||
const isSearchView = messages?.[0]?.searchResult === true;
|
const isSearchView = messages?.[0]?.searchResult === true;
|
||||||
const getPlaceholderText = () => {
|
const getPlaceholderText = () => {
|
||||||
if (isSearchView) {
|
if (isSearchView) {
|
||||||
return 'Click a message to open its conversation.'
|
return 'Click a message title to open its conversation.'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,33 @@ import rehypeKatex from 'rehype-katex';
|
||||||
import rehypeHighlight from 'rehype-highlight';
|
import rehypeHighlight from 'rehype-highlight';
|
||||||
import remarkMath from 'remark-math';
|
import remarkMath from 'remark-math';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import rehypeRaw from 'rehype-raw'
|
||||||
import CodeBlock from './CodeBlock';
|
import CodeBlock from './CodeBlock';
|
||||||
import { langSubset } from '~/utils/languages';
|
import { langSubset } from '~/utils/languages';
|
||||||
|
|
||||||
const Content = React.memo(({ content }) => {
|
const Content = React.memo(({ content, isCreatedByUser = false }) => {
|
||||||
|
const rehypePlugins = [
|
||||||
|
[rehypeKatex, { output: 'mathml' }],
|
||||||
|
[
|
||||||
|
rehypeHighlight,
|
||||||
|
{
|
||||||
|
detect: true,
|
||||||
|
ignoreMissing: true,
|
||||||
|
subset: langSubset
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[rehypeRaw],
|
||||||
|
]
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm, [remarkMath, { singleDollarTextMath: false }]]}
|
remarkPlugins={[remarkGfm, [remarkMath, { singleDollarTextMath: false }]]}
|
||||||
rehypePlugins={[
|
rehypePlugins={isCreatedByUser ? rehypePlugins.slice(-1) : rehypePlugins}
|
||||||
[rehypeKatex, { output: 'mathml' }],
|
|
||||||
[
|
|
||||||
rehypeHighlight,
|
|
||||||
{
|
|
||||||
detect: true,
|
|
||||||
ignoreMissing: true,
|
|
||||||
subset: langSubset
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]}
|
|
||||||
linkTarget="_new"
|
linkTarget="_new"
|
||||||
components={{
|
components={{
|
||||||
code,
|
code,
|
||||||
p,
|
p,
|
||||||
|
// em,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|
@ -53,33 +57,33 @@ const code = React.memo((props) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const p = React.memo((props) => {
|
const p = React.memo((props) => {
|
||||||
return <p className="whitespace-pre-wrap ">{props?.children}</p>;
|
return <span className="whitespace-pre-wrap mb-2">{props?.children}</span>;
|
||||||
});
|
});
|
||||||
|
|
||||||
const blinker = ({ node }) => {
|
// const blinker = ({ node }) => {
|
||||||
if (node.type === 'text' && node.value === '█') {
|
// if (node.type === 'text' && node.value === '█') {
|
||||||
return <span className="result-streaming">{node.value}</span>;
|
// return <span className="result-streaming">{node.value}</span>;
|
||||||
}
|
// }
|
||||||
|
|
||||||
return null;
|
// return null;
|
||||||
};
|
// };
|
||||||
|
|
||||||
const em = React.memo(({ node, ...props }) => {
|
// const em = React.memo(({ node, ...props }) => {
|
||||||
if (
|
// if (
|
||||||
props.children[0] &&
|
// props.children[0] &&
|
||||||
typeof props.children[0] === 'string' &&
|
// typeof props.children[0] === 'string' &&
|
||||||
props.children[0].startsWith('^')
|
// props.children[0].startsWith('^')
|
||||||
) {
|
// ) {
|
||||||
return <sup>{props.children[0].substring(1)}</sup>;
|
// return <sup>{props.children[0].substring(1)}</sup>;
|
||||||
}
|
// }
|
||||||
if (
|
// if (
|
||||||
props.children[0] &&
|
// props.children[0] &&
|
||||||
typeof props.children[0] === 'string' &&
|
// typeof props.children[0] === 'string' &&
|
||||||
props.children[0].startsWith('~')
|
// props.children[0].startsWith('~')
|
||||||
) {
|
// ) {
|
||||||
return <sub>{props.children[0].substring(1)}</sub>;
|
// return <sub>{props.children[0].substring(1)}</sub>;
|
||||||
}
|
// }
|
||||||
return <i {...props} />;
|
// return <i {...props} />;
|
||||||
});
|
// });
|
||||||
|
|
||||||
export default Content;
|
export default Content;
|
||||||
|
|
|
||||||
9
client/src/components/Messages/Content/SubRow.jsx
Normal file
9
client/src/components/Messages/Content/SubRow.jsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function SubRow({ children, classes = '', subclasses = '', onClick }) {
|
||||||
|
return (
|
||||||
|
<div className={`flex justify-between ${classes}`} onClick={onClick}>
|
||||||
|
<div className={`flex items-center justify-center gap-1 self-center pt-2 text-xs ${subclasses}`}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,14 +3,14 @@ import TextWrapper from './TextWrapper';
|
||||||
import Content from './Content';
|
import Content from './Content';
|
||||||
|
|
||||||
const Wrapper = React.memo(({ text, generateCursor, isCreatedByUser, searchResult }) => {
|
const Wrapper = React.memo(({ text, generateCursor, isCreatedByUser, searchResult }) => {
|
||||||
if (!isCreatedByUser && searchResult) {
|
if (searchResult) {
|
||||||
return (
|
return (
|
||||||
<Content
|
<Content
|
||||||
content={text}
|
content={text}
|
||||||
generateCursor={generateCursor}
|
isCreatedByUser={isCreatedByUser}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (!isCreatedByUser && !searchResult) {
|
} else if (!isCreatedByUser) {
|
||||||
return (
|
return (
|
||||||
<TextWrapper
|
<TextWrapper
|
||||||
text={text}
|
text={text}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import SubRow from './Content/SubRow';
|
||||||
import Wrapper from './Content/Wrapper';
|
import Wrapper from './Content/Wrapper';
|
||||||
import MultiMessage from './MultiMessage';
|
import MultiMessage from './MultiMessage';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
|
||||||
import HoverButtons from './HoverButtons';
|
import HoverButtons from './HoverButtons';
|
||||||
import SiblingSwitch from './SiblingSwitch';
|
import SiblingSwitch from './SiblingSwitch';
|
||||||
import { setConversation, setLatestMessage } from '~/store/convoSlice';
|
import { setConversation, setLatestMessage } from '~/store/convoSlice';
|
||||||
import { setModel, setCustomModel, setCustomGpt, toggleCursor, setDisabled } from '~/store/submitSlice';
|
import { setModel, setCustomModel, setCustomGpt, setDisabled } from '~/store/submitSlice';
|
||||||
import { setMessages } from '~/store/messageSlice';
|
import { setMessages } from '~/store/messageSlice';
|
||||||
import { fetchById } from '~/utils/fetchers';
|
import { fetchById } from '~/utils/fetchers';
|
||||||
import { getIconOfModel } from '~/utils';
|
import { getIconOfModel } from '~/utils';
|
||||||
|
|
@ -21,17 +22,15 @@ export default function Message({
|
||||||
siblingCount,
|
siblingCount,
|
||||||
setSiblingIdx
|
setSiblingIdx
|
||||||
}) {
|
}) {
|
||||||
const { isSubmitting, model, chatGptLabel, cursor, promptPrefix } = useSelector(
|
const { isSubmitting, model, chatGptLabel, cursor, promptPrefix } = useSelector(state => state.submit);
|
||||||
(state) => state.submit
|
|
||||||
);
|
|
||||||
const [abortScroll, setAbort] = useState(false);
|
const [abortScroll, setAbort] = useState(false);
|
||||||
const { sender, text, searchResult, isCreatedByUser, error, submitting } = message;
|
const { sender, text, searchResult, isCreatedByUser, error, submitting } = message;
|
||||||
const textEditor = useRef(null);
|
const textEditor = useRef(null);
|
||||||
// const { convos } = useSelector((state) => state.convo);
|
|
||||||
const last = !message?.children?.length;
|
const last = !message?.children?.length;
|
||||||
const edit = message.messageId == currentEditId;
|
const edit = message.messageId == currentEditId;
|
||||||
const { ask } = useMessageHandler();
|
const { ask } = useMessageHandler();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
// const currentConvo = convoMap[message.conversationId];
|
||||||
|
|
||||||
// const notUser = !isCreatedByUser; // sender.toLowerCase() !== 'user';
|
// const notUser = !isCreatedByUser; // sender.toLowerCase() !== 'user';
|
||||||
// const blinker = submitting && isSubmitting && last && !isCreatedByUser;
|
// const blinker = submitting && isSubmitting && last && !isCreatedByUser;
|
||||||
|
|
@ -62,7 +61,7 @@ export default function Message({
|
||||||
}
|
}
|
||||||
}, [last, message]);
|
}, [last, message]);
|
||||||
|
|
||||||
const enterEdit = (cancel) => setCurrentEditId(cancel ? -1 : message.messageId);
|
const enterEdit = cancel => setCurrentEditId(cancel ? -1 : message.messageId);
|
||||||
|
|
||||||
const handleWheel = () => {
|
const handleWheel = () => {
|
||||||
if (blinker) {
|
if (blinker) {
|
||||||
|
|
@ -92,11 +91,10 @@ export default function Message({
|
||||||
'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]';
|
'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]';
|
||||||
|
|
||||||
if (message.bg && searchResult) {
|
if (message.bg && searchResult) {
|
||||||
props.className = message.bg + ' cursor-pointer';
|
props.className = message.bg.split('hover')[0];
|
||||||
|
props.titleClass = message.bg.split(props.className)[1] + ' cursor-pointer';
|
||||||
}
|
}
|
||||||
|
|
||||||
// const wrapText = (text) => <TextWrapper text={text} generateCursor={generateCursor}/>;
|
|
||||||
|
|
||||||
const resubmitMessage = () => {
|
const resubmitMessage = () => {
|
||||||
const text = textEditor.current.innerText;
|
const text = textEditor.current.innerText;
|
||||||
|
|
||||||
|
|
@ -135,11 +133,11 @@ export default function Message({
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
onWheel={handleWheel}
|
onWheel={handleWheel}
|
||||||
onClick={clickSearchResult}
|
// 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 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">
|
<div className="relative flex h-[30px] w-[30px] flex-col items-end text-right text-xs md:text-sm">
|
||||||
{typeof icon === 'string' && icon.match(/[^\u0000-\u007F]+/) ? (
|
{typeof icon === 'string' && icon.match(/[^\\x00-\\x7F]+/) ? (
|
||||||
<span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span>
|
<span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span>
|
||||||
) : (
|
) : (
|
||||||
icon
|
icon
|
||||||
|
|
@ -153,6 +151,15 @@ export default function Message({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 whitespace-pre-wrap md:gap-3 lg:w-[calc(100%-115px)]">
|
<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'}
|
||||||
|
subclasses="switch-result pl-2 pb-2"
|
||||||
|
onClick={clickSearchResult}
|
||||||
|
>
|
||||||
|
<strong>{`${message.title} | ${message.sender}`}</strong>
|
||||||
|
</SubRow>
|
||||||
|
)}
|
||||||
<div className="flex flex-grow flex-col gap-3">
|
<div className="flex flex-grow flex-col gap-3">
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="flex flex min-h-[20px] flex-grow flex-col items-start gap-4 gap-2 whitespace-pre-wrap text-red-500">
|
<div className="flex flex min-h-[20px] flex-grow flex-col items-start gap-4 gap-2 whitespace-pre-wrap text-red-500">
|
||||||
|
|
@ -207,15 +214,13 @@ export default function Message({
|
||||||
visible={!error && isCreatedByUser && !edit && !searchResult}
|
visible={!error && isCreatedByUser && !edit && !searchResult}
|
||||||
onClick={() => enterEdit()}
|
onClick={() => enterEdit()}
|
||||||
/>
|
/>
|
||||||
<div className="sibling-switch-container flex justify-between">
|
<SubRow subclasses="switch-container">
|
||||||
<div className="flex items-center justify-center gap-1 self-center pt-2 text-xs">
|
<SiblingSwitch
|
||||||
<SiblingSwitch
|
siblingIdx={siblingIdx}
|
||||||
siblingIdx={siblingIdx}
|
siblingCount={siblingCount}
|
||||||
siblingCount={siblingCount}
|
setSiblingIdx={setSiblingIdx}
|
||||||
setSiblingIdx={setSiblingIdx}
|
/>
|
||||||
/>
|
</SubRow>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
38
client/src/components/Messages/MessageBar.jsx
Normal file
38
client/src/components/Messages/MessageBar.jsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const MessageBar = ({ children, dynamicProps, handleWheel, clickSearchResult }) => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIsVisible(true);
|
||||||
|
observer.unobserve(ref.current);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(ref.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(ref.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...dynamicProps}
|
||||||
|
onWheel={handleWheel}
|
||||||
|
// onClick={clickSearchResult}
|
||||||
|
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{isVisible ? children : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MessageBar;
|
||||||
|
|
@ -9,7 +9,7 @@ import { useSelector } from 'react-redux';
|
||||||
export default function Messages({ messages, messageTree }) {
|
export default function Messages({ messages, messageTree }) {
|
||||||
const [currentEditId, setCurrentEditId] = useState(-1);
|
const [currentEditId, setCurrentEditId] = useState(-1);
|
||||||
const { conversationId } = useSelector((state) => state.convo);
|
const { conversationId } = useSelector((state) => state.convo);
|
||||||
const { model, customModel, chatGptLabel } = useSelector((state) => state.submit);
|
const { model, customModel } = useSelector((state) => state.submit);
|
||||||
const { models } = useSelector((state) => state.models);
|
const { models } = useSelector((state) => state.models);
|
||||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||||
const scrollableRef = useRef(null);
|
const scrollableRef = useRef(null);
|
||||||
|
|
@ -36,7 +36,6 @@ export default function Messages({ messages, messageTree }) {
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
const scrollToBottom = useCallback(throttle(() => {
|
const scrollToBottom = useCallback(throttle(() => {
|
||||||
console.log('scrollToBottom');
|
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
setShowScrollButton(false);
|
setShowScrollButton(false);
|
||||||
}, 750, { leading: true }), [messagesEndRef]);
|
}, 750, { leading: true }), [messagesEndRef]);
|
||||||
|
|
@ -75,7 +74,7 @@ export default function Messages({ messages, messageTree }) {
|
||||||
<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">
|
<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}
|
Model: {modelName} {customModel ? `(${customModel})` : null}
|
||||||
</div>
|
</div>
|
||||||
{(messageTree.length === 0) ? (
|
{(messageTree.length === 0 || !messages) ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { useDispatch } from 'react-redux';
|
||||||
import { setNewConvo, removeAll } from '~/store/convoSlice';
|
import { setNewConvo, removeAll } from '~/store/convoSlice';
|
||||||
import { setMessages } from '~/store/messageSlice';
|
import { setMessages } from '~/store/messageSlice';
|
||||||
import { setSubmission } from '~/store/submitSlice';
|
import { setSubmission } from '~/store/submitSlice';
|
||||||
|
import { Dialog, DialogTrigger } from '../ui/Dialog.tsx';
|
||||||
|
import DialogTemplate from '../ui/DialogTemplate';
|
||||||
|
|
||||||
export default function ClearConvos() {
|
export default function ClearConvos() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
@ -25,12 +27,25 @@ export default function ClearConvos() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
<a
|
<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"
|
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}
|
// onClick={clickHandler}
|
||||||
>
|
>
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
Clear conversations
|
Clear conversations
|
||||||
</a>
|
</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,22 +1,18 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import NavLink from './NavLink';
|
|
||||||
import LogOutIcon from '../svg/LogOutIcon';
|
|
||||||
import SearchBar from './SearchBar';
|
import SearchBar from './SearchBar';
|
||||||
import ClearConvos from './ClearConvos';
|
import ClearConvos from './ClearConvos';
|
||||||
import DarkMode from './DarkMode';
|
import DarkMode from './DarkMode';
|
||||||
import Logout from './Logout';
|
import Logout from './Logout';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
export default function NavLinks({ fetch, onSearchSuccess, clearSearch }) {
|
export default function NavLinks({ fetch, onSearchSuccess, clearSearch }) {
|
||||||
|
const { searchEnabled } = useSelector((state) => state.search);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <SearchBar fetch={fetch} onSuccess={onSearchSuccess} clearSearch={clearSearch}/> */}
|
{ !!searchEnabled && <SearchBar fetch={fetch} onSuccess={onSearchSuccess} clearSearch={clearSearch}/>}
|
||||||
<ClearConvos />
|
|
||||||
<DarkMode />
|
<DarkMode />
|
||||||
|
<ClearConvos />
|
||||||
<Logout />
|
<Logout />
|
||||||
{/* <NavLink
|
|
||||||
svg={LogOutIcon}
|
|
||||||
text="Log out"
|
|
||||||
/> */}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { setNewConvo } from '~/store/convoSlice';
|
import { setNewConvo, refreshConversation } from '~/store/convoSlice';
|
||||||
import { setMessages } from '~/store/messageSlice';
|
import { setMessages } from '~/store/messageSlice';
|
||||||
import { setSubmission } from '~/store/submitSlice';
|
import { setSubmission, setDisabled } from '~/store/submitSlice';
|
||||||
import { setText } from '~/store/textSlice';
|
import { setText } from '~/store/textSlice';
|
||||||
|
import { setInputValue, setQuery } from '~/store/searchSlice';
|
||||||
|
|
||||||
export default function NewChat() {
|
export default function NewChat() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
@ -12,7 +13,11 @@ export default function NewChat() {
|
||||||
dispatch(setText(''));
|
dispatch(setText(''));
|
||||||
dispatch(setMessages([]));
|
dispatch(setMessages([]));
|
||||||
dispatch(setNewConvo());
|
dispatch(setNewConvo());
|
||||||
|
dispatch(refreshConversation());
|
||||||
dispatch(setSubmission({}));
|
dispatch(setSubmission({}));
|
||||||
|
dispatch(setDisabled(false));
|
||||||
|
dispatch(setInputValue(''));
|
||||||
|
dispatch(setQuery(''));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
import { setQuery } from '~/store/searchSlice';
|
import { setInputValue, setQuery } from '~/store/searchSlice';
|
||||||
|
|
||||||
export default function SearchBar({ fetch, clearSearch }) {
|
export default function SearchBar({ fetch, clearSearch }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [inputValue, setInputValue] = useState('');
|
const { inputValue } = useSelector((state) => state.search);
|
||||||
|
// const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
const debouncedChangeHandler = useCallback(
|
const debouncedChangeHandler = useCallback(
|
||||||
debounce((q) => {
|
debounce((q) => {
|
||||||
|
|
@ -28,10 +29,9 @@ export default function SearchBar({ fetch, clearSearch }) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const changeHandler = (e) => {
|
const changeHandler = (e) => {
|
||||||
let q = e.target.value;
|
let q = e.target.value;
|
||||||
setInputValue(q);
|
dispatch(setInputValue(q));
|
||||||
q = q.trim();
|
q = q.trim();
|
||||||
|
|
||||||
if (q === '') {
|
if (q === '') {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export default function Nav({ navVisible, setNavVisible }) {
|
||||||
setPage(res.pageNumber);
|
setPage(res.pageNumber);
|
||||||
setPages(res.pages);
|
setPages(res.pages);
|
||||||
setIsFetching(false);
|
setIsFetching(false);
|
||||||
if (res.messages) {
|
if (res.messages?.length > 0) {
|
||||||
dispatch(setMessages(res.messages));
|
dispatch(setMessages(res.messages));
|
||||||
dispatch(setDisabled(true));
|
dispatch(setDisabled(true));
|
||||||
}
|
}
|
||||||
|
|
@ -54,8 +54,10 @@ export default function Nav({ navVisible, setNavVisible }) {
|
||||||
const clearSearch = () => {
|
const clearSearch = () => {
|
||||||
setPage(1);
|
setPage(1);
|
||||||
dispatch(refreshConversation());
|
dispatch(refreshConversation());
|
||||||
dispatch(setNewConvo());
|
if (!conversationId) {
|
||||||
dispatch(setMessages([]));
|
dispatch(setNewConvo());
|
||||||
|
dispatch(setMessages([]));
|
||||||
|
}
|
||||||
dispatch(setDisabled(false));
|
dispatch(setDisabled(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
57
client/src/components/ui/DialogTemplate.jsx
Normal file
57
client/src/components/ui/DialogTemplate.jsx
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle
|
||||||
|
} from './Dialog.tsx';
|
||||||
|
|
||||||
|
export default function DialogTemplate({ title, description, main, buttons, selection }) {
|
||||||
|
const { selectHandler, selectClasses, selectText } = selection;
|
||||||
|
|
||||||
|
const defaultSelect = "bg-gray-900 text-white transition-colors hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900"
|
||||||
|
return (
|
||||||
|
<DialogContent className="shadow-2xl dark:bg-gray-800">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-gray-800 dark:text-white">{title}</DialogTitle>
|
||||||
|
<DialogDescription className="text-gray-600 dark:text-gray-300">
|
||||||
|
{description}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{/* <div className="grid gap-4 py-4">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4"> //input template
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label
|
||||||
|
htmlFor="promptPrefix"
|
||||||
|
className="text-right"
|
||||||
|
>
|
||||||
|
Prompt Prefix
|
||||||
|
</Label>
|
||||||
|
<TextareaAutosize
|
||||||
|
id="promptPrefix"
|
||||||
|
value={promptPrefix}
|
||||||
|
onChange={(e) => setPromptPrefix(e.target.value)}
|
||||||
|
placeholder="Set custom instructions. Defaults to: 'You are ChatGPT, a large language model trained by OpenAI.'"
|
||||||
|
className="col-span-3 flex h-20 w-full resize-none rounded-md border border-gray-300 bg-transparent py-2 px-3 text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-none dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-none dark:focus:border-transparent dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
{main ? main : null}
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose className="dark:hover:gray-400 border-gray-700">Cancel</DialogClose>
|
||||||
|
{ buttons ? buttons : null}
|
||||||
|
<DialogClose
|
||||||
|
onClick={selectHandler}
|
||||||
|
className={`${selectClasses || defaultSelect} inline-flex h-10 items-center justify-center rounded-md border-none py-2 px-4 text-sm font-semibold`}
|
||||||
|
>
|
||||||
|
{selectText}
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -22,11 +22,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.sibling-switch-container {
|
.switch-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.switch-result {
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
/* .sibling-switch {
|
/* .sibling-switch {
|
||||||
left: 114px;
|
left: 114px;
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,15 @@ const initialState = {
|
||||||
refreshConvoHint: 0,
|
refreshConvoHint: 0,
|
||||||
search: false,
|
search: false,
|
||||||
latestMessage: null,
|
latestMessage: null,
|
||||||
convos: []
|
convos: [],
|
||||||
|
convoMap: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentSlice = createSlice({
|
const currentSlice = createSlice({
|
||||||
name: 'convo',
|
name: 'convo',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
refreshConversation: (state, action) => {
|
refreshConversation: (state) => {
|
||||||
state.refreshConvoHint = state.refreshConvoHint + 1;
|
state.refreshConvoHint = state.refreshConvoHint + 1;
|
||||||
},
|
},
|
||||||
setConversation: (state, action) => {
|
setConversation: (state, action) => {
|
||||||
|
|
@ -69,6 +70,13 @@ const currentSlice = createSlice({
|
||||||
} else {
|
} else {
|
||||||
state.convos = convos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
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) => {
|
setPages: (state, action) => {
|
||||||
state.pages = action.payload;
|
state.pages = action.payload;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
searchEnabled: false,
|
||||||
search: false,
|
search: false,
|
||||||
query: '',
|
query: '',
|
||||||
|
inputValue: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentSlice = createSlice({
|
const currentSlice = createSlice({
|
||||||
name: 'search',
|
name: 'search',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
|
setInputValue: (state, action) => {
|
||||||
|
state.inputValue = action.payload;
|
||||||
|
},
|
||||||
setSearchState: (state, action) => {
|
setSearchState: (state, action) => {
|
||||||
state.search = action.payload;
|
state.searchEnabled = action.payload;
|
||||||
},
|
},
|
||||||
setQuery: (state, action) => {
|
setQuery: (state, action) => {
|
||||||
const q = action.payload;
|
const q = action.payload;
|
||||||
|
|
@ -25,6 +30,6 @@ const currentSlice = createSlice({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setSearchState, setQuery } = currentSlice.actions;
|
export const { setInputValue, setSearchState, setQuery } = currentSlice.actions;
|
||||||
|
|
||||||
export default currentSlice.reducer;
|
export default currentSlice.reducer;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
const even = 'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800 hover:bg-gray-100/25 hover:text-gray-700 dark:hover:bg-[#32343e] dark:hover:text-gray-200';
|
const even =
|
||||||
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';
|
'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800 hover:bg-gray-100/25 hover:text-gray-700 dark:hover:bg-gray-900 dark:hover:text-gray-200';
|
||||||
|
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) {
|
export default function buildTree(messages, groupAll = false) {
|
||||||
let messageMap = {};
|
let messageMap = {};
|
||||||
|
|
@ -7,7 +9,7 @@ export default function buildTree(messages, groupAll = false) {
|
||||||
|
|
||||||
if (!groupAll) {
|
if (!groupAll) {
|
||||||
// Traverse the messages array and store each element in messageMap.
|
// Traverse the messages array and store each element in messageMap.
|
||||||
messages.forEach((message) => {
|
messages.forEach(message => {
|
||||||
messageMap[message.messageId] = { ...message, children: [] };
|
messageMap[message.messageId] = { ...message, children: [] };
|
||||||
|
|
||||||
const parentMessage = messageMap[message.parentMessageId];
|
const parentMessage = messageMap[message.parentMessageId];
|
||||||
|
|
@ -30,4 +32,21 @@ export default function buildTree(messages, groupAll = false) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return rootMessages;
|
return rootMessages;
|
||||||
|
|
||||||
|
// Group all messages by conversation, doesn't look great
|
||||||
|
// Traverse the messages array and store each element in messageMap.
|
||||||
|
// rootMessages = {};
|
||||||
|
// let parents = 0;
|
||||||
|
// messages.forEach(message => {
|
||||||
|
// if (message.conversationId in messageMap) {
|
||||||
|
// messageMap[message.conversationId].children.push(message);
|
||||||
|
// } else {
|
||||||
|
// messageMap[message.conversationId] = { ...message, bg: parents % 2 === 0 ? even : odd, children: [] };
|
||||||
|
// rootMessages.push(messageMap[message.conversationId]);
|
||||||
|
// parents++;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // return Object.values(rootMessages);
|
||||||
|
// return rootMessages;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
client/src/utils/userAuth.js
Normal file
23
client/src/utils/userAuth.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default async function fetchData() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/me', {
|
||||||
|
timeout: 1000,
|
||||||
|
withCredentials: true
|
||||||
|
});
|
||||||
|
const user = response.data;
|
||||||
|
if (user) {
|
||||||
|
// dispatch(setUser(user));
|
||||||
|
// callback(user);
|
||||||
|
return user;
|
||||||
|
} else {
|
||||||
|
console.log('Not login!');
|
||||||
|
window.location.href = '/auth/login';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
console.log('Not login!');
|
||||||
|
window.location.href = '/auth/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,3 +40,11 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./data-node:/data/db
|
- ./data-node:/data/db
|
||||||
command: mongod --noauth
|
command: mongod --noauth
|
||||||
|
meilisearch:
|
||||||
|
image: getmeili/meilisearch:v1.0
|
||||||
|
ports:
|
||||||
|
- 7700:7700
|
||||||
|
env_file:
|
||||||
|
- ./api/.env
|
||||||
|
volumes:
|
||||||
|
- ./meili_data:/meili_data
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ services:
|
||||||
image: getmeili/meilisearch:v1.0
|
image: getmeili/meilisearch:v1.0
|
||||||
ports:
|
ports:
|
||||||
- 7700:7700
|
- 7700:7700
|
||||||
environment:
|
env_file:
|
||||||
- MEILI_MASTER_KEY=MASTER_KEY
|
- ./api/.env
|
||||||
volumes:
|
volumes:
|
||||||
- ./meili_data:/meili_data
|
- ./meili_data:/meili_data
|
||||||
Loading…
Add table
Add a link
Reference in a new issue