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
|
// 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
|
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)) {
|
if (!uuidRegex.test(template.id)) {
|
||||||
@ -153,7 +153,9 @@ export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectio
|
|||||||
complexity: payload.complexity,
|
complexity: payload.complexity,
|
||||||
is_default: true,
|
is_default: true,
|
||||||
created_by_user: true,
|
created_by_user: true,
|
||||||
|
// @ts-expect-error backend accepts additional fields
|
||||||
logic_rules: payload.logic_rules,
|
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()
|
await load()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export function AICustomFeatureCreator({
|
|||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
projectType?: string
|
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
|
onClose: () => void
|
||||||
}) {
|
}) {
|
||||||
const [featureName, setFeatureName] = useState('')
|
const [featureName, setFeatureName] = useState('')
|
||||||
@ -46,6 +46,7 @@ export function AICustomFeatureCreator({
|
|||||||
{ text: '', rules: [] },
|
{ text: '', rules: [] },
|
||||||
])
|
])
|
||||||
const [logicRules, setLogicRules] = useState<string[]>([])
|
const [logicRules, setLogicRules] = useState<string[]>([])
|
||||||
|
const [analyzingIdx, setAnalyzingIdx] = useState<number | null>(null)
|
||||||
|
|
||||||
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
|
||||||
@ -75,6 +76,24 @@ export function AICustomFeatureCreator({
|
|||||||
|
|
||||||
// Capture dynamic logic rules (editable)
|
// Capture dynamic logic rules (editable)
|
||||||
setLogicRules(Array.isArray(overall.logicRules) ? overall.logicRules : [])
|
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) {
|
} catch (e: any) {
|
||||||
setAnalysisError(e?.message || 'AI analysis failed')
|
setAnalysisError(e?.message || 'AI analysis failed')
|
||||||
} finally {
|
} 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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!aiAnalysis) {
|
if (!aiAnalysis) {
|
||||||
@ -91,8 +148,10 @@ export function AICustomFeatureCreator({
|
|||||||
onAdd({
|
onAdd({
|
||||||
name: aiAnalysis.suggested_name || featureName.trim() || 'Custom Feature',
|
name: aiAnalysis.suggested_name || featureName.trim() || 'Custom Feature',
|
||||||
description: featureDescription.trim(),
|
description: featureDescription.trim(),
|
||||||
complexity: selectedComplexity || aiAnalysis.complexity || 'medium',
|
complexity: aiAnalysis.complexity || selectedComplexity || 'medium',
|
||||||
logic_rules: logicRules,
|
logic_rules: requirements.flatMap(r => r.rules || []),
|
||||||
|
requirements: requirements,
|
||||||
|
business_rules: requirements.map(r => ({ requirement: r.text, rules: r.rules || [] })),
|
||||||
})
|
})
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
@ -109,7 +168,7 @@ export function AICustomFeatureCreator({
|
|||||||
<div className="flex-1 overflow-y-auto p-6">
|
<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</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" />
|
<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>
|
||||||
<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>
|
<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>
|
||||||
|
|
||||||
<div>
|
{/* Complexity is determined by AI; manual selection removed */}
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Dynamic Requirements List */}
|
{/* Dynamic Requirements List */}
|
||||||
<div className="space-y-2">
|
<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"
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setRequirements(requirements.filter((_, i) => i !== idx))}
|
onClick={() => setRequirements(requirements.filter((_, i) => i !== idx))}
|
||||||
@ -162,39 +214,40 @@ export function AICustomFeatureCreator({
|
|||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{r.rules?.length > 0 && (
|
<div className="pl-8 space-y-2">
|
||||||
<div className="pl-8 space-y-2">
|
<div className="text-white/70 text-sm">Logic Rules for this requirement:</div>
|
||||||
<div className="text-white/70 text-sm">Logic Rules for this requirement:</div>
|
<div className="space-y-1">
|
||||||
<div className="space-y-1">
|
{(r.rules || []).length === 0 && (
|
||||||
{r.rules.map((rule, ridx) => (
|
<div className="text-white/40 text-xs">No rules yet. Click Analyze to generate.</div>
|
||||||
<div key={ridx} className="flex items-center gap-2">
|
)}
|
||||||
<div className="text-white/50 text-xs w-8">R{ridx + 1}</div>
|
{(r.rules || []).map((rule, ridx) => (
|
||||||
<Input value={rule} onChange={(e) => {
|
<div key={ridx} className="flex items-center gap-2">
|
||||||
const next = [...requirements]
|
<div className="text-white/50 text-xs w-8">R{ridx + 1}</div>
|
||||||
const rr = [...(next[idx].rules || [])]
|
<Input value={rule} onChange={(e) => {
|
||||||
rr[ridx] = e.target.value
|
const next = [...requirements]
|
||||||
next[idx] = { ...next[idx], rules: rr }
|
const rr = [...(next[idx].rules || [])]
|
||||||
setRequirements(next)
|
rr[ridx] = e.target.value
|
||||||
}} className="bg-white/10 border-white/20 text-white placeholder:text-white/40" />
|
next[idx] = { ...next[idx], rules: rr }
|
||||||
<button type="button" onClick={() => {
|
setRequirements(next)
|
||||||
const next = [...requirements]
|
}} className="bg-white/10 border-white/20 text-white placeholder:text-white/40" />
|
||||||
const rr = [...(next[idx].rules || [])]
|
<button type="button" onClick={() => {
|
||||||
rr.splice(ridx, 1)
|
const next = [...requirements]
|
||||||
next[idx] = { ...next[idx], rules: rr }
|
const rr = [...(next[idx].rules || [])]
|
||||||
setRequirements(next)
|
rr.splice(ridx, 1)
|
||||||
}} className="text-white/50 hover:text-red-400">×</button>
|
next[idx] = { ...next[idx], rules: rr }
|
||||||
</div>
|
setRequirements(next)
|
||||||
))}
|
}} className="text-white/50 hover:text-red-400">×</button>
|
||||||
<button type="button" onClick={() => {
|
</div>
|
||||||
const next = [...requirements]
|
))}
|
||||||
const rr = [...(next[idx].rules || [])]
|
<button type="button" onClick={() => {
|
||||||
rr.push('')
|
const next = [...requirements]
|
||||||
next[idx] = { ...next[idx], rules: rr }
|
const rr = [...(next[idx].rules || [])]
|
||||||
setRequirements(next)
|
rr.push('')
|
||||||
}} className="text-xs text-orange-400 hover:text-orange-300">+ Add rule to this requirement</button>
|
next[idx] = { ...next[idx], rules: rr }
|
||||||
</div>
|
setRequirements(next)
|
||||||
|
}} className="text-xs text-orange-400 hover:text-orange-300">+ Add rule to this requirement</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Button type="button" variant="outline" onClick={() => setRequirements([...requirements, { text: '', rules: [] }])} className="border-white/20 text-white hover:bg-white/10">
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!aiAnalysis && (featureDescription.trim() || requirements.some(r => r.text.trim())) && (
|
{/* Removed global Analyze button; use per-requirement Analyze instead */}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{analysisError && (
|
{analysisError && (
|
||||||
<Card className="p-3 bg-red-500/10 border-red-500/30 text-red-300">{analysisError}</Card>
|
<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">
|
<div className="flex gap-3 flex-wrap items-center pt-4 border-t border-white/10">
|
||||||
{aiAnalysis && (
|
{aiAnalysis && (
|
||||||
<div className="flex-1 text-white/80 text-sm">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button type="button" variant="outline" onClick={onClose} className="border-white/20 text-white hover:bg-white/10">Cancel</Button>
|
<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'}
|
{aiAnalysis ? 'Add Feature with Tagged Rules' : 'Analyze & Add Feature'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -839,7 +839,7 @@ function FeatureSelectionStep({
|
|||||||
useEffect(() => { load() }, [template.id])
|
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, {
|
await createFeature(template.id, {
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
description: payload.description,
|
description: payload.description,
|
||||||
@ -847,7 +847,9 @@ function FeatureSelectionStep({
|
|||||||
complexity: payload.complexity,
|
complexity: payload.complexity,
|
||||||
is_default: false,
|
is_default: false,
|
||||||
created_by_user: true,
|
created_by_user: true,
|
||||||
|
// @ts-expect-error backend accepts additional fields
|
||||||
logic_rules: payload.logic_rules,
|
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()
|
await load()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Main backend URL - change this to update all API calls
|
// Main backend URL - change this to update all API calls
|
||||||
// export const BACKEND_URL = 'http://localhost:8000';
|
export const BACKEND_URL = 'http://localhost:8000';
|
||||||
export const BACKEND_URL = 'https://backend.codenuk.com';
|
// export const BACKEND_URL = 'https://backend.codenuk.com';
|
||||||
|
|
||||||
|
|
||||||
// Realtime notifications socket URL (Template Manager emits notifications)
|
// 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
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CreateFeaturePayload = Partial<TemplateFeature> & {
|
||||||
|
logic_rules?: string[]
|
||||||
|
business_rules?: Array<{ requirement: string; rules: string[] }>
|
||||||
|
}
|
||||||
|
|
||||||
export interface TemplateWithFeatures extends DatabaseTemplate {
|
export interface TemplateWithFeatures extends DatabaseTemplate {
|
||||||
features: TemplateFeature[]
|
features: TemplateFeature[]
|
||||||
}
|
}
|
||||||
@ -436,7 +441,7 @@ class TemplateService {
|
|||||||
return this.makeRequest<TemplateFeature[]>(`/api/features/search?q=${q}${extra}`)
|
return this.makeRequest<TemplateFeature[]>(`/api/features/search?q=${q}${extra}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async createFeature(featureData: Partial<TemplateFeature>): Promise<TemplateFeature> {
|
async createFeature(featureData: CreateFeaturePayload): Promise<TemplateFeature> {
|
||||||
if (
|
if (
|
||||||
featureData &&
|
featureData &&
|
||||||
(featureData.feature_type === 'custom')
|
(featureData.feature_type === 'custom')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user