diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index 9b6cc507..65c9e26e 100644 --- a/frontend/src/models/system.js +++ b/frontend/src/models/system.js @@ -465,7 +465,10 @@ const System = { method: "GET", headers: baseHeaders(), }) - .then((res) => res.text()) + .then((res) => { + if (res.ok) return res.text(); + throw new Error(res.statusText); + }) .catch((e) => { console.error(e); return null; diff --git a/frontend/src/pages/GeneralSettings/Chats/index.jsx b/frontend/src/pages/GeneralSettings/Chats/index.jsx index f705d18c..c44610f3 100644 --- a/frontend/src/pages/GeneralSettings/Chats/index.jsx +++ b/frontend/src/pages/GeneralSettings/Chats/index.jsx @@ -21,7 +21,7 @@ export default function WorkspaceChats() { }; const handleDumpChats = async () => { const chats = await System.exportChats(exportType); - if (chats) { + if (!!chats) { const { mimeType, fileExtension } = exportOptions[exportType]; const blob = new Blob([chats], { type: mimeType }); const link = document.createElement("a"); diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 68054503..50e63ef9 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -898,7 +898,7 @@ function systemEndpoints(app) { async (request, response) => { try { const { type = "jsonl" } = request.query; - const chats = await prepareWorkspaceChatsForExport(); + const chats = await prepareWorkspaceChatsForExport(type); const { contentType, data } = await exportChatsAsType(chats, type); await EventLogs.logEvent( "exported_chats", diff --git a/server/utils/helpers/chat/convertTo.js b/server/utils/helpers/chat/convertTo.js index 4dc3955e..5bd5b37e 100644 --- a/server/utils/helpers/chat/convertTo.js +++ b/server/utils/helpers/chat/convertTo.js @@ -4,17 +4,19 @@ const { Workspace } = require("../../../models/workspace"); const { WorkspaceChats } = require("../../../models/workspaceChats"); -// Todo: make this more useful for export by adding other columns about workspace, user, time, etc for post-filtering. -async function convertToCSV(workspaceChatsMap) { - const rows = ["role,content"]; - for (const workspaceChats of Object.values(workspaceChatsMap)) { - for (const message of workspaceChats.messages) { - // Escape double quotes and wrap content in double quotes - const escapedContent = `"${message.content - .replace(/"/g, '""') - .replace(/\n/g, " ")}"`; - rows.push(`${message.role},${escapedContent}`); - } +// Todo: add RLHF feedbackScore field support +async function convertToCSV(preparedData) { + const rows = ["id,username,workspace,prompt,response,sent_at"]; + for (const item of preparedData) { + const record = [ + item.id, + escapeCsv(item.username), + escapeCsv(item.workspace), + escapeCsv(item.prompt), + escapeCsv(item.response), + item.sent_at, + ].join(","); + rows.push(record); } return rows.join("\n"); } @@ -33,10 +35,30 @@ async function convertToJSONL(workspaceChatsMap) { .join("\n"); } -async function prepareWorkspaceChatsForExport() { +async function prepareWorkspaceChatsForExport(format = "jsonl") { + if (!exportMap.hasOwnProperty(format)) + throw new Error("Invalid export type."); + const chats = await WorkspaceChats.whereWithData({}, null, null, { id: "asc", }); + + if (format === "csv") { + const preparedData = chats.map((chat) => { + const responseJson = JSON.parse(chat.response); + return { + id: chat.id, + username: chat.user ? chat.user.username : "unknown user", + workspace: chat.workspace ? chat.workspace.name : "unknown workspace", + prompt: chat.prompt, + response: responseJson.text, + sent_at: chat.createdAt, + }; + }); + + return preparedData; + } + const workspaceIds = [...new Set(chats.map((chat) => chat.workspaceId))]; const workspacesWithPrompts = await Promise.all( @@ -97,6 +119,10 @@ const exportMap = { }, }; +function escapeCsv(str) { + return `"${str.replace(/"/g, '""').replace(/\n/g, " ")}"`; +} + async function exportChatsAsType(workspaceChatsMap, format = "jsonl") { const { contentType, func } = exportMap.hasOwnProperty(format) ? exportMap[format]