frontend features rules generation changes
This commit is contained in:
parent
a21785972d
commit
d57e20e9d8
132
src/components/DynamicSvg.tsx
Normal file
132
src/components/DynamicSvg.tsx
Normal 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
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,11 +214,13 @@ 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) => (
|
||||
{(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) => {
|
||||
@ -194,7 +248,6 @@ export function AICustomFeatureCreator({
|
||||
}} 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>
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
33
src/lib/svg.ts
Normal 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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user