frontend features rules generation changes

This commit is contained in:
Chandini 2025-09-10 17:21:09 +05:30
parent a21785972d
commit d57e20e9d8
7 changed files with 288 additions and 65 deletions

View File

@ -0,0 +1,132 @@
"use client"
import React from "react"
type DynamicSvgProps = {
svg?: string | null
// If provided, component will fetch the SVG string from this URL
src?: string
// Width/height can be CSS lengths or numbers (treated as px)
width?: number | string
height?: number | string
className?: string
// Optional role/title for accessibility
role?: string
title?: string
// If true, preserves viewBox scaling inside a wrapper
preserveAspectRatio?: boolean
}
function normalizeSize(value?: number | string): string | undefined {
if (value === undefined) return undefined
return typeof value === "number" ? `${value}px` : value
}
// Minimal sanitizer: strips script/style tags and event handlers
function sanitizeSvg(svg: string): string {
const withoutScripts = svg.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "")
const withoutStyles = withoutScripts.replace(/<style[\s\S]*?>[\s\S]*?<\/style>/gi, "")
// Remove on* attributes (onload, onclick, etc.)
const withoutEvents = withoutStyles.replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "")
// Remove javascript: URLs
const withoutJsUrls = withoutEvents.replace(/(href|xlink:href|src)\s*=\s*"javascript:[^"]*"/gi, "$1=\"\"")
return withoutJsUrls
}
export function DynamicSvg({
svg,
src,
width,
height,
className,
role = "img",
title,
preserveAspectRatio = true,
}: DynamicSvgProps) {
const [content, setContent] = React.useState<string | null>(svg ?? null)
const [error, setError] = React.useState<string | null>(null)
const w = normalizeSize(width)
const h = normalizeSize(height)
React.useEffect(() => {
setContent(svg ?? null)
}, [svg])
React.useEffect(() => {
if (!src) return
let isCancelled = false
;(async () => {
try {
setError(null)
const res = await fetch(src, { credentials: "include" })
const text = await res.text()
if (isCancelled) return
// Try to pull out raw SVG if server wraps it in JSON
let raw = text
if (!text.trim().startsWith("<svg")) {
try {
const json = JSON.parse(text)
raw = (json?.svg || json?.data?.svg || "").toString()
} catch {
// fallback to original text
}
}
if (!raw || !raw.includes("<svg")) {
throw new Error("No SVG content in response")
}
setContent(raw)
} catch (e) {
if (isCancelled) return
setError(e instanceof Error ? e.message : "Failed to load SVG")
}
})()
return () => {
isCancelled = true
}
}, [src])
if (error) {
return <div className={className} role="img" aria-label={title || "SVG error"}>Failed to render SVG: {error}</div>
}
if (!content) {
return <div className={className} role="img" aria-label={title || "Loading SVG"} />
}
const sanitized = sanitizeSvg(content)
// Wrap raw SVG in a container to control layout dimensions
const style: React.CSSProperties = {
width: w,
height: h,
display: "inline-block",
}
// When preserving aspect ratio, let inner SVG own its viewBox sizing
// Otherwise, we force width/height via a wrapper CSS
return (
<div className={className} style={style} role={role} aria-label={title} title={title}>
<div
style={{ width: "100%", height: "100%" }}
dangerouslySetInnerHTML={{
__html: preserveAspectRatio
? sanitized
: sanitized.replace(
/<svg(\s[^>]*)?>/i,
(match) =>
match.includes("width=") || match.includes("height=")
? match
: match.replace(
/<svg/i,
'<svg width="100%" height="100%"'
)
),
}}
/>
</div>
)
}
export default DynamicSvg

View File

@ -137,7 +137,7 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
}
}
const handleAddAIAnalyzed = async (payload: { name: string; description: string; complexity: 'low' | 'medium' | 'high'; logic_rules?: string[] }) => {
const handleAddAIAnalyzed = async (payload: { name: string; description: string; complexity: 'low' | 'medium' | 'high'; logic_rules?: string[]; requirements?: Array<{ text: string; rules: string[] }>; business_rules?: Array<{ requirement: string; rules: string[] }> }) => {
// Check if template ID is valid UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
if (!uuidRegex.test(template.id)) {
@ -153,7 +153,9 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
complexity: payload.complexity,
is_default: true,
created_by_user: true,
// @ts-expect-error backend accepts additional fields
logic_rules: payload.logic_rules,
business_rules: payload.business_rules ?? (payload.requirements ? payload.requirements.map(r => ({ requirement: r.text, rules: r.rules || [] })) : undefined),
})
await load()
} catch (error) {

View File

@ -33,7 +33,7 @@ export function AICustomFeatureCreator({
onClose,
}: {
projectType?: string
onAdd: (feature: { name: string; description: string; complexity: Complexity; logic_rules?: string[] }) => void
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
}) {
const [featureName, setFeatureName] = useState('')
@ -46,6 +46,7 @@ export function AICustomFeatureCreator({
{ text: '', rules: [] },
])
const [logicRules, setLogicRules] = useState<string[]>([])
const [analyzingIdx, setAnalyzingIdx] = useState<number | null>(null)
const handleAnalyze = async () => {
if (!featureDescription.trim() && requirements.every(r => !r.text.trim())) return
@ -75,6 +76,24 @@ export function AICustomFeatureCreator({
// Capture dynamic logic rules (editable)
setLogicRules(Array.isArray(overall.logicRules) ? overall.logicRules : [])
// 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 {
@ -82,6 +101,44 @@ export function AICustomFeatureCreator({
}
}
const handleAnalyzeRequirement = async (idx: number) => {
const req = requirements[idx]
if (!req?.text?.trim()) 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()
if (!aiAnalysis) {
@ -91,8 +148,10 @@ export function AICustomFeatureCreator({
onAdd({
name: aiAnalysis.suggested_name || featureName.trim() || 'Custom Feature',
description: featureDescription.trim(),
complexity: selectedComplexity || aiAnalysis.complexity || 'medium',
logic_rules: logicRules,
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()
}
@ -109,7 +168,7 @@ export function AICustomFeatureCreator({
<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>
<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>
@ -118,23 +177,7 @@ export function AICustomFeatureCreator({
<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>
<div>
<label className="block text-sm text-white/70 mb-1">Complexity Level</label>
<Select
value={selectedComplexity || ''}
onValueChange={(value: Complexity) => setSelectedComplexity(value)}
>
<SelectTrigger className="w-full bg-white/10 border-white/20 text-white">
<SelectValue placeholder="Select complexity" />
</SelectTrigger>
<SelectContent className="bg-gray-900 border-white/10">
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-white/50 mt-1">Choose the complexity level for this feature</p>
</div>
{/* Complexity is determined by AI; manual selection removed */}
{/* Dynamic Requirements List */}
<div className="space-y-2">
@ -153,6 +196,15 @@ export function AICustomFeatureCreator({
}}
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()}
className="border-white/20 text-white hover:bg-white/10"
>
{analyzingIdx === idx ? 'Analyzing…' : (r.rules?.length ? 'Re-analyze' : 'Analyze With AI')}
</Button>
<button
type="button"
onClick={() => setRequirements(requirements.filter((_, i) => i !== idx))}
@ -162,39 +214,40 @@ export function AICustomFeatureCreator({
×
</button>
</div>
{r.rules?.length > 0 && (
<div className="pl-8 space-y-2">
<div className="text-white/70 text-sm">Logic Rules for this requirement:</div>
<div className="space-y-1">
{r.rules.map((rule, ridx) => (
<div key={ridx} className="flex items-center gap-2">
<div className="text-white/50 text-xs w-8">R{ridx + 1}</div>
<Input value={rule} onChange={(e) => {
const next = [...requirements]
const rr = [...(next[idx].rules || [])]
rr[ridx] = e.target.value
next[idx] = { ...next[idx], rules: rr }
setRequirements(next)
}} className="bg-white/10 border-white/20 text-white placeholder:text-white/40" />
<button type="button" onClick={() => {
const next = [...requirements]
const rr = [...(next[idx].rules || [])]
rr.splice(ridx, 1)
next[idx] = { ...next[idx], rules: rr }
setRequirements(next)
}} className="text-white/50 hover:text-red-400">×</button>
</div>
))}
<button type="button" onClick={() => {
const next = [...requirements]
const rr = [...(next[idx].rules || [])]
rr.push('')
next[idx] = { ...next[idx], rules: rr }
setRequirements(next)
}} className="text-xs text-orange-400 hover:text-orange-300">+ Add rule to this requirement</button>
</div>
<div 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">
@ -202,11 +255,7 @@ export function AICustomFeatureCreator({
</Button>
</div>
{!aiAnalysis && (featureDescription.trim() || requirements.some(r => r.text.trim())) && (
<Button type="button" onClick={handleAnalyze} disabled={isAnalyzing} className="w-full bg-orange-500 hover:bg-orange-400 text-black">
{isAnalyzing ? 'Analyzing with AI…' : 'Analyze with AI'}
</Button>
)}
{/* 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>
@ -250,11 +299,11 @@ export function AICustomFeatureCreator({
<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">
Overall Complexity: <span className="capitalize">{aiAnalysis.complexity}</span>
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 || !selectedComplexity} className="bg-orange-500 hover:bg-orange-400 text-black">
<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>

View File

@ -839,7 +839,7 @@ function FeatureSelectionStep({
useEffect(() => { load() }, [template.id])
const handleAddAIAnalyzed = async (payload: { name: string; description: string; complexity: 'low' | 'medium' | 'high'; logic_rules?: string[] }) => {
const handleAddAIAnalyzed = async (payload: { name: string; description: string; complexity: 'low' | 'medium' | 'high'; logic_rules?: string[]; requirements?: Array<{ text: string; rules: string[] }>; business_rules?: Array<{ requirement: string; rules: string[] }> }) => {
await createFeature(template.id, {
name: payload.name,
description: payload.description,
@ -847,7 +847,9 @@ function FeatureSelectionStep({
complexity: payload.complexity,
is_default: false,
created_by_user: true,
// @ts-expect-error backend accepts additional fields
logic_rules: payload.logic_rules,
business_rules: payload.business_rules ?? (payload.requirements ? payload.requirements.map(r => ({ requirement: r.text, rules: r.rules || [] })) : undefined),
})
await load()
}

View File

@ -4,8 +4,8 @@
*/
// Main backend URL - change this to update all API calls
// export const BACKEND_URL = 'http://localhost:8000';
export const BACKEND_URL = 'https://backend.codenuk.com';
export const BACKEND_URL = 'http://localhost:8000';
// export const BACKEND_URL = 'https://backend.codenuk.com';
// Realtime notifications socket URL (Template Manager emits notifications)

33
src/lib/svg.ts Normal file
View File

@ -0,0 +1,33 @@
export function extractSvgString(input: unknown): string | null {
if (typeof input === 'string') {
if (input.trim().startsWith('<svg')) return input
try {
const j = JSON.parse(input)
return (j?.svg || j?.data?.svg || null) as string | null
} catch {
return null
}
}
if (input && typeof input === 'object') {
const obj = input as Record<string, unknown>
if (typeof obj.svg === 'string') return obj.svg
if (obj.data && typeof (obj as any).data.svg === 'string') return (obj as any).data.svg
}
return null
}
export function ensureSvgDimensions(svg: string, width?: string | number, height?: string | number): string {
const w = width === undefined ? undefined : typeof width === 'number' ? `${width}px` : width
const h = height === undefined ? undefined : typeof height === 'number' ? `${height}px` : height
if (!w && !h) return svg
return svg.replace(/<svg(\s[^>]*)?>/i, (match) => {
const hasW = /\swidth=/.test(match)
const hasH = /\sheight=/.test(match)
let m = match
if (!hasW && w) m = m.replace(/<svg/i, `<svg width="${w}"`)
if (!hasH && h) m = m.replace(/<svg/i, `<svg height="${h}"`)
return m
})
}

View File

@ -38,6 +38,11 @@ export interface TemplateFeature {
updated_at: string
}
export type CreateFeaturePayload = Partial<TemplateFeature> & {
logic_rules?: string[]
business_rules?: Array<{ requirement: string; rules: string[] }>
}
export interface TemplateWithFeatures extends DatabaseTemplate {
features: TemplateFeature[]
}
@ -436,7 +441,7 @@ class TemplateService {
return this.makeRequest<TemplateFeature[]>(`/api/features/search?q=${q}${extra}`)
}
async createFeature(featureData: Partial<TemplateFeature>): Promise<TemplateFeature> {
async createFeature(featureData: CreateFeaturePayload): Promise<TemplateFeature> {
if (
featureData &&
(featureData.feature_type === 'custom')