| | import download from 'downloadjs'; |
| | import { useCallback } from 'react'; |
| | import { useParams } from 'react-router-dom'; |
| | import exportFromJSON from 'export-from-json'; |
| | import { useQueryClient } from '@tanstack/react-query'; |
| | import { |
| | buildTree, |
| | QueryKeys, |
| | ContentTypes, |
| | ToolCallTypes, |
| | imageGenTools, |
| | isImageVisionTool, |
| | } from 'librechat-data-provider'; |
| | import type { |
| | TMessageContentParts, |
| | TConversation, |
| | TMessage, |
| | TPreset, |
| | } from 'librechat-data-provider'; |
| | import useBuildMessageTree from '~/hooks/Messages/useBuildMessageTree'; |
| | import { useScreenshot } from '~/hooks/ScreenshotContext'; |
| | import { cleanupPreset } from '~/utils'; |
| |
|
| | type ExportValues = { |
| | fieldName: string; |
| | fieldValues: string[]; |
| | }; |
| | type ExportEntries = ExportValues[]; |
| |
|
| | 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: Partial<TMessage> | undefined, 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 |
| | .filter((content) => content != null) |
| | .map((content) => getMessageContent(message.sender || '', content)) |
| | .filter((text) => text.length > 0) |
| | .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, |
| | typeof content[ContentTypes.TEXT] === 'object' |
| | ? (content[ContentTypes.TEXT].value ?? '') |
| | : (content[ContentTypes.TEXT] ?? ''), |
| | ]; |
| | } |
| |
|
| | if (content.type === ContentTypes.TEXT) { |
| | |
| | const textPart = content[ContentTypes.TEXT]; |
| | const text = typeof textPart === 'string' ? textPart : (textPart?.value ?? ''); |
| | return [sender, text]; |
| | } |
| |
|
| | 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: Partial<TMessage>[] = []; |
| |
|
| | const messages = await buildMessageTree({ |
| | messageId: conversation?.conversationId, |
| | message: null, |
| | messages: getMessageTree(), |
| | branches: Boolean(exportBranches), |
| | recursive: false, |
| | }); |
| |
|
| | if (Array.isArray(messages)) { |
| | for (const message of messages) { |
| | if (!message) { |
| | continue; |
| | } |
| | data.push(message); |
| | } |
| | } else { |
| | data.push(messages); |
| | } |
| |
|
| | exportFromJSON({ |
| | data: data, |
| | fileName: filename, |
| | extension: 'csv', |
| | exportType: exportFromJSON.types.csv, |
| | beforeTableEncode: (entries: ExportEntries | undefined) => [ |
| | { |
| | 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 === true) { |
| | 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 === true) { |
| | 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 === true) { |
| | 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 === true) { |
| | 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 === true) { |
| | 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 === true) { |
| | 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 === true) { |
| | data['options'] = cleanupPreset({ preset: conversation as TPreset }); |
| | } |
| |
|
| | const messages = await buildMessageTree({ |
| | messageId: conversation?.conversationId, |
| | message: null, |
| | messages: getMessageTree(), |
| | branches: Boolean(exportBranches), |
| | recursive: Boolean(recursive), |
| | }); |
| |
|
| | if (recursive === true && !Array.isArray(messages)) { |
| | data['messagesTree'] = messages.children; |
| | } else { |
| | data['messages'] = messages; |
| | } |
| |
|
| | |
| | const jsonString = JSON.stringify(data); |
| | const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8' }); |
| | download(blob, `${filename}.json`, 'application/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 }; |
| | } |
| |
|