Add download app button (#111)

This commit is contained in:
Brian Hackett 2025-04-30 05:52:03 -10:00 committed by GitHub
parent 46b7b58fd5
commit 194d0336f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 78 additions and 65 deletions

View File

@ -0,0 +1,57 @@
import ReactModal from 'react-modal';
import { toast } from 'react-toastify';
import { workbenchStore } from '~/lib/stores/workbench';
import { downloadRepository } from '~/lib/replay/Deploy';
ReactModal.setAppElement('#root');
// Component for downloading an app's contents to disk.
export function DownloadButton() {
const handleDownload = async () => {
const repositoryId = workbenchStore.repositoryId.get();
if (!repositoryId) {
toast.error('No repository ID found');
return;
}
try {
const repositoryContents = await downloadRepository(repositoryId);
// Convert base64 to blob
const byteCharacters = atob(repositoryContents);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: 'application/zip' });
// Create download link and trigger save dialog
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `repository-${repositoryId}.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
toast.success('Repository downloaded successfully');
} catch (error) {
console.error('Error downloading repository:', error);
toast.error('Failed to download repository');
}
};
return (
<>
<button
className="flex gap-2 bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
onClick={handleDownload}
>
<div className="i-ph:download-fill text-[1.3em]" />
</button>
</>
);
}

View File

@ -8,6 +8,7 @@ import { Feedback } from './Feedback';
import { Suspense } from 'react';
import { ClientAuth } from '~/components/auth/ClientAuth';
import { DeployChatButton } from './DeployChatButton';
import { DownloadButton } from './DownloadButton';
export function Header() {
const chatStarted = useStore(chatStore.started);
@ -43,9 +44,14 @@ export function Header() {
)}
{chatStarted && (
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
<ClientOnly>{() => <DeployChatButton />}</ClientOnly>
</span>
<>
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
<ClientOnly>{() => <DeployChatButton />}</ClientOnly>
</span>
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
<ClientOnly>{() => <DownloadButton />}</ClientOnly>
</span>
</>
)}
<div className="flex items-center gap-4">

View File

@ -65,3 +65,14 @@ export async function deployRepository(repositoryId: string, settings: DeploySet
return result;
}
export async function downloadRepository(repositoryId: string): Promise<string> {
const { repositoryContents } = (await sendCommandDedicatedClient({
method: 'Nut.getRepository',
params: {
repositoryId,
},
})) as { repositoryContents: string };
return repositoryContents;
}

View File

@ -15,6 +15,6 @@ export async function updateDevelopmentServer(repositoryId: string | undefined)
console.log('UpdateDevelopmentServer', new Date().toISOString(), repositoryURL);
workbenchStore.showWorkbench.set(repositoryURL !== undefined);
workbenchStore.repositoryId.set(repositoryURL);
workbenchStore.repositoryId.set(repositoryId);
workbenchStore.previewURL.set(repositoryURL);
}

View File

@ -1,61 +0,0 @@
import type { Message } from '~/lib/persistence/message';
import { generateId } from './fileUtils';
import JSZip from 'jszip';
interface FileArtifact {
content: string;
path: string;
}
export async function getFileRepositoryContents(files: File[]): Promise<string> {
const artifacts: FileArtifact[] = await Promise.all(
files.map(async (file) => {
return new Promise<FileArtifact>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const content = reader.result as string;
const relativePath = file.webkitRelativePath.split('/').slice(1).join('/');
resolve({
content,
path: relativePath,
});
};
reader.onerror = reject;
reader.readAsText(file);
});
}),
);
const zip = new JSZip();
for (const { path, content } of artifacts) {
zip.file(path, content);
}
return await zip.generateAsync({ type: 'base64' });
}
// TODO: Common up with createMessagesForRepository.
export function createChatFromFolder(folderName: string, repositoryId: string): Message[] {
const filesContent = `I've imported the contents of the "${folderName}" folder.`;
const userMessage: Message = {
role: 'user',
id: generateId(),
content: `Import the "${folderName}" folder`,
type: 'text',
};
const filesMessage: Message = {
role: 'assistant',
content: filesContent,
id: generateId(),
repositoryId,
type: 'text',
};
const messages = [userMessage, filesMessage];
return messages;
}