feat(tasks): move Tasks first in toolbar, add Tasks+PRD left nav and content
Made-with: Cursor
This commit is contained in:
@@ -921,6 +921,122 @@ function FileViewer({ selectedPath, fileContent, fileLoading, fileName, rootPath
|
||||
|
||||
interface PreviewApp { name: string; url: string | null; status: string; }
|
||||
|
||||
// ── PRD Content ───────────────────────────────────────────────────────────────
|
||||
|
||||
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 PrdContent({ projectId }: { projectId: string }) {
|
||||
const [prd, setPrd] = useState<string | null>(null);
|
||||
const [savedPhases, setSavedPhases] = useState<SavedPhase[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expandedPhase, setExpandedPhase] = useState<string | null>(null);
|
||||
|
||||
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]);
|
||||
|
||||
if (loading) return (
|
||||
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "#a09a90", fontSize: "0.82rem", fontFamily: "Outfit, sans-serif" }}>Loading…</div>
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1, overflow: "auto", padding: "24px 28px", fontFamily: "Outfit, sans-serif" }}>
|
||||
{prd ? (
|
||||
<div style={{ maxWidth: 720 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 20 }}>
|
||||
<h3 style={{ fontFamily: "Newsreader, serif", fontSize: "1.15rem", fontWeight: 400, color: "#1a1a1a", margin: 0 }}>Product Requirements</h3>
|
||||
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.68rem", color: "#2e7d32", background: "#2e7d3210", padding: "3px 9px", borderRadius: 5 }}>PRD complete</span>
|
||||
</div>
|
||||
<div style={{ background: "#fff", borderRadius: 10, border: "1px solid #e8e4dc", padding: "24px 28px", lineHeight: 1.8, fontSize: "0.86rem", color: "#2a2824", whiteSpace: "pre-wrap" }}>
|
||||
{prd}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ maxWidth: 640 }}>
|
||||
{/* Progress bar */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 14, padding: "14px 18px", background: "#fff", border: "1px solid #e8e4dc", borderRadius: 10, marginBottom: 20 }}>
|
||||
<div style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "1.3rem", fontWeight: 500, color: "#1a1a1a", minWidth: 46 }}>{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.72rem", color: "#a09a90" }}>{doneCount}/{sections.length} sections</span>
|
||||
</div>
|
||||
|
||||
{sections.map((s, i) => (
|
||||
<div key={s.id} style={{ padding: "12px 16px", marginBottom: 5, background: "#fff", borderRadius: 10, border: `1px solid ${s.isDone ? "#a5d6a740" : "#e8e4dc"}` }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<div style={{ width: 22, height: 22, borderRadius: 6, flexShrink: 0, background: s.isDone ? "#2e7d3210" : "#f6f4f0", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "0.6rem", fontWeight: 700, color: s.isDone ? "#2e7d32" : "#c5c0b8" }}>
|
||||
{s.isDone ? "✓" : "○"}
|
||||
</div>
|
||||
<span style={{ flex: 1, fontSize: "0.82rem", color: s.isDone ? "#1a1a1a" : "#a09a90", fontWeight: s.isDone ? 500 : 400 }}>{s.label}</span>
|
||||
{s.isDone && s.savedPhase && (
|
||||
<button onClick={() => setExpandedPhase(expandedPhase === s.id ? null : s.id)} style={{ background: "none", border: "none", cursor: "pointer", fontSize: "0.62rem", color: "#2e7d32", fontFamily: "IBM Plex Mono, monospace", padding: "2px 6px" }}>
|
||||
{expandedPhase === s.id ? "▲" : "▼"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{s.isDone && s.savedPhase && expandedPhase === s.id && (
|
||||
<div style={{ marginTop: 10, paddingTop: 10, borderTop: "1px solid #e8e4dc", paddingLeft: 32 }}>
|
||||
<div style={{ fontSize: "0.75rem", color: "#4a4640", lineHeight: 1.5, marginBottom: 8 }}>{s.savedPhase.summary}</div>
|
||||
{Object.entries(s.savedPhase.data).filter(([, v]) => v !== null && v !== undefined && v !== "").map(([k, v]) => (
|
||||
<div key={k} style={{ marginTop: 8 }}>
|
||||
<div style={{ fontSize: "0.57rem", color: "#b5b0a6", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600, marginBottom: 2 }}>{k.replace(/_/g, " ")}</div>
|
||||
<div style={{ fontSize: "0.76rem", color: "#2a2824", lineHeight: 1.5 }}>{Array.isArray(v) ? v.join(", ") : String(v)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!s.isDone && (
|
||||
<div style={{ marginTop: 5, marginLeft: 32, fontSize: "0.7rem", color: "#c5c0b8" }}>
|
||||
{s.phaseId ? "Complete this phase in Atlas" : "Generated when PRD is finalized"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{doneCount === 0 && (
|
||||
<p style={{ fontSize: "0.76rem", color: "#b5b0a6", marginTop: 16, textAlign: "center" }}>Continue chatting with Atlas — saved phases appear here automatically.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviewContent({ projectId, apps, activePreviewApp, onSelectApp }: {
|
||||
projectId: string;
|
||||
apps: PreviewApp[];
|
||||
@@ -1160,6 +1276,43 @@ function BuildHubInner() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tasks: sub-nav + app list */}
|
||||
{section === "tasks" && (
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
{/* Tasks | PRD sub-nav */}
|
||||
<div style={{ flexShrink: 0, padding: "8px 10px", borderBottom: "1px solid #e8e4dc", display: "flex", gap: 4 }}>
|
||||
{[{ id: "tasks", label: "Tasks" }, { id: "prd", label: "PRD" }].map(item => {
|
||||
const isActive = (searchParams.get("tab") ?? "tasks") === item.id;
|
||||
return (
|
||||
<button key={item.id} onClick={() => navigate({ section: "tasks", tab: item.id })} style={{
|
||||
flex: 1, padding: "5px 0", border: "none", borderRadius: 6, cursor: "pointer",
|
||||
fontSize: "0.72rem", fontWeight: 600, fontFamily: "Outfit, sans-serif",
|
||||
background: isActive ? "#1a1a1a" : "transparent",
|
||||
color: isActive ? "#fff" : "#a09a90",
|
||||
transition: "all 0.12s",
|
||||
}}>
|
||||
{item.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* App list (only in tasks tab) */}
|
||||
{(searchParams.get("tab") ?? "tasks") !== "prd" && (
|
||||
<>
|
||||
<div style={NAV_GROUP_LABEL}>Apps</div>
|
||||
{apps.length > 0 ? apps.map(app => (
|
||||
<NavItem key={app.name} label={app.name} indent
|
||||
active={activeApp === app.name}
|
||||
onClick={() => navigate({ section: "tasks", tab: "tasks", app: app.name, root: app.path })}
|
||||
/>
|
||||
)) : (
|
||||
<div style={{ padding: "8px 22px", fontSize: "0.74rem", color: "#b5b0a6", fontFamily: "Outfit, sans-serif" }}>No apps yet</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preview: deployed apps list */}
|
||||
{section === "preview" && (
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
@@ -1195,6 +1348,12 @@ function BuildHubInner() {
|
||||
{section === "infrastructure" && (
|
||||
<InfraContent tab={activeInfra} projectId={projectId} workspace={workspace} />
|
||||
)}
|
||||
{section === "tasks" && (searchParams.get("tab") ?? "tasks") !== "prd" && (
|
||||
<AgentMode projectId={projectId} appName={activeApp} appPath={activeRoot} />
|
||||
)}
|
||||
{section === "tasks" && searchParams.get("tab") === "prd" && (
|
||||
<PrdContent projectId={projectId} />
|
||||
)}
|
||||
{section === "preview" && (
|
||||
<PreviewContent
|
||||
projectId={projectId}
|
||||
|
||||
Reference in New Issue
Block a user