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:
330
components/project-main/ChatImportMain.tsx
Normal file
330
components/project-main/ChatImportMain.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
363
components/project-main/CodeImportMain.tsx
Normal file
363
components/project-main/CodeImportMain.tsx
Normal file
@@ -0,0 +1,363 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
|
||||
interface ArchRow {
|
||||
category: string;
|
||||
item: string;
|
||||
status: "found" | "partial" | "missing";
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
interface AnalysisResult {
|
||||
summary: string;
|
||||
rows: ArchRow[];
|
||||
suggestedSurfaces: string[];
|
||||
}
|
||||
|
||||
interface CodeImportMainProps {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
sourceData?: { repoUrl?: string };
|
||||
analysisResult?: AnalysisResult;
|
||||
creationStage?: string;
|
||||
}
|
||||
|
||||
type Stage = "input" | "cloning" | "mapping" | "surfaces";
|
||||
|
||||
const STATUS_COLORS = {
|
||||
found: { bg: "#f0fdf4", text: "#15803d", label: "Found" },
|
||||
partial: { bg: "#fffbeb", text: "#b45309", label: "Partial" },
|
||||
missing: { bg: "#fff1f2", text: "#be123c", label: "Missing" },
|
||||
};
|
||||
|
||||
const CATEGORY_ORDER = [
|
||||
"Tech Stack", "Infrastructure", "Database", "API Surface",
|
||||
"Frontend", "Auth", "Third-party", "Missing / Gaps",
|
||||
];
|
||||
|
||||
const PROGRESS_STEPS = [
|
||||
{ key: "cloning", label: "Cloning repository" },
|
||||
{ key: "reading", label: "Reading key files" },
|
||||
{ key: "analyzing", label: "Mapping architecture" },
|
||||
{ key: "done", label: "Analysis complete" },
|
||||
];
|
||||
|
||||
export function CodeImportMain({
|
||||
projectId,
|
||||
projectName,
|
||||
sourceData,
|
||||
analysisResult: initialResult,
|
||||
creationStage,
|
||||
}: CodeImportMainProps) {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const workspace = params?.workspace as string;
|
||||
|
||||
const hasRepo = !!sourceData?.repoUrl;
|
||||
const getInitialStage = (): Stage => {
|
||||
if (initialResult) return "mapping";
|
||||
if (creationStage === "surfaces") return "surfaces";
|
||||
if (hasRepo) return "cloning";
|
||||
return "input";
|
||||
};
|
||||
|
||||
const [stage, setStage] = useState<Stage>(getInitialStage);
|
||||
const [repoUrl, setRepoUrl] = useState(sourceData?.repoUrl ?? "");
|
||||
const [progressStep, setProgressStep] = useState<string>("cloning");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [result, setResult] = useState<AnalysisResult | null>(initialResult ?? null);
|
||||
const [confirmedSurfaces, setConfirmedSurfaces] = useState<string[]>(
|
||||
initialResult?.suggestedSurfaces ?? []
|
||||
);
|
||||
|
||||
// Kick off analysis when in cloning stage
|
||||
useEffect(() => {
|
||||
if (stage !== "cloning") return;
|
||||
startAnalysis();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [stage]);
|
||||
|
||||
// Poll for analysis status when cloning
|
||||
useEffect(() => {
|
||||
if (stage !== "cloning") 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) {
|
||||
setResult(data.analysisResult);
|
||||
setConfirmedSurfaces(data.analysisResult.suggestedSurfaces ?? []);
|
||||
clearInterval(interval);
|
||||
setStage("mapping");
|
||||
}
|
||||
} catch { /* keep polling */ }
|
||||
}, 2500);
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [stage]);
|
||||
|
||||
const startAnalysis = async () => {
|
||||
setError(null);
|
||||
try {
|
||||
await fetch(`/api/projects/${projectId}/analyze-repo`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ repoUrl }),
|
||||
});
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to start analysis");
|
||||
setStage("input");
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmSurfaces = async () => {
|
||||
try {
|
||||
await fetch(`/api/projects/${projectId}/design-surfaces`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ surfaces: confirmedSurfaces }),
|
||||
});
|
||||
router.push(`/${workspace}/project/${projectId}/design`);
|
||||
} catch { /* navigate anyway */ }
|
||||
};
|
||||
|
||||
const toggleSurface = (s: string) => {
|
||||
setConfirmedSurfaces(prev =>
|
||||
prev.includes(s) ? prev.filter(x => x !== s) : [...prev, s]
|
||||
);
|
||||
};
|
||||
|
||||
// ── Stage: input ──────────────────────────────────────────────────────────
|
||||
if (stage === "input") {
|
||||
const isValid = repoUrl.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 }}>
|
||||
Import your repository
|
||||
</h2>
|
||||
<p style={{ fontSize: "0.82rem", color: "#a09a90", margin: 0 }}>
|
||||
{projectName} — paste a clone URL to map your existing stack.
|
||||
</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 (HTTPS)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={repoUrl}
|
||||
onChange={e => setRepoUrl(e.target.value)}
|
||||
placeholder="https://github.com/yourorg/your-repo"
|
||||
style={{
|
||||
width: "100%", padding: "12px 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")}
|
||||
onKeyDown={e => { if (e.key === "Enter" && isValid) setStage("cloning"); }}
|
||||
autoFocus
|
||||
/>
|
||||
<div style={{ fontSize: "0.75rem", color: "#a09a90", marginBottom: 20, lineHeight: 1.55, padding: "12px 14px", background: "#faf8f5", borderRadius: 8, border: "1px solid #f0ece4" }}>
|
||||
Atlas will clone and map your stack — tech, database, auth, APIs, and what's missing for a complete go-to-market build.
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { if (isValid) setStage("cloning"); }}
|
||||
disabled={!isValid}
|
||||
style={{
|
||||
width: "100%", padding: "13px", borderRadius: 8, border: "none",
|
||||
background: isValid ? "#1a1a1a" : "#e0dcd4",
|
||||
color: isValid ? "#fff" : "#b5b0a6",
|
||||
fontSize: "0.9rem", fontWeight: 600, fontFamily: "Outfit, sans-serif",
|
||||
cursor: isValid ? "pointer" : "not-allowed",
|
||||
}}
|
||||
>
|
||||
Map this repo →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Stage: cloning ────────────────────────────────────────────────────────
|
||||
if (stage === "cloning") {
|
||||
const currentIdx = PROGRESS_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: "#1a1a1a",
|
||||
animation: "vibn-repo-spin 0.85s linear infinite",
|
||||
margin: "0 auto 24px",
|
||||
}} />
|
||||
<style>{`@keyframes vibn-repo-spin { to { transform:rotate(360deg); } }`}</style>
|
||||
<h3 style={{ fontSize: "1.1rem", fontWeight: 600, color: "#1a1a1a", margin: "0 0 8px" }}>
|
||||
Mapping your codebase
|
||||
</h3>
|
||||
<p style={{ fontSize: "0.8rem", color: "#a09a90", margin: "0 0 28px" }}>
|
||||
{repoUrl || sourceData?.repoUrl || "Repository"}
|
||||
</p>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8, textAlign: "left" }}>
|
||||
{PROGRESS_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 ? "#1a1a1a" : active ? "#f6f4f0" : "#f6f4f0",
|
||||
border: active ? "2px solid #1a1a1a" : 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: "#1a1a1a", 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: mapping ────────────────────────────────────────────────────────
|
||||
if (stage === "mapping" && result) {
|
||||
const byCategory: Record<string, ArchRow[]> = {};
|
||||
for (const row of result.rows) {
|
||||
const cat = row.category || "Other";
|
||||
if (!byCategory[cat]) byCategory[cat] = [];
|
||||
byCategory[cat].push(row);
|
||||
}
|
||||
const categories = [
|
||||
...CATEGORY_ORDER.filter(c => byCategory[c]),
|
||||
...Object.keys(byCategory).filter(c => !CATEGORY_ORDER.includes(c)),
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ height: "100%", overflow: "auto", padding: "32px 40px", fontFamily: "Outfit, sans-serif" }}>
|
||||
<div style={{ maxWidth: 800, margin: "0 auto" }}>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h2 style={{ fontFamily: "Newsreader, serif", fontSize: "1.7rem", fontWeight: 400, color: "#1a1a1a", margin: 0, marginBottom: 6 }}>
|
||||
Architecture map
|
||||
</h2>
|
||||
<p style={{ fontSize: "0.8rem", color: "#a09a90", margin: "0 0 4px" }}>
|
||||
{projectName} — {result.summary}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ background: "#fff", borderRadius: 12, border: "1px solid #e8e4dc", overflow: "hidden", marginBottom: 24 }}>
|
||||
{categories.map((cat, catIdx) => (
|
||||
<div key={cat}>
|
||||
{catIdx > 0 && <div style={{ height: 1, background: "#f0ece4" }} />}
|
||||
<div style={{ padding: "12px 20px", background: "#faf8f5", fontSize: "0.68rem", fontWeight: 700, color: "#6b6560", letterSpacing: "0.06em", textTransform: "uppercase" }}>
|
||||
{cat}
|
||||
</div>
|
||||
{byCategory[cat].map((row, i) => {
|
||||
const sc = STATUS_COLORS[row.status];
|
||||
return (
|
||||
<div key={i} style={{ display: "flex", alignItems: "center", gap: 12, padding: "11px 20px", borderTop: "1px solid #f6f4f0" }}>
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setStage("surfaces")}
|
||||
style={{
|
||||
width: "100%", padding: "13px", borderRadius: 8, border: "none",
|
||||
background: "#1a1a1a", color: "#fff",
|
||||
fontSize: "0.9rem", fontWeight: 600, fontFamily: "Outfit, sans-serif", cursor: "pointer",
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.opacity = "0.88")}
|
||||
onMouseLeave={e => (e.currentTarget.style.opacity = "1")}
|
||||
>
|
||||
Choose what to build next →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Stage: surfaces ───────────────────────────────────────────────────────
|
||||
const SURFACE_OPTIONS = [
|
||||
{ id: "marketing", label: "Marketing Site", icon: "◎", desc: "Landing page, pricing, blog" },
|
||||
{ id: "web-app", label: "Web App", icon: "⬡", desc: "Core SaaS product with auth" },
|
||||
{ id: "admin", label: "Admin Panel", icon: "◫", desc: "Ops dashboard, content management" },
|
||||
{ id: "api", label: "API Layer", icon: "⌁", desc: "REST/GraphQL endpoints" },
|
||||
];
|
||||
|
||||
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 }}>
|
||||
What should Atlas build?
|
||||
</h2>
|
||||
<p style={{ fontSize: "0.82rem", color: "#a09a90", margin: 0 }}>
|
||||
Based on the gap analysis, Atlas suggests the surfaces below. Confirm or adjust.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 24 }}>
|
||||
{SURFACE_OPTIONS.map(s => {
|
||||
const selected = confirmedSurfaces.includes(s.id);
|
||||
return (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => toggleSurface(s.id)}
|
||||
style={{
|
||||
padding: "18px", borderRadius: 10, textAlign: "left",
|
||||
border: `2px solid ${selected ? "#1a1a1a" : "#e8e4dc"}`,
|
||||
background: selected ? "#1a1a1a08" : "#fff",
|
||||
cursor: "pointer", fontFamily: "Outfit, sans-serif",
|
||||
transition: "all 0.12s",
|
||||
}}
|
||||
onMouseEnter={e => { if (!selected) e.currentTarget.style.borderColor = "#d0ccc4"; }}
|
||||
onMouseLeave={e => { if (!selected) e.currentTarget.style.borderColor = "#e8e4dc"; }}
|
||||
>
|
||||
<div style={{ fontSize: "1.2rem", marginBottom: 8 }}>{s.icon}</div>
|
||||
<div style={{ fontSize: "0.84rem", fontWeight: 700, color: "#1a1a1a", marginBottom: 3 }}>{s.label}</div>
|
||||
<div style={{ fontSize: "0.73rem", color: "#8a8478" }}>{s.desc}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleConfirmSurfaces}
|
||||
disabled={confirmedSurfaces.length === 0}
|
||||
style={{
|
||||
width: "100%", padding: "13px", borderRadius: 8, border: "none",
|
||||
background: confirmedSurfaces.length > 0 ? "#1a1a1a" : "#e0dcd4",
|
||||
color: confirmedSurfaces.length > 0 ? "#fff" : "#b5b0a6",
|
||||
fontSize: "0.9rem", fontWeight: 600, fontFamily: "Outfit, sans-serif",
|
||||
cursor: confirmedSurfaces.length > 0 ? "pointer" : "not-allowed",
|
||||
}}
|
||||
>
|
||||
Go to Design →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
133
components/project-main/FreshIdeaMain.tsx
Normal file
133
components/project-main/FreshIdeaMain.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AtlasChat } from "@/components/AtlasChat";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
|
||||
const DISCOVERY_PHASES = [
|
||||
"big_picture",
|
||||
"users_personas",
|
||||
"features_scope",
|
||||
"business_model",
|
||||
"screens_data",
|
||||
"risks_questions",
|
||||
];
|
||||
|
||||
interface FreshIdeaMainProps {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
export function FreshIdeaMain({ projectId, projectName }: FreshIdeaMainProps) {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const workspace = params?.workspace as string;
|
||||
|
||||
const [savedPhaseIds, setSavedPhaseIds] = useState<Set<string>>(new Set());
|
||||
const [allDone, setAllDone] = useState(false);
|
||||
const [prdLoading, setPrdLoading] = useState(false);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const poll = () => {
|
||||
fetch(`/api/projects/${projectId}/save-phase`)
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
const ids = new Set<string>((d.phases ?? []).map((p: { phase: string }) => p.phase));
|
||||
setSavedPhaseIds(ids);
|
||||
const done = DISCOVERY_PHASES.every(id => ids.has(id));
|
||||
setAllDone(done);
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
poll();
|
||||
const interval = setInterval(poll, 8_000);
|
||||
return () => clearInterval(interval);
|
||||
}, [projectId]);
|
||||
|
||||
const handleGeneratePRD = async () => {
|
||||
if (prdLoading) return;
|
||||
setPrdLoading(true);
|
||||
try {
|
||||
router.push(`/${workspace}/project/${projectId}/prd`);
|
||||
} finally {
|
||||
setPrdLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMVP = () => {
|
||||
router.push(`/${workspace}/project/${projectId}/build`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ height: "100%", display: "flex", flexDirection: "column", position: "relative" }}>
|
||||
{/* Decision banner — shown when all 6 phases are saved */}
|
||||
{allDone && !dismissed && (
|
||||
<div style={{
|
||||
background: "linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%)",
|
||||
padding: "18px 28px",
|
||||
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||
gap: 16, flexShrink: 0, flexWrap: "wrap",
|
||||
borderBottom: "1px solid #333",
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ fontSize: "0.88rem", fontWeight: 700, color: "#fff", fontFamily: "Outfit, sans-serif", marginBottom: 3 }}>
|
||||
✦ Discovery complete — what's next?
|
||||
</div>
|
||||
<div style={{ fontSize: "0.75rem", color: "#a09a90", fontFamily: "Outfit, sans-serif" }}>
|
||||
Atlas has captured all 6 discovery phases. Choose your next step.
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 10, flexShrink: 0 }}>
|
||||
<button
|
||||
onClick={handleGeneratePRD}
|
||||
disabled={prdLoading}
|
||||
style={{
|
||||
padding: "10px 20px", borderRadius: 8, border: "none",
|
||||
background: "#fff", color: "#1a1a1a",
|
||||
fontSize: "0.84rem", fontWeight: 700,
|
||||
fontFamily: "Outfit, sans-serif", cursor: "pointer",
|
||||
transition: "opacity 0.12s",
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.opacity = "0.88")}
|
||||
onMouseLeave={e => (e.currentTarget.style.opacity = "1")}
|
||||
>
|
||||
{prdLoading ? "Navigating…" : "Generate PRD →"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMVP}
|
||||
style={{
|
||||
padding: "10px 20px", borderRadius: 8,
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "transparent", color: "#fff",
|
||||
fontSize: "0.84rem", fontWeight: 600,
|
||||
fontFamily: "Outfit, sans-serif", cursor: "pointer",
|
||||
transition: "background 0.12s",
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.background = "rgba(255,255,255,0.08)")}
|
||||
onMouseLeave={e => (e.currentTarget.style.background = "transparent")}
|
||||
>
|
||||
Plan MVP Test →
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDismissed(true)}
|
||||
style={{
|
||||
background: "none", border: "none", cursor: "pointer",
|
||||
color: "#666", fontSize: "1rem", padding: "4px 6px",
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
}}
|
||||
title="Dismiss"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AtlasChat
|
||||
projectId={projectId}
|
||||
projectName={projectName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
353
components/project-main/MigrateMain.tsx
Normal file
353
components/project-main/MigrateMain.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user