Updated frontend codebase

This commit is contained in:
tejas.prakash 2025-09-04 09:13:55 +05:30
parent ff99b4df53
commit 81ef6e8954
19 changed files with 5205 additions and 219 deletions

1972
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.12",
"@tldraw/tldraw": "^3.15.4",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@ -28,6 +29,7 @@
"next": "15.4.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"svg-path-parser": "^1.1.0",
"tailwind-merge": "^3.3.1",
"zustand": "^5.0.8"
},
@ -37,6 +39,7 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/svg-path-parser": "^1.1.6",
"eslint": "^9",
"eslint-config-next": "15.4.6",
"tailwindcss": "^4",

View File

@ -0,0 +1,215 @@
"use client"
import { useMemo, useState } from "react"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import { ScrollArea } from "@/components/ui/scroll-area"
import { cn } from "@/lib/utils"
type Block = {
id: string
label: string
x: number
y: number
w: number
h: number
}
export type WireframePlan = {
version: number
blocks: Block[]
}
export function AISidePanel({
onGenerate,
onClear,
className,
}: {
onGenerate: (plan: WireframePlan) => void
onClear: () => void
className?: string
}) {
const [collapsed, setCollapsed] = useState(false)
const [prompt, setPrompt] = useState(
"Dashboard with header, left sidebar, 3 stats cards, a line chart and a data table, plus footer.",
)
const [version, setVersion] = useState(1)
const examples = useMemo(
() => [
"Landing page with header, hero, 3x2 feature grid, and footer.",
"Settings screen: header, list of toggles, and save button.",
"Ecommerce product page: header, 2-column gallery/details, reviews, sticky add-to-cart.",
"Dashboard: header, left sidebar, 3 stats cards, line chart, data table, footer.",
"Signup page: header, 2-column form, callout, submit button.",
],
[],
)
function parsePromptToPlan(input: string): WireframePlan {
const W = 1200
const H = 800
const P = 24
const blocks: Block[] = []
let y = P
let x = P
let width = W - 2 * P
const lower = input.toLowerCase()
const addBlock = (label: string, bx: number, by: number, bw: number, bh: number) => {
blocks.push({
id: `${label}-${Math.random().toString(36).slice(2, 8)}`,
label,
x: Math.round(bx),
y: Math.round(by),
w: Math.round(bw),
h: Math.round(bh),
})
}
if (/\bheader\b/.test(lower) || /\bnavbar\b/.test(lower)) {
addBlock("Header", x, y, width, 72)
y += 72 + P
}
let hasSidebar = false
if (/\bsidebar\b/.test(lower) || /\bleft sidebar\b/.test(lower)) {
hasSidebar = true
addBlock("Sidebar", x, y, 260, H - y - P)
x += 260 + P
width = W - x - P
}
if (/\bhero\b/.test(lower)) {
addBlock("Hero", x, y, width, 200)
y += 200 + P
}
if (/stats?\b/.test(lower) || /\bcards?\b/.test(lower)) {
const cols = /4/.test(lower) ? 4 : 3
const gap = P
const cardW = (width - gap * (cols - 1)) / cols
const cardH = 100
for (let i = 0; i < cols; i++) {
addBlock(`Card ${i + 1}`, x + i * (cardW + gap), y, cardW, cardH)
}
y += cardH + P
}
const gridMatch = lower.match(/(\d)\s*x\s*(\d)/)
if (gridMatch) {
const cols = Number.parseInt(gridMatch[1], 10)
const rows = Number.parseInt(gridMatch[2], 10)
const gap = P
const cellW = (width - gap * (cols - 1)) / cols
const cellH = 120
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
addBlock(`Feature ${r * cols + c + 1}`, x + c * (cellW + gap), y + r * (cellH + gap), cellW, cellH)
}
}
y += rows * cellH + (rows - 1) * gap + P
}
if (/\b2[-\s]?column\b/.test(lower) || /gallery\/details/.test(lower) || /gallery/.test(lower)) {
const gap = P
const colW = (width - gap) / 2
const colH = 260
addBlock("Left Column", x, y, colW, colH)
addBlock("Right Column", x + colW + gap, y, colW, colH)
y += colH + P
}
if (/\bchart\b/.test(lower) || /\bline chart\b/.test(lower)) {
addBlock("Chart", x, y, width, 220)
y += 220 + P
}
if (/\btable\b/.test(lower)) {
addBlock("Data Table", x, y, width, 260)
y += 260 + P
}
if (/\bform\b/.test(lower) || /\bsignup\b/.test(lower) || /\blogin\b/.test(lower)) {
const gap = P
const twoCol = /\b2[-\s]?column\b/.test(lower)
if (twoCol) {
const colW = (width - gap) / 2
addBlock("Form Left", x, y, colW, 260)
addBlock("Form Right", x + colW + gap, y, colW, 260)
y += 260 + P
} else {
addBlock("Form", x, y, width, 220)
y += 220 + P
}
}
if (/\bfooter\b/.test(lower)) {
addBlock("Footer", P, H - 80 - P, W - 2 * P, 80)
}
return { version: Date.now() + version, blocks }
}
const handleGenerate = () => {
const plan = parsePromptToPlan(prompt)
setVersion((v) => v + 1)
onGenerate(plan)
}
return (
<aside
className={cn(
"h-full border-l bg-white dark:bg-neutral-900 flex flex-col",
collapsed ? "w-12" : "w-96",
className,
)}
aria-label="AI prompt side panel"
>
<div className="flex items-center justify-between px-3 py-2 border-b">
<h2 className={cn("text-sm font-medium text-balance", collapsed && "sr-only")}>AI Wireframe</h2>
<Button variant="ghost" size="icon" onClick={() => setCollapsed((c) => !c)} aria-label="Toggle panel">
{collapsed ? <span aria-hidden></span> : <span aria-hidden></span>}
</Button>
</div>
{!collapsed && (
<div className="flex flex-col gap-3 p-3">
<label className="text-xs font-medium">Prompt</label>
<Textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe your screen: Landing with header, hero, 3x2 features, footer"
className="min-h-28"
/>
<div className="flex gap-2">
<Button onClick={handleGenerate} className="flex-1">
Generate
</Button>
<Button variant="secondary" onClick={onClear}>
Clear
</Button>
</div>
<div className="pt-1">
<p className="text-xs font-medium mb-2">Examples</p>
<ScrollArea className="h-40 border rounded">
<ul className="p-2 space-y-2">
{examples.map((ex) => (
<li key={ex}>
<button
type="button"
onClick={() => setPrompt(ex)}
className="text-left text-xs w-full hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded px-2 py-1"
>
{ex}
</button>
</li>
))}
</ul>
</ScrollArea>
</div>
</div>
)}
</aside>
)
}

View File

@ -31,7 +31,6 @@ export function SignUpForm({ onSignUpSuccess }: SignUpFormProps) {
role: "user" // default role, adjust as needed
})
const { signup } = useAuth()
const router = useRouter()
const handleSubmit = async (e: React.FormEvent) => {

View File

@ -7,13 +7,15 @@ 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 { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2 } from "lucide-react"
import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2, 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 WireframeCanvas from "@/components/wireframe-canvas"
import PromptSidePanel from "@/components/prompt-side-panel"
interface Template {
id: string
@ -934,22 +936,315 @@ function TechStackSummaryStep({
)
}
// 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')
// Load state from localStorage after component mounts
useEffect(() => {
setMounted(true)
// 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)
}
}
}, [])
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>
{/* Wireframe Canvas and Panel */}
<div className="bg-white/5 border border-white/10 rounded-xl overflow-hidden" style={{ height: '600px' }}>
<div className="flex h-full">
{/* Main Canvas Area */}
<div className="flex-1 relative">
<WireframeCanvas
className="h-full w-full"
onWireframeGenerated={handleWireframeGenerated}
onGenerationStart={handleWireframeGenerationStart}
selectedDevice={selectedDevice}
/>
</div>
{/* AI Prompt Panel */}
<PromptSidePanel
className="border-l border-white/10"
selectedDevice={selectedDevice}
onDeviceChange={handleDeviceChange}
/>
</div>
</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>(null)
const [selectedFeatures, setSelectedFeatures] = useState<TemplateFeature[]>([])
const [finalProjectData, setFinalProjectData] = useState<any>(null)
const [techStackRecommendations, setTechStackRecommendations] = useState<any>(null)
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)
// 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: "Business Context", description: "Define requirements" },
{ id: 4, name: "Generate", description: "Create project" },
{ id: 5, name: "Architecture", description: "Review & deploy" },
{ 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:
@ -971,27 +1266,36 @@ export function MainDashboard() {
) : null
case 3:
return selectedTemplate ? (
<BusinessQuestionsStep
<AIMockupStep
template={selectedTemplate}
selected={selectedFeatures}
selectedFeatures={selectedFeatures}
onNext={() => setCurrentStep(4)}
onBack={() => setCurrentStep(2)}
onDone={(data, recs) => { setFinalProjectData(data); setTechStackRecommendations(recs); setCurrentStep(4) }}
/>
) : 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(3)}
onGenerate={() => setCurrentStep(5)}
onBack={() => setCurrentStep(4)}
onGenerate={() => setCurrentStep(6)}
/>
)
case 5:
case 6:
return (
<ArchitectureDesignerStep
recommendations={techStackRecommendations}
onBack={() => setCurrentStep(4)}
onBack={() => setCurrentStep(5)}
/>
)
default:
@ -1003,13 +1307,14 @@ export function MainDashboard() {
<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.id} className="flex items-center">
<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
@ -1033,6 +1338,7 @@ export function MainDashboard() {
</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"}`}
/>
)}
@ -1043,10 +1349,22 @@ export function MainDashboard() {
</div>
</div>
</div>
)}
{/* Main Content */}
<main className="py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">{renderStep()}</div>
<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>
)

View File

@ -0,0 +1,250 @@
"use client"
import { useMemo, useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import { ScrollArea } from "@/components/ui/scroll-area"
import { SelectDevice } from "@/components/ui/select-device"
import { cn } from "@/lib/utils"
import { getHealthUrl, config } from "@/lib/config"
export function PromptSidePanel({
className,
selectedDevice = 'desktop',
onDeviceChange
}: {
className?: string
selectedDevice?: 'desktop' | 'tablet' | 'mobile'
onDeviceChange?: (device: 'desktop' | 'tablet' | 'mobile') => void
}) {
const [collapsed, setCollapsed] = useState(false)
const [prompt, setPrompt] = useState(
"Dashboard with header, left sidebar, 3 stats cards, a line chart and a data table, plus footer.",
)
const [isGenerating, setIsGenerating] = useState(false)
const [backendStatus, setBackendStatus] = useState<'connected' | 'disconnected' | 'checking'>('checking')
const examples = useMemo(
() => [
"Landing page with header, hero, 3x2 feature grid, and footer.",
"Settings screen: header, list of toggles, and save button.",
"Ecommerce product page: header, 2-column gallery/details, reviews, sticky add-to-cart.",
"Dashboard: header, left sidebar, 3 stats cards, line chart, data table, footer.",
"Signup page: header, 2-column form, callout, submit button.",
"Admin panel: header, left navigation, main content area with data tables, and footer.",
"Product catalog: header, search bar, filter sidebar, 4x3 product grid, pagination.",
"Blog layout: header, featured post hero, 2-column article list, sidebar with categories.",
],
[],
)
// Check backend connection status
useEffect(() => {
const checkBackendStatus = async () => {
try {
setBackendStatus('checking')
const response = await fetch(getHealthUrl(), {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
if (response.ok) {
setBackendStatus('connected')
} else {
setBackendStatus('disconnected')
}
} catch (error) {
console.error('Backend health check failed:', error)
setBackendStatus('disconnected')
}
}
checkBackendStatus()
// Check status every 10 seconds
const interval = setInterval(checkBackendStatus, config.ui.statusCheckInterval)
return () => clearInterval(interval)
}, [])
const dispatchGenerate = async (text: string) => {
setIsGenerating(true)
// Dispatch the event for the canvas to handle with device information
window.dispatchEvent(new CustomEvent("tldraw:generate", {
detail: {
prompt: text,
device: selectedDevice
}
}))
// Wait a bit to show the loading state
setTimeout(() => setIsGenerating(false), 1000)
}
const dispatchClear = () => {
window.dispatchEvent(new Event("tldraw:clear"))
}
const handleDeviceChange = (device: 'desktop' | 'tablet' | 'mobile') => {
console.log('DEBUG: PromptSidePanel handleDeviceChange called with:', device)
console.log('DEBUG: Current selectedDevice prop:', selectedDevice)
console.log('DEBUG: onDeviceChange function exists:', !!onDeviceChange)
if (onDeviceChange) {
console.log('DEBUG: Calling onDeviceChange with:', device)
onDeviceChange(device)
} else {
console.warn('DEBUG: onDeviceChange function not provided')
}
}
const getBackendStatusIcon = () => {
switch (backendStatus) {
case 'connected':
return '🟢'
case 'disconnected':
return '🔴'
case 'checking':
return '🟡'
default:
return '⚪'
}
}
const getBackendStatusText = () => {
switch (backendStatus) {
case 'connected':
return 'AI Backend Connected'
case 'disconnected':
return 'AI Backend Disconnected'
case 'checking':
return 'Checking Backend...'
default:
return 'Unknown Status'
}
}
return (
<aside
className={cn(
"h-full border-l bg-white dark:bg-neutral-900 flex flex-col",
collapsed ? "w-15" : "w-96",
className,
)}
aria-label="AI prompt side panel"
>
<div className="flex items-center justify-between px-3 py-2 border-b">
<h2 className={cn("text-sm font-medium text-balance", collapsed && "sr-only")}>AI Wireframe</h2>
<Button variant="ghost" size="icon" onClick={() => setCollapsed((c) => !c)} aria-label="Toggle panel">
{collapsed ? <span aria-hidden></span> : <span aria-hidden></span>}
</Button>
</div>
{!collapsed && (
<div className="flex flex-col gap-3 p-3">
{/* Backend Status */}
<div className={cn(
"flex items-center gap-2 px-3 py-2 rounded-lg text-xs",
backendStatus === 'connected' ? 'bg-green-50 text-green-700 border border-green-200' :
backendStatus === 'disconnected' ? 'bg-red-50 text-red-700 border border-red-200' :
'bg-yellow-50 text-yellow-700 border border-yellow-200'
)}>
<span>{getBackendStatusIcon()}</span>
<span className="font-medium">{getBackendStatusText()}</span>
</div>
<label className="text-xs font-medium">Prompt</label>
<Textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe your screen: Landing with header, hero, 3x2 features, footer"
className="min-h-28"
disabled={isGenerating}
/>
<div className="space-y-2">
<label className="text-xs font-medium">Device Type</label>
<SelectDevice
value={selectedDevice}
onValueChange={handleDeviceChange}
disabled={isGenerating}
/>
<div className="flex items-center gap-2 text-xs">
<span className="font-medium">Current:</span>
<span className={cn(
"px-2 py-1 rounded-full text-xs font-medium",
selectedDevice === "desktop" && "bg-blue-100 text-blue-700",
selectedDevice === "tablet" && "bg-green-100 text-green-700",
selectedDevice === "mobile" && "bg-purple-100 text-purple-700"
)}>
{selectedDevice.charAt(0).toUpperCase() + selectedDevice.slice(1)}
</span>
</div>
<p className="text-xs text-gray-500">
{selectedDevice === "desktop" && "Desktop layout with full navigation and sidebar"}
{selectedDevice === "tablet" && "Tablet layout with responsive navigation"}
{selectedDevice === "mobile" && "Mobile-first layout with stacked elements"}
</p>
</div>
<div className="flex gap-2">
<Button
onClick={() => dispatchGenerate(prompt)}
className="flex-1"
disabled={isGenerating || backendStatus !== 'connected'}
>
{isGenerating ? 'Generating...' : `Generate for ${selectedDevice}`}
</Button>
<Button variant="secondary" onClick={dispatchClear} disabled={isGenerating}>
Clear
</Button>
</div>
{backendStatus === 'disconnected' && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
<p className="text-xs text-amber-700">
<strong>Backend not connected.</strong> Make sure your Flask backend is running on port 5000.
The system will use fallback generation instead.
</p>
</div>
)}
<div className="pt-1">
<p className="text-xs font-medium mb-2">Examples</p>
<ScrollArea className="h-40 border rounded">
<ul className="p-2 space-y-2">
{examples.map((ex) => (
<li key={ex}>
<button
type="button"
onClick={() => setPrompt(ex)}
className="text-left text-xs w-full hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded px-2 py-1"
disabled={isGenerating}
>
{ex}
</button>
</li>
))}
</ul>
</ScrollArea>
</div>
{/* AI Features Info */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-xs text-blue-700">
<strong>AI-Powered Generation:</strong> Claude AI analyzes your prompts and creates professional wireframe layouts with proper spacing, proportions, and UX best practices.
</p>
<div className="mt-2 pt-2 border-t border-blue-200">
<p className="text-xs text-blue-600">
<strong>Device-Specific Generation:</strong><br/>
<strong>Desktop:</strong> Uses single-device API for faster generation<br/>
<strong>Tablet/Mobile:</strong> Uses multi-device API for responsive layouts
</p>
</div>
</div>
</div>
)}
</aside>
)
}
export default PromptSidePanel

View File

@ -0,0 +1,72 @@
"use client"
import type React from "react"
import { useState } from "react"
export default function PromptToolbar({
busy,
onGenerate,
onClear,
onExample,
}: {
busy?: boolean
onGenerate: (prompt: string) => void
onClear: () => void
onExample: (text: string) => void
}) {
const [prompt, setPrompt] = useState("Dashboard with header, sidebar, 3x2 cards grid, and footer")
const submit = (e: React.FormEvent) => {
e.preventDefault()
if (!prompt.trim() || busy) return
onGenerate(prompt.trim())
}
const examples = [
"Marketing landing page with hero, 3x2 features grid, signup form, and footer",
"Simple login screen with header and centered form",
"Ecommerce product grid 4x2 with header, sidebar filters, and footer",
"Admin dashboard with header, sidebar, 2x2 cards and a form",
]
return (
<div className="flex flex-col gap-3">
<form onSubmit={submit} className="flex items-center gap-2">
<input
aria-label="Prompt"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="e.g., Dashboard with header, sidebar, 3x2 cards grid, and footer"
className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm outline-none focus:border-blue-500"
/>
<button
type="submit"
disabled={busy}
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-60"
>
{busy ? "Generating…" : "Generate"}
</button>
<button
type="button"
onClick={onClear}
className="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-800 hover:bg-gray-50"
>
Clear
</button>
</form>
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs text-gray-600">Try:</span>
{examples.map((ex) => (
<button
key={ex}
onClick={() => onExample(ex)}
className="rounded-full border border-gray-200 bg-white px-3 py-1 text-xs text-gray-800 hover:bg-gray-50"
>
{ex}
</button>
))}
</div>
</div>
)
}

View File

@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@ -0,0 +1,157 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -0,0 +1,109 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

353
src/components/ui/chart.tsx Normal file
View File

@ -0,0 +1,353 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}) {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}) {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
const ChartLegend = RechartsPrimitive.Legend
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}) {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View File

@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View File

@ -0,0 +1,65 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
type Device = "desktop" | "tablet" | "mobile"
export interface SelectDeviceProps extends React.HTMLAttributes<HTMLDivElement> {
value: Device
onValueChange: (value: Device) => void
disabled?: boolean
}
export function SelectDevice({
value,
onValueChange,
disabled,
className,
...props
}: SelectDeviceProps) {
return (
<div
className={cn(
"flex items-center gap-1 p-1 bg-neutral-100 rounded-lg",
disabled && "opacity-50 cursor-not-allowed",
className
)}
{...props}
>
<button
type="button"
onClick={() => onValueChange("desktop")}
disabled={disabled}
className={cn(
"flex-1 px-3 py-1 text-xs rounded-md transition-colors",
value === "desktop" ? "bg-white shadow text-blue-600" : "text-neutral-600 hover:bg-white/50"
)}
>
Desktop
</button>
<button
type="button"
onClick={() => onValueChange("tablet")}
disabled={disabled}
className={cn(
"flex-1 px-3 py-1 text-xs rounded-md transition-colors",
value === "tablet" ? "bg-white shadow text-blue-600" : "text-neutral-600 hover:bg-white/50"
)}
>
Tablet
</button>
<button
type="button"
onClick={() => onValueChange("mobile")}
disabled={disabled}
className={cn(
"flex-1 px-3 py-1 text-xs rounded-md transition-colors",
value === "mobile" ? "bg-white shadow text-blue-600" : "text-neutral-600 hover:bg-white/50"
)}
>
Mobile
</button>
</div>
)
}

116
src/components/ui/table.tsx Normal file
View File

@ -0,0 +1,116 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -2,23 +2,17 @@ import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
"use client"
'use client'
import React, { createContext, useContext, useEffect, useState } from 'react'
import { safeLocalStorage } from '@/lib/utils'

137
src/lib/config.ts Normal file
View File

@ -0,0 +1,137 @@
// Configuration for the wireframe generator
export const config = {
// Backend API configuration
backend: {
baseUrl: process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8021',
endpoints: {
health: '/health',
generateWireframe: '/generate-wireframe',
generateWireframeDesktop: '/generate-wireframe/desktop',
generateWireframeTablet: '/generate-wireframe/tablet',
generateWireframeMobile: '/generate-wireframe/mobile',
generateAllDevices: '/generate-all-devices',
wireframes: '/api/wireframes',
wireframe: (id?: string) => id ? `/api/wireframes/${id}` : '/api/wireframes',
},
timeout: 30000, // 30 seconds
},
// User Authentication Service
auth: {
baseUrl: process.env.NEXT_PUBLIC_AUTH_URL || 'http://localhost:8011',
endpoints: {
health: '/health',
register: '/api/auth/register',
login: '/api/auth/login',
logout: '/api/auth/logout',
refresh: '/api/auth/refresh',
profile: '/api/auth/me',
preferences: '/api/auth/preferences',
projects: '/api/auth/projects',
},
tokenKey: 'auth_token',
refreshTokenKey: 'refresh_token',
},
// UI configuration
ui: {
maxPromptLength: 1000,
statusCheckInterval: 10000, // 10 seconds
generationTimeout: 30000, // 30 seconds
},
// Wireframe defaults
wireframe: {
defaultPageSize: { width: 1200, height: 800 },
defaultSpacing: { gap: 16, padding: 20 },
minElementSize: { width: 80, height: 40 },
},
} as const
// Helper function to get full API URL
export const getApiUrl = (endpoint: string): string => {
return `${config.backend.baseUrl}${endpoint}`
}
// Helper function to get full Auth Service URL
export const getAuthUrl = (endpoint: string): string => {
return `${config.auth.baseUrl}${endpoint}`
}
// Helper function to get health check URL
export const getHealthUrl = (): string => {
return getApiUrl(config.backend.endpoints.health)
}
// Helper function to get auth health check URL
export const getAuthHealthUrl = (): string => {
return getAuthUrl(config.auth.endpoints.health)
}
// Helper function to get wireframe generation URL for specific device
export const getWireframeGenerationUrl = (device: 'desktop' | 'tablet' | 'mobile' = 'desktop'): string => {
switch (device) {
case 'tablet':
return getApiUrl(config.backend.endpoints.generateWireframeTablet)
case 'mobile':
return getApiUrl(config.backend.endpoints.generateWireframeMobile)
case 'desktop':
default:
return getApiUrl(config.backend.endpoints.generateWireframeDesktop)
}
}
// Helper function to get universal wireframe generation URL (backward compatibility)
export const getUniversalWireframeGenerationUrl = (): string => {
return getApiUrl(config.backend.endpoints.generateWireframe)
}
// Helper function to get all devices generation URL
export const getAllDevicesGenerationUrl = (): string => {
return getApiUrl(config.backend.endpoints.generateAllDevices)
}
// Helper function to get wireframe persistence URLs
export const getWireframeUrl = (id?: string): string => {
if (id) {
return getApiUrl(config.backend.endpoints.wireframe(id))
}
return getApiUrl(config.backend.endpoints.wireframes)
}
// Helper function to get wireframe by ID URL
export const getWireframeByIdUrl = (id: string): string => {
return getApiUrl(config.backend.endpoints.wireframe(id))
}
// Authentication helper functions
export const getAuthHeaders = (): HeadersInit => {
const token = localStorage.getItem(config.auth.tokenKey)
return token ? { 'Authorization': `Bearer ${token}` } : {}
}
export const getAuthHeadersWithContentType = (): HeadersInit => {
const token = localStorage.getItem(config.auth.tokenKey)
return {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
}
export const isAuthenticated = (): boolean => {
const token = localStorage.getItem(config.auth.tokenKey)
return !!token
}
export const getCurrentUser = (): any => {
try {
const token = localStorage.getItem(config.auth.tokenKey)
if (!token) return null
// Decode JWT token to get user info
const payload = JSON.parse(atob(token.split('.')[1]))
return payload
} catch {
return null
}
}