diff --git a/app/src/components/AudioPlayer/AudioPlayer.tsx b/app/src/components/AudioPlayer/AudioPlayer.tsx index 1c398ed8..44ebb266 100644 --- a/app/src/components/AudioPlayer/AudioPlayer.tsx +++ b/app/src/components/AudioPlayer/AudioPlayer.tsx @@ -7,8 +7,8 @@ import { Slider } from '@/components/ui/slider'; import { apiClient } from '@/lib/api/client'; import { formatAudioDuration } from '@/lib/utils/audio'; import { debug } from '@/lib/utils/debug'; -import { usePlayerStore } from '@/stores/playerStore'; import { usePlatform } from '@/platform/PlatformContext'; +import { usePlayerStore } from '@/stores/playerStore'; export function AudioPlayer() { const platform = usePlatform(); @@ -360,7 +360,7 @@ export function AudioPlayer() { if (shouldAutoPlayNow) { // Clear the flag first usePlayerStore.getState().clearAutoPlayFlag(); - + // Use a small delay to ensure audio element is fully ready setTimeout(() => { wavesurfer.play().catch((error) => { @@ -665,7 +665,7 @@ export function AudioPlayer() { // 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) { @@ -833,11 +833,7 @@ export function AudioPlayer() { className="shrink-0" title={duration === 0 && !isLoading ? 'Audio not loaded' : ''} aria-label={ - duration === 0 && !isLoading - ? 'Audio not loaded' - : isPlaying - ? 'Pause' - : 'Play' + duration === 0 && !isLoading ? 'Audio not loaded' : isPlaying ? 'Pause' : 'Play' } > {isPlaying ? : } @@ -872,7 +868,9 @@ export function AudioPlayer() { {/* Title */} {title && ( -
{title}
+
+ {title} +
)} {/* Loop Button */} @@ -888,7 +886,11 @@ export function AudioPlayer() { {/* Volume Control */} -
+
- - - handlePlay(gen.id, gen.text, gen.profile_id)} - > - - Play - - handleDownloadAudio(gen.id, gen.text)} - disabled={exportGenerationAudio.isPending} - > - - Export Audio - - handleExportPackage(gen.id, gen.text)} - disabled={exportGeneration.isPending} - > - - Export Package - - handleDeleteClick(gen.id, gen.profile_name)} - disabled={deleteGeneration.isPending} - className="text-destructive focus:text-destructive" - > - - Delete - - - + {isFailed ? ( + + ) : isPlayable ? ( + + + + + + handlePlay(gen.id, gen.text, gen.profile_id)} + > + + Play + + handleDownloadAudio(gen.id, gen.text)} + disabled={exportGenerationAudio.isPending} + > + + Export Audio + + handleExportPackage(gen.id, gen.text)} + disabled={exportGeneration.isPending} + > + + Export Package + + handleDeleteClick(gen.id, gen.profile_name)} + disabled={deleteGeneration.isPending} + className="text-destructive focus:text-destructive" + > + + Delete + + + + ) : null}
); @@ -387,7 +445,8 @@ export function HistoryTable() { Delete Generation - Are you sure you want to delete this generation from "{generationToDelete?.name}"? This action cannot be undone. + Are you sure you want to delete this generation from "{generationToDelete?.name}"? + This action cannot be undone. diff --git a/app/src/components/MainEditor/MainEditor.tsx b/app/src/components/MainEditor/MainEditor.tsx index 9d597b1e..8f210bcf 100644 --- a/app/src/components/MainEditor/MainEditor.tsx +++ b/app/src/components/MainEditor/MainEditor.tsx @@ -13,7 +13,7 @@ import { } from '@/components/ui/dialog'; import { useToast } from '@/components/ui/use-toast'; import { ProfileList } from '@/components/VoiceProfiles/ProfileList'; -import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui'; + import { useImportProfile } from '@/lib/hooks/useProfiles'; import { cn } from '@/lib/utils/cn'; import { usePlayerStore } from '@/stores/playerStore'; @@ -77,9 +77,9 @@ export function MainEditor() { return ( // Main view: Profiles top left, Generator bottom left, History right -
+
{/* Left Column */} -
+
{/* Scroll Mask - Always visible, behind content */}
@@ -110,10 +110,7 @@ export function MainEditor() { {/* Scrollable Content */}
@@ -123,6 +120,9 @@ export function MainEditor() {
+ {/* Divider - single column only */} + {/*
*/} + {/* Right Column - History */}
diff --git a/app/src/components/ServerSettings/ConnectionForm.tsx b/app/src/components/ServerSettings/ConnectionForm.tsx index 3b5ad845..e84f20ea 100644 --- a/app/src/components/ServerSettings/ConnectionForm.tsx +++ b/app/src/components/ServerSettings/ConnectionForm.tsx @@ -124,6 +124,7 @@ export function ConnectionForm() {
{ setKeepServerRunningOnClose(checked); @@ -158,6 +159,7 @@ export function ConnectionForm() {
{ setMode(checked ? 'remote' : 'local'); diff --git a/app/src/components/ServerSettings/GenerationSettings.tsx b/app/src/components/ServerSettings/GenerationSettings.tsx index 048bd424..df499e17 100644 --- a/app/src/components/ServerSettings/GenerationSettings.tsx +++ b/app/src/components/ServerSettings/GenerationSettings.tsx @@ -10,6 +10,8 @@ export function GenerationSettings() { const setCrossfadeMs = useServerStore((state) => state.setCrossfadeMs); const normalizeAudio = useServerStore((state) => state.normalizeAudio); const setNormalizeAudio = useServerStore((state) => state.setNormalizeAudio); + const autoplayOnGenerate = useServerStore((state) => state.autoplayOnGenerate); + const setAutoplayOnGenerate = useServerStore((state) => state.setAutoplayOnGenerate); return ( @@ -35,7 +37,7 @@ export function GenerationSettings() { value={[maxChunkChars]} onValueChange={([value]) => setMaxChunkChars(value)} min={100} - max={2000} + max={5000} step={50} aria-label="Auto-chunking character limit" /> @@ -73,6 +75,7 @@ export function GenerationSettings() { id="normalizeAudio" checked={normalizeAudio} onCheckedChange={setNormalizeAudio} + className="mt-[6px]" />
+ +
+ +
+ +

+ Automatically play audio when a generation completes. +

+
+
diff --git a/app/src/components/Sidebar.tsx b/app/src/components/Sidebar.tsx index a849344f..5349f478 100644 --- a/app/src/components/Sidebar.tsx +++ b/app/src/components/Sidebar.tsx @@ -1,9 +1,9 @@ import { Link, useMatchRoute } from '@tanstack/react-router'; -import { Box, BookOpen, Loader2, Mic, Server, Speaker, Volume2 } from 'lucide-react'; +import { BookOpen, Box, Mic, Server, Speaker, Volume2 } from 'lucide-react'; import voiceboxLogo from '@/assets/voicebox-logo.png'; import { cn } from '@/lib/utils/cn'; -import { useGenerationStore } from '@/stores/generationStore'; import { usePlayerStore } from '@/stores/playerStore'; +import { version } from '../../package.json'; interface SidebarProps { isMacOS?: boolean; @@ -19,10 +19,8 @@ const tabs = [ ]; export function Sidebar({ isMacOS }: SidebarProps) { - const isGenerating = useGenerationStore((state) => state.isGenerating); - const audioUrl = usePlayerStore((state) => state.audioUrl); - const isPlayerVisible = !!audioUrl; const matchRoute = useMatchRoute(); + const isPlayerOpen = !!usePlayerStore((s) => s.audioUrl); return (
- {/* Spacer to push loader to bottom */} -
- - {/* Generation Loader */} - {isGenerating && ( -
- -
- )} + {/* Version */} +
+ v{version} +
); } diff --git a/app/src/components/StoriesTab/StoryContent.tsx b/app/src/components/StoriesTab/StoryContent.tsx index 483e6657..0f53c2c9 100644 --- a/app/src/components/StoriesTab/StoryContent.tsx +++ b/app/src/components/StoriesTab/StoryContent.tsx @@ -13,8 +13,11 @@ import { sortableKeyboardCoordinates, verticalListSortingStrategy, } from '@dnd-kit/sortable'; +import { Link } from '@tanstack/react-router'; +import { AnimatePresence, motion } from 'framer-motion'; import { Download, Plus } from 'lucide-react'; import { useEffect, useMemo, useRef, useState } from 'react'; +import Loader from 'react-loaders'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; @@ -28,6 +31,7 @@ import { useStory, } from '@/lib/hooks/useStories'; import { useStoryPlayback } from '@/lib/hooks/useStoryPlayback'; +import { useGenerationStore } from '@/stores/generationStore'; import { useStoryStore } from '@/stores/storyStore'; import { SortableStoryChatItem } from './StoryChatItem'; @@ -40,6 +44,7 @@ export function StoryContent() { const addStoryItem = useAddStoryItem(); const { toast } = useToast(); const scrollRef = useRef(null); + const pendingCount = useGenerationStore((s) => s.pendingGenerationIds.size); // Add generation popover state const [searchQuery, setSearchQuery] = useState(''); @@ -53,9 +58,9 @@ export function StoryContent() { const query = searchQuery.toLowerCase(); return historyData.items.filter( (gen) => + gen.status === 'completed' && !storyGenerationIds.has(gen.id) && - (gen.text.toLowerCase().includes(query) || - gen.profile_name.toLowerCase().includes(query)), + (gen.text.toLowerCase().includes(query) || gen.profile_name.toLowerCase().includes(query)), ); }, [historyData, story, searchQuery]); @@ -267,7 +272,31 @@ export function StoryContent() {

{story.description}

)}
-
+
+ + {pendingCount > 0 && ( + + +
+
+ +
+
+ + Generating {pendingCount} {pendingCount === 1 ? 'audio' : 'audios'} + + +
+ )} +