frontend ai mockup changes

This commit is contained in:
Chandini 2025-09-10 14:22:52 +05:30
parent c007095a05
commit a21785972d
19 changed files with 2403 additions and 28 deletions

83
package-lock.json generated
View File

@ -9,6 +9,9 @@
"version": "0.1.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.57.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@next/font": "^14.2.15",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
@ -17,7 +20,9 @@
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.12",
@ -29,6 +34,7 @@
"next": "15.4.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-resizable-panels": "^3.0.5",
"socket.io-client": "^4.8.1",
"svg-path-parser": "^1.1.0",
"tailwind-merge": "^3.3.1",
@ -84,6 +90,73 @@
"anthropic-ai-sdk": "bin/cli"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/modifiers": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz",
"integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
@ -8001,6 +8074,16 @@
}
}
},
"node_modules/react-resizable-panels": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.5.tgz",
"integrity": "sha512-3z1yN25DMTXLg2wfyFrW32r5k4WEcUa3F7cJ2EgtNK07lnOs4mpM8yWLGunCpkhcQRwJX4fqoLcIh/pHPxzlmQ==",
"license": "MIT",
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",

View File

@ -10,6 +10,9 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.57.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@next/font": "^14.2.15",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
@ -18,7 +21,9 @@
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.12",
@ -30,6 +35,7 @@
"next": "15.4.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-resizable-panels": "^3.0.5",
"socket.io-client": "^4.8.1",
"svg-path-parser": "^1.1.0",
"tailwind-merge": "^3.3.1",

143
src/components/canvas.tsx Normal file
View File

@ -0,0 +1,143 @@
"use client"
import { useRef, useState } from "react"
import { useEditorStore } from "@/lib/store"
import { ComponentRenderer } from "./component-renderer"
import { cn } from "@/lib/utils"
export function Canvas() {
const canvasRef = useRef<HTMLDivElement>(null)
const [isDragging, setIsDragging] = useState(false)
const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null)
const [selectedComponents, setSelectedComponents] = useState<Set<string>>(new Set())
const {
components,
selectedComponent,
selectComponent,
moveComponent,
updateComponent
} = useEditorStore()
const handleMouseDown = (e: React.MouseEvent) => {
if (e.target === canvasRef.current) {
// Clicked on empty canvas, deselect all
selectComponent(null)
setSelectedComponents(new Set())
}
}
const handleComponentClick = (componentId: string, e: React.MouseEvent) => {
e.stopPropagation()
const component = components.find(c => c.id === componentId)
if (component) {
selectComponent(component)
setSelectedComponents(new Set([componentId]))
}
}
const handleComponentMouseDown = (componentId: string, e: React.MouseEvent) => {
e.stopPropagation()
const component = components.find(c => c.id === componentId)
if (component) {
selectComponent(component)
setSelectedComponents(new Set([componentId]))
// Start dragging
setIsDragging(true)
setDragStart({ x: e.clientX, y: e.clientY })
}
}
const handleMouseMove = (e: React.MouseEvent) => {
if (isDragging && dragStart && selectedComponent) {
const deltaX = e.clientX - dragStart.x
const deltaY = e.clientY - dragStart.y
const newPosition = {
x: Math.max(0, selectedComponent.position.x + deltaX),
y: Math.max(0, selectedComponent.position.y + deltaY)
}
moveComponent(selectedComponent.id, newPosition)
setDragStart({ x: e.clientX, y: e.clientY })
}
}
const handleMouseUp = () => {
setIsDragging(false)
setDragStart(null)
}
return (
<div
ref={canvasRef}
className={cn(
"relative w-full h-full bg-white overflow-hidden",
"canvas-grid"
)}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
{/* Canvas Grid Background */}
<div className="absolute inset-0 opacity-20">
<div
className="w-full h-full"
style={{
backgroundImage: `
linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px)
`,
backgroundSize: '20px 20px'
}}
/>
</div>
{/* Components */}
{components.map((component) => (
<div
key={component.id}
className={cn(
"absolute cursor-move select-none",
selectedComponents.has(component.id) && "ring-2 ring-blue-500 ring-opacity-50"
)}
style={{
left: component.position.x,
top: component.position.y,
width: component.size?.width || 100,
height: component.size?.height || 100,
}}
onClick={(e) => handleComponentClick(component.id, e)}
onMouseDown={(e) => handleComponentMouseDown(component.id, e)}
>
<ComponentRenderer
component={component}
isSelected={selectedComponents.has(component.id)}
/>
</div>
))}
{/* Drop Zone Indicator */}
{isDragging && (
<div className="absolute inset-0 pointer-events-none">
<div className="w-full h-full border-2 border-dashed border-blue-400 bg-blue-50 bg-opacity-20" />
</div>
)}
{/* Empty State */}
{components.length === 0 && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center text-gray-500">
<div className="text-6xl mb-4">🎨</div>
<h3 className="text-lg font-medium mb-2">No components yet</h3>
<p className="text-sm">Drag components from the palette to get started</p>
</div>
</div>
)}
</div>
)
}

View File

@ -0,0 +1,213 @@
"use client"
import { useState } from "react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Textarea } from "./ui/textarea"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Checkbox } from "./ui/checkbox"
import { Switch } from "./ui/switch"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { RadioGroup, RadioGroupItem } from "./ui/radio-group"
import { Progress } from "./ui/progress"
import { Avatar, AvatarFallback } from "./ui/avatar"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
import { Badge } from "./ui/badge"
import { cn } from "@/lib/utils"
import {
MousePointer,
Square,
Circle,
Type,
Image,
Layout,
BarChart3,
Settings,
User,
Calendar,
Mail,
Phone,
Globe,
Search,
Filter,
Download,
Upload,
Edit,
Trash2,
Plus,
Minus,
Check,
X,
ArrowRight,
ArrowLeft,
ArrowUp,
ArrowDown,
ChevronDown,
ChevronUp,
ChevronLeft,
ChevronRight,
Menu,
MoreHorizontal,
Star,
Heart,
Share,
Bookmark,
Flag,
AlertCircle,
Info,
CheckCircle,
XCircle,
AlertTriangle,
HelpCircle
} from "lucide-react"
const componentCategories = [
{
name: "Basic",
icon: Square,
components: [
{ type: "button", name: "Button", icon: MousePointer },
{ type: "input", name: "Input", icon: Type },
{ type: "textarea", name: "Textarea", icon: Type },
{ type: "card", name: "Card", icon: Square },
]
},
{
name: "Form",
icon: Settings,
components: [
{ type: "checkbox", name: "Checkbox", icon: Check },
{ type: "switch", name: "Switch", icon: Settings },
{ type: "select", name: "Select", icon: ChevronDown },
{ type: "radiogroup", name: "Radio Group", icon: Circle },
]
},
{
name: "Data",
icon: BarChart3,
components: [
{ type: "table", name: "Table", icon: Layout },
{ type: "progress", name: "Progress", icon: BarChart3 },
{ type: "tabs", name: "Tabs", icon: Layout },
]
},
{
name: "Media",
icon: Image,
components: [
{ type: "avatar", name: "Avatar", icon: User },
]
}
]
export function ComponentPalette() {
const [selectedCategory, setSelectedCategory] = useState("Basic")
const [searchQuery, setSearchQuery] = useState("")
const filteredComponents = componentCategories
.find(cat => cat.name === selectedCategory)
?.components.filter(comp =>
comp.name.toLowerCase().includes(searchQuery.toLowerCase())
) || []
return (
<div className="h-full flex flex-col bg-white">
{/* Header */}
<div className="p-4 border-b">
<h2 className="text-lg font-semibold text-gray-900">Components</h2>
<p className="text-sm text-gray-500">Drag components to canvas</p>
</div>
{/* Search */}
<div className="p-4 border-b">
<Input
placeholder="Search components..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full"
/>
</div>
{/* Categories */}
<div className="p-4 border-b">
<div className="flex flex-wrap gap-2">
{componentCategories.map((category) => (
<Button
key={category.name}
variant={selectedCategory === category.name ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory(category.name)}
className="flex items-center gap-2"
>
<category.icon className="h-4 w-4" />
{category.name}
</Button>
))}
</div>
</div>
{/* Components List */}
<div className="flex-1 overflow-y-auto p-4">
<div className="grid grid-cols-2 gap-3">
{filteredComponents.map((component) => (
<ComponentPreview
key={component.type}
type={component.type}
name={component.name}
icon={component.icon}
/>
))}
</div>
{filteredComponents.length === 0 && (
<div className="text-center text-gray-500 py-8">
<Search className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No components found</p>
</div>
)}
</div>
</div>
)
}
interface ComponentPreviewProps {
type: string
name: string
icon: React.ComponentType<{ className?: string }>
}
function ComponentPreview({ type, name, icon: Icon }: ComponentPreviewProps) {
const [isDragging, setIsDragging] = useState(false)
const handleDragStart = (e: React.DragEvent) => {
setIsDragging(true)
e.dataTransfer.setData('application/json', JSON.stringify({ type }))
e.dataTransfer.effectAllowed = 'copy'
}
const handleDragEnd = () => {
setIsDragging(false)
}
return (
<div
draggable
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
className={cn(
"p-3 border border-gray-200 rounded-lg cursor-grab hover:border-blue-300 hover:shadow-sm transition-all",
isDragging && "opacity-50 scale-95"
)}
>
<div className="flex flex-col items-center space-y-2">
<div className="p-2 bg-gray-100 rounded-lg">
<Icon className="h-4 w-4 text-gray-600" />
</div>
<span className="text-xs font-medium text-gray-700 text-center">
{name}
</span>
</div>
</div>
)
}

View File

@ -0,0 +1,253 @@
"use client"
import { ComponentInstance } from "@/lib/store"
import { WireframeRenderer } from "./wireframe-renderer"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Textarea } from "./ui/textarea"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Checkbox } from "./ui/checkbox"
import { Switch } from "./ui/switch"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { RadioGroup, RadioGroupItem } from "./ui/radio-group"
import { Progress } from "./ui/progress"
import { Avatar, AvatarFallback } from "./ui/avatar"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
import { cn } from "@/lib/utils"
interface ComponentRendererProps {
component: ComponentInstance
isSelected?: boolean
onClick?: () => void
onMouseDown?: (e: React.MouseEvent) => void
}
export function ComponentRenderer({
component,
isSelected = false,
onClick,
onMouseDown
}: ComponentRendererProps) {
const { type, props } = component
// Handle wireframe components
if (type.startsWith('wireframe-')) {
return (
<WireframeRenderer
component={component}
onClick={onClick}
onMouseDown={onMouseDown}
/>
)
}
// Handle regular UI components
const renderComponent = () => {
switch (type) {
case 'button':
return (
<Button
variant={props.variant || 'default'}
size={props.size || 'default'}
className={cn("w-full h-full", isSelected && "ring-2 ring-blue-500")}
onClick={onClick}
onMouseDown={onMouseDown}
>
{props.children || 'Button'}
</Button>
)
case 'input':
return (
<Input
placeholder={props.placeholder || 'Enter text...'}
className={cn("w-full h-full", isSelected && "ring-2 ring-blue-500")}
onClick={onClick}
onMouseDown={onMouseDown}
readOnly
/>
)
case 'textarea':
return (
<Textarea
placeholder={props.placeholder || 'Enter text...'}
className={cn("w-full h-full resize-none", isSelected && "ring-2 ring-blue-500")}
onClick={onClick}
onMouseDown={onMouseDown}
readOnly
/>
)
case 'card':
return (
<Card className={cn("w-full h-full", isSelected && "ring-2 ring-blue-500")}>
<CardHeader>
<CardTitle className="text-sm">{props.title || 'Card Title'}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-xs text-gray-600">{props.description || 'Card description'}</p>
</CardContent>
</Card>
)
case 'checkbox':
return (
<div className={cn("flex items-center space-x-2 w-full h-full", isSelected && "ring-2 ring-blue-500")}>
<Checkbox
id={component.id}
checked={props.checked || false}
onClick={onClick}
onMouseDown={onMouseDown}
/>
<label htmlFor={component.id} className="text-sm">
{props.label || 'Checkbox'}
</label>
</div>
)
case 'switch':
return (
<div className={cn("flex items-center space-x-2 w-full h-full", isSelected && "ring-2 ring-blue-500")}>
<Switch
checked={props.checked || false}
onClick={onClick}
onMouseDown={onMouseDown}
/>
<label className="text-sm">
{props.label || 'Switch'}
</label>
</div>
)
case 'select':
return (
<Select>
<SelectTrigger className={cn("w-full h-full", isSelected && "ring-2 ring-blue-500")}>
<SelectValue placeholder={props.placeholder || 'Select option...'} />
</SelectTrigger>
<SelectContent>
{(props.options || ['Option 1', 'Option 2']).map((option: string, index: number) => (
<SelectItem key={index} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
)
case 'radiogroup':
return (
<RadioGroup
value={props.value || 'Option 1'}
className={cn("w-full h-full", isSelected && "ring-2 ring-blue-500")}
onClick={onClick}
onMouseDown={onMouseDown}
>
{(props.options || ['Option 1', 'Option 2']).map((option: string, index: number) => (
<div key={index} className="flex items-center space-x-2">
<RadioGroupItem value={option} id={`${component.id}-${index}`} />
<label htmlFor={`${component.id}-${index}`} className="text-sm">
{option}
</label>
</div>
))}
</RadioGroup>
)
case 'progress':
return (
<div className={cn("w-full h-full flex items-center", isSelected && "ring-2 ring-blue-500")}>
<Progress
value={props.value || 50}
max={props.max || 100}
className="w-full"
onClick={onClick}
onMouseDown={onMouseDown}
/>
</div>
)
case 'avatar':
return (
<div className={cn("w-full h-full flex items-center justify-center", isSelected && "ring-2 ring-blue-500")}>
<Avatar onClick={onClick} onMouseDown={onMouseDown}>
<AvatarFallback>{props.fallback || 'AB'}</AvatarFallback>
</Avatar>
</div>
)
case 'table':
return (
<div className={cn("w-full h-full overflow-auto", isSelected && "ring-2 ring-blue-500")}>
<Table>
<TableHeader>
<TableRow>
{(props.headers || ['Name', 'Email', 'Role']).map((header: string, index: number) => (
<TableHead key={index} className="text-xs">{header}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{(props.rows || [
['John Doe', 'john@example.com', 'Admin'],
['Jane Smith', 'jane@example.com', 'User']
]).map((row: string[], rowIndex: number) => (
<TableRow key={rowIndex}>
{row.map((cell: string, cellIndex: number) => (
<TableCell key={cellIndex} className="text-xs">{cell}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
)
case 'tabs':
return (
<Tabs defaultValue="0" className={cn("w-full h-full", isSelected && "ring-2 ring-blue-500")}>
<TabsList className="grid w-full grid-cols-2">
{(props.tabs || [
{ label: 'Tab 1', content: 'Content 1' },
{ label: 'Tab 2', content: 'Content 2' }
]).map((tab: any, index: number) => (
<TabsTrigger key={index} value={index.toString()}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{(props.tabs || [
{ label: 'Tab 1', content: 'Content 1' },
{ label: 'Tab 2', content: 'Content 2' }
]).map((tab: any, index: number) => (
<TabsContent key={index} value={index.toString()} className="mt-2">
<div className="text-sm">{tab.content}</div>
</TabsContent>
))}
</Tabs>
)
default:
return (
<div
className={cn(
"w-full h-full border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center text-gray-500 text-sm",
isSelected && "ring-2 ring-blue-500"
)}
onClick={onClick}
onMouseDown={onMouseDown}
>
{type}
</div>
)
}
}
return (
<div className="w-full h-full">
{renderComponent()}
</div>
)
}

View File

@ -0,0 +1,234 @@
"use client"
import { Suspense, useState, useEffect } from "react"
import { DndContext, type DragEndEvent, DragOverlay, type DragStartEvent } from "@dnd-kit/core"
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable"
import { Button } from "@/components/ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Trash2, Palette, PenTool, Eye, EyeOff, SquarePen } from "lucide-react"
import WireframeCanvas from "@/components/wireframe-canvas"
import PromptSidePanel from "@/components/prompt-side-panel"
import { Canvas } from "@/components/canvas"
import { ComponentPalette } from "@/components/component-palette"
import { PropertiesPanel } from "@/components/properties-panel"
import { useEditorStore } from "@/lib/store"
import { useWireframeIntegration } from "@/lib/wireframe-integration"
interface DualCanvasEditorProps {
className?: string
onWireframeGenerated?: (data: any) => void
onGenerationStart?: () => void
selectedDevice?: 'desktop' | 'tablet' | 'mobile'
onDeviceChange?: (device: 'desktop' | 'tablet' | 'mobile') => void
}
export function DualCanvasEditor({
className,
onWireframeGenerated,
onGenerationStart,
selectedDevice = 'desktop',
onDeviceChange
}: DualCanvasEditorProps) {
const [activeId, setActiveId] = useState<string | null>(null)
const [canvasMode, setCanvasMode] = useState<'wireframe' | 'components'>('wireframe')
const {
selectedComponent,
addComponent,
clearAll,
components,
showWireframes,
setShowWireframes,
wireframeRenderMode,
setWireframeRenderMode
} = useEditorStore()
// Initialize wireframe integration
useWireframeIntegration()
const handleDeviceChange = (device: 'desktop' | 'tablet' | 'mobile') => {
if (onDeviceChange) {
onDeviceChange(device)
}
}
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string)
}
const handleDragEnd = (event: DragEndEvent) => {
setActiveId(null)
if (event.over?.id === "canvas" && event.active) {
const componentType = event.active.id as string
// Get the drop position relative to the canvas
const canvasRect = event.over.rect
const dragRect = event.active.rect.current.translated
if (dragRect && canvasRect) {
const newComponent = {
id: `${componentType}-${Date.now()}`,
type: componentType,
props: getDefaultProps(componentType),
position: {
x: Math.max(0, dragRect.left - canvasRect.left),
y: Math.max(0, dragRect.top - canvasRect.top),
},
}
addComponent(newComponent)
}
}
}
const getDefaultProps = (componentType: string): Record<string, any> => {
const defaults: Record<string, any> = {
button: { children: "Button", variant: "default" },
input: { placeholder: "Enter text..." },
textarea: { placeholder: "Enter text..." },
card: { title: "Card Title", description: "Card description" },
checkbox: { label: "Checkbox", checked: false },
switch: { label: "Switch", checked: false },
select: { placeholder: "Select option...", options: ["Option 1", "Option 2"] },
radiogroup: { options: ["Option 1", "Option 2"], value: "Option 1" },
progress: { value: 50, max: 100 },
avatar: { fallback: "AB" },
table: {
headers: ["Name", "Email", "Role"],
rows: [
["John Doe", "john@example.com", "Admin"],
["Jane Smith", "jane@example.com", "User"],
["kenil", "kenil@example.com", "User"],
["kavya", "kavya@example.com", "User"],
["raj", "raj@example.com", "User"]
],
},
tabs: {
tabs: [
{ label: "Tab 1", content: "Content 1" },
{ label: "Tab 2", content: "Content 2" },
],
},
datepicker: { placeholder: "Pick a date", selectedDate: null },
dialog: { title: "Dialog Title", description: "Dialog description" },
}
return defaults[componentType] || {}
}
return (
<div className={`h-full w-full bg-background text-foreground ${className || ''}`}>
<header className="border-b border-border">
<div className="mx-auto max-w-6xl px-4 py-3">
<div className="flex items-center justify-between">
<div>
<h1 className="text-pretty text-lg font-semibold tracking-tight">Dual Canvas Editor</h1>
<p className="text-sm text-muted-foreground">
Switch between AI wireframe generation and drag-and-drop component editor.
</p>
</div>
<div className="flex items-center gap-2">
<Tabs value={canvasMode} onValueChange={(value) => setCanvasMode(value as 'wireframe' | 'components')}>
<TabsList>
<TabsTrigger value="wireframe" className="flex items-center gap-2">
<PenTool className="h-4 w-4" />
AI Wireframe
</TabsTrigger>
<TabsTrigger value="components" className="flex items-center gap-2">
<Palette className="h-4 w-4" />
Components
</TabsTrigger>
</TabsList>
</Tabs>
{canvasMode === 'components' && (
<>
<Button
variant="outline"
size="sm"
onClick={() => setShowWireframes(!showWireframes)}
className="flex items-center gap-2"
>
{showWireframes ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
{showWireframes ? 'Hide Wireframes' : 'Show Wireframes'}
</Button>
<Button
variant="outline"
size="sm"
onClick={clearAll}
disabled={components.length === 0}
className="flex items-center gap-2"
>
<Trash2 className="h-4 w-4" />
Clear All
</Button>
</>
)}
</div>
</div>
</div>
</header>
<div className="h-[calc(100vh-100px)] w-full">
{canvasMode === 'wireframe' ? (
<div className="h-full w-full flex">
<div className="flex-1 min-w-0">
<Suspense fallback={<div className="p-4">Loading canvas</div>}>
<WireframeCanvas
className="h-full w-full"
selectedDevice={selectedDevice}
onWireframeGenerated={onWireframeGenerated}
onGenerationStart={onGenerationStart}
/>
</Suspense>
</div>
<PromptSidePanel
className="shrink-0"
selectedDevice={selectedDevice}
onDeviceChange={handleDeviceChange}
/>
</div>
) : (
<DndContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<ResizablePanelGroup direction="horizontal" className="h-full">
{/* Component Palette Sidebar */}
<ResizablePanel defaultSize={20} minSize={15} maxSize={30}>
<div className="h-full bg-sidebar border-r border-sidebar-border">
<ComponentPalette />
</div>
</ResizablePanel>
<ResizableHandle className="w-1 bg-border hover:bg-accent transition-colors" />
{/* Canvas Area */}
<ResizablePanel defaultSize={selectedComponent ? 60 : 80} minSize={40}>
<div className="h-full bg-background">
<Canvas />
</div>
</ResizablePanel>
{/* Properties Panel - Only show when component is selected */}
{selectedComponent && (
<>
<ResizableHandle className="w-1 bg-border hover:bg-accent transition-colors" />
<ResizablePanel defaultSize={20} minSize={15} maxSize={35}>
<div className="h-full bg-sidebar border-l border-sidebar-border">
<PropertiesPanel />
</div>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
{/* Drag Overlay */}
<DragOverlay>
{activeId ? (
<div className="drag-overlay p-2 bg-card border border-border rounded-md shadow-lg">{activeId}</div>
) : null}
</DragOverlay>
</DndContext>
)}
</div>
</div>
)
}

View File

@ -19,6 +19,7 @@ import { BACKEND_URL } from "@/config/backend"
import { Tooltip } from "@/components/ui/tooltip"
import WireframeCanvas from "@/components/wireframe-canvas"
import PromptSidePanel from "@/components/prompt-side-panel"
import { DualCanvasEditor } from "@/components/dual-canvas-editor"
interface Template {
id: string
@ -1403,27 +1404,16 @@ function AIMockupStep({
</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
{/* Dual Canvas Editor */}
<div className="bg-white/5 border border-white/10 rounded-xl overflow-hidden" style={{ height: '700px' }}>
<DualCanvasEditor
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">

View File

@ -202,7 +202,7 @@ export function PromptSidePanel({
{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.
<strong>Backend not connected.</strong> Make sure your backend is running on port 8000.
The system will use fallback generation instead.
</p>
</div>

View File

@ -0,0 +1,323 @@
"use client"
import { useState, useEffect } from "react"
import { useEditorStore } from "@/lib/store"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Textarea } from "./ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { Switch } from "./ui/switch"
import { Slider } from "./ui/slider"
import { ColorPicker } from "./ui/color-picker"
import { Separator } from "./ui/separator"
import { Badge } from "./ui/badge"
import { cn } from "@/lib/utils"
import { Trash2, Copy, Move, RotateCcw } from "lucide-react"
export function PropertiesPanel() {
const { selectedComponent, updateComponent, removeComponent } = useEditorStore()
const [localProps, setLocalProps] = useState<Record<string, any>>({})
useEffect(() => {
if (selectedComponent) {
setLocalProps(selectedComponent.props)
}
}, [selectedComponent])
const handlePropChange = (key: string, value: any) => {
setLocalProps(prev => ({ ...prev, [key]: value }))
}
const handleSave = () => {
if (selectedComponent) {
updateComponent(selectedComponent.id, { props: localProps })
}
}
const handleReset = () => {
if (selectedComponent) {
setLocalProps(selectedComponent.props)
}
}
const handleDelete = () => {
if (selectedComponent) {
removeComponent(selectedComponent.id)
}
}
if (!selectedComponent) {
return (
<div className="h-full flex items-center justify-center text-gray-500">
<div className="text-center">
<div className="text-4xl mb-2">🎯</div>
<p className="text-sm">Select a component to edit properties</p>
</div>
</div>
)
}
const renderPropertyEditor = (key: string, value: any) => {
const type = typeof value
switch (type) {
case 'string':
if (key.includes('color') || key.includes('Color')) {
return (
<div key={key} className="space-y-2">
<Label htmlFor={key} className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<div className="flex items-center space-x-2">
<Input
id={key}
value={value}
onChange={(e) => handlePropChange(key, e.target.value)}
className="text-xs"
/>
<div
className="w-6 h-6 rounded border"
style={{ backgroundColor: value }}
/>
</div>
</div>
)
}
if (key.includes('text') || key.includes('placeholder') || key.includes('label')) {
return (
<div key={key} className="space-y-2">
<Label htmlFor={key} className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<Textarea
id={key}
value={value}
onChange={(e) => handlePropChange(key, e.target.value)}
className="text-xs min-h-[60px]"
/>
</div>
)
}
return (
<div key={key} className="space-y-2">
<Label htmlFor={key} className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<Input
id={key}
value={value}
onChange={(e) => handlePropChange(key, e.target.value)}
className="text-xs"
/>
</div>
)
case 'number':
if (key.includes('size') || key.includes('width') || key.includes('height')) {
return (
<div key={key} className="space-y-2">
<Label htmlFor={key} className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)}: {value}
</Label>
<Slider
value={[value]}
onValueChange={([newValue]) => handlePropChange(key, newValue)}
min={0}
max={key.includes('size') ? 100 : 500}
step={1}
className="w-full"
/>
</div>
)
}
return (
<div key={key} className="space-y-2">
<Label htmlFor={key} className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<Input
id={key}
type="number"
value={value}
onChange={(e) => handlePropChange(key, Number(e.target.value))}
className="text-xs"
/>
</div>
)
case 'boolean':
return (
<div key={key} className="flex items-center justify-between">
<Label htmlFor={key} className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<Switch
id={key}
checked={value}
onCheckedChange={(checked) => handlePropChange(key, checked)}
/>
</div>
)
case 'object':
if (Array.isArray(value)) {
return (
<div key={key} className="space-y-2">
<Label className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)} ({value.length} items)
</Label>
<div className="space-y-1">
{value.map((item, index) => (
<div key={index} className="flex items-center space-x-2">
<Input
value={item}
onChange={(e) => {
const newArray = [...value]
newArray[index] = e.target.value
handlePropChange(key, newArray)
}}
className="text-xs"
/>
<Button
size="sm"
variant="outline"
onClick={() => {
const newArray = value.filter((_, i) => i !== index)
handlePropChange(key, newArray)
}}
className="h-6 w-6 p-0"
>
×
</Button>
</div>
))}
<Button
size="sm"
variant="outline"
onClick={() => {
const newArray = [...value, '']
handlePropChange(key, newArray)
}}
className="w-full text-xs"
>
Add Item
</Button>
</div>
</div>
)
}
return (
<div key={key} className="space-y-2">
<Label className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)} (Object)
</Label>
<div className="text-xs text-gray-500 p-2 bg-gray-100 rounded">
{JSON.stringify(value, null, 2)}
</div>
</div>
)
default:
return (
<div key={key} className="space-y-2">
<Label className="text-xs font-medium">
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<div className="text-xs text-gray-500 p-2 bg-gray-100 rounded">
{String(value)}
</div>
</div>
)
}
}
return (
<div className="h-full flex flex-col bg-white">
{/* Header */}
<div className="p-4 border-b">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">Properties</h3>
<Badge variant="outline" className="text-xs">
{selectedComponent.type}
</Badge>
</div>
</div>
{/* Component Info */}
<div className="p-4 border-b bg-gray-50">
<div className="space-y-2">
<div className="flex justify-between text-xs">
<span className="text-gray-500">ID:</span>
<span className="font-mono">{selectedComponent.id}</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-gray-500">Position:</span>
<span className="font-mono">
{Math.round(selectedComponent.position.x)}, {Math.round(selectedComponent.position.y)}
</span>
</div>
{selectedComponent.size && (
<div className="flex justify-between text-xs">
<span className="text-gray-500">Size:</span>
<span className="font-mono">
{Math.round(selectedComponent.size.width)} × {Math.round(selectedComponent.size.height)}
</span>
</div>
)}
</div>
</div>
{/* Properties */}
<div className="flex-1 overflow-y-auto p-4">
<div className="space-y-4">
{Object.entries(localProps).map(([key, value]) =>
renderPropertyEditor(key, value)
)}
</div>
{Object.keys(localProps).length === 0 && (
<div className="text-center text-gray-500 py-8">
<div className="text-4xl mb-2"></div>
<p className="text-sm">No properties available</p>
</div>
)}
</div>
{/* Actions */}
<div className="p-4 border-t space-y-2">
<div className="flex space-x-2">
<Button
onClick={handleSave}
size="sm"
className="flex-1"
>
Save
</Button>
<Button
onClick={handleReset}
variant="outline"
size="sm"
className="flex-1"
>
Reset
</Button>
</div>
<Button
onClick={handleDelete}
variant="destructive"
size="sm"
className="w-full"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Component
</Button>
</div>
</div>
)
}

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import { Input } from "./input"
import { cn } from "@/lib/utils"
interface ColorPickerProps {
value?: string
onChange?: (value: string) => void
className?: string
}
export function ColorPicker({ value = "#000000", onChange, className }: ColorPickerProps) {
return (
<div className={cn("flex items-center space-x-2", className)}>
<Input
type="color"
value={value}
onChange={(e) => onChange?.(e.target.value)}
className="w-12 h-8 p-1 border rounded"
/>
<Input
value={value}
onChange={(e) => onChange?.(e.target.value)}
className="flex-1 text-xs font-mono"
placeholder="#000000"
/>
</div>
)
}

View File

@ -0,0 +1,59 @@
"use client"
import * as React from "react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="h-2.5 w-2.5"
>
<path
d="M5.5 4.625C6.12132 4.625 6.625 4.12132 6.625 3.5C6.625 2.87868 6.12132 2.375 5.5 2.375C4.87868 2.375 4.375 2.87868 4.375 3.5C4.375 4.12132 4.87868 4.625 5.5 4.625ZM9.5 4.625C10.1213 4.625 10.625 4.12132 10.625 3.5C10.625 2.87868 10.1213 2.375 9.5 2.375C8.87868 2.375 8.375 2.87868 8.375 3.5C8.375 4.12132 8.87868 4.625 9.5 4.625ZM5.5 8.125C6.12132 8.125 6.625 7.62132 6.625 7C6.625 6.37868 6.12132 5.875 5.5 5.875C4.87868 5.875 4.375 6.37868 4.375 7C4.375 7.62132 4.87868 8.125 5.5 8.125ZM9.5 8.125C10.1213 8.125 10.625 7.62132 10.625 7C10.625 6.37868 10.1213 5.875 9.5 5.875C8.87868 5.875 8.375 6.37868 8.375 7C8.375 7.62132 8.87868 8.125 9.5 8.125ZM5.5 11.625C6.12132 11.625 6.625 11.1213 6.625 10.5C6.625 9.87868 6.12132 9.375 5.5 9.375C4.87868 9.375 4.375 9.87868 4.375 10.5C4.375 11.1213 4.87868 11.625 5.5 11.625ZM9.5 11.625C10.1213 11.625 10.625 11.1213 10.625 10.5C10.625 9.87868 10.1213 9.375 9.5 9.375C8.87868 9.375 8.375 9.87868 8.375 10.5C8.375 11.1213 8.87868 11.625 9.5 11.625Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
/>
</svg>
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

View File

@ -12,6 +12,8 @@ import { getAccessToken } from "@/components/apis/authApiClients"
import { authApiClient, getRefreshToken, setTokens, clearTokens } from "@/components/apis/authApiClients"
import { parseSVG, makeAbsolute } from 'svg-path-parser'
import { wireframeConverter } from "@/lib/wireframe-converter"
import { useEditorStore } from "@/lib/store"
export default function WireframeCanvas({
className,
@ -1154,6 +1156,17 @@ export default function WireframeCanvas({
if (containsSVG) {
await parseSVGAndRender(editor, responseText)
// NEW: also convert SVG to component instances for the Components tab
try {
const parsed = wireframeConverter.parseSVGToWireframe(responseText, targetDevice as 'desktop' | 'tablet' | 'mobile')
const componentInstances = wireframeConverter.convertToComponents(parsed)
// Replace existing components with generated ones
useEditorStore.getState().setComponents(componentInstances)
} catch (convErr) {
console.warn('SVG to components conversion failed (non-blocking):', convErr)
}
// Save the generated wireframe
await saveWireframe(editor, false)
// Notify parent component that wireframe was generated
@ -1170,9 +1183,12 @@ export default function WireframeCanvas({
const jsonResponse = JSON.parse(responseText)
if (jsonResponse.svg) {
await parseSVGAndRender(editor, jsonResponse.svg)
// Save the generated wireframe
try {
const parsed = wireframeConverter.parseSVGToWireframe(jsonResponse.svg, targetDevice as 'desktop' | 'tablet' | 'mobile')
const componentInstances = wireframeConverter.convertToComponents(parsed)
useEditorStore.getState().setComponents(componentInstances)
} catch {}
await saveWireframe(editor, false)
// Notify parent component that wireframe was generated
onWireframeGenerated?.({
success: true,
wireframeId: currentWireframeId,
@ -1182,9 +1198,12 @@ export default function WireframeCanvas({
})
} else if (jsonResponse.data && jsonResponse.data.svg) {
await parseSVGAndRender(editor, jsonResponse.data.svg)
// Save the generated wireframe
try {
const parsed = wireframeConverter.parseSVGToWireframe(jsonResponse.data.svg, targetDevice as 'desktop' | 'tablet' | 'mobile')
const componentInstances = wireframeConverter.convertToComponents(parsed)
useEditorStore.getState().setComponents(componentInstances)
} catch {}
await saveWireframe(editor, false)
// Notify parent component that wireframe was generated
onWireframeGenerated?.({
success: true,
wireframeId: currentWireframeId,
@ -1203,7 +1222,6 @@ export default function WireframeCanvas({
} catch (err) {
console.error('Error generating wireframe:', err)
setError(err instanceof Error ? err.message : 'Failed to generate wireframe')
// Notify parent component of failure
onWireframeGenerated?.({
success: false,
error: err instanceof Error ? err.message : 'Unknown error',

View File

@ -0,0 +1,180 @@
"use client"
import { ComponentInstance } from "@/lib/store"
import { cn } from "@/lib/utils"
interface WireframeRendererProps {
component: ComponentInstance
onClick?: () => void
onMouseDown?: (e: React.MouseEvent) => void
}
export function WireframeRenderer({
component,
onClick,
onMouseDown
}: WireframeRendererProps) {
const { type, props } = component
const renderWireframeElement = (element: any, index: number) => {
const elementProps = {
x: element.x || 0,
y: element.y || 0,
width: element.width || 100,
height: element.height || 50,
fill: element.fill || 'none',
stroke: element.stroke || '#000000',
strokeWidth: element.strokeWidth || 1
}
switch (element.type) {
case 'rect':
return (
<rect
key={index}
{...elementProps}
rx={element.rx || 0}
ry={element.ry || 0}
/>
)
case 'circle':
return (
<circle
key={index}
cx={element.cx || element.x + element.width / 2}
cy={element.cy || element.y + element.height / 2}
r={element.r || Math.min(element.width, element.height) / 2}
fill={element.fill || 'none'}
stroke={element.stroke || '#000000'}
strokeWidth={element.strokeWidth || 1}
/>
)
case 'text':
return (
<text
key={index}
x={element.x || 0}
y={element.y || 0}
fill={element.fill || '#000000'}
fontSize={element.fontSize || 16}
fontFamily={element.fontFamily || 'sans-serif'}
fontWeight={element.fontWeight || 'normal'}
>
{element.text || ''}
</text>
)
case 'line':
return (
<line
key={index}
x1={element.x || 0}
y1={element.y || 0}
x2={(element.x || 0) + element.width}
y2={(element.y || 0) + element.height}
stroke={element.stroke || '#000000'}
strokeWidth={element.strokeWidth || 1}
/>
)
case 'path':
return (
<rect
key={index}
{...elementProps}
fill="none"
strokeDasharray="5,5"
/>
)
case 'group':
return (
<g key={index}>
{element.children?.map((child: any, childIndex: number) =>
renderWireframeElement(child, childIndex)
)}
</g>
)
default:
return (
<rect
key={index}
{...elementProps}
fill="none"
strokeDasharray="3,3"
/>
)
}
}
if (type === 'wireframe-svg') {
const { elements, width, height, deviceType } = props
return (
<div
className={cn(
"w-full h-full border border-gray-300 rounded-lg overflow-hidden bg-white",
"hover:shadow-lg transition-shadow"
)}
onClick={onClick}
onMouseDown={onMouseDown}
>
<div className="p-2 bg-gray-100 border-b text-xs font-medium text-gray-600">
Wireframe ({deviceType}) - {elements?.length || 0} elements
</div>
<div className="p-4">
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
className="max-w-full max-h-full"
style={{ border: '1px solid #e5e7eb' }}
>
{elements?.map((element: any, index: number) =>
renderWireframeElement(element, index)
)}
</svg>
</div>
</div>
)
}
// Handle individual wireframe elements
const elementProps = {
x: props.x || 0,
y: props.y || 0,
width: props.width || 100,
height: props.height || 50,
fill: props.fill || 'none',
stroke: props.stroke || '#000000',
strokeWidth: props.strokeWidth || 1
}
return (
<div
className={cn(
"w-full h-full border border-gray-300 rounded-lg overflow-hidden bg-white",
"hover:shadow-lg transition-shadow"
)}
onClick={onClick}
onMouseDown={onMouseDown}
>
<div className="p-1 bg-gray-100 border-b text-xs font-medium text-gray-600">
{type.replace('wireframe-', '')}
</div>
<div className="p-2">
<svg
width={elementProps.width}
height={elementProps.height}
viewBox={`0 0 ${elementProps.width} ${elementProps.height}`}
className="max-w-full max-h-full"
>
{renderWireframeElement({ type: type.replace('wireframe-', ''), ...elementProps }, 0)}
</svg>
</div>
</div>
)
}

View File

@ -4,8 +4,8 @@
*/
// Main backend URL - change this to update all API calls
export const BACKEND_URL = 'http://localhost:8000';
// export const BACKEND_URL = 'https://backend.codenuk.com';
// export const BACKEND_URL = 'http://localhost:8000';
export const BACKEND_URL = 'https://backend.codenuk.com';
// Realtime notifications socket URL (Template Manager emits notifications)

156
src/lib/store.ts Normal file
View File

@ -0,0 +1,156 @@
import { create } from "zustand"
import { persist } from "zustand/middleware"
import { ParsedWireframe } from "./wireframe-converter"
export interface ComponentInstance {
id: string
type: string
props: Record<string, any>
position: { x: number; y: number }
size?: { width: number; height: number }
}
export interface WireframeData {
id: string
name: string
deviceType: 'desktop' | 'tablet' | 'mobile'
data: ParsedWireframe
createdAt: Date
updatedAt: Date
}
interface EditorState {
components: ComponentInstance[]
selectedComponent: ComponentInstance | null
wireframes: WireframeData[]
currentWireframe: WireframeData | null
showWireframes: boolean
wireframeRenderMode: 'svg' | 'editable'
addComponent: (component: ComponentInstance) => void
addComponents: (components: ComponentInstance[]) => void
setComponents: (components: ComponentInstance[]) => void
updateComponent: (id: string, updates: Partial<ComponentInstance>) => void
removeComponent: (id: string) => void
selectComponent: (component: ComponentInstance | null) => void
moveComponent: (id: string, position: { x: number; y: number }) => void
clearAll: () => void
addWireframe: (wireframe: WireframeData) => void
updateWireframe: (id: string, updates: Partial<WireframeData>) => void
removeWireframe: (id: string) => void
setCurrentWireframe: (wireframe: WireframeData | null) => void
setShowWireframes: (show: boolean) => void
setWireframeRenderMode: (mode: 'svg' | 'editable') => void
clearWireframes: () => void
clearWireframeComponents: () => void
}
export const useEditorStore = create<EditorState>()(
persist(
(set, get) => ({
components: [],
selectedComponent: null,
wireframes: [],
currentWireframe: null,
showWireframes: false,
wireframeRenderMode: 'svg',
addComponent: (component) =>
set((state) => ({
components: [...state.components, component],
})),
addComponents: (components) =>
set((state) => ({
components: [...state.components, ...components],
})),
setComponents: (components) => set({ components }),
updateComponent: (id, updates) =>
set((state) => ({
components: state.components.map((comp) =>
comp.id === id
? {
...comp,
...updates,
props: updates.props ? { ...comp.props, ...updates.props } : comp.props,
}
: comp,
),
selectedComponent:
state.selectedComponent?.id === id
? {
...state.selectedComponent,
...updates,
props: updates.props
? { ...state.selectedComponent.props, ...updates.props }
: state.selectedComponent.props,
}
: state.selectedComponent,
})),
removeComponent: (id) =>
set((state) => ({
components: state.components.filter((comp) => comp.id !== id),
selectedComponent: state.selectedComponent?.id === id ? null : state.selectedComponent,
})),
selectComponent: (component) => set({ selectedComponent: component }),
moveComponent: (id, position) =>
set((state) => ({
components: state.components.map((comp) => (comp.id === id ? { ...comp, position } : comp)),
})),
clearAll: () =>
set({
components: [],
selectedComponent: null,
}),
addWireframe: (wireframe) =>
set((state) => ({
wireframes: [...state.wireframes, wireframe],
})),
updateWireframe: (id, updates) =>
set((state) => ({
wireframes: state.wireframes.map((wireframe) =>
wireframe.id === id
? {
...wireframe,
...updates,
updatedAt: new Date(),
}
: wireframe,
),
})),
removeWireframe: (id) =>
set((state) => ({
wireframes: state.wireframes.filter((wireframe) => wireframe.id !== id),
currentWireframe: state.currentWireframe?.id === id ? null : state.currentWireframe,
})),
setCurrentWireframe: (wireframe) => set({ currentWireframe: wireframe }),
setShowWireframes: (show) => set({ showWireframes: show }),
setWireframeRenderMode: (mode) => set({ wireframeRenderMode: mode }),
clearWireframes: () =>
set({
wireframes: [],
currentWireframe: null,
}),
clearWireframeComponents: () =>
set((state) => ({
components: state.components.filter(comp => !comp.type.startsWith('wireframe-')),
})),
}),
{
name: "editor-storage",
},
),
)

View File

@ -0,0 +1,474 @@
"use client"
import React from 'react'
import { ComponentInstance } from './store'
export interface WireframeElement {
id: string
type: 'rect' | 'circle' | 'text' | 'path' | 'line' | 'group'
x: number
y: number
width: number
height: number
fill?: string
stroke?: string
strokeWidth?: number
text?: string
fontSize?: number
fontFamily?: string
fontWeight?: string
rx?: number
ry?: number
cx?: number
cy?: number
r?: number
children?: WireframeElement[]
}
export interface ParsedWireframe {
elements: WireframeElement[]
width: number
height: number
deviceType: 'desktop' | 'tablet' | 'mobile'
}
/**
* Converts SVG wireframe data to React components for the component canvas
*/
export class WireframeToComponentsConverter {
private static instance: WireframeToComponentsConverter
private componentCounter = 0
static getInstance(): WireframeToComponentsConverter {
if (!WireframeToComponentsConverter.instance) {
WireframeToComponentsConverter.instance = new WireframeToComponentsConverter()
}
return WireframeToComponentsConverter.instance
}
/**
* Parse SVG string and extract wireframe elements
*/
parseSVGToWireframe(svgString: string, deviceType: 'desktop' | 'tablet' | 'mobile' = 'desktop'): ParsedWireframe {
try {
const parser = new DOMParser()
const svgDoc = parser.parseFromString(svgString, 'image/svg+xml')
const svgElement = svgDoc.querySelector('svg')
if (!svgElement) {
throw new Error('No SVG element found')
}
// Get SVG dimensions
const viewBox = svgElement.getAttribute('viewBox')
let width = 800
let height = 600
if (viewBox) {
const values = viewBox.split(/\s+|,/).map(Number).filter(n => !isNaN(n))
if (values.length >= 4) {
width = values[2]
height = values[3]
}
} else {
const widthAttr = svgElement.getAttribute('width')
const heightAttr = svgElement.getAttribute('height')
if (widthAttr && heightAttr) {
width = parseFloat(widthAttr.replace(/px|pt|em|rem|%/g, '')) || 800
height = parseFloat(heightAttr.replace(/px|pt|em|rem|%/g, '')) || 600
}
}
const elements = this.parseSVGElements(svgElement)
return {
elements,
width,
height,
deviceType
}
} catch (error) {
console.error('Error parsing SVG:', error)
return {
elements: [],
width: 800,
height: 600,
deviceType
}
}
}
/**
* Parse SVG elements recursively
*/
private parseSVGElements(svgElement: Element): WireframeElement[] {
const elements: WireframeElement[] = []
Array.from(svgElement.children).forEach((element) => {
const tagName = element.tagName.toLowerCase()
// Skip non-visual elements
if (['defs', 'style', 'title', 'desc', 'metadata', 'clippath', 'mask'].includes(tagName)) {
return
}
const wireframeElement = this.parseElement(element)
if (wireframeElement) {
elements.push(wireframeElement)
}
})
return elements
}
/**
* Parse individual SVG element
*/
private parseElement(element: Element): WireframeElement | null {
const tagName = element.tagName.toLowerCase()
const id = element.getAttribute('id') || `element-${++this.componentCounter}`
// Check visibility
const visibility = element.getAttribute('visibility')
const display = element.getAttribute('display')
const opacity = element.getAttribute('opacity')
if (visibility === 'hidden' || display === 'none' || opacity === '0') {
return null
}
switch (tagName) {
case 'rect':
return this.parseRect(element as SVGRectElement, id)
case 'circle':
return this.parseCircle(element as SVGCircleElement, id)
case 'ellipse':
return this.parseEllipse(element as SVGEllipseElement, id)
case 'text':
return this.parseText(element as SVGTextElement, id)
case 'path':
return this.parsePath(element as SVGPathElement, id)
case 'line':
return this.parseLine(element as SVGLineElement, id)
case 'g':
return this.parseGroup(element, id)
default:
return null
}
}
private parseRect(element: SVGRectElement, id: string): WireframeElement {
const x = parseFloat(element.getAttribute('x') || '0')
const y = parseFloat(element.getAttribute('y') || '0')
const width = parseFloat(element.getAttribute('width') || '100')
const height = parseFloat(element.getAttribute('height') || '100')
const fill = element.getAttribute('fill') || 'none'
const stroke = element.getAttribute('stroke') || '#000000'
const strokeWidth = parseFloat(element.getAttribute('stroke-width') || '1')
const rx = parseFloat(element.getAttribute('rx') || '0')
const ry = parseFloat(element.getAttribute('ry') || '0')
return {
id,
type: 'rect',
x,
y,
width,
height,
fill,
stroke,
strokeWidth,
rx,
ry
}
}
private parseCircle(element: SVGCircleElement, id: string): WireframeElement {
const cx = parseFloat(element.getAttribute('cx') || '0')
const cy = parseFloat(element.getAttribute('cy') || '0')
const r = parseFloat(element.getAttribute('r') || '50')
const fill = element.getAttribute('fill') || 'none'
const stroke = element.getAttribute('stroke') || '#000000'
const strokeWidth = parseFloat(element.getAttribute('stroke-width') || '1')
return {
id,
type: 'circle',
x: cx - r,
y: cy - r,
width: r * 2,
height: r * 2,
fill,
stroke,
strokeWidth,
cx,
cy,
r
}
}
private parseEllipse(element: SVGEllipseElement, id: string): WireframeElement {
const cx = parseFloat(element.getAttribute('cx') || '0')
const cy = parseFloat(element.getAttribute('cy') || '0')
const rx = parseFloat(element.getAttribute('rx') || '50')
const ry = parseFloat(element.getAttribute('ry') || '30')
const fill = element.getAttribute('fill') || 'none'
const stroke = element.getAttribute('stroke') || '#000000'
const strokeWidth = parseFloat(element.getAttribute('stroke-width') || '1')
return {
id,
type: 'circle',
x: cx - rx,
y: cy - ry,
width: rx * 2,
height: ry * 2,
fill,
stroke,
strokeWidth,
cx,
cy,
r: rx
}
}
private parseText(element: SVGTextElement, id: string): WireframeElement {
const x = parseFloat(element.getAttribute('x') || '0')
const y = parseFloat(element.getAttribute('y') || '0')
const text = element.textContent || ''
const fill = element.getAttribute('fill') || '#000000'
const fontSize = parseFloat(element.getAttribute('font-size') || '16')
const fontFamily = element.getAttribute('font-family') || 'sans-serif'
const fontWeight = element.getAttribute('font-weight') || 'normal'
// Estimate text dimensions
const estimatedWidth = text.length * (fontSize * 0.6)
const estimatedHeight = fontSize
return {
id,
type: 'text',
x,
y,
width: estimatedWidth,
height: estimatedHeight,
fill,
text,
fontSize,
fontFamily,
fontWeight
}
}
private parsePath(element: SVGPathElement, id: string): WireframeElement {
const d = element.getAttribute('d') || ''
const fill = element.getAttribute('fill') || 'none'
const stroke = element.getAttribute('stroke') || '#000000'
const strokeWidth = parseFloat(element.getAttribute('stroke-width') || '1')
// For paths, we'll create a bounding box
// This is a simplified approach - in a real implementation, you'd parse the path data
const bounds = this.getPathBounds(d)
return {
id,
type: 'path',
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
fill,
stroke,
strokeWidth
}
}
private parseLine(element: SVGLineElement, id: string): WireframeElement {
const x1 = parseFloat(element.getAttribute('x1') || '0')
const y1 = parseFloat(element.getAttribute('y1') || '0')
const x2 = parseFloat(element.getAttribute('x2') || '0')
const y2 = parseFloat(element.getAttribute('y2') || '0')
const stroke = element.getAttribute('stroke') || '#000000'
const strokeWidth = parseFloat(element.getAttribute('stroke-width') || '1')
const x = Math.min(x1, x2)
const y = Math.min(y1, y2)
const width = Math.abs(x2 - x1)
const height = Math.abs(y2 - y1)
return {
id,
type: 'line',
x,
y,
width,
height,
stroke,
strokeWidth
}
}
private parseGroup(element: Element, id: string): WireframeElement {
const children = this.parseSVGElements(element)
// Calculate group bounds
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
children.forEach(child => {
minX = Math.min(minX, child.x)
minY = Math.min(minY, child.y)
maxX = Math.max(maxX, child.x + child.width)
maxY = Math.max(maxY, child.y + child.height)
})
return {
id,
type: 'group',
x: minX === Infinity ? 0 : minX,
y: minY === Infinity ? 0 : minY,
width: maxX === -Infinity ? 0 : maxX - minX,
height: maxY === -Infinity ? 0 : maxY - minY,
children
}
}
private getPathBounds(d: string): { x: number; y: number; width: number; height: number } {
// Simplified path bounds calculation
// In a real implementation, you'd parse the path data properly
return { x: 0, y: 0, width: 100, height: 50 }
}
/**
* Convert wireframe elements to React components
*/
convertToComponents(wireframe: ParsedWireframe): ComponentInstance[] {
const components: ComponentInstance[] = []
wireframe.elements.forEach((element, index) => {
const component = this.elementToComponent(element, index)
if (component) {
components.push(component)
}
})
return components
}
/**
* Convert a single wireframe element to a React component
*/
private elementToComponent(element: WireframeElement, index: number): ComponentInstance | null {
const baseProps = {
x: element.x,
y: element.y,
width: element.width,
height: element.height,
fill: element.fill,
stroke: element.stroke,
strokeWidth: element.strokeWidth
}
switch (element.type) {
case 'rect':
return {
id: `wireframe-${element.id}-${index}`,
type: 'wireframe-rect',
props: {
...baseProps,
rx: element.rx || 0,
ry: element.ry || 0
},
position: { x: element.x, y: element.y },
size: { width: element.width, height: element.height }
}
case 'circle':
return {
id: `wireframe-${element.id}-${index}`,
type: 'wireframe-circle',
props: {
...baseProps,
cx: element.cx || element.x + element.width / 2,
cy: element.cy || element.y + element.height / 2,
r: element.r || Math.min(element.width, element.height) / 2
},
position: { x: element.x, y: element.y },
size: { width: element.width, height: element.height }
}
case 'text':
return {
id: `wireframe-${element.id}-${index}`,
type: 'wireframe-text',
props: {
...baseProps,
text: element.text || '',
fontSize: element.fontSize || 16,
fontFamily: element.fontFamily || 'sans-serif',
fontWeight: element.fontWeight || 'normal'
},
position: { x: element.x, y: element.y },
size: { width: element.width, height: element.height }
}
case 'line':
return {
id: `wireframe-${element.id}-${index}`,
type: 'wireframe-line',
props: baseProps,
position: { x: element.x, y: element.y },
size: { width: element.width, height: element.height }
}
case 'path':
return {
id: `wireframe-${element.id}-${index}`,
type: 'wireframe-path',
props: baseProps,
position: { x: element.x, y: element.y },
size: { width: element.width, height: element.height }
}
case 'group':
// For groups, we'll create a container component
return {
id: `wireframe-${element.id}-${index}`,
type: 'wireframe-group',
props: {
...baseProps,
children: element.children || []
},
position: { x: element.x, y: element.y },
size: { width: element.width, height: element.height }
}
default:
return null
}
}
/**
* Convert wireframe to a single SVG component for display
*/
convertToSVGComponent(wireframe: ParsedWireframe): ComponentInstance {
const timestamp = Date.now()
const randomId = Math.random().toString(36).substr(2, 9)
return {
id: `wireframe-svg-${timestamp}-${randomId}`,
type: 'wireframe-svg',
props: {
elements: wireframe.elements,
width: wireframe.width,
height: wireframe.height,
deviceType: wireframe.deviceType
},
position: { x: 0, y: 0 },
size: { width: wireframe.width, height: wireframe.height }
}
}
}
export const wireframeConverter = WireframeToComponentsConverter.getInstance()

View File

@ -0,0 +1,154 @@
"use client"
import { useEffect } from 'react'
import { useEditorStore } from './store'
import { wireframeConverter, ParsedWireframe } from './wireframe-converter.tsx'
/**
* Hook to integrate wireframe data from the backend with the component canvas
*/
export function useWireframeIntegration() {
const {
addWireframe,
setCurrentWireframe,
addComponent,
currentWireframe,
showWireframes,
clearWireframeComponents,
wireframeRenderMode
} = useEditorStore()
// Listen for wireframe generation events from the TLDraw canvas
useEffect(() => {
const handleWireframeGenerated = (event: CustomEvent<{
svgData: string
deviceType: 'desktop' | 'tablet' | 'mobile'
prompt: string
}>) => {
try {
const { svgData, deviceType, prompt } = event.detail
// Parse the SVG wireframe
const parsedWireframe = wireframeConverter.parseSVGToWireframe(svgData, deviceType)
// Create wireframe data
const wireframeData = {
id: `wireframe-${Date.now()}`,
name: `Wireframe - ${prompt.substring(0, 50)}...`,
deviceType,
data: parsedWireframe,
createdAt: new Date(),
updatedAt: new Date()
}
// Add to store
addWireframe(wireframeData)
setCurrentWireframe(wireframeData)
// Clear old wireframe components before adding new ones
clearWireframeComponents()
// Convert to a single exact-SVG component if showing wireframes in SVG mode
if (showWireframes && wireframeRenderMode === 'svg') {
const component = wireframeConverter.convertToSVGComponent(parsedWireframe)
addComponent(component)
}
console.log('Wireframe integrated:', wireframeData)
} catch (error) {
console.error('Error integrating wireframe:', error)
}
}
// Listen for wireframe generation events
window.addEventListener('wireframe:generated', handleWireframeGenerated as EventListener)
return () => {
window.removeEventListener('wireframe:generated', handleWireframeGenerated as EventListener)
}
}, [addWireframe, setCurrentWireframe, addComponent, showWireframes, clearWireframeComponents])
// Handle wireframe display toggle
useEffect(() => {
if (showWireframes && currentWireframe) {
// Clear old wireframe components first
clearWireframeComponents()
// Convert current wireframe to a single SVG component for display
const component = wireframeConverter.convertToSVGComponent(currentWireframe.data)
addComponent(component)
} else if (!showWireframes) {
// Clear wireframe components when hiding wireframes
clearWireframeComponents()
}
}, [showWireframes, currentWireframe, addComponent, clearWireframeComponents])
return {
currentWireframe,
showWireframes
}
}
/**
* Service to handle wireframe data conversion and management
*/
export class WireframeIntegrationService {
private static instance: WireframeIntegrationService
static getInstance(): WireframeIntegrationService {
if (!WireframeIntegrationService.instance) {
WireframeIntegrationService.instance = new WireframeIntegrationService()
}
return WireframeIntegrationService.instance
}
/**
* Convert SVG wireframe to components and add to canvas
*/
convertAndAddWireframe(
svgData: string,
deviceType: 'desktop' | 'tablet' | 'mobile',
prompt: string
) {
try {
// Parse the SVG wireframe
const parsedWireframe = wireframeConverter.parseSVGToWireframe(svgData, deviceType)
// Create wireframe data
const wireframeData = {
id: `wireframe-${Date.now()}`,
name: `Wireframe - ${prompt.substring(0, 50)}...`,
deviceType,
data: parsedWireframe,
createdAt: new Date(),
updatedAt: new Date()
}
// Dispatch event to update store
window.dispatchEvent(new CustomEvent('wireframe:generated', {
detail: { svgData, deviceType, prompt }
}))
return wireframeData
} catch (error) {
console.error('Error converting wireframe:', error)
throw error
}
}
/**
* Convert wireframe to individual components
*/
convertToComponents(wireframe: ParsedWireframe) {
return wireframeConverter.convertToComponents(wireframe)
}
/**
* Convert wireframe to single SVG component
*/
convertToSVGComponent(wireframe: ParsedWireframe) {
return wireframeConverter.convertToSVGComponent(wireframe)
}
}
export const wireframeIntegrationService = WireframeIntegrationService.getInstance()