From f82b0e882e1ff49db3a3a00de933e98d266b4ace Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Wed, 19 Feb 2025 00:27:20 -0600 Subject: [PATCH 1/5] feat(frontend): enhance project modal and web preview with improved URL handling and navigation --- frontend/src/app/(main)/Home.tsx | 21 +- frontend/src/app/api/runProject/route.ts | 57 +++++- .../components/code-engine/code-engine.tsx | 183 ++++++++++-------- .../src/components/code-engine/web-view.tsx | 182 +++++++++++++---- frontend/src/components/project-modal.tsx | 2 +- 5 files changed, 309 insertions(+), 136 deletions(-) diff --git a/frontend/src/app/(main)/Home.tsx b/frontend/src/app/(main)/Home.tsx index 2c08a268..2897bf03 100644 --- a/frontend/src/app/(main)/Home.tsx +++ b/frontend/src/app/(main)/Home.tsx @@ -129,17 +129,20 @@ export default function Home() { setMessages={setMessages} /> - - - - + {chatId && ( + +
+ +
+
+ )} ); } diff --git a/frontend/src/app/api/runProject/route.ts b/frontend/src/app/api/runProject/route.ts index 8be2fb36..712a75b4 100644 --- a/frontend/src/app/api/runProject/route.ts +++ b/frontend/src/app/api/runProject/route.ts @@ -43,21 +43,43 @@ function findAvailablePort( }); } +async function checkExistingContainer( + projectPath: string +): Promise { + return new Promise((resolve) => { + const subdomain = projectPath.replace(/[^\w-]/g, '').toLowerCase(); + exec( + `docker ps --filter "label=traefik.http.routers.${subdomain}.rule" --format "{{.ID}}"`, + (error, stdout) => { + if (error || !stdout.trim()) { + resolve(null); + } else { + resolve(stdout.trim()); + } + } + ); + }); +} + async function buildAndRunDocker( projectPath: string ): Promise<{ domain: string; containerId: string }> { - console.log(runningContainers); - if (runningContainers.has(projectPath)) { - console.log(`Container for project ${projectPath} is already running.`); - return runningContainers.get(projectPath)!; - } const traefikDomain = process.env.TRAEFIK_DOMAIN || 'docker.localhost'; - const directory = path.join(getProjectPath(projectPath), 'frontend'); - const imageName = projectPath.toLowerCase(); - const containerId = crypto.randomUUID(); - const containerName = `container-${containerId}`; + const existingContainerId = await checkExistingContainer(projectPath); + if (existingContainerId) { + const subdomain = projectPath.replace(/[^\w-]/g, '').toLowerCase(); + const domain = `${subdomain}.${traefikDomain}`; + runningContainers.set(projectPath, { + domain, + containerId: existingContainerId, + }); + return { domain, containerId: existingContainerId }; + } + const directory = path.join(getProjectPath(projectPath), 'frontend'); const subdomain = projectPath.replace(/[^\w-]/g, '').toLowerCase(); + const imageName = subdomain; + const containerName = `container-${subdomain}`; const domain = `${subdomain}.${traefikDomain}`; const exposedPort = await findAvailablePort(); return new Promise((resolve, reject) => { @@ -100,6 +122,8 @@ async function buildAndRunDocker( ); }); } +const processingRequests = new Set(); + export async function GET(req: Request) { const { searchParams } = new URL(req.url); const projectPath = searchParams.get('projectPath'); @@ -111,6 +135,19 @@ export async function GET(req: Request) { ); } + if (processingRequests.has(projectPath)) { + const existingContainer = runningContainers.get(projectPath); + if (existingContainer) { + return NextResponse.json({ + message: 'Docker container already running', + domain: existingContainer.domain, + containerId: existingContainer.containerId, + }); + } + } + + processingRequests.add(projectPath); + try { const { domain, containerId } = await buildAndRunDocker(projectPath); return NextResponse.json({ @@ -123,5 +160,7 @@ export async function GET(req: Request) { { error: error.message || 'Failed to start Docker container' }, { status: 500 } ); + } finally { + processingRequests.delete(projectPath); } } diff --git a/frontend/src/components/code-engine/code-engine.tsx b/frontend/src/components/code-engine/code-engine.tsx index 451fda37..ebcb24f0 100644 --- a/frontend/src/components/code-engine/code-engine.tsx +++ b/frontend/src/components/code-engine/code-engine.tsx @@ -39,12 +39,14 @@ export function CodeEngine({ chatId }: { chatId: string }) { const [activeTab, setActiveTab] = useState<'preview' | 'code' | 'console'>( 'code' ); + // Callback: Handle editor mount const handleEditorMount = (editorInstance) => { editorRef.current = editorInstance; // Set the editor DOM node's position for layout control editorInstance.getDomNode().style.position = 'absolute'; }; + useEffect(() => { async function checkChatProject() { if (curProject?.id) { @@ -135,8 +137,9 @@ export function CodeEngine({ chatId }: { chatId: string }) { setCode(value); setSaving(true); }; + // Responsive toolbar component for header tabs and buttons - const ResponsiveToolbar = () => { + const ResponsiveToolbar = ({ isLoading }: { isLoading: boolean }) => { const containerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(700); const [visibleTabs, setVisibleTabs] = useState(3); @@ -176,13 +179,14 @@ export function CodeEngine({ chatId }: { chatId: string }) { return (
@@ -233,16 +242,24 @@ export function CodeEngine({ chatId }: { chatId: string }) {
{!compactIcons && ( <> - - )} {compactIcons && ( - )} @@ -254,92 +271,100 @@ export function CodeEngine({ chatId }: { chatId: string }) { // Render the CodeEngine layout return ( -
- - {!isProjectFinished && ( - - - - )} - +
{/* Header Bar */} - + - {/* Main Content Area */} -
- {activeTab === 'code' ? ( - <> - {/* File Explorer Panel (collapsible) */} + {/* Main Content Area with Loading */} +
+ + {!isProjectFinished && ( - + +

+ Initializing project... +

-
- + +
+ {activeTab === 'code' ? ( + <> + {/* File Explorer Panel (collapsible) */} + + transition={{ duration: 0.3, ease: 'easeInOut' }} + className="overflow-y-auto border-r" + > + + +
+ +
+ + ) : activeTab === 'preview' ? ( +
+
- - ) : activeTab === 'preview' ? ( - - ) : activeTab === 'console' ? ( -
Console Content (Mock)
- ) : null} -
+ ) : activeTab === 'console' ? ( +
Console Content (Mock)
+ ) : null} +
- {/* Save Changes Bar */} - {saving && ( - - )} + {/* Save Changes Bar */} + {saving && ( + + )} - {/* File Explorer Toggle Button */} - {activeTab === 'code' && ( - - )} + {/* File Explorer Toggle Button */} + {activeTab === 'code' && ( + + )} +
); } + // SaveChangesBar component for showing unsaved changes status const SaveChangesBar = ({ saving, onSave, onReset }) => { return ( diff --git a/frontend/src/components/code-engine/web-view.tsx b/frontend/src/components/code-engine/web-view.tsx index b27d7eac..5f1140d9 100644 --- a/frontend/src/components/code-engine/web-view.tsx +++ b/frontend/src/components/code-engine/web-view.tsx @@ -1,14 +1,42 @@ import { useContext, useEffect, useRef, useState } from 'react'; import { ProjectContext } from './project-context'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + ChevronLeft, + ChevronRight, + Maximize, + ExternalLink, +} from 'lucide-react'; export default function WebPreview() { const { curProject } = useContext(ProjectContext); - const [url, setUrl] = useState(''); + const [baseUrl, setBaseUrl] = useState(''); + const [displayPath, setDisplayPath] = useState('/'); + const [history, setHistory] = useState(['/']); + const [currentIndex, setCurrentIndex] = useState(0); const iframeRef = useRef(null); + const containerRef = useRef<{ projectPath: string; domain: string } | null>( + null + ); + const lastProjectPathRef = useRef(null); useEffect(() => { const getWebUrl = async () => { + if (!curProject) return; const projectPath = curProject.projectPath; + + if (lastProjectPathRef.current === projectPath) { + return; + } + + lastProjectPathRef.current = projectPath; + + if (containerRef.current?.projectPath === projectPath) { + setBaseUrl(`http://${containerRef.current.domain}`); + return; + } + try { const response = await fetch( `/api/runProject?projectPath=${encodeURIComponent(projectPath)}`, @@ -20,9 +48,15 @@ export default function WebPreview() { } ); const json = await response.json(); - console.log(json); - await new Promise((resolve) => setTimeout(resolve, 10000)); - setUrl(`http://${json.domain}/`); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + containerRef.current = { + projectPath, + domain: json.domain, + }; + setBaseUrl(`http://${json.domain}`); + setDisplayPath('/'); } catch (error) { console.error('fetching url error:', error); } @@ -32,52 +66,124 @@ export default function WebPreview() { }, [curProject]); useEffect(() => { - if (iframeRef.current) { - iframeRef.current.src = url; + if (iframeRef.current && baseUrl) { + const fullUrl = `${baseUrl}${displayPath}`; + iframeRef.current.src = fullUrl; } - }, [url]); + }, [baseUrl, displayPath]); - const refreshIframe = () => { + const enterFullScreen = () => { if (iframeRef.current) { - iframeRef.current.src = url; + iframeRef.current.requestFullscreen(); } }; - const enterFullScreen = () => { - if (iframeRef.current) { - iframeRef.current.requestFullscreen(); + const openInNewTab = () => { + if (baseUrl) { + const fullUrl = `${baseUrl}${displayPath}`; + window.open(fullUrl, '_blank'); + } + }; + + const handlePathChange = (newPath: string) => { + if (!newPath.startsWith('/')) { + newPath = '/' + newPath; + } + setDisplayPath(newPath); + // Add new path to history, removing any forward history + const newHistory = history.slice(0, currentIndex + 1); + setHistory([...newHistory, newPath]); + setCurrentIndex(newHistory.length); + }; + + const goBack = () => { + if (currentIndex > 0) { + setCurrentIndex(currentIndex - 1); + setDisplayPath(history[currentIndex - 1]); + } + }; + + const goForward = () => { + if (currentIndex < history.length - 1) { + setCurrentIndex(currentIndex + 1); + setDisplayPath(history[currentIndex + 1]); } }; return ( -
-
- setUrl(e.target.value)} - className="flex-1 p-2 border rounded" - /> - - +
+ {/* URL Bar */} +
+ {/* Navigation Controls */} +
+ + +
+ + {/* URL Input */} +
+ handlePathChange(e.target.value)} + className="h-8 bg-secondary" + placeholder="/" + disabled={!baseUrl} + /> +
+ + {/* Actions */} +
+ + +
-
-