Skip to content

Commit 73e311b

Browse files
committed
feat: Implement API for generating instructions file and add template configuration
1 parent 87e618a commit 73e311b

File tree

4 files changed

+306
-77
lines changed

4 files changed

+306
-77
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { readFile } from 'fs/promises'
3+
import path from 'path'
4+
import type { WizardResponses } from '@/types/wizard'
5+
import { getTemplateConfig } from '@/lib/template-config'
6+
7+
export async function POST(
8+
request: NextRequest,
9+
{ params }: { params: Promise<{ fileName: string }> }
10+
) {
11+
try {
12+
const { fileName } = await params
13+
const body = await request.json()
14+
const responses: WizardResponses = body
15+
16+
const templateConfig = getTemplateConfig(fileName)
17+
if (!templateConfig) {
18+
return NextResponse.json(
19+
{ error: `Template not found for fileName: ${fileName}` },
20+
{ status: 404 }
21+
)
22+
}
23+
24+
// Read the template file
25+
const templatePath = path.join(process.cwd(), 'file-templates', templateConfig.template)
26+
const template = await readFile(templatePath, 'utf-8')
27+
28+
// Replace template variables with actual values
29+
let generatedContent = template
30+
31+
// Helper function to replace template variables gracefully
32+
const replaceVariable = (key: keyof WizardResponses, fallback: string = 'Not specified') => {
33+
const value = responses[key]
34+
const placeholder = `{{${key}}}`
35+
36+
if (value === null || value === undefined || value === '') {
37+
generatedContent = generatedContent.replace(placeholder, fallback)
38+
} else {
39+
generatedContent = generatedContent.replace(placeholder, String(value))
40+
}
41+
}
42+
43+
// Replace all template variables
44+
replaceVariable('preferredIde')
45+
replaceVariable('frameworkSelection')
46+
replaceVariable('tooling')
47+
replaceVariable('language')
48+
replaceVariable('projectPriority')
49+
replaceVariable('codeStyle')
50+
replaceVariable('variableNaming')
51+
replaceVariable('fileNaming')
52+
replaceVariable('componentNaming')
53+
replaceVariable('exports')
54+
replaceVariable('comments')
55+
replaceVariable('collaboration')
56+
replaceVariable('fileStructure')
57+
replaceVariable('styling')
58+
replaceVariable('stateManagement')
59+
replaceVariable('apiLayer')
60+
replaceVariable('folders')
61+
replaceVariable('testingUT')
62+
replaceVariable('testingE2E')
63+
replaceVariable('dataFetching')
64+
replaceVariable('reactPerf')
65+
replaceVariable('auth')
66+
replaceVariable('validation')
67+
replaceVariable('logging')
68+
replaceVariable('commitStyle')
69+
replaceVariable('prRules')
70+
71+
// Return the generated content
72+
return NextResponse.json({
73+
content: generatedContent,
74+
fileName: templateConfig.outputFileName
75+
})
76+
77+
} catch (error) {
78+
console.error('Error generating file:', error)
79+
return NextResponse.json(
80+
{ error: 'Failed to generate file' },
81+
{ status: 500 }
82+
)
83+
}
84+
}

components/instructions-wizard.tsx

Lines changed: 112 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,117 @@ export function InstructionsWizard({ onClose }: InstructionsWizardProps) {
503503
setShowResetConfirm(false)
504504
}
505505

506+
const generateInstructionsFile = async () => {
507+
// Create a JSON object with question IDs as keys and their answers as values
508+
const questionsAndAnswers: WizardResponses = {
509+
preferredIde: null,
510+
frameworkSelection: null,
511+
tooling: null,
512+
language: null,
513+
fileStructure: null,
514+
styling: null,
515+
testingUT: null,
516+
testingE2E: null,
517+
projectPriority: null,
518+
codeStyle: null,
519+
variableNaming: null,
520+
fileNaming: null,
521+
componentNaming: null,
522+
exports: null,
523+
comments: null,
524+
collaboration: null,
525+
stateManagement: null,
526+
apiLayer: null,
527+
folders: null,
528+
dataFetching: null,
529+
reactPerf: null,
530+
auth: null,
531+
validation: null,
532+
logging: null,
533+
commitStyle: null,
534+
prRules: null,
535+
outputFile: null,
536+
}
537+
538+
wizardSteps.forEach((step) => {
539+
step.questions.forEach((question) => {
540+
let key = question.id
541+
let answer = responses[question.id]
542+
543+
// Special handling for preferredIdes and outputFiles
544+
if (key === "preferredIdes") {
545+
key = "preferredIde"
546+
if (Array.isArray(answer) && answer.length > 0) {
547+
answer = answer[0]
548+
} else if (typeof answer === "string") {
549+
// already a string
550+
} else {
551+
answer = null
552+
}
553+
}
554+
if (key === "outputFiles") {
555+
key = "outputFile"
556+
if (Array.isArray(answer) && answer.length > 0) {
557+
answer = answer[0]
558+
} else if (typeof answer === "string") {
559+
// already a string
560+
} else {
561+
answer = null
562+
}
563+
}
564+
565+
if (answer !== null && answer !== undefined) {
566+
if (question.allowMultiple && Array.isArray(answer)) {
567+
// For all other multi-selects, keep as array
568+
questionsAndAnswers[key as keyof WizardResponses] = Array.isArray(answer) ? answer.join(", ") : answer
569+
} else if (!question.allowMultiple && typeof answer === 'string') {
570+
questionsAndAnswers[key as keyof WizardResponses] = Array.isArray(answer) ? answer.join(", ") : answer
571+
} else {
572+
questionsAndAnswers[key as keyof WizardResponses] = Array.isArray(answer) ? answer.join(", ") : answer
573+
}
574+
} else {
575+
questionsAndAnswers[key as keyof WizardResponses] = null
576+
}
577+
})
578+
})
579+
580+
// console.log('Questions and Answers JSON:', JSON.stringify(questionsAndAnswers, null, 2))
581+
582+
// Call the API to generate the instructions file
583+
if (questionsAndAnswers.outputFile) {
584+
try {
585+
const response = await fetch(`/api/generate/${questionsAndAnswers.outputFile}`, {
586+
method: 'POST',
587+
headers: {
588+
'Content-Type': 'application/json',
589+
},
590+
body: JSON.stringify(questionsAndAnswers),
591+
})
592+
593+
if (response.ok) {
594+
const data = await response.json()
595+
596+
// Create a downloadable file
597+
const blob = new Blob([data.content], { type: 'text/markdown' })
598+
const url = URL.createObjectURL(blob)
599+
const a = document.createElement('a')
600+
a.href = url
601+
a.download = data.fileName
602+
document.body.appendChild(a)
603+
a.click()
604+
document.body.removeChild(a)
605+
URL.revokeObjectURL(url)
606+
} else {
607+
console.error('Failed to generate file:', await response.text())
608+
}
609+
} catch (error) {
610+
console.error('Error calling generate API:', error)
611+
}
612+
}
613+
614+
onClose?.()
615+
}
616+
506617
const renderCompletion = () => {
507618
const summary = wizardSteps.flatMap((step) =>
508619
step.questions.map((question) => {
@@ -563,83 +674,7 @@ export function InstructionsWizard({ onClose }: InstructionsWizardProps) {
563674
<Button variant="ghost" onClick={requestResetWizard}>
564675
Start Over
565676
</Button>
566-
<Button onClick={() => {
567-
// Create a JSON object with question IDs as keys and their answers as values
568-
const questionsAndAnswers: WizardResponses = {
569-
preferredIde: null,
570-
frameworkSelection: null,
571-
tooling: null,
572-
language: null,
573-
fileStructure: null,
574-
styling: null,
575-
testingUT: null,
576-
testingE2E: null,
577-
projectPriority: null,
578-
codeStyle: null,
579-
variableNaming: null,
580-
fileNaming: null,
581-
componentNaming: null,
582-
exports: null,
583-
comments: null,
584-
collaboration: null,
585-
stateManagement: null,
586-
apiLayer: null,
587-
folders: null,
588-
dataFetching: null,
589-
reactPerf: null,
590-
auth: null,
591-
validation: null,
592-
logging: null,
593-
commitStyle: null,
594-
prRules: null,
595-
outputFile: null,
596-
}
597-
598-
wizardSteps.forEach((step) => {
599-
step.questions.forEach((question) => {
600-
let key = question.id
601-
let answer = responses[question.id]
602-
603-
// Special handling for preferredIdes and outputFiles
604-
if (key === "preferredIdes") {
605-
key = "preferredIde"
606-
if (Array.isArray(answer) && answer.length > 0) {
607-
answer = answer[0]
608-
} else if (typeof answer === "string") {
609-
// already a string
610-
} else {
611-
answer = null
612-
}
613-
}
614-
if (key === "outputFiles") {
615-
key = "outputFile"
616-
if (Array.isArray(answer) && answer.length > 0) {
617-
answer = answer[0]
618-
} else if (typeof answer === "string") {
619-
// already a string
620-
} else {
621-
answer = null
622-
}
623-
}
624-
625-
if (answer !== null && answer !== undefined) {
626-
if (question.allowMultiple && Array.isArray(answer)) {
627-
// For all other multi-selects, keep as array
628-
questionsAndAnswers[key as keyof WizardResponses] = Array.isArray(answer) ? answer.join(", ") : answer
629-
} else if (!question.allowMultiple && typeof answer === 'string') {
630-
questionsAndAnswers[key as keyof WizardResponses] = Array.isArray(answer) ? answer.join(", ") : answer
631-
} else {
632-
questionsAndAnswers[key as keyof WizardResponses] = Array.isArray(answer) ? answer.join(", ") : answer
633-
}
634-
} else {
635-
questionsAndAnswers[key as keyof WizardResponses] = null
636-
}
637-
})
638-
})
639-
640-
console.log('Questions and Answers JSON:', JSON.stringify(questionsAndAnswers, null, 2))
641-
onClose?.()
642-
}}>
677+
<Button onClick={() => void generateInstructionsFile()}>
643678
Generate My Instructions
644679
</Button>
645680
</div>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Copilot Instructions
2+
3+
These instructions define how GitHub Copilot should assist in this project.
4+
They are tailored to our selected IDE, framework, and coding conventions, based on real developer practices.
5+
6+
---
7+
8+
## Environment
9+
10+
- **IDE**: {{preferredIde}}
11+
- **Framework**: {{frameworkSelection}}
12+
- **Build Tooling**: {{tooling}}
13+
- **Language**: {{language}}
14+
15+
---
16+
17+
## Project Priorities
18+
19+
- **Primary focus**: {{projectPriority}}
20+
- **Code style**: {{codeStyle}}
21+
- **Variable naming**: {{variableNaming}}
22+
- **File naming**: {{fileNaming}}
23+
- **Component naming**: {{componentNaming}}
24+
- **Exports**: {{exports}}
25+
- **Comments & documentation**: {{comments}}
26+
- **Collaboration rules**: {{collaboration}}
27+
28+
---
29+
30+
## Architecture & Structure
31+
32+
- **Component structure**: {{fileStructure}}
33+
- **Styling approach**: {{styling}}
34+
- **State management**: {{stateManagement}}
35+
- **API layer**: {{apiLayer}}
36+
- **Folder structure**: {{folders}}
37+
38+
---
39+
40+
## Testing
41+
42+
- **Unit testing**: {{testingUT}}
43+
- **End-to-End (E2E) testing**: {{testingE2E}}
44+
45+
---
46+
47+
## Performance Guidelines
48+
49+
- **Data fetching**: {{dataFetching}}
50+
- **React performance**: {{reactPerf}}
51+
52+
---
53+
54+
## Security Guidelines
55+
56+
- **Authentication & secrets**: {{auth}}
57+
- **Validation**: {{validation}}
58+
- **Logging**: {{logging}}
59+
60+
---
61+
62+
## Commits & Pull Requests
63+
64+
- **Commit style**: {{commitStyle}}
65+
- **Pull request rules**: {{prRules}}
66+
67+
---
68+
69+
## IDE-Specific Guidance
70+
71+
For **VS Code**:
72+
- Always include a `.editorconfig`.
73+
- Enable **Prettier** and **ESLint** extensions.
74+
- Turn on `editor.formatOnSave: true`.
75+
- Suggested extensions:
76+
- `dbaeumer.vscode-eslint`
77+
- `esbenp.prettier-vscode`
78+
- `formulahendry.auto-rename-tag`
79+
80+
---
81+
82+
## Copilot Usage Guidelines
83+
84+
- Use Copilot to generate **boilerplate**, but always review before committing.
85+
- When writing tests, describe expected behavior clearly so Copilot can produce meaningful cases.
86+
- Prefer completions aligned with **our conventions** (naming, structure, testing).
87+
- Reject or edit Copilot suggestions that break the rules above.
88+
89+
---
90+
91+
## Notes
92+
93+
- This file is auto-generated by **DevContext**.
94+
- Regenerate this file whenever project priorities, frameworks, or rules change.

0 commit comments

Comments
 (0)