diff --git a/packages/cta-ui-base/package.json b/packages/cta-ui-base/package.json index 116b35fa..27b2a43a 100644 --- a/packages/cta-ui-base/package.json +++ b/packages/cta-ui-base/package.json @@ -32,6 +32,7 @@ "@tanstack/react-query": "^5.66.5", "@uiw/codemirror-theme-github": "^4.23.10", "@uiw/react-codemirror": "^4.23.10", + "@webcontainer/api": "^1.3.5", "chalk": "^5.4.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/packages/cta-ui-base/src/components/file-navigator.tsx b/packages/cta-ui-base/src/components/file-navigator.tsx index 7dbd64c8..3e694235 100644 --- a/packages/cta-ui-base/src/components/file-navigator.tsx +++ b/packages/cta-ui-base/src/components/file-navigator.tsx @@ -14,9 +14,12 @@ import { getFileClass, twClasses } from '../file-classes' import FileViewer from './file-viewer' import FileTree from './file-tree' +import WebContainerProvider from './web-container-provider' +import { WebContainerPreview } from './webcontainer-preview' import { Label } from './ui/label' import { Switch } from './ui/switch' +import { Tabs, TabsList, TabsTrigger, TabsContent } from './ui/tabs' import type { FileTreeItem } from '../types' @@ -181,27 +184,69 @@ export default function FileNavigator() { const ready = useReady() + // Prepare project files for WebContainer + const webContainerFiles = useMemo(() => { + console.log('Preparing WebContainer files, tree:', tree) + if (!tree) { + console.log('Tree is empty, returning empty array') + return [] + } + const files = Object.entries(tree).map(([path, content]) => ({ + path, + content, + })) + console.log('WebContainer files prepared:', files.length, 'files') + return files + }, [tree]) + if (!ready) { return null } return ( -
- {mode === 'add' && } -
-
- -
-
- {selectedFile && modifiedFileContents ? ( - - ) : null} -
+ +
+ {mode === 'add' && } + + + + Files + + + Preview + + + + +
+
+ +
+
+ {selectedFile && modifiedFileContents ? ( + + ) : null} +
+
+
+ + +
+ +
+
+
-
+ ) } diff --git a/packages/cta-ui-base/src/components/file-viewer.tsx b/packages/cta-ui-base/src/components/file-viewer.tsx index 87a21e7e..ee816622 100644 --- a/packages/cta-ui-base/src/components/file-viewer.tsx +++ b/packages/cta-ui-base/src/components/file-viewer.tsx @@ -45,10 +45,18 @@ export default function FileViewer({ } const language = getLanguage(filePath) - if (!originalFile || originalFile === modifiedFile) { + // Display placeholder for binary files + const displayModified = modifiedFile.startsWith('base64::') + ? '' + : modifiedFile + const displayOriginal = originalFile?.startsWith('base64::') + ? '' + : originalFile + + if (!displayOriginal || displayOriginal === displayModified) { return ( - - + + ) } diff --git a/packages/cta-ui-base/src/components/web-container-provider.tsx b/packages/cta-ui-base/src/components/web-container-provider.tsx new file mode 100644 index 00000000..a843b847 --- /dev/null +++ b/packages/cta-ui-base/src/components/web-container-provider.tsx @@ -0,0 +1,42 @@ +import { createContext, useEffect, useState } from 'react' +import { useStore } from 'zustand' +import createWebContainerStore from '../hooks/use-webcontainer-store' + +export const WebContainerContext = createContext | null>(null) + +export default function WebContainerProvider({ + children, + projectFiles, +}: { + children: React.ReactNode + projectFiles: Array<{ path: string; content: string }> +}) { + console.log( + 'WebContainerProvider rendering with', + projectFiles.length, + 'files', + ) + const [containerStore] = useState(() => createWebContainerStore(true)) + + const updateProjectFiles = useStore( + containerStore, + (state) => state.updateProjectFiles, + ) + + useEffect(() => { + console.log( + 'WebContainerProvider useEffect triggered with', + projectFiles.length, + 'files', + ) + updateProjectFiles(projectFiles) + }, [updateProjectFiles, projectFiles]) + + return ( + + {children} + + ) +} diff --git a/packages/cta-ui-base/src/components/webcontainer-preview.tsx b/packages/cta-ui-base/src/components/webcontainer-preview.tsx new file mode 100644 index 00000000..8b6d0ca8 --- /dev/null +++ b/packages/cta-ui-base/src/components/webcontainer-preview.tsx @@ -0,0 +1,241 @@ +import { useContext, useEffect, useRef, useState } from 'react' +import { useStore } from 'zustand' +import { ChevronDown, ChevronUp } from 'lucide-react' + +import type { SetupStep } from '../hooks/use-webcontainer-store' +import { WebContainerContext } from './web-container-provider' + +export function WebContainerPreview() { + const containerStore = useContext(WebContainerContext) + if (!containerStore) { + throw new Error('WebContainerContext not found') + } + + const webContainer = useStore(containerStore, (state) => state.webContainer) + const setupStep = useStore(containerStore, (state) => state.setupStep) + const statusMessage = useStore(containerStore, (state) => state.statusMessage) + const terminalOutput = useStore( + containerStore, + (state) => state.terminalOutput, + ) + const error = useStore(containerStore, (state) => state.error) + const previewUrl = useStore(containerStore, (state) => state.previewUrl) + const startDevServer = useStore( + containerStore, + (state) => state.startDevServer, + ) + const setTerminalOutput = useStore( + containerStore, + (state) => state.setTerminalOutput, + ) + + const [isTerminalOpen, setIsTerminalOpen] = useState(false) + + // Auto-scroll terminal to bottom when new output arrives + const terminalRef = useRef(null) + useEffect(() => { + if (terminalRef.current) { + terminalRef.current.scrollTop = terminalRef.current.scrollHeight + } + }, [terminalOutput]) + + const getStepIcon = (step: SetupStep) => { + switch (step) { + case 'mounting': + return 'πŸ“' + case 'installing': + return 'πŸ“¦' + case 'starting': + return 'πŸš€' + case 'ready': + return 'βœ…' + case 'error': + return '❌' + } + } + + const getStepColor = (step: SetupStep) => { + switch (step) { + case 'error': + return 'text-red-500' + case 'ready': + return 'text-green-500' + default: + return 'text-blue-500' + } + } + + // Show progress dialog during setup (similar to "Creating Your Application") + if ( + !webContainer || + setupStep === 'error' || + setupStep !== 'ready' || + !previewUrl + ) { + return ( +
+
+

+ {setupStep === 'error' ? 'Setup Failed' : 'Preparing Preview'} +

+ + {setupStep === 'error' ? ( +
+
❌
+
+ An error occurred +
+
+ {error} +
+ +
+ ) : ( + <> + {/* Progress Steps */} +
+
+
{getStepIcon('mounting')}
+
+ Mount Files +
+ {(setupStep === 'installing' || + setupStep === 'starting' || + setupStep === 'ready') && + 'βœ“'} +
+
+
{getStepIcon('installing')}
+
+ Install Dependencies +
+ {(setupStep === 'starting' || setupStep === 'ready') && 'βœ“'} +
+
+
{getStepIcon('starting')}
+
+ Start Server +
+ {setupStep === 'ready' && 'βœ“'} +
+
+ + {/* Current status */} +
+ {statusMessage || 'Preparing your application...'} +
+ + )} +
+
+ ) + } + + // Show the running application with collapsible terminal + return ( +
+ {/* iframe with the running app */} +
+ {previewUrl ? ( +