diff --git a/app/src/components/ServerSettings/ConnectionForm.tsx b/app/src/components/ServerSettings/ConnectionForm.tsx index c9de46a6..3b5ad845 100644 --- a/app/src/components/ServerSettings/ConnectionForm.tsx +++ b/app/src/components/ServerSettings/ConnectionForm.tsx @@ -1,7 +1,9 @@ import { zodResolver } from '@hookform/resolvers/zod'; +import { Loader2, XCircle } from 'lucide-react'; import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; +import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Checkbox } from '@/components/ui/checkbox'; @@ -15,8 +17,8 @@ import { FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; -import { Slider } from '@/components/ui/slider'; import { useToast } from '@/components/ui/use-toast'; +import { useServerHealth } from '@/lib/hooks/useServer'; import { usePlatform } from '@/platform/PlatformContext'; import { useServerStore } from '@/stores/serverStore'; @@ -34,11 +36,8 @@ export function ConnectionForm() { const setKeepServerRunningOnClose = useServerStore((state) => state.setKeepServerRunningOnClose); const mode = useServerStore((state) => state.mode); const setMode = useServerStore((state) => state.setMode); - const maxChunkChars = useServerStore((state) => state.maxChunkChars); - const setMaxChunkChars = useServerStore((state) => state.setMaxChunkChars); - const crossfadeMs = useServerStore((state) => state.crossfadeMs); - const setCrossfadeMs = useServerStore((state) => state.setCrossfadeMs); const { toast } = useToast(); + const { data: health, isLoading, error: healthError } = useServerHealth(); const form = useForm({ resolver: zodResolver(connectionSchema), @@ -56,7 +55,7 @@ export function ConnectionForm() { function onSubmit(data: ConnectionFormValues) { setServerUrl(data.serverUrl); - form.reset(data); // Reset form state after successful submission + form.reset(data); toast({ title: 'Server URL updated', description: `Connected to ${data.serverUrl}`, @@ -90,6 +89,37 @@ export function ConnectionForm() { + {/* Connection status */} +
+ {isLoading ? ( +
+ + Checking connection... +
+ ) : healthError ? ( +
+ + + Connection failed: {healthError.message} + +
+ ) : health ? ( +
+ + {health.model_loaded || health.model_downloaded ? 'Model Ready' : 'No Model'} + + + GPU: {health.gpu_available ? 'Available' : 'Not Available'} + + {health.vram_used_mb && ( + VRAM: {health.vram_used_mb.toFixed(0)} MB + )} +
+ ) : null} +
+
)} - -
-
-
- - - {maxChunkChars} chars - -
- setMaxChunkChars(value)} - min={100} - max={2000} - step={50} - aria-label="Auto-chunking character limit" - /> -

- Long text is split into chunks at sentence boundaries before generating. Lower values - can improve quality for long outputs. Default is 800. -

-
- -
-
- - - {crossfadeMs === 0 ? 'Cut' : `${crossfadeMs}ms`} - -
- setCrossfadeMs(value)} - min={0} - max={200} - step={10} - aria-label="Chunk crossfade duration" - /> -

- Blends audio between chunks to smooth transitions. Set to 0 for a hard cut. -

-
-
); diff --git a/app/src/components/ServerSettings/GenerationSettings.tsx b/app/src/components/ServerSettings/GenerationSettings.tsx new file mode 100644 index 00000000..b9b45de3 --- /dev/null +++ b/app/src/components/ServerSettings/GenerationSettings.tsx @@ -0,0 +1,71 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Slider } from '@/components/ui/slider'; +import { useServerStore } from '@/stores/serverStore'; + +export function GenerationSettings() { + const maxChunkChars = useServerStore((state) => state.maxChunkChars); + const setMaxChunkChars = useServerStore((state) => state.setMaxChunkChars); + const crossfadeMs = useServerStore((state) => state.crossfadeMs); + const setCrossfadeMs = useServerStore((state) => state.setCrossfadeMs); + + return ( + + + Generation Settings + + Controls for long text generation. These settings apply to all engines. + + + +
+
+
+ + + {maxChunkChars} chars + +
+ setMaxChunkChars(value)} + min={100} + max={2000} + step={50} + aria-label="Auto-chunking character limit" + /> +

+ Long text is split into chunks at sentence boundaries before generating. Lower values + can improve quality for long outputs. +

+
+ +
+
+ + + {crossfadeMs === 0 ? 'Cut' : `${crossfadeMs}ms`} + +
+ setCrossfadeMs(value)} + min={0} + max={200} + step={10} + aria-label="Chunk crossfade duration" + /> +

+ Blends audio between chunks to smooth transitions. Set to 0 for a hard cut. +

+
+
+
+
+ ); +} diff --git a/app/src/components/ServerSettings/GpuAcceleration.tsx b/app/src/components/ServerSettings/GpuAcceleration.tsx index 69824d63..94cc1d6d 100644 --- a/app/src/components/ServerSettings/GpuAcceleration.tsx +++ b/app/src/components/ServerSettings/GpuAcceleration.tsx @@ -1,7 +1,6 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { AlertCircle, Cpu, Download, Loader2, RotateCw, Trash2, Zap } from 'lucide-react'; +import { AlertCircle, Download, Loader2, RotateCw, Trash2 } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; @@ -216,31 +215,19 @@ export function GpuAcceleration() { return ( - - - GPU Acceleration - + GPU Acceleration {/* Current status */} -
-
-
Backend
-
- {isCurrentlyCuda ? 'CUDA (GPU accelerated)' : 'CPU'} -
+
+
Backend
+
+ {isCurrentlyCuda + ? 'CUDA (GPU accelerated)' + : hasNativeGpu + ? `${health.backend_type === 'mlx' ? 'MLX' : 'PyTorch'} (GPU accelerated)` + : 'CPU'}
- - {isCurrentlyCuda ? ( - <> - CUDA - - ) : ( - <> - CPU - - )} -
{/* GPU info from health */} @@ -257,14 +244,6 @@ export function GpuAcceleration() { )} {/* Native GPU detected - no CUDA download needed */} - {hasNativeGpu && ( -
-
- Your system uses {health.gpu_type} for acceleration. No additional - downloads needed. -
-
- )} {/* CUDA download section - only show when native GPU is NOT detected (i.e., Windows/Linux NVIDIA users) */} {!hasNativeGpu && ( diff --git a/app/src/components/ServerSettings/ModelManagement.tsx b/app/src/components/ServerSettings/ModelManagement.tsx index 9811f643..37e8b229 100644 --- a/app/src/components/ServerSettings/ModelManagement.tsx +++ b/app/src/components/ServerSettings/ModelManagement.tsx @@ -342,17 +342,18 @@ export function ModelManagement() { setDetailOpen(true); }; - const ttsModels = modelStatus?.models.filter((m) => m.model_name.startsWith('qwen-tts')) ?? []; - const otherTtsModels = + const voiceModels = modelStatus?.models.filter( - (m) => m.model_name.startsWith('luxtts') || m.model_name.startsWith('chatterbox'), + (m) => + m.model_name.startsWith('qwen-tts') || + m.model_name.startsWith('luxtts') || + m.model_name.startsWith('chatterbox'), ) ?? []; const whisperModels = modelStatus?.models.filter((m) => m.model_name.startsWith('whisper')) ?? []; // Build sections const sections: { label: string; models: ModelStatus[] }[] = [ - { label: 'Voice Generation', models: ttsModels }, - ...(otherTtsModels.length > 0 ? [{ label: 'Other Voice Models', models: otherTtsModels }] : []), + { label: 'Voice Generation', models: voiceModels }, { label: 'Transcription', models: whisperModels }, ]; @@ -564,12 +565,6 @@ export function ModelManagement() { Loaded )} - {freshSelectedModel.downloaded && !freshSelectedModel.loaded && ( - - - Downloaded - - )} {selectedState?.hasError && ( @@ -595,24 +590,6 @@ export function ModelManagement() { {hfModelInfo && (
- {/* Stats row */} -
- - - {formatDownloads(hfModelInfo.downloads)} - - - - {formatDownloads(hfModelInfo.likes)} - - {license && ( - - - {formatLicense(license)} - - )} -
- {/* Pipeline tag + author */}
{hfModelInfo.pipeline_tag && ( @@ -632,6 +609,24 @@ export function ModelManagement() { )}
+ {/* Stats row */} +
+ + + {formatDownloads(hfModelInfo.downloads)} + + + + {formatDownloads(hfModelInfo.likes)} + + {license && ( + + + {formatLicense(license)} + + )} +
+ {/* Languages */} {hfModelInfo.cardData?.language && hfModelInfo.cardData.language.length > 0 && (
@@ -647,8 +642,8 @@ export function ModelManagement() { {/* Disk size */} {freshSelectedModel.downloaded && freshSelectedModel.size_mb && ( -
- +
+ {formatSize(freshSelectedModel.size_mb)} on disk
)} @@ -661,7 +656,7 @@ export function ModelManagement() { )} {/* Actions */} -
+
{selectedState?.hasError ? ( <>
- {/* Model download progress */} -
- - - - - - -
- {isLoading ? (
diff --git a/app/src/components/ServerTab/ServerTab.tsx b/app/src/components/ServerTab/ServerTab.tsx index 1f32ac04..000ec5b7 100644 --- a/app/src/components/ServerTab/ServerTab.tsx +++ b/app/src/components/ServerTab/ServerTab.tsx @@ -1,19 +1,19 @@ import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm'; +import { GenerationSettings } from '@/components/ServerSettings/GenerationSettings'; import { GpuAcceleration } from '@/components/ServerSettings/GpuAcceleration'; -import { ServerStatus } from '@/components/ServerSettings/ServerStatus'; import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus'; import { usePlatform } from '@/platform/PlatformContext'; export function ServerTab() { const platform = usePlatform(); return ( -
+
- + + {platform.metadata.isTauri && } + {platform.metadata.isTauri && }
- {platform.metadata.isTauri && } - {platform.metadata.isTauri && }
Created by{' '}