Compare commits

...

36 Commits

Author SHA1 Message Date
Danny Avila
c6fb3018e7 Merge pull request #117 from danny-avila/search-final
Search final
2023-03-23 13:37:55 -04:00
Danny Avila
95cf27ee3e edit env example 2023-03-23 13:30:55 -04:00
Danny Avila
7afe09fa02 chore: clear timeouts 2023-03-23 13:16:07 -04:00
Danny Avila
bff33c79b3 merge latest pr from main 2023-03-23 13:04:40 -04:00
Danny Avila
f73936e5f4 Merge branch 'main' into search-final 2023-03-23 13:02:52 -04:00
Danny Avila
2b8d37c38f edit env example 2023-03-23 12:45:42 -04:00
Danny Avila
ca3da2505a edit env example 2023-03-23 11:49:54 -04:00
Danny Avila
d3046dca07 Merge pull request #115 from HyunggyuJang/sydney/branching
Enable branching (edit message) for Sydney
2023-03-23 11:38:44 -04:00
Danny Avila
25fd39c2b9 Update README.md
organize readme for readability
2023-03-23 11:35:23 -04:00
Daniel Avila
ab8724b568 edit docker-compose for meilisearch 2023-03-22 22:35:25 -04:00
Hyunggyu Jang
c240d14864 Enable branching (edit message) for Sydney 2023-03-23 11:27:14 +09:00
Daniel Avila
1d464fdcfa reduce noisy meili errors if not set 2023-03-22 21:23:01 -04:00
Daniel Avila
aacc292522 Merge branch 'main' into search-final 2023-03-22 20:57:20 -04:00
Danny Avila
350a1bbae0 Update README.md 2023-03-22 20:48:47 -04:00
Daniel Avila
37f36ec44a chore: update meilisearch compose 2023-03-22 20:12:38 -04:00
Daniel Avila
71b7eaa3f5 chore: update env example and add browser client config 2023-03-22 20:09:52 -04:00
Daniel Avila
719413f87a feat: syncs across document deletions 2023-03-22 19:53:09 -04:00
Daniel Avila
97634865eb feat: syncs across document deletions 2023-03-22 19:52:38 -04:00
Daniel Avila
1dbfb0dab7 minor styling changes, cache queried messages on server 2023-03-22 18:26:29 -04:00
Daniel Avila
0a671849b5 feat: clearing convos requires confirmation 2023-03-22 17:51:51 -04:00
Daniel Avila
655e7ce6d6 chore: improve meili error handling 2023-03-22 17:15:32 -04:00
Danny Avila
68979015c1 markdown styling changes in progress 2023-03-22 16:31:57 -04:00
Danny Avila
8f58c95452 feat: main styling/ui/ux final changes 2023-03-22 16:06:11 -04:00
Danny Avila
67161c983f chore: meilisearch setup config 2023-03-22 10:23:55 -04:00
Danny Avila
73449d9ec6 feat: build tree by convoId 2023-03-22 10:23:36 -04:00
Danny Avila
5164cf46ac chore: error handling for complete omission of env var 2023-03-22 09:38:38 -04:00
Daniel Avila
277685c218 search: updating search endpoint (wip) 2023-03-22 01:34:36 -04:00
Daniel Avila
e25aa74d7b search: correctly register/export schema/models, also made IIFE 2023-03-22 01:33:49 -04:00
Daniel Avila
47a6cfcafd chore: error controller (wip) 2023-03-22 01:32:55 -04:00
Daniel Avila
83a96706b4 chore: add prettier 2023-03-22 01:32:30 -04:00
Daniel Avila
75be4d9722 search: helper fn for invalid convoId strings 2023-03-22 01:31:49 -04:00
Daniel Avila
a5cf2f9148 search: correctly register/export schema/models for mongoMeili 2023-03-22 01:31:01 -04:00
Daniel Avila
8be19f9982 search: sync on offset between meili and mongo 2023-03-22 01:30:04 -04:00
Daniel Avila
194051e424 feat: api will plugin mongoMeili without a valid connection 2023-03-21 20:06:27 -04:00
Daniel Avila
94c0fbb525 feat: api will disable search if no meilisearch connection 2023-03-21 19:44:31 -04:00
Daniel Avila
97a6cd801b feat: simple api call to enable search 2023-03-21 19:31:57 -04:00
42 changed files with 1036 additions and 373 deletions

View File

@@ -7,6 +7,20 @@ https://user-images.githubusercontent.com/110412045/223754183-8b7f45ce-6517-4bd5
This project was started early in Feb '23, anticipating the release of the official ChatGPT API from OpenAI, and now uses it. Through this clone, you can avoid ChatGPT Plus in favor of free or pay-per-call APIs. I will soon deploy a demo of this app. Feel free to contribute, clone, or fork. Currently dockerized.
## Updates
<details open>
<summary><strong>2023-03-22</strong></summary>
**Released [v0.0.6](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.0.6)**, the latest stable release before **Searching messages** goes live tomorrow. See exact updates to date in the tag link. By request, there is now also a **[community discord server](https://discord.gg/NGaa9RPCft)**
</details>
<details>
<summary><strong>Previous Updates</strong></summary>
<details>
<summary><strong>2023-03-20</strong></summary>
@@ -18,9 +32,6 @@ The above features are next and then I will have to focus on building the **test
On that note, I had to switch the default branch due to some breaking changes that haven't been straight forward to debug, mainly related to node-chat-gpt the main dependency of the project. Thankfully, my working branch, now switched to default as main, is working as expected.
</details>
<details>
<details>
<details>
<summary><strong>2023-03-16</strong></summary>
@@ -31,6 +42,7 @@ On that note, I had to switch the default branch due to some breaking changes th
Full details and [example here](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.0.4). Message search is on the docket
</details>
<details>
<summary><strong>2023-03-12</strong></summary>
@@ -61,8 +73,6 @@ Due to increased interest in the repo, I've dockerized the app as of this update
Also worth noting, the method to access the Free Version is no longer working, so I've removed it from model selection until further notice.
</details>
<summary><strong>Previous Updates</strong></summary>
<details>
<summary><strong>2023-03-04</strong></summary>
Custom prompt prefixing and labeling is now supported through the official API. This nets some interesting results when you need ChatGPT for specific uses or entertainment. Select 'CustomGPT' in the model menu to configure this, and you can choose to save the configuration or reference it by conversation. Model selection will change by conversation.
@@ -120,7 +130,10 @@ Currently, this project is only functional with the `text-davinci-003` model.
> This is a work in progress. I'm building this in public. FYI there is still a lot of tech debt to cleanup. You can follow the progress here or on my [Linkedin](https://www.linkedin.com/in/danny-avila).
Here are my recently completed and planned features:
<details>
<summary><strong>Here are my recently completed and planned features:</strong></summary>
- [x] Persistent conversation
- [x] Rename, delete conversations
@@ -144,6 +157,8 @@ Here are my recently completed and planned features:
- [ ] Optional use of local storage for credentials
- [ ] Deploy demo
</details>
### Features
- Response streaming identical to ChatGPT through server-sent events
@@ -157,10 +172,19 @@ Here are my recently completed and planned features:
### Tech Stack
- Utilizes [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api)
<details>
<summary><strong>This project uses:</strong></summary>
- [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api)
- No React boilerplate/toolchain/clone tutorials, created from scratch with react@latest
- Use of Tailwind CSS and [shadcn/ui](https://github.com/shadcn/ui) components
- Docker, useSWR, Redux, Express, MongoDB, [Keyv](https://www.npmjs.com/package/keyv)
</details>
## Getting Started
@@ -287,10 +311,16 @@ Please write me an express module, that serve the login and logout endpoint as a
### Updating
- As the project is still a work-in-progress, you should pull the latest and run the steps over. Reset your browser cache/clear site data.
## Use Cases ##
<details>
<summary><strong> Why use this project? </strong></summary>
- One stop shop for all conversational AIs, with the added bonus of searching past conversations.
- Using the official API, you'd have to generate 7.5 million words to expense the same cost as ChatGPT Plus ($20).
- ChatGPT/Google Bard/Bing AI conversations are lost in space or
@@ -306,6 +336,8 @@ Please write me an express module, that serve the login and logout endpoint as a
- **ChatGPT Free is down.**
![use case example](./images/use_case.png "GPT is down! Plus is too expensive!")
</details>
## Origin ##
@@ -324,7 +356,12 @@ This means my implementation or the underlying model may not behave exactly the
- This works in a similar way to ChatGPT, except I'm pretty sure they have some additional way of retrieving context from earlier messages when needed (which can probably be achieved with embeddings, but I consider that out-of-scope for now).
## Contributing
If you'd like to contribute, please create a pull request with a detailed description of your changes.
Contributions and suggestions welcome! Bug reports and fixes are welcome!
For new features, components, or extensions, please open an issue and discuss before sending a PR.
- Join the [Discord community](https://discord.gg/NGaa9RPCft)
## License
This project is licensed under the MIT License.

View File

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

View File

@@ -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,33 +18,37 @@ const proxyEnvToAxiosProxy = (proxyString) => {
const titleConvo = async ({ model, text, response }) => {
let title = 'New Chat';
const request = {
model: 'gpt-3.5-turbo',
messages: [
{
role: 'system',
content:
'You are a title-generator with one job: giving a conversation, detect the language and titling the conversation provided by a user in title case, using the same language.'
},
{
role: 'user',
content: `In 5 words or less, summarize the conversation below with a title in title case using the language the user writes in. Don't refer to the participants of the conversation nor the language. Do not include punctuation or quotation marks. Your response should be in title case, exclusively containing the title. Conversation:\n\nUser: "${text}"\n\n${model}: "${JSON.stringify(
response?.text
)}"\n\nTitle: `
}
],
temperature: 0,
presence_penalty: 0,
frequency_penalty: 0
};
// console.log('REQUEST', request);
try {
const configuration = new Configuration({
apiKey: process.env.OPENAI_KEY
});
const openai = new OpenAIApi(configuration);
const completion = await openai.createChatCompletion(
{
model: 'gpt-3.5-turbo',
messages: [
{
role: 'system',
content:
'You are a title-generator with one job: giving a conversation, detect the language and titling the conversation provided by a user in title case, using the same language.'
},
{
role: 'user',
content: `In 5 words or less, summarize the conversation below with a title in title case using the language the user writes in. Don't refer to the participants of the conversation nor the language. Do not include punctuation or quotation marks. Your response should be in title case, exclusively containing the title. Conversation:\n\nUser: "${text}"\n\n${model}: "${JSON.stringify(
response?.text
)}"\n\nTitle: `
}
],
temperature: 0,
presence_penalty: 0,
frequency_penalty: 0,
},
{ proxy: proxyEnvToAxiosProxy(process.env.PROXY || null) }
);
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
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
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 });
};

View File

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

View File

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

View File

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

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 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 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 host = process.env.HOST || 'localhost';
const userSystemEnabled = process.env.ENABLE_USER_SYSTEM || false;
const projectPath = path.join(__dirname, '..', '..', 'client');
connectDb().then(() => {
(async () => {
await connectDb();
console.log('Connected to MongoDB');
migrateDb();
});
await migrateDb();
await indexSync();
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,
}))
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
})
);
/* 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) {
if (userSystemEnabled) {
const user = req?.session?.user
if (user)
res.send(JSON.stringify({username: user?.username, display: user?.display}));
app.get('/api/me', function (req, res) {
if (userSystemEnabled) {
const user = req?.session?.user;
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' }));
}
});
app.use('/api/search', routes.authenticatedOr401, routes.search);
app.use('/api/ask', routes.authenticatedOr401, routes.ask);
app.use('/api/messages', routes.authenticatedOr401, routes.messages);
app.use('/api/convos', routes.authenticatedOr401, routes.convos);
app.use('/api/customGpts', routes.authenticatedOr401, routes.customGpts);
app.use('/api/prompts', routes.authenticatedOr401, routes.prompts);
app.use('/auth', routes.auth);
app.get('/api/models', function (req, res) {
const hasOpenAI = !!process.env.OPENAI_KEY;
const hasChatGpt = !!process.env.CHATGPT_TOKEN;
const hasBing = !!process.env.BING_TOKEN;
res.send(JSON.stringify({ hasOpenAI, hasChatGpt, hasBing }));
});
app.listen(port, host, () => {
if (host == '0.0.0.0')
console.log(
`Server listening on all interface at port ${port}. Use http://localhost:${port} to access it`
);
else
res.send(JSON.stringify(null));
console.log(
`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`
);
});
})();
let messageCount = 0;
process.on('uncaughtException', (err) => {
if (!err.message.includes('fetch failed')) {
console.error('There was an uncaught error:', err.message);
}
if (err.message.includes('fetch failed')) {
if (messageCount === 0) {
console.error('Meilisearch error, search will be disabled');
messageCount++;
}
} else {
res.send(JSON.stringify({username: 'anonymous_user', display: 'Anonymous User'}));
process.exit(1);
}
});
app.use('/api/search', routes.authenticatedOr401, routes.search);
app.use('/api/ask', routes.authenticatedOr401, routes.ask);
app.use('/api/messages', routes.authenticatedOr401, routes.messages);
app.use('/api/convos', routes.authenticatedOr401, routes.convos);
app.use('/api/customGpts', routes.authenticatedOr401, routes.customGpts);
app.use('/api/prompts', routes.authenticatedOr401, routes.prompts);
app.use('/auth', routes.auth);
app.get('/api/models', function (req, res) {
const hasOpenAI = !!process.env.OPENAI_KEY;
const hasChatGpt = !!process.env.CHATGPT_TOKEN;
const hasBing = !!process.env.BING_TOKEN;
res.send(JSON.stringify({ hasOpenAI, hasChatGpt, hasBing }));
});
app.listen(port, host, () => {
if (host=='0.0.0.0')
console.log(`Server listening on all interface at port ${port}. Use http://localhost:${port} to access it`);
else
console.log(`Server listening at http://${host=='0.0.0.0'?'localhost':host}:${port}`);
});

View File

@@ -19,25 +19,22 @@ 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)
: await titleConvo({
model: convo?.model,
message: firstMessage?.text,
response: JSON.stringify(secondMessage?.text || '')
});
model: convo?.model,
message: firstMessage?.text,
response: JSON.stringify(secondMessage?.text || '')
});
await saveConvo(
req?.session?.user?.username,
{
conversationId,
title
}
)
await saveConvo(req?.session?.user?.username, {
conversationId,
title
});
res.status(200).send(title);
});

View File

@@ -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);
delete result.cache;
result.messages = messages.filter((message) => !result.filter.has(message.conversationId));
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.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
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",
"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",

View File

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

View File

@@ -6,42 +6,34 @@ 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 = () => {
const dispatch = useDispatch();
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;

View File

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

View File

@@ -4,29 +4,33 @@ 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 }) => {
const Content = React.memo(({ content, isCreatedByUser = false }) => {
const rehypePlugins = [
[rehypeKatex, { output: 'mathml' }],
[
rehypeHighlight,
{
detect: true,
ignoreMissing: true,
subset: langSubset
}
],
[rehypeRaw],
]
return (
<>
<ReactMarkdown
remarkPlugins={[remarkGfm, [remarkMath, { singleDollarTextMath: false }]]}
rehypePlugins={[
[rehypeKatex, { output: 'mathml' }],
[
rehypeHighlight,
{
detect: true,
ignoreMissing: true,
subset: langSubset
}
]
]}
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;

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

View File

@@ -3,7 +3,7 @@ import React from 'react';
import EditIcon from '../svg/EditIcon';
export default function HoverButtons({ visible, onClick, model }) {
const isBing = model === 'bingai' || model === 'sydney';
const isBing = model === 'bingai';
const enabled = !isBing;
return (

View File

@@ -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">
<SiblingSwitch
siblingIdx={siblingIdx}
siblingCount={siblingCount}
setSiblingIdx={setSiblingIdx}
/>
</div>
</div>
<SubRow subclasses="switch-container">
<SiblingSwitch
siblingIdx={siblingIdx}
siblingCount={siblingCount}
setSiblingIdx={setSiblingIdx}
/>
</SubRow>
</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 }) {
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 />
) : (
<>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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());
dispatch(setNewConvo());
dispatch(setMessages([]));
if (!conversationId) {
dispatch(setNewConvo());
dispatch(setMessages([]));
}
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) {
.sibling-switch-container {
.switch-container {
display: none;
}
}
.switch-result {
display: block !important;
visibility: visible;
}
@media (max-width: 1024px) {
/* .sibling-switch {
left: 114px;

View File

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

View File

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

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

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:
- ./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

View File

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