feat: implement 4 project type flows with unique AI experiences

- New multi-step CreateProjectFlow replaces 2-step modal with TypeSelector
  and 4 setup components (Fresh Idea, Chat Import, Code Import, Migrate)
- overview/page.tsx routes to unique main component per creationMode
- FreshIdeaMain: wraps AtlasChat with post-discovery decision banner
  (Generate PRD vs Plan MVP Test)
- ChatImportMain: 3-stage flow (intake → extracting → review) with
  editable insight buckets (decisions, ideas, questions, architecture, users)
- CodeImportMain: 4-stage flow (input → cloning → mapping → surfaces)
  with architecture map and surface selection
- MigrateMain: 5-stage flow with audit, review, planning, and migration
  plan doc with checkbox-tracked tasks and non-destructive warning banner
- New API routes: analyze-chats, analyze-repo, analysis-status,
  generate-migration-plan (all using Gemini)
- ProjectShell: accepts creationMode prop, filters/renames tabs per type
  (code-import hides PRD, migration hides PRD/Grow/Insights, renames Atlas tab)
- Right panel adapts content based on creationMode

Made-with: Cursor
This commit is contained in:
2026-03-06 12:48:28 -08:00
parent 24812df89b
commit ab100f2e76
19 changed files with 2696 additions and 403 deletions

View File

@@ -0,0 +1,353 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter, useParams } from "next/navigation";
interface MigrateMainProps {
projectId: string;
projectName: string;
sourceData?: { repoUrl?: string; liveUrl?: string; hosting?: string };
analysisResult?: Record<string, unknown>;
migrationPlan?: string;
creationStage?: string;
}
type Stage = "input" | "auditing" | "review" | "planning" | "plan";
const HOSTING_OPTIONS = [
{ value: "", label: "Select hosting provider" },
{ value: "vercel", label: "Vercel" },
{ value: "aws", label: "AWS" },
{ value: "heroku", label: "Heroku" },
{ value: "digitalocean", label: "DigitalOcean" },
{ value: "gcp", label: "Google Cloud Platform" },
{ value: "azure", label: "Microsoft Azure" },
{ value: "railway", label: "Railway" },
{ value: "render", label: "Render" },
{ value: "netlify", label: "Netlify" },
{ value: "self-hosted", label: "Self-hosted / VPS" },
{ value: "other", label: "Other" },
];
function MarkdownRenderer({ md }: { md: string }) {
const lines = md.split('\n');
return (
<div style={{ fontFamily: "Outfit, sans-serif", fontSize: "0.85rem", color: "#1a1a1a", lineHeight: 1.7 }}>
{lines.map((line, i) => {
if (line.startsWith('## ')) return <h2 key={i} style={{ fontFamily: "Newsreader, serif", fontSize: "1.2rem", fontWeight: 500, margin: "24px 0 10px", color: "#1a1a1a" }}>{line.slice(3)}</h2>;
if (line.startsWith('### ')) return <h3 key={i} style={{ fontSize: "0.88rem", fontWeight: 700, margin: "18px 0 6px", color: "#1a1a1a" }}>{line.slice(4)}</h3>;
if (line.startsWith('# ')) return <h1 key={i} style={{ fontFamily: "Newsreader, serif", fontSize: "1.5rem", fontWeight: 400, margin: "0 0 16px", color: "#1a1a1a" }}>{line.slice(2)}</h1>;
if (line.match(/^- \[ \] /)) return (
<div key={i} style={{ display: "flex", alignItems: "flex-start", gap: 8, marginBottom: 5 }}>
<input type="checkbox" style={{ marginTop: 3, accentColor: "#1a1a1a" }} />
<span>{line.slice(6)}</span>
</div>
);
if (line.match(/^- \[x\] /i)) return (
<div key={i} style={{ display: "flex", alignItems: "flex-start", gap: 8, marginBottom: 5 }}>
<input type="checkbox" defaultChecked style={{ marginTop: 3, accentColor: "#1a1a1a" }} />
<span style={{ textDecoration: "line-through", color: "#a09a90" }}>{line.slice(6)}</span>
</div>
);
if (line.startsWith('- ') || line.startsWith('* ')) return <div key={i} style={{ paddingLeft: 16, marginBottom: 4 }}> {line.slice(2)}</div>;
if (line.startsWith('---')) return <hr key={i} style={{ border: "none", borderTop: "1px solid #e8e4dc", margin: "16px 0" }} />;
if (!line.trim()) return <div key={i} style={{ height: "0.6em" }} />;
// Bold inline
const parts = line.split(/(\*\*.*?\*\*)/g).map((seg, j) =>
seg.startsWith("**") && seg.endsWith("**")
? <strong key={j}>{seg.slice(2, -2)}</strong>
: <span key={j}>{seg}</span>
);
return <p key={i} style={{ margin: "0 0 4px" }}>{parts}</p>;
})}
</div>
);
}
export function MigrateMain({
projectId,
projectName,
sourceData,
analysisResult: initialAnalysis,
migrationPlan: initialPlan,
creationStage,
}: MigrateMainProps) {
const router = useRouter();
const params = useParams();
const workspace = params?.workspace as string;
const getInitialStage = (): Stage => {
if (initialPlan) return "plan";
if (creationStage === "planning") return "planning";
if (creationStage === "review" || initialAnalysis) return "review";
if (sourceData?.repoUrl || sourceData?.liveUrl) return "auditing";
return "input";
};
const [stage, setStage] = useState<Stage>(getInitialStage);
const [repoUrl, setRepoUrl] = useState(sourceData?.repoUrl ?? "");
const [liveUrl, setLiveUrl] = useState(sourceData?.liveUrl ?? "");
const [hosting, setHosting] = useState(sourceData?.hosting ?? "");
const [analysisResult, setAnalysisResult] = useState<Record<string, unknown> | null>(initialAnalysis ?? null);
const [migrationPlan, setMigrationPlan] = useState<string>(initialPlan ?? "");
const [progressStep, setProgressStep] = useState<string>("cloning");
const [error, setError] = useState<string | null>(null);
// Poll during audit
useEffect(() => {
if (stage !== "auditing") return;
const interval = setInterval(async () => {
try {
const res = await fetch(`/api/projects/${projectId}/analysis-status`);
const data = await res.json();
setProgressStep(data.stage ?? "cloning");
if (data.stage === "done" && data.analysisResult) {
setAnalysisResult(data.analysisResult);
clearInterval(interval);
setStage("review");
}
} catch { /* keep polling */ }
}, 2500);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stage]);
const startAudit = async () => {
setError(null);
setStage("auditing");
if (repoUrl) {
try {
await fetch(`/api/projects/${projectId}/analyze-repo`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ repoUrl, liveUrl, hosting }),
});
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to start audit");
setStage("input");
}
} else {
// No repo — just use live URL fingerprinting via generate-migration-plan directly
setStage("review");
setAnalysisResult({ summary: `Live product at ${liveUrl}`, rows: [], suggestedSurfaces: [] });
}
};
const startPlanning = async () => {
setStage("planning");
setError(null);
try {
const res = await fetch(`/api/projects/${projectId}/generate-migration-plan`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ analysisResult, sourceData: { repoUrl, liveUrl, hosting } }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Planning failed");
setMigrationPlan(data.migrationPlan);
setStage("plan");
} catch (e) {
setError(e instanceof Error ? e.message : "Planning failed");
setStage("review");
}
};
// ── Stage: input ──────────────────────────────────────────────────────────
if (stage === "input") {
const canProceed = repoUrl.trim().startsWith("http") || liveUrl.trim().startsWith("http");
return (
<div style={{ height: "100%", overflow: "auto", display: "flex", alignItems: "center", justifyContent: "center", padding: 32 }}>
<div style={{ width: "100%", maxWidth: 540, fontFamily: "Outfit, sans-serif" }}>
<div style={{ marginBottom: 28 }}>
<h2 style={{ fontFamily: "Newsreader, serif", fontSize: "1.7rem", fontWeight: 400, color: "#1a1a1a", margin: 0, marginBottom: 6 }}>
Tell us about your product
</h2>
<p style={{ fontSize: "0.82rem", color: "#a09a90", margin: 0 }}>
{projectName} Atlas will audit your current setup and build a safe migration plan.
</p>
</div>
{error && (
<div style={{ padding: "12px 16px", borderRadius: 8, background: "#fff0f0", border: "1px solid #fca5a5", color: "#991b1b", fontSize: "0.8rem", marginBottom: 16 }}>
{error}
</div>
)}
<label style={{ display: "block", fontSize: "0.72rem", fontWeight: 600, color: "#6b6560", marginBottom: 6, letterSpacing: "0.02em" }}>
Repository URL (recommended)
</label>
<input type="text" value={repoUrl} onChange={e => setRepoUrl(e.target.value)}
placeholder="https://github.com/yourorg/your-repo"
style={{ width: "100%", padding: "11px 14px", marginBottom: 16, borderRadius: 8, border: "1px solid #e0dcd4", background: "#faf8f5", fontSize: "0.9rem", fontFamily: "Outfit, sans-serif", color: "#1a1a1a", outline: "none", boxSizing: "border-box" }}
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")} autoFocus
/>
<label style={{ display: "block", fontSize: "0.72rem", fontWeight: 600, color: "#6b6560", marginBottom: 6, letterSpacing: "0.02em" }}>
Live URL (optional)
</label>
<input type="text" value={liveUrl} onChange={e => setLiveUrl(e.target.value)}
placeholder="https://yourproduct.com"
style={{ width: "100%", padding: "11px 14px", marginBottom: 16, borderRadius: 8, border: "1px solid #e0dcd4", background: "#faf8f5", fontSize: "0.9rem", fontFamily: "Outfit, sans-serif", color: "#1a1a1a", outline: "none", boxSizing: "border-box" }}
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
/>
<label style={{ display: "block", fontSize: "0.72rem", fontWeight: 600, color: "#6b6560", marginBottom: 6, letterSpacing: "0.02em" }}>
Current hosting provider
</label>
<select value={hosting} onChange={e => setHosting(e.target.value)}
style={{ width: "100%", padding: "11px 14px", marginBottom: 20, borderRadius: 8, border: "1px solid #e0dcd4", background: "#faf8f5", fontSize: "0.88rem", fontFamily: "Outfit, sans-serif", color: hosting ? "#1a1a1a" : "#a09a90", outline: "none", boxSizing: "border-box", appearance: "none" }}
>
{HOSTING_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
<div style={{ fontSize: "0.75rem", color: "#a09a90", marginBottom: 20, lineHeight: 1.55, padding: "12px 14px", background: "#faf8f5", borderRadius: 8, border: "1px solid #f0ece4" }}>
<strong style={{ color: "#4a2a5a" }}>Non-destructive.</strong> Your existing product stays live throughout. Atlas duplicates, never deletes.
</div>
<button onClick={startAudit} disabled={!canProceed}
style={{ width: "100%", padding: "13px", borderRadius: 8, border: "none", background: canProceed ? "#1a1a1a" : "#e0dcd4", color: canProceed ? "#fff" : "#b5b0a6", fontSize: "0.9rem", fontWeight: 600, fontFamily: "Outfit, sans-serif", cursor: canProceed ? "pointer" : "not-allowed" }}
>
Start audit
</button>
</div>
</div>
);
}
// ── Stage: auditing ───────────────────────────────────────────────────────
if (stage === "auditing") {
const steps = [
{ key: "cloning", label: "Cloning repository" },
{ key: "reading", label: "Reading configuration" },
{ key: "analyzing", label: "Auditing infrastructure" },
{ key: "done", label: "Audit complete" },
];
const currentIdx = steps.findIndex(s => s.key === progressStep);
return (
<div style={{ height: "100%", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "Outfit, sans-serif" }}>
<div style={{ textAlign: "center", maxWidth: 400 }}>
<div style={{ width: 52, height: 52, borderRadius: "50%", border: "3px solid #e0dcd4", borderTopColor: "#4a2a5a", animation: "vibn-mig-spin 0.85s linear infinite", margin: "0 auto 24px" }} />
<style>{`@keyframes vibn-mig-spin { to { transform:rotate(360deg); } }`}</style>
<h3 style={{ fontSize: "1.1rem", fontWeight: 600, color: "#1a1a1a", margin: "0 0 8px" }}>Auditing your product</h3>
<p style={{ fontSize: "0.8rem", color: "#a09a90", margin: "0 0 28px" }}>This is non-destructive your live product is untouched</p>
<div style={{ display: "flex", flexDirection: "column", gap: 8, textAlign: "left" }}>
{steps.map((step, i) => {
const done = i < currentIdx;
const active = i === currentIdx;
return (
<div key={step.key} style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={{ width: 22, height: 22, borderRadius: "50%", flexShrink: 0, background: done ? "#4a2a5a" : "#f6f4f0", border: active ? "2px solid #4a2a5a" : done ? "none" : "2px solid #e0dcd4", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "0.6rem", fontWeight: 700, color: done ? "#fff" : "#a09a90" }}>
{done ? "✓" : active ? <span style={{ width: 8, height: 8, borderRadius: "50%", background: "#4a2a5a", display: "block" }} /> : ""}
</div>
<span style={{ fontSize: "0.8rem", fontWeight: active ? 600 : 400, color: done ? "#6b6560" : active ? "#1a1a1a" : "#b5b0a6" }}>{step.label}</span>
</div>
);
})}
</div>
</div>
</div>
);
}
// ── Stage: review ─────────────────────────────────────────────────────────
if (stage === "review") {
const rows = (analysisResult?.rows as Array<{ category: string; item: string; status: string; detail?: string }>) ?? [];
const summary = (analysisResult?.summary as string) ?? '';
return (
<div style={{ height: "100%", overflow: "auto", padding: "32px 40px", fontFamily: "Outfit, sans-serif" }}>
<div style={{ maxWidth: 760, margin: "0 auto" }}>
<div style={{ marginBottom: 24 }}>
<h2 style={{ fontFamily: "Newsreader, serif", fontSize: "1.7rem", fontWeight: 400, color: "#1a1a1a", margin: 0, marginBottom: 6 }}>Audit complete</h2>
<p style={{ fontSize: "0.8rem", color: "#a09a90", margin: 0 }}>{summary || `${projectName} — review your current infrastructure below.`}</p>
</div>
{rows.length > 0 && (
<div style={{ background: "#fff", borderRadius: 12, border: "1px solid #e8e4dc", overflow: "hidden", marginBottom: 24 }}>
{rows.map((row, i) => {
const colorMap = { found: { bg: "#f0fdf4", text: "#15803d", label: "Found" }, partial: { bg: "#fffbeb", text: "#b45309", label: "Partial" }, missing: { bg: "#fff1f2", text: "#be123c", label: "Missing" } };
const sc = colorMap[row.status as keyof typeof colorMap] ?? colorMap.found;
return (
<div key={i} style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 20px", borderTop: i > 0 ? "1px solid #f6f4f0" : "none" }}>
<div style={{ fontSize: "0.7rem", color: "#a09a90", width: 110, flexShrink: 0 }}>{row.category}</div>
<div style={{ flex: 1, fontSize: "0.82rem", color: "#1a1a1a", fontWeight: 500 }}>{row.item}</div>
{row.detail && <div style={{ fontSize: "0.75rem", color: "#8a8478", flex: 2 }}>{row.detail}</div>}
<div style={{ padding: "3px 10px", borderRadius: 4, background: sc.bg, color: sc.text, fontSize: "0.68rem", fontWeight: 700, flexShrink: 0 }}>{sc.label}</div>
</div>
);
})}
</div>
)}
{error && (
<div style={{ padding: "12px 16px", borderRadius: 8, background: "#fff0f0", border: "1px solid #fca5a5", color: "#991b1b", fontSize: "0.8rem", marginBottom: 16 }}>
{error}
</div>
)}
<div style={{ background: "#1a1a1a", borderRadius: 12, padding: "22px 24px", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16 }}>
<div>
<div style={{ fontSize: "0.88rem", fontWeight: 700, color: "#fff", marginBottom: 3 }}>Ready to build the migration plan?</div>
<div style={{ fontSize: "0.75rem", color: "#8a8478" }}>Atlas will generate a phased migration doc with Mirror, Validate, Cutover, and Decommission phases.</div>
</div>
<button onClick={startPlanning}
style={{ padding: "11px 22px", borderRadius: 8, border: "none", background: "#fff", color: "#1a1a1a", fontSize: "0.85rem", fontWeight: 700, fontFamily: "Outfit, sans-serif", cursor: "pointer", flexShrink: 0 }}
onMouseEnter={e => (e.currentTarget.style.opacity = "0.88")}
onMouseLeave={e => (e.currentTarget.style.opacity = "1")}
>
Generate plan
</button>
</div>
</div>
</div>
);
}
// ── Stage: planning ───────────────────────────────────────────────────────
if (stage === "planning") {
return (
<div style={{ height: "100%", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "Outfit, sans-serif" }}>
<div style={{ textAlign: "center" }}>
<div style={{ width: 52, height: 52, borderRadius: "50%", border: "3px solid #e0dcd4", borderTopColor: "#4a2a5a", animation: "vibn-mig-spin 0.85s linear infinite", margin: "0 auto 20px" }} />
<h3 style={{ fontSize: "1.05rem", fontWeight: 600, color: "#1a1a1a", margin: "0 0 6px" }}>Generating migration plan</h3>
<p style={{ fontSize: "0.8rem", color: "#a09a90", margin: 0 }}>Atlas is designing a safe, phased migration strategy</p>
</div>
</div>
);
}
// ── Stage: plan ───────────────────────────────────────────────────────────
return (
<div style={{ height: "100%", overflow: "auto", fontFamily: "Outfit, sans-serif" }}>
{/* Non-destructive banner */}
<div style={{ background: "#4a2a5a12", borderBottom: "1px solid #4a2a5a30", padding: "12px 32px", display: "flex", alignItems: "center", gap: 10, flexShrink: 0 }}>
<span style={{ fontSize: "1rem" }}>🛡</span>
<div>
<span style={{ fontSize: "0.8rem", fontWeight: 700, color: "#4a2a5a" }}>Non-destructive migration </span>
<span style={{ fontSize: "0.8rem", color: "#6b6560" }}>your existing product stays live throughout every phase. Atlas duplicates, never deletes.</span>
</div>
</div>
<div style={{ padding: "32px 40px" }}>
<div style={{ maxWidth: 760, margin: "0 auto" }}>
<div style={{ marginBottom: 28 }}>
<h2 style={{ fontFamily: "Newsreader, serif", fontSize: "1.7rem", fontWeight: 400, color: "#1a1a1a", margin: 0, marginBottom: 6 }}>Migration Plan</h2>
<p style={{ fontSize: "0.8rem", color: "#a09a90", margin: 0 }}>{projectName} four phased migration with rollback plan</p>
</div>
<div style={{ background: "#fff", borderRadius: 12, border: "1px solid #e8e4dc", padding: "28px 32px" }}>
<MarkdownRenderer md={migrationPlan} />
</div>
<div style={{ marginTop: 20, display: "flex", gap: 10 }}>
<button
onClick={() => router.push(`/${workspace}/project/${projectId}/design`)}
style={{ padding: "11px 22px", borderRadius: 8, border: "none", background: "#1a1a1a", color: "#fff", fontSize: "0.85rem", fontWeight: 600, fontFamily: "Outfit, sans-serif", cursor: "pointer" }}
>
Go to Design
</button>
<button
onClick={() => window.print()}
style={{ padding: "11px 22px", borderRadius: 8, border: "1px solid #e0dcd4", background: "#fff", color: "#6b6560", fontSize: "0.85rem", fontWeight: 500, fontFamily: "Outfit, sans-serif", cursor: "pointer" }}
>
Print / Export
</button>
</div>
</div>
</div>
</div>
);
}