import { useStore } from '@nanostores/react'; import { motion, type HTMLMotionProps, type Variants } from 'framer-motion'; import { computed } from 'nanostores'; import { memo, useCallback, useEffect, useState, useMemo } from 'react'; import { toast } from 'react-toastify'; import { Popover, Transition } from '@headlessui/react'; import { diffLines, type Change } from 'diff'; import { ActionRunner } from '~/lib/runtime/action-runner'; import { getLanguageFromExtension } from '~/utils/getLanguageFromExtension'; import type { FileHistory } from '~/types/actions'; import { DiffView } from './DiffView'; 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 { PushToGitHubDialog } from '~/components/@settings/tabs/connections/components/PushToGitHubDialog'; interface WorkspaceProps { chatStarted?: boolean; isStreaming?: boolean; actionRunner: ActionRunner; metadata?: { gitUrl?: string; }; updateChatMestaData?: (metadata: any) => void; } const viewTransition = { ease: cubicEasingFn }; const sliderOptions: SliderOptions = { left: { value: 'code', text: 'Code', }, middle: { value: 'diff', text: 'Diff', }, 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; const FileModifiedDropdown = memo( ({ fileHistory, onSelectFile, }: { fileHistory: Record; onSelectFile: (filePath: string) => void; }) => { const modifiedFiles = Object.entries(fileHistory); const hasChanges = modifiedFiles.length > 0; const [searchQuery, setSearchQuery] = useState(''); const filteredFiles = useMemo(() => { return modifiedFiles.filter(([filePath]) => filePath.toLowerCase().includes(searchQuery.toLowerCase())); }, [modifiedFiles, searchQuery]); return (
{({ open }: { open: boolean }) => ( <> File Changes {hasChanges && ( {modifiedFiles.length} )}
setSearchQuery(e.target.value)} className="w-full pl-8 pr-3 py-1.5 text-sm rounded-lg bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor focus:outline-none focus:ring-2 focus:ring-blue-500/50" />
{filteredFiles.length > 0 ? ( filteredFiles.map(([filePath, history]) => { const extension = filePath.split('.').pop() || ''; const language = getLanguageFromExtension(extension); return ( ); }) ) : (

{searchQuery ? 'No matching files' : 'No modified files'}

{searchQuery ? 'Try another search' : 'Changes will appear here as you edit'}

)}
{hasChanges && (
)} )}
); }, ); export const Workbench = memo( ({ chatStarted, isStreaming, actionRunner, metadata, updateChatMestaData }: WorkspaceProps) => { renderLogger.trace('Workbench'); const [isSyncing, setIsSyncing] = useState(false); const [isPushDialogOpen, setIsPushDialogOpen] = useState(false); const [fileHistory, setFileHistory] = useState>({}); // const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys()); 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); } }, []); const handleSelectFile = useCallback((filePath: string) => { workbenchStore.setSelectedFile(filePath); workbenchStore.currentView.set('diff'); }, []); return ( chatStarted && (
{selectedView === 'code' && (
{ workbenchStore.downloadZip(); }} >
Download Code {isSyncing ?
:
} {isSyncing ? 'Syncing...' : 'Sync Files'} { workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get()); }} >
Toggle Terminal setIsPushDialogOpen(true)}>
Push to GitHub
)} {selectedView === 'diff' && ( )} { workbenchStore.showWorkbench.set(false); }} />
setIsPushDialogOpen(false)} onPush={async (repoName, username, token) => { try { const commitMessage = prompt('Please enter a commit message:', 'Initial commit') || 'Initial commit'; await workbenchStore.pushToGitHub(repoName, commitMessage, username, token); const repoUrl = `https://github.com/${username}/${repoName}`; if (updateChatMestaData && !metadata?.gitUrl) { updateChatMestaData({ ...(metadata || {}), gitUrl: repoUrl, }); } return repoUrl; } catch (error) { console.error('Error pushing to GitHub:', error); toast.error('Failed to push to GitHub'); throw error; } }} /> ) ); }, ); // View component for rendering content with motion transitions interface ViewProps extends HTMLMotionProps<'div'> { children: JSX.Element; } const View = memo(({ children, ...props }: ViewProps) => { return ( {children} ); });