Overview
ZapDev provides a split-pane interface that displays your generated application in real-time. As the AI writes code, you can instantly see the visual preview alongside the source code with syntax highlighting.
The live preview uses WebContainer technology to run your application directly in the browser - no backend server required for client-side frameworks.
Interface Layout
The ZapDev interface consists of three resizable panels:
┌─────────────────┬──────────────────────────────────┐
│ │ │
│ Chat Panel │ Preview / Code Tabs │
│ │ │
│ - Messages │ ┌────────────────────────────┐ │
│ - Input │ │ Preview Tab │ Code Tab │ │
│ - History │ └────────────────────────────┘ │
│ │ │
│ │ [Live rendering or file view] │
│ │ │
│ │ │
│ [35% width] │ [65% width] │
└─────────────────┴──────────────────────────────────┘
Resizable Panels
All panels are fully resizable using React Resizable:
// From src/modules/projects/ui/views/project-view.tsx
import { ResizablePanelGroup , ResizablePanel , ResizableHandle } from "@/components/ui/resizable" ;
< ResizablePanelGroup direction = "horizontal" >
< ResizablePanel defaultSize = { 35 } minSize = { 20 } >
{ /* Chat Panel */ }
</ ResizablePanel >
< ResizableHandle className = "hover:bg-primary transition-colors" />
< ResizablePanel defaultSize = { 65 } minSize = { 30 } >
{ /* Preview/Code Panel */ }
</ ResizablePanel >
</ ResizablePanelGroup >
Drag the handle between panels to adjust their sizes. Your layout preferences persist across sessions.
Preview Tab
The Preview tab shows your application running live in an iframe:
WebContainer Integration
ZapDev uses WebContainer API to run Node.js environments directly in the browser:
// From src/modules/projects/ui/components/webcontainer-preview.tsx
import { WebContainer } from '@webcontainer/api' ;
const webcontainer = await WebContainer . boot ();
// Listen for dev server ready
webcontainer . on ( 'server-ready' , ( port , url ) => {
setPreviewUrl ( url );
setLoading ( false );
});
// Write files to WebContainer filesystem
for ( const [ filePath , contents ] of Object . entries ( files )) {
await webcontainer . fs . writeFile ( filePath , contents );
}
// Install dependencies
await webcontainer . spawn ( 'npm' , [ 'install' ]);
// Start dev server
await webcontainer . spawn ( 'npm' , [ 'run' , 'dev' ]);
Preview Features
Auto-Refresh
Hot Module Replacement
Error Overlay
Loading States
The preview automatically refreshes when:
New files are generated by AI
Existing files are updated
Dependencies are installed
Build completes successfully
const [ refreshKey , setRefreshKey ] = useState ( 0 );
useEffect (() => {
if ( activeFragment ) {
setRefreshKey ( prev => prev + 1 );
}
}, [ activeFragment ]);
< WebContainerPreview
files = { explorerFiles }
refreshKey = { refreshKey }
/>
For frameworks with HMR support (Vite, Next.js Turbopack):
Changes apply instantly without full reload
Component state is preserved
Only modified modules update
Supported :
✅ Next.js (Turbopack)
✅ React + Vite
✅ Vue + Vite
✅ Svelte + Vite
⚠️ Angular (full reload required)
Build and runtime errors display in the preview: { error && (
< div className = "p-4 bg-destructive/10 border border-destructive" >
< h3 className = "text-destructive font-semibold" > Preview Error </ h3 >
< pre className = "text-sm mt-2" > { error } </ pre >
</ div >
)}
Error Types :
Build failures (TypeScript, ESLint)
Runtime exceptions
Module resolution errors
Network/CORS issues
Visual feedback during preview boot: { loading && (
< div className = "flex items-center justify-center h-full" >
< div className = "text-center" >
< Loader2 className = "h-8 w-8 animate-spin mx-auto" />
< p className = "mt-2 text-sm text-muted-foreground" >
Starting preview server...
</ p >
</ div >
</ div >
)}
Framework-Specific Ports
Each framework runs on its designated port:
Framework Port Dev Command Next.js 3000 npx next dev --turbopackAngular 4200 ng serveReact 5173 vite devVue 5173 vite devSvelte 5173 vite dev
// WebContainer automatically detects the correct port
webcontainer . on ( 'server-ready' , ( port , url ) => {
console . log ( `Server ready on port ${ port } ` );
// Next.js: http://localhost:3000
// Vite: http://localhost:5173
});
Code Tab
The Code tab displays a file explorer with syntax-highlighted source code:
File Explorer Structure
// From src/components/file-explorer.tsx
export const FileExplorer = ({ files } : { files : Record < string , string > }) => {
const [ selectedFile , setSelectedFile ] = useState < string | null >( null );
const treeData = useMemo (() => convertFilesToTreeItems ( files ), [ files ]);
return (
< ResizablePanelGroup direction = "horizontal" >
{ /* File Tree Panel */ }
< ResizablePanel defaultSize = { 30 } minSize = { 20 } >
< TreeView
data = { treeData }
value = { selectedFile }
onSelect = { setSelectedFile }
/>
</ ResizablePanel >
< ResizableHandle />
{ /* Code Viewer Panel */ }
< ResizablePanel defaultSize = { 70 } minSize = { 50 } >
< CodeView
code = { files [ selectedFile ] }
lang = { getLanguageFromExtension ( selectedFile ) }
/>
</ ResizablePanel >
</ ResizablePanelGroup >
);
};
Tree View
Hierarchical file browser with expand/collapse:
📁 app/
├── 📄 page.tsx
├── 📄 layout.tsx
└── 📄 globals.css
📁 components/
├── 📁 ui/
│ ├── 📄 button.tsx
│ ├── 📄 card.tsx
│ └── 📄 dialog.tsx
└── 📄 hero.tsx
📁 lib/
└── 📄 utils.ts
📄 package.json
📄 tsconfig.json
Features :
Click to select/view file
Expand/collapse folders
Breadcrumb navigation
Syntax Highlighting
Prism.js provides language-aware highlighting:
// From src/components/code-view/index.tsx
import Prism from "prismjs" ;
import "prismjs/components/prism-javascript" ;
import "prismjs/components/prism-jsx" ;
import "prismjs/components/prism-tsx" ;
import "prismjs/components/prism-typescript" ;
export const CodeView = ({ code , lang } : { code : string ; lang : string }) => {
useEffect (() => {
Prism . highlightAll ();
}, [ code ]);
return (
< pre className = "p-2 bg-transparent" >
< code className = { `language- ${ lang } ` } >
{ code }
</ code >
</ pre >
);
};
Supported Languages :
TypeScript (.ts, .tsx)
JavaScript (.js, .jsx)
JSON (.json)
CSS/SCSS (.css, .scss)
HTML (.html)
Markdown (.md)
YAML (.yaml, .yml)
File Breadcrumbs
Navigate deep file paths easily:
// From src/components/file-explorer.tsx
const FileBreadcrumb = ({ filePath } : { filePath : string }) => {
const pathSegments = filePath . split ( "/" );
return (
< Breadcrumb >
< BreadcrumbList >
{ pathSegments . map (( segment , index ) => {
const isLast = index === pathSegments . length - 1 ;
return (
< Fragment key = { index } >
< BreadcrumbItem >
{ isLast ? (
< BreadcrumbPage className = "font-medium" >
{ segment }
</ BreadcrumbPage >
) : (
< span className = "text-muted-foreground" > { segment } </ span >
) }
</ BreadcrumbItem >
{ ! isLast && < BreadcrumbSeparator /> }
</ Fragment >
);
}) }
</ BreadcrumbList >
</ Breadcrumb >
);
};
Example :
components / ui / button.tsx
For long paths:
Copy to Clipboard
Quick copy button for each file:
const handleCopy = useCallback (() => {
if ( selectedFile ) {
navigator . clipboard . writeText ( files [ selectedFile ]);
setCopied ( true );
setTimeout (() => setCopied ( false ), 2000 );
}
}, [ selectedFile , files ]);
< Button variant = "outline" size = "icon" onClick = { handleCopy } >
{ copied ? < CopyCheckIcon /> : < CopyIcon /> }
</ Button >
Tab Switching
Seamless transitions between Preview and Code:
// From src/modules/projects/ui/views/project-view.tsx
import { Tabs , TabsList , TabsTrigger , TabsContent } from "@/components/ui/tabs" ;
const [ tabState , setTabState ] = useState < "preview" | "code" >( "preview" );
< Tabs value = { tabState } onValueChange = { setTabState } >
< TabsList >
< TabsTrigger value = "preview" >
< EyeIcon className = "h-4 w-4 mr-2" />
Preview
</ TabsTrigger >
< TabsTrigger value = "code" >
< CodeIcon className = "h-4 w-4 mr-2" />
Code
</ TabsTrigger >
</ TabsList >
< TabsContent value = "preview" >
< WebContainerPreview files = { files } refreshKey = { refreshKey } />
</ TabsContent >
< TabsContent value = "code" >
< FileExplorer files = { files } />
</ TabsContent >
</ Tabs >
Auto-Switching :
Automatically switches to Code tab when files start streaming
Manual switching at any time
Tab state persists during session
Real-Time Streaming
As the AI generates code, files appear instantly:
const [ streamingFiles , setStreamingFiles ] = useState < Record < string , string >>({});
const handleStreamingFiles = ( files : Record < string , string >) => {
setStreamingFiles ( files );
// Auto-switch to code tab when files arrive
if ( Object . keys ( files ). length > 0 && tabState === "preview" ) {
setTabState ( "code" );
}
};
// Merge streaming files with final fragment files
const explorerFiles = useMemo (() => {
let files = { ... streamingFiles };
if ( activeFragment ?. files ) {
files = { ... files , ... activeFragment . files };
}
return filterAIGeneratedFiles ( files );
}, [ activeFragment , streamingFiles ]);
File Filtering
Only AI-generated files are shown (E2B system files are hidden):
// From src/lib/filter-ai-files.ts
export function filterAIGeneratedFiles ( files : Record < string , string >) {
const EXCLUDED_PATTERNS = [
'node_modules/' ,
'.next/' ,
'dist/' ,
'build/' ,
'.git/' ,
'package-lock.json' ,
'bun.lockb'
];
return Object . entries ( files )
. filter (([ path ]) => ! EXCLUDED_PATTERNS . some ( pattern => path . includes ( pattern )))
. reduce (( acc , [ path , content ]) => ({ ... acc , [path]: content }), {});
}
Dynamic Imports
Heavy components load on-demand:
import dynamic from 'next/dynamic' ;
const FileExplorer = dynamic (
() => import ( '@/components/file-explorer' ). then ( m => m . FileExplorer ),
{
loading : () => < p className = "p-4" > Loading file explorer... </ p > ,
ssr: false // Client-side only
}
);
const WebContainerPreview = dynamic (
() => import ( '../components/webcontainer-preview' ). then ( m => m . WebContainerPreview ),
{
loading : () => < p className = "p-4" > Loading preview... </ p > ,
ssr: false
}
);
Memoization
Expensive computations are cached:
// Tree data conversion
const treeData = useMemo (() => {
return convertFilesToTreeItems ( files );
}, [ files ]);
// File merging
const explorerFiles = useMemo (() => {
return filterAIGeneratedFiles ({ ... streamingFiles , ... fragmentFiles });
}, [ streamingFiles , fragmentFiles ]);
Debounced Updates
Preview refreshes are throttled during rapid file changes:
const debouncedRefresh = useMemo (
() => debounce (() => setRefreshKey ( k => k + 1 ), 500 ),
[]
);
useEffect (() => {
debouncedRefresh ();
}, [ files ]);
Keyboard Shortcuts
Shortcut Action Cmd/Ctrl + PToggle Preview/Code tabs Cmd/Ctrl + CCopy selected file Cmd/Ctrl + BToggle file tree
Error Handling
Preview Errors
< ErrorBoundary
fallback = { < p className = "p-4 text-destructive" > Preview failed to load </ p > }
>
< WebContainerPreview files = { files } refreshKey = { refreshKey } />
</ ErrorBoundary >
Code View Errors
try {
const highlighted = Prism . highlight ( code , Prism . languages [ lang ], lang );
return < code dangerouslySetInnerHTML = { { __html: highlighted } } /> ;
} catch ( error ) {
return < code className = "text-destructive" > Failed to highlight code </ code > ;
}
Next Steps
AI Code Generation See how code streams to the preview in real-time
File Explorer Deep dive into the file browsing experience
Real-Time Sandboxes Learn about WebContainer and E2B integration
Multi-Framework Support Explore preview behavior across frameworks