import { SparklesIcon, TextSquareIcon } from '@hugeicons/core-free-icons'; import { HugeiconsIcon } from '@hugeicons/react'; import { Icon } from '@iconify/react'; import { useMatchRoute } from '@tanstack/react-router'; import { AnimatePresence, motion } from 'framer-motion'; import { useEffect, useRef, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { useToast } from '@/components/ui/use-toast'; import { LANGUAGE_OPTIONS } from '@/lib/constants/languages'; import { useGenerationForm } from '@/lib/hooks/useGenerationForm'; import { useProfile, useProfiles } from '@/lib/hooks/useProfiles'; import { useAddStoryItem, useStory } from '@/lib/hooks/useStories'; import { cn } from '@/lib/utils/cn'; import { useStoryStore } from '@/stores/storyStore'; import { useUIStore } from '@/stores/uiStore'; interface FloatingGenerateBoxProps { isPlayerOpen?: boolean; showVoiceSelector?: boolean; } export function FloatingGenerateBox({ isPlayerOpen = false, showVoiceSelector = false, }: FloatingGenerateBoxProps) { const selectedProfileId = useUIStore((state) => state.selectedProfileId); const setSelectedProfileId = useUIStore((state) => state.setSelectedProfileId); const { data: selectedProfile } = useProfile(selectedProfileId || ''); const { data: profiles } = useProfiles(); const [isExpanded, setIsExpanded] = useState(false); const [isInstructMode, setIsInstructMode] = useState(false); const containerRef = useRef(null); const textareaRef = useRef(null); const matchRoute = useMatchRoute(); const isStoriesRoute = matchRoute({ to: '/stories' }); const selectedStoryId = useStoryStore((state) => state.selectedStoryId); const trackEditorHeight = useStoryStore((state) => state.trackEditorHeight); const { data: currentStory } = useStory(selectedStoryId); const addStoryItem = useAddStoryItem(); const { toast } = useToast(); // Calculate if track editor is visible (on stories route with items) const hasTrackEditor = isStoriesRoute && currentStory && currentStory.items.length > 0; const { form, handleSubmit, isPending } = useGenerationForm({ onSuccess: async (generationId) => { setIsExpanded(false); // If on stories route and a story is selected, add generation to story if (isStoriesRoute && selectedStoryId && generationId) { try { await addStoryItem.mutateAsync({ storyId: selectedStoryId, data: { generation_id: generationId }, }); toast({ title: 'Added to story', description: `Generation added to "${currentStory?.name || 'story'}"`, }); } catch (error) { toast({ title: 'Failed to add to story', description: error instanceof Error ? error.message : 'Could not add generation to story', variant: 'destructive', }); } } }, }); // Click away handler to collapse the box useEffect(() => { function handleClickOutside(event: MouseEvent) { const target = event.target as HTMLElement; // Don't collapse if clicking inside the container if (containerRef.current?.contains(target)) { return; } // Don't collapse if clicking on a Select dropdown (which renders in a portal) if ( target.closest('[role="listbox"]') || target.closest('[data-radix-popper-content-wrapper]') ) { return; } setIsExpanded(false); } if (isExpanded) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isExpanded]); // Set first voice as default if none selected useEffect(() => { if (!selectedProfileId && profiles && profiles.length > 0) { setSelectedProfileId(profiles[0].id); } }, [selectedProfileId, profiles, setSelectedProfileId]); // Auto-resize textarea based on content (only when expanded) useEffect(() => { if (!isExpanded) { // Reset textarea height after collapse animation completes const timeoutId = setTimeout(() => { const textarea = textareaRef.current; if (textarea) { textarea.style.height = '32px'; textarea.style.overflowY = 'hidden'; } }, 200); // Wait for animation to complete return () => clearTimeout(timeoutId); } const textarea = textareaRef.current; if (!textarea) return; const adjustHeight = () => { textarea.style.height = 'auto'; const scrollHeight = textarea.scrollHeight; const minHeight = 100; // Expanded minimum const maxHeight = 300; // Max height in pixels const targetHeight = Math.max(minHeight, Math.min(scrollHeight, maxHeight)); textarea.style.height = `${targetHeight}px`; // Show scrollbar if content exceeds max height if (scrollHeight > maxHeight) { textarea.style.overflowY = 'auto'; } else { textarea.style.overflowY = 'hidden'; } }; // Small delay to let framer animation complete const timeoutId = setTimeout(() => { adjustHeight(); }, 200); // Adjust on mount and when value changes adjustHeight(); // Watch for input changes textarea.addEventListener('input', adjustHeight); return () => { clearTimeout(timeoutId); textarea.removeEventListener('input', adjustHeight); }; }, [isExpanded]); async function onSubmit(data: Parameters[0]) { await handleSubmit(data, selectedProfileId); } return (
{/* Text field - hidden when in instruct mode */}
(