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:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user