diff --git a/app/package.json b/app/package.json index 1c8f9790..20147133 100644 --- a/app/package.json +++ b/app/package.json @@ -13,6 +13,9 @@ "check": "biome check --write src" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^3.9.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", diff --git a/app/src/components/AudioPlayer/AudioPlayer.tsx b/app/src/components/AudioPlayer/AudioPlayer.tsx index d6b843df..a03224c9 100644 --- a/app/src/components/AudioPlayer/AudioPlayer.tsx +++ b/app/src/components/AudioPlayer/AudioPlayer.tsx @@ -402,6 +402,11 @@ export function AudioPlayer() { wavesurfer.play(); } else { setIsPlaying(false); + // Trigger finish callback if set + const onFinish = usePlayerStore.getState().onFinish; + if (onFinish) { + onFinish(); + } } }); @@ -653,6 +658,29 @@ export function AudioPlayer() { clearRestartFlag(); }, [shouldRestart, duration, setIsPlaying, clearRestartFlag]); + // Handle shouldAutoPlay flag - for story mode auto-advance + const shouldAutoPlay = usePlayerStore((state) => state.shouldAutoPlay); + const clearAutoPlayFlag = usePlayerStore((state) => state.clearAutoPlayFlag); + + useEffect(() => { + const wavesurfer = wavesurferRef.current; + if (!wavesurfer || !shouldAutoPlay || duration === 0) { + return; + } + + // Auto-play the newly loaded audio + debug.log('Auto-playing next track in story mode'); + wavesurfer.seekTo(0); + wavesurfer.play().catch((error) => { + debug.error('Failed to auto-play:', error); + setIsPlaying(false); + setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`); + }); + + // Clear the auto-play flag + clearAutoPlayFlag(); + }, [shouldAutoPlay, duration, setIsPlaying, clearAutoPlayFlag]); + // Handle loop - WaveSurfer handles this via the 'finish' event const handlePlayPause = async () => { diff --git a/app/src/components/Generation/FloatingGenerateBox.tsx b/app/src/components/Generation/FloatingGenerateBox.tsx index 3cdfd3eb..39bda39b 100644 --- a/app/src/components/Generation/FloatingGenerateBox.tsx +++ b/app/src/components/Generation/FloatingGenerateBox.tsx @@ -1,6 +1,7 @@ import { AnimatePresence, motion } from 'framer-motion'; -import { Loader2, Sparkles } from 'lucide-react'; +import { Loader2, MessageSquare, Sparkles } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; +import { useMatchRoute } from '@tanstack/react-router'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'; import { @@ -13,22 +14,55 @@ import { import { Textarea } from '@/components/ui/textarea'; import { LANGUAGE_OPTIONS } from '@/lib/constants/languages'; import { useGenerationForm } from '@/lib/hooks/useGenerationForm'; -import { useProfile } from '@/lib/hooks/useProfiles'; +import { useProfile, useProfiles } from '@/lib/hooks/useProfiles'; +import { useStory, useAddStoryItem } from '@/lib/hooks/useStories'; import { useUIStore } from '@/stores/uiStore'; +import { useStoryStore } from '@/stores/storyStore'; +import { useToast } from '@/components/ui/use-toast'; +import { cn } from '@/lib/utils/cn'; interface FloatingGenerateBoxProps { isPlayerOpen: boolean; + showVoiceSelector?: boolean; } -export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps) { +export function FloatingGenerateBox({ isPlayerOpen, 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 matchRoute = useMatchRoute(); + const isStoriesRoute = matchRoute({ to: '/stories' }); + const selectedStoryId = useStoryStore((state) => state.selectedStoryId); + const { data: currentStory } = useStory(selectedStoryId); + const addStoryItem = useAddStoryItem(); + const { toast } = useToast(); const { form, handleSubmit, isPending } = useGenerationForm({ - onSuccess: () => { + 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', + }); + } + } }, }); @@ -69,7 +103,12 @@ export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps) return ( + {isInstructMode && ( + + Delivery instructions: + + )} (