Merge pull request #117 from danny-avila/search-final

Search final
This commit is contained in:
Danny Avila 2023-03-23 13:37:55 -04:00 committed by GitHub
commit c6fb3018e7
40 changed files with 990 additions and 364 deletions

View file

@ -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
View 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"
}

View file

@ -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');

View file

@ -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
View 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
View 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
};

View file

@ -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 });
}; };

View file

@ -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;
} }

View file

@ -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 {

View file

@ -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();
}); });

View 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;

View 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;

View 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.');
}
};

View file

@ -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);
}
}); });

View file

@ -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);
}); });

View file

@ -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
View 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
View file

@ -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",

View file

@ -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",

View file

@ -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;

View file

@ -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) {

View file

@ -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;

View 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>
);
}

View file

@ -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}

View file

@ -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>

View 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;

View file

@ -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 />
) : ( ) : (
<> <>

View file

@ -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>
); );
} }

View file

@ -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"
/> */}
</> </>
); );
} }

View file

@ -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 (

View file

@ -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 === '') {

View file

@ -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));
}; };

View 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>
);
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
} }

View 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';
}
}

View file

@ -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

View file

@ -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