-
-
-
- {steps.map((step, index) => (
-
-
-
= step.id
- ? "bg-orange-500 border-orange-500 text-white shadow-lg"
- : currentStep === step.id - 1
- ? "border-orange-300 text-orange-400"
- : "border-white/30 text-white/40"
- }`}
- >
- {step.id}
-
-
-
= step.id ? "text-orange-400" : "text-white/60"
- }`}
+ {mounted && (
+
+
+
+
+
+ {steps.map((step, index) => (
+
+
+
= step.id
+ ? "bg-orange-500 border-orange-500 text-white shadow-lg"
+ : currentStep === step.id - 1
+ ? "border-orange-300 text-orange-400"
+ : "border-white/30 text-white/40"
+ }`}
>
- {step.name}
-
-
{step.description}
+
{step.id}
+
+
+
= step.id ? "text-orange-400" : "text-white/60"
+ }`}
+ >
+ {step.name}
+
+
{step.description}
+
-
- {index < steps.length - 1 && (
-
step.id ? "text-orange-400" : "text-white/40"}`}
- />
- )}
-
- ))}
-
-
+ {index < steps.length - 1 && (
+ step.id ? "text-orange-400" : "text-white/40"}`}
+ />
+ )}
+
+ ))}
+
+
+
-
+ )}
{/* Main Content */}
- {renderStep()}
+
+ {!mounted ? (
+
+ ) : (
+ renderStep()
+ )}
+
)
diff --git a/src/components/prompt-side-panel.tsx b/src/components/prompt-side-panel.tsx
new file mode 100644
index 0000000..9c7538c
--- /dev/null
+++ b/src/components/prompt-side-panel.tsx
@@ -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.",
+ "E‑commerce 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 (
+
+
+
AI Wireframe
+ setCollapsed((c) => !c)} aria-label="Toggle panel">
+ {collapsed ? › : ‹ }
+
+
+
+ {!collapsed && (
+
+ {/* Backend Status */}
+
+ {getBackendStatusIcon()}
+ {getBackendStatusText()}
+
+
+
Prompt
+
+ )}
+
+ )
+}
+
+export default PromptSidePanel
diff --git a/src/components/prompt-toolbar.tsx b/src/components/prompt-toolbar.tsx
new file mode 100644
index 0000000..a92f18b
--- /dev/null
+++ b/src/components/prompt-toolbar.tsx
@@ -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 (
+
+
+
+ Try:
+ {examples.map((ex) => (
+ 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}
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..4a8cca4
--- /dev/null
+++ b/src/components/ui/accordion.tsx
@@ -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
) {
+ return
+}
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ )
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..0863e40
--- /dev/null
+++ b/src/components/ui/alert-dialog.tsx
@@ -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) {
+ return
+}
+
+function AlertDialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
+
+function AlertDialogHeader({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogAction({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogCancel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..eb88f32
--- /dev/null
+++ b/src/components/ui/breadcrumb.tsx
@@ -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
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
+ return (
+
+ )
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function BreadcrumbSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+ svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+ )
+}
+
+function BreadcrumbEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+
+ More
+
+ )
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index 35bd8b5..cb06d8c 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -56,4 +56,4 @@ function Button({
)
}
-export { Button, buttonVariants }
+export { Button, buttonVariants }
\ No newline at end of file
diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx
new file mode 100644
index 0000000..97cc280
--- /dev/null
+++ b/src/components/ui/chart.tsx
@@ -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 }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ 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 (
+
+
+
+
+ {children}
+
+
+
+ )
+}
+
+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 (
+