Files
vibn-frontend/components/project-main/ChatImportMain.tsx
Mark Henderson ab100f2e76 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
2026-03-06 12:48:28 -08:00

331 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}