feat(tasks): move Tasks first in toolbar, add Tasks+PRD left nav and content

Made-with: Cursor
This commit is contained in:
2026-03-09 22:02:01 -07:00
parent e3c6b9a9b4
commit 1af5595e35
2 changed files with 160 additions and 1 deletions

View File

@@ -921,6 +921,122 @@ function FileViewer({ selectedPath, fileContent, fileLoading, fileName, rootPath
interface PreviewApp { name: string; url: string | null; status: string; } 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 }: { function PreviewContent({ projectId, apps, activePreviewApp, onSelectApp }: {
projectId: string; projectId: string;
apps: PreviewApp[]; apps: PreviewApp[];
@@ -1160,6 +1276,43 @@ function BuildHubInner() {
</div> </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 */} {/* Preview: deployed apps list */}
{section === "preview" && ( {section === "preview" && (
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}> <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
@@ -1195,6 +1348,12 @@ function BuildHubInner() {
{section === "infrastructure" && ( {section === "infrastructure" && (
<InfraContent tab={activeInfra} projectId={projectId} workspace={workspace} /> <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" && ( {section === "preview" && (
<PreviewContent <PreviewContent
projectId={projectId} projectId={projectId}

View File

@@ -34,8 +34,8 @@ const SECTIONS = [
// Each tool maps to a section param on the build page // Each tool maps to a section param on the build page
const TOOLS = [ const TOOLS = [
{ id: "preview", Icon: MonitorPlay, title: "Preview", section: "preview" },
{ id: "tasks", Icon: ListChecks, title: "Tasks", section: "tasks" }, { id: "tasks", Icon: ListChecks, title: "Tasks", section: "tasks" },
{ id: "preview", Icon: MonitorPlay, title: "Preview", section: "preview" },
{ id: "code", Icon: Code2, title: "Code", section: "code" }, { id: "code", Icon: Code2, title: "Code", section: "code" },
{ id: "design", Icon: Palette, title: "Design", section: "layouts" }, { id: "design", Icon: Palette, title: "Design", section: "layouts" },
{ id: "backend", Icon: Cloud, title: "Backend", section: "infrastructure" }, { id: "backend", Icon: Cloud, title: "Backend", section: "infrastructure" },