mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Add download app button (#111)
This commit is contained in:
parent
46b7b58fd5
commit
194d0336f4
57
app/components/header/DownloadButton.tsx
Normal file
57
app/components/header/DownloadButton.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -8,6 +8,7 @@ import { Feedback } from './Feedback';
|
|||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { ClientAuth } from '~/components/auth/ClientAuth';
|
import { ClientAuth } from '~/components/auth/ClientAuth';
|
||||||
import { DeployChatButton } from './DeployChatButton';
|
import { DeployChatButton } from './DeployChatButton';
|
||||||
|
import { DownloadButton } from './DownloadButton';
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const chatStarted = useStore(chatStore.started);
|
const chatStarted = useStore(chatStore.started);
|
||||||
@ -43,9 +44,14 @@ export function Header() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{chatStarted && (
|
{chatStarted && (
|
||||||
|
<>
|
||||||
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
|
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
|
||||||
<ClientOnly>{() => <DeployChatButton />}</ClientOnly>
|
<ClientOnly>{() => <DeployChatButton />}</ClientOnly>
|
||||||
</span>
|
</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">
|
<div className="flex items-center gap-4">
|
||||||
|
@ -65,3 +65,14 @@ export async function deployRepository(repositoryId: string, settings: DeploySet
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadRepository(repositoryId: string): Promise<string> {
|
||||||
|
const { repositoryContents } = (await sendCommandDedicatedClient({
|
||||||
|
method: 'Nut.getRepository',
|
||||||
|
params: {
|
||||||
|
repositoryId,
|
||||||
|
},
|
||||||
|
})) as { repositoryContents: string };
|
||||||
|
|
||||||
|
return repositoryContents;
|
||||||
|
}
|
||||||
|
@ -15,6 +15,6 @@ export async function updateDevelopmentServer(repositoryId: string | undefined)
|
|||||||
console.log('UpdateDevelopmentServer', new Date().toISOString(), repositoryURL);
|
console.log('UpdateDevelopmentServer', new Date().toISOString(), repositoryURL);
|
||||||
|
|
||||||
workbenchStore.showWorkbench.set(repositoryURL !== undefined);
|
workbenchStore.showWorkbench.set(repositoryURL !== undefined);
|
||||||
workbenchStore.repositoryId.set(repositoryURL);
|
workbenchStore.repositoryId.set(repositoryId);
|
||||||
workbenchStore.previewURL.set(repositoryURL);
|
workbenchStore.previewURL.set(repositoryURL);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user