frontend changes upto based features ruels generation
This commit is contained in:
parent
343ef63563
commit
d1c150e055
@ -31,23 +31,80 @@ export function AICustomFeatureCreator({
|
|||||||
projectType,
|
projectType,
|
||||||
onAdd,
|
onAdd,
|
||||||
onClose,
|
onClose,
|
||||||
|
editingFeature,
|
||||||
}: {
|
}: {
|
||||||
projectType?: string
|
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
|
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
|
||||||
|
editingFeature?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
complexity: Complexity;
|
||||||
|
business_rules?: any;
|
||||||
|
technical_requirements?: any;
|
||||||
|
additional_business_rules?: any;
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
const [featureName, setFeatureName] = useState('')
|
const [featureName, setFeatureName] = useState(editingFeature?.name || '')
|
||||||
const [featureDescription, setFeatureDescription] = useState('')
|
const [featureDescription, setFeatureDescription] = useState(editingFeature?.description || '')
|
||||||
const [selectedComplexity, setSelectedComplexity] = useState<Complexity | undefined>(undefined)
|
const [selectedComplexity, setSelectedComplexity] = useState<Complexity | undefined>(editingFeature?.complexity || undefined)
|
||||||
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||||||
const [aiAnalysis, setAiAnalysis] = useState<AIAnalysisResult | null>(null)
|
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 [analysisError, setAnalysisError] = useState<string | null>(null)
|
||||||
const [requirements, setRequirements] = useState<Array<{ text: string; rules: string[] }>>([
|
const [requirements, setRequirements] = useState<Array<{ text: string; rules: string[] }>>(() => {
|
||||||
{ text: '', rules: [] },
|
// 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
|
||||||
|
: JSON.parse(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
|
||||||
|
: JSON.parse((editingFeature as any).additional_business_rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || '',
|
||||||
|
rules: rule.rules || []
|
||||||
|
}))
|
||||||
|
console.log('📋 Mapped requirements:', requirements)
|
||||||
|
return requirements
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing business rules:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [{ text: '', rules: [] }]
|
||||||
|
})
|
||||||
const [analyzingIdx, setAnalyzingIdx] = useState<number | null>(null)
|
const [analyzingIdx, setAnalyzingIdx] = useState<number | null>(null)
|
||||||
|
const hasAnyAnalysis = !!aiAnalysis || requirements.some(r => (r.rules || []).length > 0)
|
||||||
|
|
||||||
const handleAnalyze = async () => {
|
const handleAnalyze = async () => {
|
||||||
|
if (hasAnyAnalysis) return
|
||||||
if (!featureDescription.trim() && requirements.every(r => !r.text.trim())) return
|
if (!featureDescription.trim() && requirements.every(r => !r.text.trim())) return
|
||||||
setIsAnalyzing(true)
|
setIsAnalyzing(true)
|
||||||
setAnalysisError(null)
|
setAnalysisError(null)
|
||||||
@ -100,7 +157,9 @@ export function AICustomFeatureCreator({
|
|||||||
|
|
||||||
const handleAnalyzeRequirement = async (idx: number) => {
|
const handleAnalyzeRequirement = async (idx: number) => {
|
||||||
const req = requirements[idx]
|
const req = requirements[idx]
|
||||||
|
if (hasAnyAnalysis) return
|
||||||
if (!req?.text?.trim()) return
|
if (!req?.text?.trim()) return
|
||||||
|
if ((req.rules || []).length > 0) return
|
||||||
setAnalyzingIdx(idx)
|
setAnalyzingIdx(idx)
|
||||||
setAnalysisError(null)
|
setAnalysisError(null)
|
||||||
try {
|
try {
|
||||||
@ -138,14 +197,10 @@ export function AICustomFeatureCreator({
|
|||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!aiAnalysis) {
|
|
||||||
await handleAnalyze()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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: aiAnalysis.complexity || selectedComplexity || 'medium',
|
complexity: aiAnalysis?.complexity || selectedComplexity || 'medium',
|
||||||
logic_rules: requirements.flatMap(r => r.rules || []),
|
logic_rules: requirements.flatMap(r => r.rules || []),
|
||||||
requirements: requirements,
|
requirements: requirements,
|
||||||
business_rules: requirements.map(r => ({ requirement: r.text, rules: r.rules || [] })),
|
business_rules: requirements.map(r => ({ requirement: r.text, rules: r.rules || [] })),
|
||||||
@ -158,7 +213,9 @@ export function AICustomFeatureCreator({
|
|||||||
<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="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="p-6 border-b border-white/10">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-white text-lg font-semibold">AI-Powered Feature Creator</h3>
|
<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>
|
<button onClick={onClose} className="text-white/60 hover:text-white">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -197,10 +254,10 @@ export function AICustomFeatureCreator({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleAnalyzeRequirement(idx)}
|
onClick={() => handleAnalyzeRequirement(idx)}
|
||||||
disabled={isAnalyzing || analyzingIdx === idx || !r.text.trim()}
|
disabled={isAnalyzing || analyzingIdx === idx || !r.text.trim() || hasAnyAnalysis || (r.rules || []).length > 0}
|
||||||
className="border-white/20 text-white hover:bg-white/10"
|
className="border-white/20 text-white hover:bg-white/10"
|
||||||
>
|
>
|
||||||
{analyzingIdx === idx ? 'Analyzing…' : (r.rules?.length ? 'Re-analyze' : 'Analyze With AI')}
|
{analyzingIdx === idx ? 'Analyzing…' : (((r.rules || []).length > 0) || hasAnyAnalysis ? 'Analyzed' : 'Analyze With AI')}
|
||||||
</Button>
|
</Button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -276,7 +333,10 @@ export function AICustomFeatureCreator({
|
|||||||
)}
|
)}
|
||||||
<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} 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'}
|
{editingFeature
|
||||||
|
? (aiAnalysis ? 'Update Feature with Tagged Rules' : 'Update Feature')
|
||||||
|
: (aiAnalysis ? 'Add Feature with Tagged Rules' : 'Add Feature')
|
||||||
|
}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -287,5 +347,3 @@ export function AICustomFeatureCreator({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default AICustomFeatureCreator
|
export default AICustomFeatureCreator
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -100,7 +100,8 @@ const addTokenRefreshInterceptor = (client: typeof authApiClient) => {
|
|||||||
(response) => response,
|
(response) => response,
|
||||||
async (error) => {
|
async (error) => {
|
||||||
const originalRequest = error.config;
|
const originalRequest = error.config;
|
||||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
const isRefreshEndpoint = originalRequest?.url?.includes('/api/auth/refresh');
|
||||||
|
if (error.response?.status === 401 && !originalRequest._retry && !isRefreshEndpoint) {
|
||||||
originalRequest._retry = true;
|
originalRequest._retry = true;
|
||||||
try {
|
try {
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
@ -112,6 +113,11 @@ const addTokenRefreshInterceptor = (client: typeof authApiClient) => {
|
|||||||
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
||||||
return client(originalRequest);
|
return client(originalRequest);
|
||||||
}
|
}
|
||||||
|
// No refresh token available
|
||||||
|
clearTokens();
|
||||||
|
safeLocalStorage.removeItem('codenuk_user');
|
||||||
|
window.location.href = '/signin';
|
||||||
|
return Promise.reject(error);
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
console.error('Token refresh failed:', refreshError);
|
console.error('Token refresh failed:', refreshError);
|
||||||
clearTokens();
|
clearTokens();
|
||||||
|
|||||||
@ -21,6 +21,7 @@ interface DualCanvasEditorProps {
|
|||||||
onGenerationStart?: () => void
|
onGenerationStart?: () => void
|
||||||
selectedDevice?: 'desktop' | 'tablet' | 'mobile'
|
selectedDevice?: 'desktop' | 'tablet' | 'mobile'
|
||||||
onDeviceChange?: (device: 'desktop' | 'tablet' | 'mobile') => void
|
onDeviceChange?: (device: 'desktop' | 'tablet' | 'mobile') => void
|
||||||
|
initialPrompt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DualCanvasEditor({
|
export function DualCanvasEditor({
|
||||||
@ -28,7 +29,8 @@ export function DualCanvasEditor({
|
|||||||
onWireframeGenerated,
|
onWireframeGenerated,
|
||||||
onGenerationStart,
|
onGenerationStart,
|
||||||
selectedDevice = 'desktop',
|
selectedDevice = 'desktop',
|
||||||
onDeviceChange
|
onDeviceChange,
|
||||||
|
initialPrompt
|
||||||
}: DualCanvasEditorProps) {
|
}: DualCanvasEditorProps) {
|
||||||
const [activeId, setActiveId] = useState<string | null>(null)
|
const [activeId, setActiveId] = useState<string | null>(null)
|
||||||
const [canvasMode, setCanvasMode] = useState<'wireframe' | 'components'>('wireframe')
|
const [canvasMode, setCanvasMode] = useState<'wireframe' | 'components'>('wireframe')
|
||||||
@ -216,6 +218,7 @@ export function DualCanvasEditor({
|
|||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
selectedDevice={selectedDevice}
|
selectedDevice={selectedDevice}
|
||||||
onDeviceChange={handleDeviceChange}
|
onDeviceChange={handleDeviceChange}
|
||||||
|
initialPrompt={initialPrompt}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { Input } from "@/components/ui/input"
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import { EditFeatureForm } from "@/components/edit-feature-form"
|
|
||||||
import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2, User, Palette } from "lucide-react"
|
import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2, User, Palette } from "lucide-react"
|
||||||
import { useTemplates } from "@/hooks/useTemplates"
|
import { useTemplates } from "@/hooks/useTemplates"
|
||||||
import { CustomTemplateForm } from "@/components/custom-template-form"
|
import { CustomTemplateForm } from "@/components/custom-template-form"
|
||||||
@ -20,6 +19,7 @@ import { Tooltip } from "@/components/ui/tooltip"
|
|||||||
import WireframeCanvas from "@/components/wireframe-canvas"
|
import WireframeCanvas from "@/components/wireframe-canvas"
|
||||||
import PromptSidePanel from "@/components/prompt-side-panel"
|
import PromptSidePanel from "@/components/prompt-side-panel"
|
||||||
import { DualCanvasEditor } from "@/components/dual-canvas-editor"
|
import { DualCanvasEditor } from "@/components/dual-canvas-editor"
|
||||||
|
import { getAccessToken } from "@/components/apis/authApiClients"
|
||||||
|
|
||||||
interface Template {
|
interface Template {
|
||||||
id: string
|
id: string
|
||||||
@ -857,14 +857,14 @@ function FeatureSelectionStep({
|
|||||||
const [editingFeature, setEditingFeature] = useState<TemplateFeature | null>(null)
|
const [editingFeature, setEditingFeature] = useState<TemplateFeature | null>(null)
|
||||||
|
|
||||||
const handleUpdate = async (f: TemplateFeature, updates: Partial<TemplateFeature>) => {
|
const handleUpdate = async (f: TemplateFeature, updates: Partial<TemplateFeature>) => {
|
||||||
const idForApi = f.feature_type === 'custom' ? (f.feature_id?.replace(/^custom_/, '') || f.id) : f.id
|
// Use the actual id field directly (no need to extract from feature_id)
|
||||||
await updateFeature(idForApi, { ...updates, isCustom: f.feature_type === 'custom' })
|
await updateFeature(f.id, { ...updates, isCustom: f.feature_type === 'custom' })
|
||||||
await load()
|
await load()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async (f: TemplateFeature) => {
|
const handleDelete = async (f: TemplateFeature) => {
|
||||||
const idForApi = f.feature_type === 'custom' ? (f.feature_id?.replace(/^custom_/, '') || f.id) : f.id
|
// Use the actual id field directly (no need to extract from feature_id)
|
||||||
await deleteFeature(idForApi, { isCustom: f.feature_type === 'custom' })
|
await deleteFeature(f.id, { isCustom: f.feature_type === 'custom' })
|
||||||
setSelectedIds((prev) => {
|
setSelectedIds((prev) => {
|
||||||
const next = new Set(prev)
|
const next = new Set(prev)
|
||||||
next.delete(f.id)
|
next.delete(f.id)
|
||||||
@ -977,23 +977,25 @@ function FeatureSelectionStep({
|
|||||||
|
|
||||||
{section('Your Custom Features', custom)}
|
{section('Your Custom Features', custom)}
|
||||||
|
|
||||||
{showAIModal && (
|
{(showAIModal || editingFeature) && (
|
||||||
<AICustomFeatureCreator
|
<AICustomFeatureCreator
|
||||||
projectType={template.type || template.title}
|
projectType={template.type || template.title}
|
||||||
onAdd={async (f) => { await handleAddAIAnalyzed(f); setShowAIModal(false) }}
|
onAdd={async (f) => {
|
||||||
onClose={() => setShowAIModal(false)}
|
if (editingFeature) {
|
||||||
/>
|
// Update existing feature
|
||||||
)}
|
await handleUpdate(editingFeature, f)
|
||||||
|
setEditingFeature(null)
|
||||||
{editingFeature && (
|
} else {
|
||||||
<EditFeatureForm
|
// Add new feature
|
||||||
feature={editingFeature}
|
await handleAddAIAnalyzed(f)
|
||||||
onSubmit={async (updates) => {
|
setShowAIModal(false)
|
||||||
await handleUpdate(editingFeature, updates)
|
}
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setShowAIModal(false)
|
||||||
setEditingFeature(null)
|
setEditingFeature(null)
|
||||||
}}
|
}}
|
||||||
onCancel={() => setEditingFeature(null)}
|
editingFeature={editingFeature || undefined}
|
||||||
isOpen={!!editingFeature}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -1037,9 +1039,10 @@ function BusinessQuestionsStep({
|
|||||||
setError('No features selected')
|
setError('No features selected')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const resp = await fetch(`${BACKEND_URL}/api/v1/generate-comprehensive-business-questions`, {
|
const token = getAccessToken()
|
||||||
|
const resp = await fetch(`${BACKEND_URL}/api/questions/generate-comprehensive-business-questions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
allFeatures: selected,
|
allFeatures: selected,
|
||||||
projectName: template.title,
|
projectName: template.title,
|
||||||
@ -1327,6 +1330,7 @@ function AIMockupStep({
|
|||||||
const [wireframeData, setWireframeData] = useState<any>(null)
|
const [wireframeData, setWireframeData] = useState<any>(null)
|
||||||
const [isGenerating, setIsGenerating] = useState(false)
|
const [isGenerating, setIsGenerating] = useState(false)
|
||||||
const [selectedDevice, setSelectedDevice] = useState<'desktop' | 'tablet' | 'mobile'>('desktop')
|
const [selectedDevice, setSelectedDevice] = useState<'desktop' | 'tablet' | 'mobile'>('desktop')
|
||||||
|
const [initialPrompt, setInitialPrompt] = useState<string>("")
|
||||||
|
|
||||||
// Load state from localStorage after component mounts
|
// Load state from localStorage after component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -1352,6 +1356,17 @@ function AIMockupStep({
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Build prompt from selected features and template
|
||||||
|
useEffect(() => {
|
||||||
|
if (!template) return
|
||||||
|
const featureNames = (selectedFeatures || []).map(f => f.name).filter(Boolean)
|
||||||
|
const base = `${template.title} dashboard`
|
||||||
|
const parts = featureNames.length > 0 ? ` with ${featureNames.join(', ')}` : ''
|
||||||
|
const footer = '. Optimize layout and spacing. Include navigation.'
|
||||||
|
const prompt = `${base}${parts}${footer}`
|
||||||
|
setInitialPrompt(prompt)
|
||||||
|
}, [template, JSON.stringify(selectedFeatures)])
|
||||||
|
|
||||||
const handleWireframeGenerated = (data: any) => {
|
const handleWireframeGenerated = (data: any) => {
|
||||||
setWireframeData(data)
|
setWireframeData(data)
|
||||||
setIsGenerating(false)
|
setIsGenerating(false)
|
||||||
@ -1414,6 +1429,7 @@ function AIMockupStep({
|
|||||||
onGenerationStart={handleWireframeGenerationStart}
|
onGenerationStart={handleWireframeGenerationStart}
|
||||||
selectedDevice={selectedDevice}
|
selectedDevice={selectedDevice}
|
||||||
onDeviceChange={handleDeviceChange}
|
onDeviceChange={handleDeviceChange}
|
||||||
|
initialPrompt={initialPrompt}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useMemo, useState, useEffect } from "react"
|
import { useMemo, useState, useEffect, useRef } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
@ -11,11 +11,13 @@ import { getAIMockupHealthUrl, AI_MOCKUP_CONFIG } from "@/lib/api-config"
|
|||||||
export function PromptSidePanel({
|
export function PromptSidePanel({
|
||||||
className,
|
className,
|
||||||
selectedDevice = 'desktop',
|
selectedDevice = 'desktop',
|
||||||
onDeviceChange
|
onDeviceChange,
|
||||||
|
initialPrompt
|
||||||
}: {
|
}: {
|
||||||
className?: string
|
className?: string
|
||||||
selectedDevice?: 'desktop' | 'tablet' | 'mobile'
|
selectedDevice?: 'desktop' | 'tablet' | 'mobile'
|
||||||
onDeviceChange?: (device: 'desktop' | 'tablet' | 'mobile') => void
|
onDeviceChange?: (device: 'desktop' | 'tablet' | 'mobile') => void
|
||||||
|
initialPrompt?: string
|
||||||
}) {
|
}) {
|
||||||
const [collapsed, setCollapsed] = useState(false)
|
const [collapsed, setCollapsed] = useState(false)
|
||||||
const [prompt, setPrompt] = useState(
|
const [prompt, setPrompt] = useState(
|
||||||
@ -23,6 +25,7 @@ export function PromptSidePanel({
|
|||||||
)
|
)
|
||||||
const [isGenerating, setIsGenerating] = useState(false)
|
const [isGenerating, setIsGenerating] = useState(false)
|
||||||
const [backendStatus, setBackendStatus] = useState<'connected' | 'disconnected' | 'checking'>('checking')
|
const [backendStatus, setBackendStatus] = useState<'connected' | 'disconnected' | 'checking'>('checking')
|
||||||
|
const autoTriggeredRef = useRef(false)
|
||||||
|
|
||||||
const examples = useMemo(
|
const examples = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@ -66,6 +69,25 @@ export function PromptSidePanel({
|
|||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Update prompt from parent when provided
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof initialPrompt === 'string' && initialPrompt.trim().length > 0) {
|
||||||
|
setPrompt(initialPrompt)
|
||||||
|
}
|
||||||
|
}, [initialPrompt])
|
||||||
|
|
||||||
|
// Auto-generate once when an initial prompt arrives
|
||||||
|
useEffect(() => {
|
||||||
|
if (!autoTriggeredRef.current && typeof initialPrompt === 'string' && initialPrompt.trim().length > 0) {
|
||||||
|
autoTriggeredRef.current = true
|
||||||
|
// Slight delay to ensure canvas listeners are mounted
|
||||||
|
const id = setTimeout(() => {
|
||||||
|
dispatchGenerate(initialPrompt)
|
||||||
|
}, 300)
|
||||||
|
return () => clearTimeout(id)
|
||||||
|
}
|
||||||
|
}, [initialPrompt])
|
||||||
|
|
||||||
const dispatchGenerate = async (text: string) => {
|
const dispatchGenerate = async (text: string) => {
|
||||||
setIsGenerating(true)
|
setIsGenerating(true)
|
||||||
|
|
||||||
@ -208,7 +230,7 @@ export function PromptSidePanel({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="pt-1">
|
{/* <div className="pt-1">
|
||||||
<p className="text-xs font-medium mb-2">Examples</p>
|
<p className="text-xs font-medium mb-2">Examples</p>
|
||||||
<ScrollArea className="h-40 border rounded">
|
<ScrollArea className="h-40 border rounded">
|
||||||
<ul className="p-2 space-y-2">
|
<ul className="p-2 space-y-2">
|
||||||
@ -226,7 +248,7 @@ export function PromptSidePanel({
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* AI Features Info */}
|
{/* AI Features Info */}
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||||
|
|||||||
@ -1375,6 +1375,19 @@ export default function WireframeCanvas({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("relative h-full w-full flex flex-col", className)}>
|
<div className={cn("relative h-full w-full flex flex-col", className)}>
|
||||||
|
{/* Centered loading overlay during generation */}
|
||||||
|
{busy && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-50 flex items-center justify-center bg-white/60 dark:bg-black/60"
|
||||||
|
aria-busy="true"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<div className="h-10 w-10 animate-spin rounded-full border-4 border-blue-600 border-t-transparent"></div>
|
||||||
|
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">Generating wireframe…</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="absolute top-4 left-4 right-4 z-50 bg-red-50 border border-red-200 rounded-lg p-3 shadow-lg">
|
<div className="absolute top-4 left-4 right-4 z-50 bg-red-50 border border-red-200 rounded-lg p-3 shadow-lg">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -1391,7 +1404,7 @@ export default function WireframeCanvas({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{busy && (
|
{busy && (
|
||||||
<div className="absolute top-4 left-4 right-4 z-50 bg-blue-50 border border-blue-200 rounded-lg p-3 shadow-lg">
|
<div className="absolute top-4 left-4 right-4 z-40 bg-blue-50 border border-blue-200 rounded-lg p-3 shadow-lg">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="animate-spin w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full"></div>
|
<div className="animate-spin w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full"></div>
|
||||||
<span className="text-blue-600 text-sm font-medium">Generating wireframe with AI...</span>
|
<span className="text-blue-600 text-sm font-medium">Generating wireframe with AI...</span>
|
||||||
|
|||||||
@ -409,9 +409,31 @@ export const featureApi = {
|
|||||||
technical_requirements?: Record<string, unknown>;
|
technical_requirements?: Record<string, unknown>;
|
||||||
created_by_user_session?: string;
|
created_by_user_session?: string;
|
||||||
}): Promise<{ data: AdminFeature; similarityInfo?: Record<string, unknown> }> => {
|
}): Promise<{ data: AdminFeature; similarityInfo?: Record<string, unknown> }> => {
|
||||||
|
const normalizeJsonField = (value: unknown): unknown => {
|
||||||
|
if (value === undefined || value === null || value === '') return undefined;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
const t = value.trim();
|
||||||
|
if ((t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']'))) {
|
||||||
|
return JSON.parse(t);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleaned = {
|
||||||
|
...featureData,
|
||||||
|
business_rules: normalizeJsonField(featureData.business_rules) as Record<string, unknown>,
|
||||||
|
technical_requirements: normalizeJsonField(featureData.technical_requirements) as Record<string, unknown>,
|
||||||
|
};
|
||||||
|
|
||||||
const response = await apiCall<AdminFeature>('/api/features/custom', {
|
const response = await apiCall<AdminFeature>('/api/features/custom', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(featureData),
|
body: JSON.stringify(cleaned),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -442,9 +442,37 @@ class TemplateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createFeature(featureData: CreateFeaturePayload): Promise<TemplateFeature> {
|
async createFeature(featureData: CreateFeaturePayload): Promise<TemplateFeature> {
|
||||||
|
// Normalize potentially stringified JSON fields to proper JSON before sending
|
||||||
|
const normalizeJsonField = (value: unknown): unknown => {
|
||||||
|
if (value === undefined || value === null || value === '') return undefined
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
||||||
|
return JSON.parse(trimmed)
|
||||||
|
}
|
||||||
|
// If it's a plain string not JSON, leave as-is for non-JSONB fields
|
||||||
|
return value
|
||||||
|
} catch {
|
||||||
|
// Leave as-is; backend will reject invalid JSON, but we prefer to avoid sending broken JSONB
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedFeatureData: CreateFeaturePayload = {
|
||||||
|
...featureData,
|
||||||
|
// Only include normalized fields if valid
|
||||||
|
business_rules: normalizeJsonField(featureData.business_rules) as CreateFeaturePayload['business_rules'],
|
||||||
|
logic_rules: normalizeJsonField(featureData.logic_rules) as CreateFeaturePayload['logic_rules'],
|
||||||
|
// @ts-expect-error: allow passthrough of optional technical_requirements if present in callers
|
||||||
|
technical_requirements: normalizeJsonField((featureData as any).technical_requirements),
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
featureData &&
|
cleanedFeatureData &&
|
||||||
(featureData.feature_type === 'custom')
|
(cleanedFeatureData.feature_type === 'custom')
|
||||||
) {
|
) {
|
||||||
const customHeaders: Record<string, string> = { 'Content-Type': 'application/json' }
|
const customHeaders: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||||
const customToken = getAccessToken()
|
const customToken = getAccessToken()
|
||||||
@ -454,7 +482,7 @@ class TemplateService {
|
|||||||
const response = await fetch(`${BACKEND_URL}/api/features/custom`, {
|
const response = await fetch(`${BACKEND_URL}/api/features/custom`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: customHeaders,
|
headers: customHeaders,
|
||||||
body: JSON.stringify(featureData),
|
body: JSON.stringify(cleanedFeatureData),
|
||||||
})
|
})
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
@ -469,7 +497,7 @@ class TemplateService {
|
|||||||
const response = await fetch(`${BACKEND_URL}/api/features`, {
|
const response = await fetch(`${BACKEND_URL}/api/features`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(featureData),
|
body: JSON.stringify(cleanedFeatureData),
|
||||||
})
|
})
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user