Skip to main content

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

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} 
/>

Framework-Specific Ports

Each framework runs on its designated port:
FrameworkPortDev Command
Next.js3000npx next dev --turbopack
Angular4200ng serve
React5173vite dev
Vue5173vite dev
Svelte5173vite 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:
app / ... / button.tsx

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 }), {});
}

Performance Optimizations

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

ShortcutAction
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

Build docs developers (and LLMs) love