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