"use client" import { useState, useEffect } from "react" import Link from "next/link" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Checkbox } from "@/components/ui/checkbox" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" 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 { CustomTemplateForm } from "@/components/custom-template-form" import { EditTemplateForm } from "@/components/edit-template-form" import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog" import { DatabaseTemplate, TemplateFeature } from "@/lib/template-service" import AICustomFeatureCreator from "@/components/ai/AICustomFeatureCreator" import { BACKEND_URL } from "@/config/backend" // Removed Tooltip import as we are no longer using tooltips for title/description import WireframeCanvas from "@/components/wireframe-canvas" import PromptSidePanel from "@/components/prompt-side-panel" import { DualCanvasEditor } from "@/components/dual-canvas-editor" import { getAccessToken } from "@/components/apis/authApiClients" interface Template { id: string title: string description: string category: string features: string[] complexity: number timeEstimate: string techStack: string[] popularity?: number lastUpdated?: string type?: string icon?: string | null gradient?: string | null border?: string | null text?: string | null subtext?: string | null is_active?: boolean created_at?: string updated_at?: string featureCount?: number is_custom?: boolean; // Add this field to identify custom templates } function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => void }) { const [selectedCategory, setSelectedCategory] = useState("all") const [searchQuery, setSearchQuery] = useState("") const [showCustomForm, setShowCustomForm] = useState(false) const [editingTemplate, setEditingTemplate] = useState(null) const [deletingTemplate, setDeletingTemplate] = useState(null) const [deleteLoading, setDeleteLoading] = useState(false) // Keep a stable list of all categories seen so the filter chips don't disappear const [knownCategories, setKnownCategories] = useState>(new Set(["all"])) // Cache counts per category using the API totals for each filtered fetch // Use undefined to indicate "unknown" instead of showing wrong initial counts const [categoryCounts, setCategoryCounts] = useState>({ all: 0 }) // Track per-card expanded state for descriptions const [expandedDescriptions, setExpandedDescriptions] = useState>({}) const [descDialogOpen, setDescDialogOpen] = useState(false) const [descDialogData, setDescDialogData] = useState<{ title: string; description: string }>({ title: '', description: '' }) const { user, combined, loading, error, paginationState, createTemplate, updateTemplate, deleteTemplate, fetchTemplatesWithPagination, loadMoreTemplates, categories: hookCategories, } = useTemplates() // Initial fetch is handled inside useTemplates hook; avoid duplicate fetch here // Handle category changes immediately (no debounce) so switching chips is snappy useEffect(() => { if (selectedCategory !== paginationState.selectedCategory) { console.log('[TemplateSelectionStep] Triggering fetch due to category change:', { selectedCategory }); fetchTemplatesWithPagination({ page: 0, pageSize: paginationState.pageSize, category: selectedCategory, search: searchQuery, resetPagination: true, }); } }, [selectedCategory, paginationState.selectedCategory, paginationState.pageSize, searchQuery, fetchTemplatesWithPagination]); // Handle search changes with debouncing useEffect(() => { const timeoutId = setTimeout(() => { if (searchQuery !== paginationState.searchQuery) { console.log('[TemplateSelectionStep] Triggering fetch due to search change:', { searchQuery }); fetchTemplatesWithPagination({ page: 0, pageSize: paginationState.pageSize, category: selectedCategory, search: searchQuery, resetPagination: true, }); } }, 500); return () => clearTimeout(timeoutId); }, [searchQuery, selectedCategory, paginationState.searchQuery, paginationState.pageSize, fetchTemplatesWithPagination]); // Track categories seen across any fetch so the chips remain visible useEffect(() => { if (!combined?.data?.length) return; setKnownCategories((prev) => { const next = new Set(prev); combined.data.forEach((t) => { if (t.category) next.add(t.category); }); return next; }); }, [combined?.data]); // Seed known categories and counts from hookCategories (pre-fetched full counts) useEffect(() => { if (!hookCategories || hookCategories.length === 0) return; setKnownCategories((prev) => { const next = new Set(prev); hookCategories.forEach((c) => next.add(c.id)); return next; }); setCategoryCounts((prev) => { const next: Record = { ...prev }; hookCategories.forEach((c) => { next[c.id] = c.count; }); // Ensure 'all' exists if provided by hook const allFromHook = hookCategories.find((c) => c.id === 'all')?.count; if (typeof allFromHook === 'number') next['all'] = allFromHook; return next; }); }, [hookCategories]); // Update counts cache based on API totals for the currently selected category useEffect(() => { const currentCat = paginationState.selectedCategory || 'all'; const totalForFilter = paginationState.total || 0; setCategoryCounts((prev) => ({ ...prev, [currentCat]: totalForFilter, })); }, [paginationState.selectedCategory, paginationState.total]); const templates: Template[] = combined?.data?.length ? combined.data.map((t) => { console.log('[TemplateSelectionStep] Processing template:', { id: t.id, title: t.title, category: t.category, feature_count: t.feature_count }); return { id: t.id, title: t.title, description: t.description || "No description available", category: t.category, features: [], complexity: 3, // Default complexity since DatabaseTemplate doesn't have this property timeEstimate: "2-4 weeks", techStack: ["Next.js", "PostgreSQL", "Tailwind CSS"], popularity: t.avg_rating ? Math.round(t.avg_rating * 20) : 75, lastUpdated: t.updated_at ? new Date(t.updated_at).toISOString().split('T')[0] : undefined, type: t.type, icon: t.icon, gradient: t.gradient, border: t.border, text: t.text, subtext: t.subtext, is_active: t.is_active, created_at: t.created_at, updated_at: t.updated_at, featureCount: t.feature_count || 0, // Add feature count from API is_custom: t.is_custom, // Add is_custom field }; }) : []; // Debug logging console.log('[TemplateSelectionStep] Debug:', { hasCombined: !!combined, hasData: !!combined?.data, dataType: typeof combined?.data, isArray: Array.isArray(combined?.data), dataLength: combined?.data?.length || 0, templatesLength: templates.length, paginationState, templates: templates.map((t) => ({ id: t.id, title: t.title, category: t.category })), }); const getCategories = () => { const categoryMap = new Map; count: number }>(); // All category: prefer live total when currently filtered by 'all', // otherwise show the last cached overall total (if any) const liveAllTotal = paginationState.selectedCategory === 'all' ? (paginationState.total || 0) : 0; const cachedAllTotal = categoryCounts['all'] ?? 0; categoryMap.set("all", { name: "All Templates", icon: Globe, count: Math.max(liveAllTotal, cachedAllTotal) }); // Build chips from the union of all categories we have ever seen Array.from(knownCategories) .filter((c) => c !== 'all') .forEach((categoryId) => { const lower = categoryId.toLowerCase(); let icon = Code; if (lower.includes('marketing') || lower.includes('branding')) icon = Zap; else if (lower.includes('seo') || lower.includes('content')) icon = BarChart3; else if (lower.includes('food') || lower.includes('delivery')) icon = Users; // Prefer cached count for this category (set when that filter is active). // Do NOT fallback to visible page count to avoid misleading numbers. const count = categoryCounts[categoryId]; categoryMap.set(categoryId, { name: categoryId, icon, // If count is unknown, represent as 0 in data and handle placeholder in UI count: typeof count === 'number' ? count : 0, }); }); return Array.from(categoryMap.entries()).map(([id, data]) => ({ id, ...data })); }; const categories = getCategories(); const getComplexityColor = (complexity: number) => { if (complexity <= 2) return "bg-emerald-900/40 text-emerald-300 border border-emerald-800" if (complexity <= 3) return "bg-amber-900/40 text-amber-300 border border-amber-800" return "bg-rose-900/40 text-rose-300 border border-rose-800" } const getComplexityLabel = (complexity: number) => { if (complexity <= 2) return "Simple" if (complexity <= 3) return "Moderate" return "Complex" } // Truncate helper to restrict displayed characters const TITLE_MAX_CHARS = 15; const DESC_MAX_CHARS = 120; const truncate = (value: string | undefined | null, max: number) => { const v = (value || '').trim(); if (v.length <= max) return v; return v.slice(0, Math.max(0, max - 1)) + '…'; }; const handleCreateTemplate = async (templateData: Partial): Promise => { try { const created = await createTemplate({ ...(templateData as any), // Ensure backend routes to custom_templates table is_custom: true, source: 'custom', // Attach user id so custom template is associated with creator user_id: (user as any)?.id, } as any); setShowCustomForm(false); return created; } catch (error) { console.error('[TemplateSelectionStep] Error creating template:', error); // Re-throw to satisfy the return type contract when errors should propagate throw error as Error; } }; const handleUpdateTemplate = async (id: string, templateData: Partial) => { try { // Find the template to determine if it's custom const template = templates.find(t => t.id === id); const isCustom = template?.is_custom || false; await updateTemplate(id, templateData, isCustom); setEditingTemplate(null); } catch (error) { console.error('[TemplateSelectionStep] Error updating template:', error); // Show user-friendly error message const errorMessage = error instanceof Error ? error.message : 'Failed to update template'; alert(`Error updating template: ${errorMessage}`); } }; const handleDeleteTemplate = async () => { if (!deletingTemplate) return; setDeleteLoading(true); try { // Find the template to determine if it's custom const template = templates.find(t => t.id === deletingTemplate.id); const isCustom = template?.is_custom || false; await deleteTemplate(deletingTemplate.id, isCustom); setDeletingTemplate(null); } catch (error) { console.error('[TemplateSelectionStep] Error deleting template:', error); // Show user-friendly error message const errorMessage = error instanceof Error ? error.message : 'Failed to delete template'; alert(`Error deleting template: ${errorMessage}`); } finally { setDeleteLoading(false); } }; if (loading && templates.length === 0) { return (

Loading Templates...

Fetching templates from /merged API

); } if (error) { // Check if this is an authentication error const isAuthError = error.includes('Please sign in') || error.includes('Authentication required'); return (
{isAuthError ? ( <>

Sign In Required

{error}

) : ( <>

Error Loading Templates

{error}

)}
); } // Show custom template form if (showCustomForm) { return (

Create Custom Template

Design your own project template

{ await handleCreateTemplate(templateData); }} onCancel={() => setShowCustomForm(false)} />
) } // Show edit template form if (editingTemplate) { return (

Edit Template

Modify your template settings

setEditingTemplate(null)} />
) } // Show delete confirmation dialog if (deletingTemplate) { return ( <>
{/* Header */}

Choose Your Project Template

Select from our comprehensive library of professionally designed templates

setDeletingTemplate(null)} loading={deleteLoading} /> ) } return (
{/* Header */}

Choose Your Project Template

Select from our comprehensive library of professionally designed templates

{!user?.id && (

You're currently viewing public templates. {' '}to access your personal templates and create custom ones.

)}
setSearchQuery(e.target.value)} className="pl-10 h-12 text-lg border border-white/10 bg-white/5 text-white placeholder:text-white/40 focus:border-white/30 focus:ring-white/30 rounded-xl" /> {paginationState.loading && (
)}
{(() => { const renderChip = (category: { id: string; name: string; icon: React.ComponentType<{ className?: string }>; count: number }) => { const Icon = category.icon; const active = selectedCategory === category.id; const knownCount = category.id === 'all' ? Math.max( selectedCategory === 'all' ? (paginationState.total || 0) : 0, categoryCounts['all'] ?? 0 ) : categoryCounts[category.id]; // Fallback to currently visible items count for initial render if unknown const fallbackCount = category.id === 'all' ? templates.length : templates.filter((t) => t.category === category.id).length; const displayCount = typeof knownCount === 'number' ? knownCount : fallbackCount; return ( ); }; const allChip = categories.find((c) => c.id === 'all'); const rest = categories.filter((c) => c.id !== 'all'); return (
{allChip && (
{renderChip(allChip)}
)}
{rest.map(renderChip)} {rest.map((c) => ({ ...c, id: `${c.id}-dup` })).map(renderChip)}
); })()}
{templates.length === 0 && !paginationState.loading ? (

No templates found for the current filters.

) : (
{templates.map((template) => (
{truncate(template.title, TITLE_MAX_CHARS)}
{getComplexityLabel(template.complexity)} {template.popularity && (
{template.popularity}%
)}
{/* Edit and Delete buttons - only show for database templates */} {template.id && template.id !== "marketing-website" && template.id !== "saas-platform" && (
)}
{(() => { const raw = (template.description || '').trim() const needsToggle = raw.length > DESC_MAX_CHARS const displayText = truncate(template.description, DESC_MAX_CHARS) return (

{displayText}

{needsToggle && ( )}
) })()}
{template.timeEstimate}
{template.featureCount || 0} features

Key Features

{template.features.slice(0, 3).map((feature, index) => ( {feature} ))} {template.features.length > 3 && ( +{template.features.length - 3} more )}
))}
)}
{(() => { const totalPages = Math.max(1, Math.ceil(paginationState.total / paginationState.pageSize)); const currentPage = paginationState.currentPage; const pages = []; // First page pages.push( ); // Ellipsis before current page if (currentPage > 3) { pages.push(...); } // Pages around current for (let i = Math.max(1, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) { if (i > 0 && i < totalPages) { pages.push( ); } } // Ellipsis after current page if (currentPage < totalPages - 3) { pages.push(...); } // Last page (only if not already rendered by the middle range) if (totalPages > 1 && currentPage < totalPages - 2) { pages.push( ); } return pages; })()}
user?.id ? setShowCustomForm(true) : window.location.href = '/signin'}>

{user?.id ? 'Create Custom Template' : 'Sign In to Create Templates'}

{user?.id ? "Don't worry, we'll guide you through each step. a custom project type with your specific requirements and tech stack." : "Sign in to create custom project templates with your specific requirements and tech stack." }

{searchQuery ? ( <> Showing {templates.length} template{templates.length !== 1 ? "s" : ""} matching "{searchQuery}" {paginationState.total > 0 && ` (${paginationState.total} total)`} ) : ( <> Showing {templates.length} template{templates.length !== 1 ? "s" : ""} {paginationState.total > 0 && ` of ${paginationState.total} total`} )} {paginationState.total > 0 && ( Page {paginationState.currentPage + 1} of {Math.ceil(paginationState.total / paginationState.pageSize)} )}

{descDialogData.title} {descDialogData.description}
) } // Feature Selection Step Component function FeatureSelectionStep({ template, onNext, onBack, }: { template: Template; onNext: (selected: TemplateFeature[]) => void; onBack: () => void }) { const { fetchFeatures, createFeature, updateFeature, deleteFeature } = useTemplates() const [features, setFeatures] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [selectedIds, setSelectedIds] = useState>(new Set()) const [showAIModal, setShowAIModal] = useState(false) const [expandedFeatureDescriptions, setExpandedFeatureDescriptions] = useState>({}) const [featureDescDialogOpen, setFeatureDescDialogOpen] = useState(false) const [featureDescDialogData, setFeatureDescDialogData] = useState<{ title: string; description: string }>({ title: '', description: '' }) const [rulesDialogOpen, setRulesDialogOpen] = useState(false) const [rulesForFeature, setRulesForFeature] = useState<{ featureId: string; featureName: string } | null>(null) const [featureRules, setFeatureRules] = useState>([]) const FEATURE_DESC_MAX_CHARS = 180 const truncateFeatureText = (value: string | undefined | null, max: number) => { const v = (value || '').trim() if (v.length <= max) return v return v.slice(0, Math.max(0, max - 1)) + '…' } const load = async () => { try { setLoading(true) console.log('[FeatureSelectionStep] Loading features for template:', template.id) console.log('[FeatureSelectionStep] Template object:', template) const data = await fetchFeatures(template.id) console.log('[FeatureSelectionStep] Raw features received:', data) console.log('[FeatureSelectionStep] API endpoint called:', `/api/templates/${template.id}/features`) console.log('[FeatureSelectionStep] Features by type:', { essential: data.filter(f => f.feature_type === 'essential').length, suggested: data.filter(f => f.feature_type === 'suggested').length, custom: data.filter(f => f.feature_type === 'custom').length, total: data.length }) console.log('[FeatureSelectionStep] All features with types:', data.map(f => ({ name: f.name, type: f.feature_type }))) setFeatures(data) } catch (e) { console.error('[FeatureSelectionStep] Error loading features:', e) setError(e instanceof Error ? e.message : 'Failed to load features') } finally { setLoading(false) } } // Initial load useEffect(() => { load() }, [template.id]) 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, feature_type: 'custom', 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() } const [editingFeature, setEditingFeature] = useState(null) const handleUpdate = async (f: TemplateFeature, updates: Partial) => { // Use the actual id field directly (no need to extract from feature_id) await updateFeature(f.id, { ...updates, isCustom: f.feature_type === 'custom' }) await load() } const handleDelete = async (f: TemplateFeature) => { // Use the actual id field directly (no need to extract from feature_id) await deleteFeature(f.id, { isCustom: f.feature_type === 'custom' }) setSelectedIds((prev) => { const next = new Set(prev) next.delete(f.id) return next }) await load() } const toggleSelect = (f: TemplateFeature) => { setSelectedIds((prev) => { const next = new Set(prev) if (next.has(f.id)) next.delete(f.id) else next.add(f.id) return next }) } const extractRules = (f: TemplateFeature): Array => { // Prefer structured business_rules if available; fall back to additional_business_rules const candidate = (f as any).business_rules ?? (f as any).additional_business_rules ?? [] if (Array.isArray(candidate)) return candidate if (candidate && typeof candidate === 'object') return Object.entries(candidate).map(([k, v]) => `${k}: ${v as string}`) if (typeof candidate === 'string') return [candidate] return [] } const section = (title: string, list: TemplateFeature[]) => (

{title} ({list.length})

6 ? 'max-h-[480px] overflow-y-auto pr-2' : ''}`}>
{list.map((f) => (
toggleSelect(f)} className="border-white/20 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500" /> {f.name}
{f.feature_type === 'custom' && (
)}
{(() => { const raw = (f.description || 'No description provided.').trim() const needsToggle = raw.length > FEATURE_DESC_MAX_CHARS const displayText = truncateFeatureText(raw, FEATURE_DESC_MAX_CHARS) return (

{displayText}

{needsToggle && ( )}
) })()}
{f.feature_type} {f.complexity} {typeof f.usage_count === 'number' && used {f.usage_count}}
))}
) if (loading) { return
Loading features...
} if (error) { return
{error}
} const essentials = features.filter(f => f.feature_type === 'essential') const suggested = features.filter(f => f.feature_type === 'suggested') const custom = features.filter(f => f.feature_type === 'custom') return (

Select Features for {template.title}

Choose defaults or add your own custom features.

{features.length > 0 && section('Template Features', features)} {/* Add custom feature with AI */}

Add Custom Feature

Use AI to analyze and create custom features for your project

AI will analyze your requirements and create optimized features
{section('Your Custom Features', custom)} {(showAIModal || editingFeature) && ( { if (editingFeature) { // Update existing feature await handleUpdate(editingFeature, f) setEditingFeature(null) } else { // Add new feature await handleAddAIAnalyzed(f) setShowAIModal(false) } }} onClose={() => { setShowAIModal(false) setEditingFeature(null) }} editingFeature={editingFeature || undefined} /> )}
Select at least 3 features to continue. Selected {selectedIds.size}/3.
{featureDescDialogData.title} {featureDescDialogData.description} {rulesForFeature?.featureName || 'Feature Rules'} {featureRules.length === 0 ? (
No rules found for this feature.
) : (
    {featureRules.map((r, idx) => { if (typeof r === 'string') return
  • {r}
  • const req = (r as any).requirement const rules = (r as any).rules return (
  • {req ?
    {req}
    : null} {Array.isArray(rules) && rules.length > 0 && (
      {rules.map((rr: string, j: number) => (
    • {rr}
    • ))}
    )}
  • ) })}
)}
) } // Business Questions Step Component function BusinessQuestionsStep({ template, selected, onBack, onDone, }: { template: Template; selected: TemplateFeature[]; onBack: () => void; onDone: (completeData: any, recommendations: any) => void }) { const [businessQuestions, setBusinessQuestions] = useState([]) const [businessAnswers, setBusinessAnswers] = useState>({}) const [loading, setLoading] = useState(true) const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) const selectedKey = selected.map(s => s.id).join(',') useEffect(() => { const load = async () => { try { setLoading(true) setError(null) if (selected.length === 0) { setError('No features selected') return } const token = getAccessToken() const resp = await fetch(`${BACKEND_URL}/api/questions/generate-comprehensive-business-questions`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: JSON.stringify({ allFeatures: selected, projectName: template.title, projectType: template.type || template.category, totalFeatures: selected.length, }), }) if (!resp.ok) throw new Error(`HTTP ${resp.status}`) const data = await resp.json() const qs: string[] = data?.data?.businessQuestions || [] setBusinessQuestions(qs) const init: Record = {} qs.forEach((_, i) => (init[i] = '')) setBusinessAnswers(init) } catch (e: any) { setError(e?.message || 'Failed to load questions') } finally { setLoading(false) } } load() }, [template.id, selectedKey]) const answeredCount = Object.values(businessAnswers).filter((a) => a && a.trim()).length if (loading) { return (

AI is generating comprehensive business questions...

Analyzing {selected.length} features as integrated system

) } if (error) { return (
Error Loading Questions
{error}
) } const handleSubmit = async () => { try { setSubmitting(true) const answeredCount = Object.values(businessAnswers).filter((a) => a && a.trim()).length if (answeredCount === 0) return const completeData = { projectName: template.title, projectType: template.type || template.category, allFeatures: selected, businessQuestions, businessAnswers, timestamp: new Date().toISOString(), featureName: `${template.title} - Integrated System`, description: `Complete ${template.type || template.category} system with ${selected.length} integrated features`, requirements: (selected as any[]).flatMap((f: any) => f.requirements || []), complexity: (selected as any[]).some((f: any) => f.complexity === 'high') ? 'high' : (selected as any[]).some((f: any) => f.complexity === 'medium') ? 'medium' : 'low', logicRules: (selected as any[]).flatMap((f: any) => f.logicRules || []), } const resp = await fetch(`${BACKEND_URL}/api/v1/select`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(completeData), }) if (!resp.ok) throw new Error(`HTTP ${resp.status}`) const recommendations = await resp.json() onDone(completeData, recommendations) } catch (e) { console.error('Tech stack selection failed', e) } finally { setSubmitting(false) } } return (

Business Context Questions

Help us refine recommendations by answering these questions.

Analyzing {selected.length} integrated features

{businessQuestions.map((q, i) => (