From d57e20e9d8cf8454a9571c3801251880a17e96ed Mon Sep 17 00:00:00 2001 From: Chandini Date: Wed, 10 Sep 2025 17:21:09 +0530 Subject: [PATCH] frontend features rules generation changes --- src/components/DynamicSvg.tsx | 132 ++++++++++++++ .../admin/admin-feature-selection.tsx | 4 +- src/components/ai/AICustomFeatureCreator.tsx | 169 +++++++++++------- src/components/main-dashboard.tsx | 4 +- src/config/backend.ts | 4 +- src/lib/svg.ts | 33 ++++ src/lib/template-service.ts | 7 +- 7 files changed, 288 insertions(+), 65 deletions(-) create mode 100644 src/components/DynamicSvg.tsx create mode 100644 src/lib/svg.ts diff --git a/src/components/DynamicSvg.tsx b/src/components/DynamicSvg.tsx new file mode 100644 index 0000000..0105feb --- /dev/null +++ b/src/components/DynamicSvg.tsx @@ -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(/[\s\S]*?<\/script>/gi, "") + const withoutStyles = withoutScripts.replace(/[\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(svg ?? null) + const [error, setError] = React.useState(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(" { + isCancelled = true + } + }, [src]) + + if (error) { + return
Failed to render SVG: {error}
+ } + + if (!content) { + return
+ } + + 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 ( +
+
]*)?>/i, + (match) => + match.includes("width=") || match.includes("height=") + ? match + : match.replace( + / +
+ ) +} + +export default DynamicSvg + + diff --git a/src/components/admin/admin-feature-selection.tsx b/src/components/admin/admin-feature-selection.tsx index f070a0a..931625b 100644 --- a/src/components/admin/admin-feature-selection.tsx +++ b/src/components/admin/admin-feature-selection.tsx @@ -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) { diff --git a/src/components/ai/AICustomFeatureCreator.tsx b/src/components/ai/AICustomFeatureCreator.tsx index 01964ae..18883dc 100644 --- a/src/components/ai/AICustomFeatureCreator.tsx +++ b/src/components/ai/AICustomFeatureCreator.tsx @@ -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([]) + const [analyzingIdx, setAnalyzingIdx] = useState(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({
- + setFeatureName(e.target.value)} placeholder="e.g., Subscriptions" className="bg-white/10 border-white/20 text-white placeholder:text-white/40" />
@@ -118,23 +177,7 @@ export function AICustomFeatureCreator({

Be as detailed as possible. The AI will analyze and break down your requirements.

-
- - -

Choose the complexity level for this feature

-
+ {/* Complexity is determined by AI; manual selection removed */} {/* Dynamic Requirements List */}
@@ -153,6 +196,15 @@ export function AICustomFeatureCreator({ }} className="bg-white/10 border-white/20 text-white placeholder:text-white/40" /> +
- {r.rules?.length > 0 && ( -
-
Logic Rules for this requirement:
-
- {r.rules.map((rule, ridx) => ( -
-
R{ridx + 1}
- { - 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" /> - -
- ))} - -
+
+
Logic Rules for this requirement:
+
+ {(r.rules || []).length === 0 && ( +
No rules yet. Click Analyze to generate.
+ )} + {(r.rules || []).map((rule, ridx) => ( +
+
R{ridx + 1}
+ { + 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" /> + +
+ ))} +
- )} +
))}
- {!aiAnalysis && (featureDescription.trim() || requirements.some(r => r.text.trim())) && ( - - )} + {/* Removed global Analyze button; use per-requirement Analyze instead */} {analysisError && ( {analysisError} @@ -250,11 +299,11 @@ export function AICustomFeatureCreator({
{aiAnalysis && (
- Overall Complexity: {aiAnalysis.complexity} + Complexity (AI): {aiAnalysis.complexity}
)} -
diff --git a/src/components/main-dashboard.tsx b/src/components/main-dashboard.tsx index ce4e6b3..130e8ea 100644 --- a/src/components/main-dashboard.tsx +++ b/src/components/main-dashboard.tsx @@ -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() } diff --git a/src/config/backend.ts b/src/config/backend.ts index f0b6bb8..1e522a7 100644 --- a/src/config/backend.ts +++ b/src/config/backend.ts @@ -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) diff --git a/src/lib/svg.ts b/src/lib/svg.ts new file mode 100644 index 0000000..935d46b --- /dev/null +++ b/src/lib/svg.ts @@ -0,0 +1,33 @@ +export function extractSvgString(input: unknown): string | null { + if (typeof input === 'string') { + if (input.trim().startsWith(' + 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(/]*)?>/i, (match) => { + const hasW = /\swidth=/.test(match) + const hasH = /\sheight=/.test(match) + let m = match + if (!hasW && w) m = m.replace(/ & { + 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(`/api/features/search?q=${q}${extra}`) } - async createFeature(featureData: Partial): Promise { + async createFeature(featureData: CreateFeaturePayload): Promise { if ( featureData && (featureData.feature_type === 'custom')