From 96aaccabb2435df7824d2c8573e25b9e53b9da97 Mon Sep 17 00:00:00 2001 From: spivakov83 Date: Thu, 2 Oct 2025 22:33:32 +0300 Subject: [PATCH] feat: enhance wizard completion summary with file output options - Added file output configuration to the WizardCompletionSummary component. - Implemented file selection functionality with visual feedback for selected files. - Removed the "change-file" confirmation dialog as it is no longer needed. - Updated stacks.json to ensure Angular is listed correctly. - Introduced wizard-config.ts and wizard-storage.ts for better state management and configuration handling. - Added utility functions for building file options and slugs. - Updated types to include slug in FileOutputConfig. - Removed unused image files from the public directory. --- agents.md | 94 ++ app/layout.tsx | 40 +- app/new/page.tsx | 176 +-- app/new/stack/[[...stackSegments]]/page.tsx | 153 ++ app/new/stack/stack-summary-page.tsx | 401 +++++ app/new/stack/stack-wizard-client.tsx | 59 + app/page.tsx | 5 +- app/sitemap.ts | 52 +- components/Hero.tsx | 197 ++- components/Logo/logo-dark.svg | 136 -- components/Logo/logo.svg | 137 -- components/Logo/logoText.svg | 1579 ------------------- components/instructions-wizard.tsx | 669 +++----- components/wizard-completion-summary.tsx | 57 +- components/wizard-confirmation-dialog.tsx | 5 - data/stacks.json | 22 +- lib/wizard-config.ts | 99 ++ lib/wizard-storage.ts | 59 + lib/wizard-summary-data.ts | 68 + lib/wizard-summary.ts | 9 +- lib/wizard-utils.ts | 24 + public/1758260486682rjzi0153.jpg | Bin 79448 -> 0 bytes public/icon-192x192.png | Bin 15267 -> 0 bytes public/icon-512x512.png | Bin 78835 -> 0 bytes public/logo.png | Bin 1448046 -> 26326 bytes public/logo3d.png | Bin 1506021 -> 0 bytes types/wizard.ts | 9 +- 27 files changed, 1515 insertions(+), 2535 deletions(-) create mode 100644 app/new/stack/[[...stackSegments]]/page.tsx create mode 100644 app/new/stack/stack-summary-page.tsx create mode 100644 app/new/stack/stack-wizard-client.tsx delete mode 100644 components/Logo/logo-dark.svg delete mode 100644 components/Logo/logo.svg delete mode 100644 components/Logo/logoText.svg create mode 100644 lib/wizard-config.ts create mode 100644 lib/wizard-storage.ts create mode 100644 lib/wizard-summary-data.ts delete mode 100644 public/1758260486682rjzi0153.jpg delete mode 100644 public/icon-192x192.png delete mode 100644 public/icon-512x512.png delete mode 100644 public/logo3d.png diff --git a/agents.md b/agents.md index da9a7bd..1295ecc 100644 --- a/agents.md +++ b/agents.md @@ -40,3 +40,97 @@ - When you identify reusable effect logic, wrap it in a custom hook under `hooks/` (e.g., `use-window-click-dismiss.ts`) and consume that hook rather than repeating `useEffect` blocks. + + +--- + +## Prompt for Codex Agent + +You are the **DevContext Assistant**, helping developers generate AI-ready instruction files (`copilot-instructions.md`, `agents.md`, `cursor-rules.md`) for their projects. + +Your role is to **analyze input (manual answers or GitHub repo scans)**, infer project conventions, and generate high-quality context/config files tailored to the project. + +--- + +### Core Capabilities + +1. **Modes of Use** + - **New Project (Wizard)** + - Ask the user step-by-step questions (IDE, framework, language, tooling, file structure, naming conventions, testing approach, etc.). + - Allow skipping questions and using defaults to generate a boilerplate instructions file. + - **Existing Project (GitHub Repo)** + - Accept a GitHub repo URL or `owner/repo`. + - Fetch metadata (via GitHub API or local scan): languages, frameworks, configs, testing tools, structure. + - Pre-fill wizard answers based on detected information. + - Ask only about missing/ambiguous details. + - Provide smart suggestions (e.g., “Detected ESLint but no Prettier — do you want me to add Prettier rules?”). + +2. **Repo Scanning Rules** + - Detect frameworks/languages: + - `package.json` → React, Next.js, Angular, etc. + - `requirements.txt`, `pyproject.toml` → Django, FastAPI, Flask, etc. + - `pom.xml`, `build.gradle` → Java/Spring. + - Detect tooling/config: + - ESLint, Prettier, Babel, Webpack, Vite, Dockerfile, TSConfig. + - Detect testing: + - Jest, Vitest, Cypress, Playwright, Mocha. + - Detect structure: + - Presence of `src/`, `tests/`, `components/`, etc. + - Summarize findings in a clear JSON object. + +3. **Output** + - Generate one or more instruction/config files depending on user choice: + - `copilot-instructions.md` + - `agents.md` + - `cursor-rules.md` + - File must include: + - Environment (IDE, framework, language, tooling). + - Project priorities. + - Code style (naming, structure, comments, testing). + - AI-related guidelines (how Copilot, Cursor, or agents should behave). + - Provide options: + - Preview file in UI. + - Copy to clipboard. + - Download file. + +--- + +### UX Rules + +- Always make it clear what value you bring: + - For wizard: *“Guided setup for AI coding guidelines.”* + - For repo scan: *“Auto-detect stack and generate context-aware instructions.”* +- Use smart defaults, but let user override. +- Keep the flow simple: *Landing → Choose (New or Existing Project) → Wizard (manual or prefilled) → Generate File.* +- At the end, provide both the file output and short explanation: *“This file was generated based on your repo + your preferences.”* + +--- + +### SEO / Positioning Notes + +- Emphasize you are not just a “file generator” but a **repo-aware, AI coding guidelines assistant**. +- Highlight keywords: *AI coding guidelines, Copilot instructions, Cursor rules, GitHub repo analyzer, IDE setup automation, developer onboarding docs.* + +--- + +### Example Workflow (Existing Project) + +1. User inputs repo URL → agent scans repo. +2. Agent outputs summary JSON: + ```json + { + "language": "TypeScript", + "frameworks": ["Next.js", "React"], + "tooling": ["ESLint", "Tailwind"], + "testing": [], + "structure": { "src": true, "tests": true } + } + ``` +3. Agent asks: + *“We detected Next.js + React + ESLint. Do you want to add Prettier rules? Do you plan to add Jest or another testing framework?”* +4. Agent generates `copilot-instructions.md` with these conventions baked in. +5. User downloads or copies the file. + +--- + +👉 Use this as your guiding instruction: **always detect what you can, ask only when needed, and generate a repo-aware instructions file that saves the user time.** diff --git a/app/layout.tsx b/app/layout.tsx index afe22c6..2795b10 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -11,16 +11,15 @@ const geistSans = Geist({ subsets: ["latin"], }); - const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"], }); -const siteUrl = "https://devcontext.com"; -const siteTitle = "DevContext – AI Coding Guidelines & Context Generator"; +const siteUrl = "https://devcontext.xyz"; +const siteTitle = "DevContext – AI Coding Guidelines & Repo Analyzer"; const siteDescription = - "DevContext helps developers generate AI config files like Copilot instructions, Cursor rules, and prompts — consistent, fast, IDE-ready."; + "DevContext helps developers generate AI config files like Copilot instructions, Cursor rules, and agents.md. Start fresh with a guided wizard or analyze your GitHub repo to auto-detect stack, frameworks, and best practices — consistent, fast, and IDE-ready."; const ogImage = `${siteUrl}/og-image.png`; @@ -46,14 +45,22 @@ const structuredData = { featureList: [ "Guided wizard for AI coding instructions", "Prebuilt templates for Copilot, Cursor, and IDE agents", - "Context-aware questions with best-practice examples", + "Scan a GitHub repo to auto-detect stack, frameworks, and tooling", + "Context-aware questions with best-practice suggestions", + "Instant boilerplate generation with smart defaults", ], keywords: [ + "AI coding guidelines", + "Copilot instructions generator", + "Cursor rules builder", + "IDE setup automation", + "Developer onboarding docs", "generate Copilot instructions file", "generate agents file", - "generate AI instructions", "generate Cursor rules", - "AI development workflows", + "AI repo analyzer", + "GitHub repo scanner for Copilot", + "repo-aware coding guidelines", ], }; @@ -79,6 +86,10 @@ export const metadata: Metadata = { "Copilot instructions", "Cursor rules", "agents md", + "GitHub repo analyzer", + "generate coding standards from GitHub repo", + "repo-aware AI coding guidelines", + "auto-detect framework coding rules", ], authors: [{ name: "DevContext" }], creator: "DevContext", @@ -89,8 +100,7 @@ export const metadata: Metadata = { }, openGraph: { title: siteTitle, - description: - "Generate AI config files like Copilot instructions, Cursor rules, and prompts. Consistent, fast, and IDE-ready.", + description: siteDescription, url: siteUrl, siteName: "DevContext", images: [ @@ -107,8 +117,7 @@ export const metadata: Metadata = { twitter: { card: "summary_large_image", title: siteTitle, - description: - "Generate AI config files like Copilot instructions, Cursor rules, and prompts. Consistent, fast, and IDE-ready.", + description: siteDescription, images: [ogImage], site: "@devcontext", }, @@ -141,20 +150,21 @@ export const metadata: Metadata = { }, }; - - export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - return ( - }> - - - ) -} - -function NewInstructionsPageContent() { - const searchParams = useSearchParams() - const [showWizard, setShowWizard] = useState(false) - const [selectedFileId, setSelectedFileId] = useState(null) - - const fileOptions = useMemo(() => fileOptionsFromData, []) - const preferredStackId = searchParams.get("stack")?.toLowerCase() ?? null - - const handleFileCtaClick = (file: FileOutputConfig) => { - setSelectedFileId(file.id) - setShowWizard(true) - track(ANALYTICS_EVENTS.CREATE_INSTRUCTIONS_FILE, { - fileId: file.id, - fileLabel: file.label, - }) - } - - const handleWizardClose = () => { - setShowWizard(false) - setSelectedFileId(null) - } - - return ( -
- -
- {/* Top utility bar */} -
- {!showWizard ? ( - <> - - DevContext - - - - - - ) : ( - - - - )} -
- - {/* Hero Section */} -
- {showWizard && selectedFileId ? ( - - ) : ( - <> -
-
-

Start a new instructions project

-

- Choose the file preset that matches what you need. The wizard will open with targeted questions and save progress as you go. -

-
-
    -
  • Pick a preset to load stack, architecture, and workflow prompts.
  • -
  • Answer or skip questions — you can revisit any step before exporting.
  • -
  • Download the generated file once every section shows as complete.
  • -
-
-
- - {/* File type CTAs */} -
-
- {fileOptions.map((file) => { - return ( - - ) - })} -
-
-
- - )} -
-
-
- ) +import type { Metadata } from "next" +import { redirect } from "next/navigation" + +const title = "Launch the DevContext Wizard" +const description = + "Start a guided flow to assemble AI-ready coding instruction files. Pick your stack, customize conventions, and export Copilot, Cursor, or agents guidelines in minutes." + +export const metadata: Metadata = { + title, + description, + alternates: { + canonical: "/new", + }, + openGraph: { + title, + description, + url: "/new", + type: "website", + siteName: "DevContext", + images: [ + { + url: "/og-image.png", + width: 1200, + height: 630, + alt: "DevContext wizard interface preview", + }, + ], + }, + twitter: { + card: "summary_large_image", + title, + description, + images: ["/og-image.png"], + }, } -function LoadingFallback() { - return ( -
- Loading wizard… -
- ) +export default function NewPage() { + redirect(`/new/stack`) } diff --git a/app/new/stack/[[...stackSegments]]/page.tsx b/app/new/stack/[[...stackSegments]]/page.tsx new file mode 100644 index 0000000..8b55e90 --- /dev/null +++ b/app/new/stack/[[...stackSegments]]/page.tsx @@ -0,0 +1,153 @@ +import Link from "next/link" +import { notFound } from "next/navigation" +import type { Metadata } from "next" + +import stacksData from "@/data/stacks.json" +import type { DataQuestionSource } from "@/types/wizard" +import { AnimatedBackground } from "@/components/AnimatedBackground" +import { Button } from "@/components/ui/button" +import { Github } from "lucide-react" +import { getHomeMainClasses } from "@/lib/utils" +import { StackWizardClient } from "../stack-wizard-client" +import { StackSummaryPage } from "../stack-summary-page" + +const stackQuestion = (stacksData as DataQuestionSource[])[0] +const stackAnswers = stackQuestion?.answers ?? [] + +type PageParams = { + stackSegments?: string[] +} + +type MetadataProps = { + params: Promise +} + +type PageProps = { + params: PageParams +} + +export async function generateMetadata({ params }: MetadataProps): Promise { + const resolvedParams = await params + const { stackSegments } = resolvedParams + const segments = Array.isArray(stackSegments) ? stackSegments : [] + const stackId = segments.length > 0 ? segments[0] : null + let mode: "default" | "user" | null = null + + if (segments.length > 1) { + if (segments.length === 2 && segments[1] === "summary") { + mode = "default" + } else if ( + segments.length === 3 && + segments[2] === "summary" && + (segments[1] === "default" || segments[1] === "user") + ) { + mode = segments[1] as "default" | "user" + } + } + + const stackLabel = stackAnswers.find((answer) => answer.value === stackId)?.label + + const title = stackLabel + ? `${stackLabel} · DevContext Wizard` + : "DevContext Wizard" + let description = "Choose your stack and build AI-ready coding instructions." + + if (stackLabel) { + if (mode === "user") { + description = `Review your saved answers for ${stackLabel} and generate tailored context files.` + } else { + description = `Review recommended defaults for ${stackLabel} and share the summary instantly.` + } + } + + const canonicalPath = `/new/stack${segments.length > 0 ? `/${segments.join("/")}` : ""}` + const ogImage = "/og-image.png" + const imageAlt = stackLabel ? `${stackLabel} DevContext wizard preview` : "DevContext wizard interface preview" + + return { + title, + description, + alternates: { + canonical: canonicalPath, + }, + openGraph: { + title, + description, + url: canonicalPath, + type: "website", + siteName: "DevContext", + images: [ + { + url: ogImage, + width: 1200, + height: 630, + alt: imageAlt, + }, + ], + }, + twitter: { + card: "summary_large_image", + title, + description, + images: [ogImage], + }, + } +} + +export default function StackRoutePage({ params }: PageProps) { + const { stackSegments } = params + let stackIdFromRoute: string | null = null + let summaryMode: "default" | "user" | null = null + + if (Array.isArray(stackSegments) && stackSegments.length > 0) { + const potentialStackId = stackSegments[0] + const stackMatch = stackAnswers.find((answer) => answer.value === potentialStackId) + + if (!stackMatch) { + notFound() + } + + stackIdFromRoute = potentialStackId + + if (stackSegments.length > 1) { + if (stackSegments.length === 2 && stackSegments[1] === "summary") { + summaryMode = "default" + } else if ( + stackSegments.length === 3 && + stackSegments[2] === "summary" && + (stackSegments[1] === "default" || stackSegments[1] === "user") + ) { + summaryMode = stackSegments[1] as "default" | "user" + } else { + notFound() + } + } + } + + return ( +
+ +
+
+ + DevContext + + + + +
+ +
+ {summaryMode ? ( + + ) : ( + + )} +
+
+
+ ) +} diff --git a/app/new/stack/stack-summary-page.tsx b/app/new/stack/stack-summary-page.tsx new file mode 100644 index 0000000..803c63f --- /dev/null +++ b/app/new/stack/stack-summary-page.tsx @@ -0,0 +1,401 @@ +"use client" + +import { useCallback, useEffect, useMemo, useState } from "react" +import Link from "next/link" + +import { Button } from "@/components/ui/button" +import FinalOutputView from "@/components/final-output-view" +import { generateInstructions } from "@/lib/instructions-api" +import { buildCompletionSummary } from "@/lib/wizard-summary" +import { serializeWizardResponses } from "@/lib/wizard-response" +import { + STACK_QUESTION_ID, + stackQuestion, + getFileOptions, + getFileSummaryQuestion, +} from "@/lib/wizard-config" +import { loadWizardState, persistWizardState } from "@/lib/wizard-storage" +import { buildDefaultSummaryData, buildStepsForStack } from "@/lib/wizard-summary-data" +import type { FileOutputConfig, Responses, WizardQuestion, WizardAnswer, WizardStep } from "@/types/wizard" +import type { GeneratedFileResult } from "@/types/output" +import { WizardEditAnswerDialog } from "@/components/wizard-edit-answer-dialog" + +const fileOptions = getFileOptions() +const fileSummaryQuestion = getFileSummaryQuestion() +type StackSummaryPageProps = { + stackId: string | null + mode: "default" | "user" +} + +export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { + const [wizardSteps, setWizardSteps] = useState(null) + const [responses, setResponses] = useState(null) + const [autoFilledMap, setAutoFilledMap] = useState>({}) + const [stackLabel, setStackLabel] = useState(null) + const [autoFillNotice, setAutoFillNotice] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [errorMessage, setErrorMessage] = useState(null) + const [generatedFile, setGeneratedFile] = useState(null) + const [isGeneratingMap, setIsGeneratingMap] = useState>({}) + const [editingQuestionId, setEditingQuestionId] = useState(null) + + useEffect(() => { + if (!stackId) { + setErrorMessage("Select a stack to review your summary.") + setIsLoading(false) + return + } + + let isActive = true + + const loadSummaryData = async () => { + setIsLoading(true) + setErrorMessage(null) + try { + if (mode === "default") { + const { steps, responses: defaultResponses, autoFilledMap: defaultsMap, stackLabel: label } = + await buildDefaultSummaryData(stackId) + + if (!isActive) { + return + } + + setWizardSteps(steps) + setResponses(defaultResponses) + setAutoFilledMap(defaultsMap) + setStackLabel(label) + setAutoFillNotice("We applied the recommended defaults for you. Tweak any section before generating.") + + persistWizardState({ + stackId, + stackLabel: label, + responses: defaultResponses, + autoFilledMap: defaultsMap, + updatedAt: Date.now(), + }) + } else { + const { steps, stackLabel: computedLabel } = await buildStepsForStack(stackId) + + if (!isActive) { + return + } + + const storedState = loadWizardState(stackId) + + if (!storedState) { + setWizardSteps(steps) + setResponses(null) + setAutoFilledMap({}) + setStackLabel(computedLabel) + setAutoFillNotice(null) + setErrorMessage("We couldn't find saved answers for this stack. Complete the wizard to generate your own summary.") + return + } + + const normalizedResponses: Responses = { + ...storedState.responses, + [STACK_QUESTION_ID]: stackId, + } + + setWizardSteps(steps) + setResponses(normalizedResponses) + setAutoFilledMap(storedState.autoFilledMap ?? {}) + setStackLabel(storedState.stackLabel ?? computedLabel) + setAutoFillNotice(null) + } + } catch (error) { + console.error(`Unable to prepare summary for stack "${stackId}"`, error) + if (isActive) { + setErrorMessage("We couldn't load the summary for this stack. Try selecting it again from the wizard.") + } + } finally { + if (isActive) { + setIsLoading(false) + } + } + } + + void loadSummaryData() + + return () => { + isActive = false + } + }, [stackId, mode]) + + const summaryEntries = useMemo(() => { + if (!wizardSteps || !responses) { + return [] + } + + return buildCompletionSummary( + fileSummaryQuestion, + null, + null, + wizardSteps, + responses, + autoFilledMap, + false + ) + }, [wizardSteps, responses, autoFilledMap]) + + const handleGenerate = useCallback( + async (fileOption: FileOutputConfig) => { + if (!wizardSteps || !responses || !stackId) { + return + } + + setIsGeneratingMap((prev) => ({ ...prev, [fileOption.id]: true })) + setGeneratedFile(null) + + try { + const payload = serializeWizardResponses(wizardSteps, responses, fileOption.id) + + const result = await generateInstructions({ + stackSegment: stackId, + outputFileId: fileOption.id, + responses: payload, + fileFormat: fileOption.format, + }) + + if (result) { + setGeneratedFile(result) + } + } catch (error) { + console.error("Error generating instructions", error) + setErrorMessage("We hit a snag generating that file. Please try again.") + } finally { + setIsGeneratingMap((prev) => ({ ...prev, [fileOption.id]: false })) + } + }, + [wizardSteps, responses, stackId] + ) + + const summaryHeader = stackLabel ?? + (stackQuestion?.answers.find((answer) => answer.value === stackId)?.label ?? stackId ?? "Your stack") + + const questionLookup = useMemo(() => { + const map: Record = {} + wizardSteps?.forEach((step) => { + step.questions.forEach((question) => { + map[question.id] = question + }) + }) + return map + }, [wizardSteps]) + + const handleEditClick = (questionId: string) => { + setEditingQuestionId(questionId) + } + + const handleCloseEdit = () => { + setEditingQuestionId(null) + } + + const applyAnswerUpdate = useCallback( + (question: WizardQuestion, answer: WizardAnswer) => { + const currentResponses: Responses = responses ? { ...responses } : {} + const currentAutoMap = { ...autoFilledMap } + const prevValue = currentResponses[question.id] + let nextValue: Responses[keyof Responses] | undefined + + if (question.allowMultiple) { + const prevArray = Array.isArray(prevValue) ? prevValue : [] + if (prevArray.includes(answer.value)) { + const filtered = prevArray.filter((item) => item !== answer.value) + nextValue = filtered.length > 0 ? filtered : undefined + } else { + nextValue = [...prevArray, answer.value] + } + } else { + nextValue = prevValue === answer.value ? undefined : answer.value + } + + if (nextValue === undefined || (Array.isArray(nextValue) && nextValue.length === 0)) { + delete currentResponses[question.id] + } else { + currentResponses[question.id] = nextValue + } + + delete currentAutoMap[question.id] + setResponses(currentResponses) + setAutoFilledMap(currentAutoMap) + setAutoFillNotice(null) + + if (stackId) { + persistWizardState({ + stackId, + stackLabel: stackLabel ?? summaryHeader, + responses: currentResponses, + autoFilledMap: currentAutoMap, + updatedAt: Date.now(), + }) + } + + if (!question.allowMultiple) { + handleCloseEdit() + } + }, + [responses, autoFilledMap, stackId, stackLabel, summaryHeader] + ) + + if (isLoading) { + return ( +
+ Preparing your summary… +
+ ) + } + + if (errorMessage) { + return ( +
+

{errorMessage}

+ +
+ ) + } + + if (!responses || !wizardSteps || !stackId) { + return null + } + + return ( +
+
+
+
+

Generate context files

+

+ Pick the output format you need. Each option uses the summary on this page. +

+
+
+ {fileOptions.map((file) => { + const isGenerating = Boolean(isGeneratingMap[file.id]) + return ( + + ) + })} +
+
+
+ +
+
+
+

{summaryHeader} instructions overview

+

+ Share this page to sync on conventions, or jump back into the wizard to fine-tune answers. +

+
+
+ +
+
+ {autoFillNotice ? ( +
+ {autoFillNotice} +
+ ) : null} +
+ +
+
+

Selections in this summary

+

+ Reopen any question from the wizard to change these selections. +

+
+
+ {summaryEntries.map((entry) => ( +
+
+

{entry.question}

+
+ {entry.hasSelection ? ( +
    + {entry.answers.map((answer) => ( +
  • +
    + {answer} + {!entry.isReadOnlyOnSummary && stackId ? ( + + ) : null} +
    +
  • + ))} +
+ ) : ( +
+ No selection + {!entry.isReadOnlyOnSummary && stackId ? ( + + ) : null} +
+ )} +
+ ))} +
+
+ + {editingQuestionId ? ( + (() => { + const editingQuestion = questionLookup[editingQuestionId] + if (!editingQuestion) { + return null + } + const currentValue = responses ? responses[editingQuestion.id] : undefined + return ( + applyAnswerUpdate(editingQuestion, answer)} + onClose={handleCloseEdit} + /> + ) + })() + ) : null} + + {generatedFile ? ( + setGeneratedFile(null)} + /> + ) : null} +
+ ) +} diff --git a/app/new/stack/stack-wizard-client.tsx b/app/new/stack/stack-wizard-client.tsx new file mode 100644 index 0000000..995e085 --- /dev/null +++ b/app/new/stack/stack-wizard-client.tsx @@ -0,0 +1,59 @@ +"use client" + +import { useRouter } from "next/navigation" + +import { InstructionsWizard } from "@/components/instructions-wizard" + +const buildStackPath = (stackId?: string | null, view?: "summary" | "default" | "user") => { + if (!stackId) { + return "/new/stack" + } + + if (view === "summary") { + return `/new/stack/${stackId}/summary` + } + + if (view === "default") { + return `/new/stack/${stackId}/default/summary` + } + + if (view === "user") { + return `/new/stack/${stackId}/user/summary` + } + + return `/new/stack/${stackId}` +} + +type StackWizardClientProps = { + stackIdFromRoute: string | null +} + +export function StackWizardClient({ stackIdFromRoute }: StackWizardClientProps) { + const router = useRouter() + const initialStackId = stackIdFromRoute + + const handleStackSelected = (stackId: string) => { + router.push(buildStackPath(stackId)) + } + + const handleStackCleared = () => { + router.push(buildStackPath()) + } + + const handleWizardComplete = (stackId: string | null) => { + if (!stackId) { + return + } + + router.push(buildStackPath(stackId, "user")) + } + + return ( + + ) +} diff --git a/app/page.tsx b/app/page.tsx index b467336..5f43227 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,7 +10,10 @@ export default function LandingPage() {
-
+
+ + DevContext +
- +
+
+

+ Start fast with popular stacks +

+ +
+
+ {popularStacks.map((stack) => { + const descriptor = getIconDescriptor(stack.icon ?? stack.value) + const iconHex = descriptor ? iconColorOverrides[descriptor.slug] ?? descriptor.hex : undefined + const iconColor = iconHex ? getAccessibleIconColor(iconHex) : undefined + const iconBackground = iconColor ? hexToRgba(iconColor, 0.18) ?? undefined : undefined + const iconRing = iconColor ? hexToRgba(iconColor, 0.32) ?? undefined : undefined + const initials = getFallbackInitials(stack.label) + + return ( + + ) + })} +
+
+ +
+
+ + or + +
+
+ +
+
+

+ Scan a GitHub repository +

+

+ Paste an owner/repo or URL and we'll prefill the wizard with detected tech and tooling. +

+
+
+ setGithubRepo(event.target.value)} + placeholder="github.com/owner/repo" + className="w-full rounded-xl border border-border/70 bg-background px-4 py-2 text-sm text-foreground shadow-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60 sm:min-w-[260px]" + /> + +
+
diff --git a/components/Logo/logo-dark.svg b/components/Logo/logo-dark.svg deleted file mode 100644 index 18d46ca..0000000 --- a/components/Logo/logo-dark.svg +++ /dev/null @@ -1,136 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/components/Logo/logo.svg b/components/Logo/logo.svg deleted file mode 100644 index 3b3c348..0000000 --- a/components/Logo/logo.svg +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/components/Logo/logoText.svg b/components/Logo/logoText.svg deleted file mode 100644 index 33e8d56..0000000 --- a/components/Logo/logoText.svg +++ /dev/null @@ -1,1579 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/components/instructions-wizard.tsx b/components/instructions-wizard.tsx index e017d1e..19caecf 100644 --- a/components/instructions-wizard.tsx +++ b/components/instructions-wizard.tsx @@ -1,157 +1,45 @@ "use client" import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import Link from "next/link" + import { Button } from "@/components/ui/button" import { Undo2 } from "lucide-react" -import type { DataQuestionSource, InstructionsWizardProps, Responses, WizardAnswer, WizardConfirmationIntent, WizardQuestion, WizardStep } from "@/types/wizard" +import type { InstructionsWizardProps, Responses, WizardAnswer, WizardQuestion, WizardStep, WizardConfirmationIntent } from "@/types/wizard" import { buildFilterPlaceholder, useAnswerFilter } from "@/hooks/use-answer-filter" -import stacksData from "@/data/stacks.json" -import generalData from "@/data/general.json" -import architectureData from "@/data/architecture.json" -import performanceData from "@/data/performance.json" -import securityData from "@/data/security.json" -import commitsData from "@/data/commits.json" -import filesData from "@/data/files.json" -import FinalOutputView from "./final-output-view" +import { + STACK_QUESTION_ID, + STACK_STEP_ID, + loadStackWizardStep, + stackQuestion, + stacksStep, + getSuffixSteps, +} from "@/lib/wizard-config" +import { persistWizardState, clearWizardState } from "@/lib/wizard-storage" import { WizardAnswerGrid } from "./wizard-answer-grid" -import { WizardCompletionSummary } from "./wizard-completion-summary" import { WizardConfirmationDialog } from "./wizard-confirmation-dialog" -import { WizardEditAnswerDialog } from "./wizard-edit-answer-dialog" -import { generateInstructions } from "@/lib/instructions-api" -import { buildCompletionSummary } from "@/lib/wizard-summary" -import { serializeWizardResponses } from "@/lib/wizard-response" -import { buildFileOptionsFromQuestion, buildStepFromQuestionSet, getFormatLabel, mapAnswerSourceToWizard } from "@/lib/wizard-utils" -import type { GeneratedFileResult } from "@/types/output" +const suffixSteps = getSuffixSteps() -const STACK_STEP_ID = "stacks" -const STACK_QUESTION_ID = "stackSelection" -const DEVCONTEXT_ROOT_URL = "https://devcontext.xyz/" - -const stackQuestionSet = stacksData as DataQuestionSource[] -const stacksStep = buildStepFromQuestionSet( - STACK_STEP_ID, - "Choose Your Stack", - stackQuestionSet -) -const stackQuestion = stacksStep.questions.find((question) => question.id === STACK_QUESTION_ID) ?? null - -const fileQuestionSet = filesData as DataQuestionSource[] -const fileQuestion = fileQuestionSet[0] ?? null -const fileOptions = buildFileOptionsFromQuestion(fileQuestion) -const defaultFileOption = - fileOptions.find((file) => file.isDefault) ?? - fileOptions[0] ?? - null -const fileSummaryQuestion = fileQuestion - ? { - id: fileQuestion.id, - question: fileQuestion.question, - isReadOnlyOnSummary: fileQuestion.isReadOnlyOnSummary, - } - : null - -const generalStep = buildStepFromQuestionSet( - "general", - "Project Foundations", - generalData as DataQuestionSource[] -) - -const architectureStep = buildStepFromQuestionSet( - "architecture", - "Architecture Practices", - architectureData as DataQuestionSource[] -) - -const performanceStep = buildStepFromQuestionSet( - "performance", - "Performance Guidelines", - performanceData as DataQuestionSource[] -) - -const securityStep = buildStepFromQuestionSet( - "security", - "Security & Compliance", - securityData as DataQuestionSource[] -) - -const commitsStep = buildStepFromQuestionSet( - "commits", - "Collaboration & Version Control", - commitsData as DataQuestionSource[] -) - -const suffixSteps: WizardStep[] = [ - generalStep, - architectureStep, - performanceStep, - securityStep, - commitsStep, -] - -export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: InstructionsWizardProps) { +export function InstructionsWizard({ + initialStackId, + onStackSelected, + onStackCleared, + autoStartAfterStackSelection = true, + onComplete, +}: InstructionsWizardProps) { const [currentStepIndex, setCurrentStepIndex] = useState(0) const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0) const [responses, setResponses] = useState({}) const [dynamicSteps, setDynamicSteps] = useState([]) - const [isComplete, setIsComplete] = useState(false) - const [pendingConfirmation, setPendingConfirmation] = useState(null) - const [generatedFile, setGeneratedFile] = useState(null) - const [isGenerating, setIsGenerating] = useState(false) const [isStackFastTrackPromptVisible, setIsStackFastTrackPromptVisible] = useState(false) + const [pendingConfirmation, setPendingConfirmation] = useState(null) const [autoFilledQuestionMap, setAutoFilledQuestionMap] = useState>({}) - const [autoFillNotice, setAutoFillNotice] = useState(null) - const [activeEditQuestionId, setActiveEditQuestionId] = useState(null) const hasAppliedInitialStack = useRef(null) + const [activeStackLabel, setActiveStackLabel] = useState(null) - useEffect(() => { - if (!isComplete && activeEditQuestionId) { - setActiveEditQuestionId(null) - } - }, [isComplete, activeEditQuestionId]) - - const selectedFile = useMemo(() => { - if (selectedFileId) { - const matchedFile = fileOptions.find((file) => file.id === selectedFileId) - if (matchedFile) { - return matchedFile - } - } - - return defaultFileOption - }, [selectedFileId]) - - const selectedFileFormatLabel = useMemo( - () => (selectedFile ? getFormatLabel(selectedFile.format) : null), - [selectedFile] - ) - - const wizardSteps = useMemo( - () => [stacksStep, ...dynamicSteps, ...suffixSteps], - [dynamicSteps] - ) - - const nonStackSteps = useMemo( - () => wizardSteps.filter((step) => step.id !== STACK_STEP_ID), - [wizardSteps] - ) - - const wizardQuestionsById = useMemo(() => { - const lookup: Record = {} - - wizardSteps.forEach((step) => { - step.questions.forEach((question) => { - lookup[question.id] = question - }) - }) - - return lookup - }, [wizardSteps]) - - const editingQuestion = activeEditQuestionId ? wizardQuestionsById[activeEditQuestionId] ?? null : null - const editingAnswerValue = editingQuestion ? responses[editingQuestion.id] : undefined - + const wizardSteps = useMemo(() => [stacksStep, ...dynamicSteps, ...suffixSteps], [dynamicSteps]) const currentStep = wizardSteps[currentStepIndex] ?? null const currentQuestion = currentStep?.questions[currentQuestionIndex] ?? null @@ -161,6 +49,7 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: setQuery: setAnswerFilterQuery, isFiltering: isFilteringAnswers, } = useAnswerFilter(currentQuestion ?? null) + const filterPlaceholder = buildFilterPlaceholder(currentQuestion ?? null) const showNoFilterMatches = Boolean( currentQuestion?.enableFilter && @@ -169,29 +58,6 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: ) const filterInputId = currentQuestion ? `answer-filter-${currentQuestion.id}` : "answer-filter" - const totalQuestions = useMemo( - () => wizardSteps.reduce((count, step) => count + step.questions.length, 0), - [wizardSteps] - ) - - const remainingQuestionCount = useMemo( - () => nonStackSteps.reduce((count, step) => count + step.questions.length, 0), - [nonStackSteps] - ) - - const completionSummary = useMemo( - () => - buildCompletionSummary( - fileSummaryQuestion, - selectedFile ?? null, - selectedFileFormatLabel, - wizardSteps, - responses, - autoFilledQuestionMap - ), - [selectedFile, selectedFileFormatLabel, wizardSteps, responses, autoFilledQuestionMap] - ) - const currentAnswerValue = currentQuestion ? responses[currentQuestion.id] : undefined const defaultAnswer = useMemo( @@ -212,7 +78,6 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: }, [currentAnswerValue, currentQuestion, defaultAnswer]) const canUseDefault = Boolean( - !isComplete && currentQuestion && defaultAnswer && !defaultAnswer.disabled && @@ -223,8 +88,10 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: ? `Use default (${defaultAnswer.label})` : "Use default" - const showStackPivot = !isComplete && isStackFastTrackPromptVisible - const showQuestionControls = !isComplete && !isStackFastTrackPromptVisible + const selectedStackId = useMemo(() => { + const value = responses[STACK_QUESTION_ID] + return typeof value === "string" && value.length > 0 ? value : null + }, [responses]) const markQuestionsAutoFilled = useCallback((questionIds: string[]) => { if (questionIds.length === 0) { @@ -254,17 +121,97 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: }) }, []) - const closeEditDialog = useCallback(() => { - setActiveEditQuestionId(null) - }, []) + const persistStateIfPossible = useCallback( + (nextResponses: Responses, nextAutoFilled: Record, stackId: string | null, stackLabel?: string | null) => { + if (!stackId) { + return + } - const isAnswerSelected = (value: string) => { - if (currentQuestion?.allowMultiple) { - return Array.isArray(currentAnswerValue) && currentAnswerValue.includes(value) + persistWizardState({ + stackId, + stackLabel: stackLabel ?? undefined, + responses: nextResponses, + autoFilledMap: nextAutoFilled, + updatedAt: Date.now(), + }) + }, + [] + ) + + const loadStackQuestions = useCallback( + async ( + stackId: string, + stackLabelFromAnswer?: string, + options?: { skipFastTrackPrompt?: boolean } + ) => { + try { + const { step, label } = await loadStackWizardStep(stackId, stackLabelFromAnswer) + setActiveStackLabel(label) + + setDynamicSteps([step]) + setIsStackFastTrackPromptVisible(!options?.skipFastTrackPrompt && step.questions.length > 0) + + setResponses((prev) => { + const next: Responses = { ...prev } + step.questions.forEach((question) => { + delete next[question.id] + }) + return next + }) + setCurrentStepIndex(1) + setCurrentQuestionIndex(0) + setAutoFilledQuestionMap({}) + } catch (error) { + console.error(`Unable to load questions for stack "${stackId}"`, error) + setDynamicSteps([]) + setIsStackFastTrackPromptVisible(false) + } + }, + [] + ) + + useEffect(() => { + if (!initialStackId) { + return } - return currentAnswerValue === value - } + if (hasAppliedInitialStack.current === initialStackId) { + return + } + + const stackAnswer = stackQuestion?.answers.find((answer) => answer.value === initialStackId) + + if (!stackAnswer) { + return + } + + hasAppliedInitialStack.current = initialStackId + + setResponses((prev) => ({ + ...prev, + [STACK_QUESTION_ID]: stackAnswer.value, + })) + + void loadStackQuestions(stackAnswer.value, stackAnswer.label, { + skipFastTrackPrompt: autoStartAfterStackSelection, + }) + }, [initialStackId, loadStackQuestions, autoStartAfterStackSelection]) + + useEffect(() => { + if (!selectedStackId) { + return + } + + persistStateIfPossible(responses, autoFilledQuestionMap, selectedStackId, activeStackLabel) + }, [responses, autoFilledQuestionMap, selectedStackId, activeStackLabel, persistStateIfPossible]) + + const handleWizardCompletion = useCallback(() => { + if (!selectedStackId) { + return + } + + onComplete?.(selectedStackId) + }, [selectedStackId, onComplete]) const advanceToNextQuestion = () => { const currentStepForAdvance = wizardSteps[currentStepIndex] @@ -273,7 +220,7 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: const isLastStep = currentStepIndex === wizardSteps.length - 1 if (isLastQuestionInStep && isLastStep) { - setIsComplete(true) + handleWizardCompletion() return } @@ -303,99 +250,13 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: const previousStep = wizardSteps[previousStepIndex] setCurrentStepIndex(previousStepIndex) setCurrentQuestionIndex(previousStep.questions.length - 1) - setIsComplete(false) return } setCurrentQuestionIndex((prev) => Math.max(prev - 1, 0)) - setIsComplete(false) } - const loadStackQuestions = useCallback(async (stackId: string, stackLabel?: string) => { - try { - setGeneratedFile(null) - - const questionsModule = await import(`@/data/questions/${stackId}.json`) - const questionsData = (questionsModule.default ?? questionsModule) as DataQuestionSource[] - - const mappedQuestions: WizardQuestion[] = questionsData.map((question) => ({ - id: question.id, - question: question.question, - allowMultiple: question.allowMultiple, - responseKey: question.responseKey, - answers: question.answers.map(mapAnswerSourceToWizard), - })) - - const followUpQuestionCount = - mappedQuestions.length + suffixSteps.reduce((count, step) => count + step.questions.length, 0) - - setAutoFilledQuestionMap({}) - setAutoFillNotice(null) - - const resolvedStackLabel = - stackLabel ?? stackQuestion?.answers.find((answer) => answer.value === stackId)?.label ?? stackId - - setDynamicSteps([ - { - id: `stack-${stackId}`, - title: `${resolvedStackLabel} Preferences`, - questions: mappedQuestions, - }, - ]) - - setIsStackFastTrackPromptVisible(followUpQuestionCount > 0) - - setResponses((prev) => { - const next = { ...prev } - mappedQuestions.forEach((question) => { - delete next[question.id] - }) - return next - }) - - setCurrentStepIndex(1) - setCurrentQuestionIndex(0) - setIsComplete(false) - } catch (error) { - console.error(`Unable to load questions for stack "${stackId}"`, error) - setDynamicSteps([]) - setIsStackFastTrackPromptVisible(false) - } - }, []) - - useEffect(() => { - if (!initialStackId) { - return - } - - if (hasAppliedInitialStack.current === initialStackId) { - return - } - - const stackAnswer = stackQuestion?.answers.find((answer) => answer.value === initialStackId) - - if (!stackAnswer) { - return - } - - hasAppliedInitialStack.current = initialStackId - - setResponses((prev) => ({ - ...prev, - [STACK_QUESTION_ID]: stackAnswer.value, - })) - - void loadStackQuestions(stackAnswer.value, stackAnswer.label) - }, [initialStackId, loadStackQuestions]) - - if (!currentStep || !currentQuestion) { - return null - } - - const applyDefaultsAcrossWizard = () => { - setGeneratedFile(null) - setAutoFilledQuestionMap({}) - + const applyDefaultsAcrossWizard = useCallback(() => { const autoFilledIds: string[] = [] setResponses((prev) => { @@ -425,23 +286,9 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: }) markQuestionsAutoFilled(autoFilledIds) - - if (autoFilledIds.length > 0) { - setAutoFillNotice("We applied the recommended defaults for you. Tweak any section before generating.") - } else { - setAutoFillNotice(null) - } - setIsStackFastTrackPromptVisible(false) - - const lastStepIndex = Math.max(wizardSteps.length - 1, 0) - const lastStep = wizardSteps[lastStepIndex] - const lastQuestionIndex = lastStep ? Math.max(lastStep.questions.length - 1, 0) : 0 - - setCurrentStepIndex(lastStepIndex) - setCurrentQuestionIndex(lastQuestionIndex) - setIsComplete(true) - } + handleWizardCompletion() + }, [markQuestionsAutoFilled, wizardSteps, handleWizardCompletion]) const beginStepByStepFlow = () => { const firstNonStackIndex = wizardSteps.findIndex((step) => step.id !== STACK_STEP_ID) @@ -452,22 +299,6 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: } setIsStackFastTrackPromptVisible(false) - setIsComplete(false) - setAutoFillNotice(null) - } - - const handleEditEntry = (entryId: string) => { - if (!entryId) { - return - } - - const question = wizardQuestionsById[entryId] - - if (!question) { - return - } - - setActiveEditQuestionId(entryId) } const handleQuestionAnswerSelection = async ( @@ -479,8 +310,6 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: return } - setGeneratedFile(null) - const prevValue = responses[question.id] let nextValue: Responses[keyof Responses] let didAddSelection = false @@ -526,10 +355,14 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: if (isStackQuestion) { if (nextValue === answer.value) { - await loadStackQuestions(answer.value, answer.label) + await loadStackQuestions(answer.value, answer.label, { + skipFastTrackPrompt: autoStartAfterStackSelection, + }) + onStackSelected?.(answer.value, answer.label) } else { setDynamicSteps([]) setIsStackFastTrackPromptVisible(false) + onStackCleared?.() } } } @@ -543,12 +376,10 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: } const applyDefaultAnswer = async () => { - if (!defaultAnswer || defaultAnswer.disabled) { + if (!defaultAnswer || defaultAnswer.disabled || !currentQuestion) { return } - setGeneratedFile(null) - const nextValue: Responses[keyof Responses] = currentQuestion.allowMultiple ? [defaultAnswer.value] : defaultAnswer.value @@ -563,7 +394,10 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: const isStackQuestion = currentQuestion.id === STACK_QUESTION_ID if (isStackQuestion) { - await loadStackQuestions(defaultAnswer.value, defaultAnswer.label) + await loadStackQuestions(defaultAnswer.value, defaultAnswer.label, { + skipFastTrackPrompt: autoStartAfterStackSelection, + }) + onStackSelected?.(defaultAnswer.value, defaultAnswer.label) return } @@ -571,40 +405,31 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: advanceToNextQuestion() }, 0) } + const resetWizardState = () => { + const stackIdToClear = selectedStackId setResponses({}) setDynamicSteps([]) setCurrentStepIndex(0) setCurrentQuestionIndex(0) - setIsComplete(false) - setGeneratedFile(null) - setIsGenerating(false) setIsStackFastTrackPromptVisible(false) setAutoFilledQuestionMap({}) - setAutoFillNotice(null) + setActiveStackLabel(null) hasAppliedInitialStack.current = null + if (stackIdToClear) { + clearWizardState(stackIdToClear) + } } const resetWizard = () => { - if (onClose) { - resetWizardState() - onClose() - return - } - resetWizardState() + onStackCleared?.() } const requestResetWizard = () => { setPendingConfirmation("reset") } - const requestChangeFile = () => { - if (typeof window !== "undefined") { - window.location.assign(DEVCONTEXT_ROOT_URL) - } - } - const confirmPendingConfirmation = () => { if (!pendingConfirmation) { return @@ -614,15 +439,6 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: resetWizard() } - if (pendingConfirmation === "change-file") { - if (typeof window !== "undefined") { - window.location.assign(DEVCONTEXT_ROOT_URL) - return - } - resetWizard() - onClose?.() - } - setPendingConfirmation(null) } @@ -630,99 +446,53 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: setPendingConfirmation(null) } - const generateInstructionsFile = async () => { - if (isGenerating) { - return - } - - const outputFileId = selectedFile?.id ?? null - if (!outputFileId) { - console.error("No instructions file selected. Cannot generate output.") - return + const isAnswerSelected = (value: string) => { + if (currentQuestion?.allowMultiple) { + return Array.isArray(currentAnswerValue) && currentAnswerValue.includes(value) } - setIsGenerating(true) - setGeneratedFile(null) - - try { - const questionsAndAnswers = serializeWizardResponses(wizardSteps, responses, outputFileId) - - console.log("Template combination data:", { - outputFile: questionsAndAnswers.outputFile, - stack: questionsAndAnswers.stackSelection, - }) - - const stackSegment = questionsAndAnswers.stackSelection ?? "general" - const fileConfig = fileOptions.find((file) => file.id === outputFileId) - - const result = await generateInstructions({ - stackSegment, - outputFileId, - responses: questionsAndAnswers, - fileFormat: fileConfig?.format, - }) - - if (result) { - setGeneratedFile(result) - } - } catch (error) { - console.error("Error calling generate API:", error) - } finally { - setIsGenerating(false) - } + return currentAnswerValue === value } const questionNumber = wizardSteps .slice(0, currentStepIndex) .reduce((count, step) => count + step.questions.length, 0) + currentQuestionIndex + 1 + const totalQuestions = useMemo( + () => wizardSteps.reduce((count, step) => count + step.questions.length, 0), + [wizardSteps] + ) + + const remainingQuestionCount = useMemo(() => { + return wizardSteps + .slice(currentStepIndex) + .reduce((count, step, stepIndex) => { + if (stepIndex === 0) { + return count + (step.questions.length - currentQuestionIndex) + } + return count + step.questions.length + }, 0) + }, [wizardSteps, currentStepIndex, currentQuestionIndex]) + const isAtFirstQuestion = currentStepIndex === 0 && currentQuestionIndex === 0 - const backDisabled = isAtFirstQuestion && !isComplete - const defaultButtonTitle = !canUseDefault - ? isComplete - ? "Questions complete" - : defaultAnswer?.disabled - ? "Default option unavailable" - : isDefaultSelected && !currentQuestion.allowMultiple - ? "Default already selected" - : defaultAnswer - ? undefined - : "No default available" - : undefined - const showChangeFile = Boolean(onClose && selectedFile) - - const topButtonLabel = showStackPivot ? "Choose a different stack" : "Start Over" - const topButtonHandler = showStackPivot ? () => goToPrevious() : () => requestResetWizard() + const backDisabled = isAtFirstQuestion + + if (!currentStep || !currentQuestion) { + return null + } const wizardLayout = (
- - - {selectedFile ? ( -
-
-
-

- {selectedFile.filename ?? selectedFile.label} -

-
- {showChangeFile ? ( - - ) : null} -
-
- ) : null} - - {showStackPivot ? ( +
+ + DevContext + +
+ + {isStackFastTrackPromptVisible ? (
@@ -730,30 +500,21 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }:

Skip the deep dive?

We can auto-apply the recommended answers for the next {remainingQuestionCount}{" "} - {remainingQuestionCount === 1 ? "question" : "questions"} across these sections. (You can still tweak the defaults.) + {remainingQuestionCount === 1 ? "question" : "questions"}. You can still tweak everything later.

-
-
- - -
+
+ +
-
- ) : null} - - {showQuestionControls ? ( + ) : (
@@ -770,7 +531,6 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: variant="outline" onClick={() => void applyDefaultAnswer()} disabled={!canUseDefault} - title={defaultButtonTitle} > {defaultButtonLabel} @@ -797,19 +557,19 @@ export function InstructionsWizard({ onClose, selectedFileId, initialStackId }: placeholder={filterPlaceholder} className="w-full rounded-lg border border-border/70 bg-background/80 px-3 py-2 text-sm text-foreground shadow-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60" /> + {isFilteringAnswers ? ( + + Filtering… + + ) : null}
- {isFilteringAnswers && !showNoFilterMatches ? ( -

- Showing {filteredAnswers.length} of {currentQuestion.answers.length} -

- ) : null}
) : null}
{showNoFilterMatches ? (

- No options match “{answerFilterQuery}”. Try a different search. + No options match “{answerFilterQuery}”. Try a different search.

) : ( )} -
+
Question {questionNumber} of {totalQuestions}
+
- ) : null} - - {isComplete ? ( - void generateInstructionsFile()} - isGenerating={isGenerating} - autoFillNotice={autoFillNotice} - onEditEntry={handleEditEntry} - /> - ) : null} - - {pendingConfirmation ? ( - - ) : null} + )}
) return ( <> {wizardLayout} - {editingQuestion ? ( - { - await handleQuestionAnswerSelection(editingQuestion, selectedAnswer, { skipAutoAdvance: true }) - - if (!editingQuestion.allowMultiple) { - closeEditDialog() - } - }} - onClose={closeEditDialog} - /> - ) : null} - {generatedFile ? ( - setGeneratedFile(null)} + {pendingConfirmation ? ( + ) : null} diff --git a/components/wizard-completion-summary.tsx b/components/wizard-completion-summary.tsx index 85f7ec3..31fd247 100644 --- a/components/wizard-completion-summary.tsx +++ b/components/wizard-completion-summary.tsx @@ -1,5 +1,6 @@ import { Button } from "@/components/ui/button" import type { CompletionSummaryEntry } from "@/lib/wizard-summary" +import type { FileOutputConfig } from "@/types/wizard" type WizardCompletionSummaryProps = { summary: CompletionSummaryEntry[] @@ -8,6 +9,9 @@ type WizardCompletionSummaryProps = { isGenerating: boolean autoFillNotice?: string | null onEditEntry?: (entryId: string) => void + fileOptions: FileOutputConfig[] + selectedFileId: string | null + onSelectFile: (fileId: string) => void } export function WizardCompletionSummary({ @@ -17,7 +21,12 @@ export function WizardCompletionSummary({ isGenerating, autoFillNotice, onEditEntry, + fileOptions, + selectedFileId, + onSelectFile, }: WizardCompletionSummaryProps) { + const selectedOption = fileOptions.find((file) => file.id === selectedFileId) ?? null + return (
@@ -32,7 +41,11 @@ export function WizardCompletionSummary({ -
@@ -44,6 +57,43 @@ export function WizardCompletionSummary({ ) : null}
+
+
+
+

+ Output file +

+

+ Pick the instructions file format to generate. You can change this anytime. +

+
+ {selectedOption ? ( + + {selectedOption.filename} + + ) : null} +
+
+ {fileOptions.map((file) => { + const isSelected = file.id === selectedFileId + return ( + + ) + })} +
+
+
{summary.map((entry) => (

{entry.question}

- {entry.isAutoFilled ? ( - - Default applied - - ) : null}
{entry.hasSelection ? (