From 3c3ce3666dd315c3160b021ba6a90aee4f1270f3 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Wed, 24 Jan 2024 15:26:00 -0800 Subject: [PATCH] Export improvements and Export to Markdown, Closes #337 --- src/modules/trade/ExportChats.tsx | 44 ++++++++++++++------ src/modules/trade/link/ChatLinkExport.tsx | 2 +- src/modules/trade/publish/PublishExport.tsx | 4 +- src/modules/trade/trade.client.ts | 46 ++++++++++++++------- 4 files changed, 65 insertions(+), 31 deletions(-) diff --git a/src/modules/trade/ExportChats.tsx b/src/modules/trade/ExportChats.tsx index 3bb055646d..dc7cf75c61 100644 --- a/src/modules/trade/ExportChats.tsx +++ b/src/modules/trade/ExportChats.tsx @@ -10,7 +10,7 @@ import { DConversationId, getConversation } from '~/common/state/store-chats'; import { ChatLinkExport } from './link/ChatLinkExport'; import { PublishExport } from './publish/PublishExport'; -import { downloadAllConversationsJson, downloadConversationJson } from './trade.client'; +import { downloadAllConversationsJson, downloadConversation } from './trade.client'; export type ExportConfig = { dir: 'export', conversationId: DConversationId | null }; @@ -22,7 +22,8 @@ export type ExportConfig = { dir: 'export', conversationId: DConversationId | nu export function ExportChats(props: { config: ExportConfig, onClose: () => void }) { // state - const [downloadedState, setDownloadedState] = React.useState<'ok' | 'fail' | null>(null); + const [downloadedJSONState, setDownloadedJSONState] = React.useState<'ok' | 'fail' | null>(null); + const [downloadedMarkdownState, setDownloadedMarkdownState] = React.useState<'ok' | 'fail' | null>(null); const [downloadedAllState, setDownloadedAllState] = React.useState<'ok' | 'fail' | null>(null); // external state @@ -31,16 +32,25 @@ export function ExportChats(props: { config: ExportConfig, onClose: () => void } // download chats - const handleDownloadConversation = () => { + const handleDownloadConversationJSON = () => { if (!props.config.conversationId) return; const conversation = getConversation(props.config.conversationId); if (!conversation) return; - downloadConversationJson(conversation) - .then(() => setDownloadedState('ok')) - .catch(() => setDownloadedState('fail')); + downloadConversation(conversation, 'json') + .then(() => setDownloadedJSONState('ok')) + .catch(() => setDownloadedJSONState('fail')); }; - const handleDownloadAllConversations = () => { + const handleDownloadConversationMarkdown = () => { + if (!props.config.conversationId) return; + const conversation = getConversation(props.config.conversationId); + if (!conversation) return; + downloadConversation(conversation, 'markdown') + .then(() => setDownloadedMarkdownState('ok')) + .catch(() => setDownloadedMarkdownState('fail')); + }; + + const handleDownloadAllConversationsJSON = () => { downloadAllConversationsJson() .then(() => setDownloadedAllState('ok')) .catch(() => setDownloadedAllState('fail')); @@ -58,11 +68,19 @@ export function ExportChats(props: { config: ExportConfig, onClose: () => void } + + {enableSharing && ( @@ -90,8 +108,8 @@ export function ExportChats(props: { config: ExportConfig, onClose: () => void } color={downloadedAllState === 'ok' ? 'success' : downloadedAllState === 'fail' ? 'warning' : 'primary'} endDecorator={downloadedAllState === 'ok' ? : downloadedAllState === 'fail' ? '✘' : } sx={{ minWidth: 240, justifyContent: 'space-between' }} - onClick={handleDownloadAllConversations}> - Download all chats + onClick={handleDownloadAllConversationsJSON}> + Download All · JSON diff --git a/src/modules/trade/link/ChatLinkExport.tsx b/src/modules/trade/link/ChatLinkExport.tsx index e2b3a504be..504278865e 100644 --- a/src/modules/trade/link/ChatLinkExport.tsx +++ b/src/modules/trade/link/ChatLinkExport.tsx @@ -125,7 +125,7 @@ export function ChatLinkExport(props: { endDecorator={linkPutResult ? : } sx={{ minWidth: 240, justifyContent: 'space-between' }} onClick={handleConfirm}> - Share on {Brand.Title.Base} + Share · {Brand.Title.Base} diff --git a/src/modules/trade/publish/PublishExport.tsx b/src/modules/trade/publish/PublishExport.tsx index 521888e1de..8d4a9a9c74 100644 --- a/src/modules/trade/publish/PublishExport.tsx +++ b/src/modules/trade/publish/PublishExport.tsx @@ -51,7 +51,7 @@ export function PublishExport(props: { setPublishUploading(true); const showSystemMessages = getChatShowSystemMessages(); - const markdownContent = conversationToMarkdown(conversation, !showSystemMessages); + const markdownContent = conversationToMarkdown(conversation, !showSystemMessages, false); try { const paste = await apiAsyncNode.trade.publishTo.mutate({ to: 'paste.gg', @@ -85,7 +85,7 @@ export function PublishExport(props: { endDecorator={} sx={{ minWidth: 240, justifyContent: 'space-between' }} onClick={handlePublishConversation}> - Publish to Paste.gg + Share · Paste.gg {/* [publish] confirmation */} diff --git a/src/modules/trade/trade.client.ts b/src/modules/trade/trade.client.ts index 693c8089df..434c02e3d3 100644 --- a/src/modules/trade/trade.client.ts +++ b/src/modules/trade/trade.client.ts @@ -4,7 +4,9 @@ import { defaultSystemPurposeId, SystemPurposeId, SystemPurposes } from '../../d import { DModelSource, useModelsStore } from '~/modules/llms/store-llms'; -import { DConversation, DMessage, useChatStore } from '~/common/state/store-chats'; +import { Brand } from '~/common/app.config'; +import { capitalizeFirstLetter } from '~/common/util/textUtils'; +import { conversationTitle, DConversation, DMessage, useChatStore } from '~/common/state/store-chats'; import { prettyBaseModel } from '~/common/util/modelUtils'; import { ImportedOutcome } from './ImportOutcomeModal'; @@ -88,14 +90,30 @@ export async function downloadAllConversationsJson() { * Download a conversation as a JSON file, for backup and future restore * @throws {Error} if the user closes the dialog, or file could not be saved */ -export async function downloadConversationJson(conversation: DConversation) { - // remove fields from the export - const exportableConversation: ExportedConversationJsonV1 = conversationToJsonV1(conversation); - const json = JSON.stringify(exportableConversation, null, 2); - const blob = new Blob([json], { type: 'application/json' }); +export async function downloadConversation(conversation: DConversation, format: 'json' | 'markdown') { + + let blob: Blob; + let extension: string; + + if (format == 'json') { + // remove fields (ephemerals, abortController, etc.) from the export + const exportableConversation: ExportedConversationJsonV1 = conversationToJsonV1(conversation); + const json = JSON.stringify(exportableConversation, null, 2); + blob = new Blob([json], { type: 'application/json' }); + extension = '.json'; + } else if (format == 'markdown') { + const exportableMarkdown = conversationToMarkdown(conversation, false, true, (sender: string) => `## ${sender} ##`); + blob = new Blob([exportableMarkdown], { type: 'text/markdown' }); + extension = '.md'; + } else { + throw new Error(`Invalid download format: ${format}`); + } + + // bonify title for saving to file (spaces to dashes, etc) + const fileTitle = conversationTitle(conversation).replace(/[^a-z0-9]/gi, '_').toLowerCase(); // link to begin the download - await fileSave(blob, { fileName: `conversation-${conversation.id}.json`, extensions: ['.json'] }); + await fileSave(blob, { fileName: `conversation-${fileTitle ? fileTitle + '-' : ''}${conversation.id}${extension}`, extensions: [extension] }); } export function conversationToJsonV1(_conversation: DConversation): ExportedConversationJsonV1 { @@ -108,13 +126,11 @@ export function conversationToJsonV1(_conversation: DConversation): ExportedConv /** * Primitive rendering of a Conversation to Markdown */ -export function conversationToMarkdown(conversation: DConversation, hideSystemMessage: boolean): string { - - // const title = - // `# ${conversation.manual/auto/name || 'Conversation'}\n` + - // (new Date(conversation.created)).toLocaleString() + '\n\n'; - - return conversation.messages.filter(message => !hideSystemMessage || message.role !== 'system').map(message => { +export function conversationToMarkdown(conversation: DConversation, hideSystemMessage: boolean, exportTitle: boolean, senderWrap?: (text: string) => string): string { + const mdTitle = exportTitle + ? `# ${capitalizeFirstLetter(conversationTitle(conversation, Brand.Title.Common + ' Chat'))}\nA ${Brand.Title.Common} conversation, updated on ${(new Date(conversation.updated || conversation.created)).toLocaleString()}.\n\n` + : ''; + return mdTitle + conversation.messages.filter(message => !hideSystemMessage || message.role !== 'system').map(message => { let sender: string = message.sender; let text = message.text; switch (message.role) { @@ -132,7 +148,7 @@ export function conversationToMarkdown(conversation: DConversation, hideSystemMe sender = '👤 You'; break; } - return `### ${sender}\n\n${text}\n\n`; + return (senderWrap?.(sender) || `### ${sender}`) + `\n\n${text}\n\n`; }).join('---\n\n'); }