fix(arch+design): wire architecture and design together

- Architecture route now uses /generate endpoint (no Atlas session
  overhead, no conflicting system prompt) for clean JSON generation
- Design page fetches saved architecture on load and maps designSurfaces
  to known surface IDs via fuzzy match; AI-suggested surfaces are
  pre-selected in the picker with an "AI" badge and explanatory note

Made-with: Cursor
This commit is contained in:
2026-03-03 21:11:27 -08:00
parent bedd7d3470
commit a3aa5e4208
2 changed files with 75 additions and 18 deletions

View File

@@ -291,12 +291,35 @@ function SurfaceSection({
);
}
// ---------------------------------------------------------------------------
// Surface ID fuzzy-match helper — maps AI-generated labels to our surface IDs
// ---------------------------------------------------------------------------
function matchSurfaceId(label: string): string | null {
const l = label.toLowerCase();
if (l.includes("web app") || l.includes("webapp") || l.includes("saas") || l.includes("dashboard") || l.includes("pwa")) return "web-app";
if (l.includes("marketing") || l.includes("landing") || l.includes("site") || l.includes("homepage")) return "marketing";
if (l.includes("admin") || l.includes("internal") || l.includes("back office") || l.includes("backoffice")) return "admin";
if (l.includes("mobile") || l.includes("ios") || l.includes("android") || l.includes("native")) return "mobile";
if (l.includes("email") || l.includes("notification")) return "email";
if (l.includes("doc") || l.includes("content") || l.includes("blog") || l.includes("knowledge")) return "docs";
return null;
}
// ---------------------------------------------------------------------------
// Phase 1 — Surface picker
// ---------------------------------------------------------------------------
function SurfacePicker({ onConfirm, saving }: { onConfirm: (ids: string[]) => void; saving: boolean }) {
const [selected, setSelected] = useState<Set<string>>(new Set());
function SurfacePicker({
onConfirm,
saving,
aiSuggested,
}: {
onConfirm: (ids: string[]) => void;
saving: boolean;
aiSuggested: string[];
}) {
const [selected, setSelected] = useState<Set<string>>(new Set(aiSuggested));
const toggle = (id: string) => {
setSelected(prev => {
@@ -307,17 +330,31 @@ function SurfacePicker({ onConfirm, saving }: { onConfirm: (ids: string[]) => vo
};
return (
<div style={{ padding: "28px 32px", fontFamily: "Outfit, sans-serif", animation: "enter 0.3s ease" }}>
<div style={{ padding: "28px 32px", fontFamily: "Outfit, sans-serif" }}>
<h3 style={{ fontFamily: "Newsreader, serif", fontSize: "1.2rem", fontWeight: 400, color: "#1a1a1a", marginBottom: 4 }}>
Design surfaces
</h3>
<p style={{ fontSize: "0.8rem", color: "#a09a90", marginBottom: 24 }}>
Which surfaces does your product need? Select all that apply.
<p style={{ fontSize: "0.8rem", color: "#a09a90", marginBottom: aiSuggested.length > 0 ? 10 : 24 }}>
Which surfaces does your product need?
</p>
{aiSuggested.length > 0 && (
<div style={{
display: "flex", alignItems: "center", gap: 8, marginBottom: 20,
padding: "10px 14px", background: "#f6f4f0", borderRadius: 8,
border: "1px solid #e8e4dc",
}}>
<span style={{ fontSize: "0.8rem" }}></span>
<span style={{ fontSize: "0.76rem", color: "#4a4640", lineHeight: 1.5 }}>
Based on your PRD, the AI pre-selected the surfaces your product needs. Adjust if needed.
</span>
</div>
)}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 8, marginBottom: 24 }}>
{ALL_SURFACES.map(surface => {
const isSelected = selected.has(surface.id);
const isAiPick = aiSuggested.includes(surface.id);
return (
<button
key={surface.id}
@@ -329,10 +366,19 @@ function SurfacePicker({ onConfirm, saving }: { onConfirm: (ids: string[]) => vo
background: isSelected ? "#1a1a1a08" : "#fff",
boxShadow: isSelected ? "0 0 0 1px #1a1a1a" : "0 1px 2px #1a1a1a05",
cursor: "pointer", transition: "all 0.12s", fontFamily: "Outfit",
position: "relative",
}}
onMouseEnter={e => { if (!isSelected) (e.currentTarget.style.borderColor = "#d0ccc4"); }}
onMouseLeave={e => { if (!isSelected) (e.currentTarget.style.borderColor = "#e8e4dc"); }}
>
{isAiPick && !isSelected && (
<div style={{
position: "absolute", top: 8, right: 8,
fontSize: "0.58rem", color: "#9a7b3a", background: "#d4a04a15",
border: "1px solid #d4a04a30", padding: "1px 5px", borderRadius: 3,
fontWeight: 600, letterSpacing: "0.05em",
}}>AI</div>
)}
<div style={{
width: 34, height: 34, borderRadius: 8, flexShrink: 0, marginTop: 1,
background: isSelected ? "#1a1a1a" : "#f6f4f0",
@@ -345,9 +391,7 @@ function SurfacePicker({ onConfirm, saving }: { onConfirm: (ids: string[]) => vo
<div style={{ fontSize: "0.84rem", fontWeight: 600, color: "#1a1a1a", marginBottom: 3 }}>{surface.name}</div>
<div style={{ fontSize: "0.74rem", color: "#8a8478", lineHeight: 1.5 }}>{surface.description}</div>
</div>
{isSelected && (
<span style={{ flexShrink: 0, color: "#1a1a1a", fontSize: "0.85rem", marginTop: 2 }}></span>
)}
{isSelected && <span style={{ flexShrink: 0, color: "#1a1a1a", fontSize: "0.85rem", marginTop: 2 }}></span>}
</button>
);
})}
@@ -392,9 +436,11 @@ export default function DesignPage({ params }: { params: Promise<{ workspace: st
const [savingLock, setSavingLock] = useState<string | null>(null);
const [savingSurfaces, setSavingSurfaces] = useState(false);
const [loading, setLoading] = useState(true);
const [aiSuggestedSurfaces, setAiSuggestedSurfaces] = useState<string[]>([]);
useEffect(() => {
fetch(`/api/projects/${projectId}/design-surfaces`)
// Load saved design surfaces
const designP = fetch(`/api/projects/${projectId}/design-surfaces`)
.then(r => r.json())
.then(d => {
const loaded = (d.surfaces ?? []) as string[];
@@ -402,7 +448,24 @@ export default function DesignPage({ params }: { params: Promise<{ workspace: st
setSurfaceThemes(d.surfaceThemes ?? {});
setSelectedThemes(d.surfaceThemes ?? {});
if (loaded.length > 0) setActiveSurfaceId(loaded[0]);
return loaded;
});
// Load architecture to get AI-suggested surfaces
const archP = fetch(`/api/projects/${projectId}/architecture`)
.then(r => r.json())
.then(d => {
const arch = d.architecture;
if (arch?.designSurfaces && Array.isArray(arch.designSurfaces)) {
const matched = (arch.designSurfaces as string[])
.map(matchSurfaceId)
.filter((id): id is string => id !== null);
setAiSuggestedSurfaces([...new Set(matched)]);
}
})
.catch(() => {});
Promise.all([designP, archP])
.catch(() => toast.error("Failed to load design data"))
.finally(() => setLoading(false));
}, [projectId]);
@@ -462,7 +525,7 @@ export default function DesignPage({ params }: { params: Promise<{ workspace: st
}
if (surfaces.length === 0) {
return <SurfacePicker onConfirm={handleConfirmSurfaces} saving={savingSurfaces} />;
return <SurfacePicker onConfirm={handleConfirmSurfaces} saving={savingSurfaces} aiSuggested={aiSuggestedSurfaces} />;
}
const activeSurfaces = ALL_SURFACES.filter(s => surfaces.includes(s.id));