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