import { useStore } from '@nanostores/react'; import { motion, type HTMLMotionProps, type Variants } from 'framer-motion'; import { computed } from 'nanostores'; import { memo, useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { type OnChangeCallback as OnEditorChange, type OnScrollCallback as OnEditorScroll, } from '~/components/editor/codemirror/CodeMirrorEditor'; import { IconButton } from '~/components/ui/IconButton'; import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton'; import { Slider, type SliderOptions } from '~/components/ui/Slider'; import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench'; import { classNames } from '~/utils/classNames'; import { cubicEasingFn } from '~/utils/easings'; import { renderLogger } from '~/utils/logger'; import { EditorPanel } from './EditorPanel'; import { Preview } from './Preview'; import useViewport from '~/lib/hooks'; import Cookies from 'js-cookie'; interface WorkspaceProps { chatStarted?: boolean; isStreaming?: boolean; } const viewTransition = { ease: cubicEasingFn }; const sliderOptions: SliderOptions = { left: { value: 'code', text: 'Code', }, right: { value: 'preview', text: 'Preview', }, }; const workbenchVariants = { closed: { width: 0, transition: { duration: 0.2, ease: cubicEasingFn, }, }, open: { width: 'var(--workbench-width)', transition: { duration: 0.2, ease: cubicEasingFn, }, }, } satisfies Variants; export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => { renderLogger.trace('Workbench'); const [isSyncing, setIsSyncing] = useState(false); const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0)); const showWorkbench = useStore(workbenchStore.showWorkbench); const selectedFile = useStore(workbenchStore.selectedFile); const currentDocument = useStore(workbenchStore.currentDocument); const unsavedFiles = useStore(workbenchStore.unsavedFiles); const files = useStore(workbenchStore.files); const selectedView = useStore(workbenchStore.currentView); const isSmallViewport = useViewport(1024); const setSelectedView = (view: WorkbenchViewType) => { workbenchStore.currentView.set(view); }; useEffect(() => { if (hasPreview) { setSelectedView('preview'); } }, [hasPreview]); useEffect(() => { workbenchStore.setDocuments(files); }, [files]); const onEditorChange = useCallback((update) => { workbenchStore.setCurrentDocumentContent(update.content); }, []); const onEditorScroll = useCallback((position) => { workbenchStore.setCurrentDocumentScrollPosition(position); }, []); const onFileSelect = useCallback((filePath: string | undefined) => { workbenchStore.setSelectedFile(filePath); }, []); const onFileSave = useCallback(() => { workbenchStore.saveCurrentDocument().catch(() => { toast.error('Failed to update file content'); }); }, []); const onFileReset = useCallback(() => { workbenchStore.resetCurrentDocument(); }, []); const handleSyncFiles = useCallback(async () => { setIsSyncing(true); try { const directoryHandle = await window.showDirectoryPicker(); await workbenchStore.syncFiles(directoryHandle); toast.success('Files synced successfully'); } catch (error) { console.error('Error syncing files:', error); toast.error('Failed to sync files'); } finally { setIsSyncing(false); } }, []); return ( chatStarted && (
{selectedView === 'code' && (
{ workbenchStore.downloadZip(); }} >
Download Code {isSyncing ?
:
} {isSyncing ? 'Syncing...' : 'Sync Files'} { workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get()); }} >
Toggle Terminal { const repoName = prompt( 'Please enter a name for your new GitHub repository:', 'bolt-generated-project', ); if (!repoName) { alert('Repository name is required. Push to GitHub cancelled.'); return; } const githubUsername = Cookies.get('githubUsername'); const githubToken = Cookies.get('githubToken'); if (!githubUsername || !githubToken) { const usernameInput = prompt('Please enter your GitHub username:'); const tokenInput = prompt('Please enter your GitHub personal access token:'); if (!usernameInput || !tokenInput) { alert('GitHub username and token are required. Push to GitHub cancelled.'); return; } workbenchStore.pushToGitHub(repoName, usernameInput, tokenInput); } else { workbenchStore.pushToGitHub(repoName, githubUsername, githubToken); } }} >
Push to GitHub
)} { workbenchStore.showWorkbench.set(false); }} />
) ); }); interface ViewProps extends HTMLMotionProps<'div'> { children: JSX.Element; } const View = memo(({ children, ...props }: ViewProps) => { return ( {children} ); });