mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
commit
c6fb3018e7
40 changed files with 990 additions and 364 deletions
|
|
@ -18,12 +18,47 @@ MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone"
|
|||
# API key configuration.
|
||||
# Leave blank if you don't want them.
|
||||
OPENAI_KEY=
|
||||
CHATGPT_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.
|
||||
# this is not a ready to use user system.
|
||||
# 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();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
const set = new Set(["gpt-4", "text-davinci-002-render", "text-davinci-002-render-paid", "text-davinci-002-render-sha"]);
|
||||
|
||||
const clientOptions = {
|
||||
// Warning: This will expose your access token to a third party. Consider the risks before using this.
|
||||
|
|
@ -10,6 +11,12 @@ const clientOptions = {
|
|||
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 { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const _ = require('lodash');
|
||||
|
||||
const proxyEnvToAxiosProxy = (proxyString) => {
|
||||
const proxyEnvToAxiosProxy = proxyString => {
|
||||
if (!proxyString) return null;
|
||||
|
||||
const regex = /^([^:]+):\/\/(?:([^:@]*):?([^:@]*)@)?([^:]+)(?::(\d+))?/;
|
||||
|
|
@ -18,13 +18,8 @@ const proxyEnvToAxiosProxy = (proxyString) => {
|
|||
|
||||
const titleConvo = async ({ model, text, response }) => {
|
||||
let title = 'New Chat';
|
||||
try {
|
||||
const configuration = new Configuration({
|
||||
apiKey: process.env.OPENAI_KEY
|
||||
});
|
||||
const openai = new OpenAIApi(configuration);
|
||||
const completion = await openai.createChatCompletion(
|
||||
{
|
||||
|
||||
const request = {
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: [
|
||||
{
|
||||
|
|
@ -41,10 +36,19 @@ const titleConvo = async ({ model, text, response }) => {
|
|||
],
|
||||
temperature: 0,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0,
|
||||
},
|
||||
{ proxy: proxyEnvToAxiosProxy(process.env.PROXY || null) }
|
||||
);
|
||||
frequency_penalty: 0
|
||||
};
|
||||
|
||||
// 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
|
||||
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
|
||||
ConfigSchema.methods.incrementCount = function () {
|
||||
configSchema.methods.incrementCount = function () {
|
||||
this.startupCounts += 1;
|
||||
};
|
||||
|
||||
// Static methods
|
||||
ConfigSchema.statics.findByTag = async function (tag) {
|
||||
configSchema.statics.findByTag = async function (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 });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,70 +1,8 @@
|
|||
const mongoose = require('mongoose');
|
||||
const mongoMeili = require('../lib/db/mongoMeili');
|
||||
// const { Conversation } = require('./plugins');
|
||||
const Conversation = require('./schema/convoSchema');
|
||||
const { cleanUpPrimaryKeyValue } = require('../lib/utils/misc');
|
||||
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) => {
|
||||
try {
|
||||
return await Conversation.findOne({ user, conversationId }).exec();
|
||||
|
|
@ -143,15 +81,16 @@ module.exports = {
|
|||
}
|
||||
|
||||
const cache = {};
|
||||
const convoMap = {};
|
||||
const promises = [];
|
||||
// will handle a syncing solution soon
|
||||
const deletedConvoIds = [];
|
||||
|
||||
convoIds.forEach((convo) =>
|
||||
convoIds.forEach(convo =>
|
||||
promises.push(
|
||||
Conversation.findOne({
|
||||
user,
|
||||
conversationId: convo.conversationId
|
||||
conversationId: cleanUpPrimaryKeyValue(convo.conversationId)
|
||||
}).exec()
|
||||
)
|
||||
);
|
||||
|
|
@ -166,6 +105,7 @@ module.exports = {
|
|||
cache[page] = [];
|
||||
}
|
||||
cache[page].push(convo);
|
||||
convoMap[convo.conversationId] = convo;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
|
@ -183,6 +123,7 @@ module.exports = {
|
|||
pageSize,
|
||||
// will handle a syncing solution soon
|
||||
filter: new Set(deletedConvoIds),
|
||||
convoMap
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
|
@ -208,6 +149,7 @@ module.exports = {
|
|||
},
|
||||
deleteConvos: async (user, filter) => {
|
||||
let deleteCount = await Conversation.deleteMany({ ...filter, user }).exec();
|
||||
console.log('deleteCount', deleteCount);
|
||||
deleteCount.messages = await deleteMessages(filter);
|
||||
return deleteCount;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +1,5 @@
|
|||
const mongoose = require('mongoose');
|
||||
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);
|
||||
|
||||
const Message = require('./schema/messageSchema');
|
||||
module.exports = {
|
||||
messageSchema,
|
||||
Message,
|
||||
saveMessage: async ({ messageId, conversationId, parentMessageId, sender, text, isCreatedByUser=false, error }) => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { MeiliSearch } = require('meilisearch');
|
||||
const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc');
|
||||
const _ = require('lodash');
|
||||
|
||||
const validateOptions = function (options) {
|
||||
|
|
@ -54,7 +56,9 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
|
|||
if (populate) {
|
||||
// Find objects into mongodb matching `objectID` from Meili search
|
||||
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(
|
||||
query,
|
||||
_.reduce(
|
||||
|
|
@ -86,13 +90,18 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
|
|||
// Push new document to Meili
|
||||
async addObjectToMeili() {
|
||||
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 {
|
||||
// console.log('Adding document to Meili', object);
|
||||
await index.addDocuments([object]);
|
||||
} catch (error) {
|
||||
console.log('Error adding document to Meili');
|
||||
console.error(error);
|
||||
// console.log('Error adding document to Meili');
|
||||
// console.error(error);
|
||||
}
|
||||
|
||||
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
|
||||
async updateObjectToMeili() {
|
||||
const object = pick(this.toJSON(), attributesToIndex);
|
||||
const object = _.pick(this.toJSON(), attributesToIndex);
|
||||
await index.updateDocuments([object]);
|
||||
}
|
||||
|
||||
|
|
@ -184,6 +193,18 @@ module.exports = function mongoMeili(schema, options) {
|
|||
schema.post('remove', function (doc) {
|
||||
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) {
|
||||
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 session = require('express-session')
|
||||
const session = require('express-session');
|
||||
const connectDb = require('../lib/db/connectDb');
|
||||
const migrateDb = require('../lib/db/migrateDb');
|
||||
const indexSync = require('../lib/db/indexSync');
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
const routes = require('./routes');
|
||||
const app = express();
|
||||
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();
|
||||
});
|
||||
const errorController = require('./controllers/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({
|
||||
const port = process.env.PORT || 3080;
|
||||
const host = process.env.HOST || 'localhost';
|
||||
const userSystemEnabled = process.env.ENABLE_USER_SYSTEM || false;
|
||||
const projectPath = path.join(__dirname, '..', '..', 'client');
|
||||
|
||||
(async () => {
|
||||
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',
|
||||
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 */
|
||||
// app.get('/', routes.authenticatedOrRedirect, function (req, res) {
|
||||
// console.log(path.join(projectPath, 'public', 'index.html'));
|
||||
// res.sendFile(path.join(projectPath, 'public', 'index.html'));
|
||||
// });
|
||||
// app.get('/', routes.authenticatedOrRedirect, function (req, res) {
|
||||
// console.log(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) {
|
||||
const user = req?.session?.user
|
||||
const user = req?.session?.user;
|
||||
|
||||
if (user)
|
||||
res.send(JSON.stringify({username: user?.username, display: user?.display}));
|
||||
else
|
||||
res.send(JSON.stringify(null));
|
||||
if (user) res.send(JSON.stringify({ username: user?.username, display: user?.display }));
|
||||
else res.send(JSON.stringify(null));
|
||||
} else {
|
||||
res.send(JSON.stringify({username: 'anonymous_user', display: 'Anonymous User'}));
|
||||
res.send(JSON.stringify({ username: 'anonymous_user', display: 'Anonymous User' }));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/api/search', routes.authenticatedOr401, routes.search);
|
||||
app.use('/api/ask', routes.authenticatedOr401, routes.ask);
|
||||
app.use('/api/messages', routes.authenticatedOr401, routes.messages);
|
||||
app.use('/api/convos', routes.authenticatedOr401, routes.convos);
|
||||
app.use('/api/customGpts', routes.authenticatedOr401, routes.customGpts);
|
||||
app.use('/api/prompts', routes.authenticatedOr401, routes.prompts);
|
||||
app.use('/auth', routes.auth);
|
||||
app.use('/api/search', routes.authenticatedOr401, routes.search);
|
||||
app.use('/api/ask', routes.authenticatedOr401, routes.ask);
|
||||
app.use('/api/messages', routes.authenticatedOr401, routes.messages);
|
||||
app.use('/api/convos', routes.authenticatedOr401, routes.convos);
|
||||
app.use('/api/customGpts', routes.authenticatedOr401, routes.customGpts);
|
||||
app.use('/api/prompts', routes.authenticatedOr401, routes.prompts);
|
||||
app.use('/auth', routes.auth);
|
||||
|
||||
|
||||
app.get('/api/models', function (req, res) {
|
||||
app.get('/api/models', function (req, res) {
|
||||
const hasOpenAI = !!process.env.OPENAI_KEY;
|
||||
const hasChatGpt = !!process.env.CHATGPT_TOKEN;
|
||||
const hasBing = !!process.env.BING_TOKEN;
|
||||
|
||||
res.send(JSON.stringify({ hasOpenAI, hasChatGpt, hasBing }));
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(port, host, () => {
|
||||
if (host=='0.0.0.0')
|
||||
console.log(`Server listening on all interface at port ${port}. Use http://localhost:${port} to access it`);
|
||||
app.listen(port, host, () => {
|
||||
if (host == '0.0.0.0')
|
||||
console.log(
|
||||
`Server listening on all interface at port ${port}. Use http://localhost:${port} to access it`
|
||||
);
|
||||
else
|
||||
console.log(`Server listening at http://${host=='0.0.0.0'?'localhost':host}:${port}`);
|
||||
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) => {
|
||||
const { conversationId } = req.body.arg;
|
||||
|
||||
const convo = await getConvo(req?.session?.user?.username, conversationId)
|
||||
const firstMessage = (await getMessages({ conversationId }))[0]
|
||||
const secondMessage = (await getMessages({ conversationId }))[1]
|
||||
const convo = await getConvo(req?.session?.user?.username, conversationId);
|
||||
const firstMessage = (await getMessages({ conversationId }))[0];
|
||||
const secondMessage = (await getMessages({ conversationId }))[1];
|
||||
|
||||
const title = convo.jailbreakConversationId
|
||||
? await getConvoTitle(req?.session?.user?.username, conversationId)
|
||||
|
|
@ -31,13 +31,10 @@ router.post('/gen_title', async (req, res) => {
|
|||
response: JSON.stringify(secondMessage?.text || '')
|
||||
});
|
||||
|
||||
await saveConvo(
|
||||
req?.session?.user?.username,
|
||||
{
|
||||
await saveConvo(req?.session?.user?.username, {
|
||||
conversationId,
|
||||
title
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
res.status(200).send(title);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { MeiliSearch } = require('meilisearch');
|
||||
const { Message } = require('../../models/Message');
|
||||
const { Conversation, getConvosQueried } = require('../../models/Conversation');
|
||||
const { reduceMessages, reduceHits } = require('../../lib/utils/reduceHits');
|
||||
// const { MeiliSearch } = require('meilisearch');
|
||||
const { reduceHits } = require('../../lib/utils/reduceHits');
|
||||
const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc');
|
||||
const cache = new Map();
|
||||
|
||||
router.get('/sync', async function (req, res) {
|
||||
|
|
@ -22,8 +23,8 @@ router.get('/', async function (req, res) {
|
|||
if (cache.has(key)) {
|
||||
console.log('cache hit', key);
|
||||
const cached = cache.get(key);
|
||||
const { pages, pageSize } = cached;
|
||||
res.status(200).send({ conversations: cached[pageNumber], pages, pageNumber, pageSize });
|
||||
const { pages, pageSize, messages } = cached;
|
||||
res.status(200).send({ conversations: cached[pageNumber], pages, pageNumber, pageSize, messages });
|
||||
return;
|
||||
} else {
|
||||
cache.clear();
|
||||
|
|
@ -49,11 +50,29 @@ router.get('/', async function (req, res) {
|
|||
};
|
||||
});
|
||||
const titles = (await Conversation.meiliSearch(q)).hits;
|
||||
console.log('message hits:', messages.length, 'convo hits:', titles.length);
|
||||
const sortedHits = reduceHits(messages, titles);
|
||||
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;
|
||||
result.messages = messages.filter((message) => !result.filter.has(message.conversationId));
|
||||
}
|
||||
delete result.convoMap;
|
||||
// for debugging
|
||||
// console.log(result, messages.length);
|
||||
res.status(200).send(result);
|
||||
} catch (error) {
|
||||
|
|
@ -78,4 +97,22 @@ router.get('/test', async function (req, res) {
|
|||
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;
|
||||
|
|
|
|||
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",
|
||||
"rehype-highlight": "^6.0.0",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"rehype-raw": "^6.1.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"remark-supersub": "^1.0.0",
|
||||
"swr": "^2.0.3",
|
||||
"tailwind-merge": "^1.9.1",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
|
|
@ -3185,6 +3187,11 @@
|
|||
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
|
||||
"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": {
|
||||
"version": "15.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||
|
|
@ -6812,6 +6819,45 @@
|
|||
"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": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz",
|
||||
|
|
@ -6926,6 +6972,15 @@
|
|||
"integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
|
||||
"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": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
|
||||
|
|
@ -10916,6 +10971,20 @@
|
|||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz",
|
||||
|
|
@ -10975,6 +11044,14 @@
|
|||
"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": {
|
||||
"version": "2.0.2",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "15.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||
|
|
@ -18033,6 +18115,37 @@
|
|||
"@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": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz",
|
||||
|
|
@ -18134,6 +18247,11 @@
|
|||
"integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
|
||||
"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": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
|
||||
|
|
@ -20727,6 +20845,16 @@
|
|||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz",
|
||||
|
|
@ -20770,6 +20898,14 @@
|
|||
"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": {
|
||||
"version": "2.0.2",
|
||||
"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",
|
||||
"rehype-highlight": "^6.0.0",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"rehype-raw": "^6.1.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"remark-supersub": "^1.0.0",
|
||||
"swr": "^2.0.3",
|
||||
"tailwind-merge": "^1.9.1",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ import Nav from './components/Nav';
|
|||
import MobileNav from './components/Nav/MobileNav';
|
||||
import useDocumentTitle from '~/hooks/useDocumentTitle';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import userAuth from './utils/userAuth';
|
||||
import { setUser } from './store/userReducer';
|
||||
import { setSearchState } from './store/searchSlice';
|
||||
import axios from 'axios';
|
||||
|
||||
const App = () => {
|
||||
|
|
@ -15,33 +17,23 @@ const App = () => {
|
|||
const { messages, messageTree } = useSelector((state) => state.messages);
|
||||
const { user } = useSelector((state) => state.user);
|
||||
const { title } = useSelector((state) => state.convo);
|
||||
const [ navVisible, setNavVisible ]= useState(false)
|
||||
const [navVisible, setNavVisible] = useState(false);
|
||||
useDocumentTitle(title);
|
||||
|
||||
useEffect(async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/me', {
|
||||
timeout: 1000,
|
||||
withCredentials: true
|
||||
});
|
||||
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';
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
axios.get('/api/search/enable').then((res) => { console.log(res.data); dispatch(setSearchState(res.data))});
|
||||
userAuth()
|
||||
.then((user) => dispatch(setUser(user)))
|
||||
.catch((err) => console.log(err));
|
||||
}, []);
|
||||
|
||||
if (user)
|
||||
return (
|
||||
<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="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} />
|
||||
|
|
@ -58,12 +50,7 @@ const App = () => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
|
||||
</div>
|
||||
)
|
||||
else return <div className="flex h-screen"></div>;
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -247,7 +247,6 @@ export default function TextChat({ messages }) {
|
|||
} else {
|
||||
let text = data.text || data.response;
|
||||
if (data.initial) {
|
||||
console.log(data);
|
||||
dispatch(toggleCursor());
|
||||
}
|
||||
if (data.message) {
|
||||
|
|
@ -350,7 +349,7 @@ export default function TextChat({ messages }) {
|
|||
const isSearchView = messages?.[0]?.searchResult === true;
|
||||
const getPlaceholderText = () => {
|
||||
if (isSearchView) {
|
||||
return 'Click a message to open its conversation.'
|
||||
return 'Click a message title to open its conversation.'
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
|
|
|
|||
|
|
@ -4,15 +4,12 @@ import rehypeKatex from 'rehype-katex';
|
|||
import rehypeHighlight from 'rehype-highlight';
|
||||
import remarkMath from 'remark-math';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import CodeBlock from './CodeBlock';
|
||||
import { langSubset } from '~/utils/languages';
|
||||
|
||||
const Content = React.memo(({ content }) => {
|
||||
return (
|
||||
<>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, [remarkMath, { singleDollarTextMath: false }]]}
|
||||
rehypePlugins={[
|
||||
const Content = React.memo(({ content, isCreatedByUser = false }) => {
|
||||
const rehypePlugins = [
|
||||
[rehypeKatex, { output: 'mathml' }],
|
||||
[
|
||||
rehypeHighlight,
|
||||
|
|
@ -21,12 +18,19 @@ const Content = React.memo(({ content }) => {
|
|||
ignoreMissing: true,
|
||||
subset: langSubset
|
||||
}
|
||||
],
|
||||
[rehypeRaw],
|
||||
]
|
||||
]}
|
||||
return (
|
||||
<>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, [remarkMath, { singleDollarTextMath: false }]]}
|
||||
rehypePlugins={isCreatedByUser ? rehypePlugins.slice(-1) : rehypePlugins}
|
||||
linkTarget="_new"
|
||||
components={{
|
||||
code,
|
||||
p,
|
||||
// em,
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
|
|
@ -53,33 +57,33 @@ const code = 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 }) => {
|
||||
if (node.type === 'text' && node.value === '█') {
|
||||
return <span className="result-streaming">{node.value}</span>;
|
||||
}
|
||||
// const blinker = ({ node }) => {
|
||||
// if (node.type === 'text' && node.value === '█') {
|
||||
// return <span className="result-streaming">{node.value}</span>;
|
||||
// }
|
||||
|
||||
return null;
|
||||
};
|
||||
// return null;
|
||||
// };
|
||||
|
||||
const em = React.memo(({ node, ...props }) => {
|
||||
if (
|
||||
props.children[0] &&
|
||||
typeof props.children[0] === 'string' &&
|
||||
props.children[0].startsWith('^')
|
||||
) {
|
||||
return <sup>{props.children[0].substring(1)}</sup>;
|
||||
}
|
||||
if (
|
||||
props.children[0] &&
|
||||
typeof props.children[0] === 'string' &&
|
||||
props.children[0].startsWith('~')
|
||||
) {
|
||||
return <sub>{props.children[0].substring(1)}</sub>;
|
||||
}
|
||||
return <i {...props} />;
|
||||
});
|
||||
// const em = React.memo(({ node, ...props }) => {
|
||||
// if (
|
||||
// props.children[0] &&
|
||||
// typeof props.children[0] === 'string' &&
|
||||
// props.children[0].startsWith('^')
|
||||
// ) {
|
||||
// return <sup>{props.children[0].substring(1)}</sup>;
|
||||
// }
|
||||
// if (
|
||||
// props.children[0] &&
|
||||
// typeof props.children[0] === 'string' &&
|
||||
// props.children[0].startsWith('~')
|
||||
// ) {
|
||||
// return <sub>{props.children[0].substring(1)}</sub>;
|
||||
// }
|
||||
// return <i {...props} />;
|
||||
// });
|
||||
|
||||
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';
|
||||
|
||||
const Wrapper = React.memo(({ text, generateCursor, isCreatedByUser, searchResult }) => {
|
||||
if (!isCreatedByUser && searchResult) {
|
||||
if (searchResult) {
|
||||
return (
|
||||
<Content
|
||||
content={text}
|
||||
generateCursor={generateCursor}
|
||||
isCreatedByUser={isCreatedByUser}
|
||||
/>
|
||||
);
|
||||
} else if (!isCreatedByUser && !searchResult) {
|
||||
} else if (!isCreatedByUser) {
|
||||
return (
|
||||
<TextWrapper
|
||||
text={text}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
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 MultiMessage from './MultiMessage';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import HoverButtons from './HoverButtons';
|
||||
import SiblingSwitch from './SiblingSwitch';
|
||||
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 { fetchById } from '~/utils/fetchers';
|
||||
import { getIconOfModel } from '~/utils';
|
||||
|
|
@ -21,17 +22,15 @@ export default function Message({
|
|||
siblingCount,
|
||||
setSiblingIdx
|
||||
}) {
|
||||
const { isSubmitting, model, chatGptLabel, cursor, promptPrefix } = useSelector(
|
||||
(state) => state.submit
|
||||
);
|
||||
const { isSubmitting, model, chatGptLabel, cursor, promptPrefix } = useSelector(state => state.submit);
|
||||
const [abortScroll, setAbort] = useState(false);
|
||||
const { sender, text, searchResult, isCreatedByUser, error, submitting } = message;
|
||||
const textEditor = useRef(null);
|
||||
// const { convos } = useSelector((state) => state.convo);
|
||||
const last = !message?.children?.length;
|
||||
const edit = message.messageId == currentEditId;
|
||||
const { ask } = useMessageHandler();
|
||||
const dispatch = useDispatch();
|
||||
// const currentConvo = convoMap[message.conversationId];
|
||||
|
||||
// const notUser = !isCreatedByUser; // sender.toLowerCase() !== 'user';
|
||||
// const blinker = submitting && isSubmitting && last && !isCreatedByUser;
|
||||
|
|
@ -62,7 +61,7 @@ export default function Message({
|
|||
}
|
||||
}, [last, message]);
|
||||
|
||||
const enterEdit = (cancel) => setCurrentEditId(cancel ? -1 : message.messageId);
|
||||
const enterEdit = cancel => setCurrentEditId(cancel ? -1 : message.messageId);
|
||||
|
||||
const handleWheel = () => {
|
||||
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]';
|
||||
|
||||
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 text = textEditor.current.innerText;
|
||||
|
||||
|
|
@ -135,11 +133,11 @@ export default function Message({
|
|||
<div
|
||||
{...props}
|
||||
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 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>
|
||||
) : (
|
||||
icon
|
||||
|
|
@ -153,6 +151,15 @@ export default function Message({
|
|||
</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)]">
|
||||
{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">
|
||||
{error ? (
|
||||
<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}
|
||||
onClick={() => enterEdit()}
|
||||
/>
|
||||
<div className="sibling-switch-container flex justify-between">
|
||||
<div className="flex items-center justify-center gap-1 self-center pt-2 text-xs">
|
||||
<SubRow subclasses="switch-container">
|
||||
<SiblingSwitch
|
||||
siblingIdx={siblingIdx}
|
||||
siblingCount={siblingCount}
|
||||
setSiblingIdx={setSiblingIdx}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SubRow>
|
||||
</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 }) {
|
||||
const [currentEditId, setCurrentEditId] = useState(-1);
|
||||
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 [showScrollButton, setShowScrollButton] = useState(false);
|
||||
const scrollableRef = useRef(null);
|
||||
|
|
@ -36,7 +36,6 @@ export default function Messages({ messages, messageTree }) {
|
|||
}, [messages]);
|
||||
|
||||
const scrollToBottom = useCallback(throttle(() => {
|
||||
console.log('scrollToBottom');
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
setShowScrollButton(false);
|
||||
}, 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">
|
||||
Model: {modelName} {customModel ? `(${customModel})` : null}
|
||||
</div>
|
||||
{(messageTree.length === 0) ? (
|
||||
{(messageTree.length === 0 || !messages) ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { useDispatch } from 'react-redux';
|
|||
import { setNewConvo, removeAll } from '~/store/convoSlice';
|
||||
import { setMessages } from '~/store/messageSlice';
|
||||
import { setSubmission } from '~/store/submitSlice';
|
||||
import { Dialog, DialogTrigger } from '../ui/Dialog.tsx';
|
||||
import DialogTemplate from '../ui/DialogTemplate';
|
||||
|
||||
export default function ClearConvos() {
|
||||
const dispatch = useDispatch();
|
||||
|
|
@ -25,12 +27,25 @@ export default function ClearConvos() {
|
|||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<a
|
||||
className="flex cursor-pointer items-center gap-3 rounded-md py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
|
||||
onClick={clickHandler}
|
||||
// onClick={clickHandler}
|
||||
>
|
||||
<TrashIcon />
|
||||
Clear conversations
|
||||
</a>
|
||||
</DialogTrigger>
|
||||
<DialogTemplate
|
||||
title="Clear conversations"
|
||||
description="Are you sure you want to clear all conversations? This is irreversible."
|
||||
selection={{
|
||||
selectHandler: clickHandler,
|
||||
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
|
||||
selectText: 'Clear',
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
import React from 'react';
|
||||
import NavLink from './NavLink';
|
||||
import LogOutIcon from '../svg/LogOutIcon';
|
||||
import SearchBar from './SearchBar';
|
||||
import ClearConvos from './ClearConvos';
|
||||
import DarkMode from './DarkMode';
|
||||
import Logout from './Logout';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
export default function NavLinks({ fetch, onSearchSuccess, clearSearch }) {
|
||||
const { searchEnabled } = useSelector((state) => state.search);
|
||||
return (
|
||||
<>
|
||||
{/* <SearchBar fetch={fetch} onSuccess={onSearchSuccess} clearSearch={clearSearch}/> */}
|
||||
<ClearConvos />
|
||||
{ !!searchEnabled && <SearchBar fetch={fetch} onSuccess={onSearchSuccess} clearSearch={clearSearch}/>}
|
||||
<DarkMode />
|
||||
<ClearConvos />
|
||||
<Logout />
|
||||
{/* <NavLink
|
||||
svg={LogOutIcon}
|
||||
text="Log out"
|
||||
/> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setNewConvo } from '~/store/convoSlice';
|
||||
import { setNewConvo, refreshConversation } from '~/store/convoSlice';
|
||||
import { setMessages } from '~/store/messageSlice';
|
||||
import { setSubmission } from '~/store/submitSlice';
|
||||
import { setSubmission, setDisabled } from '~/store/submitSlice';
|
||||
import { setText } from '~/store/textSlice';
|
||||
import { setInputValue, setQuery } from '~/store/searchSlice';
|
||||
|
||||
export default function NewChat() {
|
||||
const dispatch = useDispatch();
|
||||
|
|
@ -12,7 +13,11 @@ export default function NewChat() {
|
|||
dispatch(setText(''));
|
||||
dispatch(setMessages([]));
|
||||
dispatch(setNewConvo());
|
||||
dispatch(refreshConversation());
|
||||
dispatch(setSubmission({}));
|
||||
dispatch(setDisabled(false));
|
||||
dispatch(setInputValue(''));
|
||||
dispatch(setQuery(''));
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { Search } from 'lucide-react';
|
||||
import { setQuery } from '~/store/searchSlice';
|
||||
import { setInputValue, setQuery } from '~/store/searchSlice';
|
||||
|
||||
export default function SearchBar({ fetch, clearSearch }) {
|
||||
const dispatch = useDispatch();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const { inputValue } = useSelector((state) => state.search);
|
||||
// const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const debouncedChangeHandler = useCallback(
|
||||
debounce((q) => {
|
||||
|
|
@ -28,10 +29,9 @@ export default function SearchBar({ fetch, clearSearch }) {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
const changeHandler = (e) => {
|
||||
let q = e.target.value;
|
||||
setInputValue(q);
|
||||
dispatch(setInputValue(q));
|
||||
q = q.trim();
|
||||
|
||||
if (q === '') {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export default function Nav({ navVisible, setNavVisible }) {
|
|||
setPage(res.pageNumber);
|
||||
setPages(res.pages);
|
||||
setIsFetching(false);
|
||||
if (res.messages) {
|
||||
if (res.messages?.length > 0) {
|
||||
dispatch(setMessages(res.messages));
|
||||
dispatch(setDisabled(true));
|
||||
}
|
||||
|
|
@ -54,8 +54,10 @@ export default function Nav({ navVisible, setNavVisible }) {
|
|||
const clearSearch = () => {
|
||||
setPage(1);
|
||||
dispatch(refreshConversation());
|
||||
if (!conversationId) {
|
||||
dispatch(setNewConvo());
|
||||
dispatch(setMessages([]));
|
||||
}
|
||||
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) {
|
||||
.sibling-switch-container {
|
||||
.switch-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.switch-result {
|
||||
display: block !important;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
/* .sibling-switch {
|
||||
left: 114px;
|
||||
|
|
|
|||
|
|
@ -17,14 +17,15 @@ const initialState = {
|
|||
refreshConvoHint: 0,
|
||||
search: false,
|
||||
latestMessage: null,
|
||||
convos: []
|
||||
convos: [],
|
||||
convoMap: {},
|
||||
};
|
||||
|
||||
const currentSlice = createSlice({
|
||||
name: 'convo',
|
||||
initialState,
|
||||
reducers: {
|
||||
refreshConversation: (state, action) => {
|
||||
refreshConversation: (state) => {
|
||||
state.refreshConvoHint = state.refreshConvoHint + 1;
|
||||
},
|
||||
setConversation: (state, action) => {
|
||||
|
|
@ -69,6 +70,13 @@ const currentSlice = createSlice({
|
|||
} else {
|
||||
state.convos = convos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||
}
|
||||
|
||||
// state.convoMap = convos.reduce((acc, curr) => {
|
||||
// acc[curr.conversationId] = { ...curr };
|
||||
// delete acc[curr.conversationId].conversationId;
|
||||
// return acc;
|
||||
// }, {});
|
||||
|
||||
},
|
||||
setPages: (state, action) => {
|
||||
state.pages = action.payload;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const initialState = {
|
||||
searchEnabled: false,
|
||||
search: false,
|
||||
query: '',
|
||||
inputValue: '',
|
||||
};
|
||||
|
||||
const currentSlice = createSlice({
|
||||
name: 'search',
|
||||
initialState,
|
||||
reducers: {
|
||||
setInputValue: (state, action) => {
|
||||
state.inputValue = action.payload;
|
||||
},
|
||||
setSearchState: (state, action) => {
|
||||
state.search = action.payload;
|
||||
state.searchEnabled = action.payload;
|
||||
},
|
||||
setQuery: (state, action) => {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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 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';
|
||||
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-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) {
|
||||
let messageMap = {};
|
||||
|
|
@ -7,7 +9,7 @@ export default function buildTree(messages, groupAll = false) {
|
|||
|
||||
if (!groupAll) {
|
||||
// Traverse the messages array and store each element in messageMap.
|
||||
messages.forEach((message) => {
|
||||
messages.forEach(message => {
|
||||
messageMap[message.messageId] = { ...message, children: [] };
|
||||
|
||||
const parentMessage = messageMap[message.parentMessageId];
|
||||
|
|
@ -30,4 +32,21 @@ export default function buildTree(messages, groupAll = false) {
|
|||
});
|
||||
|
||||
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:
|
||||
- ./data-node:/data/db
|
||||
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
|
||||
ports:
|
||||
- 7700:7700
|
||||
environment:
|
||||
- MEILI_MASTER_KEY=MASTER_KEY
|
||||
env_file:
|
||||
- ./api/.env
|
||||
volumes:
|
||||
- ./meili_data:/meili_data
|
||||
Loading…
Add table
Add a link
Reference in a new issue