2024-05-17 14:10:40 -04:00
|
|
|
import download from 'downloadjs';
|
2024-05-28 15:10:33 -07:00
|
|
|
import { useCallback } from 'react';
|
2024-05-17 14:10:40 -04:00
|
|
|
import exportFromJSON from 'export-from-json';
|
2024-05-28 15:10:33 -07:00
|
|
|
import { useQueryClient } from '@tanstack/react-query';
|
2024-05-17 14:10:40 -04:00
|
|
|
import {
|
2024-05-28 15:10:33 -07:00
|
|
|
QueryKeys,
|
2024-05-17 14:10:40 -04:00
|
|
|
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';
|
2024-05-28 15:10:33 -07:00
|
|
|
import { useParams } from 'react-router-dom';
|
2024-05-17 14:10:40 -04:00
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
}) {
|
2024-05-28 15:10:33 -07:00
|
|
|
const queryClient = useQueryClient();
|
2024-05-17 14:10:40 -04:00
|
|
|
const { captureScreenshot } = useScreenshot();
|
|
|
|
|
const buildMessageTree = useBuildMessageTree();
|
2024-05-28 15:10:33 -07:00
|
|
|
|
|
|
|
|
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]);
|
2024-05-17 14:10:40 -04:00
|
|
|
|
|
|
|
|
const getMessageText = (message: TMessage, format = 'text') => {
|
|
|
|
|
if (!message) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-28 15:10:33 -07:00
|
|
|
const formatText = (sender: string, text: string) => {
|
2024-05-17 14:10:40 -04:00
|
|
|
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,
|
2024-05-28 15:10:33 -07:00
|
|
|
messages: getMessageTree(),
|
2024-05-17 14:10:40 -04:00
|
|
|
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,
|
2024-05-28 15:10:33 -07:00
|
|
|
messages: getMessageTree(),
|
2024-05-17 14:10:40 -04:00
|
|
|
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,
|
2024-05-28 15:10:33 -07:00
|
|
|
messages: getMessageTree(),
|
2024-05-17 14:10:40 -04:00
|
|
|
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,
|
2024-05-28 15:10:33 -07:00
|
|
|
messages: getMessageTree(),
|
2024-05-17 14:10:40 -04:00
|
|
|
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 };
|
|
|
|
|
}
|