feat: live phase completion in right panel + saved phase data in PRD page
Made-with: Cursor
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ReactNode } from "react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { VIBNSidebar } from "./vibn-sidebar";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
@@ -33,14 +33,22 @@ const TABS = [
|
||||
];
|
||||
|
||||
const DISCOVERY_PHASES = [
|
||||
"Big Picture",
|
||||
"Users & Personas",
|
||||
"Features",
|
||||
"Business Model",
|
||||
"Screens",
|
||||
"Risks",
|
||||
{ id: "big_picture", label: "Big Picture" },
|
||||
{ id: "users_personas", label: "Users & Personas" },
|
||||
{ id: "features_scope", label: "Features" },
|
||||
{ id: "business_model", label: "Business Model" },
|
||||
{ id: "screens_data", label: "Screens" },
|
||||
{ id: "risks_questions", label: "Risks" },
|
||||
];
|
||||
|
||||
interface SavedPhase {
|
||||
phase: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
data: Record<string, unknown>;
|
||||
saved_at: string;
|
||||
}
|
||||
|
||||
function timeAgo(dateStr?: string): string {
|
||||
if (!dateStr) return "—";
|
||||
const date = new Date(dateStr);
|
||||
@@ -90,8 +98,6 @@ export function ProjectShell({
|
||||
projectDescription,
|
||||
projectStatus,
|
||||
projectProgress,
|
||||
discoveryPhase = 0,
|
||||
capturedData = {},
|
||||
createdAt,
|
||||
updatedAt,
|
||||
featureCount = 0,
|
||||
@@ -100,7 +106,26 @@ export function ProjectShell({
|
||||
const activeTab = TABS.find((t) => pathname?.includes(`/${t.path}`))?.id ?? "overview";
|
||||
const progress = projectProgress ?? 0;
|
||||
|
||||
const capturedEntries = Object.entries(capturedData);
|
||||
const [savedPhases, setSavedPhases] = useState<SavedPhase[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/projects/${projectId}/save-phase`)
|
||||
.then(r => r.json())
|
||||
.then(d => setSavedPhases(d.phases ?? []))
|
||||
.catch(() => {});
|
||||
|
||||
// Refresh every 10s while the user is chatting with Atlas
|
||||
const interval = setInterval(() => {
|
||||
fetch(`/api/projects/${projectId}/save-phase`)
|
||||
.then(r => r.json())
|
||||
.then(d => setSavedPhases(d.phases ?? []))
|
||||
.catch(() => {});
|
||||
}, 10_000);
|
||||
return () => clearInterval(interval);
|
||||
}, [projectId]);
|
||||
|
||||
const savedPhaseIds = new Set(savedPhases.map(p => p.phase));
|
||||
const firstUnsavedIdx = DISCOVERY_PHASES.findIndex(p => !savedPhaseIds.has(p.id));
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -212,11 +237,11 @@ export function ProjectShell({
|
||||
{/* Discovery phases */}
|
||||
<SectionLabel>Discovery</SectionLabel>
|
||||
{DISCOVERY_PHASES.map((phase, i) => {
|
||||
const isDone = i < discoveryPhase;
|
||||
const isActive = i === discoveryPhase;
|
||||
const isDone = savedPhaseIds.has(phase.id);
|
||||
const isActive = !isDone && i === firstUnsavedIdx;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
key={phase.id}
|
||||
style={{
|
||||
display: "flex", alignItems: "center", gap: 10,
|
||||
padding: "9px 0",
|
||||
@@ -237,7 +262,7 @@ export function ProjectShell({
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
color: isDone ? "#6b6560" : isActive ? "#1a1a1a" : "#b5b0a6",
|
||||
}}>
|
||||
{phase}
|
||||
{phase.label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -245,20 +270,20 @@ export function ProjectShell({
|
||||
|
||||
<div style={{ height: 1, background: "#f0ece4", margin: "16px 0" }} />
|
||||
|
||||
{/* Captured data */}
|
||||
{/* Captured data — summaries from saved phases */}
|
||||
<SectionLabel>Captured</SectionLabel>
|
||||
{capturedEntries.length > 0 ? (
|
||||
capturedEntries.map(([k, v], i) => (
|
||||
<div key={i} style={{ marginBottom: 14 }}>
|
||||
{savedPhases.length > 0 ? (
|
||||
savedPhases.map((p) => (
|
||||
<div key={p.phase} style={{ marginBottom: 14 }}>
|
||||
<div style={{
|
||||
fontSize: "0.62rem", color: "#b5b0a6",
|
||||
fontSize: "0.62rem", color: "#2e7d32",
|
||||
textTransform: "uppercase", letterSpacing: "0.05em",
|
||||
marginBottom: 3, fontWeight: 600,
|
||||
marginBottom: 3, fontWeight: 600, display: "flex", alignItems: "center", gap: 4,
|
||||
}}>
|
||||
{k}
|
||||
<span>✓</span><span>{p.title}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: "0.8rem", color: "#4a4640", lineHeight: 1.45 }}>
|
||||
{v}
|
||||
<div style={{ fontSize: "0.75rem", color: "#4a4640", lineHeight: 1.45 }}>
|
||||
{p.summary}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user