- 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
331 lines
13 KiB
TypeScript
331 lines
13 KiB
TypeScript
"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>
|
||
);
|
||
}
|