diff --git a/app/api/projects/create/route.ts b/app/api/projects/create/route.ts
index 97c2e907..bc0e288e 100644
--- a/app/api/projects/create/route.ts
+++ b/app/api/projects/create/route.ts
@@ -78,6 +78,8 @@ export async function POST(request: Request) {
githubRepoUrl,
githubDefaultBranch,
githubToken,
+ creationMode,
+ sourceData,
} = body;
// Check slug uniqueness
@@ -239,6 +241,16 @@ export async function POST(request: Request) {
// Coolify project — one per VIBN project, scopes all app services + DBs.
// Apps are deployed on-demand via apps_create (no auto-scaffold).
coolifyProjectUuid,
+ // How this project was created — drives the AI's first-chat seed.
+ // Shape: { mode: "build"|"oss"|"import", sourceData: {...} } where
+ // sourceData is the path-specific payload from the wizard.
+ creationMode: creationMode ?? null,
+ kickoff: creationMode ? {
+ mode: creationMode,
+ sourceData: sourceData ?? null,
+ vision: vision || null,
+ createdAt: now,
+ } : null,
// Import metadata
isImport: !!githubRepoUrl,
importAnalysisStatus: githubRepoUrl ? 'pending' : null,
diff --git a/components/project-association-prompt.tsx b/components/project-association-prompt.tsx
index eff1a577..b5e581c7 100644
--- a/components/project-association-prompt.tsx
+++ b/components/project-association-prompt.tsx
@@ -298,7 +298,6 @@ export function ProjectAssociationPrompt({ workspace }: { workspace: string }) {
setUnassociatedWorkspace(null);
}
}}
- initialWorkspacePath={unassociatedWorkspace?.workspacePath}
workspace={workspace}
/>
>
diff --git a/components/project-creation/BuildSetup.tsx b/components/project-creation/BuildSetup.tsx
new file mode 100644
index 00000000..b2a38ea3
--- /dev/null
+++ b/components/project-creation/BuildSetup.tsx
@@ -0,0 +1,132 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
+import { JM } from "./modal-theme";
+import {
+ SetupHeader, FieldLabel, TextInput, TextArea, ForWhomSelector,
+ PrimaryButton, SecondaryButton, StepDots, type SetupProps,
+} from "./setup-shared";
+
+/**
+ * "Build your own idea" — two-step setup.
+ * Step 1: project name + audience.
+ * Step 2: describe the idea (free text). Becomes the seed message
+ * for the first AI conversation in the project.
+ */
+export function BuildSetup({ workspace, onClose, onBack }: SetupProps) {
+ const router = useRouter();
+ const [step, setStep] = useState<0 | 1>(0);
+ const [name, setName] = useState("");
+ const [forWhom, setForWhom] = useState<"personal" | "client">("personal");
+ const [idea, setIdea] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ const canContinue = name.trim().length > 0;
+ const canCreate = idea.trim().length > 4;
+
+ const handleCreate = async () => {
+ if (!canCreate) return;
+ setLoading(true);
+ try {
+ const res = await fetch("/api/projects/create", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ projectName: name.trim(),
+ projectType: "web-app",
+ slug: name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
+ vision: idea.trim(),
+ product: { name: name.trim(), isForClient: forWhom === "client" },
+ creationMode: "build",
+ sourceData: { idea: idea.trim() },
+ }),
+ });
+ if (!res.ok) {
+ const err = await res.json();
+ toast.error(err.error || "Failed to create project");
+ return;
+ }
+ const data = await res.json();
+ onClose();
+ router.push(`/${workspace}/project/${data.projectId}/overview`);
+ } catch {
+ toast.error("Something went wrong");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
setStep(0)} onClose={onClose}
+ />
+
+ {step === 0 && (
+ <>
+ Project name
+ { if (e.key === "Enter" && canContinue) setStep(1); }}
+ autoFocus
+ />
+
+
+
+ setStep(1)} disabled={!canContinue}>
+ Next →
+
+ } />
+ >
+ )}
+
+ {step === 1 && (
+ <>
+ What do you want to build?
+
+
+ Don't worry about the tech. Vibn will pick the tools and start building from this description.
+
+
+ setStep(0)}>← Back}
+ primary={
+
+ Start building →
+
+ }
+ />
+ >
+ )}
+
+ );
+}
+
+function FlowFooter({
+ step, total, primary, secondary,
+}: {
+ step: number; total: number;
+ primary: React.ReactNode; secondary?: React.ReactNode;
+}) {
+ return (
+
+
+
+ {secondary}
+
{primary}
+
+ );
+}
diff --git a/components/project-creation/ChatImportSetup.tsx b/components/project-creation/ChatImportSetup.tsx
deleted file mode 100644
index 8f15b88f..00000000
--- a/components/project-creation/ChatImportSetup.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { useRouter } from "next/navigation";
-import { toast } from "sonner";
-import { JM } from "./modal-theme";
-import { SetupHeader, FieldLabel, TextInput, PrimaryButton, type SetupProps } from "./setup-shared";
-
-export function ChatImportSetup({ workspace, onClose, onBack }: SetupProps) {
- const router = useRouter();
- const [name, setName] = useState("");
- const [chatText, setChatText] = useState("");
- const [loading, setLoading] = useState(false);
-
- const canCreate = name.trim().length > 0 && chatText.trim().length > 20;
-
- const handleCreate = async () => {
- if (!canCreate) return;
- setLoading(true);
- try {
- const res = await fetch("/api/projects/create", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- projectName: name.trim(),
- projectType: "web-app",
- slug: name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
- product: { name: name.trim() },
- creationMode: "chat-import",
- sourceData: { chatText: chatText.trim() },
- }),
- });
- if (!res.ok) {
- const err = await res.json();
- toast.error(err.error || "Failed to create project");
- return;
- }
- const data = await res.json();
- onClose();
- router.push(`/${workspace}/project/${data.projectId}/overview`);
- } catch {
- toast.error("Something went wrong");
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
Project name
-
-
-
Paste your chat history
-
- );
-}
diff --git a/components/project-creation/CodeImportSetup.tsx b/components/project-creation/CodeImportSetup.tsx
deleted file mode 100644
index 63bb2a78..00000000
--- a/components/project-creation/CodeImportSetup.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { useRouter } from "next/navigation";
-import { toast } from "sonner";
-import { JM } from "./modal-theme";
-import { SetupHeader, FieldLabel, TextInput, PrimaryButton, type SetupProps } from "./setup-shared";
-
-export function CodeImportSetup({ workspace, onClose, onBack }: SetupProps) {
- const router = useRouter();
- const [name, setName] = useState("");
- const [repoUrl, setRepoUrl] = useState("");
- const [pat, setPat] = useState("");
- const [loading, setLoading] = useState(false);
-
- const isValidUrl = repoUrl.trim().startsWith("http");
- const canCreate = name.trim().length > 0 && isValidUrl;
-
- const handleCreate = async () => {
- if (!canCreate) return;
- setLoading(true);
- try {
- const res = await fetch("/api/projects/create", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- projectName: name.trim(),
- projectType: "web-app",
- slug: name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
- product: { name: name.trim() },
- creationMode: "code-import",
- sourceData: { repoUrl: repoUrl.trim(), pat: pat.trim() || undefined },
- }),
- });
- if (!res.ok) {
- const err = await res.json();
- toast.error(err.error || "Failed to create project");
- return;
- }
- const data = await res.json();
- onClose();
- router.push(`/${workspace}/project/${data.projectId}/overview`);
- } catch {
- toast.error("Something went wrong");
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
Project name
-
-
-
Repository URL
-
-
-
- Personal Access Token{" "}
- (required for private repos)
-
-
setPat(e.target.value)}
- placeholder="ghp_… or similar"
- style={{
- width: "100%", padding: "10px 13px", marginBottom: 20,
- borderRadius: 8, border: `1px solid ${JM.border}`,
- background: JM.inputBg, fontSize: 14,
- fontFamily: JM.fontSans, color: JM.ink,
- outline: "none", boxSizing: "border-box",
- }}
- onFocus={e => (e.currentTarget.style.borderColor = JM.indigo)}
- onBlur={e => (e.currentTarget.style.borderColor = JM.border)}
- />
-
-
- Vibn will clone your repo, read key files, and build a full architecture map — tech stack, routes, database, auth, and third-party integrations. Tokens are used only for cloning and are not stored.
-
-
-
- Import & map →
-
-
- );
-}
diff --git a/components/project-creation/CreateProjectFlow.tsx b/components/project-creation/CreateProjectFlow.tsx
index 14ef96b7..77a06184 100644
--- a/components/project-creation/CreateProjectFlow.tsx
+++ b/components/project-creation/CreateProjectFlow.tsx
@@ -4,10 +4,9 @@ import { useState, useEffect } from "react";
import { createPortal } from "react-dom";
import { Plus_Jakarta_Sans } from "next/font/google";
import { TypeSelector } from "./TypeSelector";
-import { FreshIdeaSetup } from "./FreshIdeaSetup";
-import { ChatImportSetup } from "./ChatImportSetup";
-import { CodeImportSetup } from "./CodeImportSetup";
-import { MigrateSetup } from "./MigrateSetup";
+import { BuildSetup } from "./BuildSetup";
+import { OssSetup } from "./OssSetup";
+import { ImportSetup } from "./ImportSetup";
import { JM } from "./modal-theme";
const modalFont = Plus_Jakarta_Sans({
@@ -17,7 +16,16 @@ const modalFont = Plus_Jakarta_Sans({
display: "swap",
});
-export type CreationMode = "fresh" | "chat-import" | "code-import" | "migration";
+/**
+ * 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;
@@ -28,13 +36,13 @@ interface CreateProjectFlowProps {
type Step = "select-type" | "setup";
export function CreateProjectFlow({ open, onOpenChange, workspace }: CreateProjectFlowProps) {
- const [step, setStep] = useState("setup");
- const [mode, setMode] = useState("fresh");
+ const [step, setStep] = useState("select-type");
+ const [mode, setMode] = useState(null);
useEffect(() => {
if (open) {
- setStep("setup");
- setMode("fresh");
+ setStep("select-type");
+ setMode(null);
}
}, [open]);
@@ -67,7 +75,6 @@ export function CreateProjectFlow({ open, onOpenChange, workspace }: CreateProje
@keyframes vibn-spin { to { transform:rotate(360deg); } }
`}
- {/* Backdrop */}
onOpenChange(false)}
style={{
@@ -79,7 +86,6 @@ export function CreateProjectFlow({ open, onOpenChange, workspace }: CreateProje
}}
/>
- {/* Modal container — matches justine/03_dashboard.html #modal-new */}
onOpenChange(false)}
/>
)}
- {step === "setup" && mode === "fresh" && }
- {step === "setup" && mode === "chat-import" && }
- {step === "setup" && mode === "code-import" && }
- {step === "setup" && mode === "migration" && }
+ {step === "setup" && mode === "build" && }
+ {step === "setup" && mode === "oss" && }
+ {step === "setup" && mode === "import" && }
>,
diff --git a/components/project-creation/FreshIdeaSetup.tsx b/components/project-creation/FreshIdeaSetup.tsx
deleted file mode 100644
index 42924f9d..00000000
--- a/components/project-creation/FreshIdeaSetup.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-"use client";
-
-import { useRef, useState } from "react";
-import { useRouter } from "next/navigation";
-import { toast } from "sonner";
-import { JM } from "./modal-theme";
-import { FieldLabel, TextInput, PrimaryButton, ForWhomSelector, type SetupProps } from "./setup-shared";
-
-export function FreshIdeaSetup({ workspace, onClose }: SetupProps) {
- const router = useRouter();
- const [name, setName] = useState("");
- const [forWhom, setForWhom] = useState<"personal" | "client">("personal");
- const [loading, setLoading] = useState(false);
- const nameRef = useRef(null);
-
- const canCreate = name.trim().length > 0;
-
- const handleCreate = async () => {
- if (!canCreate) return;
- setLoading(true);
- try {
- const res = await fetch("/api/projects/create", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- projectName: name.trim(),
- projectType: "web-app",
- slug: name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
- product: { name: name.trim(), isForClient: forWhom === "client" },
- creationMode: "fresh",
- sourceData: {},
- }),
- });
- if (!res.ok) {
- const err = await res.json();
- toast.error(err.error || "Failed to create project");
- return;
- }
- const data = await res.json();
- onClose();
- router.push(`/${workspace}/project/${data.projectId}/overview`);
- } catch {
- toast.error("Something went wrong");
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
- New project
-
- (e.currentTarget.style.color = JM.mid)}
- onMouseLeave={e => (e.currentTarget.style.color = JM.muted)}
- >
- ×
-
-
-
-
- Project name
- { if (e.key === "Enter" && canCreate) void handleCreate(); }}
- inputRef={nameRef}
- autoFocus
- />
-
-
-
-
-
{ void handleCreate(); }} disabled={!canCreate} loading={loading}>
- Create project →
-
-
- );
-}
diff --git a/components/project-creation/ImportSetup.tsx b/components/project-creation/ImportSetup.tsx
new file mode 100644
index 00000000..7abedd8d
--- /dev/null
+++ b/components/project-creation/ImportSetup.tsx
@@ -0,0 +1,145 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
+import { JM } from "./modal-theme";
+import {
+ SetupHeader, FieldLabel, TextInput, TextArea, ForWhomSelector,
+ PrimaryButton, SecondaryButton, StepDots, type SetupProps,
+} from "./setup-shared";
+
+/**
+ * "Import existing code" — two-step setup.
+ * Step 1: project name + audience + repo URL.
+ * Step 2: describe what you want Vibn to focus on (optional, but
+ * recommended). Becomes the seed for the first AI conversation.
+ *
+ * v1: public repos only. Private-repo OAuth lands in v2.
+ */
+export function ImportSetup({ workspace, onClose, onBack }: SetupProps) {
+ const router = useRouter();
+ const [step, setStep] = useState<0 | 1>(0);
+ const [name, setName] = useState("");
+ const [forWhom, setForWhom] = useState<"personal" | "client">("personal");
+ const [repoUrl, setRepoUrl] = useState("");
+ const [intent, setIntent] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ const isValidUrl = /^https?:\/\//i.test(repoUrl.trim());
+ const canContinue = name.trim().length > 0 && isValidUrl;
+
+ const handleCreate = async () => {
+ if (!canContinue) return;
+ setLoading(true);
+ try {
+ const res = await fetch("/api/projects/create", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ projectName: name.trim(),
+ projectType: "web-app",
+ slug: name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
+ vision: intent.trim() || `Continue work on ${repoUrl.trim()}`,
+ product: { name: name.trim(), isForClient: forWhom === "client" },
+ creationMode: "import",
+ githubRepoUrl: repoUrl.trim(),
+ sourceData: { repoUrl: repoUrl.trim(), intent: intent.trim() || null },
+ }),
+ });
+ if (!res.ok) {
+ const err = await res.json();
+ toast.error(err.error || "Failed to create project");
+ return;
+ }
+ const data = await res.json();
+ onClose();
+ router.push(`/${workspace}/project/${data.projectId}/overview`);
+ } catch {
+ toast.error("Something went wrong");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
setStep(0)} onClose={onClose}
+ />
+
+ {step === 0 && (
+ <>
+ Project name
+
+
+
+
+ GitHub repository link
+
+
+ Public repos work today. Private-repo support is coming soon.
+
+
+ setStep(1)} disabled={!canContinue}>
+ Next →
+
+ } />
+ >
+ )}
+
+ {step === 1 && (
+ <>
+ What do you want to do with it? (optional)
+
+
+ This tells Vibn where to start. Skip it and you can guide things in chat once the project's open.
+
+
+ setStep(0)}>← Back}
+ primary={
+
+ Import & open →
+
+ }
+ />
+ >
+ )}
+
+ );
+}
+
+function FlowFooter({
+ step, total, primary, secondary,
+}: {
+ step: number; total: number;
+ primary: React.ReactNode; secondary?: React.ReactNode;
+}) {
+ return (
+
+
+
+ {secondary}
+
{primary}
+
+ );
+}
diff --git a/components/project-creation/MigrateSetup.tsx b/components/project-creation/MigrateSetup.tsx
deleted file mode 100644
index c449d439..00000000
--- a/components/project-creation/MigrateSetup.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { useRouter } from "next/navigation";
-import { toast } from "sonner";
-import { JM } from "./modal-theme";
-import { SetupHeader, FieldLabel, TextInput, PrimaryButton, type SetupProps } from "./setup-shared";
-
-const HOSTING_OPTIONS = [
- { value: "", label: "Select hosting provider" },
- { value: "vercel", label: "Vercel" },
- { value: "aws", label: "AWS (EC2 / ECS / Elastic Beanstalk)" },
- { value: "heroku", label: "Heroku" },
- { value: "digitalocean", label: "DigitalOcean (Droplet / App Platform)" },
- { 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" },
-];
-
-export function MigrateSetup({ workspace, onClose, onBack }: SetupProps) {
- const router = useRouter();
- const [name, setName] = useState("");
- const [repoUrl, setRepoUrl] = useState("");
- const [liveUrl, setLiveUrl] = useState("");
- const [hosting, setHosting] = useState("");
- const [pat, setPat] = useState("");
- const [loading, setLoading] = useState(false);
-
- const isValidRepo = repoUrl.trim().startsWith("http");
- const isValidLive = liveUrl.trim().startsWith("http");
- const canCreate = name.trim().length > 0 && (isValidRepo || isValidLive);
-
- const handleCreate = async () => {
- if (!canCreate) return;
- setLoading(true);
- try {
- const res = await fetch("/api/projects/create", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- projectName: name.trim(),
- projectType: "web-app",
- slug: name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
- product: { name: name.trim() },
- creationMode: "migration",
- githubRepoUrl: repoUrl.trim() || undefined,
- githubToken: pat.trim() || undefined,
- sourceData: {
- liveUrl: liveUrl.trim() || undefined,
- hosting: hosting || undefined,
- },
- }),
- });
- if (!res.ok) {
- const err = await res.json();
- toast.error(err.error || "Failed to create project");
- return;
- }
- const data = await res.json();
- onClose();
- router.push(`/${workspace}/project/${data.projectId}/overview`);
- } catch {
- toast.error("Something went wrong");
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
Product name
-
-
-
- Repository URL{" "}
- (recommended)
-
-
-
-
- Live URL{" "}
- (optional)
-
-
-
-
-
- Hosting provider
- setHosting(e.target.value)}
- style={{
- width: "100%", padding: "10px 13px", marginBottom: 16,
- borderRadius: 8, border: `1px solid ${JM.border}`,
- background: JM.inputBg, fontSize: 13,
- fontFamily: JM.fontSans, color: hosting ? JM.ink : JM.muted,
- outline: "none", boxSizing: "border-box", appearance: "none",
- backgroundImage: `url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%239CA3AF' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round'/%3E%3C/svg%3E")`,
- backgroundRepeat: "no-repeat", backgroundPosition: "right 12px center",
- }}
- onFocus={e => (e.currentTarget.style.borderColor = JM.indigo)}
- onBlur={e => (e.currentTarget.style.borderColor = JM.border)}
- >
- {HOSTING_OPTIONS.map(o => (
- {o.label}
- ))}
-
-
-
-
- PAT{" "}(private repos)
-
- setPat(e.target.value)}
- placeholder="ghp_…"
- style={{
- width: "100%", padding: "10px 13px", marginBottom: 16,
- borderRadius: 8, border: `1px solid ${JM.border}`,
- background: JM.inputBg, fontSize: 14,
- fontFamily: JM.fontSans, color: JM.ink,
- outline: "none", boxSizing: "border-box",
- }}
- onFocus={e => (e.currentTarget.style.borderColor = JM.indigo)}
- onBlur={e => (e.currentTarget.style.borderColor = JM.border)}
- />
-
-
-
-
- Non-destructive. Vibn builds a full audit and migration plan. Your existing product stays live throughout the entire migration process.
-
-
-
- Start migration plan →
-
-
- );
-}
diff --git a/components/project-creation/OssSetup.tsx b/components/project-creation/OssSetup.tsx
new file mode 100644
index 00000000..1301f9b8
--- /dev/null
+++ b/components/project-creation/OssSetup.tsx
@@ -0,0 +1,172 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
+import { JM } from "./modal-theme";
+import {
+ SetupHeader, FieldLabel, TextInput, TextArea, ForWhomSelector,
+ PrimaryButton, SecondaryButton, SegmentedTabs, StepDots, type SetupProps,
+} from "./setup-shared";
+
+/**
+ * "Run an open source tool" — two-step setup.
+ * Step 1: project name + audience.
+ * Step 2: either paste a link to the OSS repo (e.g. github.com/twentyhq/twenty)
+ * OR describe the tool you want and let Vibn find one.
+ * Both produce a free-text "what do you want to do with it?" seed
+ * that we hand to the AI on first chat.
+ */
+export function OssSetup({ workspace, onClose, onBack }: SetupProps) {
+ const router = useRouter();
+ const [step, setStep] = useState<0 | 1>(0);
+ const [name, setName] = useState("");
+ const [forWhom, setForWhom] = useState<"personal" | "client">("personal");
+
+ const [tab, setTab] = useState<"link" | "describe">("link");
+ const [repoUrl, setRepoUrl] = useState("");
+ const [needs, setNeeds] = useState("");
+
+ const [loading, setLoading] = useState(false);
+
+ const canContinue = name.trim().length > 0;
+ const isValidUrl = /^https?:\/\//i.test(repoUrl.trim());
+ const canCreate =
+ (tab === "link" && isValidUrl) ||
+ (tab === "describe" && needs.trim().length > 4);
+
+ const handleCreate = async () => {
+ if (!canCreate) return;
+ setLoading(true);
+ try {
+ const seed = tab === "link"
+ ? `Install and host this open-source project: ${repoUrl.trim()}`
+ : `Find and install an open-source tool that fits this need: ${needs.trim()}`;
+ const res = await fetch("/api/projects/create", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ projectName: name.trim(),
+ projectType: "service",
+ slug: name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
+ vision: seed,
+ product: { name: name.trim(), isForClient: forWhom === "client" },
+ creationMode: "oss",
+ sourceData: tab === "link"
+ ? { kind: "link", repoUrl: repoUrl.trim() }
+ : { kind: "describe", description: needs.trim() },
+ }),
+ });
+ if (!res.ok) {
+ const err = await res.json();
+ toast.error(err.error || "Failed to create project");
+ return;
+ }
+ const data = await res.json();
+ onClose();
+ router.push(`/${workspace}/project/${data.projectId}/overview`);
+ } catch {
+ toast.error("Something went wrong");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
setStep(0)} onClose={onClose}
+ />
+
+ {step === 0 && (
+ <>
+ Project name
+ { if (e.key === "Enter" && canContinue) setStep(1); }}
+ autoFocus
+ />
+
+
+
+ setStep(1)} disabled={!canContinue}>
+ Next →
+
+ } />
+ >
+ )}
+
+ {step === 1 && (
+ <>
+
+ value={tab}
+ onChange={setTab}
+ options={[
+ { id: "link", label: "I have a link" },
+ { id: "describe", label: "Help me find one" },
+ ]}
+ />
+
+ {tab === "link" ? (
+ <>
+ Link to the open source project
+
+
+ Paste a GitHub link. Vibn will check it works and host it for you.
+
+ >
+ ) : (
+ <>
+ What kind of tool are you looking for?
+
+
+ Vibn will suggest open-source projects that match and let you pick.
+
+ >
+ )}
+
+ setStep(0)}>← Back}
+ primary={
+
+ {tab === "link" ? "Install →" : "Find tools →"}
+
+ }
+ />
+ >
+ )}
+
+ );
+}
+
+function FlowFooter({
+ step, total, primary, secondary,
+}: {
+ step: number; total: number;
+ primary: React.ReactNode; secondary?: React.ReactNode;
+}) {
+ return (
+
+
+
+ {secondary}
+
{primary}
+
+ );
+}
diff --git a/components/project-creation/TypeSelector.tsx b/components/project-creation/TypeSelector.tsx
index d42d6e35..b51fe730 100644
--- a/components/project-creation/TypeSelector.tsx
+++ b/components/project-creation/TypeSelector.tsx
@@ -8,53 +8,36 @@ interface TypeSelectorProps {
onClose: () => void;
}
-const ALL_FLOW_TYPES: {
+const FLOW_TYPES: {
id: CreationMode;
icon: string;
label: string;
- tagline: string;
desc: string;
accent: string;
- hidden?: boolean;
}[] = [
{
- id: "fresh",
+ id: "build",
icon: "✦",
- label: "Fresh Idea",
- tagline: "Start from scratch",
- desc: "Talk through your idea with Vibn. We'll explore it together and shape it into a full product plan.",
+ label: "Build your own idea",
+ desc: "Tell Vibn what you want to make. We'll scaffold the codebase and you'll guide it from there.",
accent: "#4338CA",
},
{
- id: "chat-import",
- icon: "⌁",
- label: "Import Chats",
- tagline: "You've been thinking",
- desc: "Paste conversations from ChatGPT or Claude. Vibn extracts your decisions, ideas, and open questions.",
+ id: "oss",
+ icon: "◇",
+ label: "Run an open source tool",
+ desc: "Install something ready-made — like a CRM, automation, or analytics — and we'll host it for you.",
accent: "#059669",
},
{
- id: "code-import",
+ id: "import",
icon: "⌘",
- label: "Import Code",
- tagline: "Already have a repo",
- desc: "Point Vibn at your GitHub or Bitbucket repo. We'll map your stack and show what's missing.",
+ label: "Import existing code",
+ desc: "Bring a project you already have on GitHub. We'll mirror it, host it, and pick up where you left off.",
accent: "#1D4ED8",
- hidden: true,
- },
- {
- id: "migration",
- icon: "⇢",
- label: "Migrate Product",
- tagline: "Move an existing product",
- desc: "Bring your live product into the VIBN infrastructure. Vibn builds a safe, phased migration plan.",
- accent: "#7C3AED",
- hidden: true,
},
];
-const FLOW_TYPES = ALL_FLOW_TYPES.filter(t => !t.hidden);
-
export function TypeSelector({ onSelect, onClose }: TypeSelectorProps) {
return (
@@ -92,20 +75,19 @@ export function TypeSelector({ onSelect, onClose }: TypeSelectorProps) {
type="button"
onClick={() => onSelect(type.id)}
style={{
- display: "flex", flexDirection: "column", alignItems: "flex-start",
- gap: 0, padding: 16, borderRadius: 12, textAlign: "left",
+ display: "flex", alignItems: "center",
+ gap: 14, padding: 14, borderRadius: 12, textAlign: "left",
border: `1px solid ${JM.border}`,
background: JM.inputBg,
cursor: "pointer",
transition: "border-color 0.15s, background 0.15s, box-shadow 0.15s",
fontFamily: JM.fontSans,
position: "relative",
- overflow: "hidden",
}}
onMouseEnter={e => {
- e.currentTarget.style.borderColor = JM.indigo;
- e.currentTarget.style.background = JM.cream;
- e.currentTarget.style.boxShadow = "0 2px 12px rgba(99,102,241,0.1)";
+ e.currentTarget.style.borderColor = type.accent;
+ e.currentTarget.style.background = "#fff";
+ e.currentTarget.style.boxShadow = `0 2px 12px ${type.accent}1a`;
}}
onMouseLeave={e => {
e.currentTarget.style.borderColor = JM.border;
@@ -114,34 +96,24 @@ export function TypeSelector({ onSelect, onClose }: TypeSelectorProps) {
}}
>
{type.icon}
-
- {type.label}
-
-
- {type.tagline}
+
+
+ {type.label}
+
+
+ {type.desc}
+
-
- {type.desc}
-
-
-
- →
-
+
→
))}
diff --git a/components/project-creation/setup-shared.tsx b/components/project-creation/setup-shared.tsx
index 4b7fe0d6..d69c8319 100644
--- a/components/project-creation/setup-shared.tsx
+++ b/components/project-creation/setup-shared.tsx
@@ -173,6 +173,121 @@ export function TextInput({
);
}
+export function TextArea({
+ value,
+ onChange,
+ placeholder,
+ rows = 5,
+ autoFocus,
+}: {
+ value: string;
+ onChange: (v: string) => void;
+ placeholder?: string;
+ rows?: number;
+ autoFocus?: boolean;
+}) {
+ return (
+