Updated template features

This commit is contained in:
tejas.prakash 2025-08-20 15:31:05 +05:30
parent 722f878243
commit 81ce1da38b
8 changed files with 424 additions and 5 deletions

42
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "codenuk-frontend",
"version": "0.1.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.57.0",
"@next/font": "^14.2.15",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
@ -25,7 +26,8 @@
"next": "15.4.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwind-merge": "^3.3.1"
"tailwind-merge": "^3.3.1",
"zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@ -67,6 +69,15 @@
"node": ">=6.0.0"
}
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.57.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.57.0.tgz",
"integrity": "sha512-z5LMy0MWu0+w2hflUgj4RlJr1R+0BxKXL7ldXTO8FasU8fu599STghO+QKwId2dAD0d464aHtU+ChWuRHw4FNw==",
"license": "MIT",
"bin": {
"anthropic-ai-sdk": "bin/cli"
}
},
"node_modules/@emnapi/core": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
@ -7296,6 +7307,35 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zustand": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
}
}
}

View File

@ -9,6 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.57.0",
"@next/font": "^14.2.15",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
@ -26,7 +27,8 @@
"next": "15.4.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwind-merge": "^3.3.1"
"tailwind-merge": "^3.3.1",
"zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/eslintrc": "^3",

View File

@ -0,0 +1,36 @@
import { NextResponse } from 'next/server'
// Proxy AI analysis to the Requirement Processor backend
// Expects body: { description: string, project_type?: string, feature_name?: string, requirements?: string[] }
export async function POST(request: Request) {
try {
const body = await request.json()
const requirementProcessorUrl = process.env.REQUIREMENT_PROCESSOR_URL || 'http://localhost:8001'
const resp = await fetch(`${requirementProcessorUrl}/api/v1/analyze-feature`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
description: body.description,
project_type: body.project_type,
feature_name: body.feature_name,
requirements: Array.isArray(body.requirements) ? body.requirements : [],
}),
})
if (!resp.ok) {
const text = await resp.text()
return NextResponse.json({ success: false, message: `Upstream error: ${text}` }, { status: 500 })
}
const data = await resp.json()
return NextResponse.json({ success: true, data })
} catch (error: any) {
return NextResponse.json(
{ success: false, message: error?.message || 'AI analysis failed' },
{ status: 500 }
)
}
}

View File

@ -0,0 +1,222 @@
"use client"
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card } from '@/components/ui/card'
import { analyzeFeatureWithAI } from '@/services/aiAnalysis'
type Complexity = 'low' | 'medium' | 'high'
export interface AIAnalysisResult {
suggested_name?: string
complexity?: Complexity
implementation_details?: string[]
technical_requirements?: string[]
estimated_effort?: string
dependencies?: string[]
api_endpoints?: string[]
database_tables?: string[]
confidence_score?: number
}
export function AICustomFeatureCreator({
projectType,
onAdd,
onClose,
}: {
projectType?: string
onAdd: (feature: { name: string; description: string; complexity: Complexity }) => void
onClose: () => void
}) {
const [featureName, setFeatureName] = useState('')
const [featureDescription, setFeatureDescription] = useState('')
const [isAnalyzing, setIsAnalyzing] = useState(false)
const [aiAnalysis, setAiAnalysis] = useState<AIAnalysisResult | null>(null)
const [analysisError, setAnalysisError] = useState<string | null>(null)
const [requirements, setRequirements] = useState<Array<{ text: string; rules: string[] }>>([
{ text: '', rules: [] },
])
const handleAnalyze = async () => {
if (!featureDescription.trim() && requirements.every(r => !r.text.trim())) return
setIsAnalyzing(true)
setAnalysisError(null)
try {
// 1) Analyze overall description (if provided)
if (featureDescription.trim()) {
const overall = await analyzeFeatureWithAI(
featureName,
featureDescription,
[],
projectType
)
setAiAnalysis({
suggested_name: featureName,
complexity: overall.complexity,
implementation_details: [],
technical_requirements: [],
estimated_effort: 'Medium',
dependencies: [],
api_endpoints: [],
database_tables: [],
confidence_score: 0.8,
})
}
// 2) Analyze each requirement to get logic rules
const updated = [...requirements]
for (let i = 0; i < updated.length; i++) {
const req = updated[i]
if (!req.text.trim()) continue
const perReq = await analyzeFeatureWithAI(featureName, req.text, [], projectType)
const rules: string[] = Array.isArray(perReq.logicRules) ? perReq.logicRules : []
updated[i] = { ...req, rules }
}
setRequirements(updated)
} catch (e: any) {
setAnalysisError(e?.message || 'AI analysis failed')
} finally {
setIsAnalyzing(false)
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!aiAnalysis) {
await handleAnalyze()
return
}
onAdd({
name: aiAnalysis.suggested_name || featureName.trim() || 'Custom Feature',
description: featureDescription.trim(),
complexity: aiAnalysis.complexity || 'medium',
})
onClose()
}
return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<div className="bg-white/5 border border-white/10 rounded-xl max-w-2xl w-full backdrop-blur">
<div className="p-6 space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-white text-lg font-semibold">AI-Powered Feature Creator</h3>
<button onClick={onClose} className="text-white/60 hover:text-white">×</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm text-white/70 mb-1">Feature Name (optional)</label>
<Input value={featureName} onChange={(e) => setFeatureName(e.target.value)} placeholder="e.g., Subscriptions" className="bg-white/10 border-white/20 text-white placeholder:text-white/40" />
</div>
<div>
<label className="block text-sm text-white/70 mb-1">Describe Your Feature Requirements</label>
<textarea value={featureDescription} onChange={(e) => setFeatureDescription(e.target.value)} rows={4} className="w-full bg-white/10 border border-white/20 text-white rounded-md p-3 placeholder:text-white/40" placeholder="Describe what this feature should do..." required />
<p className="text-xs text-white/50 mt-1">Be as detailed as possible. The AI will analyze and break down your requirements.</p>
</div>
{/* Dynamic Requirements List */}
<div className="space-y-2">
<div className="text-white font-medium">Detailed Requirements (Add one by one)</div>
{requirements.map((r, idx) => (
<div key={idx} className="rounded-lg border border-white/10 bg-white/5 p-3 space-y-2">
<div className="flex items-center gap-2">
<div className="text-white/60 text-sm w-6">{idx + 1}</div>
<Input
placeholder="Requirement..."
value={r.text}
onChange={(e) => {
const next = [...requirements]
next[idx] = { ...r, text: e.target.value }
setRequirements(next)
}}
className="bg-white/10 border-white/20 text-white placeholder:text-white/40"
/>
<button
type="button"
onClick={() => setRequirements(requirements.filter((_, i) => i !== idx))}
className="text-white/50 hover:text-red-400"
aria-label="Remove requirement"
>
×
</button>
</div>
{r.rules?.length > 0 && (
<div className="pl-8 space-y-2">
<div className="text-white/70 text-sm">Logic Rules for this requirement:</div>
<div className="space-y-1">
{r.rules.map((rule, ridx) => (
<div key={ridx} className="flex items-center gap-2">
<div className="text-white/50 text-xs w-8">R{ridx + 1}</div>
<Input value={rule} onChange={(e) => {
const next = [...requirements]
const rr = [...(next[idx].rules || [])]
rr[ridx] = e.target.value
next[idx] = { ...next[idx], rules: rr }
setRequirements(next)
}} className="bg-white/10 border-white/20 text-white placeholder:text-white/40" />
<button type="button" onClick={() => {
const next = [...requirements]
const rr = [...(next[idx].rules || [])]
rr.splice(ridx, 1)
next[idx] = { ...next[idx], rules: rr }
setRequirements(next)
}} className="text-white/50 hover:text-red-400">×</button>
</div>
))}
<button type="button" onClick={() => {
const next = [...requirements]
const rr = [...(next[idx].rules || [])]
rr.push('')
next[idx] = { ...next[idx], rules: rr }
setRequirements(next)
}} className="text-xs text-orange-400 hover:text-orange-300">+ Add rule to this requirement</button>
</div>
</div>
)}
</div>
))}
<Button type="button" variant="outline" onClick={() => setRequirements([...requirements, { text: '', rules: [] }])} className="border-white/20 text-white hover:bg-white/10">
+ Add another requirement
</Button>
</div>
{!aiAnalysis && (featureDescription.trim() || requirements.some(r => r.text.trim())) && (
<Button type="button" onClick={handleAnalyze} disabled={isAnalyzing} className="w-full bg-orange-500 hover:bg-orange-400 text-black">
{isAnalyzing ? 'Analyzing with AI…' : 'Analyze with AI'}
</Button>
)}
{analysisError && (
<Card className="p-3 bg-red-500/10 border-red-500/30 text-red-300">{analysisError}</Card>
)}
{aiAnalysis && (
<div className="space-y-2 p-3 bg-emerald-500/10 border border-emerald-500/30 rounded-lg text-white/90">
<div className="font-medium">AI Analysis Complete {(aiAnalysis.confidence_score ? `(${Math.round((aiAnalysis.confidence_score || 0) * 100)}% confidence)` : '')}</div>
<div className="text-sm">Suggested Name: <span className="text-white">{aiAnalysis.suggested_name || featureName || 'Custom Feature'}</span></div>
<div className="text-sm">Complexity: <span className="capitalize text-white">{aiAnalysis.complexity}</span></div>
</div>
)}
{/* Footer */}
<div className="flex gap-3 pt-2 flex-wrap items-center">
{aiAnalysis && (
<div className="flex-1 text-white/80 text-sm">
Overall Complexity: <span className="capitalize">{aiAnalysis.complexity}</span>
</div>
)}
<Button type="button" variant="outline" onClick={onClose} className="border-white/20 text-white hover:bg-white/10">Cancel</Button>
<Button type="submit" disabled={(!featureDescription.trim() && requirements.every(r => !r.text.trim())) || isAnalyzing} className="bg-orange-500 hover:bg-orange-400 text-black">
{aiAnalysis ? 'Add Feature with Tagged Rules' : 'Analyze & Add Feature'}
</Button>
</div>
</form>
</div>
</div>
</div>
)
}
export default AICustomFeatureCreator

View File

@ -12,6 +12,7 @@ import { CustomTemplateForm } from "@/components/custom-template-form"
import { EditTemplateForm } from "@/components/edit-template-form"
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
import { DatabaseTemplate, TemplateFeature } from "@/lib/template-service"
import AICustomFeatureCreator from "@/components/ai/AICustomFeatureCreator"
interface Template {
id: string
@ -446,6 +447,7 @@ function FeatureSelectionStep({
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [newFeature, setNewFeature] = useState({ name: '', description: '', complexity: 'medium' as 'low' | 'medium' | 'high' })
const [showAIModal, setShowAIModal] = useState(false)
const load = async () => {
try {
@ -476,6 +478,18 @@ function FeatureSelectionStep({
await load()
}
const handleAddAIAnalyzed = async (payload: { name: string; description: string; complexity: 'low' | 'medium' | 'high' }) => {
await createFeature(template.id, {
name: payload.name,
description: payload.description,
feature_type: 'custom',
complexity: payload.complexity,
is_default: false,
created_by_user: true,
})
await load()
}
const handleUpdate = async (f: TemplateFeature, updates: Partial<TemplateFeature>) => {
await updateFeature(f.id, { ...updates, isCustom: f.feature_type === 'custom' })
await load()
@ -555,14 +569,25 @@ function FeatureSelectionStep({
<option value="high">high</option>
</select>
</div>
<div className="flex justify-between items-center">
<div className="flex justify-between items-center gap-3 flex-wrap">
<div className="text-white/60 text-sm">Custom features are saved to this template.</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => setShowAIModal(true)} className="border-white/20 text-white hover:bg-white/10">Analyze with AI</Button>
<Button onClick={handleAddCustom} className="bg-orange-500 hover:bg-orange-400 text-black">Add Feature</Button>
</div>
</div>
</div>
{section('Your Custom Features', custom)}
{showAIModal && (
<AICustomFeatureCreator
projectType={template.type || template.title}
onAdd={async (f) => { await handleAddAIAnalyzed(f); setShowAIModal(false) }}
onClose={() => setShowAIModal(false)}
/>
)}
<div className="text-center py-4">
<div className="space-x-4">
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Back</Button>

View File

@ -111,7 +111,10 @@ export function useTemplates() {
return templateService.createFeature(payload)
}
const updateFeature = async (featureId: string, updates: Partial<TemplateFeature>) => {
const updateFeature = async (
featureId: string,
updates: Partial<TemplateFeature> & { isCustom?: boolean }
) => {
// If updates indicate custom, route accordingly
return templateService.updateFeature(featureId, updates)
}

View File

@ -0,0 +1,85 @@
import Anthropic from '@anthropic-ai/sdk'
export type Complexity = 'low' | 'medium' | 'high'
export interface AIAnalysisResponse {
complexity: Complexity
logicRules: string[]
}
// Direct Anthropic client, mirroring web-dashboard/services/aiAnalysis.js (browser usage)
const anthropic = new Anthropic({
apiKey: process.env.NEXT_PUBLIC_ANTHROPIC_API_KEY || '',
dangerouslyAllowBrowser: true,
})
export async function analyzeFeatureWithAI(
featureName: string,
description: string,
requirements: string[],
projectType?: string
): Promise<AIAnalysisResponse> {
try {
const requirementsText = (requirements || [])
.filter((r) => r && r.trim())
.map((r) => `- ${r}`)
.join('\n')
const prompt = `
Analyze this feature and provide complexity assessment and business logic rules:
Feature Name: ${featureName || 'Custom Feature'}
Description: ${description || ''}
Requirements:
${requirementsText}
Based on these requirements, provide a JSON response with:
1. Complexity level (low/medium/high)
2. Business logic rules that should be implemented
Complexity Guidelines:
- LOW: Simple CRUD operations, basic display features
- MEDIUM: Moderate business logic, some validations, basic integrations
- HIGH: Complex business rules, security requirements, external integrations, compliance needs
Return ONLY a JSON object in this format:
{
"complexity": "low|medium|high",
"logicRules": [
"Business rule 1 based on requirements",
"Business rule 2 based on requirements",
"Business rule 3 based on requirements"
]
}`
const message = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1000,
temperature: 0.1,
messages: [{ role: 'user', content: prompt }],
})
const responseText = (message as any).content?.[0]?.text?.trim?.() || ''
const jsonMatch = responseText.match(/\{[\s\S]*\}/)
if (!jsonMatch) throw new Error('Invalid AI response format')
const parsed = JSON.parse(jsonMatch[0])
return {
complexity: (parsed.complexity as Complexity) || 'medium',
logicRules: Array.isArray(parsed.logicRules) ? parsed.logicRules : [],
}
} catch (error) {
// Fallback similar to CRA implementation
return {
complexity: 'medium',
logicRules: [
'Unable to determine specific business rules due to insufficient requirements information',
'Generic logic implementation may be required',
'Basic validation rules should be considered',
],
}
}
}

6
src/types/anthropic.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
declare module '@anthropic-ai/sdk' {
const Anthropic: any;
export default Anthropic;
}