Files
vibn-frontend/components/project-creation/ImportSetup.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

146 lines
4.8 KiB
TypeScript

"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 (
<div style={{ padding: 28 }}>
<SetupHeader
icon="⌘" label="Import existing code" tagline="From GitHub"
accent="#1D4ED8" onBack={step === 0 ? onBack : () => setStep(0)} onClose={onClose}
/>
{step === 0 && (
<>
<FieldLabel>Project name</FieldLabel>
<TextInput
value={name}
onChange={setName}
placeholder="What do you want to call this?"
autoFocus
/>
<ForWhomSelector value={forWhom} onChange={setForWhom} />
<FieldLabel>GitHub repository link</FieldLabel>
<TextInput
value={repoUrl}
onChange={setRepoUrl}
placeholder="https://github.com/yourname/your-repo"
/>
<p style={{ fontSize: 12, color: JM.muted, marginTop: -8, marginBottom: 18, lineHeight: 1.5 }}>
Public repos work today. Private-repo support is coming soon.
</p>
<FlowFooter step={0} total={2} primary={
<PrimaryButton onClick={() => setStep(1)} disabled={!canContinue}>
Next
</PrimaryButton>
} />
</>
)}
{step === 1 && (
<>
<FieldLabel>What do you want to do with it? <span style={{ color: JM.muted, fontWeight: 400 }}>(optional)</span></FieldLabel>
<TextArea
value={intent}
onChange={setIntent}
placeholder="Add a payments page, fix the login bug, and clean up the dashboard."
rows={6}
autoFocus
/>
<p style={{ fontSize: 12, color: JM.muted, marginTop: -8, marginBottom: 18, lineHeight: 1.5 }}>
This tells Vibn where to start. Skip it and you can guide things in chat once the project's open.
</p>
<FlowFooter
step={1} total={2}
secondary={<SecondaryButton onClick={() => setStep(0)}> Back</SecondaryButton>}
primary={
<PrimaryButton onClick={handleCreate} disabled={!canContinue} loading={loading}>
Import & open
</PrimaryButton>
}
/>
</>
)}
</div>
);
}
function FlowFooter({
step, total, primary, secondary,
}: {
step: number; total: number;
primary: React.ReactNode; secondary?: React.ReactNode;
}) {
return (
<div style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 6 }}>
<StepDots step={step} total={total} />
<div style={{ flex: 1 }} />
{secondary}
<div style={{ minWidth: 160 }}>{primary}</div>
</div>
);
}