feat(preview): add 1-click start dev server button to empty state
This commit is contained in:
@@ -174,6 +174,8 @@ export default function PreviewTab() {
|
|||||||
bridge.registerPreviewIframe(iframeDomRef.current, iframeSrc);
|
bridge.registerPreviewIframe(iframeDomRef.current, iframeSrc);
|
||||||
}, [bridge, iframeSrc]);
|
}, [bridge, iframeSrc]);
|
||||||
|
|
||||||
|
const [isForceStarting, setIsForceStarting] = useState(false);
|
||||||
|
|
||||||
// Determine which empty state to show.
|
// Determine which empty state to show.
|
||||||
const emptyContent = (() => {
|
const emptyContent = (() => {
|
||||||
if (loading && !anatomy) return <InitialLoader />;
|
if (loading && !anatomy) return <InitialLoader />;
|
||||||
@@ -190,7 +192,11 @@ export default function PreviewTab() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (ensureStatus === "calling" || ensureStatus === "starting") {
|
if (
|
||||||
|
ensureStatus === "calling" ||
|
||||||
|
ensureStatus === "starting" ||
|
||||||
|
isForceStarting
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<WarmingUpState
|
<WarmingUpState
|
||||||
startedAt={undefined}
|
startedAt={undefined}
|
||||||
@@ -200,7 +206,19 @@ export default function PreviewTab() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Never had a dev server — needs the AI to start one.
|
// Never had a dev server — needs the AI to start one.
|
||||||
return <NotRunningState />;
|
return (
|
||||||
|
<NotRunningState
|
||||||
|
onStart={() => {
|
||||||
|
setIsForceStarting(true);
|
||||||
|
fetch(
|
||||||
|
`/api/projects/${projectId}/dev-server/ensure?forceStart=true`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
|
).catch(() => setIsForceStarting(false));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -421,7 +439,7 @@ function FailedState({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NotRunningState() {
|
function NotRunningState({ onStart }: { onStart: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ textAlign: "center", maxWidth: 260 }}>
|
<div style={{ textAlign: "center", maxWidth: 260 }}>
|
||||||
<div
|
<div
|
||||||
@@ -443,9 +461,25 @@ function NotRunningState() {
|
|||||||
</div>
|
</div>
|
||||||
<p style={emptyTitle}>Preview not running</p>
|
<p style={emptyTitle}>Preview not running</p>
|
||||||
<p style={emptySubtext}>No dev server found on port 3000.</p>
|
<p style={emptySubtext}>No dev server found on port 3000.</p>
|
||||||
<p style={{ ...emptySubtext, marginTop: 4 }}>
|
<button
|
||||||
Ask the AI to start the dev server.
|
onClick={onStart}
|
||||||
</p>
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
background: "#18181b",
|
||||||
|
color: "white",
|
||||||
|
border: "none",
|
||||||
|
padding: "8px 16px",
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
⚡ Start Dev Server
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
} from "@/lib/dev-container";
|
} from "@/lib/dev-container";
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
_req: Request,
|
request: Request,
|
||||||
{ params }: { params: Promise<{ projectId: string }> },
|
{ params }: { params: Promise<{ projectId: string }> },
|
||||||
) {
|
) {
|
||||||
const { projectId } = await params;
|
const { projectId } = await params;
|
||||||
@@ -96,7 +96,10 @@ export async function POST(
|
|||||||
[projectId],
|
[projectId],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!last) {
|
const forceStart =
|
||||||
|
new URL(request.url).searchParams.get("forceStart") === "true";
|
||||||
|
|
||||||
|
if (!last && !forceStart) {
|
||||||
return NextResponse.json({ status: "no_history" });
|
return NextResponse.json({ status: "no_history" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,13 +114,12 @@ export async function POST(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Fire restart in background — don't block the response.
|
// 4. Fire restart in background — don't block the response.
|
||||||
// The probe (up to 300s) runs in background; anatomy polling at 5s
|
// If forceStart is true but we have no history, default to Next.js start command.
|
||||||
// will surface state='starting' immediately, then 'running' when ready.
|
|
||||||
const restartOpts = {
|
const restartOpts = {
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
projectSlug: project.slug,
|
projectSlug: project.slug,
|
||||||
command: last.command,
|
command: last?.command || "next dev -H 0.0.0.0 --no-turbopack",
|
||||||
port: last.port,
|
port: last?.port || 3000,
|
||||||
workspace,
|
workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user