diff --git a/packages/ui/package.json b/packages/ui/package.json index 986cfd86..854f7625 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -27,14 +27,11 @@ "test:all": "playwright test", "test:home": "playwright test tests/home.spec.ts --project=chromium", "test:post": "playwright test tests/post.spec.ts --project=chromium", - "test:wizard": "playwright test tests/wizard.spec.ts --project=chromium --ui", - "test:responsive": "playwright test tests/responsive.spec.ts --project=chromium", "test:ui": "playwright test --ui", "test:headed": "playwright test --headed --project=chromium", "test:debug": "playwright test --debug", "test:report": "playwright show-report", "test:verify-env": "node tests/verify-env.js", - "supabase:types": "npx supabase gen types typescript --linked > src/integrations/supabase/types.ts", "screenshots": "playwright test tests/example.spec.ts" }, "dependencies": { diff --git a/packages/ui/src/components/AIPageGenerator.tsx b/packages/ui/src/components/AIPageGenerator.tsx new file mode 100644 index 00000000..2562f265 --- /dev/null +++ b/packages/ui/src/components/AIPageGenerator.tsx @@ -0,0 +1,440 @@ +/** + * AI Page Generator Component + * A specialized version of AITextGenerator for creating new pages from scratch. + * It removes application-specific logic like 'Apply', 'Replace', 'Append'. + */ + +import React from 'react'; +import { T } from '@/i18n'; +import { + Sparkles, + Mic, + MicOff, + Loader2, + FileTextIcon, + Plus, + Trash2, + ArrowUp, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { Card } from '@/components/ui/card'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { ProviderSelector } from '@/components/filters/ProviderSelector'; +import { ModelSelector } from '@/components/ImageWizard/components/ModelSelector'; +import { useVoiceInput } from '@/hooks/useVoiceInput'; +import { useProviderSettings } from '@/hooks/useProviderSettings'; +import { ImagePickerDialog } from '@/components/widgets/ImagePickerDialog'; +import { Image as ImageIcon, X } from 'lucide-react'; +import { useLog } from '@/contexts/LogContext'; + +interface AIPageGeneratorProps { + // Prompt state + prompt: string; + onPromptChange: (prompt: string) => void; + + // Actions + onGenerate: (options: { useImageTools: boolean; model?: string; imageModel?: string; referenceImages?: string[] }) => void; + onCancel?: () => void; + + // State flags + isGenerating: boolean; + generationStatus?: 'idle' | 'transcribing' | 'generating' | 'creating' | 'success' | 'error'; + disabled?: boolean; + promptHistory?: string[]; + historyIndex?: number; + onNavigateHistory?: (direction: 'up' | 'down') => void; + initialReferenceImages?: string[]; +} + +export const AIPageGenerator: React.FC = ({ + prompt, + onPromptChange, + onGenerate, + onCancel, + isGenerating, + generationStatus, + disabled = false, + promptHistory = [], + historyIndex = -1, + onNavigateHistory, + initialReferenceImages = [] +}) => { + const { + loading: loadingSettings, + selectedProvider, + selectedModel, + onProviderChange, + onModelChange, + } = useProviderSettings(); + + const [imageToolsEnabled, setImageToolsEnabled] = React.useState(true); + const [referenceImages, setReferenceImages] = React.useState([]); // Using any[] for now to avoid extensive type imports, will refine + const [showImagePicker, setShowImagePicker] = React.useState(false); + + // Initialize reference images from prop + React.useEffect(() => { + if (initialReferenceImages.length > 0) { + const initialPics = initialReferenceImages.map((url, index) => ({ + id: `preload-${index}`, + url: url, + title: 'Selected Image', + image_url: url, + width: 0, + height: 0, + created_at: new Date().toISOString(), + user_id: 'current-user' + })); + setReferenceImages(initialPics); + } + }, [initialReferenceImages]); + + // Image model state with persistence + const [imageModel, setImageModel] = React.useState(() => { + return localStorage.getItem('aipagegenerator-image-model') || 'google/gemini-3-pro-image-preview'; + }); + + // Save image model to localStorage when it changes + React.useEffect(() => { + if (imageModel) { + localStorage.setItem('aipagegenerator-image-model', imageModel); + } + }, [imageModel]); + + // Templates and optimization state removed as they are not fully implemented + // const [templates, setTemplates] = React.useState<{ name: string; template: string }[]>([]); + const [isOptimizing, setIsOptimizing] = React.useState(false); + + const { isRecording, isTranscribing, handleMicrophoneToggle } = useVoiceInput(onPromptChange); + const { logs } = useLog(); + const logsEndRef = React.useRef(null); + + // Auto-scroll logs + React.useEffect(() => { + if (isGenerating && logsEndRef.current) { + logsEndRef.current.scrollIntoView({ behavior: 'smooth' }); + } + }, [logs, isGenerating]); + + const getStatusMessage = () => { + switch (generationStatus) { + case 'transcribing': return 'Transcribing audio...'; + case 'generating': return 'Generating content...'; + case 'creating': return 'Creating page...'; + case 'success': return 'Success!'; + default: return onCancel ? 'Cancel Generation' : 'Generating...'; + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if ((e.key === 'Enter' && e.ctrlKey) && prompt.trim() && !isGenerating) { + e.preventDefault(); + onGenerate({ + useImageTools: imageToolsEnabled, + model: selectedModel, + imageModel, + referenceImages: referenceImages.map(img => img.image_url || img.src) + }); + } else if (e.key === 'ArrowUp' && e.ctrlKey && onNavigateHistory) { + e.preventDefault(); + onNavigateHistory('up'); + } else if (e.key === 'ArrowDown' && e.ctrlKey && onNavigateHistory) { + e.preventDefault(); + onNavigateHistory('down'); + } + }; + + return ( +
+
+ {/* Left Column: Settings */} + +

+ Settings +

+ + + +
+
+ + +
+ + {imageToolsEnabled ? 'Enabled' : 'Text Only'} + +
+ + {imageToolsEnabled && ( +
+ +
+ )} + + {imageToolsEnabled && ( +
+
+ + + {referenceImages.length} selected + +
+ + {referenceImages.length === 0 ? ( + + ) : ( +
+ {referenceImages.map((img) => ( +
+ {img.title} +
+ +
+ ))} + +
+ )} +

+ Images to help guide the AI generation +

+
+ )} + + setShowImagePicker(false)} + multiple={true} + currentValues={referenceImages.map(img => img.id)} + onMultiSelectPictures={(pictures) => { + setReferenceImages(pictures); + setShowImagePicker(false); + }} + /> + +
+ + + + + + {/* Templates logic would go here */} +
+ No templates saved yet +
+ + + + Save current as template + +
+
+ + +
+ + + {/* Right Column: Prompt Input */} +
+ {promptHistory.length > 0 && onNavigateHistory && ( +
+ + History: {historyIndex >= 0 ? `${historyIndex + 1}/${promptHistory.length}` : 'Current'} + +
+ + +
+
+ )} +
+