Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ Build high-signal agent and instruction files from community-proven best practic
1. Launch the app and switch from the landing hero to the Instructions Wizard.
2. Pick the instruction file you want to assemble (from templates defined in `data/files.json`).
3. Choose your framework and automatically load its follow-up question set (dynamic imports from `data/questions/<framework>.json`).
4. Answer or skip topic prompts across general, architecture, performance, security, commits, and more.
5. Review a completion summary that highlights which best practices made it into your file and which were skipped for later.
4. Answer topic prompts across general, architecture, performance, security, commits, and more—or lean on the recommended defaults when you need a fast decision.
5. Review a completion summary that highlights what made it into your file and which areas still need decisions.

## Community knowledge base
- Every topic originates from the developer community—playbooks, real-world retrospectives, and shared tooling habits.
Expand All @@ -24,8 +24,8 @@ Build high-signal agent and instruction files from community-proven best practic

## Key interaction details
- Tooltips open from the info icon, letting you explore examples, pros/cons, tags, and external docs without losing your place.
- Multi-select questions support skipping (recorded as `null`) so uncertain topics never block progress.
- Progress indicators keep a running count of answered versus skipped items, making gaps obvious before export.
- Multi-select questions let you apply the curated default choice with a single click so momentum never stalls.
- Progress indicators keep a running count of answered versus unanswered items, making gaps obvious before export.

## Run devcontext locally
```bash
Expand Down
4 changes: 2 additions & 2 deletions agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
- Framework selection (`data/frameworks.json`) with branching into framework-specific question sets (e.g., `data/questions/react.json`).
- Dynamic question sets loaded via `import()` based on the chosen framework.
- User actions per question:
- Select single or multiple answers (with skip support that records `null`).
- Select single or multiple answers, or apply the recommended default when unsure.
- Review hover tooltips with examples, pros/cons, tags, and documentation links.
- Complete flow with a summary of answered vs skipped items.
- Complete flow with a summary of answered selections and remaining gaps.

## Data Conventions
- Every answer object may define: `value`, `label`, `icon`, `example`, `infoLines` (derived from `pros`/`cons`), `tags`, `isDefault`, `disabled`, `disabledLabel`, and `docs`.
Expand Down
117 changes: 83 additions & 34 deletions components/instructions-wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import { buildStepFromQuestionSet, getFormatLabel, getMimeTypeForFormat, mapAnsw
import type { GeneratedFileResult } from "@/types/output"

const fileOptions = filesData as FileOutputConfig[]
const defaultFileOption = fileOptions.find((file) => file.enabled !== false) ?? fileOptions[0] ?? null
const defaultFileOption =
fileOptions.find((file) => file.isDefault) ??
fileOptions.find((file) => file.enabled !== false) ??
fileOptions[0] ??
null

const FRAMEWORK_STEP_ID = "frameworks"
const FRAMEWORK_QUESTION_ID = "frameworkSelection"
Expand Down Expand Up @@ -206,15 +210,14 @@ const frameworksStep: WizardStep = {
{
id: FRAMEWORK_QUESTION_ID,
question: "Which framework are you working with?",
skippable: false,
answers: (rawFrameworks as FrameworkConfig[]).map((framework) => ({
value: framework.id,
label: framework.label,
icon: framework.icon,
disabled: framework.enabled === false,
disabledLabel: framework.enabled === false ? "Soon" : undefined,
docs: framework.docs,
skippable: framework.skippable,
isDefault: framework.isDefault,
})),
},
],
Expand Down Expand Up @@ -311,6 +314,34 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza
return currentAnswerValue === value
}

const defaultAnswer = useMemo(
() => currentQuestion.answers.find((answer) => answer.isDefault),
[currentQuestion]
)

const isDefaultSelected = useMemo(() => {
if (!defaultAnswer) {
return false
}

if (currentQuestion.allowMultiple) {
return Array.isArray(currentAnswerValue) && currentAnswerValue.includes(defaultAnswer.value)
}

return currentAnswerValue === defaultAnswer.value
}, [currentAnswerValue, currentQuestion.allowMultiple, defaultAnswer])

const canUseDefault = Boolean(
!isComplete &&
defaultAnswer &&
!defaultAnswer.disabled &&
(!isDefaultSelected || currentQuestion.allowMultiple)
)

const defaultButtonLabel = defaultAnswer
? `Use default (${defaultAnswer.label})`
: "Use default"

const advanceToNextQuestion = () => {
const isLastQuestionInStep = currentQuestionIndex === currentStep.questions.length - 1
const isLastStep = currentStepIndex === wizardSteps.length - 1
Expand Down Expand Up @@ -361,7 +392,7 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza
id: question.id,
question: question.question,
allowMultiple: question.allowMultiple,
skippable: question.skippable,
responseKey: question.responseKey,
answers: question.answers.map(mapAnswerSourceToWizard),
}))

Expand Down Expand Up @@ -445,25 +476,33 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza
}
}

const skipQuestion = () => {
if (currentQuestion.skippable === false) {
const applyDefaultAnswer = async () => {
if (!defaultAnswer || defaultAnswer.disabled) {
return
}

setGeneratedFile(null)

const nextValue: Responses[keyof Responses] = currentQuestion.allowMultiple
? [defaultAnswer.value]
: defaultAnswer.value

setResponses((prev) => ({
...prev,
[currentQuestion.id]: null,
[currentQuestion.id]: nextValue,
}))

if (currentQuestion.id === FRAMEWORK_QUESTION_ID) {
setDynamicSteps([])
const isFrameworkQuestion = currentQuestion.id === FRAMEWORK_QUESTION_ID

if (isFrameworkQuestion) {
await loadFrameworkQuestions(defaultAnswer.value, defaultAnswer.label)
return
}

advanceToNextQuestion()
setTimeout(() => {
advanceToNextQuestion()
}, 0)
}

const resetWizard = () => {
setResponses({})
setDynamicSteps([])
Expand Down Expand Up @@ -559,20 +598,26 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza

wizardSteps.forEach((step) => {
step.questions.forEach((question) => {
const key = question.id
const responseKey = question.responseKey ?? question.id

if (!(responseKey in questionsAndAnswers)) {
return
}

const answer = responses[question.id]
const targetKey = responseKey as keyof WizardResponses

if (answer !== null && answer !== undefined) {
if (question.allowMultiple && Array.isArray(answer)) {
// For all other multi-selects, keep as array
questionsAndAnswers[key as keyof WizardResponses] = Array.isArray(answer) ? answer.join(", ") : answer
questionsAndAnswers[targetKey] = answer.join(", ")
} else if (!question.allowMultiple && typeof answer === 'string') {
questionsAndAnswers[key as keyof WizardResponses] = Array.isArray(answer) ? answer.join(", ") : answer
questionsAndAnswers[targetKey] = answer
} else {
questionsAndAnswers[key as keyof WizardResponses] = Array.isArray(answer) ? answer.join(", ") : answer
questionsAndAnswers[targetKey] = Array.isArray(answer) ? answer.join(", ") : (answer as string)
}
} else {
questionsAndAnswers[key as keyof WizardResponses] = null
questionsAndAnswers[targetKey] = null
}
})
})
Expand Down Expand Up @@ -632,7 +677,7 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza
selectedFile
? {
question: "Instructions file",
skipped: false,
hasSelection: true,
answers: [
selectedFile.label,
selectedFile.filename ? `Filename: ${selectedFile.filename}` : null,
Expand All @@ -641,14 +686,14 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza
}
: {
question: "Instructions file",
skipped: true,
hasSelection: false,
answers: [],
},
...wizardSteps.flatMap((step) =>
step.questions.map((question) => {
const value = responses[question.id]
const selectedAnswers = question.answers.filter((answer) => {
if (value === null) {
if (value === null || value === undefined) {
return false
}

Expand All @@ -661,7 +706,7 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza

return {
question: question.question,
skipped: value === null,
hasSelection: selectedAnswers.length > 0,
answers: selectedAnswers.map((answer) => answer.label),
}
})
Expand All @@ -684,14 +729,14 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza
className="rounded-2xl border border-border/70 bg-background/90 p-5"
>
<p className="text-sm font-medium text-muted-foreground">{entry.question}</p>
{entry.skipped ? (
<p className="mt-2 text-base font-semibold text-foreground">Skipped</p>
) : (
{entry.hasSelection ? (
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm text-foreground">
{entry.answers.map((answer) => (
<li key={answer}>{answer}</li>
))}
</ul>
) : (
<p className="mt-2 text-base font-semibold text-foreground">No selection</p>
)}
</div>
))}
Expand Down Expand Up @@ -722,7 +767,17 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza

const isAtFirstQuestion = currentStepIndex === 0 && currentQuestionIndex === 0
const backDisabled = isAtFirstQuestion && !isComplete
const canSkipCurrentQuestion = !isComplete && currentQuestion.skippable !== false
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 actionBar = (
Expand All @@ -738,17 +793,11 @@ export function InstructionsWizard({ onClose, selectedFileId }: InstructionsWiza
</Button>
<Button
variant="ghost"
onClick={skipQuestion}
disabled={!canSkipCurrentQuestion}
title={
canSkipCurrentQuestion
? undefined
: isComplete
? "Questions complete"
: "This question must be answered"
}
onClick={() => void applyDefaultAnswer()}
disabled={!canUseDefault}
title={defaultButtonTitle}
>
Skip
{defaultButtonLabel}
</Button>
</div>
<div className="ml-auto flex flex-wrap justify-end gap-2">
Expand Down
9 changes: 6 additions & 3 deletions data/architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"value": "reactQuery",
"label": "React Query",
"example": "Use React Query for server state management",
"docs": "https://tanstack.com/query/latest/docs/react/overview"
"docs": "https://tanstack.com/query/latest/docs/react/overview",
"isDefault": true
},
{
"value": "reduxToolkit",
Expand All @@ -30,7 +31,8 @@
"value": "hooks",
"label": "Custom hooks",
"example": "Encapsulate API calls in useFetchSomething()",
"docs": "https://react.dev/learn/reusing-logic-with-custom-hooks"
"docs": "https://react.dev/learn/reusing-logic-with-custom-hooks",
"isDefault": true
}
]
},
Expand All @@ -41,7 +43,8 @@
{
"value": "featureFolders",
"label": "Feature-based folders",
"example": "src/features/auth/LoginForm.tsx"
"example": "src/features/auth/LoginForm.tsx",
"isDefault": true
},
{
"value": "domainDriven",
Expand Down
8 changes: 5 additions & 3 deletions data/commits.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
"value": "conventional",
"label": "Conventional Commits",
"example": "feat(auth): add login endpoint",
"docs": "https://www.conventionalcommits.org/en/v1.0.0/"
"docs": "https://www.conventionalcommits.org/en/v1.0.0/",
"isDefault": true
},
{
"value": "gitmoji",
"label": "Gitmoji",
"example": " add login endpoint",
"example": "\u2728 add login endpoint",
"docs": "https://gitmoji.dev/"
},
{
Expand All @@ -34,7 +35,8 @@
{
"value": "reviewRequired",
"label": "Require at least one review",
"example": "PRs must be approved by another team member."
"example": "PRs must be approved by another team member.",
"isDefault": true
},
{
"value": "changelog",
Expand Down
8 changes: 3 additions & 5 deletions data/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"enabled": true,
"icon": "markdown",
"docs": "https://docs.github.com/en/copilot",
"skippable": false
"isDefault": true
},
{
"id": "agents-md",
Expand All @@ -16,16 +16,14 @@
"format": "markdown",
"enabled": true,
"icon": "markdown",
"docs": "https://docs.github.com/en/copilot",
"skippable": false
"docs": "https://docs.github.com/en/copilot"
},
{
"id": "cursor-rules",
"label": "Cursor Rules",
"filename": ".cursor/rules",
"format": "json",
"enabled": true,
"docs": "https://docs.cursor.com/workflows/rules",
"skippable": false
"docs": "https://docs.cursor.com/workflows/rules"
}
]
8 changes: 3 additions & 5 deletions data/frameworks.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,20 @@
"icon": "react",
"enabled": true,
"docs": "https://react.dev/learn",
"skippable": false
"isDefault": true
},
{
"id": "nextjs",
"label": "Next.js",
"icon": "nextdotjs",
"enabled": true,
"docs": "https://nextjs.org/docs",
"skippable": false
"docs": "https://nextjs.org/docs"
},
{
"id": "angular",
"label": "Angular",
"icon": "angular",
"enabled": true,
"docs": "https://angular.io/docs",
"skippable": false
"docs": "https://angular.io/docs"
}
]
Loading