| import download from 'downloadjs'; |
| import { useCallback } from 'react'; |
| import exportFromJSON from 'export-from-json'; |
| import { useQueryClient } from '@tanstack/react-query'; |
| import { |
| QueryKeys, |
| 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'; |
| import { useParams } from 'react-router-dom'; |
|
|
| 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 queryClient = useQueryClient(); |
| const { captureScreenshot } = useScreenshot(); |
| const buildMessageTree = useBuildMessageTree(); |
|
|
| const { conversationId: paramId } = useParams(); |
|
|
| const getMessageTree = useCallback(() => { |
| const queryParam = paramId === 'new' ? paramId : conversation?.conversationId ?? paramId ?? ''; |
| const messages = queryClient.getQueryData<TMessage[]>([QueryKeys.messages, queryParam]) ?? []; |
| const dataTree = buildTree({ messages }); |
| return dataTree?.length === 0 ? null : dataTree ?? null; |
| }, [paramId, conversation?.conversationId, queryClient]); |
|
|
| const getMessageText = (message: TMessage, format = 'text') => { |
| if (!message) { |
| return ''; |
| } |
|
|
| const formatText = (sender: string, text: string) => { |
| 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'); |
| }; |
|
|
| |
| |
| |
| |
| |
| const getMessageContent = (sender: string, content: TMessageContentParts): string[] => { |
| if (!content) { |
| return []; |
| } |
|
|
| if (content.type === ContentTypes.ERROR) { |
| |
| return [sender, content[ContentTypes.TEXT].value]; |
| } |
|
|
| if (content.type === ContentTypes.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) { |
| |
| const toolCall = content[ContentTypes.TOOL_CALL]; |
| const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER]; |
| return ['Code Interpreter', JSON.stringify(code_interpreter)]; |
| } |
|
|
| if (type === ToolCallTypes.RETRIEVAL) { |
| |
| const toolCall = content[ContentTypes.TOOL_CALL]; |
| return ['Retrieval', JSON.stringify(toolCall)]; |
| } |
|
|
| if ( |
| type === ToolCallTypes.FUNCTION && |
| imageGenTools.has(content[ContentTypes.TOOL_CALL].function.name) |
| ) { |
| |
| const toolCall = content[ContentTypes.TOOL_CALL]; |
| return ['Tool', JSON.stringify(toolCall)]; |
| } |
|
|
| if (type === ToolCallTypes.FUNCTION) { |
| |
| 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) { |
| |
| 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: getMessageTree(), |
| 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: getMessageTree(), |
| 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: getMessageTree(), |
| 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: getMessageTree(), |
| 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 }; |
| } |
|
|