import download from 'downloadjs'; import exportFromJSON from 'export-from-json'; import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query'; import { ContentTypes, ToolCallTypes, imageGenTools, isImageVisionTool, } from 'librechat-data-provider'; import type { TMessage, TPreset, TConversation, TMessageContentParts, } from 'librechat-data-provider'; import useBuildMessageTree from '~/hooks/Messages/useBuildMessageTree'; import { useScreenshot } from '~/hooks/ScreenshotContext'; import { cleanupPreset, buildTree } from '~/utils'; export default function useExportConversation({ conversation, filename, type, includeOptions, exportBranches, recursive, }: { conversation: TConversation | null; filename: string; type: string; includeOptions: boolean | 'indeterminate'; exportBranches: boolean | 'indeterminate'; recursive: boolean | 'indeterminate'; }) { const { captureScreenshot } = useScreenshot(); const buildMessageTree = useBuildMessageTree(); const { data: messagesTree = null } = useGetMessagesByConvoId( conversation?.conversationId ?? '', { select: (data) => { const dataTree = buildTree({ messages: data }); return dataTree?.length === 0 ? null : dataTree ?? null; }, }, ); const getMessageText = (message: TMessage, format = 'text') => { if (!message) { return ''; } const formatText = (sender, text) => { if (format === 'text') { return `>> ${sender}:\n${text}`; } return `**${sender}**\n${text}`; }; if (!message.content) { return formatText(message.sender, message.text); } return message.content .map((content) => getMessageContent(message.sender, content)) .map((text) => { return formatText(text[0], text[1]); }) .join('\n\n\n'); }; /** * Format and return message texts according to the type of content. * Currently, content whose type is `TOOL_CALL` basically returns JSON as is. * In the future, different formatted text may be returned for each type. */ const getMessageContent = (sender: string, content: TMessageContentParts): string[] => { if (!content) { return []; } if (content.type === ContentTypes.ERROR) { // ERROR return [sender, content[ContentTypes.TEXT].value]; } if (content.type === ContentTypes.TEXT) { // TEXT return [sender, content[ContentTypes.TEXT].value]; } if (content.type === ContentTypes.TOOL_CALL) { const type = content[ContentTypes.TOOL_CALL].type; if (type === ToolCallTypes.CODE_INTERPRETER) { // CODE_INTERPRETER const toolCall = content[ContentTypes.TOOL_CALL]; const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER]; return ['Code Interpreter', JSON.stringify(code_interpreter)]; } if (type === ToolCallTypes.RETRIEVAL) { // RETRIEVAL const toolCall = content[ContentTypes.TOOL_CALL]; return ['Retrieval', JSON.stringify(toolCall)]; } if ( type === ToolCallTypes.FUNCTION && imageGenTools.has(content[ContentTypes.TOOL_CALL].function.name) ) { // IMAGE_GENERATION const toolCall = content[ContentTypes.TOOL_CALL]; return ['Tool', JSON.stringify(toolCall)]; } if (type === ToolCallTypes.FUNCTION) { // IMAGE_VISION const toolCall = content[ContentTypes.TOOL_CALL]; if (isImageVisionTool(toolCall)) { return ['Tool', JSON.stringify(toolCall)]; } return ['Tool', JSON.stringify(toolCall)]; } } if (content.type === ContentTypes.IMAGE_FILE) { // IMAGE const imageFile = content[ContentTypes.IMAGE_FILE]; return ['Image', JSON.stringify(imageFile)]; } return [sender, JSON.stringify(content)]; }; const exportScreenshot = async () => { let data; try { data = await captureScreenshot(); } catch (err) { console.error('Failed to capture screenshot'); return console.error(err); } download(data, `${filename}.png`, 'image/png'); }; const exportCSV = async () => { const data: TMessage[] = []; const messages = await buildMessageTree({ messageId: conversation?.conversationId, message: null, messages: messagesTree, branches: !!exportBranches, recursive: false, }); if (Array.isArray(messages)) { for (const message of messages) { data.push(message); } } else { data.push(messages); } 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: 'unfinished', fieldValues: entries?.find((e) => e.fieldName == 'unfinished')?.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 as TPreset }); 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'; if (Array.isArray(messages)) { for (const message of messages) { data += `${getMessageText(message, 'md')}\n`; if (message.error) { data += '*(This is an error message)*\n'; } if (message.unfinished) { data += '*(This is an unfinished message)*\n'; } data += '\n\n'; } } else { data += `${getMessageText(messages, 'md')}\n`; if (messages.error) { data += '*(This is an error message)*\n'; } if (messages.unfinished) { data += '*(This is an unfinished message)*\n'; } } exportFromJSON({ data: data, fileName: filename, extension: 'md', exportType: exportFromJSON.types.txt, }); }; 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 as TPreset }); 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'; if (Array.isArray(messages)) { for (const message of messages) { data += `${getMessageText(message)}\n`; if (message.error) { data += '(This is an error message)\n'; } if (message.unfinished) { data += '(This is an unfinished message)\n'; } data += '\n\n'; } } else { data += `${getMessageText(messages)}\n`; if (messages.error) { data += '(This is an error message)\n'; } if (messages.unfinished) { data += '(This is an unfinished message)\n'; } } exportFromJSON({ data: data, fileName: filename, extension: 'txt', exportType: exportFromJSON.types.txt, }); }; const exportJSON = async () => { const 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 as TPreset }); } const messages = await buildMessageTree({ messageId: conversation?.conversationId, message: null, messages: messagesTree, branches: !!exportBranches, recursive: !!recursive, }); if (recursive && !Array.isArray(messages)) { 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(); } }; return { exportConversation }; }