Files
vibn-frontend/components/project-creation/CreateProjectFlow.tsx
Mark Henderson 7a9cd68ea8 feat(project-creation): 3-path wizard — Build / OSS / Import
User feedback: the previous flow was a single-screen "name + audience"
dialog that gave AI no context about what the user actually wanted to
make. That worked for the demo but produced messy projects in practice
because everything was decided after the fact in chat.

The new flow asks the user one human question first ("How would you
like to begin?") and then captures the minimum context needed to seed
the AI's first conversation in the project.

Three paths, each is a 2-step setup screen with internal step dots:

  - Build your own idea  — Step 1: name + audience.  Step 2: free-text
    "what do you want to build". Becomes the project's vision and the
    AI's first-message context.

  - Run an open source tool  — Step 1: name + audience.  Step 2:
    segmented tabs to either (a) paste a GitHub link or (b) describe
    the kind of tool you want and have Vibn find one. Vision is set
    to either "Install and host this open-source project: <url>" or
    "Find and install an open-source tool that fits this need: <desc>"
    so the AI knows which mode to operate in on first chat.

  - Import existing code  — Step 1: name + audience + repo URL.
    Step 2: optional "what do you want to do with it" textarea.
    Public repos only for v1; private-repo OAuth lands later.

Backend:
  - /api/projects/create now accepts and persists `creationMode` and
    `sourceData` on the project record under a `kickoff` blob:
      { mode, sourceData, vision, createdAt }
    The chat endpoint will read this on first turn to seed the AI
    with the user's stated intent rather than asking them to re-type
    it in chat.

Cleanup:
  - Removed FreshIdeaSetup, CodeImportSetup, ChatImportSetup,
    MigrateSetup — replaced by BuildSetup, OssSetup, ImportSetup.
  - Removed the unused initialWorkspacePath prop from
    project-association-prompt (the new flow doesn't take it).
  - TypeSelector defaults are restored — the modal opens on the
    type-picker step now, not directly on a setup form.

UI building blocks added to setup-shared:
  - TextArea (multi-line input)
  - StepDots (page indicator)
  - SegmentedTabs (generic-typed tab selector, used in OSS Step 2)
  - SecondaryButton (used as ← Back inside Step 2)

Made-with: Cursor
2026-04-29 16:16:53 -07:00

123 lines
3.8 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { createPortal } from "react-dom";
import { Plus_Jakarta_Sans } from "next/font/google";
import { TypeSelector } from "./TypeSelector";
import { BuildSetup } from "./BuildSetup";
import { OssSetup } from "./OssSetup";
import { ImportSetup } from "./ImportSetup";
import { JM } from "./modal-theme";
const modalFont = Plus_Jakarta_Sans({
subsets: ["latin"],
weight: ["400", "500", "600", "700", "800"],
variable: "--font-justine-jakarta",
display: "swap",
});
/**
* Three project-creation paths surfaced to the user:
* - "build" — describe an idea, AI scaffolds and builds it.
* - "oss" — install an open-source tool (link OR describe-it-and-find).
* - "import" — bring a public GitHub repo in.
*
* Each path is its own setup screen with internal step dots (basics → describe).
* The TypeSelector is the entry point; ESC closes; backdrop click closes.
*/
export type CreationMode = "build" | "oss" | "import";
interface CreateProjectFlowProps {
open: boolean;
onOpenChange: (open: boolean) => void;
workspace: string;
}
type Step = "select-type" | "setup";
export function CreateProjectFlow({ open, onOpenChange, workspace }: CreateProjectFlowProps) {
const [step, setStep] = useState<Step>("select-type");
const [mode, setMode] = useState<CreationMode | null>(null);
useEffect(() => {
if (open) {
setStep("select-type");
setMode(null);
}
}, [open]);
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onOpenChange(false); };
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [open, onOpenChange]);
if (!open) return null;
const handleSelectType = (selected: CreationMode) => {
setMode(selected);
setStep("setup");
};
const handleBack = () => {
setStep("select-type");
setMode(null);
};
const setupProps = { workspace, onClose: () => onOpenChange(false), onBack: handleBack };
return createPortal(
<>
<style>{`
@keyframes vibn-fadeIn { from { opacity:0; } to { opacity:1; } }
@keyframes vibn-slideUp { from { opacity:0; transform:translateY(14px); } to { opacity:1; transform:translateY(0); } }
@keyframes vibn-spin { to { transform:rotate(360deg); } }
`}</style>
<div
onClick={() => onOpenChange(false)}
style={{
position: "fixed", inset: 0, zIndex: 200,
background: JM.overlay,
backdropFilter: "blur(2px)",
WebkitBackdropFilter: "blur(2px)",
animation: "vibn-fadeIn 0.15s ease",
}}
/>
<div style={{
position: "fixed", inset: 0, zIndex: 201,
display: "flex", alignItems: "center", justifyContent: "center",
padding: 24, pointerEvents: "none",
}}>
<div
onClick={e => e.stopPropagation()}
className={modalFont.variable}
style={{
background: "#fff", borderRadius: 16,
boxShadow: JM.cardShadow,
width: "100%",
maxWidth: JM.cardMaxWidth,
fontFamily: JM.fontSans,
pointerEvents: "all",
animation: "vibn-slideUp 0.18s cubic-bezier(0.4,0,0.2,1)",
overflow: "hidden",
}}
>
{step === "select-type" && (
<TypeSelector
onSelect={handleSelectType}
onClose={() => onOpenChange(false)}
/>
)}
{step === "setup" && mode === "build" && <BuildSetup {...setupProps} />}
{step === "setup" && mode === "oss" && <OssSetup {...setupProps} />}
{step === "setup" && mode === "import" && <ImportSetup {...setupProps} />}
</div>
</div>
</>,
document.body
);
}