247 lines
9.7 KiB
TypeScript
247 lines
9.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useParams } from "next/navigation";
|
|
|
|
// Maps each PRD section to the discovery phase that populates it
|
|
const PRD_SECTIONS = [
|
|
{ id: "executive_summary", label: "Executive Summary", phaseId: "big_picture" },
|
|
{ id: "problem_statement", label: "Problem Statement", phaseId: "big_picture" },
|
|
{ id: "vision_metrics", label: "Vision & Success Metrics", phaseId: "big_picture" },
|
|
{ id: "users_personas", label: "Users & Personas", phaseId: "users_personas" },
|
|
{ id: "user_flows", label: "User Flows", phaseId: "users_personas" },
|
|
{ id: "feature_requirements", label: "Feature Requirements", phaseId: "features_scope" },
|
|
{ id: "screen_specs", label: "Screen Specs", phaseId: "screens_data" },
|
|
{ id: "business_model", label: "Business Model", phaseId: "business_model" },
|
|
{ id: "integrations", label: "Integrations & Dependencies", phaseId: "features_scope" },
|
|
{ id: "non_functional", label: "Non-Functional Reqs", phaseId: null },
|
|
{ id: "risks", label: "Risks & Mitigations", phaseId: "risks_questions" },
|
|
{ id: "open_questions", label: "Open Questions", phaseId: "risks_questions" },
|
|
];
|
|
|
|
interface SavedPhase {
|
|
phase: string;
|
|
title: string;
|
|
summary: string;
|
|
data: Record<string, unknown>;
|
|
saved_at: string;
|
|
}
|
|
|
|
function formatValue(v: unknown): string {
|
|
if (v === null || v === undefined) return "—";
|
|
if (Array.isArray(v)) return v.map(item => typeof item === "object" ? JSON.stringify(item) : String(item)).join(", ");
|
|
return String(v);
|
|
}
|
|
|
|
function PhaseDataCard({ phase }: { phase: SavedPhase }) {
|
|
const [expanded, setExpanded] = useState(false);
|
|
const entries = Object.entries(phase.data).filter(([, v]) => v !== null && v !== undefined && v !== "");
|
|
return (
|
|
<div style={{
|
|
marginTop: 10, background: "#f6f4f0", borderRadius: 8,
|
|
border: "1px solid #e8e4dc", overflow: "hidden",
|
|
}}>
|
|
<button
|
|
onClick={() => setExpanded(e => !e)}
|
|
style={{
|
|
width: "100%", textAlign: "left", padding: "10px 14px",
|
|
background: "none", border: "none", cursor: "pointer",
|
|
display: "flex", alignItems: "center", justifyContent: "space-between",
|
|
fontFamily: "Outfit, sans-serif",
|
|
}}
|
|
>
|
|
<span style={{ fontSize: "0.78rem", color: "#4a4640", lineHeight: 1.45 }}>
|
|
{phase.summary}
|
|
</span>
|
|
<span style={{ fontSize: "0.7rem", color: "#a09a90", marginLeft: 8, flexShrink: 0 }}>
|
|
{expanded ? "▲" : "▼"}
|
|
</span>
|
|
</button>
|
|
{expanded && entries.length > 0 && (
|
|
<div style={{ padding: "4px 14px 14px", borderTop: "1px solid #e8e4dc" }}>
|
|
{entries.map(([k, v]) => (
|
|
<div key={k} style={{ marginTop: 10 }}>
|
|
<div style={{
|
|
fontSize: "0.6rem", color: "#b5b0a6", textTransform: "uppercase",
|
|
letterSpacing: "0.06em", fontWeight: 600, marginBottom: 2,
|
|
}}>
|
|
{k.replace(/_/g, " ")}
|
|
</div>
|
|
<div style={{ fontSize: "0.78rem", color: "#2a2824", lineHeight: 1.5 }}>
|
|
{formatValue(v)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function PRDPage() {
|
|
const params = useParams();
|
|
const projectId = params.projectId as string;
|
|
const [prd, setPrd] = useState<string | null>(null);
|
|
const [savedPhases, setSavedPhases] = useState<SavedPhase[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
Promise.all([
|
|
fetch(`/api/projects/${projectId}`).then(r => r.json()).catch(() => ({})),
|
|
fetch(`/api/projects/${projectId}/save-phase`).then(r => r.json()).catch(() => ({ phases: [] })),
|
|
]).then(([projectData, phaseData]) => {
|
|
setPrd(projectData?.project?.prd ?? null);
|
|
setSavedPhases(phaseData?.phases ?? []);
|
|
setLoading(false);
|
|
});
|
|
}, [projectId]);
|
|
|
|
const phaseMap = new Map(savedPhases.map(p => [p.phase, p]));
|
|
const savedPhaseIds = new Set(savedPhases.map(p => p.phase));
|
|
|
|
const sections = PRD_SECTIONS.map(s => ({
|
|
...s,
|
|
savedPhase: s.phaseId ? phaseMap.get(s.phaseId) ?? null : null,
|
|
isDone: s.phaseId ? savedPhaseIds.has(s.phaseId) : false,
|
|
}));
|
|
|
|
const doneCount = sections.filter(s => s.isDone).length;
|
|
const totalPct = Math.round((doneCount / sections.length) * 100);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", fontFamily: "Outfit, sans-serif", color: "#a09a90" }}>
|
|
Loading…
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={{ padding: "28px 32px", flex: 1, overflow: "auto", fontFamily: "Outfit, sans-serif" }}>
|
|
{prd ? (
|
|
/* ── Finalized PRD view ── */
|
|
<div style={{ maxWidth: 760 }}>
|
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 20 }}>
|
|
<h3 style={{ fontFamily: "Newsreader, serif", fontSize: "1.2rem", fontWeight: 400, color: "#1a1a1a", margin: 0 }}>
|
|
Product Requirements
|
|
</h3>
|
|
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.72rem", color: "#6b6560", background: "#f0ece4", padding: "4px 10px", borderRadius: 5 }}>
|
|
PRD complete
|
|
</span>
|
|
</div>
|
|
<div style={{
|
|
background: "#fff", borderRadius: 10, border: "1px solid #e8e4dc",
|
|
padding: "28px 32px", lineHeight: 1.8,
|
|
fontSize: "0.88rem", color: "#2a2824",
|
|
whiteSpace: "pre-wrap", fontFamily: "Outfit, sans-serif",
|
|
}}>
|
|
{prd}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
/* ── Section progress view ── */
|
|
<div style={{ maxWidth: 680 }}>
|
|
{/* Progress bar */}
|
|
<div style={{
|
|
display: "flex", alignItems: "center", gap: 16,
|
|
padding: "16px 20px", background: "#fff",
|
|
border: "1px solid #e8e4dc", borderRadius: 10,
|
|
marginBottom: 24, boxShadow: "0 1px 2px #1a1a1a05",
|
|
}}>
|
|
<div style={{
|
|
fontFamily: "IBM Plex Mono, monospace",
|
|
fontSize: "1.4rem", fontWeight: 500, color: "#1a1a1a", minWidth: 52,
|
|
}}>
|
|
{totalPct}%
|
|
</div>
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ height: 4, borderRadius: 2, background: "#eae6de" }}>
|
|
<div style={{
|
|
height: "100%", borderRadius: 2,
|
|
width: `${totalPct}%`, background: "#1a1a1a",
|
|
transition: "width 0.6s ease",
|
|
}} />
|
|
</div>
|
|
</div>
|
|
<span style={{ fontSize: "0.75rem", color: "#a09a90" }}>
|
|
{doneCount}/{sections.length} sections
|
|
</span>
|
|
</div>
|
|
|
|
{/* Sections */}
|
|
{sections.map((s, i) => (
|
|
<div
|
|
key={s.id}
|
|
style={{
|
|
padding: "14px 18px", marginBottom: 6,
|
|
background: "#fff", borderRadius: 10,
|
|
border: `1px solid ${s.isDone ? "#a5d6a740" : "#e8e4dc"}`,
|
|
animationDelay: `${i * 0.04}s`,
|
|
}}
|
|
>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
{/* Status icon */}
|
|
<div style={{
|
|
width: 24, height: 24, borderRadius: 6, flexShrink: 0,
|
|
background: s.isDone ? "#2e7d3210" : "#f6f4f0",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
fontSize: "0.65rem", fontWeight: 700,
|
|
color: s.isDone ? "#2e7d32" : "#c5c0b8",
|
|
}}>
|
|
{s.isDone ? "✓" : "○"}
|
|
</div>
|
|
|
|
<span style={{
|
|
flex: 1, fontSize: "0.84rem",
|
|
color: s.isDone ? "#1a1a1a" : "#a09a90",
|
|
fontWeight: s.isDone ? 500 : 400,
|
|
}}>
|
|
{s.label}
|
|
</span>
|
|
|
|
{s.isDone && s.savedPhase && (
|
|
<span style={{
|
|
fontSize: "0.65rem", fontFamily: "IBM Plex Mono, monospace",
|
|
color: "#2e7d32", background: "#2e7d3210",
|
|
padding: "2px 7px", borderRadius: 4, fontWeight: 500,
|
|
}}>
|
|
saved
|
|
</span>
|
|
)}
|
|
{!s.isDone && !s.phaseId && (
|
|
<span style={{
|
|
fontSize: "0.65rem", fontFamily: "IBM Plex Mono, monospace",
|
|
color: "#b5b0a6", padding: "2px 7px",
|
|
}}>
|
|
generated
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Expandable phase data */}
|
|
{s.isDone && s.savedPhase && (
|
|
<PhaseDataCard phase={s.savedPhase} />
|
|
)}
|
|
|
|
{/* Pending hint */}
|
|
{!s.isDone && (
|
|
<div style={{ marginTop: 6, marginLeft: 36, fontSize: "0.72rem", color: "#c5c0b8" }}>
|
|
{s.phaseId
|
|
? `Complete the ${s.savedPhase ? s.savedPhase.title : "discovery"} phase in Atlas`
|
|
: "Will be generated when PRD is finalized"}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{doneCount === 0 && (
|
|
<p style={{ fontSize: "0.78rem", color: "#b5b0a6", marginTop: 20, textAlign: "center" }}>
|
|
Continue chatting with Atlas — saved phases will appear here automatically.
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|