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
123 lines
3.8 KiB
TypeScript
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
|
|
);
|
|
}
|