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
|
export const runtime = 'nodejs'
|
||||||
// Expects body: { description: string, project_type?: string, feature_name?: string, requirements?: string[] }
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await req.json()
|
||||||
const requirementProcessorUrl = process.env.REQUIREMENT_PROCESSOR_URL || 'http://localhost:8001'
|
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`, {
|
const apiKey =
|
||||||
method: 'POST',
|
process.env.ANTHROPIC_API_KEY ||
|
||||||
headers: { 'Content-Type': 'application/json' },
|
process.env.REACT_APP_ANTHROPIC_API_KEY ||
|
||||||
body: JSON.stringify({
|
process.env.NEXT_PUBLIC_ANTHROPIC_API_KEY ||
|
||||||
description: body.description,
|
''
|
||||||
project_type: body.project_type,
|
|
||||||
feature_name: body.feature_name,
|
|
||||||
requirements: Array.isArray(body.requirements) ? body.requirements : [],
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!apiKey) {
|
||||||
const text = await resp.text()
|
return NextResponse.json(
|
||||||
return NextResponse.json({ success: false, message: `Upstream error: ${text}` }, { status: 500 })
|
{ success: false, message: 'Missing Anthropic API key in env' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await resp.json()
|
const anthropic = new Anthropic({ apiKey })
|
||||||
return NextResponse.json({ success: true, data })
|
|
||||||
|
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) {
|
} catch (error: any) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: error?.message || 'AI analysis failed' },
|
{ 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[] }>>([
|
const [requirements, setRequirements] = useState<Array<{ text: string; rules: string[] }>>([
|
||||||
{ text: '', rules: [] },
|
{ text: '', rules: [] },
|
||||||
])
|
])
|
||||||
|
const [logicRules, setLogicRules] = useState<string[]>([])
|
||||||
|
|
||||||
const handleAnalyze = async () => {
|
const handleAnalyze = async () => {
|
||||||
if (!featureDescription.trim() && requirements.every(r => !r.text.trim())) return
|
if (!featureDescription.trim() && requirements.every(r => !r.text.trim())) return
|
||||||
setIsAnalyzing(true)
|
setIsAnalyzing(true)
|
||||||
setAnalysisError(null)
|
setAnalysisError(null)
|
||||||
try {
|
try {
|
||||||
// 1) Analyze overall description (if provided)
|
// Aggregate requirements texts for richer context
|
||||||
if (featureDescription.trim()) {
|
const reqTexts = requirements.map(r => r.text).filter(t => t && t.trim())
|
||||||
const overall = await analyzeFeatureWithAI(
|
const overall = await analyzeFeatureWithAI(
|
||||||
featureName,
|
featureName,
|
||||||
featureDescription,
|
featureDescription,
|
||||||
[],
|
reqTexts,
|
||||||
projectType
|
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
|
setAiAnalysis({
|
||||||
const updated = [...requirements]
|
suggested_name: featureName,
|
||||||
for (let i = 0; i < updated.length; i++) {
|
complexity: overall.complexity,
|
||||||
const req = updated[i]
|
implementation_details: [],
|
||||||
if (!req.text.trim()) continue
|
technical_requirements: [],
|
||||||
const perReq = await analyzeFeatureWithAI(featureName, req.text, [], projectType)
|
estimated_effort: 'Medium',
|
||||||
const rules: string[] = Array.isArray(perReq.logicRules) ? perReq.logicRules : []
|
dependencies: [],
|
||||||
updated[i] = { ...req, rules }
|
api_endpoints: [],
|
||||||
}
|
database_tables: [],
|
||||||
setRequirements(updated)
|
confidence_score: 0.9,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Capture dynamic logic rules (editable)
|
||||||
|
setLogicRules(Array.isArray(overall.logicRules) ? overall.logicRules : [])
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setAnalysisError(e?.message || 'AI analysis failed')
|
setAnalysisError(e?.message || 'AI analysis failed')
|
||||||
} finally {
|
} finally {
|
||||||
@ -97,12 +90,14 @@ export function AICustomFeatureCreator({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
<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="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 space-y-4">
|
<div className="p-6 border-b border-white/10">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-white text-lg font-semibold">AI-Powered Feature Creator</h3>
|
<h3 className="text-white text-lg font-semibold">AI-Powered Feature Creator</h3>
|
||||||
<button onClick={onClose} className="text-white/60 hover:text-white">×</button>
|
<button onClick={onClose} className="text-white/60 hover:text-white">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-white/70 mb-1">Feature Name (optional)</label>
|
<label className="block text-sm text-white/70 mb-1">Feature Name (optional)</label>
|
||||||
@ -198,20 +193,46 @@ export function AICustomFeatureCreator({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Footer */}
|
{aiAnalysis && (
|
||||||
<div className="flex gap-3 pt-2 flex-wrap items-center">
|
<div>
|
||||||
{aiAnalysis && (
|
<label className="block text-sm text-white/70 mb-2">Logic Rules (AI-generated, editable)</label>
|
||||||
<div className="flex-1 text-white/80 text-sm">
|
<div className="space-y-2">
|
||||||
Overall Complexity: <span className="capitalize">{aiAnalysis.complexity}</span>
|
{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>
|
</div>
|
||||||
)}
|
<Button type="button" variant="outline" onClick={() => setLogicRules([...logicRules, ''])} className="mt-2 border-white/20 text-white hover:bg-white/10">
|
||||||
<Button type="button" variant="outline" onClick={onClose} className="border-white/20 text-white hover:bg-white/10">Cancel</Button>
|
+ Add logic rule
|
||||||
<Button type="submit" disabled={(!featureDescription.trim() && requirements.every(r => !r.text.trim())) || isAnalyzing} className="bg-orange-500 hover:bg-orange-400 text-black">
|
</Button>
|
||||||
{aiAnalysis ? 'Add Feature with Tagged Rules' : 'Analyze & Add Feature'}
|
</div>
|
||||||
</Button>
|
)}
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
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 { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2 } from "lucide-react"
|
||||||
import { useTemplates } from "@/hooks/useTemplates"
|
import { useTemplates } from "@/hooks/useTemplates"
|
||||||
import { CustomTemplateForm } from "@/components/custom-template-form"
|
import { CustomTemplateForm } from "@/components/custom-template-form"
|
||||||
@ -441,12 +442,13 @@ function FeatureSelectionStep({
|
|||||||
template,
|
template,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
}: { template: Template; onNext: () => void; onBack: () => void }) {
|
}: { template: Template; onNext: (selected: TemplateFeature[]) => void; onBack: () => void }) {
|
||||||
const { fetchFeatures, createFeature, updateFeature, deleteFeature } = useTemplates()
|
const { fetchFeatures, createFeature, updateFeature, deleteFeature } = useTemplates()
|
||||||
const [features, setFeatures] = useState<TemplateFeature[]>([])
|
const [features, setFeatures] = useState<TemplateFeature[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [newFeature, setNewFeature] = useState({ name: '', description: '', complexity: 'medium' as 'low' | 'medium' | 'high' })
|
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 [showAIModal, setShowAIModal] = useState(false)
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
@ -466,7 +468,7 @@ function FeatureSelectionStep({
|
|||||||
|
|
||||||
const handleAddCustom = async () => {
|
const handleAddCustom = async () => {
|
||||||
if (!newFeature.name.trim()) return
|
if (!newFeature.name.trim()) return
|
||||||
await createFeature(template.id, {
|
const created = await createFeature(template.id, {
|
||||||
name: newFeature.name,
|
name: newFeature.name,
|
||||||
description: newFeature.description,
|
description: newFeature.description,
|
||||||
feature_type: 'custom',
|
feature_type: 'custom',
|
||||||
@ -475,6 +477,11 @@ function FeatureSelectionStep({
|
|||||||
created_by_user: true,
|
created_by_user: true,
|
||||||
})
|
})
|
||||||
setNewFeature({ name: '', description: '', complexity: 'medium' })
|
setNewFeature({ name: '', description: '', complexity: 'medium' })
|
||||||
|
setSelectedIds((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (created?.id) next.add(created.id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
await load()
|
await load()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,18 +504,39 @@ function FeatureSelectionStep({
|
|||||||
|
|
||||||
const handleDelete = async (f: TemplateFeature) => {
|
const handleDelete = async (f: TemplateFeature) => {
|
||||||
await deleteFeature(f.id, { isCustom: f.feature_type === 'custom' })
|
await deleteFeature(f.id, { isCustom: f.feature_type === 'custom' })
|
||||||
|
setSelectedIds((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
next.delete(f.id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
await load()
|
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[]) => (
|
const section = (title: string, list: TemplateFeature[]) => (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-white mb-3">{title} ({list.length})</h3>
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{list.map((f) => (
|
{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">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-white flex items-center justify-between">
|
<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' && (
|
{f.feature_type === 'custom' && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button size="sm" variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={async () => {
|
<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="text-center py-4">
|
||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Back</Button>
|
<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>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -602,6 +933,9 @@ function FeatureSelectionStep({
|
|||||||
export function MainDashboard() {
|
export function MainDashboard() {
|
||||||
const [currentStep, setCurrentStep] = useState(1)
|
const [currentStep, setCurrentStep] = useState(1)
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null)
|
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 = [
|
const steps = [
|
||||||
{ id: 1, name: "Project Type", description: "Choose template" },
|
{ id: 1, name: "Project Type", description: "Choose template" },
|
||||||
@ -626,54 +960,34 @@ export function MainDashboard() {
|
|||||||
return selectedTemplate ? (
|
return selectedTemplate ? (
|
||||||
<FeatureSelectionStep
|
<FeatureSelectionStep
|
||||||
template={selectedTemplate}
|
template={selectedTemplate}
|
||||||
onNext={() => setCurrentStep(3)}
|
onNext={(sel) => { setSelectedFeatures(sel); setCurrentStep(3) }}
|
||||||
onBack={() => setCurrentStep(1)}
|
onBack={() => setCurrentStep(1)}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
case 3:
|
case 3:
|
||||||
return (
|
return selectedTemplate ? (
|
||||||
<div className="text-center py-20 bg-white/5">
|
<BusinessQuestionsStep
|
||||||
<h2 className="text-2xl font-bold mb-4 text-white">Business Context Step</h2>
|
template={selectedTemplate}
|
||||||
<p className="text-white/60 mb-8">Coming soon - Define your business requirements and scaling needs</p>
|
selected={selectedFeatures}
|
||||||
<div className="space-x-4">
|
onBack={() => setCurrentStep(2)}
|
||||||
<Button variant="outline" onClick={() => setCurrentStep(2)} className="border-white/20 text-white hover:bg-white/10">
|
onDone={(data, recs) => { setFinalProjectData(data); setTechStackRecommendations(recs); setCurrentStep(4) }}
|
||||||
Back
|
/>
|
||||||
</Button>
|
) : null
|
||||||
<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>
|
|
||||||
)
|
|
||||||
case 4:
|
case 4:
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-20 bg-white/5">
|
<TechStackSummaryStep
|
||||||
<h2 className="text-2xl font-bold mb-4 text-white">Generate Step</h2>
|
recommendations={techStackRecommendations}
|
||||||
<p className="text-white/60 mb-8">Coming soon - Generate your project architecture and code</p>
|
completeData={finalProjectData}
|
||||||
<div className="space-x-4">
|
onBack={() => setCurrentStep(3)}
|
||||||
<Button variant="outline" onClick={() => setCurrentStep(3)} className="border-white/20 text-white hover:bg-white/10">
|
onGenerate={() => setCurrentStep(5)}
|
||||||
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>
|
|
||||||
)
|
)
|
||||||
case 5:
|
case 5:
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-20 bg-white/5">
|
<ArchitectureDesignerStep
|
||||||
<h2 className="text-2xl font-bold mb-4 text-white">Architecture Step</h2>
|
recommendations={techStackRecommendations}
|
||||||
<p className="text-white/60 mb-8">Coming soon - Review architecture and deploy your project</p>
|
onBack={() => setCurrentStep(4)}
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
@ -733,4 +1047,205 @@ export function MainDashboard() {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</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
|
// Features API
|
||||||
async getFeaturesForTemplate(templateId: string): Promise<TemplateFeature[]> {
|
async getFeaturesForTemplate(templateId: string): Promise<TemplateFeature[]> {
|
||||||
// Use merged endpoint to include custom features
|
// 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 {
|
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 {
|
} catch {
|
||||||
// Fallback to default-only if merged endpoint unsupported
|
// 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 type Complexity = 'low' | 'medium' | 'high'
|
||||||
|
|
||||||
export interface AIAnalysisResponse {
|
export interface AIAnalysisResponse {
|
||||||
@ -7,12 +5,6 @@ export interface AIAnalysisResponse {
|
|||||||
logicRules: string[]
|
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(
|
export async function analyzeFeatureWithAI(
|
||||||
featureName: string,
|
featureName: string,
|
||||||
description: string,
|
description: string,
|
||||||
@ -20,57 +12,19 @@ export async function analyzeFeatureWithAI(
|
|||||||
projectType?: string
|
projectType?: string
|
||||||
): Promise<AIAnalysisResponse> {
|
): Promise<AIAnalysisResponse> {
|
||||||
try {
|
try {
|
||||||
const requirementsText = (requirements || [])
|
const res = await fetch('/api/ai/analyze', {
|
||||||
.filter((r) => r && r.trim())
|
method: 'POST',
|
||||||
.map((r) => `- ${r}`)
|
headers: { 'Content-Type': 'application/json' },
|
||||||
.join('\n')
|
body: JSON.stringify({ featureName, description, requirements, projectType }),
|
||||||
|
|
||||||
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 json = await res.json()
|
||||||
const responseText = (message as any).content?.[0]?.text?.trim?.() || ''
|
if (!res.ok || !json.success) {
|
||||||
const jsonMatch = responseText.match(/\{[\s\S]*\}/)
|
throw new Error(json.message || `AI request failed (${res.status})`)
|
||||||
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 data = json.data as AIAnalysisResponse
|
||||||
|
return { complexity: data.complexity, logicRules: data.logicRules }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Fallback similar to CRA implementation
|
// Fallback if the server route fails
|
||||||
return {
|
return {
|
||||||
complexity: 'medium',
|
complexity: 'medium',
|
||||||
logicRules: [
|
logicRules: [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user