feat(build): AI architecture recommendation with review + confirm flow
- New /api/projects/[projectId]/architecture (GET/POST/PATCH) — reads PRD + phases, calls AI to generate structured monorepo architecture JSON, persists to fs_projects.data.architecture; PATCH sets confirmed flag - Rebuilt Build tab to show the AI-generated recommendation: expandable app cards (tech stack, key screens), shared packages, infrastructure, integrations, and risk notes; confirm button + "adjustable later" note Made-with: Cursor
This commit is contained in:
@@ -4,39 +4,190 @@ import { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
status?: string;
|
||||
prd?: string;
|
||||
giteaRepoUrl?: string;
|
||||
interface App {
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
tech: string[];
|
||||
screens: string[];
|
||||
}
|
||||
|
||||
const BUILD_FEATURES = [
|
||||
"Authentication system",
|
||||
"Database schema",
|
||||
"API endpoints",
|
||||
"Core UI",
|
||||
"Business logic",
|
||||
"Tests",
|
||||
];
|
||||
interface Package {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Infra {
|
||||
name: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
interface Integration {
|
||||
name: string;
|
||||
required: boolean;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
interface Architecture {
|
||||
productName: string;
|
||||
productType: string;
|
||||
summary: string;
|
||||
apps: App[];
|
||||
packages: Package[];
|
||||
infrastructure: Infra[];
|
||||
integrations: Integration[];
|
||||
designSurfaces: string[];
|
||||
riskNotes: string[];
|
||||
}
|
||||
|
||||
function SectionLabel({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{
|
||||
fontSize: "0.6rem", fontWeight: 700, color: "#a09a90",
|
||||
letterSpacing: "0.12em", textTransform: "uppercase",
|
||||
marginBottom: 10, marginTop: 28,
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AppCard({ app }: { app: App }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const icons: Record<string, string> = {
|
||||
web: "🌐", api: "⚡", simulator: "🎮", admin: "🔧",
|
||||
mobile: "📱", worker: "⚙️", engine: "🎯",
|
||||
};
|
||||
const icon = Object.entries(icons).find(([k]) => app.name.toLowerCase().includes(k))?.[1] ?? "📦";
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 10,
|
||||
marginBottom: 8, overflow: "hidden",
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setOpen(o => !o)}
|
||||
style={{
|
||||
width: "100%", textAlign: "left", background: "none", border: "none",
|
||||
cursor: "pointer", padding: "14px 18px",
|
||||
display: "flex", alignItems: "center", gap: 12,
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: "1.2rem" }}>{icon}</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span style={{ fontSize: "0.88rem", fontWeight: 600, color: "#1a1a1a" }}>
|
||||
apps/{app.name}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: "0.65rem", fontFamily: "IBM Plex Mono, monospace",
|
||||
color: "#6b6560", background: "#f0ece4",
|
||||
padding: "2px 7px", borderRadius: 4,
|
||||
}}>
|
||||
{app.type}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: "0.78rem", color: "#8a8478", marginTop: 2 }}>
|
||||
{app.description}
|
||||
</div>
|
||||
</div>
|
||||
<span style={{ fontSize: "0.7rem", color: "#c5c0b8" }}>{open ? "▲" : "▼"}</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div style={{ padding: "0 18px 16px", borderTop: "1px solid #f0ece4" }}>
|
||||
{app.tech.length > 0 && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div style={{ fontSize: "0.62rem", color: "#b5b0a6", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 6 }}>Stack</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>
|
||||
{app.tech.map((t, i) => (
|
||||
<span key={i} style={{
|
||||
fontSize: "0.72rem", fontFamily: "IBM Plex Mono, monospace",
|
||||
color: "#4a4640", background: "#f6f4f0",
|
||||
border: "1px solid #e8e4dc", padding: "2px 8px", borderRadius: 4,
|
||||
}}>{t}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{app.screens.length > 0 && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div style={{ fontSize: "0.62rem", color: "#b5b0a6", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 6 }}>Key screens</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
||||
{app.screens.map((s, i) => (
|
||||
<div key={i} style={{ fontSize: "0.78rem", color: "#4a4640", display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span style={{ color: "#c5c0b8" }}>→</span> {s}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BuildPage() {
|
||||
const params = useParams();
|
||||
const projectId = params.projectId as string;
|
||||
const workspace = params.workspace as string;
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
|
||||
const [prd, setPrd] = useState<string | null>(null);
|
||||
const [architecture, setArchitecture] = useState<Architecture | null>(null);
|
||||
const [architectureConfirmed, setArchitectureConfirmed] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/projects/${projectId}`)
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
setProject(d.project);
|
||||
fetch(`/api/projects/${projectId}/architecture`)
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
setPrd(d.prd);
|
||||
setArchitecture(d.architecture ?? null);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
|
||||
// Also check confirmed flag
|
||||
fetch(`/api/projects/${projectId}`)
|
||||
.then(r => r.json())
|
||||
.then(d => setArchitectureConfirmed(d.project?.architectureConfirmed === true))
|
||||
.catch(() => {});
|
||||
}, [projectId]);
|
||||
|
||||
const handleGenerate = async (force = false) => {
|
||||
setGenerating(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(`/api/projects/${projectId}/architecture`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ forceRegenerate: force }),
|
||||
});
|
||||
const d = await res.json();
|
||||
if (!res.ok) throw new Error(d.error || "Generation failed");
|
||||
setArchitecture(d.architecture);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Something went wrong");
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
setConfirming(true);
|
||||
try {
|
||||
await fetch(`/api/projects/${projectId}/architecture`, { method: "PATCH" });
|
||||
setArchitectureConfirmed(true);
|
||||
} catch { /* swallow */ } finally {
|
||||
setConfirming(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", fontFamily: "Outfit, sans-serif", color: "#a09a90" }}>
|
||||
@@ -45,48 +196,24 @@ export default function BuildPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const hasRepo = Boolean(project?.giteaRepoUrl);
|
||||
const hasPRD = Boolean(project?.prd);
|
||||
|
||||
if (!hasPRD) {
|
||||
// No PRD yet
|
||||
if (!prd) {
|
||||
return (
|
||||
<div
|
||||
className="vibn-enter"
|
||||
style={{
|
||||
flex: 1, display: "flex", alignItems: "center", justifyContent: "center",
|
||||
padding: 40, fontFamily: "Outfit, sans-serif",
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", padding: 40, fontFamily: "Outfit, sans-serif" }}>
|
||||
<div style={{ textAlign: "center", maxWidth: 360 }}>
|
||||
<div style={{
|
||||
width: 56, height: 56, borderRadius: 14,
|
||||
background: "#fff", border: "1px solid #e8e4dc",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: "1.4rem", margin: "0 auto 18px",
|
||||
boxShadow: "0 2px 8px #1a1a1a08",
|
||||
}}>
|
||||
🔒
|
||||
</div>
|
||||
<h3 style={{
|
||||
fontFamily: "Newsreader, serif", fontSize: "1.3rem",
|
||||
fontWeight: 400, color: "#1a1a1a", marginBottom: 8,
|
||||
}}>
|
||||
<div style={{ fontSize: "2.5rem", marginBottom: 16 }}>🔒</div>
|
||||
<h3 style={{ fontFamily: "Newsreader, serif", fontSize: "1.3rem", fontWeight: 400, color: "#1a1a1a", marginBottom: 8 }}>
|
||||
Complete your PRD first
|
||||
</h3>
|
||||
<p style={{ fontSize: "0.82rem", color: "#a09a90", lineHeight: 1.6, marginBottom: 20 }}>
|
||||
Finish your discovery with Atlas, then the builder unlocks automatically.
|
||||
Finish your discovery conversation with Atlas, then the architect will unlock automatically.
|
||||
</p>
|
||||
<Link
|
||||
href={`/${workspace}/project/${projectId}/overview`}
|
||||
style={{
|
||||
display: "inline-block",
|
||||
padding: "9px 20px", borderRadius: 7,
|
||||
background: "#1a1a1a", color: "#fff",
|
||||
fontSize: "0.78rem", fontWeight: 600,
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
>
|
||||
<Link href={`/${workspace}/project/${projectId}/overview`} style={{
|
||||
display: "inline-block", padding: "9px 20px", borderRadius: 7,
|
||||
background: "#1a1a1a", color: "#fff",
|
||||
fontSize: "0.78rem", fontWeight: 600, fontFamily: "Outfit, sans-serif",
|
||||
textDecoration: "none",
|
||||
}}>
|
||||
Continue with Atlas →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -94,83 +221,255 @@ export default function BuildPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasRepo) {
|
||||
// PRD exists but no architecture yet — prompt to generate
|
||||
if (!architecture) {
|
||||
return (
|
||||
<div
|
||||
className="vibn-enter"
|
||||
style={{
|
||||
flex: 1, display: "flex", alignItems: "center", justifyContent: "center",
|
||||
padding: 40, fontFamily: "Outfit, sans-serif",
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: "center", maxWidth: 360 }}>
|
||||
<div style={{
|
||||
width: 56, height: 56, borderRadius: 14,
|
||||
background: "#fff", border: "1px solid #e8e4dc",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: "1.4rem", margin: "0 auto 18px",
|
||||
boxShadow: "0 2px 8px #1a1a1a08",
|
||||
}}>
|
||||
⚡
|
||||
</div>
|
||||
<h3 style={{
|
||||
fontFamily: "Newsreader, serif", fontSize: "1.3rem",
|
||||
fontWeight: 400, color: "#1a1a1a", marginBottom: 8,
|
||||
}}>
|
||||
PRD ready — build coming soon
|
||||
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", padding: 40, fontFamily: "Outfit, sans-serif" }}>
|
||||
<div style={{ textAlign: "center", maxWidth: 440 }}>
|
||||
<div style={{ fontSize: "2.5rem", marginBottom: 16 }}>🏗️</div>
|
||||
<h3 style={{ fontFamily: "Newsreader, serif", fontSize: "1.4rem", fontWeight: 400, color: "#1a1a1a", marginBottom: 10 }}>
|
||||
Ready to architect {architecture ? (architecture as Architecture).productName : "your product"}
|
||||
</h3>
|
||||
<p style={{ fontSize: "0.82rem", color: "#a09a90", lineHeight: 1.6 }}>
|
||||
The Architect agent will generate your project structure and kick off the build pipeline.
|
||||
This feature is in active development.
|
||||
<p style={{ fontSize: "0.84rem", color: "#6b6560", lineHeight: 1.65, marginBottom: 28, maxWidth: 380, margin: "0 auto 28px" }}>
|
||||
The AI will read your PRD and recommend the technical structure — apps, services, database, and integrations. You'll review it before anything gets built.
|
||||
</p>
|
||||
{error && (
|
||||
<div style={{ marginBottom: 16, padding: "10px 14px", background: "#fff0f0", border: "1px solid #ffcdd2", borderRadius: 8, fontSize: "0.78rem", color: "#c62828" }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleGenerate(false)}
|
||||
disabled={generating}
|
||||
style={{
|
||||
padding: "11px 28px", borderRadius: 8,
|
||||
background: generating ? "#8a8478" : "#1a1a1a",
|
||||
color: "#fff", border: "none", cursor: generating ? "default" : "pointer",
|
||||
fontSize: "0.84rem", fontWeight: 600, fontFamily: "Outfit, sans-serif",
|
||||
transition: "background 0.15s",
|
||||
}}
|
||||
>
|
||||
{generating ? "Analysing PRD…" : "Generate architecture →"}
|
||||
</button>
|
||||
{generating && (
|
||||
<p style={{ fontSize: "0.72rem", color: "#a09a90", marginTop: 12 }}>
|
||||
This takes about 15–30 seconds
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Architecture loaded — show full review UI
|
||||
return (
|
||||
<div
|
||||
className="vibn-enter"
|
||||
style={{
|
||||
flex: 1, display: "flex", alignItems: "center", justifyContent: "center",
|
||||
padding: 40, fontFamily: "Outfit, sans-serif",
|
||||
}}
|
||||
>
|
||||
<div style={{ width: "100%", maxWidth: 500 }}>
|
||||
<h3 style={{
|
||||
fontFamily: "Newsreader, serif", fontSize: "1.2rem",
|
||||
fontWeight: 400, color: "#1a1a1a", marginBottom: 18,
|
||||
}}>
|
||||
Build progress
|
||||
</h3>
|
||||
{BUILD_FEATURES.map((f, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="vibn-enter"
|
||||
<div style={{ padding: "28px 32px", overflow: "auto", fontFamily: "Outfit, sans-serif", maxWidth: 780 }}>
|
||||
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", marginBottom: 4 }}>
|
||||
<div>
|
||||
<h2 style={{ fontFamily: "Newsreader, serif", fontSize: "1.35rem", fontWeight: 400, color: "#1a1a1a", margin: 0 }}>
|
||||
Architecture
|
||||
</h2>
|
||||
<p style={{ fontSize: "0.75rem", color: "#a09a90", marginTop: 4 }}>
|
||||
{architecture.productType}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||
{architectureConfirmed && (
|
||||
<span style={{
|
||||
fontSize: "0.72rem", fontFamily: "IBM Plex Mono, monospace",
|
||||
color: "#2e7d32", background: "#2e7d3210",
|
||||
border: "1px solid #a5d6a740", padding: "4px 10px", borderRadius: 5,
|
||||
}}>
|
||||
✓ Confirmed
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleGenerate(true)}
|
||||
disabled={generating}
|
||||
style={{
|
||||
display: "flex", alignItems: "center", gap: 12,
|
||||
padding: "12px 16px", marginBottom: 4, borderRadius: 8,
|
||||
background: "#fff", border: "1px solid #e8e4dc",
|
||||
animationDelay: `${i * 0.05}s`,
|
||||
padding: "6px 14px", borderRadius: 6,
|
||||
background: "none", border: "1px solid #e0dcd4",
|
||||
fontSize: "0.72rem", color: "#8a8478", cursor: "pointer",
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
width: 7, height: 7, borderRadius: "50%",
|
||||
background: "#d4a04a", display: "inline-block", flexShrink: 0,
|
||||
}} />
|
||||
<span style={{ flex: 1, fontSize: "0.84rem", color: "#1a1a1a" }}>{f}</span>
|
||||
<div style={{ width: 80, height: 3, borderRadius: 2, background: "#eae6de" }}>
|
||||
<div style={{ height: "100%", width: "0%", borderRadius: 2, background: "#3d5afe" }} />
|
||||
{generating ? "Regenerating…" : "Regenerate"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div style={{
|
||||
marginTop: 18, padding: "16px 20px",
|
||||
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 10,
|
||||
fontSize: "0.84rem", color: "#2a2824", lineHeight: 1.7,
|
||||
}}>
|
||||
{architecture.summary}
|
||||
</div>
|
||||
|
||||
{/* Apps */}
|
||||
<SectionLabel>Apps — monorepo/apps/</SectionLabel>
|
||||
{architecture.apps.map((app, i) => <AppCard key={i} app={app} />)}
|
||||
|
||||
{/* Packages */}
|
||||
<SectionLabel>Shared packages — monorepo/packages/</SectionLabel>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
|
||||
{architecture.packages.map((pkg, i) => (
|
||||
<div key={i} style={{
|
||||
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 10,
|
||||
padding: "12px 16px",
|
||||
}}>
|
||||
<div style={{ fontSize: "0.82rem", fontWeight: 600, color: "#1a1a1a", fontFamily: "IBM Plex Mono, monospace" }}>
|
||||
packages/{pkg.name}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.76rem", color: "#8a8478", marginTop: 4, lineHeight: 1.5 }}>
|
||||
{pkg.description}
|
||||
</div>
|
||||
<span style={{
|
||||
fontFamily: "IBM Plex Mono, monospace",
|
||||
fontSize: "0.7rem", color: "#a09a90", minWidth: 28, textAlign: "right",
|
||||
}}>
|
||||
0%
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Infrastructure */}
|
||||
{architecture.infrastructure.length > 0 && (
|
||||
<>
|
||||
<SectionLabel>Infrastructure</SectionLabel>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
{architecture.infrastructure.map((infra, i) => (
|
||||
<div key={i} style={{
|
||||
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 8,
|
||||
padding: "10px 16px", display: "flex", gap: 12, alignItems: "flex-start",
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: "0.65rem", fontFamily: "IBM Plex Mono, monospace",
|
||||
color: "#3d5afe", background: "#3d5afe0d",
|
||||
border: "1px solid #3d5afe20", padding: "2px 7px", borderRadius: 4,
|
||||
flexShrink: 0, marginTop: 1,
|
||||
}}>
|
||||
{infra.name}
|
||||
</span>
|
||||
<span style={{ fontSize: "0.78rem", color: "#4a4640", lineHeight: 1.5 }}>
|
||||
{infra.reason}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Integrations */}
|
||||
{architecture.integrations.length > 0 && (
|
||||
<>
|
||||
<SectionLabel>External integrations</SectionLabel>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
{architecture.integrations.map((intg, i) => (
|
||||
<div key={i} style={{
|
||||
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 8,
|
||||
padding: "10px 16px", display: "flex", gap: 12, alignItems: "flex-start",
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: "0.65rem", fontFamily: "IBM Plex Mono, monospace",
|
||||
color: intg.required ? "#9a7b3a" : "#8a8478",
|
||||
background: intg.required ? "#d4a04a12" : "#f6f4f0",
|
||||
border: `1px solid ${intg.required ? "#d4a04a30" : "#e8e4dc"}`,
|
||||
padding: "2px 7px", borderRadius: 4, flexShrink: 0, marginTop: 1,
|
||||
}}>
|
||||
{intg.required ? "required" : "optional"}
|
||||
</span>
|
||||
<div>
|
||||
<div style={{ fontSize: "0.82rem", fontWeight: 600, color: "#1a1a1a" }}>{intg.name}</div>
|
||||
<div style={{ fontSize: "0.75rem", color: "#8a8478", marginTop: 2 }}>{intg.notes}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Risk notes */}
|
||||
{architecture.riskNotes.length > 0 && (
|
||||
<>
|
||||
<SectionLabel>Architecture risks</SectionLabel>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
{architecture.riskNotes.map((risk, i) => (
|
||||
<div key={i} style={{
|
||||
background: "#fff8f0", border: "1px solid #ffe0b2",
|
||||
borderRadius: 8, padding: "10px 16px",
|
||||
fontSize: "0.78rem", color: "#6d4c00", lineHeight: 1.55,
|
||||
display: "flex", gap: 8,
|
||||
}}>
|
||||
<span>⚠️</span><span>{risk}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Confirm section */}
|
||||
<div style={{
|
||||
marginTop: 32, padding: "20px 24px",
|
||||
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 12,
|
||||
borderLeft: "3px solid #1a1a1a",
|
||||
}}>
|
||||
{architectureConfirmed ? (
|
||||
<div>
|
||||
<div style={{ fontSize: "0.88rem", fontWeight: 600, color: "#1a1a1a", marginBottom: 6 }}>
|
||||
✓ Architecture confirmed
|
||||
</div>
|
||||
<p style={{ fontSize: "0.78rem", color: "#6b6560", margin: "0 0 14px" }}>
|
||||
You can still regenerate or adjust the architecture before scaffolding begins. Nothing has been built yet.
|
||||
</p>
|
||||
<Link href={`/${workspace}/project/${projectId}/design`} style={{
|
||||
display: "inline-block", padding: "9px 20px", borderRadius: 7,
|
||||
background: "#1a1a1a", color: "#fff",
|
||||
fontSize: "0.78rem", fontWeight: 600, fontFamily: "Outfit, sans-serif",
|
||||
textDecoration: "none",
|
||||
}}>
|
||||
Choose your design →
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div style={{ fontSize: "0.88rem", fontWeight: 600, color: "#1a1a1a", marginBottom: 6 }}>
|
||||
Does this look right?
|
||||
</div>
|
||||
<p style={{ fontSize: "0.78rem", color: "#6b6560", margin: "0 0 16px", lineHeight: 1.6 }}>
|
||||
Review the structure above. You can regenerate if something's off, or confirm to move to design.
|
||||
You can always come back and adjust before the build starts — nothing gets scaffolded yet.
|
||||
</p>
|
||||
<div style={{ display: "flex", gap: 10 }}>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
disabled={confirming}
|
||||
style={{
|
||||
padding: "9px 22px", borderRadius: 7,
|
||||
background: confirming ? "#8a8478" : "#1a1a1a",
|
||||
color: "#fff", border: "none",
|
||||
fontSize: "0.78rem", fontWeight: 600,
|
||||
fontFamily: "Outfit, sans-serif", cursor: confirming ? "default" : "pointer",
|
||||
}}
|
||||
>
|
||||
{confirming ? "Confirming…" : "Confirm architecture →"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleGenerate(true)}
|
||||
disabled={generating}
|
||||
style={{
|
||||
padding: "9px 18px", borderRadius: 7,
|
||||
background: "none", border: "1px solid #e0dcd4",
|
||||
fontSize: "0.78rem", color: "#8a8478",
|
||||
fontFamily: "Outfit, sans-serif", cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Regenerate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ height: 40 }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user