364 lines
16 KiB
TypeScript
364 lines
16 KiB
TypeScript
"use client"
|
||
|
||
import { useState } from 'react'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Input } from '@/components/ui/input'
|
||
import { Card } from '@/components/ui/card'
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select"
|
||
import { analyzeFeatureWithAI } from '@/services/aiAnalysis'
|
||
|
||
type Complexity = 'low' | 'medium' | 'high'
|
||
|
||
export interface AIAnalysisResult {
|
||
suggested_name?: string
|
||
complexity?: Complexity
|
||
implementation_details?: string[]
|
||
technical_requirements?: string[]
|
||
estimated_effort?: string
|
||
dependencies?: string[]
|
||
api_endpoints?: string[]
|
||
database_tables?: string[]
|
||
confidence_score?: number
|
||
}
|
||
|
||
export function AICustomFeatureCreator({
|
||
projectType,
|
||
onAdd,
|
||
onClose,
|
||
editingFeature,
|
||
}: {
|
||
projectType?: string
|
||
onAdd: (feature: { name: string; description: string; complexity: Complexity; logic_rules?: string[]; requirements?: Array<{ text: string; rules: string[] }>; business_rules?: Array<{ requirement: string; rules: string[] }> }) => void
|
||
onClose: () => void
|
||
editingFeature?: {
|
||
id: string;
|
||
name: string;
|
||
description: string;
|
||
complexity: Complexity;
|
||
business_rules?: any;
|
||
technical_requirements?: any;
|
||
additional_business_rules?: any;
|
||
}
|
||
}) {
|
||
const [featureName, setFeatureName] = useState(editingFeature?.name || '')
|
||
const [featureDescription, setFeatureDescription] = useState(editingFeature?.description || '')
|
||
const [selectedComplexity, setSelectedComplexity] = useState<Complexity | undefined>(editingFeature?.complexity || undefined)
|
||
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||
const [aiAnalysis, setAiAnalysis] = useState<AIAnalysisResult | null>(() => {
|
||
if (editingFeature) {
|
||
return {
|
||
suggested_name: editingFeature.name,
|
||
complexity: editingFeature.complexity,
|
||
confidence_score: 1,
|
||
}
|
||
}
|
||
return null
|
||
})
|
||
const [analysisError, setAnalysisError] = useState<string | null>(null)
|
||
const [requirements, setRequirements] = useState<Array<{ text: string; rules: string[] }>>(() => {
|
||
// Initialize requirements from existing feature data
|
||
if (editingFeature) {
|
||
console.log('🔍 Editing feature data:', editingFeature)
|
||
try {
|
||
// Try to get business rules from multiple sources
|
||
let businessRules = null;
|
||
|
||
// First try direct business_rules field
|
||
if (editingFeature.business_rules) {
|
||
console.log('📋 Found business_rules field:', editingFeature.business_rules)
|
||
businessRules = Array.isArray(editingFeature.business_rules)
|
||
? editingFeature.business_rules
|
||
: (typeof editingFeature.business_rules === 'string' ? JSON.parse(editingFeature.business_rules) : editingFeature.business_rules)
|
||
}
|
||
// Then try additional_business_rules from feature_business_rules table
|
||
else if ((editingFeature as any).additional_business_rules) {
|
||
console.log('📋 Found additional_business_rules field:', (editingFeature as any).additional_business_rules)
|
||
businessRules = Array.isArray((editingFeature as any).additional_business_rules)
|
||
? (editingFeature as any).additional_business_rules
|
||
: (typeof (editingFeature as any).additional_business_rules === 'string' ? JSON.parse((editingFeature as any).additional_business_rules) : (editingFeature as any).additional_business_rules)
|
||
}
|
||
// Also try technical_requirements field
|
||
else if ((editingFeature as any).technical_requirements) {
|
||
console.log('📋 Found technical_requirements field:', (editingFeature as any).technical_requirements)
|
||
const techReqs = Array.isArray((editingFeature as any).technical_requirements)
|
||
? (editingFeature as any).technical_requirements
|
||
: (typeof (editingFeature as any).technical_requirements === 'string' ? JSON.parse((editingFeature as any).technical_requirements) : (editingFeature as any).technical_requirements)
|
||
|
||
// Convert technical requirements to business rules format
|
||
if (Array.isArray(techReqs)) {
|
||
businessRules = techReqs.map((req: string, index: number) => ({
|
||
requirement: `Requirement ${index + 1}`,
|
||
rules: [req]
|
||
}))
|
||
}
|
||
}
|
||
|
||
console.log('📋 Parsed business rules:', businessRules)
|
||
|
||
if (businessRules && Array.isArray(businessRules) && businessRules.length > 0) {
|
||
const requirements = businessRules.map((rule: any) => ({
|
||
text: rule.requirement || rule.text || rule.name || `Requirement`,
|
||
rules: Array.isArray(rule.rules) ? rule.rules : (rule.rules ? [rule.rules] : [])
|
||
}))
|
||
console.log('📋 Mapped requirements:', requirements)
|
||
return requirements.length > 0 ? requirements : [{ text: '', rules: [] }]
|
||
}
|
||
} catch (error) {
|
||
console.error('Error parsing business rules:', error)
|
||
}
|
||
}
|
||
return [{ text: '', rules: [] }]
|
||
})
|
||
const [analyzingIdx, setAnalyzingIdx] = useState<number | null>(null)
|
||
const hasAnyAnalysis = !!aiAnalysis || requirements.some(r => (r.rules || []).length > 0)
|
||
|
||
const handleAnalyze = async () => {
|
||
if (hasAnyAnalysis) return
|
||
if (!featureDescription.trim() && requirements.every(r => !r.text.trim())) return
|
||
setIsAnalyzing(true)
|
||
setAnalysisError(null)
|
||
try {
|
||
// 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
|
||
)
|
||
|
||
setAiAnalysis({
|
||
suggested_name: featureName,
|
||
complexity: overall.complexity, // Using the complexity from the API response
|
||
implementation_details: [],
|
||
technical_requirements: [],
|
||
estimated_effort: overall.complexity === 'high' ? 'High' : overall.complexity === 'low' ? 'Low' : 'Medium',
|
||
dependencies: [],
|
||
api_endpoints: [],
|
||
database_tables: [],
|
||
confidence_score: 0.9,
|
||
})
|
||
|
||
|
||
// Generate logic rules per requirement in parallel and attach to each requirement
|
||
const perRequirementRules = await Promise.all(
|
||
requirements.map(async (r) => {
|
||
try {
|
||
const res = await analyzeFeatureWithAI(
|
||
featureName,
|
||
featureDescription,
|
||
r.text ? [r.text] : [],
|
||
projectType
|
||
)
|
||
return Array.isArray(res?.logicRules) ? res.logicRules : []
|
||
} catch {
|
||
return []
|
||
}
|
||
})
|
||
)
|
||
setRequirements((prev) => prev.map((r, idx) => ({ ...r, rules: perRequirementRules[idx] || [] })))
|
||
} catch (e: any) {
|
||
setAnalysisError(e?.message || 'AI analysis failed')
|
||
} finally {
|
||
setIsAnalyzing(false)
|
||
}
|
||
}
|
||
|
||
const handleAnalyzeRequirement = async (idx: number) => {
|
||
const req = requirements[idx]
|
||
if (hasAnyAnalysis) return
|
||
if (!req?.text?.trim()) return
|
||
if ((req.rules || []).length > 0) return
|
||
setAnalyzingIdx(idx)
|
||
setAnalysisError(null)
|
||
try {
|
||
const res = await analyzeFeatureWithAI(
|
||
featureName,
|
||
featureDescription,
|
||
[req.text],
|
||
projectType
|
||
)
|
||
const rules = Array.isArray(res?.logicRules) ? res.logicRules : []
|
||
setRequirements(prev => {
|
||
const next = [...prev]
|
||
next[idx] = { ...next[idx], rules }
|
||
return next
|
||
})
|
||
if (!aiAnalysis) {
|
||
setAiAnalysis({
|
||
suggested_name: featureName,
|
||
complexity: res?.complexity || 'medium',
|
||
implementation_details: [],
|
||
technical_requirements: [],
|
||
estimated_effort: res?.complexity === 'high' ? 'High' : res?.complexity === 'low' ? 'Low' : 'Medium',
|
||
dependencies: [],
|
||
api_endpoints: [],
|
||
database_tables: [],
|
||
confidence_score: 0.9,
|
||
})
|
||
}
|
||
} catch (e: any) {
|
||
setAnalysisError(e?.message || 'AI analysis failed')
|
||
} finally {
|
||
setAnalyzingIdx(null)
|
||
}
|
||
}
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
onAdd({
|
||
name: aiAnalysis?.suggested_name || featureName.trim() || 'Custom Feature',
|
||
description: featureDescription.trim(),
|
||
complexity: aiAnalysis?.complexity || selectedComplexity || 'medium',
|
||
logic_rules: requirements.flatMap(r => r.rules || []),
|
||
requirements: requirements,
|
||
business_rules: requirements.map(r => ({ requirement: r.text, rules: r.rules || [] })),
|
||
})
|
||
onClose()
|
||
}
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white/5 border border-white/10 rounded-xl max-w-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">
|
||
{editingFeature ? 'Edit Custom Feature' : '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</label>
|
||
<Input value={featureName} onChange={(e) => setFeatureName(e.target.value)} placeholder="e.g., Subscriptions" className="bg-white/10 border-white/20 text-white placeholder:text-white/40" />
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm text-white/70 mb-1">Describe Your Feature Requirements</label>
|
||
<textarea value={featureDescription} onChange={(e) => setFeatureDescription(e.target.value)} rows={4} className="w-full bg-white/10 border border-white/20 text-white rounded-md p-3 placeholder:text-white/40" placeholder="Describe what this feature should do..." required />
|
||
<p className="text-xs text-white/50 mt-1">Be as detailed as possible. The AI will analyze and break down your requirements.</p>
|
||
</div>
|
||
|
||
{/* Complexity is determined by AI; manual selection removed */}
|
||
|
||
{/* Dynamic Requirements List */}
|
||
<div className="space-y-2">
|
||
<div className="text-white font-medium">Detailed Requirements (Add one by one)</div>
|
||
{requirements.map((r, idx) => (
|
||
<div key={idx} className="rounded-lg border border-white/10 bg-white/5 p-3 space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
<div className="text-white/60 text-sm w-6">{idx + 1}</div>
|
||
<Input
|
||
placeholder="Requirement..."
|
||
value={r.text}
|
||
onChange={(e) => {
|
||
const next = [...requirements]
|
||
next[idx] = { ...r, text: e.target.value }
|
||
setRequirements(next)
|
||
}}
|
||
className="bg-white/10 border-white/20 text-white placeholder:text-white/40"
|
||
/>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() => handleAnalyzeRequirement(idx)}
|
||
disabled={isAnalyzing || analyzingIdx === idx || !r.text.trim() || hasAnyAnalysis || (r.rules || []).length > 0}
|
||
className="border-white/20 text-white hover:bg-white/10"
|
||
>
|
||
{analyzingIdx === idx ? 'Analyzing…' : (((r.rules || []).length > 0) || hasAnyAnalysis ? 'Analyzed' : 'Analyze With AI')}
|
||
</Button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setRequirements(requirements.filter((_, i) => i !== idx))}
|
||
className="text-white/50 hover:text-red-400"
|
||
aria-label="Remove requirement"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<div className="pl-8 space-y-2">
|
||
<div className="text-white/70 text-sm">Logic Rules for this requirement:</div>
|
||
<div className="space-y-1">
|
||
{(r.rules || []).length === 0 && (
|
||
<div className="text-white/40 text-xs">No rules yet. Click Analyze to generate.</div>
|
||
)}
|
||
{(r.rules || []).map((rule, ridx) => (
|
||
<div key={ridx} className="flex items-center gap-2">
|
||
<div className="text-white/50 text-xs w-8">R{ridx + 1}</div>
|
||
<Input value={rule} onChange={(e) => {
|
||
const next = [...requirements]
|
||
const rr = [...(next[idx].rules || [])]
|
||
rr[ridx] = e.target.value
|
||
next[idx] = { ...next[idx], rules: rr }
|
||
setRequirements(next)
|
||
}} className="bg-white/10 border-white/20 text-white placeholder:text-white/40" />
|
||
<button type="button" onClick={() => {
|
||
const next = [...requirements]
|
||
const rr = [...(next[idx].rules || [])]
|
||
rr.splice(ridx, 1)
|
||
next[idx] = { ...next[idx], rules: rr }
|
||
setRequirements(next)
|
||
}} className="text-white/50 hover:text-red-400">×</button>
|
||
</div>
|
||
))}
|
||
<button type="button" onClick={() => {
|
||
const next = [...requirements]
|
||
const rr = [...(next[idx].rules || [])]
|
||
rr.push('')
|
||
next[idx] = { ...next[idx], rules: rr }
|
||
setRequirements(next)
|
||
}} className="text-xs text-orange-400 hover:text-orange-300">+ Add rule to this requirement</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
<Button type="button" variant="outline" onClick={() => setRequirements([...requirements, { text: '', rules: [] }])} className="border-white/20 text-white hover:bg-white/10">
|
||
+ Add another requirement
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Removed global Analyze button; use per-requirement Analyze instead */}
|
||
|
||
{analysisError && (
|
||
<Card className="p-3 bg-red-500/10 border-red-500/30 text-red-300">{analysisError}</Card>
|
||
)}
|
||
|
||
{aiAnalysis && (
|
||
<div className="space-y-2 p-3 bg-emerald-500/10 border border-emerald-500/30 rounded-lg text-white/90">
|
||
<div className="font-medium">AI Analysis Complete {(aiAnalysis.confidence_score ? `(${Math.round((aiAnalysis.confidence_score || 0) * 100)}% confidence)` : '')}</div>
|
||
<div className="text-sm">Suggested Name: <span className="text-white">{aiAnalysis.suggested_name || featureName || 'Custom Feature'}</span></div>
|
||
<div className="text-sm">Complexity: <span className="capitalize text-white">{aiAnalysis.complexity}</span></div>
|
||
</div>
|
||
)}
|
||
|
||
|
||
{/* Form Actions */}
|
||
<div className="flex gap-3 flex-wrap items-center pt-4 border-t border-white/10">
|
||
{aiAnalysis && (
|
||
<div className="flex-1 text-white/80 text-sm">
|
||
Complexity (AI): <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">
|
||
{editingFeature
|
||
? (aiAnalysis ? 'Update Feature with Tagged Rules' : 'Update Feature')
|
||
: (aiAnalysis ? 'Add Feature with Tagged Rules' : 'Add Feature')
|
||
}
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default AICustomFeatureCreator |