codenuk_frontend_mine/src/components/prompt-side-panel.tsx

251 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { 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