diff --git a/app/new/stack/stack-summary-page.tsx b/app/new/stack/stack-summary-page.tsx index 64f340d..ceef512 100644 --- a/app/new/stack/stack-summary-page.tsx +++ b/app/new/stack/stack-summary-page.tsx @@ -16,7 +16,14 @@ import { } 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 { + FileOutputConfig, + FreeTextResponses, + Responses, + WizardQuestion, + WizardAnswer, + WizardStep, +} from "@/types/wizard" import type { GeneratedFileResult } from "@/types/output" import { WizardEditAnswerDialog } from "@/components/wizard-edit-answer-dialog" @@ -30,6 +37,7 @@ type StackSummaryPageProps = { export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { const [wizardSteps, setWizardSteps] = useState(null) const [responses, setResponses] = useState(null) + const [freeTextResponses, setFreeTextResponses] = useState({}) const [autoFilledMap, setAutoFilledMap] = useState>({}) const [stackLabel, setStackLabel] = useState(null) const [autoFillNotice, setAutoFillNotice] = useState(null) @@ -53,7 +61,13 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { setErrorMessage(null) try { if (mode === "default") { - const { steps, responses: defaultResponses, autoFilledMap: defaultsMap, stackLabel: label } = + const { + steps, + responses: defaultResponses, + freeTextResponses: defaultFreeTextResponses, + autoFilledMap: defaultsMap, + stackLabel: label, + } = await buildDefaultSummaryData(stackId) if (!isActive) { @@ -62,6 +76,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { setWizardSteps(steps) setResponses(defaultResponses) + setFreeTextResponses(defaultFreeTextResponses) setAutoFilledMap(defaultsMap) setStackLabel(label) setAutoFillNotice("We applied the recommended defaults for you. Tweak any section before generating.") @@ -70,6 +85,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { stackId, stackLabel: label, responses: defaultResponses, + freeTextResponses: defaultFreeTextResponses, autoFilledMap: defaultsMap, updatedAt: Date.now(), }) @@ -85,6 +101,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { if (!storedState) { setWizardSteps(steps) setResponses(null) + setFreeTextResponses({}) setAutoFilledMap({}) setStackLabel(computedLabel) setAutoFillNotice(null) @@ -99,6 +116,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { setWizardSteps(steps) setResponses(normalizedResponses) + setFreeTextResponses(storedState.freeTextResponses ?? {}) setAutoFilledMap(storedState.autoFilledMap ?? {}) setStackLabel(storedState.stackLabel ?? computedLabel) setAutoFillNotice(null) @@ -133,10 +151,11 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { null, wizardSteps, responses, + freeTextResponses, autoFilledMap, false ) - }, [wizardSteps, responses, autoFilledMap]) + }, [wizardSteps, responses, freeTextResponses, autoFilledMap]) const handleGenerate = useCallback( async (fileOption: FileOutputConfig) => { @@ -148,7 +167,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { setGeneratedFile(null) try { - const payload = serializeWizardResponses(wizardSteps, responses, fileOption.id) + const payload = serializeWizardResponses(wizardSteps, responses, freeTextResponses, fileOption.id) const result = await generateInstructions({ stackSegment: stackId, @@ -167,7 +186,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { setIsGeneratingMap((prev) => ({ ...prev, [fileOption.id]: false })) } }, - [wizardSteps, responses, stackId] + [wizardSteps, responses, freeTextResponses, stackId] ) const summaryHeader = stackLabel ?? @@ -187,9 +206,56 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { setEditingQuestionId(questionId) } - const handleCloseEdit = () => { + const handleCloseEdit = useCallback(() => { setEditingQuestionId(null) - } + }, []) + + const applyFreeTextUpdate = useCallback( + (question: WizardQuestion, submittedValue: string) => { + if (!responses) { + return + } + + const trimmed = submittedValue.trim() + const nextFreeText: FreeTextResponses = (() => { + if (trimmed.length === 0) { + if (!(question.id in freeTextResponses)) { + return { ...freeTextResponses } + } + + const next = { ...freeTextResponses } + delete next[question.id] + return next + } + + return { + ...freeTextResponses, + [question.id]: trimmed, + } + })() + + const nextAutoFilledMap = { ...autoFilledMap } + delete nextAutoFilledMap[question.id] + + setFreeTextResponses(nextFreeText) + setAutoFilledMap(nextAutoFilledMap) + setAutoFillNotice(null) + + if (stackId) { + persistWizardState({ + stackId, + stackLabel: stackLabel ?? summaryHeader, + responses, + freeTextResponses: nextFreeText, + autoFilledMap: nextAutoFilledMap, + updatedAt: Date.now(), + }) + } + + handleCloseEdit() + }, + [responses, freeTextResponses, autoFilledMap, stackId, stackLabel, summaryHeader, handleCloseEdit] + ) const applyAnswerUpdate = useCallback( (question: WizardQuestion, answer: WizardAnswer) => { @@ -226,6 +292,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { stackId, stackLabel: stackLabel ?? summaryHeader, responses: currentResponses, + freeTextResponses, autoFilledMap: currentAutoMap, updatedAt: Date.now(), }) @@ -235,7 +302,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { handleCloseEdit() } }, - [responses, autoFilledMap, stackId, stackLabel, summaryHeader] + [responses, freeTextResponses, autoFilledMap, stackId, stackLabel, summaryHeader, handleCloseEdit] ) if (isLoading) { @@ -386,11 +453,16 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { return null } const currentValue = responses ? responses[editingQuestion.id] : undefined + const currentFreeText = typeof freeTextResponses[editingQuestion.id] === "string" + ? freeTextResponses[editingQuestion.id] + : "" return ( applyAnswerUpdate(editingQuestion, answer)} + freeTextValue={currentFreeText} + onFreeTextSave={(nextValue) => applyFreeTextUpdate(editingQuestion, nextValue)} onClose={handleCloseEdit} /> ) diff --git a/components/instructions-wizard.tsx b/components/instructions-wizard.tsx index f13fa4f..f619297 100644 --- a/components/instructions-wizard.tsx +++ b/components/instructions-wizard.tsx @@ -1,12 +1,21 @@ "use client" -import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type FormEvent } from "react" import Link from "next/link" import { Button } from "@/components/ui/button" -import { Undo2 } from "lucide-react" - -import type { InstructionsWizardProps, Responses, WizardAnswer, WizardQuestion, WizardStep, WizardConfirmationIntent } from "@/types/wizard" +import { Input } from "@/components/ui/input" +import { CheckCircle2, Undo2 } from "lucide-react" + +import type { + InstructionsWizardProps, + Responses, + WizardAnswer, + WizardQuestion, + WizardStep, + WizardConfirmationIntent, + FreeTextResponses, +} from "@/types/wizard" import { buildFilterPlaceholder, useAnswerFilter } from "@/hooks/use-answer-filter" import { STACK_QUESTION_ID, @@ -36,6 +45,7 @@ export function InstructionsWizard({ const [responses, setResponses] = useState(() => initialStackId ? { [STACK_QUESTION_ID]: initialStackId } : {} ) + const [freeTextResponses, setFreeTextResponses] = useState({}) const [dynamicSteps, setDynamicSteps] = useState(() => initialStackStep ? [initialStackStep] : [] ) @@ -77,6 +87,20 @@ export function InstructionsWizard({ const filterInputId = currentQuestion ? `answer-filter-${currentQuestion.id}` : "answer-filter" const currentAnswerValue = currentQuestion ? responses[currentQuestion.id] : undefined + const currentFreeTextValue = useMemo(() => { + if (!currentQuestion) { + return "" + } + + const value = freeTextResponses[currentQuestion.id] + return typeof value === "string" ? value : "" + }, [currentQuestion, freeTextResponses]) + const freeTextConfig = currentQuestion?.freeText ?? null + const freeTextInputId = currentQuestion ? `free-text-${currentQuestion.id}` : "free-text" + const canSubmitFreeText = Boolean(freeTextConfig?.enabled && currentFreeTextValue.trim().length > 0) + const hasSavedCustomFreeText = Boolean(freeTextConfig?.enabled && currentFreeTextValue.trim().length > 0) + + const savedCustomFreeTextValue = currentFreeTextValue.trim() const defaultAnswer = useMemo( () => currentQuestion?.answers.find((answer) => answer.isDefault) ?? null, @@ -140,7 +164,13 @@ export function InstructionsWizard({ }, []) const persistStateIfPossible = useCallback( - (nextResponses: Responses, nextAutoFilled: Record, stackId: string | null, stackLabel?: string | null) => { + ( + nextResponses: Responses, + nextFreeText: FreeTextResponses, + nextAutoFilled: Record, + stackId: string | null, + stackLabel?: string | null + ) => { if (!stackId) { return } @@ -149,6 +179,7 @@ export function InstructionsWizard({ stackId, stackLabel: stackLabel ?? undefined, responses: nextResponses, + freeTextResponses: nextFreeText, autoFilledMap: nextAutoFilled, updatedAt: Date.now(), }) @@ -183,6 +214,28 @@ export function InstructionsWizard({ return next }) + setFreeTextResponses((prev) => { + if (Object.keys(prev).length === 0) { + return prev + } + + let didMutate = false + const next = { ...prev } + + step.questions.forEach((question) => { + if (question.id === STACK_QUESTION_ID) { + return + } + + if (next[question.id] !== undefined) { + delete next[question.id] + didMutate = true + } + }) + + return didMutate ? next : prev + }) + setCurrentStepIndex(1) setCurrentQuestionIndex(0) setAutoFilledQuestionMap({}) @@ -261,8 +314,15 @@ export function InstructionsWizard({ return } - persistStateIfPossible(responses, autoFilledQuestionMap, selectedStackId, activeStackLabel) - }, [responses, autoFilledQuestionMap, selectedStackId, activeStackLabel, persistStateIfPossible]) + persistStateIfPossible(responses, freeTextResponses, autoFilledQuestionMap, selectedStackId, activeStackLabel) + }, [ + responses, + freeTextResponses, + autoFilledQuestionMap, + selectedStackId, + activeStackLabel, + persistStateIfPossible, + ]) const handleWizardCompletion = useCallback(() => { if (!selectedStackId) { @@ -354,6 +414,8 @@ export function InstructionsWizard({ return next }) + setFreeTextResponses((prev) => (Object.keys(prev).length > 0 ? {} : prev)) + markQuestionsAutoFilled(autoFilledIds) setIsStackFastTrackPromptVisible(false) handleWizardCompletion() @@ -444,6 +506,125 @@ export function InstructionsWizard({ void handleQuestionAnswerSelection(currentQuestion, answer) } + const handleFreeTextChange = useCallback( + (event: ChangeEvent) => { + const question = currentQuestion + + if (!question) { + return + } + + const { value } = event.target + let didChange = false + + setFreeTextResponses((prev) => { + const existing = prev[question.id] + + if (value.length === 0) { + if (existing === undefined) { + return prev + } + + const next = { ...prev } + delete next[question.id] + didChange = true + return next + } + + if (existing === value) { + return prev + } + + didChange = true + return { + ...prev, + [question.id]: value, + } + }) + + if (didChange) { + clearAutoFilledFlag(question.id) + } + }, + [clearAutoFilledFlag, currentQuestion] + ) + + const hasSelectionForQuestion = useCallback( + (question: WizardQuestion) => { + const value = responses[question.id] + + if (question.allowMultiple) { + return Array.isArray(value) && value.length > 0 + } + + return typeof value === "string" && value.length > 0 + }, + [responses] + ) + + const commitFreeTextValue = ( + question: WizardQuestion, + rawValue: string, + options?: { allowAutoAdvance?: boolean } + ) => { + const allowAutoAdvance = options?.allowAutoAdvance ?? true + const trimmedValue = rawValue.trim() + const existingValue = typeof freeTextResponses[question.id] === "string" ? freeTextResponses[question.id] : "" + + if (trimmedValue === existingValue) { + if (allowAutoAdvance && trimmedValue.length > 0 && !hasSelectionForQuestion(question)) { + setTimeout(() => { + advanceToNextQuestion() + }, 0) + } + + return + } + + setFreeTextResponses((prev) => { + if (trimmedValue.length === 0) { + if (!(question.id in prev)) { + return prev + } + + const next = { ...prev } + delete next[question.id] + return next + } + + return { + ...prev, + [question.id]: trimmedValue, + } + }) + + clearAutoFilledFlag(question.id) + + if (allowAutoAdvance && trimmedValue.length > 0 && !hasSelectionForQuestion(question)) { + setTimeout(() => { + advanceToNextQuestion() + }, 0) + } + } + + const handleFreeTextSubmit = (event: FormEvent) => { + event.preventDefault() + + if (!currentQuestion) { + return + } + + commitFreeTextValue(currentQuestion, currentFreeTextValue) + } + + const handleFreeTextClear = () => { + if (!currentQuestion) { + return + } + + commitFreeTextValue(currentQuestion, "", { allowAutoAdvance: false }) + } + const applyDefaultAnswer = async () => { if (!defaultAnswer || defaultAnswer.disabled || !currentQuestion) { return @@ -478,6 +659,7 @@ export function InstructionsWizard({ const resetWizardState = () => { const stackIdToClear = selectedStackId setResponses({}) + setFreeTextResponses({}) setDynamicSteps([]) setCurrentStepIndex(0) setCurrentQuestionIndex(0) @@ -661,6 +843,59 @@ export function InstructionsWizard({ /> )} + {freeTextConfig?.enabled ? ( +
+
+

Need something else?

+

+ {hasSavedCustomFreeText + ? "Your custom answer below is what we'll keep when generating the AI context file." + : "Whatever you type here replaces the presets and goes straight into the AI context file."} +

+
+
+ +
+ + {currentFreeTextValue.length > 0 ? ( + + ) : null} +
+
+ {hasSavedCustomFreeText ? ( +

+ + + We'll use + {" "} + + {savedCustomFreeTextValue} + + {" "} + for this question when we generate your context file. + +

+ ) : null} +
+ ) : null} +
Question {questionNumber} of {totalQuestions} diff --git a/components/wizard-edit-answer-dialog.tsx b/components/wizard-edit-answer-dialog.tsx index 1ce202a..87c96c6 100644 --- a/components/wizard-edit-answer-dialog.tsx +++ b/components/wizard-edit-answer-dialog.tsx @@ -1,20 +1,41 @@ +import { useEffect, useState, type FormEvent } from "react" + import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" import { WizardAnswerGrid } from "./wizard-answer-grid" import type { Responses, WizardAnswer, WizardQuestion } from "@/types/wizard" import { buildFilterPlaceholder, useAnswerFilter } from "@/hooks/use-answer-filter" +import { CheckCircle2 } from "lucide-react" type WizardEditAnswerDialogProps = { question: WizardQuestion value: Responses[keyof Responses] | undefined onAnswerSelect: (answer: WizardAnswer) => void | Promise + freeTextValue?: string + onFreeTextSave?: (value: string) => void onClose: () => void } -export function WizardEditAnswerDialog({ question, value, onAnswerSelect, onClose }: WizardEditAnswerDialogProps) { +export function WizardEditAnswerDialog({ + question, + value, + onAnswerSelect, + freeTextValue = "", + onFreeTextSave, + onClose, +}: WizardEditAnswerDialogProps) { const { answers, query, setQuery, isFiltering } = useAnswerFilter(question) const filterPlaceholder = buildFilterPlaceholder(question) const showNoMatches = Boolean(question.enableFilter && answers.length === 0 && query.trim().length > 0) const filterInputId = `edit-answer-filter-${question.id}` + const freeTextEnabled = Boolean(question.freeText?.enabled) + const [freeTextDraft, setFreeTextDraft] = useState(freeTextValue) + const hasPersistedCustomAnswer = freeTextValue.trim().length > 0 + const persistedCustomAnswer = freeTextValue.trim() + + useEffect(() => { + setFreeTextDraft(freeTextValue) + }, [freeTextValue]) const isSelected = (candidate: string) => { if (question.allowMultiple) { @@ -24,6 +45,27 @@ export function WizardEditAnswerDialog({ question, value, onAnswerSelect, onClos return value === candidate } + const handleFreeTextSubmit = (event: FormEvent) => { + event.preventDefault() + const trimmed = freeTextDraft.trim() + + if (!onFreeTextSave) { + return + } + + onFreeTextSave(trimmed) + setFreeTextDraft(trimmed) + } + + const handleFreeTextClear = () => { + if (!onFreeTextSave) { + return + } + + onFreeTextSave("") + setFreeTextDraft("") + } + return (
)} + + {freeTextEnabled ? ( +
+
+

Custom answer

+

+ {hasPersistedCustomAnswer + ? "Your saved custom answer below is what we'll keep when generating the AI context file." + : "Whatever you add here replaces the presets and goes straight into the AI context file."} +

+
+
+ setFreeTextDraft(event.target.value)} + placeholder="Type your custom answer" + autoComplete="off" + className="sm:flex-1" + /> +
+ + {freeTextDraft.length > 0 ? ( + + ) : null} +
+
+ {hasPersistedCustomAnswer ? ( +

+ + + We'll use + {" "} + + {persistedCustomAnswer} + + {" "} + for this question when we generate your context file. + +

+ ) : null} +
+ ) : null}
) diff --git a/data/architecture.json b/data/architecture.json index 75f9fb3..959c25f 100644 --- a/data/architecture.json +++ b/data/architecture.json @@ -16,7 +16,16 @@ "example": "Use Redux Toolkit for app-wide state", "docs": "https://redux-toolkit.js.org/introduction/getting-started" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Zustand", + "MobX", + "Recoil", + "Context + reducers" + ] + } }, { "id": "apiLayer", @@ -34,7 +43,16 @@ "docs": "https://react.dev/learn/reusing-logic-with-custom-hooks", "isDefault": true } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "GraphQL via Apollo", + "REST helpers in /lib/api", + "tRPC procedures", + "Direct fetch in components" + ] + } }, { "id": "folders", @@ -51,6 +69,15 @@ "label": "Domain-driven folders", "example": "src/domain/order/components/OrderCard.tsx" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Atomic design folders", + "Keep /pages flat", + "Use src/features only", + "Layered architecture" + ] + } } ] diff --git a/data/commits.json b/data/commits.json index d32e93a..fee209a 100644 --- a/data/commits.json +++ b/data/commits.json @@ -21,7 +21,16 @@ "label": "Custom team rules", "example": "LOGIN: implemented login endpoint" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Prefix with ticket ID", + "Use semantic-release format", + "Limit subject to 50 chars", + "Use imperative mood" + ] + } }, { "id": "prRules", @@ -43,6 +52,15 @@ "label": "Update changelog", "example": "Each PR should update CHANGELOG.md with notable changes." } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Include screenshots for UI", + "Assign PRs to QA", + "Pair review required", + "Link Jira tickets" + ] + } } ] diff --git a/data/general.json b/data/general.json index ff7d46e..62fe3fc 100644 --- a/data/general.json +++ b/data/general.json @@ -25,7 +25,16 @@ "label": "Performance", "example": "Optimize code for runtime speed and bundle size." } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Developer velocity", + "Long-term maintainability", + "Regulatory compliance", + "Performance-first roadmap" + ] + } }, { "id": "codeStyle", @@ -49,7 +58,16 @@ "label": "Custom team rules", "example": "See internal style guide." } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Google TypeScript style guide", + "Internal lint rules", + "Prettier defaults only", + "Custom JavaScript guidelines" + ] + } }, { "id": "variableNaming", @@ -68,7 +86,16 @@ "example": "const user_name = 'john'", "docs": "https://en.wikipedia.org/wiki/Snake_case" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "camelCase with descriptive names", + "snake_case for shared libs", + "Prefix booleans with is/has", + "No abbreviations" + ] + } }, { "id": "fileNaming", @@ -99,7 +126,16 @@ "example": "user_profile.tsx", "docs": "https://en.wikipedia.org/wiki/Snake_case" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Feature-based naming", + "No spaces in filenames", + "Match backend naming", + "Use index files sparingly" + ] + } }, { "id": "componentNaming", @@ -118,7 +154,16 @@ "example": "export function loginForm() {}", "docs": "https://en.wikipedia.org/wiki/Camel_case" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "PascalCase with suffix", + "Domain-specific prefixes", + "Export anonymous components", + "Use Async prefix for lazy components" + ] + } }, { "id": "exports", @@ -137,7 +182,16 @@ "example": "export default function Button() {}", "docs": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export#default_exports" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Named exports everywhere", + "Default exports for pages", + "Prefer factory exports", + "Export objects with namespaces" + ] + } }, { "id": "comments", @@ -160,7 +214,16 @@ "label": "Inline explanations", "example": "// Explain why, not what" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Document intent over implementation", + "Use JSDoc for public APIs", + "Prefer ADRs for major changes", + "No inline comments" + ] + } }, { "id": "collaboration", @@ -183,6 +246,15 @@ "label": "Documentation required", "example": "New APIs must be documented in /docs." } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Two approvals required", + "Link Jira tickets in PRs", + "Use feature branch naming", + "Async standups in Slack" + ] + } } ] diff --git a/data/performance.json b/data/performance.json index 37c123d..41dda70 100644 --- a/data/performance.json +++ b/data/performance.json @@ -17,7 +17,16 @@ "docs": "https://web.dev/infinite-scroll-without-chaos/", "isDefault": false } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Cache responses for 5 minutes", + "Use SWR for caching", + "Batch API requests", + "Enable GraphQL persisted queries" + ] + } }, { "id": "reactPerf", @@ -36,6 +45,15 @@ "example": "import('chart.js') only when needed", "docs": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import#dynamic_imports" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Audit with React Profiler monthly", + "Avoid inline anonymous handlers", + "Use Suspense boundaries", + "Lazy load heavy widgets" + ] + } } -] \ No newline at end of file +] diff --git a/data/questions/angular.json b/data/questions/angular.json index c46f218..b941883 100644 --- a/data/questions/angular.json +++ b/data/questions/angular.json @@ -45,7 +45,16 @@ ], "example": "ng add @angular-builders/custom-webpack" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Angular Universal setup", + "Nx workspace generators", + "Custom esbuild pipeline", + "Monorepo with Turborepo" + ] + } }, { "id": "angular-language", @@ -93,7 +102,16 @@ ], "example": "ng new my-app --no-standalone --defaults" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "TypeScript strict with template checks", + "JavaScript transitional", + "Flow experimentation", + "Hybrid TS/JS modules" + ] + } }, { "id": "angular-fileStructure", @@ -143,7 +161,16 @@ ], "example": "libs/dashboard/ui/src/lib/widget.component.ts" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Domain-driven modules", + "Core/shared library split", + "Nx libs per feature", + "Standalone components only" + ] + } }, { "id": "angular-styling", @@ -191,7 +218,16 @@ ], "example": "ng add @angular/material" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "CSS Modules via webpack", + "Styled Components", + "Windi CSS", + "Scoped styles only" + ] + } }, { "id": "angular-stateManagement", @@ -240,7 +276,16 @@ ], "example": "userService.user$.pipe(map(user => !!user))" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Akita state management", + "ComponentStore", + "BehaviorSubject services", + "Apollo cache only" + ] + } }, { "id": "angular-apiLayer", @@ -288,7 +333,16 @@ ], "example": "ng-openapi-gen --config openapi.json" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "gRPC-web clients", + "GraphQL codegen", + "REST helpers in /core/api", + "Supabase client" + ] + } }, { "id": "angular-validation", @@ -335,7 +389,16 @@ ], "example": "const schema = z.object({ email: z.string().email() })" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Custom validators in shared lib", + "Use class-validator", + "Central schema package", + "Backend-only validation" + ] + } }, { "id": "angular-logging", @@ -381,7 +444,16 @@ ], "example": "this.logger.info('User created', payload)" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "LogRocket sessions", + "Elastic APM agent", + "New Relic browser agent", + "Console logging only" + ] + } }, { "id": "angular-testingUT", @@ -430,7 +502,16 @@ ], "example": "npm install --save-dev vitest @nx/vite" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Angular Testing Library", + "Spectator", + "No unit tests", + "Jest + RTL" + ] + } }, { "id": "angular-testingE2E", @@ -478,6 +559,15 @@ ], "example": "npx wdio run wdio.conf.ts" } - ] + ], + "freeText": { + "enabled": true, + "suggestions": [ + "Nightwatch", + "Protractor legacy", + "Manual smoke tests", + "TestCafe" + ] + } } ] diff --git a/data/questions/astro.json b/data/questions/astro.json index 2325788..ab5c77e 100644 --- a/data/questions/astro.json +++ b/data/questions/astro.json @@ -33,6 +33,15 @@ "example": "npx astro add svelte" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Preact components", + "SolidJS islands", + "Alpine.js widgets", + "HTMX partials" + ] + }, "explanation": "Primary integration influences hydration directives and linting." }, { @@ -66,6 +75,15 @@ "example": "export const revalidate = 60" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Client-side only", + "Edge SSR", + "On-demand rebuilds", + "Static marketing pages" + ] + }, "explanation": "Rendering model informs content strategy and hosting provider." }, { @@ -99,6 +117,15 @@ "example": "src/pages/posts/post-1.md" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Headless CMS via GraphQL", + "Notion sync", + "Markdown in /content/blog", + "Sanity dataset" + ] + }, "explanation": "Content strategy shapes file organization and build scripts." }, { @@ -135,6 +162,15 @@ "example": "wrangler pages deploy dist" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "GitHub Pages", + "Azure Static Web Apps", + "Render", + "Self-hosted Node server" + ] + }, "explanation": "Hosting target sets adapter, environment variables, and CI strategy." } ] diff --git a/data/questions/nextjs.json b/data/questions/nextjs.json index 4c1387d..b9e7a93 100644 --- a/data/questions/nextjs.json +++ b/data/questions/nextjs.json @@ -46,6 +46,15 @@ "example": "npm install next react react-dom" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Nx workspace", + "Blitz toolkit", + "Custom esbuild pipeline", + "Remix coexistence" + ] + }, "explanation": "Choose the tooling you rely on for bootstrapping and evolving the project." }, { @@ -95,6 +104,15 @@ "example": "app/(marketing)/page.tsx + pages/api/health.ts" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Feature-sliced routing", + "Monorepo modules", + "Pages for marketing only", + "src/modules structure" + ] + }, "explanation": "Routing choice impacts data fetching patterns, layouts, and deployment expectations." }, { @@ -131,6 +149,15 @@ "example": "npx create-next-app@latest my-app" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "TypeScript strict mode", + "JavaScript with JSDoc", + "Flow types", + "Hybrid JS/TS" + ] + }, "explanation": "Language choice impacts type safety, linting, and Copilot suggestions." }, { @@ -181,6 +208,15 @@ "example": "npm install styled-components" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Vanilla Extract", + "Tailwind + Radix", + "Chakra UI theme", + "UnoCSS" + ] + }, "explanation": "Styling stack influences rendering performance and component ergonomics." }, { @@ -231,6 +267,15 @@ "example": "const ThemeContext = createContext('light')" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Recoil", + "Jotai", + "Apollo cache only", + "Segment-specific contexts" + ] + }, "explanation": "State tooling guides component structure and cross-page coordination." }, { @@ -281,6 +326,15 @@ "example": "const queryClient = new QueryClient()" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Server Actions only", + "GraphQL clients", + "REST with revalidateTag", + "Edge runtime fetch" + ] + }, "explanation": "Data strategy shapes caching, hydration, and API integration patterns." }, { @@ -329,6 +383,15 @@ "example": "async function authorize(credentials) { /* validate */ }" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "AWS Cognito", + "Firebase Auth", + "Magic links only", + "Edge middleware tokens" + ] + }, "explanation": "Auth strategy drives session handling, middleware, and deployment needs." }, { @@ -377,6 +440,15 @@ "example": "if (!emailRegex.test(payload.email)) throw new Error('Invalid email')" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Superstruct", + "class-validator", + "Form-only validators", + "Backend-only validation" + ] + }, "explanation": "Validation approach affects reliability of Server Actions and API routes." }, { @@ -426,6 +498,15 @@ "example": "import pino from 'pino'" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Datadog RUM", + "Logflare", + "New Relic", + "Console logging only" + ] + }, "explanation": "Logging strategy determines observability and incident response workflows." }, { @@ -462,6 +543,15 @@ "example": "npm install --save-dev vitest @testing-library/react" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Testing Library only", + "RTL + Jest DOM", + "Integration tests in Node", + "No unit tests" + ] + }, "explanation": "Unit testing ensures components and helpers behave correctly in isolation." }, { @@ -498,6 +588,15 @@ "example": "npm install --save-dev cypress" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Nightwatch", + "TestCafe", + "Manual smoke tests", + "QA automation team" + ] + }, "explanation": "E2E coverage validates full Next.js flows from routing to data." } ] diff --git a/data/questions/nuxt.json b/data/questions/nuxt.json index 9c86f9f..c8594cb 100644 --- a/data/questions/nuxt.json +++ b/data/questions/nuxt.json @@ -30,6 +30,15 @@ "example": "export default defineCachedEventHandler(handler, { swr: true })" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Edge-only rendering", + "Client-side only", + "Prerender marketing pages", + "Nightly rebuild schedule" + ] + }, "explanation": "Rendering mode determines deployment targets and caching patterns." }, { @@ -63,6 +72,15 @@ "example": "const client = new GraphQLClient(endpoint)" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "useAsyncData", + "Direct axios calls", + "Supabase client", + "GraphQL codegen" + ] + }, "explanation": "Fetching strategy affects runtime config and environment variables." }, { @@ -97,6 +115,15 @@ "example": "" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Windi CSS", + "Design tokens via CSS vars", + "Sass modules", + "Tailwind + DaisyUI" + ] + }, "explanation": "Styling choice influences runtime head bleed and bundle size." }, { @@ -132,6 +159,15 @@ "example": "node .output/server/index.mjs" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Cloudflare Pages", + "AWS Amplify", + "Azure Static Web Apps", + "Self-hosted Kubernetes" + ] + }, "explanation": "Deployment target signals adapter, environment variables, and caching policies." } ] diff --git a/data/questions/python.json b/data/questions/python.json index c68d739..9bbc50d 100644 --- a/data/questions/python.json +++ b/data/questions/python.json @@ -33,6 +33,15 @@ "example": "app = Flask(__name__)" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Django REST Framework", + "Flask + Blueprints", + "Starlette", + "No framework (scripts only)" + ] + }, "explanation": "Framework choice informs project structure, CLI commands, and deployment guidance." }, { @@ -66,6 +75,15 @@ "example": "def create_user(payload): ..." } ], + "freeText": { + "enabled": true, + "suggestions": [ + "MyPy strict", + "Pyright enforcement", + "Gradual typed packages", + "Untyped prototypes" + ] + }, "explanation": "Typing policy guides lint rules, CI checks, and editor setup." }, { @@ -99,6 +117,15 @@ "example": "uv pip install fastapi" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "Pipenv", + "Conda environments", + "Poetry + uv lock", + "System pip only" + ] + }, "explanation": "Package tooling defines scripts, lockfiles, and reproducibility guidance." }, { @@ -132,6 +159,15 @@ "example": "behave features/" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "pytest + factoryboy", + "Hypothesis property tests", + "No automated tests", + "Robot Framework" + ] + }, "explanation": "Testing strategy controls fixtures, assertions, and coverage expectations." }, { @@ -165,6 +201,15 @@ "example": "# Document conventions in README" } ], + "freeText": { + "enabled": true, + "suggestions": [ + "PyLint only", + "Two-space indentation", + "Pre-commit hooks", + "Custom formatting scripts" + ] + }, "explanation": "Lint command shapes pre-commit hooks and CI tasks." } ] diff --git a/data/questions/react.json b/data/questions/react.json index b621022..c05f792 100644 --- a/data/questions/react.json +++ b/data/questions/react.json @@ -33,8 +33,67 @@ "Less customizable without eject" ], "example": "npx create-react-app my-app" + }, + { + "value": "nx", + "label": "Nx", + "icon": "nx", + "docs": "https://nx.dev/getting-started/intro", + "pros": [ + "Great for monorepos with shared utilities", + "Built-in generators and task caching" + ], + "cons": [ + "Higher initial setup complexity" + ], + "example": "npx create-nx-workspace@latest my-workspace" + }, + { + "value": "turborepo", + "label": "Turborepo", + "icon": "turborepo", + "docs": "https://turbo.build/repo/docs", + "pros": [ + "Parallel and incremental builds", + "Integrates tightly with Vercel deployments" + ], + "cons": [ + "Requires configuring your preferred bundler" + ], + "example": "npx create-turbo@latest my-monorepo" + }, + { + "value": "webpack", + "label": "Custom Webpack", + "icon": "webpack", + "docs": "https://webpack.js.org/concepts/", + "pros": [ + "Full control over the build pipeline", + "Massive plugin and loader ecosystem" + ], + "cons": [ + "Manual configuration and maintenance overhead" + ], + "example": "npm install --save-dev webpack webpack-cli webpack-dev-server" + }, + { + "value": "razzle", + "label": "Razzle", + "docs": "https://razzlejs.org/docs/getting-started", + "pros": [ + "Zero-config server and client rendering", + "Built-in support for React Router" + ], + "cons": [ + "Smaller community and fewer recent updates" + ], + "example": "npx create-razzle-app my-app" } ], + "freeText": { + "enabled": true, + "suggestions": [] + }, "explanation": "Choose the build tool / project scaffolder you\u2019re using." }, { @@ -70,8 +129,71 @@ "No static typing" ], "example": "npx create-vite@latest my-app --template react" + }, + { + "value": "typescript-strict", + "label": "TypeScript (strict mode)", + "icon": "/icons/typescript.svg", + "docs": "https://www.typescriptlang.org/tsconfig/#strict", + "pros": [ + "Maximum type safety and early error detection", + "Improves Copilot suggestions with precise types" + ], + "cons": [ + "Requires more explicit annotations in complex areas" + ], + "example": "Add \"strict\": true to compilerOptions in tsconfig.json" + }, + { + "value": "javascript-jsdoc", + "label": "JavaScript with JSDoc", + "icon": "/icons/javascript.svg", + "docs": "https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html", + "pros": [ + "Gradual typing without leaving .js files", + "Enhances IntelliSense while keeping build simple" + ], + "cons": [ + "Type comments can clutter implementation code", + "Limited deep type support compared to TypeScript" + ], + "example": "/** @param {number} count */ function increment(count) { return count + 1; }" + }, + { + "value": "flow", + "label": "Flow", + "icon": "flow", + "docs": "https://flow.org/en/docs/getting-started/", + "pros": [ + "Optional static typing for React apps", + "Powerful inference with minimal annotations" + ], + "cons": [ + "Smaller ecosystem compared to TypeScript", + "Requires Babel or build tooling to strip types" + ], + "example": "// @flow\nfunction greet(name: string): string {\n return `Hello ${name}`;\n}" + }, + { + "value": "reasonml", + "label": "ReasonML", + "icon": "reason", + "docs": "https://reasonml.github.io/docs/en/quickstart", + "pros": [ + "Expressive functional language with JSX interop", + "Strong type system and pattern matching" + ], + "cons": [ + "Smaller hiring pool and ecosystem", + "Requires compilation via ReScript toolchain" + ], + "example": "[@react.component]\nlet make = (~name) =>
{React.string(name)}
;" } ], + "freeText": { + "enabled": true, + "suggestions": [] + }, "explanation": "Language choice impacts type safety and Copilot suggestions." }, { @@ -117,8 +239,70 @@ "Adds boilerplate files per component" ], "example": "components/Button/Button.tsx + components/Button/index.tsx" + }, + { + "value": "feature-sliced", + "label": "Feature-sliced design", + "icon": "/icons/folder-tree.svg", + "docs": "https://feature-sliced.design/docs/get-started/overview", + "pros": [ + "Domain-driven slices keep modules cohesive", + "Supports scaling with shared layering conventions" + ], + "cons": [ + "Requires team alignment on slice boundaries" + ], + "example": "src/entities/user/ui/ProfileCard/index.tsx" + }, + { + "value": "atomic-design", + "label": "Atomic design folders", + "icon": "/icons/layout.svg", + "docs": "https://bradfrost.com/blog/post/atomic-web-design/", + "pros": [ + "Enforces a consistent component hierarchy", + "Encourages reuse of UI primitives" + ], + "cons": [ + "Rigid layers can feel heavyweight for small apps" + ], + "example": "components/atoms/Button/Button.tsx" + }, + { + "value": "component-tests-colocated", + "label": "Components with co-located tests", + "icon": "/icons/folder-tree.svg", + "docs": "https://kentcdodds.com/blog/colocate-tests", + "pros": [ + "Tests live beside implementation for quick discovery", + "Keeps refactors and specs in sync" + ], + "cons": [ + "Adds nested folders to the tree", + "Requires tooling to ignore *.test files in production bundles" + ], + "example": "Button/Button.tsx + Button/Button.test.tsx" + }, + { + "value": "next-app-router", + "label": "Next.js app router", + "icon": "nextdotjs", + "docs": "https://nextjs.org/docs/app/building-your-application/routing", + "pros": [ + "File-system routing with nested layouts", + "Server and client components by default" + ], + "cons": [ + "Specific to Next.js projects", + "Requires understanding React Server Components" + ], + "example": "app/dashboard/(marketing)/page.tsx" } ], + "freeText": { + "enabled": true, + "suggestions": [] + }, "explanation": "Component organization affects maintainability as the project grows." }, { @@ -154,8 +338,71 @@ "Verbose imports" ], "example": "import styles from './Button.module.css'" + }, + { + "value": "styled-components", + "label": "Styled Components", + "icon": "styledcomponents", + "docs": "https://styled-components.com/docs", + "pros": [ + "Component-scoped styles with tagged template literals", + "Dynamic styling based on props" + ], + "cons": [ + "Adds runtime overhead", + "SSR requires additional configuration" + ], + "example": "const Button = styled.button`\n background: #1d4ed8;\n color: white;\n`;" + }, + { + "value": "emotion", + "label": "Emotion", + "icon": "emotion", + "docs": "https://emotion.sh/docs/introduction", + "pros": [ + "Flexible CSS-in-JS with object or string styles", + "Great TypeScript support" + ], + "cons": [ + "Runtime styling can impact performance on low-end devices" + ], + "example": "const className = css`\n font-weight: 600;\n`;" + }, + { + "value": "vanilla-extract", + "label": "Vanilla Extract", + "icon": "vanillaextract", + "docs": "https://vanilla-extract.style/documentation/getting-started", + "pros": [ + "Zero-runtime CSS-in-TypeScript", + "Static extraction keeps bundles lean" + ], + "cons": [ + "Requires build tooling configuration", + "Less ergonomic for highly dynamic styles" + ], + "example": "// button.css.ts\nexport const root = style({ padding: '0.75rem' });" + }, + { + "value": "sass-modules", + "label": "Sass modules", + "icon": "sass", + "docs": "https://sass-lang.com/documentation", + "pros": [ + "Powerful nesting and variables", + "Scoped imports avoid global leaks" + ], + "cons": [ + "Build step required", + "Mixins can hide complexity" + ], + "example": "import styles from './Button.module.scss'" } ], + "freeText": { + "enabled": true, + "suggestions": [] + }, "explanation": "Styling choice impacts developer experience and bundle size." }, { @@ -191,8 +438,59 @@ "Less mature than Jest" ], "example": "npm install --save-dev vitest @testing-library/react" + }, + { + "value": "react-testing-library", + "label": "React Testing Library", + "icon": "testinglibrary", + "docs": "https://testing-library.com/docs/react-testing-library/intro/", + "pros": [ + "Encourages tests from the user perspective", + "Works with Jest, Vitest, and other runners" + ], + "cons": [ + "Requires pairing with an assertion library", + "Focused on DOM tests rather than class instance details" + ], + "example": "render(