Skip to content

Commit

Permalink
Export improvements and Export to Markdown, Closes #337
Browse files Browse the repository at this point in the history
  • Loading branch information
enricoros committed Jan 24, 2024
1 parent 1f83210 commit 58896a7
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 31 deletions.
44 changes: 31 additions & 13 deletions src/modules/trade/ExportChats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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
Expand All @@ -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'));
Expand All @@ -58,11 +68,19 @@ export function ExportChats(props: { config: ExportConfig, onClose: () => void }
</Typography>

<Button variant='soft' disabled={!hasConversation}
color={downloadedState === 'ok' ? 'success' : downloadedState === 'fail' ? 'warning' : 'primary'}
endDecorator={downloadedState === 'ok' ? <DoneIcon /> : downloadedState === 'fail' ? '✘' : <FileDownloadIcon />}
color={downloadedJSONState === 'ok' ? 'success' : downloadedJSONState === 'fail' ? 'warning' : 'primary'}
endDecorator={downloadedJSONState === 'ok' ? <DoneIcon /> : downloadedJSONState === 'fail' ? '✘' : <FileDownloadIcon />}
sx={{ minWidth: 240, justifyContent: 'space-between' }}
onClick={handleDownloadConversationJSON}>
Download · JSON
</Button>

<Button variant='soft' disabled={!hasConversation}
color={downloadedMarkdownState === 'ok' ? 'success' : downloadedMarkdownState === 'fail' ? 'warning' : 'primary'}
endDecorator={downloadedMarkdownState === 'ok' ? <DoneIcon /> : downloadedMarkdownState === 'fail' ? '✘' : <FileDownloadIcon />}
sx={{ minWidth: 240, justifyContent: 'space-between' }}
onClick={handleDownloadConversation}>
Download chat
onClick={handleDownloadConversationMarkdown}>
Export · Markdown
</Button>

{enableSharing && (
Expand Down Expand Up @@ -90,8 +108,8 @@ export function ExportChats(props: { config: ExportConfig, onClose: () => void }
color={downloadedAllState === 'ok' ? 'success' : downloadedAllState === 'fail' ? 'warning' : 'primary'}
endDecorator={downloadedAllState === 'ok' ? <DoneIcon /> : downloadedAllState === 'fail' ? '✘' : <FileDownloadIcon />}
sx={{ minWidth: 240, justifyContent: 'space-between' }}
onClick={handleDownloadAllConversations}>
Download all chats
onClick={handleDownloadAllConversationsJSON}>
Download All · JSON
</Button>
</Box>

Expand Down
2 changes: 1 addition & 1 deletion src/modules/trade/link/ChatLinkExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export function ChatLinkExport(props: {
endDecorator={linkPutResult ? <DoneIcon /> : <IosShareIcon />}
sx={{ minWidth: 240, justifyContent: 'space-between' }}
onClick={handleConfirm}>
Share on {Brand.Title.Base}
Share · {Brand.Title.Base}
</Button>
</Badge>

Expand Down
4 changes: 2 additions & 2 deletions src/modules/trade/publish/PublishExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -85,7 +85,7 @@ export function PublishExport(props: {
endDecorator={<ExitToAppIcon />}
sx={{ minWidth: 240, justifyContent: 'space-between' }}
onClick={handlePublishConversation}>
Publish to Paste.gg
Share · Paste.gg
</Button>

{/* [publish] confirmation */}
Expand Down
46 changes: 31 additions & 15 deletions src/modules/trade/trade.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand All @@ -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');

}
Expand Down

0 comments on commit 58896a7

Please sign in to comment.