Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions app/api/save-file/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from 'next/server';
import { sandboxManager } from '@/lib/sandbox/sandbox-manager';

declare global {
var activeSandboxProvider: any;
}

export async function POST(req: NextRequest) {
try {
const { sandboxId, filePath, content } = await req.json();

if (!filePath) {
return NextResponse.json({ success: false, error: 'filePath is required' }, { status: 400 });
}

let provider = sandboxId ? sandboxManager.getProvider(sandboxId) : sandboxManager.getActiveProvider();
if (!provider) {
provider = global.activeSandboxProvider;
}

if (!provider) {
return NextResponse.json({ success: false, error: 'No active sandbox provider found' }, { status: 404 });
}

const normalizedPath = filePath.startsWith('/') ? filePath.slice(1) : filePath;

await provider.writeFile(normalizedPath, content);

return NextResponse.json({ success: true, message: `File ${normalizedPath} updated successfully` });
} catch (error) {
console.error('[save-file] Error saving file:', error);
return NextResponse.json({ success: false, error: (error as Error).message }, { status: 500 });
}
}
186 changes: 161 additions & 25 deletions app/generation/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import HeaderBrandKit from '@/components/shared/header/BrandKit/BrandKit';
import { HeaderProvider } from '@/components/shared/header/HeaderContext';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import Editor from '@monaco-editor/react';
// Import icons from centralized module to avoid Turbopack chunk issues
import {
FiFile,
Expand Down Expand Up @@ -94,6 +95,128 @@ function AISandboxPage() {
const [hasInitialSubmission, setHasInitialSubmission] = useState<boolean>(false);
const [fileStructure, setFileStructure] = useState<string>('');

const [editorValue, setEditorValue] = useState<string>('');
const [editingFile, setEditingFile] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);

const getFileContent = (path: string): string => {
if (sandboxFiles && sandboxFiles[path]) {
return sandboxFiles[path];
}
const file = generationProgress.files.find(f => f.path === path);
return file?.content || '';
};

useEffect(() => {
if (selectedFile) {
if (editingFile !== selectedFile) {
setEditorValue(getFileContent(selectedFile));
setEditingFile(selectedFile);
}
} else {
setEditingFile(null);
}
}, [selectedFile, editingFile, sandboxFiles, generationProgress.files]);

const handleSaveFile = async () => {
if (!selectedFile || !sandboxData) return;
setIsSaving(true);
try {
const response = await fetch('/api/save-file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sandboxId: sandboxData.sandboxId,
filePath: selectedFile,
content: editorValue
})
});
const data = await response.json();
if (data.success) {
toast.success(`Successfully saved ${selectedFile.split('/').pop()}`);
setSandboxFiles(prev => ({
...prev,
[selectedFile]: editorValue
}));
setGenerationProgress(prev => ({
...prev,
files: prev.files.map(f => f.path === selectedFile ? { ...f, content: editorValue } : f)
}));
if (iframeRef.current) {
const currentSrc = iframeRef.current.src;
iframeRef.current.src = '';
iframeRef.current.src = currentSrc;
}
} else {
toast.error(`Failed to save: ${data.error}`);
}
} catch (err: any) {
toast.error(`Error saving file: ${err.message}`);
} finally {
setIsSaving(false);
}
};

const editorValueRef = useRef(editorValue);
const selectedFileRef = useRef(selectedFile);
const sandboxDataRef = useRef(sandboxData);

useEffect(() => {
editorValueRef.current = editorValue;
}, [editorValue]);

useEffect(() => {
selectedFileRef.current = selectedFile;
}, [selectedFile]);

useEffect(() => {
sandboxDataRef.current = sandboxData;
}, [sandboxData]);

const handleEditorMount = (editor: any, monaco: any) => {
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
const currentVal = editorValueRef.current;
const currentFile = selectedFileRef.current;
const currentSandbox = sandboxDataRef.current;
if (!currentFile || !currentSandbox) return;

fetch('/api/save-file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sandboxId: currentSandbox.sandboxId,
filePath: currentFile,
content: currentVal
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
toast.success(`Successfully saved ${currentFile.split('/').pop()}`);
setSandboxFiles(prev => ({
...prev,
[currentFile]: currentVal
}));
setGenerationProgress(prev => ({
...prev,
files: prev.files.map(f => f.path === currentFile ? { ...f, content: currentVal } : f)
}));
if (iframeRef.current) {
const currentSrc = iframeRef.current.src;
iframeRef.current.src = '';
iframeRef.current.src = currentSrc;
}
} else {
toast.error(`Failed to save: ${data.error}`);
}
})
.catch(err => {
toast.error(`Error saving file: ${err.message}`);
});
});
};


const [conversationContext, setConversationContext] = useState<{
scrapedWebsites: Array<{ url: string; content: any; timestamp: Date }>;
generatedComponents: Array<{ name: string; path: string; content: string }>;
Expand Down Expand Up @@ -1557,40 +1680,53 @@ Tip: I automatically detect and install npm packages from your code imports (lik
<div className="flex items-center gap-2">
{getFileIcon(selectedFile)}
<span className="font-mono text-sm">{selectedFile}</span>
{editorValue !== getFileContent(selectedFile) && (
<span className="text-xs text-orange-400 font-medium ml-2 animate-pulse">• Unsaved Changes</span>
)}
</div>
<div className="flex items-center gap-2">
<button
onClick={handleSaveFile}
disabled={isSaving || editorValue === getFileContent(selectedFile)}
className="px-3 py-1 text-xs font-semibold rounded bg-orange-500 hover:bg-orange-600 disabled:bg-gray-600 text-white transition-colors"
>
{isSaving ? 'Saving...' : 'Save'}
</button>
<button
onClick={() => setSelectedFile(null)}
className="hover:bg-black/20 p-1 rounded transition-colors"
>
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<button
onClick={() => setSelectedFile(null)}
className="hover:bg-black/20 p-1 rounded transition-colors"
>
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="bg-gray-900 border border-gray-700 rounded">
<SyntaxHighlighter
<div className="bg-gray-900 border border-gray-700 rounded overflow-hidden" style={{ height: '550px' }}>
<Editor
height="100%"
language={(() => {
const ext = selectedFile.split('.').pop()?.toLowerCase();
if (ext === 'css') return 'css';
if (ext === 'json') return 'json';
if (ext === 'html') return 'html';
return 'jsx';
if (ext === 'ts' || ext === 'tsx') return 'typescript';
return 'javascript';
})()}
style={vscDarkPlus}
customStyle={{
margin: 0,
padding: '1rem',
fontSize: '0.875rem',
background: 'transparent',
theme="vs-dark"
value={editorValue}
onChange={(value) => setEditorValue(value || '')}
onMount={handleEditorMount}
options={{
minimap: { enabled: false },
fontSize: 14,
lineNumbers: 'on',
roundedSelection: false,
scrollBeyondLastLine: false,
readOnly: false,
automaticLayout: true,
}}
showLineNumbers={true}
>
{(() => {
// Find the file content from generated files
const file = generationProgress.files.find(f => f.path === selectedFile);
return file?.content || '// File content will appear here';
})()}
</SyntaxHighlighter>
/>
</div>
</div>
</div>
Expand Down
Loading