Skip to content

Commit 9c268da

Browse files
committed
feat: refactor instructions wizard and API for improved file generation and template handling
1 parent 2755ab5 commit 9c268da

File tree

18 files changed

+466
-565
lines changed

18 files changed

+466
-565
lines changed

agents.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
## Core UI Flow
88
- Entry point at `app/page.tsx` toggles between a marketing hero and the instructions wizard.
99
- Wizard steps currently cover:
10-
- IDE selection (`data/ides.json`).
10+
- Instructions file selection (`data/files.json`).
1111
- Framework selection (`data/frameworks.json`) with branching into framework-specific question sets (e.g., `data/questions/react.json`).
1212
- Dynamic question sets loaded via `import()` based on the chosen framework.
1313
- User actions per question:
@@ -18,7 +18,7 @@
1818
## Data Conventions
1919
- Every answer object may define: `value`, `label`, `icon`, `example`, `infoLines` (derived from `pros`/`cons`), `tags`, `isDefault`, `disabled`, `disabledLabel`, and `docs`.
2020
- JSON files in `data/` supply domain-specific options:
21-
- `ides.json`, `frameworks.json`, `files.json`, `general.json`, `architecture.json`, `performance.json`, `security.json`, `commits.json`.
21+
- `files.json`, `frameworks.json`, `general.json`, `architecture.json`, `performance.json`, `security.json`, `commits.json`.
2222
- Framework-specific questionnaires live in `data/questions/<framework>.json`.
2323
- Newly added `docs` fields should point to authoritative resources and are surfaced in tooltips as external links.
2424

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,91 @@
11
import { NextRequest, NextResponse } from 'next/server'
22
import { readFile } from 'fs/promises'
33
import path from 'path'
4+
45
import type { WizardResponses } from '@/types/wizard'
56
import { getTemplateConfig, type TemplateKey } from '@/lib/template-config'
67

7-
// Helper function to map output file types to template types
88
function mapOutputFileToTemplateType(outputFile: string): string {
99
const mapping: Record<string, string> = {
1010
'instructions-md': 'copilot-instructions',
11+
'agents-md': 'agents',
1112
'cursor-rules': 'cursor-rules',
1213
'json-rules': 'json-rules',
13-
'agents-md': 'agents'
1414
}
15-
return mapping[outputFile] || outputFile
15+
16+
return mapping[outputFile] ?? outputFile
1617
}
1718

1819
export async function POST(
1920
request: NextRequest,
20-
{ params }: { params: { ide: string; framework: string; fileName: string } }
21+
{ params }: { params: { framework: string; fileName: string } },
2122
) {
2223
try {
23-
const { ide, framework, fileName } = params
24-
const body = await request.json()
25-
const responses: WizardResponses = body
26-
27-
// Determine template configuration based on the request
28-
let templateConfig
29-
30-
const frameworkFromPath = framework && !['general', 'none', 'undefined'].includes(framework)
31-
? framework
32-
: undefined
33-
34-
if (ide) {
35-
const templateKeyFromParams: TemplateKey = {
36-
ide,
37-
templateType: mapOutputFileToTemplateType(fileName),
38-
framework: frameworkFromPath
39-
}
40-
templateConfig = getTemplateConfig(templateKeyFromParams)
24+
const { framework, fileName } = params
25+
const responses = (await request.json()) as WizardResponses
26+
27+
const frameworkFromPath =
28+
framework && !['general', 'none', 'undefined'].includes(framework)
29+
? framework
30+
: undefined
31+
32+
const templateKeyFromParams: TemplateKey = {
33+
templateType: mapOutputFileToTemplateType(fileName),
34+
framework: frameworkFromPath,
4135
}
4236

43-
// Check if this is a combination-based request
44-
if (!templateConfig && responses.preferredIde && responses.outputFile) {
45-
const templateKey: TemplateKey = {
46-
ide: responses.preferredIde,
37+
let templateConfig = getTemplateConfig(templateKeyFromParams)
38+
39+
if (!templateConfig && responses.outputFile) {
40+
const templateKeyFromBody: TemplateKey = {
4741
templateType: mapOutputFileToTemplateType(responses.outputFile),
48-
framework: responses.frameworkSelection || undefined
42+
framework: responses.frameworkSelection || undefined,
4943
}
50-
templateConfig = getTemplateConfig(templateKey)
44+
45+
templateConfig = getTemplateConfig(templateKeyFromBody)
5146
}
5247

53-
// Fallback to legacy fileName-based approach
5448
if (!templateConfig) {
5549
templateConfig = getTemplateConfig(fileName)
5650
}
5751

5852
if (!templateConfig) {
5953
return NextResponse.json(
6054
{ error: `Template not found for fileName: ${fileName}` },
61-
{ status: 404 }
55+
{ status: 404 },
6256
)
6357
}
6458

65-
// Read the template file
6659
const templatePath = path.join(process.cwd(), 'file-templates', templateConfig.template)
6760
const template = await readFile(templatePath, 'utf-8')
6861

69-
// Replace template variables with actual values
7062
let generatedContent = template
63+
const isJsonTemplate = templateConfig.template.toLowerCase().endsWith('.json')
7164

72-
// Helper function to replace template variables gracefully
73-
const replaceVariable = (key: keyof WizardResponses, fallback: string = 'Not specified') => {
74-
const value = responses[key]
65+
const escapeForJson = (value: string) => {
66+
const escaped = JSON.stringify(value)
67+
return escaped.slice(1, -1)
68+
}
69+
70+
const replaceVariable = (key: keyof WizardResponses, fallback = 'Not specified') => {
7571
const placeholder = `{{${key}}}`
7672

73+
if (!generatedContent.includes(placeholder)) {
74+
return
75+
}
76+
77+
const value = responses[key]
78+
7779
if (value === null || value === undefined || value === '') {
78-
generatedContent = generatedContent.replace(placeholder, fallback)
80+
const replacement = isJsonTemplate ? escapeForJson(fallback) : fallback
81+
generatedContent = generatedContent.replace(placeholder, replacement)
7982
} else {
80-
generatedContent = generatedContent.replace(placeholder, String(value))
83+
const replacementValue = String(value)
84+
const replacement = isJsonTemplate ? escapeForJson(replacementValue) : replacementValue
85+
generatedContent = generatedContent.replace(placeholder, replacement)
8186
}
8287
}
8388

84-
// Replace all template variables
85-
replaceVariable('preferredIde')
8689
replaceVariable('frameworkSelection')
8790
replaceVariable('tooling')
8891
replaceVariable('language')
@@ -108,18 +111,14 @@ export async function POST(
108111
replaceVariable('logging')
109112
replaceVariable('commitStyle')
110113
replaceVariable('prRules')
114+
replaceVariable('outputFile')
111115

112-
// Return the generated content
113116
return NextResponse.json({
114117
content: generatedContent,
115-
fileName: templateConfig.outputFileName
118+
fileName: templateConfig.outputFileName,
116119
})
117-
118120
} catch (error) {
119121
console.error('Error generating file:', error)
120-
return NextResponse.json(
121-
{ error: 'Failed to generate file' },
122-
{ status: 500 }
123-
)
122+
return NextResponse.json({ error: 'Failed to generate file' }, { status: 500 })
124123
}
125124
}

app/layout.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const geistSans = Geist({
1010
subsets: ["latin"],
1111
});
1212

13+
1314
const geistMono = Geist_Mono({
1415
variable: "--font-geist-mono",
1516
subsets: ["latin"],
@@ -55,7 +56,7 @@ export const metadata: Metadata = {
5556
shortcut: "/favicon.ico",
5657
apple: "/apple-touch-icon.png",
5758
},
58-
themeColor: "#ffffff",
59+
themeColor: "#09090b",
5960
};
6061

6162

@@ -67,14 +68,14 @@ export default function RootLayout({
6768
}>) {
6869

6970
return (
70-
<html lang="en" suppressHydrationWarning>
71+
<html lang="en" suppressHydrationWarning className="dark">
7172
<body
7273
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
7374
>
7475
<ThemeProvider
7576
attribute="class"
76-
defaultTheme="system"
77-
enableSystem
77+
defaultTheme="dark"
78+
forcedTheme="dark"
7879
disableTransitionOnChange
7980
>
8081
<Suspense fallback={null}>

app/page.tsx

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,45 @@
11
"use client"
22

3-
import { useState } from "react"
3+
import { useMemo, useState } from "react"
44

55
import { Button } from "@/components/ui/button"
66
import { InstructionsWizard } from "@/components/instructions-wizard"
7-
import HeroIconsRow from "@/components/HeroIconsRow"
8-
import { ThemeToggle } from "@/components/theme-toggle"
9-
import { getHeroIconItems, getHomeMainClasses } from "@/lib/utils"
7+
import { getHomeMainClasses } from "@/lib/utils"
8+
import { getFormatLabel } from "@/lib/wizard-utils"
109
import { ANALYTICS_EVENTS } from "@/lib/analytics-events"
1110
import { track } from "@/lib/mixpanel"
11+
import type { FileOutputConfig } from "@/types/wizard"
1212
import { Github } from "lucide-react"
1313
import Link from "next/link"
1414

1515
import Logo from "./../components/Logo"
16+
import filesData from "@/data/files.json"
1617

1718
export default function Home() {
1819
const [showWizard, setShowWizard] = useState(false)
19-
const heroIcons = getHeroIconItems()
20+
const [selectedFileId, setSelectedFileId] = useState<string | null>(null)
21+
22+
const fileOptions = useMemo(() => {
23+
return (filesData as FileOutputConfig[]).filter((file) => file.enabled !== false)
24+
}, [])
25+
26+
const handleFileCtaClick = (file: FileOutputConfig) => {
27+
setSelectedFileId(file.id)
28+
setShowWizard(true)
29+
track(ANALYTICS_EVENTS.CREATE_INSTRUCTIONS_FILE, {
30+
fileId: file.id,
31+
fileLabel: file.label,
32+
})
33+
}
34+
35+
const handleWizardClose = () => {
36+
setShowWizard(false)
37+
setSelectedFileId(null)
38+
}
2039

2140
return (
2241
<div className="min-h-screen bg-background text-foreground">
2342
{/* Top utility bar */}
24-
<div className="absolute top-4 left-4 z-10">
25-
<ThemeToggle />
26-
</div>
2743
<div className="absolute top-4 right-4 z-10">
2844
<Link href="https://github.com/spivx/devcontext" target="_blank">
2945
<Button variant="outline" size="sm">
@@ -35,8 +51,8 @@ export default function Home() {
3551

3652
{/* Hero Section */}
3753
<main className={getHomeMainClasses(showWizard)}>
38-
{showWizard ? (
39-
<InstructionsWizard onClose={() => setShowWizard(false)} />
54+
{showWizard && selectedFileId ? (
55+
<InstructionsWizard selectedFileId={selectedFileId} onClose={handleWizardClose} />
4056
) : (
4157
<>
4258
<div className="space-y-6">
@@ -53,22 +69,47 @@ export default function Home() {
5369
Turn developer best practices into ready-to-use AI instructions.
5470
</p>
5571

56-
{/* CTA Button */}
57-
<div className="pt-4">
58-
<Button
59-
size="lg"
60-
className="px-8 py-6 text-lg"
61-
onClick={() => {
62-
track(ANALYTICS_EVENTS.CREATE_INSTRUCTIONS_FILE)
63-
setShowWizard(true)
64-
}}
65-
>
66-
Create My Instructions File
67-
</Button>
72+
{/* File type CTAs */}
73+
<div className="pt-6">
74+
<div className="grid gap-4 md:grid-cols-2">
75+
{fileOptions.map((file) => {
76+
const formatLabel = getFormatLabel(file.format)
77+
return (
78+
<button
79+
key={file.id}
80+
type="button"
81+
className="group relative flex w-full flex-col items-start gap-3 rounded-3xl border border-border/60 bg-card/80 p-6 text-left shadow-sm ring-offset-background transition-all hover:-translate-y-1 hover:border-primary/60 hover:shadow-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/80"
82+
onClick={() => handleFileCtaClick(file)}
83+
aria-label={`Create ${file.label}`}
84+
>
85+
<div className="flex items-center gap-2 rounded-full bg-secondary/60 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-secondary-foreground/80">
86+
<span className="inline-flex h-2 w-2 rounded-full bg-primary/70" aria-hidden />
87+
<span>Start with</span>
88+
</div>
89+
<div>
90+
<p className="text-xl font-semibold text-foreground transition-colors group-hover:text-primary">
91+
{file.label}
92+
</p>
93+
{file.filename ? (
94+
<p className="mt-1 text-sm text-muted-foreground">
95+
{file.filename}
96+
</p>
97+
) : null}
98+
</div>
99+
{formatLabel ? (
100+
<span className="inline-flex items-center gap-2 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary">
101+
{formatLabel} format
102+
</span>
103+
) : null}
104+
<span className="pointer-events-none absolute right-6 top-6 text-primary/60 transition-transform group-hover:translate-x-1">
105+
106+
</span>
107+
</button>
108+
)
109+
})}
110+
</div>
68111
</div>
69112
</div>
70-
71-
<HeroIconsRow items={heroIcons} />
72113
</>
73114
)}
74115
</main>

components/HeroIconsRow/HeroIconsRow.tsx

Lines changed: 0 additions & 20 deletions
This file was deleted.

components/HeroIconsRow/index.tsx

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)