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
146 lines
4.8 KiB
TypeScript
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>
|
|
);
|
|
}
|