Skip to content

Commit

Permalink
Add Download Button for Code Snippets (#693)
Browse files Browse the repository at this point in the history
* added ButtonDownload to code snippet

* added ButtonDownload.stories.tsx

* reverted tailwind.config.js filename

* fix lint error

* change type definition from global to local

---------

Co-authored-by: iwasawamnb <[email protected]>
  • Loading branch information
iwasawamnb and iwasawamnb authored Jan 30, 2025
1 parent 93869ea commit aee2433
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 1 deletion.
3 changes: 3 additions & 0 deletions frontend/src/components/ButtonDownload.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ButtonDownload from './ButtonDownload';

export const Ideal = () => <ButtonDownload text={'DownloadText'} />;
86 changes: 86 additions & 0 deletions frontend/src/components/ButtonDownload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { useCallback } from 'react';
import ButtonIcon from './ButtonIcon';
import { BaseProps } from '../@types/common';
import { PiDownload } from 'react-icons/pi';

interface WindowWithFileSaveAPI {
showSaveFilePicker?: (options?: {
suggestedName?: string;
types?: Array<{
description: string;
accept: Record<string, string[]>;
}>;
}) => Promise<FileSystemFileHandle>;
}

type Props = BaseProps & {
text: string;
};

const ButtonDownload: React.FC<Props> = (props) => {
const getDefaultFileName = () => {
const now = new Date();
const date = now.getFullYear() +
('0' + (now.getMonth() + 1)).slice(-2) +
('0' + now.getDate()).slice(-2);

const time = ('0' + now.getHours()).slice(-2) +
('0' + now.getMinutes()).slice(-2) +
('0' + now.getSeconds()).slice(-2) +
('00' + now.getMilliseconds()).slice(-3);

const timestamp = `${date}_${time}`;

return `code_${timestamp}.txt`;
};

const downloadText = useCallback(async (text: string) => {
const fileName = getDefaultFileName();
const blob = new Blob([text], { type: 'text/plain' });

try {
// Modern browsers - Using File System Access API
const savePicker = (window as Window & WindowWithFileSaveAPI).showSaveFilePicker;
if (savePicker) {
try {
const handle = await savePicker({
suggestedName: fileName,
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
// User cancelled the save dialog or other error
if (err.name !== 'AbortError') {
console.error('Failed to save file:', err);
}
return;
}
}

// Fallback for older browsers
const downloadLink = document.createElement('a');
downloadLink.download = fileName;
downloadLink.href = window.URL.createObjectURL(blob);
downloadLink.click();
window.URL.revokeObjectURL(downloadLink.href);

} catch (error) {
console.error('Failed to download file:', error);
}
}, []);

return (
<ButtonIcon
className={props.className}
onClick={() => {
downloadText(props.text);
}}>
<PiDownload />
</ButtonIcon>
);
};

export default ButtonDownload;
6 changes: 5 additions & 1 deletion frontend/src/components/ChatMessageMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
import ButtonDownload from './ButtonDownload';
import ButtonCopy from './ButtonCopy';
import { RelatedDocument } from '../@types/conversation';
import { twMerge } from 'tailwind-merge';
Expand Down Expand Up @@ -244,7 +245,10 @@ const CopyToClipboard = ({
return (
<div className="relative">
{children}
<ButtonCopy text={codeText} className="absolute right-2 top-2" />
<div className="absolute right-2 top-2 flex gap-0">
<ButtonDownload text={codeText} />
<ButtonCopy text={codeText} />
</div>
</div>
);
};
Expand Down

0 comments on commit aee2433

Please sign in to comment.