Updated codenuk
This commit is contained in:
parent
81ce1da38b
commit
de360a574e
@ -1,30 +1,86 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
|
||||
// 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) {
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const requirementProcessorUrl = process.env.REQUIREMENT_PROCESSOR_URL || 'http://localhost:8001'
|
||||
const body = await req.json()
|
||||
const featureName: string = body.featureName || 'Custom Feature'
|
||||
const description: string = body.description || ''
|
||||
const requirements: string[] = Array.isArray(body.requirements) ? body.requirements : []
|
||||
const projectType: string | undefined = body.projectType
|
||||
|
||||
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 : [],
|
||||
}),
|
||||
})
|
||||
const apiKey =
|
||||
process.env.ANTHROPIC_API_KEY ||
|
||||
process.env.REACT_APP_ANTHROPIC_API_KEY ||
|
||||
process.env.NEXT_PUBLIC_ANTHROPIC_API_KEY ||
|
||||
''
|
||||
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text()
|
||||
return NextResponse.json({ success: false, message: `Upstream error: ${text}` }, { status: 500 })
|
||||
if (!apiKey) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Missing Anthropic API key in env' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await resp.json()
|
||||
return NextResponse.json({ success: true, data })
|
||||
const anthropic = new Anthropic({ apiKey })
|
||||
|
||||
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:
|
||||
|
||||
Project Type: ${projectType || 'Generic'}
|
||||
Feature Name: ${featureName}
|
||||
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 exact 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.2,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
})
|
||||
|
||||
const responseText = (message as any).content?.[0]?.text?.trim?.() || ''
|
||||
const jsonMatch = responseText.match(/\{[\s\S]*\}/)
|
||||
if (!jsonMatch) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Invalid AI response format' },
|
||||
{ status: 502 }
|
||||
)
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0])
|
||||
const complexity = (parsed.complexity as 'low' | 'medium' | 'high') || 'medium'
|
||||
const logicRules = Array.isArray(parsed.logicRules) ? parsed.logicRules : []
|
||||
|
||||
return NextResponse.json({ success: true, data: { complexity, logicRules } })
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: error?.message || 'AI analysis failed' },
|
||||
@ -33,4 +89,3 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -37,43 +37,36 @@ export function AICustomFeatureCreator({
|
||||
const [requirements, setRequirements] = useState<Array<{ text: string; rules: string[] }>>([
|
||||
{ text: '', rules: [] },
|
||||
])
|
||||
const [logicRules, setLogicRules] = useState<string[]>([])
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
// Aggregate requirements texts for richer context
|
||||
const reqTexts = requirements.map(r => r.text).filter(t => t && t.trim())
|
||||
const overall = await analyzeFeatureWithAI(
|
||||
featureName,
|
||||
featureDescription,
|
||||
reqTexts,
|
||||
projectType
|
||||
)
|
||||
|
||||
// 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)
|
||||
setAiAnalysis({
|
||||
suggested_name: featureName,
|
||||
complexity: overall.complexity,
|
||||
implementation_details: [],
|
||||
technical_requirements: [],
|
||||
estimated_effort: 'Medium',
|
||||
dependencies: [],
|
||||
api_endpoints: [],
|
||||
database_tables: [],
|
||||
confidence_score: 0.9,
|
||||
})
|
||||
|
||||
// Capture dynamic logic rules (editable)
|
||||
setLogicRules(Array.isArray(overall.logicRules) ? overall.logicRules : [])
|
||||
} catch (e: any) {
|
||||
setAnalysisError(e?.message || 'AI analysis failed')
|
||||
} finally {
|
||||
@ -97,12 +90,14 @@ export function AICustomFeatureCreator({
|
||||
|
||||
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="bg-white/5 border border-white/10 rounded-xl max-w-4xl w-full max-h-[90vh] backdrop-blur flex flex-col">
|
||||
<div className="p-6 border-b border-white/10">
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1">Feature Name (optional)</label>
|
||||
@ -198,20 +193,46 @@ export function AICustomFeatureCreator({
|
||||
</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>
|
||||
{aiAnalysis && (
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-2">Logic Rules (AI-generated, editable)</label>
|
||||
<div className="space-y-2">
|
||||
{logicRules.map((rule, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<div className="text-white/60 text-xs w-8">R{idx + 1}</div>
|
||||
<Input
|
||||
value={rule}
|
||||
onChange={(e) => {
|
||||
const next = [...logicRules]
|
||||
next[idx] = e.target.value
|
||||
setLogicRules(next)
|
||||
}}
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-white/40"
|
||||
/>
|
||||
<button type="button" onClick={() => setLogicRules(logicRules.filter((_, i) => i !== idx))} className="text-white/50 hover:text-red-400">×</button>
|
||||
</div>
|
||||
))}
|
||||
</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>
|
||||
<Button type="button" variant="outline" onClick={() => setLogicRules([...logicRules, ''])} className="mt-2 border-white/20 text-white hover:bg-white/10">
|
||||
+ Add logic rule
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
<div className="p-6 border-t border-white/10">
|
||||
<div className="flex gap-3 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2 } from "lucide-react"
|
||||
import { useTemplates } from "@/hooks/useTemplates"
|
||||
import { CustomTemplateForm } from "@/components/custom-template-form"
|
||||
@ -441,12 +442,13 @@ function FeatureSelectionStep({
|
||||
template,
|
||||
onNext,
|
||||
onBack,
|
||||
}: { template: Template; onNext: () => void; onBack: () => void }) {
|
||||
}: { template: Template; onNext: (selected: TemplateFeature[]) => void; onBack: () => void }) {
|
||||
const { fetchFeatures, createFeature, updateFeature, deleteFeature } = useTemplates()
|
||||
const [features, setFeatures] = useState<TemplateFeature[]>([])
|
||||
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 [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||
const [showAIModal, setShowAIModal] = useState(false)
|
||||
|
||||
const load = async () => {
|
||||
@ -466,7 +468,7 @@ function FeatureSelectionStep({
|
||||
|
||||
const handleAddCustom = async () => {
|
||||
if (!newFeature.name.trim()) return
|
||||
await createFeature(template.id, {
|
||||
const created = await createFeature(template.id, {
|
||||
name: newFeature.name,
|
||||
description: newFeature.description,
|
||||
feature_type: 'custom',
|
||||
@ -475,6 +477,11 @@ function FeatureSelectionStep({
|
||||
created_by_user: true,
|
||||
})
|
||||
setNewFeature({ name: '', description: '', complexity: 'medium' })
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (created?.id) next.add(created.id)
|
||||
return next
|
||||
})
|
||||
await load()
|
||||
}
|
||||
|
||||
@ -497,18 +504,39 @@ function FeatureSelectionStep({
|
||||
|
||||
const handleDelete = async (f: TemplateFeature) => {
|
||||
await deleteFeature(f.id, { isCustom: f.feature_type === 'custom' })
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.delete(f.id)
|
||||
return next
|
||||
})
|
||||
await load()
|
||||
}
|
||||
|
||||
const toggleSelect = (f: TemplateFeature) => {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(f.id)) next.delete(f.id)
|
||||
else next.add(f.id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const section = (title: string, list: TemplateFeature[]) => (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white mb-3">{title} ({list.length})</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{list.map((f) => (
|
||||
<Card key={f.id} className="bg-white/5 border-white/10">
|
||||
<Card key={f.id} className={`bg-white/5 ${selectedIds.has(f.id) ? 'border-orange-400' : 'border-white/10'}`}>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center justify-between">
|
||||
<span>{f.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={selectedIds.has(f.id)}
|
||||
onCheckedChange={() => toggleSelect(f)}
|
||||
className="border-white/20 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500"
|
||||
/>
|
||||
<span>{f.name}</span>
|
||||
</div>
|
||||
{f.feature_type === 'custom' && (
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={async () => {
|
||||
@ -591,8 +619,311 @@ function FeatureSelectionStep({
|
||||
<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>
|
||||
<Button onClick={onNext} className="bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow">Continue</Button>
|
||||
<Button
|
||||
onClick={() => onNext(features.filter(f => selectedIds.has(f.id)))}
|
||||
disabled={selectedIds.size < 3}
|
||||
className={`bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow ${selectedIds.size < 3 ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-white/60 text-sm mt-2">Select at least 3 features to continue. Selected {selectedIds.size}/3.</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Business Questions Step Component
|
||||
function BusinessQuestionsStep({
|
||||
template,
|
||||
selected,
|
||||
onBack,
|
||||
onDone,
|
||||
}: { template: Template; selected: TemplateFeature[]; onBack: () => void; onDone: (completeData: any, recommendations: any) => void }) {
|
||||
const [businessQuestions, setBusinessQuestions] = useState<string[]>([])
|
||||
const [businessAnswers, setBusinessAnswers] = useState<Record<number, string>>({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
if (selected.length === 0) {
|
||||
setError('No features selected')
|
||||
return
|
||||
}
|
||||
const resp = await fetch('http://localhost:8001/api/v1/generate-comprehensive-business-questions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
allFeatures: selected,
|
||||
projectName: template.title,
|
||||
projectType: template.type || template.category,
|
||||
totalFeatures: selected.length,
|
||||
}),
|
||||
})
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
const data = await resp.json()
|
||||
const qs: string[] = data?.data?.businessQuestions || []
|
||||
setBusinessQuestions(qs)
|
||||
const init: Record<number, string> = {}
|
||||
qs.forEach((_, i) => (init[i] = ''))
|
||||
setBusinessAnswers(init)
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Failed to load questions')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
load()
|
||||
}, [template.id, selected.map(s => s.id).join(',')])
|
||||
|
||||
const answeredCount = Object.values(businessAnswers).filter((a) => a && a.trim()).length
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-24">
|
||||
<div className="text-center text-white/80">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500 mx-auto mb-4"></div>
|
||||
<p>AI is generating comprehensive business questions...</p>
|
||||
<p className="text-white/50 text-sm mt-2">Analyzing {selected.length} features as integrated system</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<div className="max-w-md mx-auto bg-red-500/10 border border-red-500/30 rounded-lg p-6 text-red-300">
|
||||
<div className="font-semibold mb-2">Error Loading Questions</div>
|
||||
<div className="mb-4">{error}</div>
|
||||
<div className="space-x-3">
|
||||
<Button onClick={() => location.reload()} className="bg-red-500 hover:bg-red-400 text-black">Try Again</Button>
|
||||
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Go Back</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setSubmitting(true)
|
||||
const answeredCount = Object.values(businessAnswers).filter((a) => a && a.trim()).length
|
||||
if (answeredCount === 0) return
|
||||
|
||||
const completeData = {
|
||||
projectName: template.title,
|
||||
projectType: template.type || template.category,
|
||||
allFeatures: selected,
|
||||
businessQuestions,
|
||||
businessAnswers,
|
||||
timestamp: new Date().toISOString(),
|
||||
featureName: `${template.title} - Integrated System`,
|
||||
description: `Complete ${template.type || template.category} system with ${selected.length} integrated features`,
|
||||
requirements: (selected as any[]).flatMap((f: any) => f.requirements || []),
|
||||
complexity:
|
||||
(selected as any[]).some((f: any) => f.complexity === 'high')
|
||||
? 'high'
|
||||
: (selected as any[]).some((f: any) => f.complexity === 'medium')
|
||||
? 'medium'
|
||||
: 'low',
|
||||
logicRules: (selected as any[]).flatMap((f: any) => f.logicRules || []),
|
||||
}
|
||||
|
||||
const resp = await fetch('http://localhost:8002/api/v1/select', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(completeData),
|
||||
})
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
const recommendations = await resp.json()
|
||||
onDone(completeData, recommendations)
|
||||
} catch (e) {
|
||||
console.error('Tech stack selection failed', e)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center space-y-1">
|
||||
<h2 className="text-2xl font-bold text-white">Business Context Questions</h2>
|
||||
<p className="text-white/60">Help us refine recommendations by answering these questions.</p>
|
||||
<p className="text-sm text-orange-400">Analyzing {selected.length} integrated features</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{businessQuestions.map((q, i) => (
|
||||
<div key={i} className="bg-white/5 border border-white/10 rounded-lg p-4">
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="bg-orange-500/20 text-orange-300 rounded-full w-6 h-6 flex items-center justify-center text-xs font-semibold">{i + 1}</span>
|
||||
<span>{q}</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={businessAnswers[i] || ''}
|
||||
onChange={(e) => setBusinessAnswers((prev) => ({ ...prev, [i]: e.target.value }))}
|
||||
className="w-full bg-white/10 border border-white/20 text-white rounded-md p-3 placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-orange-400/50"
|
||||
placeholder="Your answer..."
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 border border-white/10 rounded-lg p-4 text-white/80">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div>Questions answered: {answeredCount} of {businessQuestions.length}</div>
|
||||
<div>Completion: {businessQuestions.length ? Math.round((answeredCount / businessQuestions.length) * 100) : 0}%</div>
|
||||
<div>Features analyzing: {selected.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center pt-2">
|
||||
<div className="space-x-4">
|
||||
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Back</Button>
|
||||
<Button
|
||||
disabled={submitting || answeredCount === 0}
|
||||
onClick={handleSubmit}
|
||||
className={`bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow ${submitting || answeredCount === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{submitting ? 'Getting Recommendations...' : 'Generate Technology Recommendations'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Tech Stack Summary Step
|
||||
function TechStackSummaryStep({
|
||||
recommendations,
|
||||
completeData,
|
||||
onBack,
|
||||
onGenerate,
|
||||
}: { recommendations: any; completeData: any; onBack: () => void; onGenerate: () => void }) {
|
||||
const functional = recommendations?.functional_requirements || {}
|
||||
const claude = recommendations?.claude_recommendations || {}
|
||||
const tech = claude?.technology_recommendations || {}
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto space-y-6">
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-3xl font-bold text-white">Technology Stack Recommendations</h2>
|
||||
<p className="text-white/60">AI-powered recommendations for your project</p>
|
||||
</div>
|
||||
|
||||
{functional?.feature_name && (
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Functional Requirements Analysis</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 className="font-semibold text-white/80 mb-2">Core Feature</h4>
|
||||
<div className="bg-blue-500/10 rounded-lg p-4">
|
||||
<div className="font-medium text-blue-200">{functional.feature_name}</div>
|
||||
<div className="text-blue-300 text-sm mt-1">{functional.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-white/80 mb-2">Complexity Level</h4>
|
||||
<div className="bg-purple-500/10 rounded-lg p-4">
|
||||
<span className="inline-block px-3 py-1 rounded-full text-sm font-medium bg-white/10 text-white">
|
||||
{(functional.complexity_level || 'medium').toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Array.isArray(functional.technical_requirements) && functional.technical_requirements.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h4 className="font-semibold text-white/80 mb-3">Technical Requirements</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{functional.technical_requirements.map((req: string, i: number) => (
|
||||
<span key={i} className="bg-emerald-500/10 text-emerald-200 px-3 py-1 rounded-full text-sm">{req}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Array.isArray(functional.business_logic_rules) && functional.business_logic_rules.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h4 className="font-semibold text-white/80 mb-3">Business Logic Rules</h4>
|
||||
<div className="space-y-2">
|
||||
{functional.business_logic_rules.map((rule: string, i: number) => (
|
||||
<div key={i} className="bg-orange-500/10 border-l-4 border-orange-400 p-3 text-orange-200 text-sm">{rule}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-6">AI Technology Recommendations</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{tech?.frontend && (
|
||||
<div className="bg-blue-500/10 rounded-lg p-5">
|
||||
<div className="font-bold text-blue-200 mb-2">Frontend</div>
|
||||
<div className="text-blue-300">Framework: {tech.frontend.framework}</div>
|
||||
{Array.isArray(tech.frontend.libraries) && (
|
||||
<div className="mt-2 text-blue-300 text-sm">Libraries: {tech.frontend.libraries.join(', ')}</div>
|
||||
)}
|
||||
{tech.frontend.reasoning && <div className="mt-2 text-blue-300 text-sm">{tech.frontend.reasoning}</div>}
|
||||
</div>
|
||||
)}
|
||||
{tech?.backend && (
|
||||
<div className="bg-emerald-500/10 rounded-lg p-5">
|
||||
<div className="font-bold text-emerald-200 mb-2">Backend</div>
|
||||
<div className="text-emerald-300">Language: {tech.backend.language}</div>
|
||||
<div className="text-emerald-300">Framework: {tech.backend.framework}</div>
|
||||
{Array.isArray(tech.backend.libraries) && (
|
||||
<div className="mt-2 text-emerald-300 text-sm">Libraries: {tech.backend.libraries.join(', ')}</div>
|
||||
)}
|
||||
{tech.backend.reasoning && <div className="mt-2 text-emerald-300 text-sm">{tech.backend.reasoning}</div>}
|
||||
</div>
|
||||
)}
|
||||
{tech?.database && (
|
||||
<div className="bg-purple-500/10 rounded-lg p-5">
|
||||
<div className="font-bold text-purple-200 mb-2">Database</div>
|
||||
<div className="text-purple-300">Primary: {tech.database.primary}</div>
|
||||
{Array.isArray(tech.database.secondary) && tech.database.secondary.length > 0 && (
|
||||
<div className="mt-2 text-purple-300 text-sm">Secondary: {tech.database.secondary.join(', ')}</div>
|
||||
)}
|
||||
{tech.database.reasoning && <div className="mt-2 text-purple-300 text-sm">{tech.database.reasoning}</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{claude?.implementation_strategy && (
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Implementation Strategy</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-white/80">
|
||||
<div>
|
||||
<div className="font-semibold mb-2">Architecture Pattern</div>
|
||||
<div className="bg-white/10 rounded-lg p-3">{claude.implementation_strategy.architecture_pattern}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold mb-2">Deployment Strategy</div>
|
||||
<div className="bg-white/10 rounded-lg p-3">{claude.implementation_strategy.deployment_strategy}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center py-6">
|
||||
<div className="space-x-4">
|
||||
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Back</Button>
|
||||
<Button onClick={onGenerate} className="bg-gradient-to-r from-indigo-500 to-purple-600 text-white px-6 py-2 rounded-lg font-semibold">Generate Architecture Design →</Button>
|
||||
</div>
|
||||
<div className="text-white/60 text-sm mt-2">AI will design complete architecture</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -602,6 +933,9 @@ function FeatureSelectionStep({
|
||||
export function MainDashboard() {
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null)
|
||||
const [selectedFeatures, setSelectedFeatures] = useState<TemplateFeature[]>([])
|
||||
const [finalProjectData, setFinalProjectData] = useState<any>(null)
|
||||
const [techStackRecommendations, setTechStackRecommendations] = useState<any>(null)
|
||||
|
||||
const steps = [
|
||||
{ id: 1, name: "Project Type", description: "Choose template" },
|
||||
@ -626,54 +960,34 @@ export function MainDashboard() {
|
||||
return selectedTemplate ? (
|
||||
<FeatureSelectionStep
|
||||
template={selectedTemplate}
|
||||
onNext={() => setCurrentStep(3)}
|
||||
onNext={(sel) => { setSelectedFeatures(sel); setCurrentStep(3) }}
|
||||
onBack={() => setCurrentStep(1)}
|
||||
/>
|
||||
) : null
|
||||
case 3:
|
||||
return (
|
||||
<div className="text-center py-20 bg-white/5">
|
||||
<h2 className="text-2xl font-bold mb-4 text-white">Business Context Step</h2>
|
||||
<p className="text-white/60 mb-8">Coming soon - Define your business requirements and scaling needs</p>
|
||||
<div className="space-x-4">
|
||||
<Button variant="outline" onClick={() => setCurrentStep(2)} className="border-white/20 text-white hover:bg-white/10">
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={() => setCurrentStep(4)} className="bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow">
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return selectedTemplate ? (
|
||||
<BusinessQuestionsStep
|
||||
template={selectedTemplate}
|
||||
selected={selectedFeatures}
|
||||
onBack={() => setCurrentStep(2)}
|
||||
onDone={(data, recs) => { setFinalProjectData(data); setTechStackRecommendations(recs); setCurrentStep(4) }}
|
||||
/>
|
||||
) : null
|
||||
case 4:
|
||||
return (
|
||||
<div className="text-center py-20 bg-white/5">
|
||||
<h2 className="text-2xl font-bold mb-4 text-white">Generate Step</h2>
|
||||
<p className="text-white/60 mb-8">Coming soon - Generate your project architecture and code</p>
|
||||
<div className="space-x-4">
|
||||
<Button variant="outline" onClick={() => setCurrentStep(3)} className="border-white/20 text-white hover:bg-white/10">
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={() => setCurrentStep(5)} className="bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow">
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TechStackSummaryStep
|
||||
recommendations={techStackRecommendations}
|
||||
completeData={finalProjectData}
|
||||
onBack={() => setCurrentStep(3)}
|
||||
onGenerate={() => setCurrentStep(5)}
|
||||
/>
|
||||
)
|
||||
case 5:
|
||||
return (
|
||||
<div className="text-center py-20 bg-white/5">
|
||||
<h2 className="text-2xl font-bold mb-4 text-white">Architecture Step</h2>
|
||||
<p className="text-white/60 mb-8">Coming soon - Review architecture and deploy your project</p>
|
||||
<div className="space-x-4">
|
||||
<Button variant="outline" onClick={() => setCurrentStep(4)} className="border-white/20 text-white hover:bg-white/10">
|
||||
Back
|
||||
</Button>
|
||||
<Button className="bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow">
|
||||
Deploy Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ArchitectureDesignerStep
|
||||
recommendations={techStackRecommendations}
|
||||
onBack={() => setCurrentStep(4)}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
@ -734,3 +1048,204 @@ export function MainDashboard() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ArchitectureDesignerStep({ recommendations, onBack }: { recommendations: any; onBack: () => void }) {
|
||||
const [architectureDesign, setArchitectureDesign] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'frontend' | 'backend' | 'database' | 'deployment'>('overview')
|
||||
|
||||
useEffect(() => {
|
||||
const generate = async () => {
|
||||
if (!recommendations) {
|
||||
setError('Missing technology recommendations')
|
||||
return
|
||||
}
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const resp = await fetch('http://localhost:8003/api/v1/design-architecture', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tech_stack_recommendations: recommendations }),
|
||||
})
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
const data = await resp.json()
|
||||
setArchitectureDesign(data)
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Failed to generate architecture')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
generate()
|
||||
}, [JSON.stringify(recommendations)])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-24">
|
||||
<div className="text-center text-white/80">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500 mx-auto mb-4"></div>
|
||||
<div className="font-semibold mb-1">AI Designing Your Architecture</div>
|
||||
<div className="text-white/60 text-sm">Creating React components, Node.js APIs, and PostgreSQL schema…</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<div className="max-w-md mx-auto bg-red-500/10 border border-red-500/30 rounded-lg p-6 text-red-300">
|
||||
<div className="font-semibold mb-2">Architecture Generation Failed</div>
|
||||
<div className="mb-4">{error}</div>
|
||||
<div className="space-x-3">
|
||||
<Button onClick={() => location.reload()} className="bg-red-500 hover:bg-red-400 text-black">Try Again</Button>
|
||||
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Back</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!architectureDesign) {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<div className="max-w-md mx-auto bg-white/5 border border-white/10 rounded-lg p-6 text-white/80">
|
||||
<div className="font-semibold mb-2">No Architecture Data</div>
|
||||
<div className="mb-4">Please complete the tech stack selection first.</div>
|
||||
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Go Back</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const projectMeta = architectureDesign?.project_metadata
|
||||
const arch = architectureDesign?.architecture_design
|
||||
const tech = architectureDesign?.technology_specifications
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white/5 border-b border-white/10">
|
||||
<div className="max-w-7xl mx-auto px-4 py-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">Architecture Design</h1>
|
||||
<p className="text-white/60">AI-generated architecture for <span className="text-orange-400 font-semibold">{projectMeta?.project_name}</span></p>
|
||||
</div>
|
||||
<div className="space-x-3">
|
||||
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Back</Button>
|
||||
<Button onClick={() => location.reload()} className="bg-orange-500 hover:bg-orange-400 text-black">Regenerate</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="border-b border-white/10 mb-6">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
{[
|
||||
{ id: 'overview', name: 'Overview' },
|
||||
{ id: 'frontend', name: 'Frontend (React)' },
|
||||
{ id: 'backend', name: 'Backend (Node.js)' },
|
||||
{ id: 'database', name: 'Database (PostgreSQL)' },
|
||||
{ id: 'deployment', name: 'Deployment' },
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as any)}
|
||||
className={`py-2 px-1 border-b-2 text-sm transition-colors ${
|
||||
activeTab === (tab.id as any)
|
||||
? 'border-orange-400 text-orange-400'
|
||||
: 'border-transparent text-white/60 hover:text-white/80 hover:border-white/20'
|
||||
}`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Project Overview</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-white/80">
|
||||
<div className="bg-white/5 rounded-lg p-4">
|
||||
<div className="font-semibold">Project Name</div>
|
||||
<div>{projectMeta?.project_name}</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-4">
|
||||
<div className="font-semibold">Complexity</div>
|
||||
<div className="capitalize">{projectMeta?.complexity}</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-4">
|
||||
<div className="font-semibold">Generated</div>
|
||||
<div>{projectMeta?.architecture_generated_at ? new Date(projectMeta.architecture_generated_at).toLocaleDateString() : '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Technology Stack</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-white/80">
|
||||
<div className="bg-white/5 rounded-lg p-4">
|
||||
<div className="font-semibold">Frontend</div>
|
||||
<div>{tech?.frontend_framework}</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-4">
|
||||
<div className="font-semibold">Backend</div>
|
||||
<div>{tech?.backend_language}</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-4">
|
||||
<div className="font-semibold">Database</div>
|
||||
<div>{tech?.database_system}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'frontend' && (
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">React Architecture</h3>
|
||||
<pre className="bg-black/30 rounded-lg p-4 overflow-x-auto text-sm text-white/80">{JSON.stringify(arch?.frontend_architecture, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'backend' && (
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Node.js Architecture</h3>
|
||||
<pre className="bg-black/30 rounded-lg p-4 overflow-x-auto text-sm text-white/80">{JSON.stringify(arch?.backend_architecture, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'database' && (
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">PostgreSQL Architecture</h3>
|
||||
<pre className="bg-black/30 rounded-lg p-4 overflow-x-auto text-sm text-white/80">{JSON.stringify(arch?.database_architecture, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'deployment' && (
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-6 text-white/80">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Deployment Configuration</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="bg-white/5 rounded-lg p-4">
|
||||
<div className="font-semibold mb-2">Frontend Deployment</div>
|
||||
<ul className="text-sm space-y-1">
|
||||
<li>• Vercel/Netlify hosting</li>
|
||||
<li>• React build optimization</li>
|
||||
<li>• CDN distribution</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-4">
|
||||
<div className="font-semibold mb-2">Backend Deployment</div>
|
||||
<ul className="text-sm space-y-1">
|
||||
<li>• Docker containerization</li>
|
||||
<li>• AWS/GCP hosting</li>
|
||||
<li>• Auto-scaling setup</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -112,11 +112,49 @@ class TemplateService {
|
||||
// Features API
|
||||
async getFeaturesForTemplate(templateId: string): Promise<TemplateFeature[]> {
|
||||
// Use merged endpoint to include custom features
|
||||
const dedupe = (items: TemplateFeature[]) => {
|
||||
const byKey = new Map<string, TemplateFeature>()
|
||||
|
||||
const toKey = (f: TemplateFeature) => {
|
||||
const normName = (f.name || '').trim().toLowerCase()
|
||||
// For custom features, dedupe by normalized name; for others, prefer feature_id
|
||||
if (f.feature_type === 'custom') return `custom:${normName}`
|
||||
return `std:${f.feature_id || normName}`
|
||||
}
|
||||
|
||||
const prefer = (a: TemplateFeature, b: TemplateFeature): TemplateFeature => {
|
||||
// Prefer user-created, then higher usage_count, then newer updated_at
|
||||
const aUser = !!a.created_by_user
|
||||
const bUser = !!b.created_by_user
|
||||
if (aUser !== bUser) return aUser ? a : b
|
||||
const aUsage = typeof a.usage_count === 'number' ? a.usage_count : -1
|
||||
const bUsage = typeof b.usage_count === 'number' ? b.usage_count : -1
|
||||
if (aUsage !== bUsage) return aUsage > bUsage ? a : b
|
||||
const aTime = a.updated_at ? Date.parse(a.updated_at) : 0
|
||||
const bTime = b.updated_at ? Date.parse(b.updated_at) : 0
|
||||
return aTime >= bTime ? a : b
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const key = toKey(item)
|
||||
const existing = byKey.get(key)
|
||||
if (!existing) {
|
||||
byKey.set(key, item)
|
||||
} else {
|
||||
byKey.set(key, prefer(existing, item))
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(byKey.values())
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.makeRequest<TemplateFeature[]>(`/api/features/templates/${templateId}/merged`)
|
||||
const merged = await this.makeRequest<TemplateFeature[]>(`/api/features/templates/${templateId}/merged`)
|
||||
return dedupe(merged)
|
||||
} catch {
|
||||
// Fallback to default-only if merged endpoint unsupported
|
||||
return this.makeRequest<TemplateFeature[]>(`/api/templates/${templateId}/features`)
|
||||
const defaults = await this.makeRequest<TemplateFeature[]>(`/api/templates/${templateId}/features`)
|
||||
return dedupe(defaults)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
|
||||
export type Complexity = 'low' | 'medium' | 'high'
|
||||
|
||||
export interface AIAnalysisResponse {
|
||||
@ -7,12 +5,6 @@ export interface AIAnalysisResponse {
|
||||
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,
|
||||
@ -20,57 +12,19 @@ export async function analyzeFeatureWithAI(
|
||||
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 res = await fetch('/api/ai/analyze', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ featureName, description, requirements, projectType }),
|
||||
})
|
||||
|
||||
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 : [],
|
||||
const json = await res.json()
|
||||
if (!res.ok || !json.success) {
|
||||
throw new Error(json.message || `AI request failed (${res.status})`)
|
||||
}
|
||||
const data = json.data as AIAnalysisResponse
|
||||
return { complexity: data.complexity, logicRules: data.logicRules }
|
||||
} catch (error) {
|
||||
// Fallback similar to CRA implementation
|
||||
// Fallback if the server route fails
|
||||
return {
|
||||
complexity: 'medium',
|
||||
logicRules: [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user