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
4 changes: 2 additions & 2 deletions agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
## Core UI Flow
- Entry point at `app/page.tsx` toggles between a marketing hero and the instructions wizard.
- Wizard steps currently cover:
- IDE selection (`data/ides.json`).
- Instructions file selection (`data/files.json`).
- 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:
Expand All @@ -18,7 +18,7 @@
## Data Conventions
- Every answer object may define: `value`, `label`, `icon`, `example`, `infoLines` (derived from `pros`/`cons`), `tags`, `isDefault`, `disabled`, `disabledLabel`, and `docs`.
- JSON files in `data/` supply domain-specific options:
- `ides.json`, `frameworks.json`, `files.json`, `general.json`, `architecture.json`, `performance.json`, `security.json`, `commits.json`.
- `files.json`, `frameworks.json`, `general.json`, `architecture.json`, `performance.json`, `security.json`, `commits.json`.
- Framework-specific questionnaires live in `data/questions/<framework>.json`.
- Newly added `docs` fields should point to authoritative resources and are surfaced in tooltips as external links.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,88 +1,91 @@
import { NextRequest, NextResponse } from 'next/server'
import { readFile } from 'fs/promises'
import path from 'path'

import type { WizardResponses } from '@/types/wizard'
import { getTemplateConfig, type TemplateKey } from '@/lib/template-config'

// Helper function to map output file types to template types
function mapOutputFileToTemplateType(outputFile: string): string {
const mapping: Record<string, string> = {
'instructions-md': 'copilot-instructions',
'agents-md': 'agents',
'cursor-rules': 'cursor-rules',
'json-rules': 'json-rules',
'agents-md': 'agents'
}
return mapping[outputFile] || outputFile

return mapping[outputFile] ?? outputFile
}

export async function POST(
request: NextRequest,
{ params }: { params: { ide: string; framework: string; fileName: string } }
{ params }: { params: { framework: string; fileName: string } },
) {
try {
const { ide, framework, fileName } = params
const body = await request.json()
const responses: WizardResponses = body

// Determine template configuration based on the request
let templateConfig

const frameworkFromPath = framework && !['general', 'none', 'undefined'].includes(framework)
? framework
: undefined

if (ide) {
const templateKeyFromParams: TemplateKey = {
ide,
templateType: mapOutputFileToTemplateType(fileName),
framework: frameworkFromPath
}
templateConfig = getTemplateConfig(templateKeyFromParams)
const { framework, fileName } = params
const responses = (await request.json()) as WizardResponses

const frameworkFromPath =
framework && !['general', 'none', 'undefined'].includes(framework)
? framework
: undefined

const templateKeyFromParams: TemplateKey = {
templateType: mapOutputFileToTemplateType(fileName),
framework: frameworkFromPath,
}

// Check if this is a combination-based request
if (!templateConfig && responses.preferredIde && responses.outputFile) {
const templateKey: TemplateKey = {
ide: responses.preferredIde,
let templateConfig = getTemplateConfig(templateKeyFromParams)

if (!templateConfig && responses.outputFile) {
const templateKeyFromBody: TemplateKey = {
templateType: mapOutputFileToTemplateType(responses.outputFile),
framework: responses.frameworkSelection || undefined
framework: responses.frameworkSelection || undefined,
}
templateConfig = getTemplateConfig(templateKey)

templateConfig = getTemplateConfig(templateKeyFromBody)
}

// Fallback to legacy fileName-based approach
if (!templateConfig) {
templateConfig = getTemplateConfig(fileName)
}

if (!templateConfig) {
return NextResponse.json(
{ error: `Template not found for fileName: ${fileName}` },
{ status: 404 }
{ status: 404 },
)
}

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

// Replace template variables with actual values
let generatedContent = template
const isJsonTemplate = templateConfig.template.toLowerCase().endsWith('.json')

// Helper function to replace template variables gracefully
const replaceVariable = (key: keyof WizardResponses, fallback: string = 'Not specified') => {
const value = responses[key]
const escapeForJson = (value: string) => {
const escaped = JSON.stringify(value)
return escaped.slice(1, -1)
}

const replaceVariable = (key: keyof WizardResponses, fallback = 'Not specified') => {
const placeholder = `{{${key}}}`

if (!generatedContent.includes(placeholder)) {
return
}

const value = responses[key]

if (value === null || value === undefined || value === '') {
generatedContent = generatedContent.replace(placeholder, fallback)
const replacement = isJsonTemplate ? escapeForJson(fallback) : fallback
generatedContent = generatedContent.replace(placeholder, replacement)
} else {
generatedContent = generatedContent.replace(placeholder, String(value))
const replacementValue = String(value)
const replacement = isJsonTemplate ? escapeForJson(replacementValue) : replacementValue
generatedContent = generatedContent.replace(placeholder, replacement)
}
}

// Replace all template variables
replaceVariable('preferredIde')
replaceVariable('frameworkSelection')
replaceVariable('tooling')
replaceVariable('language')
Expand All @@ -108,18 +111,14 @@ export async function POST(
replaceVariable('logging')
replaceVariable('commitStyle')
replaceVariable('prRules')
replaceVariable('outputFile')

// Return the generated content
return NextResponse.json({
content: generatedContent,
fileName: templateConfig.outputFileName
fileName: templateConfig.outputFileName,
})

} catch (error) {
console.error('Error generating file:', error)
return NextResponse.json(
{ error: 'Failed to generate file' },
{ status: 500 }
)
return NextResponse.json({ error: 'Failed to generate file' }, { status: 500 })
}
}
9 changes: 5 additions & 4 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const geistSans = Geist({
subsets: ["latin"],
});


const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
Expand Down Expand Up @@ -55,7 +56,7 @@ export const metadata: Metadata = {
shortcut: "/favicon.ico",
apple: "/apple-touch-icon.png",
},
themeColor: "#ffffff",
themeColor: "#09090b",
};


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

return (
<html lang="en" suppressHydrationWarning>
<html lang="en" suppressHydrationWarning className="dark">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
defaultTheme="dark"
forcedTheme="dark"
disableTransitionOnChange
>
<Suspense fallback={null}>
Expand Down
89 changes: 65 additions & 24 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
"use client"

import { useState } from "react"
import { useMemo, useState } from "react"

import { Button } from "@/components/ui/button"
import { InstructionsWizard } from "@/components/instructions-wizard"
import HeroIconsRow from "@/components/HeroIconsRow"
import { ThemeToggle } from "@/components/theme-toggle"
import { getHeroIconItems, getHomeMainClasses } from "@/lib/utils"
import { getHomeMainClasses } from "@/lib/utils"
import { getFormatLabel } from "@/lib/wizard-utils"
import { ANALYTICS_EVENTS } from "@/lib/analytics-events"
import { track } from "@/lib/mixpanel"
import type { FileOutputConfig } from "@/types/wizard"
import { Github } from "lucide-react"
import Link from "next/link"

import Logo from "./../components/Logo"
import filesData from "@/data/files.json"

export default function Home() {
const [showWizard, setShowWizard] = useState(false)
const heroIcons = getHeroIconItems()
const [selectedFileId, setSelectedFileId] = useState<string | null>(null)

const fileOptions = useMemo(() => {
return (filesData as FileOutputConfig[]).filter((file) => file.enabled !== false)
}, [])

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 (
<div className="min-h-screen bg-background text-foreground">
{/* Top utility bar */}
<div className="absolute top-4 left-4 z-10">
<ThemeToggle />
</div>
<div className="absolute top-4 right-4 z-10">
<Link href="https://github.com/spivx/devcontext" target="_blank">
<Button variant="outline" size="sm">
Expand All @@ -35,8 +51,8 @@ export default function Home() {

{/* Hero Section */}
<main className={getHomeMainClasses(showWizard)}>
{showWizard ? (
<InstructionsWizard onClose={() => setShowWizard(false)} />
{showWizard && selectedFileId ? (
<InstructionsWizard selectedFileId={selectedFileId} onClose={handleWizardClose} />
) : (
<>
<div className="space-y-6">
Expand All @@ -53,22 +69,47 @@ export default function Home() {
Turn developer best practices into ready-to-use AI instructions.
</p>

{/* CTA Button */}
<div className="pt-4">
<Button
size="lg"
className="px-8 py-6 text-lg"
onClick={() => {
track(ANALYTICS_EVENTS.CREATE_INSTRUCTIONS_FILE)
setShowWizard(true)
}}
>
Create My Instructions File
</Button>
{/* File type CTAs */}
<div className="pt-6">
<div className="grid gap-4 md:grid-cols-2">
{fileOptions.map((file) => {
const formatLabel = getFormatLabel(file.format)
return (
<button
key={file.id}
type="button"
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"
onClick={() => handleFileCtaClick(file)}
aria-label={`Create ${file.label}`}
>
<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">
<span className="inline-flex h-2 w-2 rounded-full bg-primary/70" aria-hidden />
<span>Start with</span>
</div>
<div>
<p className="text-xl font-semibold text-foreground transition-colors group-hover:text-primary">
{file.label}
</p>
{file.filename ? (
<p className="mt-1 text-sm text-muted-foreground">
{file.filename}
</p>
) : null}
</div>
{formatLabel ? (
<span className="inline-flex items-center gap-2 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary">
{formatLabel} format
</span>
) : null}
<span className="pointer-events-none absolute right-6 top-6 text-primary/60 transition-transform group-hover:translate-x-1">
</span>
</button>
)
})}
</div>
</div>
</div>

<HeroIconsRow items={heroIcons} />
</>
)}
</main>
Expand Down
20 changes: 0 additions & 20 deletions components/HeroIconsRow/HeroIconsRow.tsx

This file was deleted.

1 change: 0 additions & 1 deletion components/HeroIconsRow/index.tsx

This file was deleted.

Loading
Loading