mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 01:10:14 +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,13 +18,8 @@ const proxyEnvToAxiosProxy = (proxyString) => {
|
||||||
|
|
||||||
const titleConvo = async ({ model, text, response }) => {
|
const titleConvo = async ({ model, text, response }) => {
|
||||||
let title = 'New Chat';
|
let title = 'New Chat';
|
||||||
try {
|
|
||||||
const configuration = new Configuration({
|
const request = {
|
||||||
apiKey: process.env.OPENAI_KEY
|
|
||||||
});
|
|
||||||
const openai = new OpenAIApi(configuration);
|
|
||||||
const completion = await openai.createChatCompletion(
|
|
||||||
{
|
|
||||||
model: 'gpt-3.5-turbo',
|
model: 'gpt-3.5-turbo',
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
|
|
@ -41,10 +36,19 @@ const titleConvo = async ({ model, text, response }) => {
|
||||||
],
|
],
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
presence_penalty: 0,
|
presence_penalty: 0,
|
||||||
frequency_penalty: 0,
|
frequency_penalty: 0
|
||||||
},
|
};
|
||||||
{ proxy: proxyEnvToAxiosProxy(process.env.PROXY || null) }
|
|
||||||
);
|
// console.log('REQUEST', request);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configuration = new Configuration({
|
||||||
|
apiKey: process.env.OPENAI_KEY
|
||||||
|
});
|
||||||
|
const openai = new OpenAIApi(configuration);
|
||||||
|
const completion = await openai.createChatCompletion(request, {
|
||||||
|
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 host = process.env.HOST || 'localhost'
|
|
||||||
const userSystemEnabled = process.env.ENABLE_USER_SYSTEM || false
|
|
||||||
const projectPath = path.join(__dirname, '..', '..', 'client');
|
|
||||||
connectDb().then(() => {
|
|
||||||
console.log('Connected to MongoDB');
|
|
||||||
migrateDb();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(cors());
|
const port = process.env.PORT || 3080;
|
||||||
app.use(express.json());
|
const host = process.env.HOST || 'localhost';
|
||||||
app.use(express.static(path.join(projectPath, 'public')));
|
const userSystemEnabled = process.env.ENABLE_USER_SYSTEM || false;
|
||||||
app.set('trust proxy', 1) // trust first proxy
|
const projectPath = path.join(__dirname, '..', '..', 'client');
|
||||||
app.use(session({
|
|
||||||
|
(async () => {
|
||||||
|
await connectDb();
|
||||||
|
console.log('Connected to MongoDB');
|
||||||
|
await migrateDb();
|
||||||
|
await indexSync();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(errorController);
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.static(path.join(projectPath, 'public')));
|
||||||
|
app.set('trust proxy', 1); // trust first proxy
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
secret: 'chatgpt-clone-random-secrect',
|
secret: 'chatgpt-clone-random-secrect',
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: true,
|
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(null));
|
|
||||||
} else {
|
} else {
|
||||||
res.send(JSON.stringify({username: 'anonymous_user', display: 'Anonymous User'}));
|
res.send(JSON.stringify({ username: 'anonymous_user', display: 'Anonymous User' }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use('/api/search', routes.authenticatedOr401, routes.search);
|
app.use('/api/search', routes.authenticatedOr401, routes.search);
|
||||||
app.use('/api/ask', routes.authenticatedOr401, routes.ask);
|
app.use('/api/ask', routes.authenticatedOr401, routes.ask);
|
||||||
app.use('/api/messages', routes.authenticatedOr401, routes.messages);
|
app.use('/api/messages', routes.authenticatedOr401, routes.messages);
|
||||||
app.use('/api/convos', routes.authenticatedOr401, routes.convos);
|
app.use('/api/convos', routes.authenticatedOr401, routes.convos);
|
||||||
app.use('/api/customGpts', routes.authenticatedOr401, routes.customGpts);
|
app.use('/api/customGpts', routes.authenticatedOr401, routes.customGpts);
|
||||||
app.use('/api/prompts', routes.authenticatedOr401, routes.prompts);
|
app.use('/api/prompts', routes.authenticatedOr401, routes.prompts);
|
||||||
app.use('/auth', routes.auth);
|
app.use('/auth', routes.auth);
|
||||||
|
|
||||||
|
app.get('/api/models', function (req, res) {
|
||||||
app.get('/api/models', function (req, res) {
|
|
||||||
const hasOpenAI = !!process.env.OPENAI_KEY;
|
const hasOpenAI = !!process.env.OPENAI_KEY;
|
||||||
const hasChatGpt = !!process.env.CHATGPT_TOKEN;
|
const hasChatGpt = !!process.env.CHATGPT_TOKEN;
|
||||||
const hasBing = !!process.env.BING_TOKEN;
|
const hasBing = !!process.env.BING_TOKEN;
|
||||||
|
|
||||||
res.send(JSON.stringify({ hasOpenAI, hasChatGpt, hasBing }));
|
res.send(JSON.stringify({ hasOpenAI, hasChatGpt, hasBing }));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(port, host, () => {
|
app.listen(port, host, () => {
|
||||||
if (host=='0.0.0.0')
|
if (host == '0.0.0.0')
|
||||||
console.log(`Server listening on all interface at port ${port}. Use http://localhost:${port} to access it`);
|
console.log(
|
||||||
|
`Server listening on all interface at port ${port}. Use http://localhost:${port} to access it`
|
||||||
|
);
|
||||||
else
|
else
|
||||||
console.log(`Server listening at http://${host=='0.0.0.0'?'localhost':host}:${port}`);
|
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 {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -19,9 +19,9 @@ 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)
|
||||||
|
|
@ -31,13 +31,10 @@ router.post('/gen_title', async (req, res) => {
|
||||||
response: JSON.stringify(secondMessage?.text || '')
|
response: JSON.stringify(secondMessage?.text || '')
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveConvo(
|
await saveConvo(req?.session?.user?.username, {
|
||||||
req?.session?.user?.username,
|
|
||||||
{
|
|
||||||
conversationId,
|
conversationId,
|
||||||
title
|
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);
|
|
||||||
|
const activeMessages = [];
|
||||||
|
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.cache;
|
||||||
result.messages = messages.filter((message) => !result.filter.has(message.conversationId));
|
}
|
||||||
|
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,7 +6,9 @@ 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 = () => {
|
||||||
|
|
@ -15,33 +17,23 @@ const App = () => {
|
||||||
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,15 +4,12 @@ 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 }) => {
|
||||||
return (
|
const rehypePlugins = [
|
||||||
<>
|
|
||||||
<ReactMarkdown
|
|
||||||
remarkPlugins={[remarkGfm, [remarkMath, { singleDollarTextMath: false }]]}
|
|
||||||
rehypePlugins={[
|
|
||||||
[rehypeKatex, { output: 'mathml' }],
|
[rehypeKatex, { output: 'mathml' }],
|
||||||
[
|
[
|
||||||
rehypeHighlight,
|
rehypeHighlight,
|
||||||
|
|
@ -21,12 +18,19 @@ const Content = React.memo(({ content }) => {
|
||||||
ignoreMissing: true,
|
ignoreMissing: true,
|
||||||
subset: langSubset
|
subset: langSubset
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
[rehypeRaw],
|
||||||
]
|
]
|
||||||
]}
|
return (
|
||||||
|
<>
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm, [remarkMath, { singleDollarTextMath: false }]]}
|
||||||
|
rehypePlugins={isCreatedByUser ? rehypePlugins.slice(-1) : rehypePlugins}
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
</div>
|
</SubRow>
|
||||||
</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());
|
||||||
|
if (!conversationId) {
|
||||||
dispatch(setNewConvo());
|
dispatch(setNewConvo());
|
||||||
dispatch(setMessages([]));
|
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