diff --git a/apps/web/src/pages/BlueprintBuilder.tsx b/apps/web/src/pages/BlueprintBuilder.tsx index baeef94..4ed2c1d 100644 --- a/apps/web/src/pages/BlueprintBuilder.tsx +++ b/apps/web/src/pages/BlueprintBuilder.tsx @@ -1,112 +1,637 @@ -import { useState } from "react"; -import { useGenerateBlueprint } from "../hooks/useApi"; -import { Loader2, Sparkles, AlertCircle, ArrowRight } from "lucide-react"; +import { useMemo, useState } from "react"; +import { + AlertCircle, + ArrowLeft, + ArrowRight, + Check, + Download, + FileText, + Loader2, + Sparkles, +} from "lucide-react"; +import type { ScaffoldResponse } from "@stackfast/schemas"; +import { useGenerateBlueprint, useGenerateScaffold } from "../hooks/useApi"; import { BlueprintOutputCard } from "../components/BlueprintOutputCard"; import { Layout } from "../components/Layout"; +import { downloadArchive, generateArchive } from "../lib/archive-generator"; + +type WizardStep = "idea" | "constraints" | "results" | "export"; + +const STEPS: { id: WizardStep; label: string }[] = [ + { id: "idea", label: "Idea" }, + { id: "constraints", label: "Constraints" }, + { id: "results", label: "Results" }, + { id: "export", label: "Export" }, +]; + +type BudgetOption = "low" | "medium" | "high" | "enterprise"; +type TimelineOption = "prototype" | "mvp" | "production"; export function BlueprintBuilder() { + const [step, setStep] = useState("idea"); const [idea, setIdea] = useState(""); const [constraints, setConstraints] = useState(""); - const { mutate: generateBlueprint, data: blueprint, isPending, error } = useGenerateBlueprint(); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!idea) return; - - const constraintsArray = constraints - .split('\n') - .map(c => c.trim()) - .filter(Boolean); - - generateBlueprint({ - idea, - constraints: constraintsArray.length > 0 ? constraintsArray : undefined - }); + const [budget, setBudget] = useState(""); + const [timeline, setTimeline] = useState(""); + const [teamSize, setTeamSize] = useState(""); + const [projectName, setProjectName] = useState("stackfast-app"); + const [downloadError, setDownloadError] = useState(null); + + const { + mutate: generateBlueprint, + data: blueprint, + isPending: isGenerating, + error: generateError, + reset: resetBlueprint, + } = useGenerateBlueprint(); + + const { + mutate: generateScaffold, + data: scaffold, + isPending: isScaffolding, + error: scaffoldError, + reset: resetScaffold, + } = useGenerateScaffold(); + + const stepIndex = STEPS.findIndex((s) => s.id === step); + const canGoNext = useMemo(() => { + if (step === "idea") return idea.trim().length > 0; + if (step === "constraints") return true; + if (step === "results") return !!blueprint; + return false; + }, [step, idea, blueprint]); + + const constraintsArray = useMemo( + () => + constraints + .split("\n") + .map((line) => line.trim()) + .filter(Boolean), + [constraints], + ); + + const handleBack = () => { + const previous = STEPS[stepIndex - 1]; + if (previous) setStep(previous.id); + }; + + const handleGenerate = () => { + if (!idea.trim()) return; + const teamSizeValue = teamSize ? Number(teamSize) : undefined; + generateBlueprint( + { + idea: idea.trim(), + ...(constraintsArray.length > 0 ? { constraints: constraintsArray } : {}), + ...(budget ? { budget } : {}), + ...(timeline ? { timeline } : {}), + ...(teamSizeValue && Number.isFinite(teamSizeValue) + ? { teamSize: teamSizeValue } + : {}), + }, + { + onSuccess: () => { + setStep("results"); + }, + }, + ); + }; + + const handleGenerateScaffold = () => { + if (!blueprint) return; + setDownloadError(null); + generateScaffold( + { + toolIds: blueprint.recommendedStack.toolIds, + projectName: projectName.trim() || "stackfast-app", + }, + { + onSuccess: () => { + setStep("export"); + }, + }, + ); + }; + + const handleDownload = async () => { + if (!scaffold) return; + setDownloadError(null); + try { + const blob = await generateArchive(scaffold.files, scaffold.format, projectName); + downloadArchive(blob, `${projectName || "stackfast-app"}.${scaffold.format}`); + } catch (error) { + setDownloadError( + error instanceof Error ? error.message : "Failed to download archive.", + ); + } + }; + + const handleStartOver = () => { + resetBlueprint(); + resetScaffold(); + setStep("idea"); + setDownloadError(null); }; return (
-
-
- -
-

Idea to Stack

-

- Describe your application idea, and we'll architect the perfect modern tech stack for you. +

+
+ +
+

Idea to Stack

+

+ Describe your application idea, refine the constraints, and export a ready-to-clone + starter repo. +

+
+ + + + {step === "idea" && ( + + )} + + {step === "constraints" && ( + + )} + + {step === "results" && blueprint && ( +
+ +
+ )} + + {step === "export" && ( + + )} + + { + if (step === "idea") setStep("constraints"); + else if (step === "constraints") handleGenerate(); + else if (step === "results") handleGenerateScaffold(); + }} + onStartOver={handleStartOver} + /> +
+ + ); +} + +// --------------------------------------------------------------------------- +// Progress indicator +// --------------------------------------------------------------------------- + +function WizardProgress({ currentStep }: { currentStep: WizardStep }) { + const currentIndex = STEPS.findIndex((s) => s.id === currentStep); + + return ( + + ); +} + +// --------------------------------------------------------------------------- +// Step: idea +// --------------------------------------------------------------------------- + +interface IdeaStepProps { + idea: string; + onIdeaChange: (value: string) => void; +} + +function IdeaStep({ idea, onIdeaChange }: IdeaStepProps) { + return ( +
+
+
+ +