feat: add Architecture tab to PRD page and inject arch into COO context
- PRD page now has a tabbed view: PRD | Architecture Architecture tab renders apps, packages, infrastructure, integrations, and risk notes as structured cards. Only shown when arch doc exists. - Advisor route now includes the architecture summary and key fields in the COO's knowledge context so the orchestrator knows what's been planned technically Made-with: Cursor
This commit is contained in:
@@ -78,12 +78,117 @@ function PhaseDataCard({ phase }: { phase: SavedPhase }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ArchApp { name: string; type: string; description: string; tech?: string[]; screens?: string[] }
|
||||||
|
interface ArchInfra { name: string; reason: string }
|
||||||
|
interface ArchPackage { name: string; description: string }
|
||||||
|
interface ArchIntegration { name: string; required?: boolean; notes?: string }
|
||||||
|
interface Architecture {
|
||||||
|
productName?: string;
|
||||||
|
productType?: string;
|
||||||
|
summary?: string;
|
||||||
|
apps?: ArchApp[];
|
||||||
|
packages?: ArchPackage[];
|
||||||
|
infrastructure?: ArchInfra[];
|
||||||
|
integrations?: ArchIntegration[];
|
||||||
|
designSurfaces?: string[];
|
||||||
|
riskNotes?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ArchitectureView({ arch }: { arch: Architecture }) {
|
||||||
|
const Section = ({ title, children }: { title: string; children: React.ReactNode }) => (
|
||||||
|
<div style={{ marginBottom: 24 }}>
|
||||||
|
<div style={{ fontSize: "0.6rem", fontWeight: 700, color: "#b5b0a6", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 10 }}>{title}</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const Card = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<div style={{ background: "#fff", border: "1px solid #e8e4dc", borderRadius: 9, padding: "14px 16px", marginBottom: 8 }}>{children}</div>
|
||||||
|
);
|
||||||
|
const Tag = ({ label }: { label: string }) => (
|
||||||
|
<span style={{ background: "#f0ece4", borderRadius: 4, padding: "2px 7px", fontSize: "0.68rem", color: "#6b6560", fontFamily: "IBM Plex Mono, monospace", marginRight: 4, display: "inline-block", marginBottom: 3 }}>{label}</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: 760 }}>
|
||||||
|
{arch.summary && (
|
||||||
|
<div style={{ background: "#1a1a1a", borderRadius: 10, padding: "18px 22px", marginBottom: 24, color: "#e8e4dc", fontSize: "0.88rem", lineHeight: 1.7 }}>
|
||||||
|
{arch.summary}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(arch.apps ?? []).length > 0 && (
|
||||||
|
<Section title="Applications">
|
||||||
|
{arch.apps!.map(a => (
|
||||||
|
<Card key={a.name}>
|
||||||
|
<div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 4 }}>
|
||||||
|
<span style={{ fontSize: "0.88rem", fontWeight: 600, color: "#1a1a1a" }}>{a.name}</span>
|
||||||
|
<span style={{ fontSize: "0.72rem", color: "#9a9490" }}>{a.type}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: "0.78rem", color: "#4a4640", lineHeight: 1.55, marginBottom: a.tech?.length ? 8 : 0 }}>{a.description}</div>
|
||||||
|
{a.tech?.map(t => <Tag key={t} label={t} />)}
|
||||||
|
{a.screens && a.screens.length > 0 && (
|
||||||
|
<div style={{ marginTop: 6, fontSize: "0.72rem", color: "#a09a90" }}>Screens: {a.screens.join(", ")}</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
{(arch.packages ?? []).length > 0 && (
|
||||||
|
<Section title="Shared Packages">
|
||||||
|
{arch.packages!.map(p => (
|
||||||
|
<Card key={p.name}>
|
||||||
|
<div style={{ display: "flex", gap: 8, alignItems: "baseline" }}>
|
||||||
|
<span style={{ fontSize: "0.84rem", fontWeight: 600, color: "#1a1a1a", fontFamily: "IBM Plex Mono, monospace" }}>{p.name}</span>
|
||||||
|
<span style={{ fontSize: "0.78rem", color: "#4a4640" }}>{p.description}</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
{(arch.infrastructure ?? []).length > 0 && (
|
||||||
|
<Section title="Infrastructure">
|
||||||
|
{arch.infrastructure!.map(i => (
|
||||||
|
<Card key={i.name}>
|
||||||
|
<div style={{ fontSize: "0.84rem", fontWeight: 600, color: "#1a1a1a", marginBottom: 3 }}>{i.name}</div>
|
||||||
|
<div style={{ fontSize: "0.78rem", color: "#4a4640", lineHeight: 1.5 }}>{i.reason}</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
{(arch.integrations ?? []).length > 0 && (
|
||||||
|
<Section title="Integrations">
|
||||||
|
{arch.integrations!.map(i => (
|
||||||
|
<Card key={i.name}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: i.notes ? 4 : 0 }}>
|
||||||
|
<span style={{ fontSize: "0.84rem", fontWeight: 600, color: "#1a1a1a" }}>{i.name}</span>
|
||||||
|
{i.required && <span style={{ fontSize: "0.62rem", background: "#fef3c7", color: "#92400e", padding: "1px 6px", borderRadius: 4 }}>required</span>}
|
||||||
|
</div>
|
||||||
|
{i.notes && <div style={{ fontSize: "0.78rem", color: "#4a4640" }}>{i.notes}</div>}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
{(arch.riskNotes ?? []).length > 0 && (
|
||||||
|
<Section title="Architectural Risks">
|
||||||
|
{arch.riskNotes!.map((r, i) => (
|
||||||
|
<div key={i} style={{ display: "flex", gap: 10, alignItems: "flex-start", marginBottom: 8 }}>
|
||||||
|
<span style={{ fontSize: "0.72rem", color: "#d97706", marginTop: 2, flexShrink: 0 }}>⚠</span>
|
||||||
|
<span style={{ fontSize: "0.82rem", color: "#4a4640", lineHeight: 1.5 }}>{r}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function PRDPage() {
|
export default function PRDPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const projectId = params.projectId as string;
|
const projectId = params.projectId as string;
|
||||||
const [prd, setPrd] = useState<string | null>(null);
|
const [prd, setPrd] = useState<string | null>(null);
|
||||||
|
const [architecture, setArchitecture] = useState<Architecture | null>(null);
|
||||||
const [savedPhases, setSavedPhases] = useState<SavedPhase[]>([]);
|
const [savedPhases, setSavedPhases] = useState<SavedPhase[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [activeTab, setActiveTab] = useState<"prd" | "architecture">("prd");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
@@ -91,6 +196,7 @@ export default function PRDPage() {
|
|||||||
fetch(`/api/projects/${projectId}/save-phase`).then(r => r.json()).catch(() => ({ phases: [] })),
|
fetch(`/api/projects/${projectId}/save-phase`).then(r => r.json()).catch(() => ({ phases: [] })),
|
||||||
]).then(([projectData, phaseData]) => {
|
]).then(([projectData, phaseData]) => {
|
||||||
setPrd(projectData?.project?.prd ?? null);
|
setPrd(projectData?.project?.prd ?? null);
|
||||||
|
setArchitecture(projectData?.project?.architecture ?? null);
|
||||||
setSavedPhases(phaseData?.phases ?? []);
|
setSavedPhases(phaseData?.phases ?? []);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
@@ -116,9 +222,48 @@ export default function PRDPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: "prd" as const, label: "PRD", available: true },
|
||||||
|
{ id: "architecture" as const, label: "Architecture", available: !!architecture },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: "28px 32px", flex: 1, overflow: "auto", fontFamily: "Outfit, sans-serif" }}>
|
<div style={{ padding: "28px 32px", flex: 1, overflow: "auto", fontFamily: "Outfit, sans-serif" }}>
|
||||||
{prd ? (
|
|
||||||
|
{/* Tab bar — only when at least one doc exists */}
|
||||||
|
{(prd || architecture) && (
|
||||||
|
<div style={{ display: "flex", gap: 4, marginBottom: 24 }}>
|
||||||
|
{tabs.map(t => {
|
||||||
|
const isActive = activeTab === t.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
onClick={() => t.available && setActiveTab(t.id)}
|
||||||
|
disabled={!t.available}
|
||||||
|
style={{
|
||||||
|
padding: "6px 14px", borderRadius: 8, border: "none", cursor: t.available ? "pointer" : "default",
|
||||||
|
background: isActive ? "#1a1a1a" : "transparent",
|
||||||
|
color: isActive ? "#fff" : t.available ? "#6b6560" : "#c5c0b8",
|
||||||
|
fontSize: "0.8rem", fontWeight: isActive ? 600 : 400,
|
||||||
|
fontFamily: "Outfit, sans-serif",
|
||||||
|
transition: "all 0.1s",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
{!t.available && <span style={{ marginLeft: 5, fontSize: "0.65rem", opacity: 0.6 }}>—</span>}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Architecture tab */}
|
||||||
|
{activeTab === "architecture" && architecture && (
|
||||||
|
<ArchitectureView arch={architecture} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PRD tab */}
|
||||||
|
{activeTab === "prd" && prd ? (
|
||||||
/* ── Finalized PRD view ── */
|
/* ── Finalized PRD view ── */
|
||||||
<div style={{ maxWidth: 760 }}>
|
<div style={{ maxWidth: 760 }}>
|
||||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 20 }}>
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 20 }}>
|
||||||
@@ -138,7 +283,7 @@ export default function PRDPage() {
|
|||||||
{prd}
|
{prd}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : activeTab === "prd" ? (
|
||||||
/* ── Section progress view ── */
|
/* ── Section progress view ── */
|
||||||
<div style={{ maxWidth: 680 }}>
|
<div style={{ maxWidth: 680 }}>
|
||||||
{/* Progress bar */}
|
{/* Progress bar */}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ async function buildKnowledgeContext(projectId: string, email: string): Promise<
|
|||||||
const vision = (d.productVision as string) ?? (d.vision as string) ?? '';
|
const vision = (d.productVision as string) ?? (d.vision as string) ?? '';
|
||||||
const giteaRepo = (d.giteaRepo as string) ?? '';
|
const giteaRepo = (d.giteaRepo as string) ?? '';
|
||||||
const prd = (d.prd as string) ?? '';
|
const prd = (d.prd as string) ?? '';
|
||||||
|
const architecture = d.architecture as Record<string, unknown> | null ?? null;
|
||||||
const apps = (d.apps as Array<{ name: string; domain?: string; coolifyServiceUuid?: string }>) ?? [];
|
const apps = (d.apps as Array<{ name: string; domain?: string; coolifyServiceUuid?: string }>) ?? [];
|
||||||
const coolifyProjectUuid = (d.coolifyProjectUuid as string) ?? '';
|
const coolifyProjectUuid = (d.coolifyProjectUuid as string) ?? '';
|
||||||
const theiaUrl = (d.theiaWorkspaceUrl as string) ?? '';
|
const theiaUrl = (d.theiaWorkspaceUrl as string) ?? '';
|
||||||
@@ -73,6 +74,15 @@ Operating principles:
|
|||||||
if (coolifyProjectUuid) lines.push(`Coolify project UUID: ${coolifyProjectUuid} — use coolify_list_applications to find its apps`);
|
if (coolifyProjectUuid) lines.push(`Coolify project UUID: ${coolifyProjectUuid} — use coolify_list_applications to find its apps`);
|
||||||
if (theiaUrl) lines.push(`Theia IDE: ${theiaUrl}`);
|
if (theiaUrl) lines.push(`Theia IDE: ${theiaUrl}`);
|
||||||
|
|
||||||
|
// Architecture document
|
||||||
|
if (architecture) {
|
||||||
|
const archApps = (architecture.apps as Array<{ name: string; type: string; description: string }> ?? [])
|
||||||
|
.map(a => ` - ${a.name} (${a.type}): ${a.description}`).join('\n');
|
||||||
|
const archInfra = (architecture.infrastructure as Array<{ name: string; reason: string }> ?? [])
|
||||||
|
.map(i => ` - ${i.name}: ${i.reason}`).join('\n');
|
||||||
|
lines.push(`\n## Technical Architecture\nSummary: ${architecture.summary ?? ''}\n\nApps:\n${archApps}\n\nInfrastructure:\n${archInfra}`);
|
||||||
|
}
|
||||||
|
|
||||||
// PRD or discovery phases
|
// PRD or discovery phases
|
||||||
if (prd) {
|
if (prd) {
|
||||||
// Claude Sonnet has a 200k token context — pass the full PRD, no truncation needed
|
// Claude Sonnet has a 200k token context — pass the full PRD, no truncation needed
|
||||||
|
|||||||
Reference in New Issue
Block a user