diff --git a/src/common/attachment-drafts/attachment.mimetypes.ts b/src/common/attachment-drafts/attachment.mimetypes.ts index 97df7af10..a16b0f02b 100644 --- a/src/common/attachment-drafts/attachment.mimetypes.ts +++ b/src/common/attachment-drafts/attachment.mimetypes.ts @@ -98,6 +98,32 @@ const GuessedMimeLookupTable: Record = { 'application/x-bzip2': { ext: ['bz2'], dt: 'other' }, }; +const MdTitleToMimeLookupTable: Record = { + 'typescript': 'text/x-typescript', + 'ts': 'text/x-typescript', + 'tsx': 'text/x-typescript', + 'javascript': 'text/javascript', + 'js': 'text/javascript', + 'jsx': 'text/javascript', + 'python': 'text/x-python', + 'py': 'text/x-python', + 'json': 'application/json', + 'html': 'text/html', + 'htm': 'text/html', + 'css': 'text/css', + 'md': 'text/markdown', + 'markdown': 'text/markdown', + 'csv': 'text/csv', + 'tsv': 'text/csv', + 'xml': 'text/xml', + 'pdf': 'application/pdf', + 'doc': 'application/msword', + 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'xls': 'application/vnd.ms-excel', + 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'ppt': 'application/vnd.ms-powerpoint', + 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', +}; export function reverseLookupMimeType(fileExtension: string): GuessedMimeType | null { for (const [mimeType, { ext }] of Object.entries(GuessedMimeLookupTable)) { @@ -107,6 +133,15 @@ export function reverseLookupMimeType(fileExtension: string): GuessedMimeType | return null; } +export function reverseLookupMdTitle(mdTitle: string): { mimeType: GuessedMimeType, extension: string | null } | null { + const guessedMimeType = MdTitleToMimeLookupTable[mdTitle] || null; + if (guessedMimeType) { + const { ext } = GuessedMimeLookupTable[guessedMimeType]; + return { mimeType: guessedMimeType, extension: (ext ? ext[0] : null) || null }; + } + return null; +} + export function guessInputContentTypeFromMime(mimeType: GuessedMimeType): GuessedMimeContents { return GuessedMimeLookupTable[mimeType]?.dt ?? 'plain'; } diff --git a/src/modules/blocks/enhanced-code/EnhancedRenderCode.tsx b/src/modules/blocks/enhanced-code/EnhancedRenderCode.tsx index 97d69f5c7..984300bd8 100644 --- a/src/modules/blocks/enhanced-code/EnhancedRenderCode.tsx +++ b/src/modules/blocks/enhanced-code/EnhancedRenderCode.tsx @@ -198,6 +198,7 @@ export function EnhancedRenderCode(props: { {contextMenuAnchor && ( void, isCollapsed: boolean, onToggleCollapse: () => void, @@ -46,6 +51,33 @@ export function EnhancedRenderCodeMenu(props: { getCodeCollapseManager().triggerCollapseAll(false); }, []); + const handleSaveAs = React.useCallback(async () => { + // guess the mimetype from the markdown title + let mimeType = 'text/plain'; + let extension = ''; + const hasExtension = props.title.includes('.'); + if (hasExtension) { + extension = props.title.split('.').pop()!; + mimeType = reverseLookupMimeType(extension) || 'text/plain'; + } else { + const data = reverseLookupMdTitle(props.title); + if (data?.extension) + extension = data.extension; + if (data?.mimeType) + mimeType = data.mimeType; + } + + // content to be saved + const blob = new Blob([props.code], { type: mimeType }); + + // save content + await fileSave(blob, { + fileName: props.title || undefined, + extensions: extension ? [`.${extension}`] : undefined, + mimeTypes: mimeType ? [mimeType] : undefined, + }).catch(() => null); + }, [props.code, props.title]); + const toggleEnhanceCodeBlocks = React.useCallback(() => { // turn blocks on (may not even be called, ever) if (!labsEnhanceCodeBlocks) { @@ -69,6 +101,7 @@ export function EnhancedRenderCodeMenu(props: { const liveFileSupported = isLiveFileSupported(); + return ( - {/* TODO: add Download here */} + + + Save As ... + {(labsEnhanceCodeLiveFile && liveFileSupported) && } diff --git a/src/modules/trade/trade.client.ts b/src/modules/trade/trade.client.ts index e093b485f..fc147706f 100644 --- a/src/modules/trade/trade.client.ts +++ b/src/modules/trade/trade.client.ts @@ -135,7 +135,7 @@ export async function downloadAllJsonV1B() { fileName: `backup_chats_${window?.location?.hostname || 'all'}_${payload.conversations.length}_${prettyTimestampForFilenames(false)}.agi.json`, // mimeTypes: ['application/json', 'application/big-agi'], extensions: ['.json'], - }); + }).catch(() => null); } /** @@ -172,7 +172,7 @@ export async function downloadSingleChat(conversation: DConversation, format: 'j await fileSave(blob, { fileName: `conversation_${fileTitle}_${prettyTimestampForFilenames(false)}.agi${extension}`, extensions: [extension], - }); + }).catch(() => null); } /**