diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 8ce6ca82..fb47fa00 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.1.1
+current_version = 0.1.3
commit = True
tag = True
tag_name = v{new_version}
diff --git a/.gitignore b/.gitignore
index cece5001..05f7ef0d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,8 +30,6 @@ target/
*.swo
*~
-tauri/src-tauri/gen/Assets.car
-
# OS
.DS_Store
Thumbs.db
diff --git a/app/package.json b/app/package.json
index aaa054c3..b9f48eba 100644
--- a/app/package.json
+++ b/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@voicebox/app",
- "version": "0.1.1",
+ "version": "0.1.3",
"private": true,
"type": "module",
"scripts": {
diff --git a/app/src/App.tsx b/app/src/App.tsx
index 202c8ac4..8168494a 100644
--- a/app/src/App.tsx
+++ b/app/src/App.tsx
@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react';
import voiceboxLogo from '@/assets/voicebox-logo.png';
import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer';
-import { GenerationForm } from '@/components/Generation/GenerationForm';
+// import { GenerationForm } from '@/components/Generation/GenerationForm';
+import { FloatingGenerateBox } from '@/components/Generation/FloatingGenerateBox';
import { HistoryTable } from '@/components/History/HistoryTable';
import ShinyText from '@/components/ShinyText';
import { Sidebar } from '@/components/Sidebar';
@@ -20,6 +21,7 @@ import {
setupWindowCloseHandler,
startServer,
} from '@/lib/tauri';
+import { usePlayerStore } from '@/stores/playerStore';
import { useServerStore } from '@/stores/serverStore';
// Track if server is starting to prevent duplicate starts
@@ -52,6 +54,7 @@ function App() {
const [activeTab, setActiveTab] = useState('main');
const [serverReady, setServerReady] = useState(false);
const [loadingMessageIndex, setLoadingMessageIndex] = useState(0);
+ const audioUrl = usePlayerStore((state) => state.audioUrl);
// Monitor active downloads/generations and show toasts for them
const activeDownloads = useRestoreActiveTasks();
@@ -175,7 +178,7 @@ function App() {
{activeTab === 'main' && (
// Main view: Profiles top left, Generator bottom left, History right
-
+
{/* Left Column */}
{/* Profiles - Top Left */}
@@ -184,15 +187,18 @@ function App() {
{/* Generator - Bottom Left */}
-
*/}
{/* Right Column - History */}
+
+ {/* Floating Generate Box */}
+
)}
{activeTab === 'voices' &&
}
diff --git a/app/src/components/Generation/.gitkeep b/app/src/components/Generation/.gitkeep
deleted file mode 100644
index 18bc6d98..00000000
--- a/app/src/components/Generation/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-# Voice generation components
diff --git a/app/src/components/Generation/FloatingGenerateBox.tsx b/app/src/components/Generation/FloatingGenerateBox.tsx
new file mode 100644
index 00000000..e0f1daf4
--- /dev/null
+++ b/app/src/components/Generation/FloatingGenerateBox.tsx
@@ -0,0 +1,282 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { AnimatePresence, motion } from 'framer-motion';
+import { Loader2, Sparkles } from 'lucide-react';
+import { useEffect, useRef, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import * as z from 'zod';
+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 { apiClient } from '@/lib/api/client';
+import { LANGUAGE_CODES, LANGUAGE_OPTIONS, type LanguageCode } from '@/lib/constants/languages';
+import { useGeneration } from '@/lib/hooks/useGeneration';
+import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
+import { useProfile } from '@/lib/hooks/useProfiles';
+import { useGenerationStore } from '@/stores/generationStore';
+import { usePlayerStore } from '@/stores/playerStore';
+import { useUIStore } from '@/stores/uiStore';
+
+const generationSchema = z.object({
+ text: z.string().min(1, 'Text is required').max(5000),
+ language: z.enum(LANGUAGE_CODES as [LanguageCode, ...LanguageCode[]]),
+ modelSize: z.enum(['1.7B', '0.6B']).optional(),
+});
+
+type GenerationFormValues = z.infer
;
+
+interface FloatingGenerateBoxProps {
+ isPlayerOpen: boolean;
+}
+
+export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps) {
+ const selectedProfileId = useUIStore((state) => state.selectedProfileId);
+ const { data: selectedProfile } = useProfile(selectedProfileId || '');
+ const generation = useGeneration();
+ const { toast } = useToast();
+ const setAudio = usePlayerStore((state) => state.setAudio);
+ const setIsGenerating = useGenerationStore((state) => state.setIsGenerating);
+ const [downloadingModelName, setDownloadingModelName] = useState(null);
+ const [downloadingDisplayName, setDownloadingDisplayName] = useState(null);
+ const [isExpanded, setIsExpanded] = useState(false);
+ const containerRef = useRef(null);
+
+ useModelDownloadToast({
+ modelName: downloadingModelName || '',
+ displayName: downloadingDisplayName || '',
+ enabled: !!downloadingModelName,
+ });
+
+ const form = useForm({
+ resolver: zodResolver(generationSchema),
+ defaultValues: {
+ text: '',
+ language: 'en',
+ modelSize: '1.7B',
+ },
+ });
+
+ // 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]);
+
+ async function onSubmit(data: GenerationFormValues) {
+ if (!selectedProfileId) {
+ toast({
+ title: 'No profile selected',
+ description: 'Please select a voice profile from the cards above.',
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ try {
+ setIsGenerating(true);
+
+ const modelName = `qwen-tts-${data.modelSize}`;
+ const displayName = data.modelSize === '1.7B' ? 'Qwen TTS 1.7B' : 'Qwen TTS 0.6B';
+
+ try {
+ const modelStatus = await apiClient.getModelStatus();
+ const model = modelStatus.models.find((m) => m.model_name === modelName);
+
+ if (model && !model.downloaded) {
+ setDownloadingModelName(modelName);
+ setDownloadingDisplayName(displayName);
+ }
+ } catch (error) {
+ console.error('Failed to check model status:', error);
+ }
+
+ const result = await generation.mutateAsync({
+ profile_id: selectedProfileId,
+ text: data.text,
+ language: data.language,
+ model_size: data.modelSize,
+ });
+
+ toast({
+ title: 'Generation complete!',
+ description: `Audio generated (${result.duration.toFixed(2)}s)`,
+ });
+
+ const audioUrl = apiClient.getAudioUrl(result.id);
+ setAudio(audioUrl, result.id, data.text.substring(0, 50));
+
+ form.reset();
+ setIsExpanded(false);
+ } catch (error) {
+ toast({
+ title: 'Generation failed',
+ description: error instanceof Error ? error.message : 'Failed to generate audio',
+ variant: 'destructive',
+ });
+ } finally {
+ setIsGenerating(false);
+ setDownloadingModelName(null);
+ setDownloadingDisplayName(null);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/app/src/components/ServerSettings/.gitkeep b/app/src/components/ServerSettings/.gitkeep
deleted file mode 100644
index 2d8e46e8..00000000
--- a/app/src/components/ServerSettings/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-# Server settings and connection components
diff --git a/app/src/components/ServerSettings/UpdateStatus.tsx b/app/src/components/ServerSettings/UpdateStatus.tsx
index 7e1e160d..475aabc6 100644
--- a/app/src/components/ServerSettings/UpdateStatus.tsx
+++ b/app/src/components/ServerSettings/UpdateStatus.tsx
@@ -8,7 +8,7 @@ import { useAutoUpdater } from '@/hooks/useAutoUpdater';
import { getVersion } from '@tauri-apps/api/app';
export function UpdateStatus() {
- const { status, checkForUpdates, downloadAndInstall } = useAutoUpdater(false);
+ const { status, checkForUpdates, downloadAndInstall, restartAndInstall } = useAutoUpdater(false);
const [currentVersion, setCurrentVersion] = useState('');
useEffect(() => {
@@ -30,7 +30,7 @@ export function UpdateStatus() {