diff --git a/.gitignore b/.gitignore index fa70c3c4c4..e201974879 100644 --- a/.gitignore +++ b/.gitignore @@ -47,7 +47,6 @@ bower_components/ .env cache.json api/data/ -.eslintrc.js owner.yml archive .vscode/settings.json diff --git a/api/app/bingai.js b/api/app/bingai.js index d1175c0bfc..dcf150d1cf 100644 --- a/api/app/bingai.js +++ b/api/app/bingai.js @@ -1,7 +1,7 @@ require('dotenv').config(); const { KeyvFile } = require('keyv-file'); -const askBing = async ({ text, progressCallback, convo }) => { +const askBing = async ({ text, onProgress, convo }) => { const { BingAIClient } = (await import('@waylaidwanderer/chatgpt-api')); const bingAIClient = new BingAIClient({ @@ -14,16 +14,15 @@ const askBing = async ({ text, progressCallback, convo }) => { proxy: process.env.PROXY || null, }); - let options = { - onProgress: async (partialRes) => await progressCallback(partialRes), - }; - + let options = { onProgress }; if (convo) { options = { ...options, ...convo }; } - const res = await bingAIClient.sendMessage(text, options - ); + if (options?.jailbreakConversationId == 'false') + options.jailbreakConversationId = false + + const res = await bingAIClient.sendMessage(text, options); return res; diff --git a/api/app/chatgpt-browser.js b/api/app/chatgpt-browser.js index 8a3c903641..a5bea22729 100644 --- a/api/app/chatgpt-browser.js +++ b/api/app/chatgpt-browser.js @@ -10,7 +10,7 @@ const clientOptions = { proxy: process.env.PROXY || null, }; -const browserClient = async ({ text, progressCallback, convo }) => { +const browserClient = async ({ text, onProgress, convo }) => { const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api'); const store = { @@ -18,10 +18,7 @@ const browserClient = async ({ text, progressCallback, convo }) => { }; const client = new ChatGPTBrowserClient(clientOptions, store); - - let options = { - onProgress: async (partialRes) => await progressCallback(partialRes) - }; + let options = { onProgress }; if (!!convo.parentMessageId && !!convo.conversationId) { options = { ...options, ...convo }; diff --git a/api/app/chatgpt-client.js b/api/app/chatgpt-client.js index afd31e0a81..350d1210ce 100644 --- a/api/app/chatgpt-client.js +++ b/api/app/chatgpt-client.js @@ -9,17 +9,14 @@ const clientOptions = { debug: false }; -const askClient = async ({ text, progressCallback, convo }) => { +const askClient = async ({ text, onProgress, convo }) => { const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default; const store = { store: new KeyvFile({ filename: './data/cache.json' }) }; const client = new ChatGPTClient(process.env.OPENAI_KEY, clientOptions, store); - - let options = { - onProgress: async (partialRes) => await progressCallback(partialRes) - }; + let options = { onProgress }; if (!!convo.parentMessageId && !!convo.conversationId) { options = { ...options, ...convo }; diff --git a/api/app/chatgpt-custom.js b/api/app/chatgpt-custom.js index 9eabb80ccd..e7a0ee0503 100644 --- a/api/app/chatgpt-custom.js +++ b/api/app/chatgpt-custom.js @@ -9,7 +9,7 @@ const clientOptions = { debug: false }; -const customClient = async ({ text, progressCallback, convo, promptPrefix, chatGptLabel }) => { +const customClient = async ({ text, onProgress, convo, promptPrefix, chatGptLabel }) => { const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default; const store = { store: new KeyvFile({ filename: './data/cache.json' }) @@ -23,10 +23,7 @@ const customClient = async ({ text, progressCallback, convo, promptPrefix, chatG const client = new ChatGPTClient(process.env.OPENAI_KEY, clientOptions, store); - let options = { - onProgress: async (partialRes) => await progressCallback(partialRes) - }; - + let options = { onProgress }; if (!!convo.parentMessageId && !!convo.conversationId) { options = { ...options, ...convo }; } diff --git a/api/app/chatgpt.js b/api/app/chatgpt.js deleted file mode 100644 index 18edcfca83..0000000000 --- a/api/app/chatgpt.js +++ /dev/null @@ -1,38 +0,0 @@ -require('dotenv').config(); -const Keyv = require('keyv'); -const { Configuration, OpenAIApi } = require('openai'); -const messageStore = new Keyv(process.env.MONGODB_URI, { namespace: 'chatgpt' }); - -const ask = async (question, progressCallback, convo) => { - const { ChatGPTAPI } = await import('chatgpt'); - const api = new ChatGPTAPI({ apiKey: process.env.OPENAI_KEY, messageStore }); - let options = { - onProgress: async (partialRes) => { - if (partialRes.text.length > 0) { - await progressCallback(partialRes); - } - } - }; - - if (!!convo.parentMessageId && !!convo.conversationId) { - options = { ...options, ...convo }; - } - - const res = await api.sendMessage(question, options); - return res; -}; - -const titleConvo = async (message, response, model) => { - const configuration = new Configuration({ - apiKey: process.env.OPENAI_KEY - }); - const openai = new OpenAIApi(configuration); - const completion = await openai.createCompletion({ - model: 'text-davinci-002', - prompt: `Write a short title in title case, ideally in 5 words or less, and do not refer to the user or ${model}, that summarizes this conversation:\nUser:"${message}"\n${model}:"${response}"\nTitle: ` - }); - - return completion.data.choices[0].text.replace(/\n/g, ''); -}; - -module.exports = { ask, titleConvo }; diff --git a/api/app/sydney.js b/api/app/sydney.js index fe47c74f57..3466f71c17 100644 --- a/api/app/sydney.js +++ b/api/app/sydney.js @@ -1,7 +1,7 @@ require('dotenv').config(); const { KeyvFile } = require('keyv-file'); -const askSydney = async ({ text, progressCallback, convo }) => { +const askSydney = async ({ text, onProgress, convo }) => { const { BingAIClient } = (await import('@waylaidwanderer/chatgpt-api')); const sydneyClient = new BingAIClient({ @@ -15,10 +15,10 @@ const askSydney = async ({ text, progressCallback, convo }) => { let options = { jailbreakConversationId: true, - onProgress: async (partialRes) => await progressCallback(partialRes), + onProgress, }; - if (convo.parentMessageId) { + if (convo.jailbreakConversationId) { options = { ...options, jailbreakConversationId: convo.jailbreakConversationId, parentMessageId: convo.parentMessageId }; } diff --git a/api/app/titleConvo.js b/api/app/titleConvo.js index e0461eabaf..e37fcb3b0c 100644 --- a/api/app/titleConvo.js +++ b/api/app/titleConvo.js @@ -1,24 +1,59 @@ const { Configuration, OpenAIApi } = require('openai'); +const _ = require('lodash'); -const titleConvo = async ({ message, response, model }) => { - 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 by name. Do not include punctuation or quotation marks. Your response should be in title case, exclusively containing the title. Conversation:\n\nUser: "${message}"\n\n${model}: "${response}"\n\nTitle: ` }, - ] - }); +const proxyEnvToAxiosProxy = (proxyString) => { + if (!proxyString) return null; - //eslint-disable-next-line - return completion.data.choices[0].message.content.replace(/["\.]/g, ''); + const regex = /^([^:]+):\/\/(?:([^:@]*):?([^:@]*)@)?([^:]+)(?::(\d+))?/; + const [, protocol, username, password, host, port] = proxyString.match(regex); + const proxyConfig = { + protocol, + host, + port: port ? parseInt(port) : undefined, + auth: username && password ? { username, password } : undefined + }; + + return proxyConfig; }; -module.exports = titleConvo; +const titleConvo = async ({ model, text, response }) => { + let title = 'New Chat'; + try { + const configuration = new Configuration({ + apiKey: process.env.OPENAI_KEY + }); + const openai = new OpenAIApi(configuration); + const completion = await openai.createChatCompletion( + { + 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 by name. 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: ` + } + ] + }, + { proxy: proxyEnvToAxiosProxy(process.env.PROXY || null) } + ); + + //eslint-disable-next-line + title = completion.data.choices[0].message.content.replace(/["\.]/g, ''); + } catch (e) { + console.error(e); + console.log('There was an issue generating title, see error above'); + } + + console.log('CONVERSATION TITLE', title); + return title; +}; + +const throttledTitleConvo = _.throttle(titleConvo, 1000); + +module.exports = throttledTitleConvo; diff --git a/api/models/Conversation.js b/api/models/Conversation.js index 1ff778e957..45834379e8 100644 --- a/api/models/Conversation.js +++ b/api/models/Conversation.js @@ -1,48 +1,53 @@ const mongoose = require('mongoose'); +const crypto = require('crypto'); const { getMessages, deleteMessages } = require('./Message'); -const convoSchema = mongoose.Schema({ - conversationId: { - type: String, - unique: true, - required: true +const convoSchema = mongoose.Schema( + { + conversationId: { + type: String, + unique: true, + required: true + }, + parentMessageId: { + type: String, + required: true + }, + title: { + type: String, + default: 'New Chat' + }, + 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 + }, + suggestions: [{ type: String }], + messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }] }, - parentMessageId: { - type: String, - required: true - }, - title: { - type: String, - default: 'New conversation' - }, - jailbreakConversationId: { - type: String - }, - conversationSignature: { - type: String - }, - clientId: { - type: String - }, - invocationId: { - type: String - }, - chatGptLabel: { - type: String - }, - promptPrefix: { - type: String - }, - model: { - type: String - }, - suggestions: [{ type: String }], - messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }], - created: { - type: Date, - default: Date.now - } -}); + { timestamps: true } +); const Conversation = mongoose.models.Conversation || mongoose.model('Conversation', convoSchema); @@ -57,13 +62,24 @@ const getConvo = async (conversationId) => { }; module.exports = { - saveConvo: async ({ conversationId, title, ...convo }) => { + saveConvo: async ({ conversationId, newConversationId, title, ...convo }) => { try { const messages = await getMessages({ conversationId }); const update = { ...convo, messages }; if (title) { update.title = title; } + if (newConversationId) { + update.conversationId = newConversationId; + } + if (!update.jailbreakConversationId) { + update.jailbreakConversationId = null; + } + if (update.model !== 'chatgptCustom' && update.chatGptLabel && update.promptPrefix) { + console.log('Validation error: resetting chatgptCustom fields', update); + update.chatGptLabel = null; + update.promptPrefix = null; + } return await Conversation.findOneAndUpdate( { conversationId }, @@ -85,20 +101,18 @@ module.exports = { return { message: 'Error updating conversation' }; } }, - // getConvos: async () => await Conversation.find({}).sort({ created: -1 }).exec(), - getConvos: async (pageNumber = 1, pageSize = 12) => { + // getConvos: async () => await Conversation.find({}).sort({ createdAt: -1 }).exec(), + getConvosByPage: async (pageNumber = 1, pageSize = 12) => { try { - const skip = (pageNumber - 1) * pageSize; - // const limit = pageNumber * pageSize; - - const conversations = await Conversation.find({}) - .sort({ created: -1 }) - .skip(skip) - // .limit(limit) + const totalConvos = (await Conversation.countDocuments()) || 1; + const totalPages = Math.ceil(totalConvos / pageSize); + const convos = await Conversation.find() + .sort({ createdAt: -1, created: -1 }) + .skip((pageNumber - 1) * pageSize) .limit(pageSize) .exec(); - return conversations; + return { conversations: convos, pages: totalPages, pageNumber, pageSize }; } catch (error) { console.log(error); return { message: 'Error getting conversations' }; @@ -118,5 +132,60 @@ module.exports = { let deleteCount = await Conversation.deleteMany(filter).exec(); deleteCount.messages = await deleteMessages(filter); return deleteCount; + }, + migrateDb: async () => { + try { + const conversations = await Conversation.find({ model: null }).exec(); + + if (!conversations || conversations.length === 0) + return { message: '[Migrate] No conversations to migrate' }; + + for (let convo of conversations) { + const messages = await getMessages({ + conversationId: convo.conversationId, + messageId: { $exists: false } + }); + + let model; + let oldId; + const promises = []; + messages.forEach((message, i) => { + const msgObj = message.toObject(); + const newId = msgObj.id; + if (i === 0) { + message.parentMessageId = '00000000-0000-0000-0000-000000000000'; + } else { + message.parentMessageId = oldId; + } + + oldId = newId; + message.messageId = newId; + if (message.sender.toLowerCase() !== 'user' && !model) { + model = message.sender.toLowerCase(); + } + + if (message.sender.toLowerCase() === 'user') { + message.isCreatedByUser = true; + } + promises.push(message.save()); + }); + await Promise.all(promises); + + await Conversation.findOneAndUpdate( + { conversationId: convo.conversationId }, + { model }, + { new: true } + ).exec(); + } + + try { + await mongoose.connection.db.collection('messages').dropIndex('id_1'); + } catch (error) { + console.log("[Migrate] Index doesn't exist or already dropped"); + } + } catch (error) { + console.log(error); + return { message: '[Migrate] Error migrating conversations' }; + } } }; diff --git a/api/models/CustomGpt.js b/api/models/CustomGpt.js index 33bb75b124..1f02bdc4db 100644 --- a/api/models/CustomGpt.js +++ b/api/models/CustomGpt.js @@ -12,11 +12,7 @@ const customGptSchema = mongoose.Schema({ type: String, required: true }, - created: { - type: Date, - default: Date.now - } -}); +}, { timestamps: true }); const CustomGpt = mongoose.models.CustomGpt || mongoose.model('CustomGpt', customGptSchema); diff --git a/api/models/Message.js b/api/models/Message.js index e6657c060a..90165acb86 100644 --- a/api/models/Message.js +++ b/api/models/Message.js @@ -1,7 +1,7 @@ const mongoose = require('mongoose'); const messageSchema = mongoose.Schema({ - id: { + messageId: { type: String, unique: true, required: true @@ -32,33 +32,50 @@ const messageSchema = mongoose.Schema({ type: String, required: true }, - created: { - type: Date, - default: Date.now - } -}); + isCreatedByUser: { + type: Boolean, + required: true, + default: false + }, + error: { + type: Boolean, + default: false + }, +}, { timestamps: true }); const Message = mongoose.models.Message || mongoose.model('Message', messageSchema); module.exports = { - saveMessage: async ({ id, conversationId, parentMessageId, sender, text }) => { + saveMessage: async ({ messageId, conversationId, parentMessageId, sender, text, isCreatedByUser=false, error }) => { try { - await Message.create({ - id, + await Message.findOneAndUpdate({ messageId }, { conversationId, parentMessageId, sender, - text - }); - return { id, conversationId, parentMessageId, sender, text }; + text, + isCreatedByUser, + error + }, { upsert: true, new: true }); + return { messageId, conversationId, parentMessageId, sender, text, isCreatedByUser }; } catch (error) { console.error(error); return { message: 'Error saving message' }; } }, + deleteMessagesSince: async ({ messageId, conversationId }) => { + try { + const message = await Message.findOne({ messageId }).exec() + + if (message) + return await Message.find({ conversationId }).deleteMany({ createdAt: { $gt: message.createdAt } }).exec(); + } catch (error) { + console.error(error); + return { message: 'Error deleting messages' }; + } + }, getMessages: async (filter) => { try { - return await Message.find(filter).exec() + return await Message.find(filter).sort({createdAt: 1}).exec() } catch (error) { console.error(error); return { message: 'Error getting messages' }; diff --git a/api/models/Prompt.js b/api/models/Prompt.js index 612278d038..f122d5af40 100644 --- a/api/models/Prompt.js +++ b/api/models/Prompt.js @@ -12,11 +12,7 @@ const promptSchema = mongoose.Schema({ category: { type: String, }, - created: { - type: Date, - default: Date.now - } -}); +}, { timestamps: true }); const Prompt = mongoose.models.Prompt || mongoose.model('Prompt', promptSchema); diff --git a/api/models/index.js b/api/models/index.js index 8af5a1aa9c..cd75b9c6a3 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -1,13 +1,15 @@ -const { saveMessage, deleteMessages } = require('./Message'); +const { saveMessage, deleteMessagesSince, deleteMessages } = require('./Message'); const { getCustomGpts, updateCustomGpt, updateByLabel, deleteCustomGpts } = require('./CustomGpt'); -const { getConvoTitle, getConvo, saveConvo } = require('./Conversation'); +const { getConvoTitle, getConvo, saveConvo, migrateDb } = require('./Conversation'); module.exports = { saveMessage, + deleteMessagesSince, deleteMessages, getConvoTitle, getConvo, saveConvo, + migrateDb, getCustomGpts, updateCustomGpt, updateByLabel, diff --git a/api/package-lock.json b/api/package-lock.json index 911fda953e..f934f00b8d 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -17,8 +17,10 @@ "express": "^4.18.2", "keyv": "^4.5.2", "keyv-file": "^0.2.0", + "lodash": "^4.17.21", "mongoose": "^6.9.0", - "openai": "^3.1.0" + "openai": "^3.1.0", + "sanitize-html": "^2.10.0" }, "devDependencies": { "nodemon": "^2.0.20", @@ -2210,6 +2212,14 @@ } } }, + "node_modules/deepmerge": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", + "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -2246,6 +2256,57 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", + "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.1" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", @@ -2277,6 +2338,17 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2750,6 +2822,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/htmlparser2": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", + "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "entities": "^4.3.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2957,6 +3047,14 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3272,6 +3370,17 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -3468,6 +3577,11 @@ "p-defer": "^3.0.0" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3576,6 +3690,29 @@ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz", "integrity": "sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==" }, + "node_modules/postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -3795,6 +3932,30 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sanitize-html": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.10.0.tgz", + "integrity": "sha512-JqdovUd81dG4k87vZt6uA6YhDfWkUGruUu/aPmXLxXi45gZExnt9Bnw/qeQU8oGf82vPyaE0vO4aH0PbobB9JQ==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/saslprep": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", @@ -3984,6 +4145,14 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -6274,6 +6443,11 @@ "ms": "2.1.2" } }, + "deepmerge": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", + "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==" + }, "defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -6297,6 +6471,39 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", + "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.1" + } + }, "dotenv": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", @@ -6322,6 +6529,11 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, + "entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -6686,6 +6898,17 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, + "htmlparser2": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", + "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "entities": "^4.3.0" + } + }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -6825,6 +7048,11 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -7073,6 +7301,11 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -7216,6 +7449,11 @@ "p-defer": "^3.0.0" } }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -7302,6 +7540,16 @@ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz", "integrity": "sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==" }, + "postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -7457,6 +7705,26 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sanitize-html": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.10.0.tgz", + "integrity": "sha512-JqdovUd81dG4k87vZt6uA6YhDfWkUGruUu/aPmXLxXi45gZExnt9Bnw/qeQU8oGf82vPyaE0vO4aH0PbobB9JQ==", + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + } + } + }, "saslprep": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", @@ -7614,6 +7882,11 @@ "atomic-sleep": "^1.0.0" } }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, "sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", diff --git a/api/package.json b/api/package.json index 28bd6769d3..561908fad3 100644 --- a/api/package.json +++ b/api/package.json @@ -27,8 +27,10 @@ "express": "^4.18.2", "keyv": "^4.5.2", "keyv-file": "^0.2.0", + "lodash": "^4.17.21", "mongoose": "^6.9.0", - "openai": "^3.1.0" + "openai": "^3.1.0", + "sanitize-html": "^2.10.0" }, "devDependencies": { "nodemon": "^2.0.20", diff --git a/api/server/index.js b/api/server/index.js index b380932531..bda88b14f4 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -1,5 +1,6 @@ const express = require('express'); const dbConnect = require('../models/dbConnect'); +const { migrateDb } = require('../models'); const path = require('path'); const cors = require('cors'); const routes = require('./routes'); @@ -7,7 +8,10 @@ const app = express(); const port = process.env.PORT || 3080; const host = process.env.HOST || 'localhost' const projectPath = path.join(__dirname, '..', '..', 'client'); -dbConnect().then(() => console.log('Connected to MongoDB')); +dbConnect().then(() => { + console.log('Connected to MongoDB'); + migrateDb(); +}); app.use(cors()); app.use(express.json()); diff --git a/api/server/routes/ask.js b/api/server/routes/ask.js index 1f18082dd4..ad2a7178c2 100644 --- a/api/server/routes/ask.js +++ b/api/server/routes/ask.js @@ -3,37 +3,100 @@ const crypto = require('crypto'); const router = express.Router(); const askBing = require('./askBing'); const askSydney = require('./askSydney'); -const { - titleConvo, - askClient, - browserClient, - customClient, - detectCode -} = require('../../app/'); -const { getConvo, saveMessage, deleteMessages, saveConvo } = require('../../models'); -const { handleError, sendMessage } = require('./handlers'); +const { titleConvo, askClient, browserClient, customClient } = require('../../app/'); +const { getConvo, saveMessage, getConvoTitle, saveConvo } = require('../../models'); +const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); +const { getMessages } = require('../../models/Message'); router.use('/bing', askBing); router.use('/sydney', askSydney); router.post('/', async (req, res) => { - let { model, text, parentMessageId, conversationId, chatGptLabel, promptPrefix } = req.body; + let { model, text, parentMessageId, conversationId: oldConversationId, ...convo } = req.body; if (text.length === 0) { - return handleError(res, 'Prompt empty or too short'); + return handleError(res, { text: 'Prompt empty or too short' }); } + const conversationId = oldConversationId || crypto.randomUUID(); + const userMessageId = crypto.randomUUID(); - let userMessage = { id: userMessageId, sender: 'User', text }; + const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000'; + let userMessage = { + messageId: userMessageId, + sender: 'User', + text, + parentMessageId: userParentMessageId, + conversationId, + isCreatedByUser: true + }; console.log('ask log', { model, ...userMessage, - parentMessageId, - conversationId, - chatGptLabel, - promptPrefix + ...convo }); + await saveMessage(userMessage); + await saveConvo({ ...userMessage, model, ...convo }); + + return await ask({ + userMessage, + model, + convo, + preSendRequest: true, + req, + res + }); +}); + +router.post('/regenerate', async (req, res) => { + const { model } = req.body; + + const oldUserMessage = await getMessages({ messageId: req.body }); + + if (oldUserMessage) { + const convo = await getConvo(userMessage?.conversationId); + + const userMessageId = crypto.randomUUID(); + + let userMessage = { + ...userMessage, + messageId: userMessageId + }; + + console.log('ask log for regeneration', { + model, + ...userMessage, + ...convo + }); + + return await ask({ + userMessage, + model, + convo, + preSendRequest: false, + req, + res + }); + } else return handleError(res, { text: 'Parent message not found' }); +}); + +const ask = async ({ + userMessage, + overrideParentMessageId = null, + model, + convo, + preSendRequest = true, + req, + res +}) => { + let { + text, + parentMessageId: userParentMessageId, + conversationId, + messageId: userMessageId + } = userMessage; + let client; if (model === 'chatgpt') { @@ -44,15 +107,6 @@ router.post('/', async (req, res) => { client = browserClient; } - if (model === 'chatgptCustom' && !chatGptLabel && conversationId) { - const convo = await getConvo({ conversationId }); - if (convo) { - console.log('found convo for custom gpt', { convo }) - chatGptLabel = convo.chatGptLabel; - promptPrefix = convo.promptPrefix; - } - } - res.writeHead(200, { Connection: 'keep-alive', 'Content-Type': 'text/event-stream', @@ -61,54 +115,27 @@ router.post('/', async (req, res) => { 'X-Accel-Buffering': 'no' }); + if (preSendRequest) sendMessage(res, { message: userMessage, created: true }); + try { - let i = 0; - let tokens = ''; - const progressCallback = async (partial) => { - if (i === 0 && typeof partial === 'object') { - userMessage.parentMessageId = parentMessageId ? parentMessageId : partial.id; - userMessage.conversationId = conversationId ? conversationId : partial.conversationId; - await saveMessage(userMessage); - sendMessage(res, { ...partial, initial: true }); - i++; - } - - if (typeof partial === 'object') { - sendMessage(res, { ...partial, message: true }); - } else { - tokens += partial === text ? '' : partial; - if (tokens.match(/^\n/)) { - tokens = tokens.replace(/^\n/, ''); - } - - if (tokens.includes('[DONE]')) { - tokens = tokens.replace('[DONE]', ''); - } - - // tokens = await detectCode(tokens); - sendMessage(res, { text: tokens, message: true, initial: i === 0 ? true : false }); - i++; - } - }; - + const progressCallback = createOnProgress(); let gptResponse = await client({ text, - progressCallback, + onProgress: progressCallback.call(null, model, { res, text }), convo: { - parentMessageId, - conversationId + parentMessageId: userParentMessageId, + conversationId, + ...convo }, - chatGptLabel, - promptPrefix + ...convo }); console.log('CLIENT RESPONSE', gptResponse); if (!gptResponse.parentMessageId) { gptResponse.text = gptResponse.response; - gptResponse.id = gptResponse.messageId; - gptResponse.parentMessageId = gptResponse.messageId; - userMessage.parentMessageId = parentMessageId ? parentMessageId : gptResponse.messageId; + // gptResponse.id = gptResponse.messageId; + gptResponse.parentMessageId = overrideParentMessageId || userMessageId; userMessage.conversationId = conversationId ? conversationId : gptResponse.conversationId; @@ -121,37 +148,65 @@ router.post('/', async (req, res) => { gptResponse.text.toLowerCase().includes('no response') || gptResponse.text.toLowerCase().includes('no answer') ) { - return handleError(res, 'Prompt empty or too short'); - } - - if (!parentMessageId) { - gptResponse.title = await titleConvo({ - model, - message: text, - response: JSON.stringify(gptResponse.text) + await saveMessage({ + messageId: crypto.randomUUID(), + sender: model, + conversationId, + parentMessageId: overrideParentMessageId || userMessageId, + error: true, + text: 'Prompt empty or too short' }); - } - gptResponse.sender = model === 'chatgptCustom' ? chatGptLabel : model; - gptResponse.final = true; - gptResponse.text = await detectCode(gptResponse.text); - - if (chatGptLabel?.length > 0 && model === 'chatgptCustom') { - gptResponse.chatGptLabel = chatGptLabel; + return handleError(res, { text: 'Prompt empty or too short' }); } - if (promptPrefix?.length > 0 && model === 'chatgptCustom') { - gptResponse.promptPrefix = promptPrefix; + gptResponse.sender = model === 'chatgptCustom' ? convo.chatGptLabel : model; + gptResponse.model = model; + // gptResponse.final = true; + gptResponse.text = await handleText(gptResponse); + + if (convo.chatGptLabel?.length > 0 && model === 'chatgptCustom') { + gptResponse.chatGptLabel = convo.chatGptLabel; } + if (convo.promptPrefix?.length > 0 && model === 'chatgptCustom') { + gptResponse.promptPrefix = convo.promptPrefix; + } + + // override the parentMessageId, for the regeneration. + gptResponse.parentMessageId = overrideParentMessageId || userMessageId; + await saveMessage(gptResponse); await saveConvo(gptResponse); - sendMessage(res, gptResponse); + sendMessage(res, { + title: await getConvoTitle(conversationId), + final: true, + requestMessage: userMessage, + responseMessage: gptResponse + }); res.end(); + + if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { + const title = await titleConvo({ model, text, response: gptResponse }); + + await saveConvo({ + conversationId, + title + }); + } } catch (error) { console.log(error); - await deleteMessages({ id: userMessageId }); - handleError(res, error.message); + // await deleteMessages({ messageId: userMessageId }); + const errorMessage = { + messageId: crypto.randomUUID(), + sender: model, + conversationId, + parentMessageId: overrideParentMessageId || userMessageId, + error: true, + text: error.message + }; + await saveMessage(errorMessage); + handleError(res, errorMessage); } -}); +}; module.exports = router; diff --git a/api/server/routes/askBing.js b/api/server/routes/askBing.js index 76c39f6804..cfbac77185 100644 --- a/api/server/routes/askBing.js +++ b/api/server/routes/askBing.js @@ -1,21 +1,72 @@ const express = require('express'); const crypto = require('crypto'); const router = express.Router(); -const { titleConvo, getCitations, citeText, askBing } = require('../../app/'); -const { saveMessage, deleteMessages, saveConvo } = require('../../models'); -const { handleError, sendMessage } = require('./handlers'); -const citationRegex = /\[\^\d+?\^]/g; +const { titleConvo, askBing } = require('../../app/'); +const { saveMessage, getConvoTitle, saveConvo } = require('../../models'); +const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); router.post('/', async (req, res) => { - const { model, text, ...convo } = req.body; + const { + model, + text, + parentMessageId, + conversationId: oldConversationId, + ...convo + } = req.body; if (text.length === 0) { - return handleError(res, 'Prompt empty or too short'); + return handleError(res, { text: 'Prompt empty or too short' }); } - const userMessageId = crypto.randomUUID(); - let userMessage = { id: userMessageId, sender: 'User', text }; + const conversationId = oldConversationId || crypto.randomUUID(); + const isNewConversation = !oldConversationId; - console.log('ask log', { model, ...userMessage, ...convo }); + const userMessageId = crypto.randomUUID(); + const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000'; + let userMessage = { + messageId: userMessageId, + sender: 'User', + text, + parentMessageId: userParentMessageId, + conversationId, + isCreatedByUser: true + }; + + console.log('ask log', { + model, + ...userMessage, + ...convo + }); + + await saveMessage(userMessage); + await saveConvo({ ...userMessage, model, ...convo }); + + return await ask({ + isNewConversation, + userMessage, + model, + convo, + preSendRequest: true, + req, + res + }); +}); + +const ask = async ({ + isNewConversation, + overrideParentMessageId = null, + userMessage, + model, + convo, + preSendRequest = true, + req, + res +}) => { + let { + text, + parentMessageId: userParentMessageId, + conversationId, + messageId: userMessageId + } = userMessage; res.writeHead(200, { Connection: 'keep-alive', @@ -25,62 +76,91 @@ router.post('/', async (req, res) => { 'X-Accel-Buffering': 'no' }); - try { - let tokens = ''; - const progressCallback = async (partial) => { - tokens += partial === text ? '' : partial; - // tokens = appendCode(tokens); - tokens = citeText(tokens, true); - sendMessage(res, { text: tokens, message: true }); - }; + if (preSendRequest) sendMessage(res, { message: userMessage, created: true }); + try { + const progressCallback = createOnProgress(); let response = await askBing({ text, - progressCallback, - convo + onProgress: progressCallback.call(null, model, { + res, + text, + parentMessageId: overrideParentMessageId || userMessageId + }), + convo: { + ...convo, + parentMessageId: userParentMessageId, + conversationId + } }); - console.log('BING RESPONSE'); + console.log('BING RESPONSE', response); // console.dir(response, { depth: null }); const hasCitations = response.response.match(citationRegex)?.length > 0; userMessage.conversationSignature = convo.conversationSignature || response.conversationSignature; - userMessage.conversationId = convo.conversationId || response.conversationId; + userMessage.conversationId = response.conversationId || conversationId; userMessage.invocationId = response.invocationId; await saveMessage(userMessage); - if (!convo.conversationSignature) { - response.title = await titleConvo({ - model, - message: text, - response: JSON.stringify(response.response) + // Bing API will not use our conversationId at the first time, + // so change the placeholder conversationId to the real one. + // Attition: the api will also create new conversationId while using invalid userMessage.parentMessageId, + // but in this situation, don't change the conversationId, but create new convo. + if (conversationId != userMessage.conversationId && isNewConversation) + await saveConvo({ + conversationId: conversationId, + newConversationId: userMessage.conversationId }); - } + conversationId = userMessage.conversationId; response.text = response.response; delete response.response; - response.id = response.details.messageId; + // response.id = response.details.messageId; response.suggestions = response.details.suggestedResponses && response.details.suggestedResponses.map((s) => s.text); response.sender = model; - response.final = true; + // response.final = true; - const links = getCitations(response); - response.text = - citeText(response) + - (links?.length > 0 && hasCitations ? `\n${links}` : ''); + // override the parentMessageId, for the regeneration. + response.parentMessageId = + overrideParentMessageId || response.parentMessageId || userMessageId; + response.text = await handleText(response, true); await saveMessage(response); - await saveConvo(response); - sendMessage(res, response); + await saveConvo({ ...response, model, chatGptLabel: null, promptPrefix: null, ...convo }); + sendMessage(res, { + title: await getConvoTitle(conversationId), + final: true, + requestMessage: userMessage, + responseMessage: response + }); res.end(); + + if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { + const title = await titleConvo({ model, text, response }); + + await saveConvo({ + conversationId, + title + }); + } } catch (error) { console.log(error); - await deleteMessages({ id: userMessageId }); - handleError(res, error.message); + // await deleteMessages({ messageId: userMessageId }); + const errorMessage = { + messageId: crypto.randomUUID(), + sender: model, + conversationId, + parentMessageId: overrideParentMessageId || userMessageId, + error: true, + text: error.message + }; + await saveMessage(errorMessage); + handleError(res, errorMessage); } -}); +}; module.exports = router; diff --git a/api/server/routes/askSydney.js b/api/server/routes/askSydney.js index 219f9b2437..00f287fa7b 100644 --- a/api/server/routes/askSydney.js +++ b/api/server/routes/askSydney.js @@ -1,21 +1,72 @@ const express = require('express'); const crypto = require('crypto'); const router = express.Router(); -const { titleConvo, getCitations, citeText, askSydney } = require('../../app/'); -const { saveMessage, deleteMessages, saveConvo, getConvoTitle } = require('../../models'); -const { handleError, sendMessage } = require('./handlers'); -const citationRegex = /\[\^\d+?\^]/g; +const { titleConvo, askSydney } = require('../../app/'); +const { saveMessage, saveConvo, getConvoTitle } = require('../../models'); +const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); router.post('/', async (req, res) => { - const { model, text, ...convo } = req.body; + const { + model, + text, + parentMessageId, + conversationId: oldConversationId, + ...convo + } = req.body; if (text.length === 0) { - return handleError(res, 'Prompt empty or too short'); + return handleError(res, { text: 'Prompt empty or too short' }); } - const userMessageId = crypto.randomUUID(); - let userMessage = { id: userMessageId, sender: 'User', text }; + const conversationId = oldConversationId || crypto.randomUUID(); + const isNewConversation = !oldConversationId; - console.log('ask log', { model, ...userMessage, ...convo }); + const userMessageId = crypto.randomUUID(); + const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000'; + let userMessage = { + messageId: userMessageId, + sender: 'User', + text, + parentMessageId: userParentMessageId, + conversationId, + isCreatedByUser: true + }; + + console.log('ask log', { + model, + ...userMessage, + ...convo + }); + + await saveMessage(userMessage); + await saveConvo({ ...userMessage, model, ...convo }); + + return await ask({ + isNewConversation, + userMessage, + model, + convo, + preSendRequest: true, + req, + res + }); +}); + +const ask = async ({ + isNewConversation, + overrideParentMessageId = null, + userMessage, + model, + convo, + preSendRequest = true, + req, + res +}) => { + let { + text, + parentMessageId: userParentMessageId, + conversationId, + messageId: userMessageId + } = userMessage; res.writeHead(200, { Connection: 'keep-alive', @@ -25,41 +76,38 @@ router.post('/', async (req, res) => { 'X-Accel-Buffering': 'no' }); - try { - let tokens = ''; - const progressCallback = async (partial) => { - tokens += partial === text ? '' : partial; - // tokens = appendCode(tokens); - tokens = citeText(tokens, true); - sendMessage(res, { text: tokens, message: true }); - }; + if (preSendRequest) sendMessage(res, { message: userMessage, created: true }); + try { + const progressCallback = createOnProgress(); let response = await askSydney({ text, - progressCallback, - convo + onProgress: progressCallback.call(null, model, { + res, + text, + parentMessageId: overrideParentMessageId || userMessageId + }), + convo: { + parentMessageId: userParentMessageId, + conversationId, + ...convo + } }); - console.log('SYDNEY RESPONSE'); - console.log(response.response); + console.log('SYDNEY RESPONSE', response); // console.dir(response, { depth: null }); - const hasCitations = response.response.match(citationRegex)?.length > 0; + + userMessage.conversationSignature = + convo.conversationSignature || response.conversationSignature; + userMessage.conversationId = response.conversationId || conversationId; + userMessage.invocationId = response.invocationId; + // Unlike gpt and bing, Sydney will never accept our given userMessage.messageId, it will generate its own one. + await saveMessage(userMessage); // Save sydney response - response.id = response.messageId; - // response.parentMessageId = convo.parentMessageId ? convo.parentMessageId : response.messageId; - response.parentMessageId = response.messageId; + // response.id = response.messageId; response.invocationId = convo.invocationId ? convo.invocationId + 1 : 1; - response.title = convo.jailbreakConversationId - ? await getConvoTitle(convo.conversationId) - : await titleConvo({ - model, - message: text, - response: JSON.stringify(response.response) - }); - response.conversationId = convo.conversationId - ? convo.conversationId - : crypto.randomUUID(); + response.conversationId = conversationId ? conversationId : crypto.randomUUID(); response.conversationSignature = convo.conversationSignature ? convo.conversationSignature : crypto.randomUUID(); @@ -69,28 +117,61 @@ router.post('/', async (req, res) => { response.details.suggestedResponses && response.details.suggestedResponses.map((s) => s.text); response.sender = model; - response.final = true; + // response.final = true; - const links = getCitations(response); - response.text = - citeText(response) + - (links?.length > 0 && hasCitations ? `\n${links}` : ''); + // override the parentMessageId, for the regeneration. + response.parentMessageId = + overrideParentMessageId || response.parentMessageId || userMessageId; // Save user message - userMessage.conversationId = response.conversationId; - userMessage.parentMessageId = response.parentMessageId; + userMessage.conversationId = response.conversationId || conversationId; await saveMessage(userMessage); + // Bing API will not use our conversationId at the first time, + // so change the placeholder conversationId to the real one. + // Attition: the api will also create new conversationId while using invalid userMessage.parentMessageId, + // but in this situation, don't change the conversationId, but create new convo. + if (conversationId != userMessage.conversationId && isNewConversation) + await saveConvo({ + conversationId: conversationId, + newConversationId: userMessage.conversationId + }); + conversationId = userMessage.conversationId; + + response.text = await handleText(response, true); // Save sydney response & convo, then send await saveMessage(response); - await saveConvo(response); - sendMessage(res, response); + await saveConvo({ ...response, model, chatGptLabel: null, promptPrefix: null, ...convo }); + sendMessage(res, { + title: await getConvoTitle(conversationId), + final: true, + requestMessage: userMessage, + responseMessage: response + }); res.end(); + + if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { + const title = await titleConvo({ model, text, response }); + + await saveConvo({ + conversationId, + title + }); + } } catch (error) { console.log(error); - await deleteMessages({ id: userMessageId }); - handleError(res, error.message); + // await deleteMessages({ messageId: userMessageId }); + const errorMessage = { + messageId: crypto.randomUUID(), + sender: model, + conversationId, + parentMessageId: overrideParentMessageId || userMessageId, + error: true, + text: error.message + }; + await saveMessage(errorMessage); + handleError(res, errorMessage); } -}); +}; module.exports = router; diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 4b9320873f..99fb824708 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -1,10 +1,10 @@ const express = require('express'); const router = express.Router(); -const { getConvos, deleteConvos, updateConvo } = require('../../models/Conversation'); +const { getConvosByPage, deleteConvos, updateConvo } = require('../../models/Conversation'); router.get('/', async (req, res) => { const pageNumber = req.query.pageNumber || 1; - res.status(200).send(await getConvos(pageNumber)); + res.status(200).send(await getConvosByPage(pageNumber)); }); router.post('/clear', async (req, res) => { diff --git a/api/server/routes/handlers.js b/api/server/routes/handlers.js index edd64e6184..3ba4e81243 100644 --- a/api/server/routes/handlers.js +++ b/api/server/routes/handlers.js @@ -1,5 +1,11 @@ -const handleError = (res, errorMessage) => { - res.status(500).write(`event: error\ndata: ${errorMessage}`); +const _ = require('lodash'); +const sanitizeHtml = require('sanitize-html'); +const citationRegex = /\[\^\d+?\^]/g; +const { getCitations, citeText, detectCode } = require('../../app/'); +// const htmlTagRegex = /(<\/?\s*[a-zA-Z]*\s*(?:\s+[a-zA-Z]+\s*=\s*(?:"[^"]*"|'[^']*'))*\s*(?:\/?)>|<\s*[a-zA-Z]+\s*(?:\s+[a-zA-Z]+\s*=\s*(?:"[^"]*"|'[^']*'))*\s*(?:\/?>|<\/?>))/g; + +const handleError = (res, message) => { + res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`); res.end(); }; @@ -10,4 +16,65 @@ const sendMessage = (res, message) => { res.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`); }; -module.exports = { handleError, sendMessage }; +const createOnProgress = () => { + let i = 0; + let tokens = ''; + + const progressCallback = async (partial, { res, text, bing = false, ...rest }) => { + tokens += partial === text ? '' : partial; + tokens = tokens.replaceAll('[DONE]', ''); + + if (tokens.match(/^\n/)) { + tokens = tokens.replace(/^\n/, ''); + } + + // const htmlTags = tokens.match(htmlTagRegex); + // if (tokens.includes('```') && htmlTags && htmlTags.length > 0) { + // htmlTags.forEach((tag) => { + // const sanitizedTag = sanitizeHtml(tag); + // tokens = tokens.replaceAll(tag, sanitizedTag); + // }); + // } + + if (bing) { + tokens = citeText(tokens, true); + } + + sendMessage(res, { text: tokens, message: true, initial: i === 0, ...rest }); + i++; + }; + + const onProgress = (model, opts) => { + const bingModels = new Set(['bingai', 'sydney']); + return _.partialRight(progressCallback, { ...opts, bing: bingModels.has(model) }); + }; + + return onProgress; +}; + +const handleText = async (response, bing = false) => { + let { text } = response; + text = await detectCode(text); + response.text = text; + + if (bing) { + // const hasCitations = response.response.match(citationRegex)?.length > 0; + const links = getCitations(response); + if (response.text.match(citationRegex)?.length > 0) { + text = citeText(response); + } + text += links?.length > 0 ? `\n${links}` : ''; + } + + // const htmlTags = text.match(htmlTagRegex); + // if (text.includes('```') && htmlTags && htmlTags.length > 0) { + // htmlTags.forEach((tag) => { + // const sanitizedTag = sanitizeHtml(tag); + // text = text.replaceAll(tag, sanitizedTag); + // }); + // } + + return text; +}; + +module.exports = { handleError, sendMessage, createOnProgress, handleText }; diff --git a/client/package-lock.json b/client/package-lock.json index fbfcb2b345..d5e3024751 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11125,9 +11125,9 @@ } }, "node_modules/webpack": { - "version": "5.75.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", - "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -19431,9 +19431,9 @@ } }, "webpack": { - "version": "5.75.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", - "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", diff --git a/client/src/App.jsx b/client/src/App.jsx index 9bab1f5939..5f1446fd0b 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -8,7 +8,7 @@ import useDocumentTitle from '~/hooks/useDocumentTitle'; import { useSelector } from 'react-redux'; const App = () => { - const { messages } = useSelector((state) => state.messages); + const { messages, messageTree } = useSelector((state) => state.messages); const { title } = useSelector((state) => state.convo); const { conversationId } = useSelector((state) => state.convo); const [ navVisible, setNavVisible ]= useState(false) @@ -25,6 +25,7 @@ const App = () => { ) : ( )} diff --git a/client/src/components/Conversations/Conversation.jsx b/client/src/components/Conversations/Conversation.jsx index 34e384f624..9e5c3dce6d 100644 --- a/client/src/components/Conversations/Conversation.jsx +++ b/client/src/components/Conversations/Conversation.jsx @@ -8,12 +8,14 @@ import { setMessages, setEmptyMessage } from '~/store/messageSlice'; import { setText } from '~/store/textSlice'; import manualSWR from '~/utils/fetchers'; import ConvoIcon from '../svg/ConvoIcon'; +import { refreshConversation } from '../../store/convoSlice'; export default function Conversation({ id, + model, parentMessageId, conversationId, - title = 'New conversation', + title, chatGptLabel = null, promptPrefix = null, bingData, @@ -75,17 +77,19 @@ export default function Conversation({ if (chatGptLabel) { dispatch(setModel('chatgptCustom')); + dispatch(setCustomModel(chatGptLabel.toLowerCase())); } else { - dispatch(setModel(data[1].sender)); - } - - if (modelMap[data[1].sender.toLowerCase()]) { - console.log('sender', data[1].sender); - dispatch(setCustomModel(data[1].sender.toLowerCase())); - } else { + dispatch(setModel(model)); dispatch(setCustomModel(null)); } + // if (modelMap[chatGptLabel.toLowerCase()]) { + // console.log('custom model', chatGptLabel); + // dispatch(setCustomModel(chatGptLabel.toLowerCase())); + // } else { + // dispatch(setCustomModel(null)); + // } + dispatch(setMessages(data)); dispatch(setCustomGpt(convo)); dispatch(setText('')); @@ -94,6 +98,7 @@ export default function Conversation({ const renameHandler = (e) => { e.preventDefault(); + setTitleInput(title); setRenaming(true); setTimeout(() => { inputRef.current.focus(); @@ -111,7 +116,10 @@ export default function Conversation({ if (titleInput === title) { return; } - rename.trigger({ conversationId, title: titleInput }); + rename.trigger({ conversationId, title: titleInput }) + .then(() => { + dispatch(refreshConversation()) + }); }; const handleKeyDown = (e) => { @@ -148,7 +156,7 @@ export default function Conversation({ onKeyDown={handleKeyDown} /> ) : ( - titleInput + title )} {conversationId === id ? ( diff --git a/client/src/components/Conversations/index.jsx b/client/src/components/Conversations/index.jsx index 6fcac5e27f..4142cb8ed1 100644 --- a/client/src/components/Conversations/index.jsx +++ b/client/src/components/Conversations/index.jsx @@ -1,10 +1,10 @@ import React from 'react'; import Conversation from './Conversation'; -export default function Conversations({ conversations, conversationId, showMore }) { - const clickHandler = async (e) => { +export default function Conversations({ conversations, conversationId, pageNumber, pages, nextPage, previousPage, moveToTop }) { + const clickHandler = (func) => async (e) => { e.preventDefault(); - await showMore(); + await func(); }; return ( @@ -26,24 +26,36 @@ export default function Conversations({ conversations, conversationId, showMore ); })} - {conversations?.length >= 12 && ( - - )} +
+ + + {pageNumber} / {pages} + + +
); } diff --git a/client/src/components/Main/SubmitButton.jsx b/client/src/components/Main/SubmitButton.jsx index 802d1a88c3..ed570f5c76 100644 --- a/client/src/components/Main/SubmitButton.jsx +++ b/client/src/components/Main/SubmitButton.jsx @@ -10,7 +10,7 @@ export default function SubmitButton({ submitMessage }) { if (isSubmitting) { return ( - - )} + {(visible&&enabled)?( + <> + + + ):null} {/* */} diff --git a/client/src/components/Messages/Message.jsx b/client/src/components/Messages/Message.jsx index 37f7a5c63b..622b907454 100644 --- a/client/src/components/Messages/Message.jsx +++ b/client/src/components/Messages/Message.jsx @@ -1,32 +1,60 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import TextWrapper from './TextWrapper'; -import { useSelector } from 'react-redux'; -import GPTIcon from '../svg/GPTIcon'; -import BingIcon from '../svg/BingIcon'; +import MultiMessage from './MultiMessage'; +import { useSelector, useDispatch } from 'react-redux'; import HoverButtons from './HoverButtons'; -import Spinner from '../svg/Spinner'; +import SiblingSwitch from './SiblingSwitch'; +import { setError } from '~/store/convoSlice'; +import { setMessages } from '~/store/messageSlice'; +import { setSubmitState, setSubmission } from '~/store/submitSlice'; +import { setText } from '~/store/textSlice'; +import { setConversation } from '../../store/convoSlice'; +import { getIconOfModel } from '../../utils'; export default function Message({ - sender, - text, - last = false, - error = false, - scrollToBottom + message, + messages, + scrollToBottom, + currentEditId, + setCurrentEditId, + siblingIdx, + siblingCount, + setSiblingIdx }) { - const { isSubmitting } = useSelector((state) => state.submit); + const { isSubmitting, model, chatGptLabel, promptPrefix } = useSelector( + (state) => state.submit + ); const [abortScroll, setAbort] = useState(false); - const notUser = sender.toLowerCase() !== 'user'; - const blinker = isSubmitting && last && notUser; + const { sender, text, isCreatedByUser, error, submitting } = message; + const textEditor = useRef(null); + const convo = useSelector((state) => state.convo); + const { initial } = useSelector((state) => state.models); + const { error: convoError } = convo; + const last = !message?.children?.length; + const edit = message.messageId == currentEditId; + const dispatch = useDispatch(); + + // const notUser = !isCreatedByUser; // sender.toLowerCase() !== 'user'; + const blinker = submitting && isSubmitting && last && !isCreatedByUser; + const generateCursor = useCallback(() => { + if (!blinker) { + return ''; + } + + return ; + }, [blinker]); useEffect(() => { if (blinker && !abortScroll) { scrollToBottom(); } }, [isSubmitting, text, blinker, scrollToBottom, abortScroll]); - - if (sender === '') { - return ; - } + + useEffect(() => { + if (last) dispatch(setConversation({ parentMessageId: message?.messageId })); + }, [last]); + + const enterEdit = (cancel) => setCurrentEditId(cancel ? -1 : message.messageId); const handleWheel = () => { if (blinker) { @@ -41,79 +69,164 @@ export default function Message({ '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' }; - const bgColors = { - chatgpt: 'rgb(16, 163, 127)', - chatgptBrowser: 'rgb(25, 207, 207)', - bingai: '', - sydney: '' - }; + const icon = getIconOfModel({ + sender, + isCreatedByUser, + model, + chatGptLabel, + promptPrefix, + error + }); - const isBing = sender === 'bingai' || sender === 'sydney'; - - let icon = `${sender}:`; - let backgroundColor = bgColors[sender]; - - if (notUser) { + if (!isCreatedByUser) props.className = '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 ((notUser && backgroundColor) || isBing) { - icon = ( -
- {isBing ? : } - {error && ( - - ! - - )} -
- ); - } + // const wrapText = (text) => ; - const wrapText = (text) => ; + const resubmitMessage = () => { + const text = textEditor.current.innerText; + + if (convoError) { + dispatch(setError(false)); + } + + if (!!isSubmitting || text.trim() === '') { + return; + } + + // this is not a real messageId, it is used as placeholder before real messageId returned + const fakeMessageId = crypto.randomUUID(); + const isCustomModel = model === 'chatgptCustom' || !initial[model]; + const currentMsg = { + sender: 'User', + text: text.trim(), + current: true, + isCreatedByUser: true, + parentMessageId: message?.parentMessageId, + conversationId: message?.conversationId, + messageId: fakeMessageId + }; + const sender = model === 'chatgptCustom' ? chatGptLabel : model; + + const initialResponse = { + sender, + text: '', + parentMessageId: fakeMessageId, + submitting: true + }; + + dispatch(setSubmitState(true)); + dispatch(setMessages([...messages, currentMsg, initialResponse])); + dispatch(setText('')); + + const submission = { + isCustomModel, + message: { + ...currentMsg, + model, + chatGptLabel, + promptPrefix + }, + messages: messages, + currentMsg, + initialResponse, + sender + }; + console.log('User Input:', currentMsg?.text); + // handleSubmit(submission); + dispatch(setSubmission(submission)); + + setSiblingIdx(siblingCount - 1); + enterEdit(true); + }; return ( -
-
- - {typeof icon === 'string' && icon.match(/[^\u0000-\u007F]+/) ? ( - {icon} - ) : ( - icon - )} - -
-
- {error ? ( -
-
- {text} -
-
+ <> +
+
+
+ {typeof icon === 'string' && icon.match(/[^\u0000-\u007F]+/) ? ( + {icon} ) : ( -
- {/*
*/} -
- {notUser ? wrapText(text) : text} - {blinker && } -
-
+ icon )} + +
+
+
+ {error ? ( +
+
+ {`An error occurred. Please try again in a few moments.\n\nError message: ${text}`} +
+
+ ) : edit ? ( +
+ {/*
*/} + +
+ {text} +
+
+ + +
+
+ ) : ( +
+ {/*
*/} +
+ {!isCreatedByUser ? ( + + ) : ( + text + )} +
+
+ )} +
+ enterEdit()} + />
-
-
+ + ); } diff --git a/client/src/components/Messages/MultiMessage.jsx b/client/src/components/Messages/MultiMessage.jsx new file mode 100644 index 0000000000..24ab761eb6 --- /dev/null +++ b/client/src/components/Messages/MultiMessage.jsx @@ -0,0 +1,40 @@ +import React, { useState } from 'react'; +import Message from './Message'; + +export default function MultiMessage({ + messageList, + messages, + scrollToBottom, + currentEditId, + setCurrentEditId +}) { + const [siblingIdx, setSiblingIdx] = useState(0); + + const setSiblingIdxRev = (value) => { + setSiblingIdx(messageList?.length - value - 1); + }; + + // if (!messageList?.length) return null; + if (!(messageList && messageList.length)) { + return null; + } + + if (siblingIdx >= messageList?.length) { + setSiblingIdx(0); + return null; + } + + return ( + + ); +} diff --git a/client/src/components/Messages/SiblingSwitch.jsx b/client/src/components/Messages/SiblingSwitch.jsx new file mode 100644 index 0000000000..2d746210b1 --- /dev/null +++ b/client/src/components/Messages/SiblingSwitch.jsx @@ -0,0 +1,26 @@ +import React from 'react'; + +export default function SiblingSwitch({ + siblingIdx, + siblingCount, + setSiblingIdx +}) { + const previous = () => { + setSiblingIdx(siblingIdx - 1); + } + + const next = () => { + setSiblingIdx(siblingIdx + 1); + } + return siblingCount > 1 ? ( +
+ + {siblingIdx + 1}/{siblingCount} + +
+ ):null; +} diff --git a/client/src/components/Messages/TextWrapper.jsx b/client/src/components/Messages/TextWrapper.jsx index af38b56a5c..3cfd0a1b37 100644 --- a/client/src/components/Messages/TextWrapper.jsx +++ b/client/src/components/Messages/TextWrapper.jsx @@ -46,8 +46,9 @@ const inLineWrap = (parts) => { }); }; -export default function TextWrapper({ text }) { +export default function TextWrapper({ text, generateCursor }) { let embedTest = false; + let result = null; // to match unenclosed code blocks if (text.match(/```/g)?.length === 1) { @@ -137,13 +138,23 @@ export default function TextWrapper({ text }) { } }); - return <>{codeParts}; // return the wrapped text + // return <>{codeParts}; // return the wrapped text + result = <>{codeParts}; } else if (text.match(markupRegex)) { // map over the parts and wrap any text between tildes with tags const parts = text.split(markupRegex); const codeParts = inLineWrap(parts); - return <>{codeParts}; // return the wrapped text + // return <>{codeParts}; // return the wrapped text + result = <>{codeParts}; } else { - return {text}; + // return {text}; + result = {text}; } + + return ( + <> + {result} + {generateCursor()} + + ); } diff --git a/client/src/components/Messages/index.jsx b/client/src/components/Messages/index.jsx index c6457d6c62..611a0231d3 100644 --- a/client/src/components/Messages/index.jsx +++ b/client/src/components/Messages/index.jsx @@ -1,9 +1,13 @@ -import React, { useEffect, useState, useRef } from 'react'; +import React, { useEffect, useState, useRef, useMemo } from 'react'; +import Spinner from '../svg/Spinner'; import { CSSTransition } from 'react-transition-group'; import ScrollToBottom from './ScrollToBottom'; -import Message from './Message'; +import MultiMessage from './MultiMessage'; +import { useSelector } from 'react-redux'; -const Messages = ({ messages }) => { +const Messages = ({ messages, messageTree }) => { + const [currentEditId, setCurrentEditId] = useState(-1); + const { conversationId } = useSelector((state) => state.convo); const [showScrollButton, setShowScrollButton] = useState(false); const scrollableRef = useRef(null); const messagesEndRef = useRef(null); @@ -55,30 +59,33 @@ const Messages = ({ messages }) => { onScroll={debouncedHandleScroll} > {/*
*/} -
-
- {messages.map((message, i) => ( - - ))} - - {() => showScrollButton && } - - +
+
+ {messageTree.length === 0 ? ( + + ) : ( + <> + + + {() => showScrollButton && } + + + )}
@@ -88,4 +95,4 @@ const Messages = ({ messages }) => { ); }; -export default Messages; +export default React.memo(Messages); diff --git a/client/src/components/Models/MenuItems.jsx b/client/src/components/Models/MenuItems.jsx index a119bee381..d0a277ea9e 100644 --- a/client/src/components/Models/MenuItems.jsx +++ b/client/src/components/Models/MenuItems.jsx @@ -10,7 +10,10 @@ export default function MenuItems({ models, onSelect }) { id={modelItem._id} modelName={modelItem.name} value={modelItem.value} + model={modelItem.model || 'chatgptCustom'} onSelect={onSelect} + chatGptLabel={modelItem.chatGptLabel} + promptPrefix={modelItem.promptPrefix} /> ))} diff --git a/client/src/components/Models/ModelItem.jsx b/client/src/components/Models/ModelItem.jsx index c3df204351..163c4206fd 100644 --- a/client/src/components/Models/ModelItem.jsx +++ b/client/src/components/Models/ModelItem.jsx @@ -7,8 +7,9 @@ import { DialogTrigger } from '../ui/Dialog.tsx'; import RenameButton from '../Conversations/RenameButton'; import TrashIcon from '../svg/TrashIcon'; import manualSWR from '~/utils/fetchers'; +import { getIconOfModel } from '../../utils'; -export default function ModelItem({ modelName, value, onSelect, id }) { +export default function ModelItem({ modelName, value, model, onSelect, id, chatGptLabel, promptPrefix }) { const dispatch = useDispatch(); const { customModel } = useSelector((state) => state.submit); const { initial } = useSelector((state) => state.models); @@ -27,6 +28,8 @@ export default function ModelItem({ modelName, value, onSelect, id }) { dispatch(setModels(fetchedModels)); }); + const icon = getIconOfModel({ size: 16, sender: modelName, isCreatedByUser: false, model, chatGptLabel, promptPrefix, error: false, className: "mr-2" }); + if (value === 'chatgptCustom') { return ( @@ -34,6 +37,7 @@ export default function ModelItem({ modelName, value, onSelect, id }) { value={value} className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800" > + {icon} {modelName} $ @@ -47,6 +51,7 @@ export default function ModelItem({ modelName, value, onSelect, id }) { value={value} className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800" > + {icon} {modelName} {value === 'chatgpt' && $} @@ -122,6 +127,9 @@ export default function ModelItem({ modelName, value, onSelect, id }) { )} + + {icon} + {renaming === true ? ( { mutate(); - const lastSelected = JSON.parse(localStorage.getItem('model')); - if (lastSelected && lastSelected !== 'chatgptCustom' && initial[lastSelected]) { - dispatch(setModel(lastSelected)); + try { + const lastSelected = JSON.parse(localStorage.getItem('model')); + + if (lastSelected === 'chatgptCustom') { + return; + } else if (initial[lastSelected]) { + dispatch(setModel(lastSelected)); + } + } catch (err) { + console.log(err); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -56,32 +65,32 @@ export default function ModelMenu() { localStorage.setItem('model', JSON.stringify(model)); }, [model]); - const onChange = (value, custom = false) => { - // if (custom) { - // mutate(); - // } + const onChange = (value) => { if (!value) { return; + } else if (value === model) { + return; } else if (value === 'chatgptCustom') { - // dispatch(setMessages([])); + // return; } else if (initial[value]) { dispatch(setModel(value)); dispatch(setDisabled(false)); dispatch(setCustomModel(null)); + dispatch(setCustomGpt({ chatGptLabel: null, promptPrefix: null })); } else if (!initial[value]) { const chatGptLabel = modelMap[value]?.chatGptLabel; const promptPrefix = modelMap[value]?.promptPrefix; dispatch(setCustomGpt({ chatGptLabel, promptPrefix })); dispatch(setModel('chatgptCustom')); dispatch(setCustomModel(value)); - // if (custom) { - // setMenuOpen((prevOpen) => !prevOpen); - // } + setMenuOpen(false); } else if (!modelMap[value]) { dispatch(setCustomModel(null)); } // Set new conversation + dispatch(setText('')); + dispatch(setMessages([])); dispatch(setNewConvo()); dispatch(setSubmission({})); }; @@ -148,7 +157,7 @@ export default function ModelMenu() { {icon} - + event.preventDefault()}> Select a Model state.convo); + const { conversationId, convos, title } = useSelector((state) => state.convo); const toggleNavVisible = () => { setNavVisible((prev) => { @@ -21,8 +22,6 @@ export default function MobileNav({ setNavVisible }) { dispatch(setSubmission({})); } - const title = convos?.find(element => element?.conversationId == conversationId)?.title || 'New Chat'; - return (
-

{title}

+

{title || 'New Chat'}

-
-
+
); } diff --git a/client/src/components/svg/BingIcon.jsx b/client/src/components/svg/BingIcon.jsx index 6da92e462e..6dd9c346fe 100644 --- a/client/src/components/svg/BingIcon.jsx +++ b/client/src/components/svg/BingIcon.jsx @@ -1,10 +1,10 @@ import React from 'react'; -export default function BingIcon() { +export default function BingIcon({ size=25 }) { return ( { + state.refreshConvoHint = state.refreshConvoHint + 1; + }, setConversation: (state, action) => { return { ...state, ...action.payload }; }, setError: (state, action) => { state.error = action.payload; }, - incrementPage: (state) => { + increasePage: (state) => { state.pageNumber = state.pageNumber + 1; }, + decreasePage: (state) => { + state.pageNumber = state.pageNumber - 1; + }, + setPage: (state, action) => { + state.pageNumber = action.payload; + }, setNewConvo: (state) => { state.error = false; - state.title = 'New Chat'; + state.title = 'ChatGPT Clone'; state.conversationId = null; state.parentMessageId = null; state.jailbreakConversationId = null; @@ -41,16 +52,15 @@ const currentSlice = createSlice({ state.chatGptLabel = null; state.promptPrefix = null; state.convosLoading = false; - state.pageNumber = 1; }, setConvos: (state, action) => { - const newConvos = action.payload.filter((convo) => { - return !state.convos.some((c) => c.conversationId === convo.conversationId); - }); - state.convos = [...state.convos, ...newConvos].sort( - (a, b) => new Date(b.created) - new Date(a.created) + state.convos = action.payload.sort( + (a, b) => new Date(b.createdAt) - new Date(a.createdAt) ); }, + setPages: (state, action) => { + state.pages = action.payload; + }, removeConvo: (state, action) => { state.convos = state.convos.filter((convo) => convo.conversationId !== action.payload); }, @@ -60,7 +70,7 @@ const currentSlice = createSlice({ } }); -export const { setConversation, setConvos, setNewConvo, setError, incrementPage, removeConvo, removeAll } = +export const { refreshConversation, setConversation, setPages, setConvos, setNewConvo, setError, increasePage, decreasePage, setPage, removeConvo, removeAll } = currentSlice.actions; export default currentSlice.reducer; diff --git a/client/src/store/messageSlice.js b/client/src/store/messageSlice.js index c93f70048e..955d0c98f8 100644 --- a/client/src/store/messageSlice.js +++ b/client/src/store/messageSlice.js @@ -1,7 +1,9 @@ import { createSlice } from '@reduxjs/toolkit'; +import buildTree from '~/utils/buildTree'; const initialState = { messages: [], + messageTree: [] }; const currentSlice = createSlice({ @@ -10,11 +12,12 @@ const currentSlice = createSlice({ reducers: { setMessages: (state, action) => { state.messages = action.payload; + state.messageTree = buildTree(action.payload); }, setEmptyMessage: (state) => { state.messages = [ { - id: '1', + messageId: '1', conversationId: '1', parentMessageId: '1', sender: '', diff --git a/client/src/store/modelSlice.js b/client/src/store/modelSlice.js index bc34673bfc..7caefdc548 100644 --- a/client/src/store/modelSlice.js +++ b/client/src/store/modelSlice.js @@ -5,27 +5,32 @@ const initialState = { { _id: '0', name: 'ChatGPT', - value: 'chatgpt' + value: 'chatgpt', + model: 'chatgpt' }, { _id: '1', name: 'CustomGPT', - value: 'chatgptCustom' + value: 'chatgptCustom', + model: 'chatgptCustom' }, { _id: '2', name: 'BingAI', - value: 'bingai' + value: 'bingai', + model: 'bingai' }, { _id: '3', name: 'Sydney', - value: 'sydney' + value: 'sydney', + model: 'sydney' }, { _id: '4', name: 'ChatGPT', - value: 'chatgptBrowser' + value: 'chatgptBrowser', + model: 'chatgptBrowser' }, ], modelMap: {}, @@ -45,7 +50,8 @@ const currentSlice = createSlice({ models.slice(initialState.models.length).forEach((modelItem) => { modelMap[modelItem.value] = { chatGptLabel: modelItem.chatGptLabel, - promptPrefix: modelItem.promptPrefix + promptPrefix: modelItem.promptPrefix, + model: 'chatgptCustom' }; }); diff --git a/client/src/store/submitSlice.js b/client/src/store/submitSlice.js index a1b4afee34..a965c29335 100644 --- a/client/src/store/submitSlice.js +++ b/client/src/store/submitSlice.js @@ -6,8 +6,8 @@ const initialState = { stopStream: false, disabled: false, model: 'chatgpt', - promptPrefix: '', - chatGptLabel: '', + promptPrefix: null, + chatGptLabel: null, customModel: null, }; @@ -34,6 +34,7 @@ const currentSlice = createSlice({ state.model = action.payload; }, setCustomGpt: (state, action) => { + console.log('setCustomGpt', action.payload); state.promptPrefix = action.payload.promptPrefix; state.chatGptLabel = action.payload.chatGptLabel; }, diff --git a/client/src/style.css b/client/src/style.css index 6b17c388e2..30fae19b40 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -1251,7 +1251,6 @@ html { vertical-align: baseline; } - /* .result-streaming>:not(ol):not(ul):not(pre):last-child:after, .result-streaming>ol:last-child li:last-child:after, .result-streaming>pre:last-child code:after, @@ -1876,3 +1875,6 @@ button.scroll-convo { background-color:hsla(0,0%,100%,.4) } } +.hidden-visibility { + visibility: hidden; +} \ No newline at end of file diff --git a/client/src/utils/buildTree.js b/client/src/utils/buildTree.js new file mode 100644 index 0000000000..a030509bca --- /dev/null +++ b/client/src/utils/buildTree.js @@ -0,0 +1,17 @@ +export default function buildTree(messages) { + let messageMap = {}; + let rootMessages = []; + + // Traverse the messages array and store each element in messageMap. + messages.forEach(message => { + messageMap[message.messageId] = {...message, children: []}; + + const parentMessage = messageMap[message.parentMessageId]; + if (parentMessage) + parentMessage.children.push(messageMap[message.messageId]); + else + rootMessages.push(messageMap[message.messageId]); + }); + + return rootMessages; +} \ No newline at end of file diff --git a/client/src/utils/createPayload.js b/client/src/utils/createPayload.js new file mode 100644 index 0000000000..fd195e43e1 --- /dev/null +++ b/client/src/utils/createPayload.js @@ -0,0 +1,31 @@ +export default function createPayload({ convo, message }) { + const endpoint = `/api/ask`; + let payload = { ...message }; + const { model } = message; + + if (!payload.conversationId) + if (convo?.conversationId && convo?.parentMessageId) { + payload = { + ...payload, + conversationId: convo.conversationId, + parentMessageId: convo.parentMessageId || '00000000-0000-0000-0000-000000000000' + }; + } + + const isBing = model === 'bingai' || model === 'sydney'; + if (isBing && convo?.conversationId) { + payload = { + ...payload, + jailbreakConversationId: convo.jailbreakConversationId, + conversationId: convo.conversationId, + conversationSignature: convo.conversationSignature, + clientId: convo.clientId, + invocationId: convo.invocationId + }; + } + + let server = endpoint; + server = model === 'bingai' ? server + '/bing' : server; + server = model === 'sydney' ? server + '/sydney' : server; + return { server, payload }; +}; diff --git a/client/src/utils/fetchers.js b/client/src/utils/fetchers.js index 9faa211d4e..d9cfa54116 100644 --- a/client/src/utils/fetchers.js +++ b/client/src/utils/fetchers.js @@ -9,14 +9,14 @@ const postRequest = async (url, { arg }) => { return await axios.post(url, { arg }); }; -export const swr = (path, successCallback) => { - const options = {}; +export const swr = (path, successCallback, options) => { + const _options = {...options}; if (successCallback) { - options.onSuccess = successCallback; + _options.onSuccess = successCallback; } - return useSWR(path, fetcher, options); + return useSWR(path, fetcher, _options); } export default function manualSWR(path, type, successCallback) { diff --git a/client/src/utils/index.js b/client/src/utils/index.js index 67428a7e94..a48f2ff3c8 100644 --- a/client/src/utils/index.js +++ b/client/src/utils/index.js @@ -1,5 +1,8 @@ import { clsx } from 'clsx'; +import React from 'react'; import { twMerge } from 'tailwind-merge'; +import GPTIcon from '../components/svg/GPTIcon'; +import BingIcon from '../components/svg/BingIcon'; export function cn(...inputs) { return twMerge(clsx(inputs)); @@ -42,3 +45,56 @@ export const wrapperRegex = { languageMatch: /^```(\w+)/, newLineMatch: /^```(\n+)/ }; + +export const getIconOfModel = ({ size=30, sender, isCreatedByUser, model, chatGptLabel, error, ...props }) => { + const bgColors = { + chatgpt: 'rgb(16, 163, 127)', + chatgptBrowser: 'rgb(25, 207, 207)', + bingai: 'transparent', + sydney: 'radial-gradient(circle at 90% 110%, #F0F0FA, #D0E0F9)', + chatgptCustom: 'rgb(0, 163, 255)', + }; + + if (isCreatedByUser) + return ( +
+ User +
+ ) + else if (!isCreatedByUser) { + // TODO: use model from convo, rather than submit + // const { model, chatGptLabel, promptPrefix } = convo; + let background = bgColors[model]; + const isBing = model === 'bingai' || model === 'sydney'; + + return ( +
+ {isBing ? : } + {error && ( + + ! + + )} +
+ ); + } else + return ( +
+ {chatGptLabel} +
+ ) +} diff --git a/client/src/utils/resetConvo.js b/client/src/utils/resetConvo.js new file mode 100644 index 0000000000..8ce01fda57 --- /dev/null +++ b/client/src/utils/resetConvo.js @@ -0,0 +1,22 @@ +export default function resetConvo(messages, sender) { + if (messages.length === 0) { + return false; + } + let modelMessages = messages.filter((message) => !message.isCreatedByUser); + let lastModel = modelMessages[modelMessages.length - 1].sender; + if (lastModel !== sender) { + console.log( + 'Model change! Reseting convo. Original messages: ', + messages, + 'filtered messages: ', + modelMessages, + 'last model: ', + lastModel, + 'sender: ', + sender + ); + return true; + } + + return false; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..bd60daac40 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,293 @@ +{ + "name": "chatgpt-clone", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "sanitize-html": "^2.10.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", + "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", + "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.1" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", + "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "entities": "^4.3.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/sanitize-html": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.10.0.tgz", + "integrity": "sha512-JqdovUd81dG4k87vZt6uA6YhDfWkUGruUu/aPmXLxXi45gZExnt9Bnw/qeQU8oGf82vPyaE0vO4aH0PbobB9JQ==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + } + }, + "dependencies": { + "deepmerge": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", + "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==" + }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", + "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.1" + } + }, + "entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "htmlparser2": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", + "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "entities": "^4.3.0" + } + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "sanitize-html": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.10.0.tgz", + "integrity": "sha512-JqdovUd81dG4k87vZt6uA6YhDfWkUGruUu/aPmXLxXi45gZExnt9Bnw/qeQU8oGf82vPyaE0vO4aH0PbobB9JQ==", + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000..4da6b6e8c4 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "sanitize-html": "^2.10.0" + } +}