251 lines
9.5 KiB
TypeScript
251 lines
9.5 KiB
TypeScript
"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 (
|
||
<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
|