From 087118113ce8acc1186315e1cf82c9ff9549a88d Mon Sep 17 00:00:00 2001 From: Jack Herrington Date: Tue, 7 Oct 2025 06:41:58 -0700 Subject: [PATCH 1/4] feat: CTA-UI preview --- packages/cta-ui-base/package.json | 1 + .../src/components/file-navigator.tsx | 63 ++- .../src/components/file-viewer.tsx | 22 +- .../src/components/web-container-provider.tsx | 42 ++ .../src/components/webcontainer-preview.tsx | 250 ++++++++++ .../src/hooks/use-web-container.ts | 7 + .../src/hooks/use-webcontainer-store.ts | 431 ++++++++++++++++++ packages/cta-ui-base/src/index.ts | 8 +- packages/cta-ui-base/src/lib/als-shim.ts | 127 ++++++ .../lib/engine-handling/add-to-app-wrapper.ts | 3 +- .../lib/engine-handling/create-app-wrapper.ts | 11 +- .../lib/engine-handling/file-helpers.ts | 8 +- .../generate-initial-payload.ts | 6 +- packages/cta-ui/vite.config.ts | 4 + pnpm-lock.yaml | 8 + 15 files changed, 963 insertions(+), 28 deletions(-) create mode 100644 packages/cta-ui-base/src/components/web-container-provider.tsx create mode 100644 packages/cta-ui-base/src/components/webcontainer-preview.tsx create mode 100644 packages/cta-ui-base/src/hooks/use-web-container.ts create mode 100644 packages/cta-ui-base/src/hooks/use-webcontainer-store.ts create mode 100644 packages/cta-ui-base/src/lib/als-shim.ts 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..c34b48ff 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,6 +184,21 @@ 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 } @@ -188,20 +206,37 @@ export default function FileNavigator() { return (
{mode === 'add' && } -
-
- -
-
- {selectedFile && modifiedFileContents ? ( - - ) : null} -
-
+ + + 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..0c35122c --- /dev/null +++ b/packages/cta-ui-base/src/components/webcontainer-preview.tsx @@ -0,0 +1,250 @@ +import { useContext } from 'react' +import { useStore } from 'zustand' + +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 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' + } + } + + if (!webContainer) { + return ( +
+
+
⏳
+
+ Initializing WebContainer... +
+
+
+ ) + } + + if (setupStep === 'error') { + return ( +
+
+
❌
+
+ Setup Failed +
+
+ {error} +
+
+
+ ) + } + + if (setupStep !== 'ready' || !previewUrl) { + return ( +
+ {/* Progress Header */} +
+
+
{getStepIcon(setupStep)}
+
+ {statusMessage} +
+
+ + {/* Progress Steps */} +
+
+
+ Mount Files +
+
+
+ Install Dependencies +
+
+
+ Start Server +
+
+
+ + {/* Terminal Output */} +
+
+ {terminalOutput.length > 0 ? ( + terminalOutput.map((line, index) => ( +
+ {line} +
+ )) + ) : ( +
Waiting for output...
+ )} +
+
+
+ ) + } + + // Show the running application with persistent terminal + return ( +
+ {/* Header with URL and controls */} +
+
+
βœ…
+
+
+ {setupStep === 'ready' + ? 'Development Server Running' + : statusMessage} +
+
+ {previewUrl || 'No URL available'} +
+
+
+ + {previewUrl && ( + + )} +
+
+
+ + {/* iframe with the running app - 80% height */} +
+ {previewUrl ? ( +