From 3b94a987199fe5b8bf6f3d649d36ec1bf64eb768 Mon Sep 17 00:00:00 2001 From: Wentao Lyu <35-wentao.lyu@users.noreply.git.stereye.tech> Date: Thu, 6 Apr 2023 23:14:42 +0800 Subject: [PATCH 01/10] fix: use new conversation Id --- api/server/routes/ask/askBingAI.js | 91 +++++++++++----------- api/server/routes/ask/askChatGPTBrowser.js | 49 +++++++----- api/server/routes/ask/askOpenAI.js | 66 ++++++++++------ client/src/store/conversation.js | 3 + 4 files changed, 120 insertions(+), 89 deletions(-) diff --git a/api/server/routes/ask/askBingAI.js b/api/server/routes/ask/askBingAI.js index c8d1fd075..6a1fe8d77 100644 --- a/api/server/routes/ask/askBingAI.js +++ b/api/server/routes/ask/askBingAI.js @@ -35,21 +35,21 @@ router.post('/', async (req, res) => { let endpointOption = {}; if (req.body?.jailbreak) endpointOption = { - jailbreak: req.body?.jailbreak || false, - jailbreakConversationId: req.body?.jailbreakConversationId || null, - systemMessage: req.body?.systemMessage || null, - context: req.body?.context || null, - toneStyle: req.body?.toneStyle || 'fast' + jailbreak: req.body?.jailbreak ?? false, + jailbreakConversationId: req.body?.jailbreakConversationId ?? null, + systemMessage: req.body?.systemMessage ?? null, + context: req.body?.context ?? null, + toneStyle: req.body?.toneStyle ?? 'fast' }; else endpointOption = { - jailbreak: req.body?.jailbreak || false, - systemMessage: req.body?.systemMessage || null, - context: req.body?.context || null, - conversationSignature: req.body?.conversationSignature || null, - clientId: req.body?.clientId || null, - invocationId: req.body?.invocationId || null, - toneStyle: req.body?.toneStyle || 'fast' + jailbreak: req.body?.jailbreak ?? false, + systemMessage: req.body?.systemMessage ?? null, + context: req.body?.context ?? null, + conversationSignature: req.body?.conversationSignature ?? null, + clientId: req.body?.clientId ?? null, + invocationId: req.body?.invocationId ?? null, + toneStyle: req.body?.toneStyle ?? 'fast' }; console.log('ask log', { @@ -122,31 +122,23 @@ const ask = async ({ 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 response.text = response.response || response.details.spokenText || '**Bing refused to answer.**'; let responseMessage = { + conversationId: newConversationId, + messageId: newResponseMessageId, + parentMessageId: overrideParentMessageId || newUserMassageId, + sender: endpointOption?.jailbreak ? 'Sydney' : 'BingAI', text: await handleText(response, true), - suggestions: - response.details.suggestedResponses && response.details.suggestedResponses.map(s => s.text), - jailbreak: endpointOption?.jailbreak + suggestions: response.details.suggestedResponses && response.details.suggestedResponses.map(s => s.text) }; - // // 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); @@ -159,14 +151,22 @@ const ask = async ({ // 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. - let conversationUpdate = { conversationId, endpoint: 'bingAI' }; - if (conversationId != responseMessage.conversationId && isNewConversation) - conversationUpdate = { - ...conversationUpdate, - conversationId: conversationId, - newConversationId: responseMessage.conversationId || conversationId - }; - conversationId = responseMessage.conversationId || conversationId; + let conversationUpdate = { conversationId: newConversationId, endpoint: 'bingAI' }; + if (conversationId != newConversationId) + if (isNewConversation) { + // change the conversationId to new one + conversationUpdate = { + ...conversationUpdate, + conversationId: conversationId, + newConversationId: newConversationId + }; + } else { + // create new conversation + conversationUpdate = { + ...conversationUpdate, + ...endpointOption + }; + } if (endpointOption?.jailbreak) { conversationUpdate.jailbreak = true; @@ -179,17 +179,16 @@ const ask = async ({ } await saveConvo(req?.session?.user?.username, conversationUpdate); + conversationId = newConversationId; // STEP3 update the user message - userMessage.conversationId = conversationId; - userMessage.messageId = responseMessage.parentMessageId; + userMessage.conversationId = newConversationId; + userMessage.messageId = newUserMassageId; // If response has parentMessageId, the fake userMessage.messageId should be updated to the real one. - if (!overrideParentMessageId) { - const oldUserMessageId = userMessageId; - await saveMessage({ ...userMessage, messageId: oldUserMessageId, newMessageId: userMessage.messageId }); - } - userMessageId = userMessage.messageId; + if (!overrideParentMessageId) + await saveMessage({ ...userMessage, messageId: userMessageId, newMessageId: newUserMassageId }); + userMessageId = newUserMassageId; sendMessage(res, { title: await getConvoTitle(req?.session?.user?.username, conversationId), diff --git a/api/server/routes/ask/askChatGPTBrowser.js b/api/server/routes/ask/askChatGPTBrowser.js index d9d2909de..84e2e75a0 100644 --- a/api/server/routes/ask/askChatGPTBrowser.js +++ b/api/server/routes/ask/askChatGPTBrowser.js @@ -33,7 +33,7 @@ router.post('/', async (req, res) => { // build endpoint option const endpointOption = { - model: req.body?.model || 'text-davinci-002-render-sha' + model: req.body?.model ?? 'text-davinci-002-render-sha' }; const availableModels = getChatGPTBrowserModels(); @@ -106,13 +106,17 @@ const ask = async ({ console.log('CLIENT RESPONSE', response); + const newConversationId = response.conversationId || conversationId; + const newUserMassageId = response.parentMessageId || userMessageId; + const newResponseMessageId = response.messageId; + // STEP1 generate response message response.text = response.response || '**ChatGPT refused to answer.**'; let responseMessage = { - conversationId: response.conversationId, - messageId: response.messageId, - parentMessageId: overrideParentMessageId || response.parentMessageId || userMessageId, + conversationId: newConversationId, + messageId: newResponseMessageId, + parentMessageId: overrideParentMessageId || newUserMassageId, text: await handleText(response), sender: endpointOption?.chatGptLabel || 'ChatGPT' }; @@ -122,27 +126,34 @@ const ask = async ({ // STEP2 update the conversation // First update conversationId if needed - let conversationUpdate = { conversationId, endpoint: 'chatGPTBrowser' }; - if (conversationId != responseMessage.conversationId && isNewConversation) - conversationUpdate = { - ...conversationUpdate, - conversationId: conversationId, - newConversationId: responseMessage.conversationId || conversationId - }; - conversationId = responseMessage.conversationId || conversationId; + let conversationUpdate = { conversationId: newConversationId, endpoint: 'chatGPTBrowser' }; + if (conversationId != newConversationId) + if (isNewConversation) { + // 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 - userMessage.conversationId = conversationId; - userMessage.messageId = responseMessage.parentMessageId; + userMessage.conversationId = newConversationId; + userMessage.messageId = newUserMassageId; // If response has parentMessageId, the fake userMessage.messageId should be updated to the real one. - if (!overrideParentMessageId) { - const oldUserMessageId = userMessageId; - await saveMessage({ ...userMessage, messageId: oldUserMessageId, newMessageId: userMessage.messageId }); - } - userMessageId = userMessage.messageId; + if (!overrideParentMessageId) + await saveMessage({ ...userMessage, messageId: userMessageId, newMessageId: newUserMassageId }); + userMessageId = newUserMassageId; sendMessage(res, { title: await getConvoTitle(req?.session?.user?.username, conversationId), diff --git a/api/server/routes/ask/askOpenAI.js b/api/server/routes/ask/askOpenAI.js index e44a05909..9c5d12163 100644 --- a/api/server/routes/ask/askOpenAI.js +++ b/api/server/routes/ask/askOpenAI.js @@ -19,6 +19,7 @@ router.post('/', async (req, res) => { // build user message const conversationId = oldConversationId || crypto.randomUUID(); + const isNewConversation = !oldConversationId; const userMessageId = crypto.randomUUID(); const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000'; const userMessage = { @@ -32,13 +33,13 @@ router.post('/', async (req, res) => { // build endpoint option const endpointOption = { - model: req.body?.model || 'gpt-3.5-turbo', - chatGptLabel: req.body?.chatGptLabel || null, - promptPrefix: req.body?.promptPrefix || null, - temperature: req.body?.temperature || 1, - top_p: req.body?.top_p || 1, - presence_penalty: req.body?.presence_penalty || 0, - frequency_penalty: req.body?.frequency_penalty || 0 + model: req.body?.model ?? 'gpt-3.5-turbo', + chatGptLabel: req.body?.chatGptLabel ?? null, + promptPrefix: req.body?.promptPrefix ?? null, + temperature: req.body?.temperature ?? 1, + top_p: req.body?.top_p ?? 1, + presence_penalty: req.body?.presence_penalty ?? 0, + frequency_penalty: req.body?.frequency_penalty ?? 0 }; const availableModels = getOpenAIModels(); @@ -63,6 +64,7 @@ router.post('/', async (req, res) => { // eslint-disable-next-line no-use-before-define return await ask({ + isNewConversation, userMessage, endpointOption, conversationId, @@ -74,6 +76,7 @@ router.post('/', async (req, res) => { }); const ask = async ({ + isNewConversation, userMessage, endpointOption, conversationId, @@ -84,8 +87,6 @@ const ask = async ({ }) => { let { text, parentMessageId: userParentMessageId, messageId: userMessageId } = userMessage; - const client = askClient; - res.writeHead(200, { Connection: 'keep-alive', 'Content-Type': 'text/event-stream', @@ -100,7 +101,7 @@ const ask = async ({ const progressCallback = createOnProgress(); const abortController = new AbortController(); res.on('close', () => abortController.abort()); - let response = await client({ + let response = await askClient({ text, parentMessageId: userParentMessageId, conversationId, @@ -115,13 +116,17 @@ const ask = async ({ console.log('CLIENT RESPONSE', response); + const newConversationId = response.conversationId || conversationId; + const newUserMassageId = response.parentMessageId || userMessageId; + const newResponseMessageId = response.messageId; + // STEP1 generate response message response.text = response.response || '**ChatGPT refused to answer.**'; let responseMessage = { - conversationId: response.conversationId, - messageId: response.messageId, - parentMessageId: overrideParentMessageId || userMessageId, + conversationId: newConversationId, + messageId: newResponseMessageId, + parentMessageId: overrideParentMessageId || newUserMassageId, text: await handleText(response), sender: endpointOption?.chatGptLabel || 'ChatGPT' }; @@ -129,21 +134,34 @@ const ask = async ({ await saveMessage(responseMessage); // STEP2 update the conversation - conversationId = responseMessage.conversationId || conversationId; - // it seems openAI will not change the conversationId. - // let conversationUpdate = { conversationId, endpoint: 'openAI' }; - // await saveConvo(req?.session?.user?.username, conversationUpdate); + let conversationUpdate = { conversationId: newConversationId, endpoint: 'openAI' }; + if (conversationId != newConversationId) + if (isNewConversation) { + // 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 - userMessage.conversationId = conversationId; - userMessage.messageId = responseMessage.parentMessageId; + userMessage.conversationId = newConversationId; + userMessage.messageId = newUserMassageId; // If response has parentMessageId, the fake userMessage.messageId should be updated to the real one. - if (!overrideParentMessageId) { - const oldUserMessageId = userMessageId; - await saveMessage({ ...userMessage, messageId: oldUserMessageId, newMessageId: userMessage.messageId }); - } - userMessageId = userMessage.messageId; + if (!overrideParentMessageId) + await saveMessage({ ...userMessage, messageId: userMessageId, newMessageId: newUserMassageId }); + userMessageId = newUserMassageId; sendMessage(res, { title: await getConvoTitle(req?.session?.user?.username, conversationId), diff --git a/client/src/store/conversation.js b/client/src/store/conversation.js index 06520b275..79433f7d5 100644 --- a/client/src/store/conversation.js +++ b/client/src/store/conversation.js @@ -2,6 +2,7 @@ import endpoints from './endpoints'; import { atom, selector, useSetRecoilState, useResetRecoilState, useRecoilCallback } from 'recoil'; import buildTree from '~/utils/buildTree'; import getDefaultConversation from '~/utils/getDefaultConversation'; +import submission from './submission.js'; // current conversation, can be null (need to be fetched from server) // sample structure @@ -59,6 +60,7 @@ const latestMessage = atom({ const useConversation = () => { const setConversation = useSetRecoilState(conversation); const setMessages = useSetRecoilState(messages); + const setSubmission = useSetRecoilState(submission.submission); const resetLatestMessage = useResetRecoilState(latestMessage); const switchToConversation = useRecoilCallback( @@ -93,6 +95,7 @@ const useConversation = () => { setConversation(conversation); setMessages(messages); + setSubmission({}); resetLatestMessage(); }; From 6f0b559927c24bb841c1d5a028fc980b5a2d5773 Mon Sep 17 00:00:00 2001 From: Wentao Lyu <35-wentao.lyu@users.noreply.git.stereye.tech> Date: Thu, 6 Apr 2023 02:06:39 +0800 Subject: [PATCH 02/10] feat: export conversation: csv, json, txt, markdown --- client/src/components/Messages/Message.jsx | 1 + .../src/components/Messages/MultiMessage.jsx | 8 +- client/src/components/Messages/index.jsx | 1 + .../Nav/ExportConversation/ExportModel.jsx | 404 ++++++++++++++++++ .../Nav/ExportConversation/index.jsx | 43 ++ client/src/components/Nav/NavLinks.jsx | 2 + client/src/store/conversation.js | 15 +- 7 files changed, 472 insertions(+), 2 deletions(-) create mode 100644 client/src/components/Nav/ExportConversation/ExportModel.jsx create mode 100644 client/src/components/Nav/ExportConversation/index.jsx diff --git a/client/src/components/Messages/Message.jsx b/client/src/components/Messages/Message.jsx index 62d14c62f..8e250cce8 100644 --- a/client/src/components/Messages/Message.jsx +++ b/client/src/components/Messages/Message.jsx @@ -205,6 +205,7 @@ export default function Message({ { setSiblingIdx(messagesTree?.length - value - 1); diff --git a/client/src/components/Messages/index.jsx b/client/src/components/Messages/index.jsx index a458bf56e..3a7f95c3d 100644 --- a/client/src/components/Messages/index.jsx +++ b/client/src/components/Messages/index.jsx @@ -97,6 +97,7 @@ export default function Messages({ isSearchView = false }) { <> + async messageId => + await snapshot.getPromise(store.messagesSiblingIdxFamily(messageId)), + [] + ); + + const typeOptions = ['text', 'markdown', 'csv', 'json']; //, 'screenshot', 'webpage']; + + useEffect(() => { + setFileName( + String(conversation?.title) + .replace(/[^a-z0-9]/gi, '_') + .toLowerCase() || '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 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(); + }; + + 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 ( + + +
+
+ + setFileName(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' + )} + /> +
+
+ + +
+
+
+ {type !== 'csv' ? ( +
+
+ +
+ + +
+
+
+ ) : null} +
+ +
+ + +
+
+ {type === 'json' ? ( +
+ +
+ + +
+
+ ) : null} +
+ + } + buttons={ + <> + + Export + + + } + selection={null} + /> +
+ ); +} diff --git a/client/src/components/Nav/ExportConversation/index.jsx b/client/src/components/Nav/ExportConversation/index.jsx new file mode 100644 index 000000000..7548c4837 --- /dev/null +++ b/client/src/components/Nav/ExportConversation/index.jsx @@ -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 ( + <> + + + Export conversation + + + + + ); +} diff --git a/client/src/components/Nav/NavLinks.jsx b/client/src/components/Nav/NavLinks.jsx index 9626845d4..33f0e453c 100644 --- a/client/src/components/Nav/NavLinks.jsx +++ b/client/src/components/Nav/NavLinks.jsx @@ -3,6 +3,7 @@ import SearchBar from './SearchBar'; import ClearConvos from './ClearConvos'; import DarkMode from './DarkMode'; import Logout from './Logout'; +import ExportConversation from './ExportConversation'; export default function NavLinks({ fetch, onSearchSuccess, clearSearch, isSearchEnabled }) { return ( @@ -14,6 +15,7 @@ export default function NavLinks({ fetch, onSearchSuccess, clearSearch, isSearch clearSearch={clearSearch} /> )} + diff --git a/client/src/store/conversation.js b/client/src/store/conversation.js index 79433f7d5..c13cbc48e 100644 --- a/client/src/store/conversation.js +++ b/client/src/store/conversation.js @@ -1,5 +1,12 @@ 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 getDefaultConversation from '~/utils/getDefaultConversation'; import submission from './submission.js'; @@ -57,6 +64,11 @@ const latestMessage = atom({ default: null }); +const messagesSiblingIdxFamily = atomFamily({ + key: 'messagesSiblingIdx', + default: 0 +}); + const useConversation = () => { const setConversation = useSetRecoilState(conversation); const setMessages = useSetRecoilState(messages); @@ -129,5 +141,6 @@ export default { messages, messagesTree, latestMessage, + messagesSiblingIdxFamily, useConversation }; From 96914387a6cd774d7745f2308dbc5484f842a43a Mon Sep 17 00:00:00 2001 From: Wentao Lyu <35-wentao.lyu@users.noreply.git.stereye.tech> Date: Thu, 6 Apr 2023 02:48:32 +0800 Subject: [PATCH 03/10] feat: export to screenshot --- client/package.json | 3 + client/src/App.jsx | 8 ++- client/src/components/Messages/index.jsx | 10 +++- .../Nav/ExportConversation/ExportModel.jsx | 56 +++++++++++-------- client/src/utils/screenshotContext.jsx | 21 +++++++ 5 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 client/src/utils/screenshotContext.jsx diff --git a/client/package.json b/client/package.json index f0f477bac..27227c2be 100644 --- a/client/package.json +++ b/client/package.json @@ -40,8 +40,10 @@ "clsx": "^1.2.1", "copy-to-clipboard": "^3.3.3", "crypto-browserify": "^3.12.0", + "downloadjs": "^1.4.7", "esbuild": "0.17.15", "export-from-json": "^1.7.2", + "html2canvas": "^1.4.1", "lodash": "^4.17.21", "lucide-react": "^0.113.0", "rc-input-number": "^7.4.2", @@ -65,6 +67,7 @@ "tailwindcss-animate": "^1.0.5", "tailwindcss-radix": "^2.8.0", "url": "^0.11.0", + "use-react-screenshot": "^3.0.0", "uuidv4": "^6.2.13" }, "devDependencies": { diff --git a/client/src/App.jsx b/client/src/App.jsx index b63e47467..cca1de99a 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -7,6 +7,8 @@ import store from './store'; import userAuth from './utils/userAuth'; import { useRecoilState, useSetRecoilState } from 'recoil'; +import { ScreenshotProvider } from './utils/screenshotContext.jsx'; + import axios from 'axios'; const router = createBrowserRouter([ @@ -97,4 +99,8 @@ const App = () => { else return
; }; -export default App; +export default () => ( + + + +); diff --git a/client/src/components/Messages/index.jsx b/client/src/components/Messages/index.jsx index 3a7f95c3d..dae528eb9 100644 --- a/client/src/components/Messages/index.jsx +++ b/client/src/components/Messages/index.jsx @@ -6,6 +6,7 @@ import { CSSTransition } from 'react-transition-group'; import ScrollToBottom from './ScrollToBottom'; import MultiMessage from './MultiMessage'; import MessageHeader from './MessageHeader'; +import { useScreenshot } from '~/utils/screenshotContext.jsx'; import store from '~/store'; @@ -23,6 +24,8 @@ export default function Messages({ isSearchView = false }) { const conversation = useRecoilValue(store.conversation) || {}; const { conversationId } = conversation; + const { screenshotTargetRef } = useScreenshot(); + // const models = useRecoilValue(store.models) || []; // const modelName = models.find(element => element.model == model)?.name; @@ -84,8 +87,11 @@ export default function Messages({ isSearchView = false }) { ref={scrollableRef} onScroll={debouncedHandleScroll} > -
-
+
+
{_messagesTree === null ? ( diff --git a/client/src/components/Nav/ExportConversation/ExportModel.jsx b/client/src/components/Nav/ExportConversation/ExportModel.jsx index dfcc9cba2..b4c0080c8 100644 --- a/client/src/components/Nav/ExportConversation/ExportModel.jsx +++ b/client/src/components/Nav/ExportConversation/ExportModel.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useRecoilValue, useRecoilCallback } from 'recoil'; 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'; @@ -8,11 +9,14 @@ 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(''); @@ -32,7 +36,7 @@ export default function ExportModel({ open, onOpenChange }) { [] ); - const typeOptions = ['text', 'markdown', 'csv', 'json']; //, 'screenshot', 'webpage']; + const typeOptions = ['text', 'markdown', 'csv', 'json', 'screenshot']; //,, 'webpage']; useEffect(() => { setFileName( @@ -105,6 +109,11 @@ export default function ExportModel({ open, onOpenChange }) { } }; + const exportScreenshot = async () => { + const data = await captureScreenshot(); + download(data, `${filename}.png`, 'image/png'); + }; + const exportCSV = async () => { let data = []; @@ -256,6 +265,7 @@ export default function ExportModel({ open, onOpenChange }) { else if (type == 'text') exportText(); else if (type == 'markdown') exportMarkdown(); else if (type == 'csv') exportCSV(); + else if (type == 'screenshot') exportScreenshot(); }; const defaultTextProps = @@ -311,7 +321,7 @@ export default function ExportModel({ open, onOpenChange }) {
- {type !== 'csv' ? ( + {type !== 'csv' && type !== 'screenshot' ? (
) : null} -
- -
- - +
+ + +
-
+ ) : null} {type === 'json' ? (