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,330 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter, useParams } from "next/navigation";
interface AnalysisResult {
decisions: string[];
ideas: string[];
openQuestions: string[];
architecture: string[];
targetUsers: string[];
}
interface ChatImportMainProps {
projectId: string;
projectName: string;
sourceData?: { chatText?: string };
analysisResult?: AnalysisResult;
}
type Stage = "intake" | "extracting" | "review";
function EditableList({
label,
items,
accent,
onChange,
}: {
label: string;
items: string[];
accent: string;
onChange: (items: string[]) => void;
}) {
const handleEdit = (i: number, value: string) => {
const next = [...items];
next[i] = value;
onChange(next);
};
const handleDelete = (i: number) => {
onChange(items.filter((_, idx) => idx !== i));
};
const handleAdd = () => {
onChange([...items, ""]);
};
return (
<div style={{ marginBottom: 16 }}>
<div style={{ fontSize: "0.68rem", fontWeight: 700, color: accent, letterSpacing: "0.06em", textTransform: "uppercase", marginBottom: 8 }}>
{label}
</div>
{items.length === 0 && (
<p style={{ fontSize: "0.75rem", color: "#b5b0a6", fontFamily: "Outfit, sans-serif", margin: "0 0 6px" }}>
Nothing captured.
</p>
)}
{items.map((item, i) => (
<div key={i} style={{ display: "flex", gap: 6, marginBottom: 5 }}>
<input
type="text"
value={item}
onChange={e => handleEdit(i, e.target.value)}
style={{
flex: 1, padding: "7px 10px", borderRadius: 6,
border: "1px solid #e0dcd4", background: "#faf8f5",
fontSize: "0.81rem", fontFamily: "Outfit, sans-serif",
color: "#1a1a1a", outline: "none",
}}
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
/>
<button
onClick={() => handleDelete(i)}
style={{ background: "none", border: "none", cursor: "pointer", color: "#c5c0b8", fontSize: "0.85rem", padding: "4px 6px" }}
onMouseEnter={e => (e.currentTarget.style.color = "#e53e3e")}
onMouseLeave={e => (e.currentTarget.style.color = "#c5c0b8")}
>
×
</button>
</div>
))}
<button
onClick={handleAdd}
style={{
background: "none", border: "1px dashed #e0dcd4", cursor: "pointer",
borderRadius: 6, padding: "5px 10px", fontSize: "0.72rem", color: "#a09a90",
fontFamily: "Outfit, sans-serif", width: "100%",
}}
onMouseEnter={e => (e.currentTarget.style.borderColor = "#b5b0a6")}
onMouseLeave={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
>
+ Add
</button>
</div>
);
}
export function ChatImportMain({
projectId,
projectName,
sourceData,
analysisResult: initialResult,
}: ChatImportMainProps) {
const router = useRouter();
const params = useParams();
const workspace = params?.workspace as string;
const hasChatText = !!sourceData?.chatText;
const [stage, setStage] = useState<Stage>(
initialResult ? "review" : hasChatText ? "extracting" : "intake"
);
const [chatText, setChatText] = useState(sourceData?.chatText ?? "");
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<AnalysisResult>(
initialResult ?? { decisions: [], ideas: [], openQuestions: [], architecture: [], targetUsers: [] }
);
// Kick off extraction automatically if chatText is ready
useEffect(() => {
if (stage === "extracting") {
runExtraction();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stage]);
const runExtraction = async () => {
setError(null);
try {
const res = await fetch(`/api/projects/${projectId}/analyze-chats`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chatText }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Extraction failed");
setResult(data.analysisResult);
setStage("review");
} catch (e) {
setError(e instanceof Error ? e.message : "Something went wrong");
setStage("intake");
}
};
const handlePRD = () => router.push(`/${workspace}/project/${projectId}/prd`);
const handleMVP = () => router.push(`/${workspace}/project/${projectId}/build`);
// ── Stage: intake ─────────────────────────────────────────────────────────
if (stage === "intake") {
return (
<div style={{ height: "100%", overflow: "auto", display: "flex", alignItems: "center", justifyContent: "center", padding: 32 }}>
<div style={{ width: "100%", maxWidth: 640, fontFamily: "Outfit, sans-serif" }}>
<div style={{ marginBottom: 28 }}>
<h2 style={{ fontFamily: "Newsreader, serif", fontSize: "1.7rem", fontWeight: 400, color: "#1a1a1a", margin: 0, marginBottom: 6 }}>
Paste your chat history
</h2>
<p style={{ fontSize: "0.82rem", color: "#a09a90", margin: 0 }}>
{projectName} Atlas will extract decisions, ideas, architecture notes, and more.
</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>
)}
<textarea
value={chatText}
onChange={e => setChatText(e.target.value)}
placeholder={"Paste conversations from ChatGPT, Claude, Gemini, or any AI tool.\n\nCopy the full conversation — Atlas handles the cleanup."}
rows={14}
style={{
width: "100%", padding: "14px 16px", marginBottom: 16,
borderRadius: 10, border: "1px solid #e0dcd4",
background: "#faf8f5", fontSize: "0.85rem", lineHeight: 1.6,
fontFamily: "Outfit, sans-serif", color: "#1a1a1a",
outline: "none", resize: "vertical", boxSizing: "border-box",
}}
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
/>
<button
onClick={() => {
if (chatText.trim().length > 20) {
setStage("extracting");
}
}}
disabled={chatText.trim().length < 20}
style={{
width: "100%", padding: "13px",
borderRadius: 8, border: "none",
background: chatText.trim().length > 20 ? "#1a1a1a" : "#e0dcd4",
color: chatText.trim().length > 20 ? "#fff" : "#b5b0a6",
fontSize: "0.9rem", fontWeight: 600,
fontFamily: "Outfit, sans-serif",
cursor: chatText.trim().length > 20 ? "pointer" : "not-allowed",
}}
>
Extract insights
</button>
</div>
</div>
);
}
// ── Stage: extracting ─────────────────────────────────────────────────────
if (stage === "extracting") {
return (
<div style={{ height: "100%", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "Outfit, sans-serif" }}>
<div style={{ textAlign: "center" }}>
<div style={{
width: 48, height: 48, borderRadius: "50%",
border: "3px solid #e0dcd4", borderTopColor: "#1a1a1a",
animation: "vibn-chat-spin 0.8s linear infinite",
margin: "0 auto 20px",
}} />
<style>{`@keyframes vibn-chat-spin { to { transform:rotate(360deg); } }`}</style>
<h3 style={{ fontSize: "1.05rem", fontWeight: 600, color: "#1a1a1a", margin: "0 0 6px" }}>
Analysing your chats
</h3>
<p style={{ fontSize: "0.8rem", color: "#a09a90", margin: 0 }}>
Atlas is extracting decisions, ideas, and insights
</p>
</div>
</div>
);
}
// ── Stage: review ─────────────────────────────────────────────────────────
return (
<div style={{ height: "100%", overflow: "auto", padding: "32px 40px", fontFamily: "Outfit, sans-serif" }}>
<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 }}>
What Atlas found
</h2>
<p style={{ fontSize: "0.8rem", color: "#a09a90", margin: 0 }}>
Review and edit the extracted insights for <strong>{projectName}</strong>. These will seed your PRD or MVP plan.
</p>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20, marginBottom: 28 }}>
{/* Left column */}
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<div style={{ background: "#fff", borderRadius: 12, border: "1px solid #e8e4dc", padding: "20px 22px" }}>
<EditableList
label="Decisions made"
items={result.decisions}
accent="#1a3a5c"
onChange={items => setResult(r => ({ ...r, decisions: items }))}
/>
</div>
<div style={{ background: "#fff", borderRadius: 12, border: "1px solid #e8e4dc", padding: "20px 22px" }}>
<EditableList
label="Ideas & features"
items={result.ideas}
accent="#2e5a4a"
onChange={items => setResult(r => ({ ...r, ideas: items }))}
/>
</div>
</div>
{/* Right column */}
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<div style={{ background: "#fff", borderRadius: 12, border: "1px solid #e8e4dc", padding: "20px 22px" }}>
<EditableList
label="Open questions"
items={result.openQuestions}
accent="#9a7b3a"
onChange={items => setResult(r => ({ ...r, openQuestions: items }))}
/>
</div>
<div style={{ background: "#fff", borderRadius: 12, border: "1px solid #e8e4dc", padding: "20px 22px" }}>
<EditableList
label="Architecture notes"
items={result.architecture}
accent="#4a3728"
onChange={items => setResult(r => ({ ...r, architecture: items }))}
/>
</div>
<div style={{ background: "#fff", borderRadius: 12, border: "1px solid #e8e4dc", padding: "20px 22px" }}>
<EditableList
label="Target users"
items={result.targetUsers}
accent="#4a2a5a"
onChange={items => setResult(r => ({ ...r, targetUsers: items }))}
/>
</div>
</div>
</div>
{/* Decision buttons */}
<div style={{
background: "#1a1a1a", borderRadius: 12, padding: "22px 24px",
display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16, flexWrap: "wrap",
}}>
<div>
<div style={{ fontSize: "0.88rem", fontWeight: 700, color: "#fff", marginBottom: 3 }}>Ready to move forward?</div>
<div style={{ fontSize: "0.75rem", color: "#8a8478" }}>Choose how you want to proceed with {projectName}.</div>
</div>
<div style={{ display: "flex", gap: 10 }}>
<button
onClick={handlePRD}
style={{
padding: "11px 22px", borderRadius: 8, border: "none",
background: "#fff", color: "#1a1a1a",
fontSize: "0.85rem", fontWeight: 700, fontFamily: "Outfit, sans-serif", cursor: "pointer",
}}
onMouseEnter={e => (e.currentTarget.style.opacity = "0.88")}
onMouseLeave={e => (e.currentTarget.style.opacity = "1")}
>
Generate PRD
</button>
<button
onClick={handleMVP}
style={{
padding: "11px 22px", borderRadius: 8,
border: "1px solid rgba(255,255,255,0.2)", background: "transparent", color: "#fff",
fontSize: "0.85rem", fontWeight: 600, fontFamily: "Outfit, sans-serif", cursor: "pointer",
}}
onMouseEnter={e => (e.currentTarget.style.background = "rgba(255,255,255,0.08)")}
onMouseLeave={e => (e.currentTarget.style.background = "transparent")}
>
Plan MVP Test
</button>
</div>
</div>
</div>
</div>
);
}