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 }
: downloadedState === 'fail' ? '✘' : }
+ color={downloadedJSONState === 'ok' ? 'success' : downloadedJSONState === 'fail' ? 'warning' : 'primary'}
+ endDecorator={downloadedJSONState === 'ok' ? : downloadedJSONState === 'fail' ? '✘' : }
+ sx={{ minWidth: 240, justifyContent: 'space-between' }}
+ onClick={handleDownloadConversationJSON}>
+ Download · JSON
+
+
+ : downloadedMarkdownState === 'fail' ? '✘' : }
sx={{ minWidth: 240, justifyContent: 'space-between' }}
- onClick={handleDownloadConversation}>
- Download chat
+ onClick={handleDownloadConversationMarkdown}>
+ Export · Markdown
{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');
}