feat(ui): hardcode visual preview tab to primary frontend port 3000
This commit is contained in:
@@ -25,9 +25,9 @@ export default function PreviewTab() {
|
|||||||
const { anatomy, loading } = useAnatomy(projectId, { pollMs: 0 });
|
const { anatomy, loading } = useAnatomy(projectId, { pollMs: 0 });
|
||||||
|
|
||||||
const previews = anatomy?.hosting.previews ?? [];
|
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<string | null>(null);
|
|
||||||
const [iframeSrc, setIframeSrc] = useState<string | null>(null);
|
const [iframeSrc, setIframeSrc] = useState<string | null>(null);
|
||||||
const iframeDomRef = useRef<HTMLIFrameElement | null>(null);
|
const iframeDomRef = useRef<HTMLIFrameElement | null>(null);
|
||||||
const bridge = usePreviewBridge();
|
const bridge = usePreviewBridge();
|
||||||
@@ -35,16 +35,9 @@ export default function PreviewTab() {
|
|||||||
|
|
||||||
const deviceMode = usePreviewToolbarStore((s) => s.deviceMode);
|
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(() => {
|
useLayoutEffect(() => {
|
||||||
setIframeSrc(selectedUrl ?? null);
|
setIframeSrc(primaryPreview?.url ?? null);
|
||||||
}, [selectedUrl]);
|
}, [primaryPreview?.url]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bridge || !iframeSrc || !iframeDomRef.current) return;
|
if (!bridge || !iframeSrc || !iframeDomRef.current) return;
|
||||||
@@ -53,23 +46,6 @@ export default function PreviewTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={canvas}>
|
<div style={canvas}>
|
||||||
<div style={toolbar}>
|
|
||||||
{options.length > 1 && (
|
|
||||||
<select
|
|
||||||
value={selectedUrl ?? ""}
|
|
||||||
onChange={(e) => setSelectedUrl(e.target.value)}
|
|
||||||
style={select}
|
|
||||||
>
|
|
||||||
{options.map((p) => (
|
|
||||||
<option key={p.id} value={p.url}>
|
|
||||||
{p.name} :{p.port} — {p.state}
|
|
||||||
{p.command ? ` (${p.command.slice(0, 60)})` : ""}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -101,7 +77,7 @@ export default function PreviewTab() {
|
|||||||
title="Preview"
|
title="Preview"
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
iframeDomRef.current = el;
|
iframeDomRef.current = el;
|
||||||
bridge?.registerPreviewIframe(el, iframeSrc);
|
if (el) bridge?.registerPreviewIframe(el, iframeSrc);
|
||||||
}}
|
}}
|
||||||
onLoad={() => bridge?.notifyPreviewIframeLoaded()}
|
onLoad={() => bridge?.notifyPreviewIframeLoaded()}
|
||||||
style={{
|
style={{
|
||||||
@@ -114,7 +90,8 @@ export default function PreviewTab() {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div style={loaderWrap}>
|
<div style={loaderWrap}>
|
||||||
<p style={emptyText}>No preview available</p>
|
<p style={emptyText}>Preview not running on port 3000.</p>
|
||||||
|
<p style={{...emptyText, fontSize: '0.75rem', marginTop: 8}}>Ask the AI to start the dev server.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -137,25 +114,6 @@ const canvas: React.CSSProperties = {
|
|||||||
background: "linear-gradient(165deg, #faf8f5 0%, #f4f0ea 42%, #ebe7df 100%)",
|
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 = {
|
const desktopFrame: React.CSSProperties = {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@@ -176,135 +134,77 @@ const mobileFrame: React.CSSProperties = {
|
|||||||
height: 844,
|
height: 844,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
borderRadius: 56,
|
borderRadius: 56,
|
||||||
padding: 12,
|
overflow: "hidden",
|
||||||
background: "linear-gradient(160deg, #2a2a2c 0%, #1a1a1c 50%, #0e0e10 100%)",
|
background: "#000",
|
||||||
|
border: "14px solid #1a1a1a",
|
||||||
boxShadow:
|
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",
|
display: "flex",
|
||||||
flexDirection: "column",
|
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 (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 22,
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
width: 124,
|
|
||||||
height: 36,
|
|
||||||
background: "#000",
|
|
||||||
borderRadius: 999,
|
|
||||||
zIndex: 15,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 12,
|
|
||||||
left: 12,
|
|
||||||
right: 12,
|
|
||||||
height: 47,
|
|
||||||
padding: "18px 26px 0",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: 600,
|
|
||||||
letterSpacing: "-0.01em",
|
|
||||||
color: "#1a1916",
|
|
||||||
pointerEvents: "none",
|
|
||||||
zIndex: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>9:41</span>
|
|
||||||
<span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 17 11"
|
|
||||||
style={{ width: 17, height: 11, fill: "currentColor" }}
|
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
<rect x="0" y="7" width="3" height="4" rx="0.6" />
|
|
||||||
<rect x="4" y="5" width="3" height="6" rx="0.6" />
|
|
||||||
<rect x="8" y="3" width="3" height="8" rx="0.6" />
|
|
||||||
<rect x="12" y="0" width="3" height="11" rx="0.6" />
|
|
||||||
</svg>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 17 11"
|
|
||||||
style={{ width: 17, height: 11, fill: "currentColor" }}
|
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
<path d="M8.5 1.5C5.5 1.5 2.7 2.6 0.5 4.6L2 6.1C3.8 4.5 6.1 3.6 8.5 3.6c2.4 0 4.7 0.9 6.5 2.5l1.5-1.5c-2.2-2-5-3.1-8-3.1zM3.5 7.6L5 9.1c1-0.9 2.2-1.4 3.5-1.4 1.3 0 2.5 0.5 3.5 1.4l1.5-1.5c-1.4-1.3-3.1-2-5-2-1.9 0-3.6 0.7-5 2zM6.5 10.6l2 2 2-2c-0.5-0.5-1.2-0.8-2-0.8s-1.5 0.3-2 0.8z" />
|
|
||||||
</svg>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 25 11"
|
|
||||||
style={{ width: 25, height: 11, fill: "currentColor" }}
|
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
x="0.5"
|
|
||||||
y="0.5"
|
|
||||||
width="21"
|
|
||||||
height="10"
|
|
||||||
rx="2.5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeOpacity="0.45"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="22"
|
|
||||||
y="3.5"
|
|
||||||
width="1.5"
|
|
||||||
height="4"
|
|
||||||
rx="0.4"
|
|
||||||
fill="currentColor"
|
|
||||||
fillOpacity="0.45"
|
|
||||||
/>
|
|
||||||
<rect x="2" y="2" width="18" height="7" rx="1.4" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const iframeStyle: React.CSSProperties = {
|
const iframeStyle: React.CSSProperties = {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
minHeight: 0,
|
height: "100%",
|
||||||
border: "none",
|
border: "none",
|
||||||
background: "#fcfcfb",
|
background: "#fff",
|
||||||
display: "block",
|
pointerEvents: "auto",
|
||||||
};
|
};
|
||||||
|
|
||||||
const loaderWrap: React.CSSProperties = {
|
const loaderWrap: React.CSSProperties = {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
minHeight: 200,
|
background: "#fff",
|
||||||
background: "#fcfcfb",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyText: React.CSSProperties = {
|
const emptyText: React.CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
fontSize: "0.85rem",
|
fontSize: "0.85rem",
|
||||||
color: "#a09a90",
|
color: "#a1a1aa",
|
||||||
fontFamily: '"Outfit", "Inter", ui-sans-serif, sans-serif',
|
};
|
||||||
|
|
||||||
|
function MobileChrome() {
|
||||||
|
return (
|
||||||
|
<div style={notchWrap}>
|
||||||
|
<div style={notch} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
**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 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.
|
- `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\` defaults to 3000, range 3000–3009 (10 Traefik routers pre-allocated per project).
|
- **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 ?? "<slug>"}/\`. You MUST \`cd\` into your project folder first! Example: \`command: "cd ${activeProject.slug ?? "<slug>"} && npm run dev"\`.
|
- **Directory:** The command runs from the root `/workspace` directory, but your project code is inside `/workspace/${activeProject.slug ?? "<slug>"}/`. You MUST `cd` into your project folder first! Example: `command: "cd ${activeProject.slug ?? "<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_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):**
|
**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 previewUrl host, no protocol>' } }\`. 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.
|
- **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 previewUrl host, no protocol>' } }\`. 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user