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 // Phase 1 — Surface picker
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function SurfacePicker({ onConfirm, saving }: { onConfirm: (ids: string[]) => void; saving: boolean }) { function SurfacePicker({
const [selected, setSelected] = useState<Set<string>>(new Set()); onConfirm,
saving,
aiSuggested,
}: {
onConfirm: (ids: string[]) => void;
saving: boolean;
aiSuggested: string[];
}) {
const [selected, setSelected] = useState<Set<string>>(new Set(aiSuggested));
const toggle = (id: string) => { const toggle = (id: string) => {
setSelected(prev => { setSelected(prev => {
@@ -307,17 +330,31 @@ function SurfacePicker({ onConfirm, saving }: { onConfirm: (ids: string[]) => vo
}; };
return ( 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 }}> <h3 style={{ fontFamily: "Newsreader, serif", fontSize: "1.2rem", fontWeight: 400, color: "#1a1a1a", marginBottom: 4 }}>
Design surfaces Design surfaces
</h3> </h3>
<p style={{ fontSize: "0.8rem", color: "#a09a90", marginBottom: 24 }}> <p style={{ fontSize: "0.8rem", color: "#a09a90", marginBottom: aiSuggested.length > 0 ? 10 : 24 }}>
Which surfaces does your product need? Select all that apply. Which surfaces does your product need?
</p> </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 }}> <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 8, marginBottom: 24 }}>
{ALL_SURFACES.map(surface => { {ALL_SURFACES.map(surface => {
const isSelected = selected.has(surface.id); const isSelected = selected.has(surface.id);
const isAiPick = aiSuggested.includes(surface.id);
return ( return (
<button <button
key={surface.id} key={surface.id}
@@ -329,10 +366,19 @@ function SurfacePicker({ onConfirm, saving }: { onConfirm: (ids: string[]) => vo
background: isSelected ? "#1a1a1a08" : "#fff", background: isSelected ? "#1a1a1a08" : "#fff",
boxShadow: isSelected ? "0 0 0 1px #1a1a1a" : "0 1px 2px #1a1a1a05", boxShadow: isSelected ? "0 0 0 1px #1a1a1a" : "0 1px 2px #1a1a1a05",
cursor: "pointer", transition: "all 0.12s", fontFamily: "Outfit", cursor: "pointer", transition: "all 0.12s", fontFamily: "Outfit",
position: "relative",
}} }}
onMouseEnter={e => { if (!isSelected) (e.currentTarget.style.borderColor = "#d0ccc4"); }} onMouseEnter={e => { if (!isSelected) (e.currentTarget.style.borderColor = "#d0ccc4"); }}
onMouseLeave={e => { if (!isSelected) (e.currentTarget.style.borderColor = "#e8e4dc"); }} 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={{ <div style={{
width: 34, height: 34, borderRadius: 8, flexShrink: 0, marginTop: 1, width: 34, height: 34, borderRadius: 8, flexShrink: 0, marginTop: 1,
background: isSelected ? "#1a1a1a" : "#f6f4f0", 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.84rem", fontWeight: 600, color: "#1a1a1a", marginBottom: 3 }}>{surface.name}</div>
<div style={{ fontSize: "0.74rem", color: "#8a8478", lineHeight: 1.5 }}>{surface.description}</div> <div style={{ fontSize: "0.74rem", color: "#8a8478", lineHeight: 1.5 }}>{surface.description}</div>
</div> </div>
{isSelected && ( {isSelected && <span style={{ flexShrink: 0, color: "#1a1a1a", fontSize: "0.85rem", marginTop: 2 }}></span>}
<span style={{ flexShrink: 0, color: "#1a1a1a", fontSize: "0.85rem", marginTop: 2 }}></span>
)}
</button> </button>
); );
})} })}
@@ -392,9 +436,11 @@ export default function DesignPage({ params }: { params: Promise<{ workspace: st
const [savingLock, setSavingLock] = useState<string | null>(null); const [savingLock, setSavingLock] = useState<string | null>(null);
const [savingSurfaces, setSavingSurfaces] = useState(false); const [savingSurfaces, setSavingSurfaces] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [aiSuggestedSurfaces, setAiSuggestedSurfaces] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
fetch(`/api/projects/${projectId}/design-surfaces`) // Load saved design surfaces
const designP = fetch(`/api/projects/${projectId}/design-surfaces`)
.then(r => r.json()) .then(r => r.json())
.then(d => { .then(d => {
const loaded = (d.surfaces ?? []) as string[]; const loaded = (d.surfaces ?? []) as string[];
@@ -402,7 +448,24 @@ export default function DesignPage({ params }: { params: Promise<{ workspace: st
setSurfaceThemes(d.surfaceThemes ?? {}); setSurfaceThemes(d.surfaceThemes ?? {});
setSelectedThemes(d.surfaceThemes ?? {}); setSelectedThemes(d.surfaceThemes ?? {});
if (loaded.length > 0) setActiveSurfaceId(loaded[0]); 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")) .catch(() => toast.error("Failed to load design data"))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, [projectId]); }, [projectId]);
@@ -462,7 +525,7 @@ export default function DesignPage({ params }: { params: Promise<{ workspace: st
} }
if (surfaces.length === 0) { 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)); const activeSurfaces = ALL_SURFACES.filter(s => surfaces.includes(s.id));

View File

@@ -137,16 +137,10 @@ ${phaseContext}
${prd}`; ${prd}`;
try { try {
const res = await fetch(`${AGENT_RUNNER_URL}/atlas/chat`, { const res = await fetch(`${AGENT_RUNNER_URL}/generate`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({ prompt }),
message: prompt,
session_id: `arch_${projectId}_${Date.now()}`,
history: [],
is_init: false,
tools: [], // no tools needed — just structured generation
}),
signal: AbortSignal.timeout(120_000), signal: AbortSignal.timeout(120_000),
}); });