From 3204e193fad67c1f6dcbba061dedec1782f2b860 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Wed, 28 Jan 2026 19:10:58 -0800 Subject: [PATCH 1/9] Implement story management features and update dependencies - Introduced story management functionality, including creating, listing, and managing story items. - Added new components for story display and interaction, including StoriesTab, StoryList, and StoryContent. - Integrated drag-and-drop functionality for reordering story items using @dnd-kit. - Updated dependencies for @dnd-kit packages to enhance drag-and-drop capabilities. - Bumped version for @voicebox/app, @voicebox/landing, @voicebox/tauri, and @voicebox/web to 0.1.5. - Enhanced audio playback features to support story mode with auto-play functionality. - Improved error handling and user feedback through toast notifications in story-related actions. --- app/package.json | 3 + .../components/AudioPlayer/AudioPlayer.tsx | 28 + .../Generation/FloatingGenerateBox.tsx | 133 +++- app/src/components/Sidebar.tsx | 3 +- app/src/components/StoriesTab/StoriesTab.tsx | 25 + .../components/StoriesTab/StoryChatItem.tsx | 160 +++++ .../components/StoriesTab/StoryContent.tsx | 304 +++++++++ app/src/components/StoriesTab/StoryList.tsx | 174 +++++ app/src/lib/api/client.ts | 77 +++ app/src/lib/api/types.ts | 58 ++ app/src/lib/hooks/useGenerationForm.ts | 4 +- app/src/lib/hooks/useStories.ts | 164 +++++ app/src/lib/hooks/useStoryPlayback.ts | 193 ++++++ app/src/router.tsx | 9 + app/src/stores/playerStore.ts | 23 + app/src/stores/storyStore.ts | 113 ++++ backend/database.py | 111 ++++ backend/main.py | 150 ++++- backend/models.py | 76 +++ backend/stories.py | 598 ++++++++++++++++++ bun.lock | 19 +- tauri/src-tauri/gen/Assets.car | Bin 3847048 -> 3847048 bytes 22 files changed, 2396 insertions(+), 29 deletions(-) create mode 100644 app/src/components/StoriesTab/StoriesTab.tsx create mode 100644 app/src/components/StoriesTab/StoryChatItem.tsx create mode 100644 app/src/components/StoriesTab/StoryContent.tsx create mode 100644 app/src/components/StoriesTab/StoryList.tsx create mode 100644 app/src/lib/hooks/useStories.ts create mode 100644 app/src/lib/hooks/useStoryPlayback.ts create mode 100644 app/src/stores/storyStore.ts create mode 100644 backend/stories.py 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: + + )} (