codenuk_frontend_mine/src/components/main-dashboard.tsx
2025-10-16 10:53:36 +05:30

2792 lines
114 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 { 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, GitBranch } 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 TypeformSurvey, { SurveySummary } from "@/components/business-context/typeform-survey"
import PromptSidePanel from "@/components/prompt-side-panel"
import { DualCanvasEditor } from "@/components/dual-canvas-editor"
import { getAccessToken } from "@/components/apis/authApiClients"
import TechStackSummary from "@/components/tech-stack-summary"
import { attachRepository, detectProvider, connectProvider, AttachRepositoryResponse } from "@/lib/api/vcs"
import { getGitHubAuthStatus } from "@/lib/api/github"
import ViewUserReposButton from "@/components/github/ViewUserReposButton"
import { ErrorBanner } from "@/components/ui/error-banner"
import { useAuth } from "@/contexts/auth-context"
import { authApiClient } 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 { user } = useAuth()
const [selectedCategory, setSelectedCategory] = useState("all")
const [searchQuery, setSearchQuery] = useState("")
const [showCustomForm, setShowCustomForm] = useState(false)
const [showCreateOptionDialog, setShowCreateOptionDialog] = useState(false)
const [showGitForm, setShowGitForm] = useState(false)
const [gitProvider, setGitProvider] = useState('')
const [gitUrl, setGitUrl] = useState('')
const [gitBranch, setGitBranch] = useState('main')
const [gitAuthMethod, setGitAuthMethod] = useState('')
const [gitCredentials, setGitCredentials] = useState({
username: '',
password: '',
token: '',
sshKey: ''
})
const [authLoading, setAuthLoading] = useState(false)
const [gitStep, setGitStep] = useState<'provider' | 'url'>('provider')
// Sync progress state for private repositories
const [syncProgress, setSyncProgress] = useState<{
show: boolean;
provider: string;
repositoryUrl: string;
branchName: string;
status: string;
stage: string;
} | null>(null)
// Monitor private repository sync progress
const monitorPrivateRepoSync = async (provider: string, repositoryUrl: string) => {
let pollCount = 0;
const maxPolls = 60; // 2 minutes with 2-second intervals (reduced from 5 minutes)
const pollInterval = setInterval(async () => {
pollCount++;
try {
// Poll for repository sync status
const response = await authApiClient.get(`/api/vcs/${provider}/repositories?t=${Date.now()}`, {
headers: { 'x-user-id': user?.id }
});
if (response.data?.success) {
const repositories = response.data.data || [];
const repo = repositories.find((r: any) => r.repository_url === repositoryUrl);
if (repo) {
const status = repo.sync_status;
let stage = '';
switch (status) {
case 'authenticating':
stage = 'Authenticating with provider...';
break;
case 'syncing':
stage = 'Downloading repository files...';
break;
case 'synced':
stage = 'Repository sync completed!';
clearInterval(pollInterval);
setSyncProgress(null);
alert(`✅ Repository attached successfully!\n\nProvider: ${provider.toUpperCase()}\nRepository: ${repositoryUrl}\n\nYour repository is now available in your repositories list.`);
// Refresh repositories list
window.location.reload();
return;
case 'error':
stage = 'Sync failed. Please try again.';
clearInterval(pollInterval);
setTimeout(() => {
setSyncProgress(null);
}, 3000);
alert(`❌ Repository sync failed!\n\nProvider: ${provider.toUpperCase()}\nRepository: ${repositoryUrl}\n\nPlease try again or contact support.`);
return;
default:
stage = 'Processing...';
}
setSyncProgress(prev => prev ? { ...prev, status, stage } : null);
} else {
// Repository not found in list yet, continue polling
setSyncProgress(prev => prev ? { ...prev, stage: 'Processing...' } : null);
}
}
} catch (error) {
console.error('Error monitoring sync:', error);
// If we get too many errors, stop polling
if (pollCount > 10) {
clearInterval(pollInterval);
setSyncProgress(null);
alert(`⚠️ Unable to monitor repository sync status.\n\nRepository: ${repositoryUrl}\n\nPlease check your repositories list manually.`);
}
}
// Stop polling after max attempts
if (pollCount >= maxPolls) {
clearInterval(pollInterval);
setSyncProgress(null);
alert(`⏰ Repository sync is taking longer than expected.\n\nRepository: ${repositoryUrl}\n\nPlease check your repositories list manually.`);
}
}, 2000); // Poll every 2 seconds
}
const [isGithubConnected, setIsGithubConnected] = useState<boolean | null>(null)
const [connectionError, setConnectionError] = useState<string | null>(null)
// Cleanup sync progress on component unmount
useEffect(() => {
return () => {
setSyncProgress(null);
};
}, []);
useEffect(() => {
(async () => {
try {
setConnectionError(null)
const status = await getGitHubAuthStatus()
setIsGithubConnected(!!status?.data?.connected)
} catch (error: any) {
console.warn('Failed to check GitHub auth status:', error)
setIsGithubConnected(false)
// Check if it's a connectivity issue
if (error?.code === 'ECONNREFUSED' || error?.code === 'ENOTFOUND' || error?.code === 'ETIMEDOUT') {
setConnectionError('Unable to connect to the server. Please ensure the backend is running.')
} else if (error?.response?.status >= 500) {
setConnectionError('Server error. Please try again later.')
}
}
})()
}, [])
// Handle OAuth callback for all providers with enhanced private repo flow
useEffect(() => {
const handleVcsCallback = async () => {
// Check for private repo sync info from project-builder redirect
try {
const syncInfo = sessionStorage.getItem('private_repo_sync');
if (syncInfo) {
const { provider, repositoryUrl, branchName, syncStatus } = JSON.parse(syncInfo);
console.log(`🔄 [Main Dashboard] Restoring private repo sync:`, { provider, repositoryUrl, syncStatus });
setSyncProgress({
show: true,
provider,
repositoryUrl,
branchName,
status: syncStatus,
stage: 'Starting sync...'
});
// Start monitoring sync progress
monitorPrivateRepoSync(provider, repositoryUrl);
// Clear the stored info
sessionStorage.removeItem('private_repo_sync');
}
} catch (e) {
console.warn('Failed to restore sync info:', e);
}
const urlParams = new URLSearchParams(window.location.search);
const oauthSuccess = urlParams.get('oauth_success');
const provider = urlParams.get('provider');
const syncPrivateRepo = urlParams.get('sync_private_repo');
const repositoryUrl = urlParams.get('repository_url');
const branchName = urlParams.get('branch_name');
const syncStatus = urlParams.get('sync_status');
// Handle OAuth success redirect from backend
if (oauthSuccess === 'true' && provider) {
console.log(`🔐 [VCS OAuth] OAuth success for ${provider.toUpperCase()}`);
if (syncPrivateRepo === 'true' && repositoryUrl) {
console.log(`🔄 [VCS OAuth] Starting private repository sync monitoring: ${repositoryUrl}`);
// Show progress notification for private repo sync
setSyncProgress({
show: true,
provider: provider,
repositoryUrl: repositoryUrl,
branchName: branchName || 'main',
status: syncStatus || 'authenticating',
stage: 'Starting sync...'
});
// Start monitoring sync progress
monitorPrivateRepoSync(provider, repositoryUrl);
// Clean up URL parameters but keep sync progress visible
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
} else {
const attachRepo = urlParams.get('attach_repo');
if (attachRepo === 'true' && repositoryUrl) {
console.log(`🔄 [VCS OAuth] Auto-attaching repository after OAuth: ${repositoryUrl}`);
try {
// Automatically attach the repository
const response = await authApiClient.post(`/api/vcs/${provider}/attach-repository`, {
repository_url: repositoryUrl,
branch_name: branchName || undefined,
user_id: user?.id
}, {
headers: {
'x-user-id': user?.id
}
});
if (response.data?.success) {
alert(`${provider.toUpperCase()} account connected and repository attached successfully!`);
// Clean up URL parameters
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
// Reset form and close dialogs
setShowGitForm(false);
setShowCreateOptionDialog(false);
setGitUrl('');
setGitBranch('main');
setGitProvider('');
} else {
alert(`${provider.toUpperCase()} account connected, but failed to attach repository. Please try again.`);
}
} catch (attachError) {
console.error('Failed to attach repository after OAuth:', attachError);
alert(`${provider.toUpperCase()} account connected, but failed to attach repository. Please try again.`);
}
} else {
console.log(`🔐 [VCS OAuth] ${provider.toUpperCase()} account connected successfully`);
alert(`${provider.toUpperCase()} account connected successfully!`);
// Clean up URL parameters
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
}
}
}
};
handleVcsCallback();
}, [user])
const [editingTemplate, setEditingTemplate] = useState<DatabaseTemplate | null>(null)
// Authentication handlers for different providers
const handleOAuthAuth = async (provider: string) => {
setAuthLoading(true)
try {
console.log(`🔐 [handleOAuthAuth] Starting OAuth for provider: ${provider}`)
// Use the new VCS API for all providers
await connectProvider(provider)
} catch (error) {
console.error('OAuth error:', error)
alert('OAuth authentication failed')
} finally {
setAuthLoading(false)
}
}
const handleTokenAuth = async (provider: string, token: string) => {
setAuthLoading(true)
try {
const providerConfig = gitProviders[provider as keyof typeof gitProviders]
// Validate token with provider API
const response = await fetch(`${providerConfig.apiEndpoint}/user`, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
})
if (!response.ok) {
throw new Error('Invalid token')
}
const userData = await response.json()
alert(`Authenticated as ${userData.login || userData.username || userData.name}`)
return true
} catch (error) {
console.error('Token auth error:', error)
alert('Token authentication failed')
return false
} finally {
setAuthLoading(false)
}
}
const handleUsernamePasswordAuth = async (provider: string, username: string, password: string) => {
setAuthLoading(true)
try {
// For username/password auth, you'd typically use Basic Auth
// or convert to token-based auth
const credentials = btoa(`${username}:${password}`)
const response = await fetch(`${gitProviders[provider as keyof typeof gitProviders].apiEndpoint}/user`, {
headers: {
'Authorization': `Basic ${credentials}`,
'Accept': 'application/json'
}
})
if (!response.ok) {
throw new Error('Invalid credentials')
}
const userData = await response.json()
alert(`Authenticated as ${userData.login || userData.username || userData.name}`)
return true
} catch (error) {
console.error('Username/password auth error:', error)
alert('Authentication failed')
return false
} finally {
setAuthLoading(false)
}
}
// Provider-specific configuration
const gitProviders = {
github: {
name: 'GitHub',
icon: 'M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z',
placeholder: 'https://github.com/org/repo.git',
authMethods: ['token', 'ssh', 'oauth'],
oauthEndpoint: '/api/vcs/github/auth/start',
apiEndpoint: 'https://api.github.com'
},
bitbucket: {
name: 'Bitbucket',
icon: 'M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891L.778 1.213zM14.518 18.197H9.482l-1.4-8.893h7.436l-1.4 8.893z',
placeholder: 'https://bitbucket.org/org/repo.git',
authMethods: ['username_password', 'app_password', 'oauth'],
oauthEndpoint: '/api/vcs/bitbucket/auth/start',
apiEndpoint: 'https://api.bitbucket.org/2.0'
},
gitlab: {
name: 'GitLab',
icon: 'M23.6004 9.5927l-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.874.874 0 0 0-.9997.0539.874.874 0 0 0-.29.4399l-2.5465 7.7838H7.2162l-2.5465-7.7838a.857.857 0 0 0-.29-.4412.874.874 0 0 0-.9997-.0537.858.858 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.065 6.065 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.008 1.008 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7466.0125-.01a6.068 6.068 0 0 0 2.0094-7.003z',
placeholder: 'https://gitlab.com/org/repo.git',
authMethods: ['token', 'oauth', 'ssh'],
oauthEndpoint: '/api/vcs/gitlab/auth/start',
apiEndpoint: 'https://gitlab.com/api/v4'
},
gitea: {
name: 'Gitea',
icon: 'M12 0C5.374 0 0 5.373 0 12s5.374 12 12 12 12-5.373 12-12S18.626 0 12 0zm0 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-6h2v2h-2v-2zm0-8h2v6h-2V8z',
placeholder: 'https://gitea.com/org/repo.git',
authMethods: ['token', 'oauth', 'ssh'],
oauthEndpoint: '/api/vcs/gitea/auth/start',
apiEndpoint: 'https://gitea.com/api/v1'
}
}
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: templateUser,
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: (templateUser 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 handleCreateFromGit = async () => {
try {
if (!gitUrl.trim()) {
alert('Please enter a Git repository URL');
return;
}
// Detect provider from URL
const detectedProvider = detectProvider(gitUrl.trim());
console.log('🔍 [handleCreateFromGit] Detected provider:', detectedProvider);
console.log('🔍 [handleCreateFromGit] Git URL:', gitUrl.trim());
// Check if repository already exists for this user
try {
const existingReposResponse = await authApiClient.get(`/api/vcs/${detectedProvider}/repositories?t=${Date.now()}`, {
headers: { 'x-user-id': user?.id }
});
if (existingReposResponse.data?.success) {
const repositories = existingReposResponse.data.data || [];
const existingRepo = repositories.find((r: any) => r.repository_url === gitUrl.trim());
if (existingRepo) {
alert(`✅ Repository already exists!\n\nProvider: ${detectedProvider.toUpperCase()}\nRepository: ${gitUrl.trim()}\nStatus: ${existingRepo.sync_status}\n\nThis repository is already available in your repositories list.`);
setShowCreateOptionDialog(false);
setShowGitForm(false);
return;
}
}
} catch (error) {
console.log('🔍 [handleCreateFromGit] Could not check existing repositories, proceeding with attachment:', error);
// Continue with attachment even if we can't check existing repos
}
// Attach the repository via backend (skip template creation)
try {
const attachResult = await attachRepository({
repository_url: gitUrl.trim(),
branch_name: (gitBranch?.trim() || undefined),
})
// Check if authentication is required
if (attachResult.requires_auth) {
console.log('🔐 Private repository detected, auto-redirecting to OAuth:', attachResult)
// Store the auth_url for OAuth redirect
try {
sessionStorage.setItem('pending_git_attach', JSON.stringify({
repository_url: gitUrl.trim(),
branch_name: (gitBranch?.trim() || undefined),
provider: detectedProvider,
auth_url: attachResult.auth_url
}))
console.log('💾 Stored pending git attach with auth_url in sessionStorage')
} catch (e) {
console.warn('⚠️ Failed to store pending git attach:', e)
}
// Auto-redirect to OAuth
console.log('🔐 Private repository detected - auto-redirecting to OAuth:', attachResult.auth_url)
if (attachResult.auth_url) {
window.location.replace(attachResult.auth_url)
} else {
alert('Authentication URL not available. Please try again.')
}
return
}
// Show success message and reset form
alert('Repository attached successfully! You can now proceed with your project.');
setShowCreateOptionDialog(false)
setShowGitForm(false)
setGitProvider('')
setGitUrl('')
setGitBranch('main')
// Return a mock template object to proceed to next step
return {
id: 'git-imported',
title: `Imported from ${detectedProvider.charAt(0).toUpperCase() + detectedProvider.slice(1)}`,
description: `Template imported from ${detectedProvider.charAt(0).toUpperCase() + detectedProvider.slice(1)}: ${gitUrl}`,
type: 'custom',
category: 'imported',
is_custom: true,
source: 'git',
git_url: gitUrl.trim(),
git_branch: gitBranch?.trim() || 'main',
git_provider: detectedProvider
}
} catch (attachErr) {
console.error('[TemplateSelectionStep] attachRepository failed:', attachErr)
const err: any = attachErr
const status = err?.response?.status
const data = err?.response?.data
console.log('🔍 HandleCreateFromGit error response:', { status, data })
console.log('🔍 Error details:', {
message: err?.message,
requires_auth: data?.requires_auth,
auth_url: data?.auth_url
})
console.log('🔍 Full error object:', err)
console.log('🔍 Error response object:', err?.response)
// If backend signals auth required, show authentication button instead of auto-redirect
if ((status === 401 || status === 200) && (data?.requires_auth || data?.message?.includes('authentication'))) {
console.log('🔐 Private repository detected, showing authentication button:', { status, requires_auth: data?.requires_auth, message: data?.message })
// Store the auth_url for when user clicks the authenticate button
try {
sessionStorage.setItem('pending_git_attach', JSON.stringify({
repository_url: gitUrl.trim(),
branch_name: (gitBranch?.trim() || undefined),
provider: detectedProvider,
auth_url: data?.auth_url
}))
console.log('💾 Stored pending git attach with auth_url in sessionStorage')
} catch (e) {
console.warn('⚠️ Failed to store pending git attach:', e)
}
// Don't auto-redirect, let the user click the authenticate button
console.log('🔐 Private repository detected - user must click authenticate button')
return
}
if (status === 403) {
alert('Repository not accessible - you may not have permission to access this repository')
return
}
// Fallback: if we have an auth_url in the error, try to redirect anyway
if (data?.auth_url && !data?.success) {
console.log('🔐 Fallback OAuth redirect - auth_url found in error response:', data.auth_url)
window.location.replace(data.auth_url)
return
}
// Additional fallback: check if the error message contains auth_url
if (err?.message && err.message.includes('authentication') && data?.auth_url) {
console.log('🔐 Additional fallback OAuth redirect - authentication message with auth_url:', data.auth_url)
window.location.replace(data.auth_url)
return
}
if (status === 404) {
alert('Repository not found - please check the URL and try again')
return
}
alert(data?.message || 'Failed to attach repository. Please verify the URL/branch and your auth.');
return;
}
} catch (error) {
console.error('[TemplateSelectionStep] Error importing from Git:', error)
const errorMessage = error instanceof Error ? error.message : 'Failed to import from Git'
alert(`Error: ${errorMessage}`)
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={() => {
const returnUrl = encodeURIComponent(window.location.pathname + window.location.search)
window.location.href = `/signin?returnUrl=${returnUrl}`
}}
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">
{/* Sync Progress Notification */}
{syncProgress?.show && (
<div className="bg-orange-500/10 border border-orange-500/20 rounded-lg p-4 mb-6">
<div className="flex items-center space-x-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-orange-500"></div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-white">
Syncing {syncProgress.provider.toUpperCase()} Repository
</h3>
<p className="text-white/80 text-sm">
{syncProgress.stage}
</p>
<div className="mt-2 text-xs text-white/60">
Repository: {syncProgress.repositoryUrl}
{syncProgress.branchName !== 'main' && ` | Branch: ${syncProgress.branchName}`}
</div>
</div>
<div className="flex items-center space-x-2">
<div className={`px-2 py-1 rounded text-xs font-medium ${
syncProgress.status === 'synced' ? 'bg-green-500/20 text-green-400' :
syncProgress.status === 'error' ? 'bg-red-500/20 text-red-400' :
'bg-orange-500/20 text-orange-400'
}`}>
{syncProgress.status}
</div>
<button
onClick={() => setSyncProgress(null)}
className="text-white/60 hover:text-white/80 transition-colors"
title="Dismiss sync progress"
>
</button>
</div>
</div>
</div>
)}
{/* 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>
{/* Connection Error Banner */}
{connectionError && (
<div className="max-w-2xl mx-auto">
<ErrorBanner
title="Connection Issue"
message={connectionError}
onRetry={async () => {
setConnectionError(null)
// Retry the GitHub auth status check
try {
const status = await getGitHubAuthStatus()
setIsGithubConnected(!!status?.data?.connected)
} catch (error: any) {
console.warn('Retry failed:', error)
setIsGithubConnected(false)
if (error?.code === 'ECONNREFUSED' || error?.code === 'ENOTFOUND' || error?.code === 'ETIMEDOUT') {
setConnectionError('Unable to connect to the server. Please ensure the backend is running.')
}
}
}}
/>
</div>
)}
{!templateUser?.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={() => {
const returnUrl = encodeURIComponent(window.location.pathname + window.location.search)
window.location.href = `/signin?returnUrl=${returnUrl}`
}}
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>
{/* Right-aligned quick navigation to user repos */}
<div className="flex justify-end space-x-2">
<ViewUserReposButton className="bg-orange-500 hover:bg-orange-400 text-black" label="My Repository" />
</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={() => {
if (!templateUser?.id) { window.location.href = '/signin'; return }
setShowCreateOptionDialog(true)
}}>
<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">
{templateUser?.id ? 'Create Custom Template' : 'Sign In to Create Templates'}
</h3>
<p className="mb-8 max-w-md mx-auto text-lg leading-relaxed">
{templateUser?.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" onClick={(e) => {
e.stopPropagation()
if (!templateUser?.id) { window.location.href = '/signin'; return }
setShowCreateOptionDialog(true)
}}>
{templateUser?.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>
{/* Create Template Options Modal */}
<Dialog open={showCreateOptionDialog} onOpenChange={(open) => {
setShowCreateOptionDialog(open)
if (!open) {
setShowGitForm(false)
setGitProvider('')
setGitUrl('')
setGitBranch('main')
setGitAuthMethod('')
setGitCredentials({ username: '', password: '', token: '', sshKey: '' })
setGitStep('provider')
}
}}>
<DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton>
<DialogHeader>
<DialogTitle className="text-white">Create Template</DialogTitle>
<DialogDescription className="text-white/80">
Choose how you want to create a template.
</DialogDescription>
</DialogHeader>
{!showGitForm ? (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<Button className="bg-orange-500 hover:bg-orange-400 text-black" onClick={() => {
setShowCreateOptionDialog(false)
setShowCustomForm(true)
}}>Create Manually</Button>
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={() => {
setShowGitForm(true)
setGitStep('provider')
}}>
Import from Git
</Button>
</div>
) : gitStep === 'provider' ? (
<div className="space-y-4">
<div>
<label className="block text-sm text-white/70 mb-3">Select Git Provider</label>
<div className="grid grid-cols-2 gap-3">
{Object.entries(gitProviders).map(([key, provider]) => (
<Button
key={key}
variant="outline"
className="border-white/20 text-white hover:bg-white/10 h-12 flex flex-col items-center gap-1"
onClick={() => {
setGitProvider(key)
setGitStep('url')
}}
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d={provider.icon}/>
</svg>
{provider.name}
</Button>
))}
</div>
</div>
<div className="flex gap-3 justify-end pt-2">
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={() => { setShowGitForm(false) }}>Back</Button>
</div>
</div>
) : (
<div className="space-y-4">
<div className="flex items-center gap-2 mb-4">
<Button
variant="ghost"
size="sm"
className="text-white/70 hover:text-white p-1"
onClick={() => setGitStep('provider')}
>
Back to Provider
</Button>
<span className="text-sm text-white/70">
Import from {gitProviders[gitProvider as keyof typeof gitProviders]?.name}
</span>
</div>
<div>
<label className="block text-sm text-white/70 mb-1">Repository URL</label>
<Input
value={gitUrl}
onChange={(e) => {
setGitUrl(e.target.value)
}}
placeholder={gitProviders[gitProvider as keyof typeof gitProviders]?.placeholder}
className="bg-white/10 border-white/20 text-white"
/>
<p className="text-xs text-white/50 mt-1">
Enter the full URL to your {gitProviders[gitProvider as keyof typeof gitProviders]?.name} repository
</p>
</div>
<div>
<label className="block text-sm text-white/70 mb-1">Branch (optional)</label>
<Input
value={gitBranch}
onChange={(e) => setGitBranch(e.target.value)}
placeholder="main"
className="bg-white/10 border-white/20 text-white"
/>
<p className="text-xs text-white/50 mt-1">
Leave empty to use the default branch
</p>
</div>
<div className="flex gap-3 justify-end pt-2">
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10" onClick={() => setGitStep('provider')}>Back</Button>
<Button
className="bg-orange-500 hover:bg-orange-400 text-black"
type="button"
onClick={(e) => { e.preventDefault(); handleCreateFromGit(); }}
disabled={!gitUrl.trim()}
>
Import Template
</Button>
</div>
</div>
)}
</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 [essentialFeatures, setEssentialFeatures] = useState<TemplateFeature[]>([])
const [customFeatures, setCustomFeatures] = 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)
// Separate custom features from essential features
setEssentialFeatures(data.filter(f => f.feature_type !== 'custom'))
setCustomFeatures(data.filter(f => f.feature_type === 'custom'))
} 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 from essential and suggested features.</p>
</div>
{essentialFeatures.length > 0 && section('Essential Features', essentialFeatures)}
{/* 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>
{customFeatures.length > 0 && section('Custom Features', customFeatures)}
{(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([...essentialFeatures, ...customFeatures].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 [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [showSummary, setShowSummary] = useState(false)
const [answers, setAnswers] = useState<any[]>([])
const [submitting, setSubmitting] = useState(false)
const [answeredQuestionsCount, setAnsweredQuestionsCount] = useState(0)
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)
} catch (e: any) {
setError(e?.message || 'Failed to load questions')
} finally {
setLoading(false)
}
}
load()
}, [template.id, selectedKey])
const handleSurveyComplete = (surveyAnswers: any[]) => {
setAnswers(surveyAnswers)
setAnsweredQuestionsCount(surveyAnswers.length)
setShowSummary(true)
}
const handleSurveyProgress = (currentAnswers: any[]) => {
const answeredCount = currentAnswers.filter(answer => answer.answer && answer.answer.trim().length > 0).length
setAnsweredQuestionsCount(answeredCount)
// Store the current answers so they're available for submission
setAnswers(currentAnswers)
}
const handleSubmit = async () => {
try {
setSubmitting(true)
setError(null)
const token = getAccessToken()
// Filter to only include answered questions
const answeredQuestions = answers.filter(answer => answer.answer && answer.answer.trim().length > 0)
const questionsWithAnswers = answeredQuestions.map(answer => ({
question: answer.question,
answer: answer.answer,
}))
const completeData = {
template,
features: selected,
businessContext: {
questions: questionsWithAnswers,
},
projectName: template.title,
projectType: template.type || template.category,
}
console.log('🚀 Submitting comprehensive recommendations request...')
console.log('📊 Total answers received:', answers.length)
console.log('📊 Answered questions:', answeredQuestions.length)
console.log('📊 Questions with answers:', questionsWithAnswers)
console.log('📊 Complete data:', completeData)
const response = await fetch(`${BACKEND_URL}/api/unified/comprehensive-recommendations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({
template,
features: selected,
businessContext: {
questions: questionsWithAnswers,
},
projectName: template.title,
projectType: template.type || template.category,
templateId: template.id,
budget: 15000, // Default budget - could be made configurable
domain: template.category?.toLowerCase() || 'general',
includeClaude: true,
includeTemplateBased: true,
includeDomainBased: true,
}),
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const recommendations = await response.json()
console.log('✅ Comprehensive recommendations received:', recommendations)
// Extract the tech recommendations from the response for the frontend component
const techRecommendations = recommendations?.data?.claude?.data?.claude_recommendations || null
console.log('🎯 Extracted tech recommendations:', techRecommendations)
// Proceed to next step with complete data and recommendations
onDone(completeData, techRecommendations)
} catch (error: any) {
console.error('❌ Error submitting business context:', error)
setError(error?.message || 'Failed to submit business context')
} finally {
setSubmitting(false)
}
}
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>
)
}
if (showSummary) {
return (
<SurveySummary
questions={answers}
onBack={() => setShowSummary(false)}
onSubmit={handleSubmit}
loading={submitting}
/>
)
}
return (
<div className="max-w-6xl mx-auto space-y-4">
<div className="text-center space-y-2">
<h1 className="text-3xl font-bold text-white">Business Context Questions</h1>
<p className="text-lg text-white/60 max-w-2xl mx-auto">
Help us understand your {template.title} project better with these AI-generated questions
</p>
</div>
<TypeformSurvey
questions={businessQuestions}
onComplete={handleSurveyComplete}
onProgress={handleSurveyProgress}
onBack={onBack}
projectName={template.title}
/>
{/* Navigation buttons */}
<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 to Features
</Button>
<Button
onClick={handleSubmit}
disabled={submitting || answeredQuestionsCount < 3}
className={`bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow cursor-pointer ${
submitting || answeredQuestionsCount < 3 ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{submitting ? (
<div className="flex items-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-black"></div>
Generating Tech Stack...
</div>
) : (
<>
Generate Tech Stack
</>
)}
</Button>
</div>
<div className="text-white/60 text-sm mt-2">
{answeredQuestionsCount < 3
? `Answer at least 3 questions to generate your tech stack recommendations (${answeredQuestionsCount}/3 answered)`
: `Ready to generate tech stack for ${template.title} (${answeredQuestionsCount} questions answered)`
}
</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)
}
}
}
}, [mounted])
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: 3, name: "Business Context", description: "Define requirements" },
{ id: 4, name: "Generate", description: "Create project" },
{ id: 5, 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 ? (
<BusinessQuestionsStep
template={selectedTemplate}
selected={selectedFeatures}
onBack={() => setCurrentStep(2)}
onDone={(data, recs) => { setFinalProjectData(data); setTechStackRecommendations(recs); setCurrentStep(4) }}
/>
) : null
case 4:
console.log('🔍 MainDashboard - techStackRecommendations:', techStackRecommendations)
console.log('🔍 MainDashboard - finalProjectData:', finalProjectData)
return (
<TechStackSummary
recommendations={techStackRecommendations}
completeData={finalProjectData}
onBack={() => setCurrentStep(3)}
onGenerate={(userTechStack) => {
// Store user's tech stack if provided
if (userTechStack) {
console.log('Storing user tech stack:', userTechStack)
// You can store this in state or pass it to the next step
}
setCurrentStep(5)
}}
/>
)
case 5:
return (
<ArchitectureDesignerStep
recommendations={techStackRecommendations}
onBack={() => setCurrentStep(4)}
/>
)
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>
)
}