mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
Merge pull request #155 from danny-avila/feat-export-convo
Feature support export conversation
This commit is contained in:
commit
21920dd864
20 changed files with 948 additions and 636 deletions
|
|
@ -35,21 +35,21 @@ router.post('/', async (req, res) => {
|
||||||
let endpointOption = {};
|
let endpointOption = {};
|
||||||
if (req.body?.jailbreak)
|
if (req.body?.jailbreak)
|
||||||
endpointOption = {
|
endpointOption = {
|
||||||
jailbreak: req.body?.jailbreak || false,
|
jailbreak: req.body?.jailbreak ?? false,
|
||||||
jailbreakConversationId: req.body?.jailbreakConversationId || null,
|
jailbreakConversationId: req.body?.jailbreakConversationId ?? null,
|
||||||
systemMessage: req.body?.systemMessage || null,
|
systemMessage: req.body?.systemMessage ?? null,
|
||||||
context: req.body?.context || null,
|
context: req.body?.context ?? null,
|
||||||
toneStyle: req.body?.toneStyle || 'fast'
|
toneStyle: req.body?.toneStyle ?? 'fast'
|
||||||
};
|
};
|
||||||
else
|
else
|
||||||
endpointOption = {
|
endpointOption = {
|
||||||
jailbreak: req.body?.jailbreak || false,
|
jailbreak: req.body?.jailbreak ?? false,
|
||||||
systemMessage: req.body?.systemMessage || null,
|
systemMessage: req.body?.systemMessage ?? null,
|
||||||
context: req.body?.context || null,
|
context: req.body?.context ?? null,
|
||||||
conversationSignature: req.body?.conversationSignature || null,
|
conversationSignature: req.body?.conversationSignature ?? null,
|
||||||
clientId: req.body?.clientId || null,
|
clientId: req.body?.clientId ?? null,
|
||||||
invocationId: req.body?.invocationId || null,
|
invocationId: req.body?.invocationId ?? null,
|
||||||
toneStyle: req.body?.toneStyle || 'fast'
|
toneStyle: req.body?.toneStyle ?? 'fast'
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('ask log', {
|
console.log('ask log', {
|
||||||
|
|
@ -122,31 +122,23 @@ const ask = async ({
|
||||||
|
|
||||||
console.log('BING RESPONSE', response);
|
console.log('BING RESPONSE', response);
|
||||||
|
|
||||||
|
const newConversationId = endpointOption?.jailbreak
|
||||||
|
? response.jailbreakConversationId
|
||||||
|
: response.conversationId || conversationId;
|
||||||
|
const newUserMassageId = response.parentMessageId || userMessageId;
|
||||||
|
const newResponseMessageId = response.parentMessageId || response.details.requestId || userMessageId;
|
||||||
|
|
||||||
// STEP1 generate response message
|
// STEP1 generate response message
|
||||||
response.text = response.response || response.details.spokenText || '**Bing refused to answer.**';
|
response.text = response.response || response.details.spokenText || '**Bing refused to answer.**';
|
||||||
|
|
||||||
let responseMessage = {
|
let responseMessage = {
|
||||||
|
conversationId: newConversationId,
|
||||||
|
messageId: newResponseMessageId,
|
||||||
|
parentMessageId: overrideParentMessageId || newUserMassageId,
|
||||||
|
sender: endpointOption?.jailbreak ? 'Sydney' : 'BingAI',
|
||||||
text: await handleText(response, true),
|
text: await handleText(response, true),
|
||||||
suggestions:
|
suggestions: response.details.suggestedResponses && response.details.suggestedResponses.map(s => s.text)
|
||||||
response.details.suggestedResponses && response.details.suggestedResponses.map(s => s.text),
|
|
||||||
jailbreak: endpointOption?.jailbreak
|
|
||||||
};
|
};
|
||||||
// // response.text = await handleText(response, true);
|
|
||||||
// response.suggestions =
|
|
||||||
// response.details.suggestedResponses && response.details.suggestedResponses.map(s => s.text);
|
|
||||||
|
|
||||||
if (endpointOption?.jailbreak) {
|
|
||||||
responseMessage.conversationId = response.jailbreakConversationId;
|
|
||||||
responseMessage.messageId = response.messageId || response.details.messageId;
|
|
||||||
responseMessage.parentMessageId = overrideParentMessageId || response.parentMessageId || userMessageId;
|
|
||||||
responseMessage.sender = 'Sydney';
|
|
||||||
} else {
|
|
||||||
responseMessage.conversationId = response.conversationId;
|
|
||||||
responseMessage.messageId = response.messageId || response.details.messageId;
|
|
||||||
responseMessage.parentMessageId =
|
|
||||||
overrideParentMessageId || response.parentMessageId || response.details.requestId || userMessageId;
|
|
||||||
responseMessage.sender = 'BingAI';
|
|
||||||
}
|
|
||||||
|
|
||||||
await saveMessage(responseMessage);
|
await saveMessage(responseMessage);
|
||||||
|
|
||||||
|
|
@ -159,14 +151,22 @@ const ask = async ({
|
||||||
// Attition: the api will also create new conversationId while using invalid userMessage.parentMessageId,
|
// 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.
|
// but in this situation, don't change the conversationId, but create new convo.
|
||||||
|
|
||||||
let conversationUpdate = { conversationId, endpoint: 'bingAI' };
|
let conversationUpdate = { conversationId: newConversationId, endpoint: 'bingAI' };
|
||||||
if (conversationId != responseMessage.conversationId && isNewConversation)
|
if (conversationId != newConversationId)
|
||||||
|
if (isNewConversation) {
|
||||||
|
// change the conversationId to new one
|
||||||
conversationUpdate = {
|
conversationUpdate = {
|
||||||
...conversationUpdate,
|
...conversationUpdate,
|
||||||
conversationId: conversationId,
|
conversationId: conversationId,
|
||||||
newConversationId: responseMessage.conversationId || conversationId
|
newConversationId: newConversationId
|
||||||
};
|
};
|
||||||
conversationId = responseMessage.conversationId || conversationId;
|
} else {
|
||||||
|
// create new conversation
|
||||||
|
conversationUpdate = {
|
||||||
|
...conversationUpdate,
|
||||||
|
...endpointOption
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (endpointOption?.jailbreak) {
|
if (endpointOption?.jailbreak) {
|
||||||
conversationUpdate.jailbreak = true;
|
conversationUpdate.jailbreak = true;
|
||||||
|
|
@ -179,17 +179,16 @@ const ask = async ({
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveConvo(req?.session?.user?.username, conversationUpdate);
|
await saveConvo(req?.session?.user?.username, conversationUpdate);
|
||||||
|
conversationId = newConversationId;
|
||||||
|
|
||||||
// STEP3 update the user message
|
// STEP3 update the user message
|
||||||
userMessage.conversationId = conversationId;
|
userMessage.conversationId = newConversationId;
|
||||||
userMessage.messageId = responseMessage.parentMessageId;
|
userMessage.messageId = newUserMassageId;
|
||||||
|
|
||||||
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
|
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
|
||||||
if (!overrideParentMessageId) {
|
if (!overrideParentMessageId)
|
||||||
const oldUserMessageId = userMessageId;
|
await saveMessage({ ...userMessage, messageId: userMessageId, newMessageId: newUserMassageId });
|
||||||
await saveMessage({ ...userMessage, messageId: oldUserMessageId, newMessageId: userMessage.messageId });
|
userMessageId = newUserMassageId;
|
||||||
}
|
|
||||||
userMessageId = userMessage.messageId;
|
|
||||||
|
|
||||||
sendMessage(res, {
|
sendMessage(res, {
|
||||||
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ router.post('/', async (req, res) => {
|
||||||
|
|
||||||
// build endpoint option
|
// build endpoint option
|
||||||
const endpointOption = {
|
const endpointOption = {
|
||||||
model: req.body?.model || 'text-davinci-002-render-sha'
|
model: req.body?.model ?? 'text-davinci-002-render-sha'
|
||||||
};
|
};
|
||||||
|
|
||||||
const availableModels = getChatGPTBrowserModels();
|
const availableModels = getChatGPTBrowserModels();
|
||||||
|
|
@ -106,13 +106,17 @@ const ask = async ({
|
||||||
|
|
||||||
console.log('CLIENT RESPONSE', response);
|
console.log('CLIENT RESPONSE', response);
|
||||||
|
|
||||||
|
const newConversationId = response.conversationId || conversationId;
|
||||||
|
const newUserMassageId = response.parentMessageId || userMessageId;
|
||||||
|
const newResponseMessageId = response.messageId;
|
||||||
|
|
||||||
// STEP1 generate response message
|
// STEP1 generate response message
|
||||||
response.text = response.response || '**ChatGPT refused to answer.**';
|
response.text = response.response || '**ChatGPT refused to answer.**';
|
||||||
|
|
||||||
let responseMessage = {
|
let responseMessage = {
|
||||||
conversationId: response.conversationId,
|
conversationId: newConversationId,
|
||||||
messageId: response.messageId,
|
messageId: newResponseMessageId,
|
||||||
parentMessageId: overrideParentMessageId || response.parentMessageId || userMessageId,
|
parentMessageId: overrideParentMessageId || newUserMassageId,
|
||||||
text: await handleText(response),
|
text: await handleText(response),
|
||||||
sender: endpointOption?.chatGptLabel || 'ChatGPT'
|
sender: endpointOption?.chatGptLabel || 'ChatGPT'
|
||||||
};
|
};
|
||||||
|
|
@ -122,27 +126,34 @@ const ask = async ({
|
||||||
// STEP2 update the conversation
|
// STEP2 update the conversation
|
||||||
|
|
||||||
// First update conversationId if needed
|
// First update conversationId if needed
|
||||||
let conversationUpdate = { conversationId, endpoint: 'chatGPTBrowser' };
|
let conversationUpdate = { conversationId: newConversationId, endpoint: 'chatGPTBrowser' };
|
||||||
if (conversationId != responseMessage.conversationId && isNewConversation)
|
if (conversationId != newConversationId)
|
||||||
|
if (isNewConversation) {
|
||||||
|
// change the conversationId to new one
|
||||||
conversationUpdate = {
|
conversationUpdate = {
|
||||||
...conversationUpdate,
|
...conversationUpdate,
|
||||||
conversationId: conversationId,
|
conversationId: conversationId,
|
||||||
newConversationId: responseMessage.conversationId || conversationId
|
newConversationId: newConversationId
|
||||||
};
|
};
|
||||||
conversationId = responseMessage.conversationId || conversationId;
|
} else {
|
||||||
|
// create new conversation
|
||||||
|
conversationUpdate = {
|
||||||
|
...conversationUpdate,
|
||||||
|
...endpointOption
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await saveConvo(req?.session?.user?.username, conversationUpdate);
|
await saveConvo(req?.session?.user?.username, conversationUpdate);
|
||||||
|
conversationId = newConversationId;
|
||||||
|
|
||||||
// STEP3 update the user message
|
// STEP3 update the user message
|
||||||
userMessage.conversationId = conversationId;
|
userMessage.conversationId = newConversationId;
|
||||||
userMessage.messageId = responseMessage.parentMessageId;
|
userMessage.messageId = newUserMassageId;
|
||||||
|
|
||||||
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
|
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
|
||||||
if (!overrideParentMessageId) {
|
if (!overrideParentMessageId)
|
||||||
const oldUserMessageId = userMessageId;
|
await saveMessage({ ...userMessage, messageId: userMessageId, newMessageId: newUserMassageId });
|
||||||
await saveMessage({ ...userMessage, messageId: oldUserMessageId, newMessageId: userMessage.messageId });
|
userMessageId = newUserMassageId;
|
||||||
}
|
|
||||||
userMessageId = userMessage.messageId;
|
|
||||||
|
|
||||||
sendMessage(res, {
|
sendMessage(res, {
|
||||||
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ router.post('/', async (req, res) => {
|
||||||
|
|
||||||
// build user message
|
// build user message
|
||||||
const conversationId = oldConversationId || crypto.randomUUID();
|
const conversationId = oldConversationId || crypto.randomUUID();
|
||||||
|
const isNewConversation = !oldConversationId;
|
||||||
const userMessageId = crypto.randomUUID();
|
const userMessageId = crypto.randomUUID();
|
||||||
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
|
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||||
const userMessage = {
|
const userMessage = {
|
||||||
|
|
@ -32,13 +33,13 @@ router.post('/', async (req, res) => {
|
||||||
|
|
||||||
// build endpoint option
|
// build endpoint option
|
||||||
const endpointOption = {
|
const endpointOption = {
|
||||||
model: req.body?.model || 'gpt-3.5-turbo',
|
model: req.body?.model ?? 'gpt-3.5-turbo',
|
||||||
chatGptLabel: req.body?.chatGptLabel || null,
|
chatGptLabel: req.body?.chatGptLabel ?? null,
|
||||||
promptPrefix: req.body?.promptPrefix || null,
|
promptPrefix: req.body?.promptPrefix ?? null,
|
||||||
temperature: req.body?.temperature || 1,
|
temperature: req.body?.temperature ?? 1,
|
||||||
top_p: req.body?.top_p || 1,
|
top_p: req.body?.top_p ?? 1,
|
||||||
presence_penalty: req.body?.presence_penalty || 0,
|
presence_penalty: req.body?.presence_penalty ?? 0,
|
||||||
frequency_penalty: req.body?.frequency_penalty || 0
|
frequency_penalty: req.body?.frequency_penalty ?? 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const availableModels = getOpenAIModels();
|
const availableModels = getOpenAIModels();
|
||||||
|
|
@ -63,6 +64,7 @@ router.post('/', async (req, res) => {
|
||||||
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
// eslint-disable-next-line no-use-before-define
|
||||||
return await ask({
|
return await ask({
|
||||||
|
isNewConversation,
|
||||||
userMessage,
|
userMessage,
|
||||||
endpointOption,
|
endpointOption,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
|
@ -74,6 +76,7 @@ router.post('/', async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const ask = async ({
|
const ask = async ({
|
||||||
|
isNewConversation,
|
||||||
userMessage,
|
userMessage,
|
||||||
endpointOption,
|
endpointOption,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
|
@ -84,8 +87,6 @@ const ask = async ({
|
||||||
}) => {
|
}) => {
|
||||||
let { text, parentMessageId: userParentMessageId, messageId: userMessageId } = userMessage;
|
let { text, parentMessageId: userParentMessageId, messageId: userMessageId } = userMessage;
|
||||||
|
|
||||||
const client = askClient;
|
|
||||||
|
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
Connection: 'keep-alive',
|
Connection: 'keep-alive',
|
||||||
'Content-Type': 'text/event-stream',
|
'Content-Type': 'text/event-stream',
|
||||||
|
|
@ -100,7 +101,7 @@ const ask = async ({
|
||||||
const progressCallback = createOnProgress();
|
const progressCallback = createOnProgress();
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
res.on('close', () => abortController.abort());
|
res.on('close', () => abortController.abort());
|
||||||
let response = await client({
|
let response = await askClient({
|
||||||
text,
|
text,
|
||||||
parentMessageId: userParentMessageId,
|
parentMessageId: userParentMessageId,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
|
@ -115,13 +116,17 @@ const ask = async ({
|
||||||
|
|
||||||
console.log('CLIENT RESPONSE', response);
|
console.log('CLIENT RESPONSE', response);
|
||||||
|
|
||||||
|
const newConversationId = response.conversationId || conversationId;
|
||||||
|
const newUserMassageId = response.parentMessageId || userMessageId;
|
||||||
|
const newResponseMessageId = response.messageId;
|
||||||
|
|
||||||
// STEP1 generate response message
|
// STEP1 generate response message
|
||||||
response.text = response.response || '**ChatGPT refused to answer.**';
|
response.text = response.response || '**ChatGPT refused to answer.**';
|
||||||
|
|
||||||
let responseMessage = {
|
let responseMessage = {
|
||||||
conversationId: response.conversationId,
|
conversationId: newConversationId,
|
||||||
messageId: response.messageId,
|
messageId: newResponseMessageId,
|
||||||
parentMessageId: overrideParentMessageId || userMessageId,
|
parentMessageId: overrideParentMessageId || newUserMassageId,
|
||||||
text: await handleText(response),
|
text: await handleText(response),
|
||||||
sender: endpointOption?.chatGptLabel || 'ChatGPT'
|
sender: endpointOption?.chatGptLabel || 'ChatGPT'
|
||||||
};
|
};
|
||||||
|
|
@ -129,21 +134,34 @@ const ask = async ({
|
||||||
await saveMessage(responseMessage);
|
await saveMessage(responseMessage);
|
||||||
|
|
||||||
// STEP2 update the conversation
|
// STEP2 update the conversation
|
||||||
conversationId = responseMessage.conversationId || conversationId;
|
let conversationUpdate = { conversationId: newConversationId, endpoint: 'openAI' };
|
||||||
// it seems openAI will not change the conversationId.
|
if (conversationId != newConversationId)
|
||||||
// let conversationUpdate = { conversationId, endpoint: 'openAI' };
|
if (isNewConversation) {
|
||||||
// await saveConvo(req?.session?.user?.username, conversationUpdate);
|
// change the conversationId to new one
|
||||||
|
conversationUpdate = {
|
||||||
|
...conversationUpdate,
|
||||||
|
conversationId: conversationId,
|
||||||
|
newConversationId: newConversationId
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// create new conversation
|
||||||
|
conversationUpdate = {
|
||||||
|
...conversationUpdate,
|
||||||
|
...endpointOption
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveConvo(req?.session?.user?.username, conversationUpdate);
|
||||||
|
conversationId = newConversationId;
|
||||||
|
|
||||||
// STEP3 update the user message
|
// STEP3 update the user message
|
||||||
userMessage.conversationId = conversationId;
|
userMessage.conversationId = newConversationId;
|
||||||
userMessage.messageId = responseMessage.parentMessageId;
|
userMessage.messageId = newUserMassageId;
|
||||||
|
|
||||||
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
|
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
|
||||||
if (!overrideParentMessageId) {
|
if (!overrideParentMessageId)
|
||||||
const oldUserMessageId = userMessageId;
|
await saveMessage({ ...userMessage, messageId: userMessageId, newMessageId: newUserMassageId });
|
||||||
await saveMessage({ ...userMessage, messageId: oldUserMessageId, newMessageId: userMessage.messageId });
|
userMessageId = newUserMassageId;
|
||||||
}
|
|
||||||
userMessageId = userMessage.messageId;
|
|
||||||
|
|
||||||
sendMessage(res, {
|
sendMessage(res, {
|
||||||
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
||||||
|
|
|
||||||
810
client/package-lock.json
generated
810
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -40,8 +40,11 @@
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
|
"downloadjs": "^1.4.7",
|
||||||
"esbuild": "0.17.15",
|
"esbuild": "0.17.15",
|
||||||
"export-from-json": "^1.7.2",
|
"export-from-json": "^1.7.2",
|
||||||
|
"filenamify": "^5.1.1",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.113.0",
|
"lucide-react": "^0.113.0",
|
||||||
"rc-input-number": "^7.4.2",
|
"rc-input-number": "^7.4.2",
|
||||||
|
|
@ -65,6 +68,7 @@
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"tailwindcss-radix": "^2.8.0",
|
"tailwindcss-radix": "^2.8.0",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
|
"use-react-screenshot": "github:danny-avila/use-react-screenshot#master",
|
||||||
"uuidv4": "^6.2.13"
|
"uuidv4": "^6.2.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import store from './store';
|
||||||
import userAuth from './utils/userAuth';
|
import userAuth from './utils/userAuth';
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { ScreenshotProvider } from './utils/screenshotContext.jsx';
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
|
|
@ -97,4 +99,8 @@ const App = () => {
|
||||||
else return <div className="flex h-screen"></div>;
|
else return <div className="flex h-screen"></div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default () => (
|
||||||
|
<ScreenshotProvider>
|
||||||
|
<App />
|
||||||
|
</ScreenshotProvider>
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ export default function Conversation({ conversation, retainView }) {
|
||||||
// stop existing submission
|
// stop existing submission
|
||||||
setSubmission(null);
|
setSubmission(null);
|
||||||
|
|
||||||
|
// set document title
|
||||||
|
document.title = title;
|
||||||
|
|
||||||
// set conversation to the new conversation
|
// set conversation to the new conversation
|
||||||
switchToConversation(conversation);
|
switchToConversation(conversation);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
import filenamify from 'filenamify';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import exportFromJSON from 'export-from-json';
|
import exportFromJSON from 'export-from-json';
|
||||||
import DialogTemplate from '../ui/DialogTemplate';
|
import DialogTemplate from '../ui/DialogTemplate';
|
||||||
|
|
@ -51,9 +52,10 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportPreset = () => {
|
const exportPreset = () => {
|
||||||
|
const fileName = filenamify(preset?.title || 'preset');
|
||||||
exportFromJSON({
|
exportFromJSON({
|
||||||
data: cleanupPreset({ preset, endpointsFilter }),
|
data: cleanupPreset({ preset, endpointsFilter }),
|
||||||
fileName: `${preset?.title}.json`,
|
fileName,
|
||||||
exportType: exportFromJSON.types.json
|
exportType: exportFromJSON.types.json
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,8 @@ export default function NewConversationMenu() {
|
||||||
// update the default model when availableModels changes
|
// update the default model when availableModels changes
|
||||||
// typically, availableModels changes => modelsFilter or customGPTModels changes
|
// typically, availableModels changes => modelsFilter or customGPTModels changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (conversationId == 'new') {
|
const isInvalidConversation = !availableEndpoints.find(e => e === endpoint);
|
||||||
|
if (conversationId == 'new' && isInvalidConversation) {
|
||||||
newConversation();
|
newConversation();
|
||||||
}
|
}
|
||||||
}, [availableEndpoints]);
|
}, [availableEndpoints]);
|
||||||
|
|
|
||||||
|
|
@ -56,16 +56,7 @@ export default function PresetItem({ preset = {}, value, onSelect, onChangePrese
|
||||||
/> */}
|
/> */}
|
||||||
<div className="flex w-4 flex-1" />
|
<div className="flex w-4 flex-1" />
|
||||||
<button
|
<button
|
||||||
className="invisible m-0 mr-1 rounded-md text-gray-400 hover:text-gray-700 group-hover:visible dark:text-gray-400 dark:hover:text-gray-200 "
|
className="invisible m-0 p-2 mr-1 rounded-md text-gray-400 hover:text-gray-700 group-hover:visible dark:text-gray-400 dark:hover:text-gray-200 "
|
||||||
onClick={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
onDeletePreset(preset);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="invisible m-0 p-2 rounded-md text-gray-400 hover:text-gray-700 group-hover:visible dark:text-gray-400 dark:hover:text-gray-200 "
|
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onChangePreset(preset);
|
onChangePreset(preset);
|
||||||
|
|
@ -73,6 +64,15 @@ export default function PresetItem({ preset = {}, value, onSelect, onChangePrese
|
||||||
>
|
>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="invisible m-0 rounded-md text-gray-400 hover:text-gray-700 group-hover:visible dark:text-gray-400 dark:hover:text-gray-200 "
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
onDeletePreset(preset);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</button>
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import OpenAIOptions from './OpenAIOptions';
|
||||||
import ChatGPTOptions from './ChatGPTOptions';
|
import ChatGPTOptions from './ChatGPTOptions';
|
||||||
import BingAIOptions from './BingAIOptions';
|
import BingAIOptions from './BingAIOptions';
|
||||||
// import BingStyles from './BingStyles';
|
// import BingStyles from './BingStyles';
|
||||||
import EndpointMenu from './Endpoints/NewConversationMenu';
|
import NewConversationMenu from './Endpoints/NewConversationMenu';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import { useMessageHandler } from '../../utils/handleSubmit';
|
import { useMessageHandler } from '../../utils/handleSubmit';
|
||||||
|
|
@ -145,7 +145,7 @@ export default function TextChat({ isSearchView = false }) {
|
||||||
disabled ? 'dark:bg-gray-900' : 'dark:bg-gray-700'
|
disabled ? 'dark:bg-gray-900' : 'dark:bg-gray-700'
|
||||||
} dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] md:py-3 md:pl-4`}
|
} dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] md:py-3 md:pl-4`}
|
||||||
>
|
>
|
||||||
<EndpointMenu />
|
<NewConversationMenu />
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,7 @@ export default function Message({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MultiMessage
|
<MultiMessage
|
||||||
|
messageId={message.messageId}
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
messagesTree={message.children}
|
messagesTree={message.children}
|
||||||
scrollToBottom={scrollToBottom}
|
scrollToBottom={scrollToBottom}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
import Message from './Message';
|
import Message from './Message';
|
||||||
|
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
export default function MultiMessage({
|
export default function MultiMessage({
|
||||||
|
messageId,
|
||||||
conversation,
|
conversation,
|
||||||
messagesTree,
|
messagesTree,
|
||||||
scrollToBottom,
|
scrollToBottom,
|
||||||
|
|
@ -9,7 +13,9 @@ export default function MultiMessage({
|
||||||
setCurrentEditId,
|
setCurrentEditId,
|
||||||
isSearchView
|
isSearchView
|
||||||
}) {
|
}) {
|
||||||
const [siblingIdx, setSiblingIdx] = useState(0);
|
// const [siblingIdx, setSiblingIdx] = useState(0);
|
||||||
|
|
||||||
|
const [siblingIdx, setSiblingIdx] = useRecoilState(store.messagesSiblingIdxFamily(messageId));
|
||||||
|
|
||||||
const setSiblingIdxRev = value => {
|
const setSiblingIdxRev = value => {
|
||||||
setSiblingIdx(messagesTree?.length - value - 1);
|
setSiblingIdx(messagesTree?.length - value - 1);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { CSSTransition } from 'react-transition-group';
|
||||||
import ScrollToBottom from './ScrollToBottom';
|
import ScrollToBottom from './ScrollToBottom';
|
||||||
import MultiMessage from './MultiMessage';
|
import MultiMessage from './MultiMessage';
|
||||||
import MessageHeader from './MessageHeader';
|
import MessageHeader from './MessageHeader';
|
||||||
|
import { useScreenshot } from '~/utils/screenshotContext.jsx';
|
||||||
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
|
@ -23,6 +24,8 @@ export default function Messages({ isSearchView = false }) {
|
||||||
const conversation = useRecoilValue(store.conversation) || {};
|
const conversation = useRecoilValue(store.conversation) || {};
|
||||||
const { conversationId } = conversation;
|
const { conversationId } = conversation;
|
||||||
|
|
||||||
|
const { screenshotTargetRef } = useScreenshot();
|
||||||
|
|
||||||
// const models = useRecoilValue(store.models) || [];
|
// const models = useRecoilValue(store.models) || [];
|
||||||
// const modelName = models.find(element => element.model == model)?.name;
|
// const modelName = models.find(element => element.model == model)?.name;
|
||||||
|
|
||||||
|
|
@ -84,8 +87,11 @@ export default function Messages({ isSearchView = false }) {
|
||||||
ref={scrollableRef}
|
ref={scrollableRef}
|
||||||
onScroll={debouncedHandleScroll}
|
onScroll={debouncedHandleScroll}
|
||||||
>
|
>
|
||||||
<div className="dark:gpt-dark-gray h-full">
|
<div
|
||||||
<div className="dark:gpt-dark-gray flex h-full flex-col items-center text-sm">
|
className="dark:gpt-dark-gray mb-32 h-auto md:mb-48"
|
||||||
|
ref={screenshotTargetRef}
|
||||||
|
>
|
||||||
|
<div className="dark:gpt-dark-gray flex h-auto flex-col items-center text-sm">
|
||||||
<MessageHeader isSearchView={isSearchView} />
|
<MessageHeader isSearchView={isSearchView} />
|
||||||
{_messagesTree === null ? (
|
{_messagesTree === null ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
|
@ -97,6 +103,7 @@ export default function Messages({ isSearchView = false }) {
|
||||||
<>
|
<>
|
||||||
<MultiMessage
|
<MultiMessage
|
||||||
key={conversationId} // avoid internal state mixture
|
key={conversationId} // avoid internal state mixture
|
||||||
|
messageId={conversationId}
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
messagesTree={_messagesTree}
|
messagesTree={_messagesTree}
|
||||||
scrollToBottom={scrollToBottom}
|
scrollToBottom={scrollToBottom}
|
||||||
|
|
@ -116,7 +123,7 @@ export default function Messages({ isSearchView = false }) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className="dark:gpt-dark-gray group h-32 w-full flex-shrink-0 dark:border-gray-900/50 md:h-48"
|
className="dark:gpt-dark-gray group h-0 w-full flex-shrink-0 dark:border-gray-900/50"
|
||||||
ref={messagesEndRef}
|
ref={messagesEndRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
415
client/src/components/Nav/ExportConversation/ExportModel.jsx
Normal file
415
client/src/components/Nav/ExportConversation/ExportModel.jsx
Normal file
|
|
@ -0,0 +1,415 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useRecoilValue, useRecoilCallback } from 'recoil';
|
||||||
|
import filenamify from 'filenamify';
|
||||||
|
import exportFromJSON from 'export-from-json';
|
||||||
|
import download from 'downloadjs';
|
||||||
|
import DialogTemplate from '~/components/ui/DialogTemplate.jsx';
|
||||||
|
import { Dialog, DialogClose, DialogButton } from '~/components/ui/Dialog.tsx';
|
||||||
|
import { Input } from '~/components/ui/Input.tsx';
|
||||||
|
import { Label } from '~/components/ui/Label.tsx';
|
||||||
|
import { Checkbox } from '~/components/ui/Checkbox.tsx';
|
||||||
|
import Dropdown from '~/components/ui/Dropdown';
|
||||||
|
import { cn } from '~/utils/';
|
||||||
|
import { useScreenshot } from '~/utils/screenshotContext';
|
||||||
|
|
||||||
|
import store from '~/store';
|
||||||
|
import cleanupPreset from '~/utils/cleanupPreset.js';
|
||||||
|
|
||||||
|
export default function ExportModel({ open, onOpenChange }) {
|
||||||
|
const { captureScreenshot } = useScreenshot();
|
||||||
|
|
||||||
|
const [filename, setFileName] = useState('');
|
||||||
|
const [type, setType] = useState('');
|
||||||
|
|
||||||
|
const [includeOptions, setIncludeOptions] = useState(true);
|
||||||
|
const [exportBranches, setExportBranches] = useState(false);
|
||||||
|
const [exportBranchesSupport, setExportBranchesSupport] = useState(false);
|
||||||
|
const [recursive, setRecursive] = useState(true);
|
||||||
|
|
||||||
|
const conversation = useRecoilValue(store.conversation) || {};
|
||||||
|
const messagesTree = useRecoilValue(store.messagesTree) || [];
|
||||||
|
const endpointsFilter = useRecoilValue(store.endpointsFilter);
|
||||||
|
|
||||||
|
const getSiblingIdx = useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
async messageId =>
|
||||||
|
await snapshot.getPromise(store.messagesSiblingIdxFamily(messageId)),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const typeOptions = ['text', 'markdown', 'csv', 'json', 'screenshot']; //,, 'webpage'];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFileName(
|
||||||
|
filenamify(String(conversation?.title || 'file'))
|
||||||
|
);
|
||||||
|
setType('text');
|
||||||
|
setIncludeOptions(true);
|
||||||
|
setExportBranches(false);
|
||||||
|
setExportBranchesSupport(false);
|
||||||
|
setRecursive(true);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const _setType = newType => {
|
||||||
|
if (newType === 'json' || newType === 'csv' || newType === 'webpage') {
|
||||||
|
setExportBranches(true);
|
||||||
|
setExportBranchesSupport(true);
|
||||||
|
} else {
|
||||||
|
setExportBranches(false);
|
||||||
|
setExportBranchesSupport(false);
|
||||||
|
}
|
||||||
|
setType(newType);
|
||||||
|
};
|
||||||
|
|
||||||
|
// return an object or an array based on branches and recursive option
|
||||||
|
// messageId is used to get siblindIdx from recoil snapshot
|
||||||
|
const buildMessageTree = async ({ messageId, message, messages, branches = false, recursive = false }) => {
|
||||||
|
let children = [];
|
||||||
|
if (messages?.length)
|
||||||
|
if (branches)
|
||||||
|
for (const message of messages)
|
||||||
|
children.push(
|
||||||
|
await buildMessageTree({
|
||||||
|
messageId: message?.messageId,
|
||||||
|
message: message,
|
||||||
|
messages: message?.children,
|
||||||
|
branches,
|
||||||
|
recursive
|
||||||
|
})
|
||||||
|
);
|
||||||
|
else {
|
||||||
|
let message = messages[0];
|
||||||
|
if (messages?.length > 1) {
|
||||||
|
const siblingIdx = await getSiblingIdx(messageId);
|
||||||
|
message = messages[messages.length - siblingIdx - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
children = [
|
||||||
|
await buildMessageTree({
|
||||||
|
messageId: message?.messageId,
|
||||||
|
message: message,
|
||||||
|
messages: message?.children,
|
||||||
|
branches,
|
||||||
|
recursive
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recursive) return { ...message, children: children };
|
||||||
|
else {
|
||||||
|
let ret = [];
|
||||||
|
if (message) {
|
||||||
|
let _message = { ...message };
|
||||||
|
delete _message.children;
|
||||||
|
ret = [_message];
|
||||||
|
}
|
||||||
|
for (const child of children) ret = ret.concat(child);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportScreenshot = async () => {
|
||||||
|
const data = await captureScreenshot();
|
||||||
|
download(data, `${filename}.png`, 'image/png');
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportCSV = async () => {
|
||||||
|
let data = [];
|
||||||
|
|
||||||
|
const messages = await buildMessageTree({
|
||||||
|
messageId: conversation?.conversationId,
|
||||||
|
message: null,
|
||||||
|
messages: messagesTree,
|
||||||
|
branches: exportBranches,
|
||||||
|
recursive: false
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
data.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
exportFromJSON({
|
||||||
|
data: data,
|
||||||
|
fileName: filename,
|
||||||
|
extension: 'csv',
|
||||||
|
exportType: exportFromJSON.types.csv,
|
||||||
|
beforeTableEncode: entries => [
|
||||||
|
{ fieldName: 'sender', fieldValues: entries.find(e => e.fieldName == 'sender').fieldValues },
|
||||||
|
{ fieldName: 'text', fieldValues: entries.find(e => e.fieldName == 'text').fieldValues },
|
||||||
|
{
|
||||||
|
fieldName: 'isCreatedByUser',
|
||||||
|
fieldValues: entries.find(e => e.fieldName == 'isCreatedByUser').fieldValues
|
||||||
|
},
|
||||||
|
{ fieldName: 'error', fieldValues: entries.find(e => e.fieldName == 'error').fieldValues },
|
||||||
|
{ fieldName: 'messageId', fieldValues: entries.find(e => e.fieldName == 'messageId').fieldValues },
|
||||||
|
{
|
||||||
|
fieldName: 'parentMessageId',
|
||||||
|
fieldValues: entries.find(e => e.fieldName == 'parentMessageId').fieldValues
|
||||||
|
},
|
||||||
|
{ fieldName: 'createdAt', fieldValues: entries.find(e => e.fieldName == 'createdAt').fieldValues }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportMarkdown = async () => {
|
||||||
|
let data =
|
||||||
|
`# Conversation\n` +
|
||||||
|
`- conversationId: ${conversation?.conversationId}\n` +
|
||||||
|
`- endpoint: ${conversation?.endpoint}\n` +
|
||||||
|
`- title: ${conversation?.title}\n` +
|
||||||
|
`- exportAt: ${new Date().toTimeString()}\n`;
|
||||||
|
|
||||||
|
if (includeOptions) {
|
||||||
|
data += `\n## Options\n`;
|
||||||
|
const options = cleanupPreset({ preset: conversation, endpointsFilter });
|
||||||
|
|
||||||
|
for (const key of Object.keys(options)) {
|
||||||
|
data += `- ${key}: ${options[key]}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = await buildMessageTree({
|
||||||
|
messageId: conversation?.conversationId,
|
||||||
|
message: null,
|
||||||
|
messages: messagesTree,
|
||||||
|
branches: false,
|
||||||
|
recursive: false
|
||||||
|
});
|
||||||
|
|
||||||
|
data += `\n## History\n`;
|
||||||
|
for (const message of messages) {
|
||||||
|
data += `**${message?.sender}:**\n${message?.text}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
exportFromJSON({
|
||||||
|
data: data,
|
||||||
|
fileName: filename,
|
||||||
|
extension: 'md',
|
||||||
|
exportType: exportFromJSON.types.text
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportText = async () => {
|
||||||
|
let data =
|
||||||
|
`Conversation\n` +
|
||||||
|
`########################\n` +
|
||||||
|
`conversationId: ${conversation?.conversationId}\n` +
|
||||||
|
`endpoint: ${conversation?.endpoint}\n` +
|
||||||
|
`title: ${conversation?.title}\n` +
|
||||||
|
`exportAt: ${new Date().toTimeString()}\n`;
|
||||||
|
|
||||||
|
if (includeOptions) {
|
||||||
|
data += `\nOptions\n########################\n`;
|
||||||
|
const options = cleanupPreset({ preset: conversation, endpointsFilter });
|
||||||
|
|
||||||
|
for (const key of Object.keys(options)) {
|
||||||
|
data += `${key}: ${options[key]}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = await buildMessageTree({
|
||||||
|
messageId: conversation?.conversationId,
|
||||||
|
message: null,
|
||||||
|
messages: messagesTree,
|
||||||
|
branches: false,
|
||||||
|
recursive: false
|
||||||
|
});
|
||||||
|
|
||||||
|
data += `\nHistory\n########################\n`;
|
||||||
|
for (const message of messages) {
|
||||||
|
data += `${message?.sender}:\n${message?.text}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
exportFromJSON({
|
||||||
|
data: data,
|
||||||
|
fileName: filename,
|
||||||
|
extension: 'txt',
|
||||||
|
exportType: exportFromJSON.types.text
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportJSON = async () => {
|
||||||
|
let data = {
|
||||||
|
conversationId: conversation?.conversationId,
|
||||||
|
endpoint: conversation?.endpoint,
|
||||||
|
title: conversation?.title,
|
||||||
|
exportAt: new Date().toTimeString(),
|
||||||
|
branches: exportBranches,
|
||||||
|
recursive: recursive
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeOptions) data.options = cleanupPreset({ preset: conversation, endpointsFilter });
|
||||||
|
|
||||||
|
const messages = await buildMessageTree({
|
||||||
|
messageId: conversation?.conversationId,
|
||||||
|
message: null,
|
||||||
|
messages: messagesTree,
|
||||||
|
branches: exportBranches,
|
||||||
|
recursive: recursive
|
||||||
|
});
|
||||||
|
|
||||||
|
if (recursive) data.messagesTree = messages.children;
|
||||||
|
else data.messages = messages;
|
||||||
|
|
||||||
|
exportFromJSON({
|
||||||
|
data: data,
|
||||||
|
fileName: filename,
|
||||||
|
extension: 'json',
|
||||||
|
exportType: exportFromJSON.types.json
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportConversation = () => {
|
||||||
|
if (type === 'json') exportJSON();
|
||||||
|
else if (type == 'text') exportText();
|
||||||
|
else if (type == 'markdown') exportMarkdown();
|
||||||
|
else if (type == 'csv') exportCSV();
|
||||||
|
else if (type == 'screenshot') exportScreenshot();
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultTextProps =
|
||||||
|
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 focus:dark:bg-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
>
|
||||||
|
<DialogTemplate
|
||||||
|
title="Export conversation"
|
||||||
|
className="max-w-full sm:max-w-2xl"
|
||||||
|
main={
|
||||||
|
<div className="flex w-full flex-col items-center gap-6">
|
||||||
|
<div className="grid w-full gap-6 sm:grid-cols-2">
|
||||||
|
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||||
|
<Label
|
||||||
|
htmlFor="filename"
|
||||||
|
className="text-left text-sm font-medium"
|
||||||
|
>
|
||||||
|
Filename
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="filename"
|
||||||
|
value={filename}
|
||||||
|
onChange={e => setFileName(filenamify(e.target.value || ''))}
|
||||||
|
placeholder="Set the filename"
|
||||||
|
className={cn(
|
||||||
|
defaultTextProps,
|
||||||
|
'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||||
|
<Label
|
||||||
|
htmlFor="type"
|
||||||
|
className="text-left text-sm font-medium"
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</Label>
|
||||||
|
<Dropdown
|
||||||
|
id="type"
|
||||||
|
value={type}
|
||||||
|
onChange={_setType}
|
||||||
|
options={typeOptions}
|
||||||
|
className={cn(
|
||||||
|
defaultTextProps,
|
||||||
|
'flex h-10 max-h-10 w-full resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
|
||||||
|
)}
|
||||||
|
containerClassName="flex w-full resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid w-full gap-6 sm:grid-cols-2">
|
||||||
|
{type !== 'csv' && type !== 'screenshot' ? (
|
||||||
|
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||||
|
<div className="grid w-full items-center gap-2">
|
||||||
|
<Label
|
||||||
|
htmlFor="includeOptions"
|
||||||
|
className="text-left text-sm font-medium"
|
||||||
|
>
|
||||||
|
Include endpoint options
|
||||||
|
</Label>
|
||||||
|
<div className="flex h-[40px] w-full items-center space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id="includeOptions"
|
||||||
|
checked={includeOptions}
|
||||||
|
className="focus:ring-opacity-20 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:focus:ring-gray-600 dark:focus:ring-opacity-50 dark:focus:ring-offset-0"
|
||||||
|
onCheckedChange={setIncludeOptions}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="includeOptions"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||||
|
>
|
||||||
|
Enabled
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{type !== 'screenshot' ? (
|
||||||
|
<div className="grid w-full items-center gap-2">
|
||||||
|
<Label
|
||||||
|
htmlFor="exportBranches"
|
||||||
|
className="text-left text-sm font-medium"
|
||||||
|
>
|
||||||
|
Export all message branches
|
||||||
|
</Label>
|
||||||
|
<div className="flex h-[40px] w-full items-center space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id="exportBranches"
|
||||||
|
disabled={!exportBranchesSupport}
|
||||||
|
checked={exportBranches}
|
||||||
|
className="focus:ring-opacity-20 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:focus:ring-gray-600 dark:focus:ring-opacity-50 dark:focus:ring-offset-0"
|
||||||
|
onCheckedChange={setExportBranches}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="exportBranches"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||||
|
>
|
||||||
|
{exportBranchesSupport ? 'Enabled' : 'Not Supported'}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{type === 'json' ? (
|
||||||
|
<div className="grid w-full items-center gap-2">
|
||||||
|
<Label
|
||||||
|
htmlFor="recursive"
|
||||||
|
className="text-left text-sm font-medium"
|
||||||
|
>
|
||||||
|
Recursive or sequential?
|
||||||
|
</Label>
|
||||||
|
<div className="flex h-[40px] w-full items-center space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id="recursive"
|
||||||
|
checked={recursive}
|
||||||
|
className="focus:ring-opacity-20 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:focus:ring-gray-600 dark:focus:ring-opacity-50 dark:focus:ring-offset-0"
|
||||||
|
onCheckedChange={setRecursive}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="recursive"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||||
|
>
|
||||||
|
Recursive
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttons={
|
||||||
|
<>
|
||||||
|
<DialogButton
|
||||||
|
onClick={exportConversation}
|
||||||
|
className="dark:hover:gray-400 border-gray-700 bg-green-600 text-white hover:bg-green-700 dark:hover:bg-green-800"
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</DialogButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
selection={null}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
client/src/components/Nav/ExportConversation/index.jsx
Normal file
43
client/src/components/Nav/ExportConversation/index.jsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { Download } from 'lucide-react';
|
||||||
|
import { cn } from '~/utils/';
|
||||||
|
|
||||||
|
import ExportModel from './ExportModel';
|
||||||
|
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
|
export default function ExportConversation() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const conversation = useRecoilValue(store.conversation) || {};
|
||||||
|
|
||||||
|
const exportable =
|
||||||
|
conversation?.conversationId &&
|
||||||
|
conversation?.conversationId !== 'new' &&
|
||||||
|
conversation?.conversationId !== 'search';
|
||||||
|
|
||||||
|
const clickHandler = () => {
|
||||||
|
if (exportable) setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 rounded-md py-3 px-3 text-sm transition-colors duration-200 hover:bg-gray-500/10',
|
||||||
|
exportable ? 'cursor-pointer text-white' : 'cursor-not-allowed text-gray-400'
|
||||||
|
)}
|
||||||
|
onClick={clickHandler}
|
||||||
|
>
|
||||||
|
<Download size={16} />
|
||||||
|
Export conversation
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ExportModel
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import SearchBar from './SearchBar';
|
||||||
import ClearConvos from './ClearConvos';
|
import ClearConvos from './ClearConvos';
|
||||||
import DarkMode from './DarkMode';
|
import DarkMode from './DarkMode';
|
||||||
import Logout from './Logout';
|
import Logout from './Logout';
|
||||||
|
import ExportConversation from './ExportConversation';
|
||||||
|
|
||||||
export default function NavLinks({ fetch, onSearchSuccess, clearSearch, isSearchEnabled }) {
|
export default function NavLinks({ fetch, onSearchSuccess, clearSearch, isSearchEnabled }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -14,6 +15,7 @@ export default function NavLinks({ fetch, onSearchSuccess, clearSearch, isSearch
|
||||||
clearSearch={clearSearch}
|
clearSearch={clearSearch}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<ExportConversation />
|
||||||
<DarkMode />
|
<DarkMode />
|
||||||
<ClearConvos />
|
<ClearConvos />
|
||||||
<Logout />
|
<Logout />
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,15 @@
|
||||||
import endpoints from './endpoints';
|
import endpoints from './endpoints';
|
||||||
import { atom, selector, useSetRecoilState, useResetRecoilState, useRecoilCallback } from 'recoil';
|
import {
|
||||||
|
atom,
|
||||||
|
selector,
|
||||||
|
atomFamily,
|
||||||
|
useSetRecoilState,
|
||||||
|
useResetRecoilState,
|
||||||
|
useRecoilCallback
|
||||||
|
} from 'recoil';
|
||||||
import buildTree from '~/utils/buildTree';
|
import buildTree from '~/utils/buildTree';
|
||||||
import getDefaultConversation from '~/utils/getDefaultConversation';
|
import getDefaultConversation from '~/utils/getDefaultConversation';
|
||||||
|
import submission from './submission.js';
|
||||||
|
|
||||||
// current conversation, can be null (need to be fetched from server)
|
// current conversation, can be null (need to be fetched from server)
|
||||||
// sample structure
|
// sample structure
|
||||||
|
|
@ -56,9 +64,15 @@ const latestMessage = atom({
|
||||||
default: null
|
default: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const messagesSiblingIdxFamily = atomFamily({
|
||||||
|
key: 'messagesSiblingIdx',
|
||||||
|
default: 0
|
||||||
|
});
|
||||||
|
|
||||||
const useConversation = () => {
|
const useConversation = () => {
|
||||||
const setConversation = useSetRecoilState(conversation);
|
const setConversation = useSetRecoilState(conversation);
|
||||||
const setMessages = useSetRecoilState(messages);
|
const setMessages = useSetRecoilState(messages);
|
||||||
|
const setSubmission = useSetRecoilState(submission.submission);
|
||||||
const resetLatestMessage = useResetRecoilState(latestMessage);
|
const resetLatestMessage = useResetRecoilState(latestMessage);
|
||||||
|
|
||||||
const switchToConversation = useRecoilCallback(
|
const switchToConversation = useRecoilCallback(
|
||||||
|
|
@ -93,6 +107,7 @@ const useConversation = () => {
|
||||||
|
|
||||||
setConversation(conversation);
|
setConversation(conversation);
|
||||||
setMessages(messages);
|
setMessages(messages);
|
||||||
|
setSubmission({});
|
||||||
resetLatestMessage();
|
resetLatestMessage();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -126,5 +141,6 @@ export default {
|
||||||
messages,
|
messages,
|
||||||
messagesTree,
|
messagesTree,
|
||||||
latestMessage,
|
latestMessage,
|
||||||
|
messagesSiblingIdxFamily,
|
||||||
useConversation
|
useConversation
|
||||||
};
|
};
|
||||||
|
|
|
||||||
21
client/src/utils/screenshotContext.jsx
Normal file
21
client/src/utils/screenshotContext.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import React, { createContext, useRef, useContext, useCallback } from 'react';
|
||||||
|
import { useScreenshot as useScreenshot_ } from 'use-react-screenshot';
|
||||||
|
|
||||||
|
const ScreenshotContext = createContext({});
|
||||||
|
|
||||||
|
export const useScreenshot = () => {
|
||||||
|
const { ref } = useContext(ScreenshotContext);
|
||||||
|
const [image, takeScreenshot] = useScreenshot_();
|
||||||
|
|
||||||
|
const captureScreenshot = () => {
|
||||||
|
return takeScreenshot(ref.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { screenshotTargetRef: ref, captureScreenshot };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ScreenshotProvider = ({ children }) => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
return <ScreenshotContext.Provider value={{ ref }}>{children}</ScreenshotContext.Provider>;
|
||||||
|
};
|
||||||
|
|
@ -29,6 +29,7 @@ services:
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- MONGO_URI=mongodb://mongodb:27017/chatgpt-clone
|
- MONGO_URI=mongodb://mongodb:27017/chatgpt-clone
|
||||||
|
# - CHATGPT_REVERSE_PROXY=http://host.docker.internal:8080/api/conversation # if you are hosting your own chatgpt reverse proxy
|
||||||
- MEILI_HOST=http://meilisearch:7700
|
- MEILI_HOST=http://meilisearch:7700
|
||||||
- MEILI_HTTP_ADDR=meilisearch:7700
|
- MEILI_HTTP_ADDR=meilisearch:7700
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue