codenuk_frontend_mine/src/components/main-dashboard.tsx
2025-09-16 12:54:16 +05:30

2131 lines
88 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<DatabaseTemplate | null>(null)
const [deletingTemplate, setDeletingTemplate] = useState<DatabaseTemplate | null>(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<Set<string>>(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<Record<string, number | undefined>>({ all: 0 })
// Track per-card expanded state for descriptions
const [expandedDescriptions, setExpandedDescriptions] = useState<Record<string, boolean>>({})
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<string, number | undefined> = { ...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<string, { name: string; icon: React.ComponentType<{ className?: string }>; 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<DatabaseTemplate>): Promise<DatabaseTemplate> => {
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<DatabaseTemplate>) => {
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 (
<div className="max-w-7xl mx-auto space-y-6">
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Loading Templates...</h1>
<p className="text-xl text-white/60">Fetching templates from /merged API</p>
</div>
<div className="flex justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-orange-500"></div>
</div>
</div>
);
}
if (error) {
// Check if this is an authentication error
const isAuthError = error.includes('Please sign in') || error.includes('Authentication required');
return (
<div className="max-w-7xl mx-auto space-y-6">
<div className="text-center space-y-3">
{isAuthError ? (
<>
<div className="flex items-center justify-center space-x-2 text-orange-400">
<AlertCircle className="h-6 w-6" />
<h1 className="text-2xl font-bold">Sign In Required</h1>
</div>
<p className="text-white/60">{error}</p>
<div className="flex justify-center space-x-4">
<Button
onClick={() => window.location.href = '/signin'}
className="bg-orange-500 hover:bg-orange-400 text-black"
>
Sign In
</Button>
<Button
onClick={() => {
// Try to fetch public templates without user ID
fetchTemplatesWithPagination({
page: 0,
pageSize: paginationState.pageSize,
category: selectedCategory,
search: searchQuery,
resetPagination: true,
});
}}
variant="outline"
className="border-white/20 text-white hover:bg-white/10"
>
Browse Public Templates
</Button>
</div>
</>
) : (
<>
<div className="flex items-center justify-center space-x-2 text-red-400">
<AlertCircle className="h-6 w-6" />
<h1 className="text-2xl font-bold">Error Loading Templates</h1>
</div>
<p className="text-white/60">{error}</p>
<Button
onClick={() =>
fetchTemplatesWithPagination({
page: 0,
pageSize: paginationState.pageSize,
category: selectedCategory,
search: searchQuery,
resetPagination: true,
})
}
className="bg-orange-500 hover:bg-orange-400 text-black"
>
Retry
</Button>
</>
)}
</div>
</div>
);
}
// Show custom template form
if (showCustomForm) {
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Create Custom Template</h1>
<p className="text-xl text-white/60">Design your own project template</p>
</div>
<CustomTemplateForm
onSubmit={async (templateData) => {
await handleCreateTemplate(templateData);
}}
onCancel={() => setShowCustomForm(false)}
/>
</div>
)
}
// Show edit template form
if (editingTemplate) {
return (
<div className="max-w-4xl mx-auto space-y-6">
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Edit Template</h1>
<p className="text-xl text-white/60">Modify your template settings</p>
</div>
<EditTemplateForm
template={editingTemplate}
onSubmit={handleUpdateTemplate}
onCancel={() => setEditingTemplate(null)}
/>
</div>
)
}
// Show delete confirmation dialog
if (deletingTemplate) {
return (
<>
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Choose Your Project Template</h1>
<p className="text-xl text-white/60 max-w-3xl mx-auto">
Select from our comprehensive library of professionally designed templates
</p>
</div>
</div>
<DeleteConfirmationDialog
templateTitle={deletingTemplate.title}
onConfirm={handleDeleteTemplate}
onCancel={() => setDeletingTemplate(null)}
loading={deleteLoading}
/>
</>
)
}
return (
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="text-center space-y-3">
<h1 className="text-4xl font-bold text-white">Choose Your Project Template</h1>
<p className="text-xl text-white/60 max-w-3xl mx-auto">
Select from our comprehensive library of professionally designed templates
</p>
{!user?.id && (
<div className="bg-orange-500/10 border border-orange-500/20 rounded-lg p-4 max-w-2xl mx-auto">
<p className="text-orange-300 text-sm">
You&apos;re currently viewing public templates.
<Button
variant="link"
onClick={() => window.location.href = '/signin'}
className="text-orange-400 hover:text-orange-300 p-0 h-auto font-semibold ml-1"
>
Sign in
</Button>
{' '}to access your personal templates and create custom ones.
</p>
</div>
)}
</div>
<div className="space-y-4">
<div className="max-w-2xl mx-auto relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-white/40 h-5 w-5" />
<Input
placeholder="Search templates, features, or technologies..."
value={searchQuery}
onChange={(e) => 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 && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-orange-500"></div>
</div>
)}
</div>
{(() => {
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 (
<button
key={`cat-${category.id}`}
onClick={() => setSelectedCategory(category.id)}
className={`shrink-0 whitespace-nowrap flex items-center space-x-3 px-6 py-3 rounded-xl border transition-all cursor-pointer ${
active ? "bg-orange-500 text-black border-orange-500" : "bg-white/5 text-white/80 border-white/10 hover:bg-white/10"
}`}
>
<div className={`p-2 rounded-lg ${active ? "bg-white/10" : "bg-white/10"}`}>
<Icon className="h-5 w-5" />
</div>
<div className="text-left">
<div className="font-semibold">{category.name}</div>
<div className={`text-sm ${active ? "text-black/80" : "text-white/60"}`}>
{`${displayCount} templates`}
</div>
</div>
</button>
);
};
const allChip = categories.find((c) => c.id === 'all');
const rest = categories.filter((c) => c.id !== 'all');
return (
<div className="flex items-center gap-3">
{allChip && (
<div className="shrink-0">
{renderChip(allChip)}
</div>
)}
<div className="relative overflow-hidden flex-1 pb-2">
<div className="flex gap-3 whitespace-nowrap animate-scroll-x will-change-transform">
{rest.map(renderChip)}
{rest.map((c) => ({ ...c, id: `${c.id}-dup` })).map(renderChip)}
</div>
<style jsx>{`
@keyframes scroll-x { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.animate-scroll-x { animation: scroll-x 20s linear infinite; }
`}</style>
</div>
</div>
);
})()}
</div>
{templates.length === 0 && !paginationState.loading ? (
<div className="text-center py-12 text-white/60">
<p>No templates found for the current filters.</p>
<Button
onClick={() =>
fetchTemplatesWithPagination({
page: 0,
pageSize: paginationState.pageSize,
category: 'all',
search: '',
resetPagination: true,
})
}
className="mt-4 bg-orange-500 hover:bg-orange-400 text-black"
>
Reset Filters
</Button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{templates.map((template) => (
<Card key={template.id} className="group bg-white/5 border-white/10 hover:border-white/20 rounded-xl shadow-sm hover:shadow-md transition-all">
<div className="bg-white/5 px-4 py-4 border-b border-white/10">
<div className="flex items-start justify-between mb-2">
<div className="space-y-2 flex-1 min-w-0">
<CardTitle className="text-xl font-bold text-white group-hover:text-orange-400 transition-colors">
<div className="whitespace-nowrap overflow-hidden text-ellipsis max-w-full break-words hyphens-auto">
{truncate(template.title, TITLE_MAX_CHARS)}
</div>
</CardTitle>
<div className="flex items-center space-x-2">
<Badge className={`${getComplexityColor(template.complexity)} font-medium px-3 py-1 rounded-full`}>{getComplexityLabel(template.complexity)}</Badge>
{template.popularity && (
<div className="flex items-center space-x-1 bg-white/10 px-2 py-1 rounded-full text-white/80">
<Star className="h-4 w-4 text-amber-400 fill-current" />
<span className="text-sm font-semibold">{template.popularity}%</span>
</div>
)}
</div>
</div>
{/* Edit and Delete buttons - only show for database templates */}
{template.id && template.id !== "marketing-website" && template.id !== "saas-platform" && (
<div className="flex items-center space-x-1 ml-2">
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation()
setEditingTemplate(template as DatabaseTemplate)
}}
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10 cursor-pointer"
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation()
setDeletingTemplate(template as DatabaseTemplate)
}}
className="h-8 w-8 p-0 text-white/60 hover:text-red-400 hover:bg-red-400/10 cursor-pointer"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}
</div>
{(() => {
const raw = (template.description || '').trim()
const needsToggle = raw.length > DESC_MAX_CHARS
const displayText = truncate(template.description, DESC_MAX_CHARS)
return (
<div>
<p className={`text-white/80 text-sm leading-relaxed break-words hyphens-auto line-clamp-2 overflow-hidden`}>
{displayText}
</p>
{needsToggle && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
setDescDialogData({ title: template.title, description: raw })
setDescDialogOpen(true)
}}
className="mt-1 text-xs text-orange-400 hover:text-orange-300 font-medium"
>
Show more
</button>
)}
</div>
)
})()}
</div>
<CardContent className="p-4 flex flex-col h-full text-white/80">
<div className="flex-1 space-y-4">
<div className="flex items-center justify-between text-sm bg-white/5 px-3 py-2 rounded-lg">
<div className="flex items-center space-x-2">
<Clock className="h-4 w-4 text-orange-400" />
<span className="font-medium">{template.timeEstimate}</span>
</div>
<div className="flex items-center space-x-2">
<Layers className="h-4 w-4 text-emerald-400" />
<span className="font-medium">{template.featureCount || 0} features</span>
</div>
</div>
<div className="space-y-2">
<div>
<h4 className="font-semibold text-sm text-white mb-1 flex items-center">
<span className="w-2 h-2 bg-orange-500 rounded-full mr-2"></span>
Key Features
</h4>
<div className="flex flex-wrap gap-1">
{template.features.slice(0, 3).map((feature, index) => (
<Badge key={index} variant="outline" className="text-xs bg-white/5 border-white/10 text-white/80 px-3 py-2 rounded-full">
{feature}
</Badge>
))}
{template.features.length > 3 && (
<Badge variant="outline" className="text-xs bg-white/5 border-white/10 text-white/70 px-3 py-2 rounded-full">
+{template.features.length - 3} more
</Badge>
)}
</div>
</div>
</div>
</div>
<div className="mt-4">
<Button onClick={() => onNext(template)} className="w-full bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow">
<span className="flex items-center justify-center cursor-pointer">
Select Template
<ArrowRight className="ml-2 h-4 w-4" />
</span>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
<div className="flex justify-center pt-6">
<div className="flex items-center space-x-2">
<Button
onClick={() => {
if (paginationState.currentPage > 0) {
fetchTemplatesWithPagination({
page: paginationState.currentPage - 1,
pageSize: paginationState.pageSize,
category: paginationState.selectedCategory,
search: paginationState.searchQuery,
});
}
}}
disabled={paginationState.currentPage === 0 || paginationState.loading}
variant="outline"
size="sm"
className="border-white/20 text-white hover:bg-white/10 disabled:opacity-50 disabled:cursor-not-allowed"
>
</Button>
{(() => {
const totalPages = Math.max(1, Math.ceil(paginationState.total / paginationState.pageSize));
const currentPage = paginationState.currentPage;
const pages = [];
// First page
pages.push(
<Button
key={1}
onClick={() => {
if (currentPage !== 0) {
fetchTemplatesWithPagination({
page: 0,
pageSize: paginationState.pageSize,
category: paginationState.selectedCategory,
search: paginationState.searchQuery,
});
}
}}
variant={currentPage === 0 ? "default" : "outline"}
size="sm"
className={currentPage === 0 ? "bg-orange-500 text-black border-orange-500 hover:bg-orange-400" : "border-white/20 text-white hover:bg-white/10"}
>
1
</Button>
);
// Ellipsis before current page
if (currentPage > 3) {
pages.push(<span key="ellipsis1" className="px-2 text-white/40">...</span>);
}
// 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(
<Button
key={i + 1}
onClick={() => {
fetchTemplatesWithPagination({
page: i,
pageSize: paginationState.pageSize,
category: paginationState.selectedCategory,
search: paginationState.searchQuery,
});
}}
variant={currentPage === i ? "default" : "outline"}
size="sm"
className={currentPage === i ? "bg-orange-500 text-black border-orange-500 hover:bg-orange-400" : "border-white/20 text-white hover:bg-white/10"}
>
{i + 1}
</Button>
);
}
}
// Ellipsis after current page
if (currentPage < totalPages - 3) {
pages.push(<span key="ellipsis2" className="px-2 text-white/40">...</span>);
}
// Last page (only if not already rendered by the middle range)
if (totalPages > 1 && currentPage < totalPages - 2) {
pages.push(
<Button
key={totalPages}
onClick={() => {
fetchTemplatesWithPagination({
page: totalPages - 1,
pageSize: paginationState.pageSize,
category: paginationState.selectedCategory,
search: paginationState.searchQuery,
});
}}
variant={currentPage === totalPages - 1 ? "default" : "outline"}
size="sm"
className={currentPage === totalPages - 1 ? "bg-orange-500 text-black border-orange-500 hover:bg-orange-400" : "border-white/20 text-white hover:bg-white/10"}
>
{totalPages}
</Button>
);
}
return pages;
})()}
<Button
onClick={() => loadMoreTemplates()}
disabled={paginationState.total <= paginationState.pageSize || !paginationState.hasMore || paginationState.loading}
variant="outline"
size="sm"
className="border-white/20 text-white hover:bg-white/10 disabled:opacity-50 disabled:cursor-not-allowed"
>
</Button>
</div>
</div>
<Card className="group border-dashed border-2 border-white/15 bg-white/5 hover:border-white/25 transition-all cursor-pointer" onClick={() => user?.id ? setShowCustomForm(true) : window.location.href = '/signin'}>
<CardContent className="text-center py-16 px-8 text-white/80">
<div className="w-20 h-20 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-6">
<Plus className="h-10 w-10 text-orange-400" />
</div>
<h3 className="text-2xl font-bold text-white mb-3">
{user?.id ? 'Create Custom Template' : 'Sign In to Create Templates'}
</h3>
<p className="mb-8 max-w-md mx-auto text-lg leading-relaxed">
{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."
}
</p>
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10">
{user?.id ? (
<>
<Plus className="mr-2 h-5 w-5" />
Create Custom Template
</>
) : (
<>
<User className="mr-2 h-5 w-5" />
Sign In to Create
</>
)}
</Button>
</CardContent>
</Card>
<div className="text-center py-4">
<p className="text-white/70">
{searchQuery ? (
<>
Showing {templates.length} template{templates.length !== 1 ? "s" : ""} matching &quot;{searchQuery}&quot;
{paginationState.total > 0 && ` (${paginationState.total} total)`}
</>
) : (
<>
Showing {templates.length} template{templates.length !== 1 ? "s" : ""}
{paginationState.total > 0 && ` of ${paginationState.total} total`}
</>
)}
{paginationState.total > 0 && (
<span className="block text-sm text-white/50 mt-1">
Page {paginationState.currentPage + 1} of {Math.ceil(paginationState.total / paginationState.pageSize)}
</span>
)}
</p>
</div>
<Dialog open={descDialogOpen} onOpenChange={setDescDialogOpen}>
<DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton>
<DialogHeader>
<DialogTitle className="text-white">{descDialogData.title}</DialogTitle>
<DialogDescription className="text-white/80 whitespace-pre-wrap">
{descDialogData.description}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</div>
)
}
// 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<TemplateFeature[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
const [showAIModal, setShowAIModal] = useState(false)
const [expandedFeatureDescriptions, setExpandedFeatureDescriptions] = useState<Record<string, boolean>>({})
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<Array<string | { requirement?: string; rules?: string[] }>>([])
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<TemplateFeature | null>(null)
const handleUpdate = async (f: TemplateFeature, updates: Partial<TemplateFeature>) => {
// 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<string | { requirement?: string; rules?: string[] }> => {
// 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[]) => (
<div>
<h3 className="text-lg font-semibold text-white mb-3">{title} ({list.length})</h3>
<div className={`${list.length > 6 ? 'max-h-[480px] overflow-y-auto pr-2' : ''}`}>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{list.map((f) => (
<Card key={f.id} className={`bg-white/5 ${selectedIds.has(f.id) ? 'border-orange-400' : 'border-white/10'}`}>
<CardHeader className="pb-3">
<CardTitle className="text-white flex items-center justify-between">
<div className="flex items-center gap-2">
<Checkbox
checked={selectedIds.has(f.id)}
onCheckedChange={() => toggleSelect(f)}
className="border-white/20 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500"
/>
<span>{f.name}</span>
</div>
{f.feature_type === 'custom' && (
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="border-white/20 text-white hover:bg-white/10"
onClick={() => setEditingFeature(f)}
>
Edit
</Button>
<Button
size="sm"
variant="outline"
className="border-red-500 text-red-300 hover:bg-red-500/10"
onClick={() => handleDelete(f)}
>
Delete
</Button>
</div>
)}
</CardTitle>
</CardHeader>
<CardContent className="text-white/80 text-sm space-y-2">
{(() => {
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 (
<div>
<p className={`leading-relaxed break-words hyphens-auto line-clamp-2 overflow-hidden`}>{displayText}</p>
{needsToggle && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
setFeatureDescDialogData({ title: f.name, description: raw })
setFeatureDescDialogOpen(true)
}}
className="mt-1 text-xs text-orange-400 hover:text-orange-300 font-medium"
>
Show more
</button>
)}
</div>
)
})()}
<div className="flex gap-2 text-xs">
<Badge variant="outline" className="bg-white/5 border-white/10">{f.feature_type}</Badge>
<Badge variant="outline" className="bg-white/5 border-white/10">{f.complexity}</Badge>
{typeof f.usage_count === 'number' && <Badge variant="outline" className="bg-white/5 border-white/10">used {f.usage_count}</Badge>}
<Button
size="sm"
variant="outline"
className="ml-auto h-6 px-2 border-orange-500 text-orange-300 hover:bg-orange-500/10"
onClick={async (e) => {
e.stopPropagation()
setRulesForFeature({ featureId: f.id, featureName: f.name })
setFeatureRules(extractRules(f))
setRulesDialogOpen(true)
}}
>
Rules
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
</div>
)
if (loading) {
return <div className="text-center py-20 text-white/60">Loading features...</div>
}
if (error) {
return <div className="text-center py-20 text-red-400">{error}</div>
}
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 (
<div className="max-w-7xl mx-auto space-y-8">
<div className="text-center space-y-4">
<h1 className="text-4xl font-bold text-white">Select Features for {template.title}</h1>
<p className="text-xl text-white/60 max-w-3xl mx-auto">Choose defaults or add your own custom features.</p>
</div>
{features.length > 0 && section('Template Features', features)}
{/* Add custom feature with AI */}
<div className="bg-white/5 border border-white/10 rounded-xl p-6 space-y-4">
<div className="text-center space-y-2">
<h3 className="text-white font-semibold text-lg">Add Custom Feature</h3>
<p className="text-white/60 text-sm">Use AI to analyze and create custom features for your project</p>
</div>
<div className="flex justify-center">
<Button
onClick={() => setShowAIModal(true)}
className="bg-orange-500 hover:bg-orange-400 text-black font-semibold px-8 py-3 rounded-lg cursor-pointer"
>
<Zap className="mr-2 h-5 w-5" />
Analyze with AI
</Button>
</div>
<div className="text-center text-white/60 text-sm">
AI will analyze your requirements and create optimized features
</div>
</div>
{section('Your Custom Features', custom)}
{(showAIModal || editingFeature) && (
<AICustomFeatureCreator
projectType={template.type || template.title}
onAdd={async (f) => {
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}
/>
)}
<div className="text-center py-4">
<div className="space-x-4">
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10 cursor-pointer">Back</Button>
<Button
onClick={() => onNext(features.filter(f => selectedIds.has(f.id)))}
disabled={selectedIds.size < 3}
className={`bg-orange-500 hover:bg-orange-400 text-black cursor-pointer font-semibold py-2 rounded-lg shadow ${selectedIds.size < 3 ? 'opacity-50 cursor-not-allowed' : ''}`}
>
Continue
</Button>
</div>
<div className="text-white/60 text-sm mt-2">Select at least 3 features to continue. Selected {selectedIds.size}/3.</div>
</div>
<Dialog open={featureDescDialogOpen} onOpenChange={setFeatureDescDialogOpen}>
<DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton>
<DialogHeader>
<DialogTitle className="text-white">{featureDescDialogData.title}</DialogTitle>
<DialogDescription className="text-white/80 whitespace-pre-wrap">
{featureDescDialogData.description}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
<Dialog open={rulesDialogOpen} onOpenChange={setRulesDialogOpen}>
<DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton>
<DialogHeader>
<DialogTitle className="text-white">{rulesForFeature?.featureName || 'Feature Rules'}</DialogTitle>
<DialogDescription className="text-white/80">
{featureRules.length === 0 ? (
<div className="py-2">No rules found for this feature.</div>
) : (
<ul className="list-disc pl-5 space-y-2">
{featureRules.map((r, idx) => {
if (typeof r === 'string') return <li key={idx} className="text-sm leading-relaxed">{r}</li>
const req = (r as any).requirement
const rules = (r as any).rules
return (
<li key={idx} className="text-sm leading-relaxed">
{req ? <div className="font-medium text-white/90">{req}</div> : null}
{Array.isArray(rules) && rules.length > 0 && (
<ul className="list-disc pl-5 mt-1 space-y-1">
{rules.map((rr: string, j: number) => (
<li key={j} className="text-xs text-white/80">{rr}</li>
))}
</ul>
)}
</li>
)
})}
</ul>
)}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</div>
)
}
// 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<string[]>([])
const [businessAnswers, setBusinessAnswers] = useState<Record<number, string>>({})
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(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<number, string> = {}
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 (
<div className="flex items-center justify-center py-24">
<div className="text-center text-white/80">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500 mx-auto mb-4"></div>
<p>AI is generating comprehensive business questions...</p>
<p className="text-white/50 text-sm mt-2">Analyzing {selected.length} features as integrated system</p>
</div>
</div>
)
}
if (error) {
return (
<div className="text-center py-20">
<div className="max-w-md mx-auto bg-red-500/10 border border-red-500/30 rounded-lg p-6 text-red-300">
<div className="font-semibold mb-2">Error Loading Questions</div>
<div className="mb-4">{error}</div>
<div className="space-x-3">
<Button onClick={() => location.reload()} className="bg-red-500 hover:bg-red-400 text-black">Try Again</Button>
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Go Back</Button>
</div>
</div>
</div>
)
}
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 (
<div className="space-y-6">
<div className="text-center space-y-1">
<h2 className="text-2xl font-bold text-white">Business Context Questions</h2>
<p className="text-white/60">Help us refine recommendations by answering these questions.</p>
<p className="text-sm text-orange-400">Analyzing {selected.length} integrated features</p>
</div>
<div className="space-y-4">
{businessQuestions.map((q, i) => (
<div key={i} className="bg-white/5 border border-white/10 rounded-lg p-4">
<label className="block text-sm font-medium text-white mb-2">
<span className="inline-flex items-center gap-2">
<span className="bg-orange-500/20 text-orange-300 rounded-full w-6 h-6 flex items-center justify-center text-xs font-semibold">{i + 1}</span>
<span>{q}</span>
</span>
</label>
<textarea
rows={3}
value={businessAnswers[i] || ''}
onChange={(e) => setBusinessAnswers((prev) => ({ ...prev, [i]: e.target.value }))}
className="w-full bg-white/10 border border-white/20 text-white rounded-md p-3 placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-orange-400/50"
placeholder="Your answer..."
/>
</div>
))}
</div>
<div className="bg-white/5 border border-white/10 rounded-lg p-4 text-white/80">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>Questions answered: {answeredCount} of {businessQuestions.length}</div>
<div>Completion: {businessQuestions.length ? Math.round((answeredCount / businessQuestions.length) * 100) : 0}%</div>
<div>Features analyzing: {selected.length}</div>
</div>
</div>
<div className="text-center pt-2">
<div className="space-x-4">
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Back</Button>
<Button
disabled={submitting || answeredCount === 0}
onClick={handleSubmit}
className={`bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow ${submitting || answeredCount === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{submitting ? 'Getting Recommendations...' : 'Generate Technology Recommendations'}
</Button>
</div>
</div>
</div>
)
}
// Tech Stack Summary Step
function TechStackSummaryStep({
recommendations,
completeData,
onBack,
onGenerate,
}: { recommendations: any; completeData: any; onBack: () => void; onGenerate: () => void }) {
const functional = recommendations?.functional_requirements || {}
const claude = recommendations?.claude_recommendations || {}
const tech = claude?.technology_recommendations || {}
return (
<div className="max-w-6xl mx-auto space-y-6">
<div className="text-center space-y-2">
<h2 className="text-3xl font-bold text-white">Technology Stack Recommendations</h2>
<p className="text-white/60">AI-powered recommendations for your project</p>
</div>
{functional?.feature_name && (
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-xl font-bold text-white mb-4">Functional Requirements Analysis</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-semibold text-white/80 mb-2">Core Feature</h4>
<div className="bg-orange-500 rounded-lg p-4">
<div className="font-medium text-white-200">{functional.feature_name}</div>
<div className="text-white-300 text-sm mt-1">{functional.description}</div>
</div>
</div>
<div>
{/* <h4 className="font-semibold text-white/80 mb-2">Complexity Level</h4>
<div className="rounded-lg p-4">
<span className="inline-block px-3 py-1 rounded-full text-sm font-medium bg-white/10 text-white">
{(functional.complexity_level || 'medium').toUpperCase()}
</span>
</div> */}
</div>
</div>
{Array.isArray(functional.technical_requirements) && functional.technical_requirements.length > 0 && (
<div className="mt-6">
<h4 className="font-semibold text-white/80 mb-3">Technical Requirements</h4>
<div className="flex flex-wrap gap-2">
{functional.technical_requirements.map((req: string, i: number) => (
<span key={i} className="bg-emerald-500/10 text-emerald-200 px-3 py-1 rounded-full text-sm">{req}</span>
))}
</div>
</div>
)}
{Array.isArray(functional.business_logic_rules) && functional.business_logic_rules.length > 0 && (
<div className="mt-6">
<h4 className="font-semibold text-white/80 mb-3">Business Logic Rules</h4>
<div className="space-y-2">
{functional.business_logic_rules.map((rule: string, i: number) => (
<div key={i} className="bg-orange-500/10 border-l-4 border-orange-400 p-3 text-orange-200 text-sm">{rule}</div>
))}
</div>
</div>
)}
</div>
)}
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-xl font-bold text-white mb-6">AI Technology Recommendations</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{tech?.frontend && (
<div className="bg-blue-500/10 rounded-lg p-5">
<div className="font-bold text-blue-200 mb-2">Frontend</div>
<div className="text-blue-300">Framework: {tech.frontend.framework}</div>
{Array.isArray(tech.frontend.libraries) && (
<div className="mt-2 text-blue-300 text-sm">Libraries: {tech.frontend.libraries.join(', ')}</div>
)}
{tech.frontend.reasoning && <div className="mt-2 text-blue-300 text-sm">{tech.frontend.reasoning}</div>}
</div>
)}
{tech?.backend && (
<div className="bg-emerald-500/10 rounded-lg p-5">
<div className="font-bold text-emerald-200 mb-2">Backend</div>
<div className="text-emerald-300">Language: {tech.backend.language}</div>
<div className="text-emerald-300">Framework: {tech.backend.framework}</div>
{Array.isArray(tech.backend.libraries) && (
<div className="mt-2 text-emerald-300 text-sm">Libraries: {tech.backend.libraries.join(', ')}</div>
)}
{tech.backend.reasoning && <div className="mt-2 text-emerald-300 text-sm">{tech.backend.reasoning}</div>}
</div>
)}
{tech?.database && (
<div className="bg-purple-500/10 rounded-lg p-5">
<div className="font-bold text-purple-200 mb-2">Database</div>
<div className="text-purple-300">Primary: {tech.database.primary}</div>
{Array.isArray(tech.database.secondary) && tech.database.secondary.length > 0 && (
<div className="mt-2 text-purple-300 text-sm">Secondary: {tech.database.secondary.join(', ')}</div>
)}
{tech.database.reasoning && <div className="mt-2 text-purple-300 text-sm">{tech.database.reasoning}</div>}
</div>
)}
</div>
</div>
{claude?.implementation_strategy && (
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-xl font-bold text-white mb-4">Implementation Strategy</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-white/80">
<div>
<div className="font-semibold mb-2">Architecture Pattern</div>
<div className="bg-white/10 rounded-lg p-3">{claude.implementation_strategy.architecture_pattern}</div>
</div>
<div>
<div className="font-semibold mb-2">Deployment Strategy</div>
<div className="bg-white/10 rounded-lg p-3">{claude.implementation_strategy.deployment_strategy}</div>
</div>
</div>
</div>
)}
<div className="text-center py-6">
<div className="space-x-4">
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10 cursor-pointer">Back</Button>
<Button onClick={onGenerate} className="bg-gradient-to-r from-orange-500 to-orange-600 text-white px-6 py-2 rounded-lg font-semibold cursor-pointer">Generate Architecture Design </Button>
</div>
<div className="text-white/60 text-sm mt-2">AI will design complete architecture</div>
</div>
</div>
)
}
// AI Mockup Step Component
function AIMockupStep({
template,
selectedFeatures,
onNext,
onBack,
}: {
template: Template;
selectedFeatures: TemplateFeature[];
onNext: () => void;
onBack: () => void
}) {
const [mounted, setMounted] = useState(false)
const [wireframeData, setWireframeData] = useState<any>(null)
const [isGenerating, setIsGenerating] = useState(false)
const [selectedDevice, setSelectedDevice] = useState<'desktop' | 'tablet' | 'mobile'>('desktop')
const [initialPrompt, setInitialPrompt] = useState<string>("")
// Load state from localStorage after component mounts
useEffect(() => {
setMounted(true)
if (typeof window !== 'undefined') {
// Load device type
const savedDevice = localStorage.getItem('wireframe_device_type')
if (savedDevice && ['desktop', 'tablet', 'mobile'].includes(savedDevice)) {
setSelectedDevice(savedDevice as 'desktop' | 'tablet' | 'mobile')
}
// Load wireframe data
const savedWireframeData = localStorage.getItem('wireframe_data')
if (savedWireframeData) {
try {
const parsed = JSON.parse(savedWireframeData)
setWireframeData(parsed)
} catch (error) {
console.error('Failed to parse saved wireframe data:', error)
}
}
}
}, [])
// 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) => {
setWireframeData(data)
setIsGenerating(false)
}
const handleWireframeGenerationStart = () => {
setIsGenerating(true)
}
const handleDeviceChange = (device: 'desktop' | 'tablet' | 'mobile') => {
console.log('DEBUG: AIMockupStep handleDeviceChange called with:', device)
console.log('DEBUG: Previous selectedDevice state:', selectedDevice)
setSelectedDevice(device)
// Save to localStorage (only after mounting)
if (mounted) {
localStorage.setItem('wireframe_device_type', device)
console.log('DEBUG: Saved device type to localStorage:', device)
}
console.log('DEBUG: New selectedDevice state:', device)
}
// Save wireframe data to localStorage when it changes (only after mounting)
useEffect(() => {
if (wireframeData && mounted) {
localStorage.setItem('wireframe_data', JSON.stringify(wireframeData))
}
}, [wireframeData, mounted])
// Debug: Log when selectedDevice prop changes
useEffect(() => {
console.log('DEBUG: AIMockupStep selectedDevice state changed to:', selectedDevice)
}, [selectedDevice])
// Debug: Log when selectedDevice prop changes
useEffect(() => {
console.log('DEBUG: WireframeCanvas selectedDevice prop changed to:', selectedDevice)
}, [selectedDevice])
return (
<div className="max-w-7xl mx-auto space-y-6">
<div className="text-center space-y-4">
<h1 className="text-4xl font-bold text-white">AI Wireframe Mockup</h1>
<p className="text-xl text-white/60 max-w-3xl mx-auto">
Generate and customize wireframes for {template.title} using AI-powered design tools
</p>
<div className="flex items-center justify-center gap-2 text-orange-400">
<Palette className="h-5 w-5" />
<span className="text-sm font-medium">AI-Powered Wireframe Generation</span>
</div>
</div>
{/* Dual Canvas Editor */}
<div className="bg-white/5 border border-white/10 rounded-xl overflow-hidden h-[80vh] min-h-[600px]">
<DualCanvasEditor
className="h-full w-full"
onWireframeGenerated={handleWireframeGenerated}
onGenerationStart={handleWireframeGenerationStart}
selectedDevice={selectedDevice}
onDeviceChange={handleDeviceChange}
initialPrompt={initialPrompt}
/>
</div>
{/* Wireframe Status and Controls */}
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center">
<div className="text-2xl font-bold text-white mb-2">
{wireframeData ? '✅' : '⏳'}
</div>
<div className="text-white/80 font-medium">
{wireframeData ? 'Wireframe Generated' : 'Ready to Generate'}
</div>
<div className="text-white/60 text-sm">
{wireframeData ? 'Click Continue to proceed' : 'Use the AI panel to create wireframes'}
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-white mb-2">
{selectedFeatures.length}
</div>
<div className="text-white/80 font-medium">Features Selected</div>
<div className="text-white/60 text-sm">
{template.title} template
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-white mb-2">
{isGenerating ? '🔄' : '🎨'}
</div>
<div className="text-white/80 font-medium">
{isGenerating ? 'Generating...' : 'AI Ready'}
</div>
<div className="text-white/60 text-sm">
{isGenerating ? 'Creating wireframe layout' : 'Claude AI powered'}
</div>
</div>
</div>
</div>
{/* Navigation */}
<div className="text-center py-6">
<div className="space-x-4">
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">
Back to Features
</Button>
<Button
onClick={onNext}
disabled={!wireframeData}
className={`bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow ${!wireframeData ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
Continue to Business Context
</Button>
</div>
<div className="text-white/60 text-sm mt-2">
{wireframeData
? 'Wireframe generated successfully! Continue to define business requirements.'
: 'Generate a wireframe first to continue to the next step.'
}
</div>
</div>
</div>
)
}
// Main Dashboard Component
export function MainDashboard() {
const [mounted, setMounted] = useState(false)
const [currentStep, setCurrentStep] = useState(1)
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(() => {
// Only access localStorage after component mounts to prevent hydration mismatch
return null
})
const [selectedFeatures, setSelectedFeatures] = useState<TemplateFeature[]>(() => {
// Only access localStorage after component mounts to prevent hydration mismatch
return []
})
const [finalProjectData, setFinalProjectData] = useState<any>(() => {
// Only access localStorage after component mounts to prevent hydration mismatch
return null
})
const [techStackRecommendations, setTechStackRecommendations] = useState<any>(() => {
// Only access localStorage after component mounts to prevent hydration mismatch
return null
})
// Load state from localStorage after component mounts
useEffect(() => {
setMounted(true)
if (typeof window !== 'undefined') {
// Load current step
const savedStep = localStorage.getItem('dashboard_current_step')
if (savedStep) {
setCurrentStep(parseInt(savedStep) || 1)
}
// Load selected template
const savedTemplate = localStorage.getItem('dashboard_selected_template')
if (savedTemplate) {
try {
setSelectedTemplate(JSON.parse(savedTemplate))
} catch (error) {
console.error('Failed to parse saved template:', error)
}
}
// Load selected features
const savedFeatures = localStorage.getItem('dashboard_selected_features')
if (savedFeatures) {
try {
setSelectedFeatures(JSON.parse(savedFeatures))
} catch (error) {
console.error('Failed to parse saved features:', error)
}
}
// Load final project data
const savedProjectData = localStorage.getItem('dashboard_final_project_data')
if (savedProjectData) {
try {
setFinalProjectData(JSON.parse(savedProjectData))
} catch (error) {
console.error('Failed to parse saved project data:', error)
}
}
// Load tech stack recommendations
const savedRecommendations = localStorage.getItem('dashboard_tech_stack_recommendations')
if (savedRecommendations) {
try {
setTechStackRecommendations(JSON.parse(savedRecommendations))
} catch (error) {
console.error('Failed to parse saved recommendations:', error)
}
}
}
}, [])
const steps = [
{ id: 1, name: "Project Type", description: "Choose template" },
{ id: 2, name: "Features", description: "Select features" },
{ id: 3, name: "AI Mockup", description: "Generate wireframes" },
{ id: 4, name: "Business Context", description: "Define requirements" },
{ id: 5, name: "Generate", description: "Create project" },
{ id: 6, name: "Architecture", description: "Review & deploy" },
]
// Save state to localStorage when it changes (only after mounting)
useEffect(() => {
if (mounted) {
localStorage.setItem('dashboard_current_step', currentStep.toString())
}
}, [currentStep, mounted])
useEffect(() => {
if (mounted) {
if (selectedTemplate) {
localStorage.setItem('dashboard_selected_template', JSON.stringify(selectedTemplate))
} else {
localStorage.removeItem('dashboard_selected_template')
}
}
}, [selectedTemplate, mounted])
useEffect(() => {
if (mounted) {
if (selectedFeatures.length > 0) {
localStorage.setItem('dashboard_selected_features', JSON.stringify(selectedFeatures))
} else {
localStorage.removeItem('dashboard_selected_features')
}
}
}, [selectedFeatures, mounted])
useEffect(() => {
if (mounted) {
if (finalProjectData) {
localStorage.setItem('dashboard_final_project_data', JSON.stringify(finalProjectData))
} else {
localStorage.removeItem('dashboard_final_project_data')
}
}
}, [finalProjectData, mounted])
useEffect(() => {
if (mounted) {
if (techStackRecommendations) {
localStorage.setItem('dashboard_tech_stack_recommendations', JSON.stringify(techStackRecommendations))
} else {
localStorage.removeItem('dashboard_tech_stack_recommendations')
}
}
}, [techStackRecommendations, mounted])
const renderStep = () => {
switch (currentStep) {
case 1:
return (
<TemplateSelectionStep
onNext={(template) => {
setSelectedTemplate(template)
setCurrentStep(2)
}}
/>
)
case 2:
return selectedTemplate ? (
<FeatureSelectionStep
template={selectedTemplate}
onNext={(sel) => { setSelectedFeatures(sel); setCurrentStep(3) }}
onBack={() => setCurrentStep(1)}
/>
) : null
case 3:
return selectedTemplate ? (
<AIMockupStep
template={selectedTemplate}
selectedFeatures={selectedFeatures}
onNext={() => setCurrentStep(4)}
onBack={() => setCurrentStep(2)}
/>
) : null
case 4:
return selectedTemplate ? (
<BusinessQuestionsStep
template={selectedTemplate}
selected={selectedFeatures}
onBack={() => setCurrentStep(3)}
onDone={(data, recs) => { setFinalProjectData(data); setTechStackRecommendations(recs); setCurrentStep(5) }}
/>
) : null
case 5:
return (
<TechStackSummaryStep
recommendations={techStackRecommendations}
completeData={finalProjectData}
onBack={() => setCurrentStep(4)}
onGenerate={() => setCurrentStep(6)}
/>
)
case 6:
return (
<ArchitectureDesignerStep
recommendations={techStackRecommendations}
onBack={() => setCurrentStep(5)}
/>
)
default:
return null
}
}
return (
<div className="min-h-screen bg-black">
{/* Progress Steps */}
{mounted && (
<div className="bg-white/5 border-b border-white/10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="py-4">
<nav className="flex justify-center">
<ol className="flex items-center space-x-8 text-white/60">
{steps.map((step, index) => (
<li key={`step-${step.id}`} className="flex items-center">
<div className="flex items-center">
<div
className={`flex items-center justify-center w-10 h-10 rounded-full border-2 transition-all ${currentStep >= step.id
? "bg-orange-500 border-orange-500 text-white shadow-lg"
: currentStep === step.id - 1
? "border-orange-300 text-orange-400"
: "border-white/30 text-white/40"
}`}
>
<span className="text-sm font-semibold">{step.id}</span>
</div>
<div className="ml-4">
<p
className={`text-sm font-semibold ${currentStep >= step.id ? "text-orange-400" : "text-white/60"
}`}
>
{step.name}
</p>
<p className="text-xs text-white/40">{step.description}</p>
</div>
</div>
{index < steps.length - 1 && (
<ArrowRight
key={`arrow-${step.id}`}
className={`ml-8 h-5 w-5 ${currentStep > step.id ? "text-orange-400" : "text-white/40"}`}
/>
)}
</li>
))}
</ol>
</nav>
</div>
</div>
</div>
)}
{/* Main Content */}
<main className="py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{!mounted ? (
<div className="flex items-center justify-center py-12">
<div className="text-center space-y-4">
<div className="animate-spin w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full mx-auto"></div>
<p className="text-white/60">Loading...</p>
</div>
</div>
) : (
renderStep()
)}
</div>
</main>
</div>
)
}
function ArchitectureDesignerStep({ recommendations, onBack }: { recommendations: any; onBack: () => void }) {
const [architectureDesign, setArchitectureDesign] = useState<any>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<'overview' | 'frontend' | 'backend' | 'database' | 'deployment'>('overview')
useEffect(() => {
const generate = async () => {
if (!recommendations) {
setError('Missing technology recommendations')
return
}
try {
setLoading(true)
setError(null)
const resp = await fetch(`${BACKEND_URL}/api/v1/design-architecture`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tech_stack_recommendations: recommendations }),
})
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const data = await resp.json()
setArchitectureDesign(data)
} catch (e: any) {
setError(e?.message || 'Failed to generate architecture')
} finally {
setLoading(false)
}
}
generate()
}, [JSON.stringify(recommendations)])
if (loading) {
return (
<div className="flex items-center justify-center py-24">
<div className="text-center text-white/80">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500 mx-auto mb-4"></div>
<div className="font-semibold mb-1">AI Designing Your Architecture</div>
<div className="text-white/60 text-sm">Creating React components, Node.js APIs, and PostgreSQL schema</div>
</div>
</div>
)
}
if (error) {
return (
<div className="text-center py-20">
<div className="max-w-md mx-auto bg-red-500/10 border border-red-500/30 rounded-lg p-6 text-red-300">
<div className="font-semibold mb-2">Architecture Generation Failed</div>
<div className="mb-4">{error}</div>
<div className="space-x-3">
<Button onClick={() => location.reload()} className="bg-red-500 hover:bg-red-400 text-black">Try Again</Button>
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Back</Button>
</div>
</div>
</div>
)
}
if (!architectureDesign) {
return (
<div className="text-center py-20">
<div className="max-w-md mx-auto bg-white/5 border border-white/10 rounded-lg p-6 text-white/80">
<div className="font-semibold mb-2">No Architecture Data</div>
<div className="mb-4">Please complete the tech stack selection first.</div>
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Go Back</Button>
</div>
</div>
)
}
const projectMeta = architectureDesign?.project_metadata
const arch = architectureDesign?.architecture_design
const tech = architectureDesign?.technology_specifications
return (
<div className="space-y-6">
<div className="bg-white/5 border-b border-white/10">
<div className="max-w-7xl mx-auto px-4 py-6 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-white">Architecture Design</h1>
<p className="text-white/60">AI-generated architecture for <span className="text-orange-400 font-semibold">{projectMeta?.project_name}</span></p>
</div>
<div className="space-x-3">
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Back</Button>
<Button onClick={() => location.reload()} className="bg-orange-500 hover:bg-orange-400 text-black">Regenerate</Button>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4">
<div className="border-b border-white/10 mb-6">
<nav className="-mb-px flex space-x-8">
{[
{ id: 'overview', name: 'Overview' },
{ id: 'frontend', name: 'Frontend (React)' },
{ id: 'backend', name: 'Backend (Node.js)' },
{ id: 'database', name: 'Database (PostgreSQL)' },
{ id: 'deployment', name: 'Deployment' },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`py-2 px-1 border-b-2 text-sm transition-colors ${activeTab === (tab.id as any)
? 'border-orange-400 text-orange-400'
: 'border-transparent text-white/60 hover:text-white/80 hover:border-white/20'
}`}
>
{tab.name}
</button>
))}
</nav>
</div>
{activeTab === 'overview' && (
<div className="space-y-6">
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-xl font-bold text-white mb-4">Project Overview</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-white/80">
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold">Project Name</div>
<div>{projectMeta?.project_name}</div>
</div>
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold">Complexity</div>
<div className="capitalize">{projectMeta?.complexity}</div>
</div>
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold">Generated</div>
<div>{projectMeta?.architecture_generated_at ? new Date(projectMeta.architecture_generated_at).toLocaleDateString() : '-'}</div>
</div>
</div>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-xl font-bold text-white mb-4">Technology Stack</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-white/80">
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold">Frontend</div>
<div>{tech?.frontend_framework}</div>
</div>
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold">Backend</div>
<div>{tech?.backend_language}</div>
</div>
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold">Database</div>
<div>{tech?.database_system}</div>
</div>
</div>
</div>
</div>
)}
{activeTab === 'frontend' && (
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-xl font-bold text-white mb-4">React Architecture</h3>
<pre className="bg-black/30 rounded-lg p-4 overflow-x-auto text-sm text-white/80">{JSON.stringify(arch?.frontend_architecture, null, 2)}</pre>
</div>
)}
{activeTab === 'backend' && (
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-xl font-bold text-white mb-4">Node.js Architecture</h3>
<pre className="bg-black/30 rounded-lg p-4 overflow-x-auto text-sm text-white/80">{JSON.stringify(arch?.backend_architecture, null, 2)}</pre>
</div>
)}
{activeTab === 'database' && (
<div className="bg-white/5 border border-white/10 rounded-xl p-6">
<h3 className="text-xl font-bold text-white mb-4">PostgreSQL Architecture</h3>
<pre className="bg-black/30 rounded-lg p-4 overflow-x-auto text-sm text-white/80">{JSON.stringify(arch?.database_architecture, null, 2)}</pre>
</div>
)}
{activeTab === 'deployment' && (
<div className="bg-white/5 border border-white/10 rounded-xl p-6 text-white/80">
<h3 className="text-xl font-bold text-white mb-4">Deployment Configuration</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold mb-2">Frontend Deployment</div>
<ul className="text-sm space-y-1">
<li> Vercel/Netlify hosting</li>
<li> React build optimization</li>
<li> CDN distribution</li>
</ul>
</div>
<div className="bg-white/5 rounded-lg p-4">
<div className="font-semibold mb-2">Backend Deployment</div>
<ul className="text-sm space-y-1">
<li> Docker containerization</li>
<li> AWS/GCP hosting</li>
<li> Auto-scaling setup</li>
</ul>
</div>
</div>
</div>
)}
</div>
</div>
)
}