diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx index 58ec5ebf..06d1f135 100644 --- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx @@ -25,9 +25,9 @@ export default function PreviewTab() { const { anatomy, loading } = useAnatomy(projectId, { pollMs: 0 }); const previews = anatomy?.hosting.previews ?? []; - const options = previews.filter((p) => p.url); + // Find the port 3000 preview if it exists, otherwise fall back to null + const primaryPreview = previews.find(p => p.port === 3000); - const [selectedUrl, setSelectedUrl] = useState(null); const [iframeSrc, setIframeSrc] = useState(null); const iframeDomRef = useRef(null); const bridge = usePreviewBridge(); @@ -35,16 +35,9 @@ export default function PreviewTab() { const deviceMode = usePreviewToolbarStore((s) => s.deviceMode); - // Auto-select first preview on load - useEffect(() => { - if (!selectedUrl && options.length > 0) { - setSelectedUrl(options[0].url); - } - }, [options, selectedUrl]); - useLayoutEffect(() => { - setIframeSrc(selectedUrl ?? null); - }, [selectedUrl]); + setIframeSrc(primaryPreview?.url ?? null); + }, [primaryPreview?.url]); useEffect(() => { if (!bridge || !iframeSrc || !iframeDomRef.current) return; @@ -53,23 +46,6 @@ export default function PreviewTab() { return (
-
- {options.length > 1 && ( - - )} -
-
{ iframeDomRef.current = el; - bridge?.registerPreviewIframe(el, iframeSrc); + if (el) bridge?.registerPreviewIframe(el, iframeSrc); }} onLoad={() => bridge?.notifyPreviewIframeLoaded()} style={{ @@ -114,7 +90,8 @@ export default function PreviewTab() { /> ) : (
-

No preview available

+

Preview not running on port 3000.

+

Ask the AI to start the dev server.

)} @@ -137,25 +114,6 @@ const canvas: React.CSSProperties = { background: "linear-gradient(165deg, #faf8f5 0%, #f4f0ea 42%, #ebe7df 100%)", }; -const toolbar: React.CSSProperties = { - display: "flex", - gap: 8, - marginBottom: 10, - flexWrap: "wrap", -}; - -const select: React.CSSProperties = { - flex: 1, - maxWidth: 480, - padding: "6px 10px", - borderRadius: 8, - border: "1px solid rgba(26, 26, 26, 0.12)", - background: "rgba(255,255,255,0.85)", - fontSize: "0.8rem", - fontFamily: "inherit", - color: "#1a1a1a", -}; - const desktopFrame: React.CSSProperties = { flex: 1, width: "100%", @@ -176,135 +134,77 @@ const mobileFrame: React.CSSProperties = { height: 844, flexShrink: 0, borderRadius: 56, - padding: 12, - background: "linear-gradient(160deg, #2a2a2c 0%, #1a1a1c 50%, #0e0e10 100%)", + overflow: "hidden", + background: "#000", + border: "14px solid #1a1a1a", boxShadow: - "0 0 0 1px rgba(255,255,255,0.04) inset, 0 0 0 2px #000 inset, 0 28px 60px -12px rgba(0,0,0,0.45), 0 8px 20px -8px rgba(0,0,0,0.35)", + "0 0 0 1px rgba(255,255,255,0.1) inset, 0 24px 60px rgba(26, 26, 26, 0.15)", display: "flex", flexDirection: "column", - overflow: "hidden", }; -const homeIndicator: React.CSSProperties = { - position: "absolute", - left: "50%", - bottom: 20, - transform: "translateX(-50%)", - width: 134, - height: 5, - background: "#1a1916", - borderRadius: 999, - opacity: 0.85, - pointerEvents: "none", - zIndex: 10, -}; - -function MobileChrome() { - return ( - <> -
-
- 9:41 - - - - - - - - - - - - - - - - -
- - ); -} - const iframeStyle: React.CSSProperties = { flex: 1, width: "100%", - minHeight: 0, + height: "100%", border: "none", - background: "#fcfcfb", - display: "block", + background: "#fff", + pointerEvents: "auto", }; const loaderWrap: React.CSSProperties = { flex: 1, display: "flex", + flexDirection: "column", alignItems: "center", justifyContent: "center", - minHeight: 200, - background: "#fcfcfb", + background: "#fff", }; const emptyText: React.CSSProperties = { + margin: 0, fontSize: "0.85rem", - color: "#a09a90", - fontFamily: '"Outfit", "Inter", ui-sans-serif, sans-serif', + color: "#a1a1aa", +}; + +function MobileChrome() { + return ( +
+
+
+ ); +} + +const notchWrap: React.CSSProperties = { + position: "absolute", + top: 0, + left: 0, + right: 0, + height: 32, + display: "flex", + justifyContent: "center", + zIndex: 10, + pointerEvents: "none", +}; + +const notch: React.CSSProperties = { + width: 120, + height: 30, + background: "#1a1a1a", + borderBottomLeftRadius: 20, + borderBottomRightRadius: 20, +}; + +const homeIndicator: React.CSSProperties = { + position: "absolute", + bottom: 8, + left: "50%", + transform: "translateX(-50%)", + width: 140, + height: 5, + borderRadius: 10, + background: "rgba(255, 255, 255, 0.4)", + mixBlendMode: "difference", + zIndex: 10, + pointerEvents: "none", }; diff --git a/vibn-frontend/app/api/chat/route.ts b/vibn-frontend/app/api/chat/route.ts index 9457e6c9..67f0d9a3 100644 --- a/vibn-frontend/app/api/chat/route.ts +++ b/vibn-frontend/app/api/chat/route.ts @@ -200,11 +200,11 @@ Each project has a persistent \`vibn-dev\` container. Edit files via \`fs_*\` an **Iterate:**\n- \`shell_exec { projectId, command }\` — anything: \`ls\`, \`npm install\`, \`npm test\`, \`npx create-next-app .\`, \`git status\`. Cwd defaults to \`/workspace\`. Node (LTS), Python 3.12, and Go 1.23 are pre-installed — no setup needed.\n- \`fs_read\` / \`fs_write\` / \`fs_edit { path, oldString, newString }\` (include 2–3 lines of context in \`oldString\` for uniqueness; fails fast if missing or non-unique).\n- \`fs_glob\` / \`fs_grep\` (ripgrep, respects .gitignore) / \`fs_list\` / \`fs_delete\`.\n -**Dev servers (preview URL via \`*.preview.vibnai.com\` wildcard):** -- \`dev_server_start { projectId, command, port?, name? }\` is a **one-shot** call. It kills old processes on the port, checks the port is free, sets HOST=0.0.0.0 + PORT, launches your command, and returns a clickable \`previewUrl\`. Do NOT pre-flight with \`devcontainer_status\`, \`fs_list\`, \`dev_server_logs\`, or manual \`shell_exec\` kills — the function handles all of that. Just call it. The error tells you what to fix: \`PORT_BUSY\` → pick 3001–3009; \`npm: command not found\` → project needs \`npm install\` first. -- \`port\` defaults to 3000, range 3000–3009 (10 Traefik routers pre-allocated per project). -- **Directory:** The command runs from the root \`/workspace\` directory, but your project code is inside \`/workspace/${activeProject.slug ?? ""}/\`. You MUST \`cd\` into your project folder first! Example: \`command: "cd ${activeProject.slug ?? ""} && npm run dev"\`. -- \`dev_server_stop\` / \`dev_server_list\` / \`dev_server_logs\` — use only AFTER a failed start, and only to diagnose the error the function returned. Never on success. +**Dev servers (preview URL via `*.preview.vibnai.com` wildcard):** +- `dev_server_start { projectId, command, port: 3000 }` is a **one-shot** call. It kills old processes on the port, checks the port is free, sets HOST=0.0.0.0 + PORT, launches your command, and returns a clickable `previewUrl`. Do NOT pre-flight with `devcontainer_status`, `fs_list`, `dev_server_logs`, or manual `shell_exec` kills — the function handles all of that. Just call it. The error tells you what to fix: `PORT_BUSY` → pick 3001–3009; `npm: command not found` → project needs `npm install` first. +- **Port:** The primary frontend service MUST ALWAYS be bound to port `3000`. Do not use any other port for the user-facing UI. If you are spinning up secondary services (like an API or Storybook) alongside it, you may bind them to ports `3001–3009`, but port `3000` is reserved exclusively for the primary visual preview. +- **Directory:** The command runs from the root `/workspace` directory, but your project code is inside `/workspace/${activeProject.slug ?? ""}/`. You MUST `cd` into your project folder first! Example: `command: "cd ${activeProject.slug ?? ""} && npm run dev"`. +- `dev_server_stop` / `dev_server_list` / `dev_server_logs` — use only AFTER a failed start, and only to diagnose the error the function returned. Never on success. **HMR through the proxy (apply when scaffolding):** - **Vite (verified working):** in \`vite.config\` set \`server: { host: '0.0.0.0', port: <3000-3009>, strictPort: true, hmr: { clientPort: 443, protocol: 'wss', host: '' } }\`. The \`hmr.host\` is REQUIRED — without it Vite's HMR client can guess the wrong host and the WS handshake fails through Traefik. Default localhost binding looks fine locally but breaks HMR through the proxy.