Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions app/src/components/ServerSettings/ConnectionForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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';
import {
Form,
FormControl,
Expand All @@ -14,10 +17,10 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Checkbox } from '@/components/ui/checkbox';
import { useToast } from '@/components/ui/use-toast';
import { useServerStore } from '@/stores/serverStore';
import { useServerHealth } from '@/lib/hooks/useServer';
import { usePlatform } from '@/platform/PlatformContext';
import { useServerStore } from '@/stores/serverStore';

const connectionSchema = z.object({
serverUrl: z.string().url('Please enter a valid URL'),
Expand All @@ -34,6 +37,7 @@ export function ConnectionForm() {
const mode = useServerStore((state) => state.mode);
const setMode = useServerStore((state) => state.setMode);
const { toast } = useToast();
const { data: health, isLoading, error: healthError } = useServerHealth();

const form = useForm<ConnectionFormValues>({
resolver: zodResolver(connectionSchema),
Expand All @@ -51,19 +55,15 @@ 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}`,
});
}

return (
<Card
role="region"
aria-label="Server Connection"
tabIndex={0}
>
<Card role="region" aria-label="Server Connection" tabIndex={0}>
<CardHeader>
<CardTitle>Server Connection</CardTitle>
</CardHeader>
Expand All @@ -89,6 +89,37 @@ export function ConnectionForm() {
</form>
</Form>

{/* Connection status */}
<div className="mt-4">
{isLoading ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm text-muted-foreground">Checking connection...</span>
</div>
) : healthError ? (
<div className="flex items-center gap-2">
<XCircle className="h-4 w-4 text-destructive" />
<span className="text-sm text-destructive">
Connection failed: {healthError.message}
</span>
</div>
) : health ? (
<div className="flex flex-wrap gap-2">
<Badge
variant={health.model_loaded || health.model_downloaded ? 'default' : 'secondary'}
>
{health.model_loaded || health.model_downloaded ? 'Model Ready' : 'No Model'}
</Badge>
<Badge variant={health.gpu_available ? 'default' : 'secondary'}>
GPU: {health.gpu_available ? 'Available' : 'Not Available'}
</Badge>
{health.vram_used_mb && (
<Badge variant="outline">VRAM: {health.vram_used_mb.toFixed(0)} MB</Badge>
)}
Comment on lines +116 to +118
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Render VRAM badge when usage is 0 MB.

Line 116 uses a truthy check, so 0 won’t render even though it’s valid data.

Proposed fix
-              {health.vram_used_mb && (
+              {health.vram_used_mb != null && (
                 <Badge variant="outline">VRAM: {health.vram_used_mb.toFixed(0)} MB</Badge>
               )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{health.vram_used_mb && (
<Badge variant="outline">VRAM: {health.vram_used_mb.toFixed(0)} MB</Badge>
)}
{health.vram_used_mb != null && (
<Badge variant="outline">VRAM: {health.vram_used_mb.toFixed(0)} MB</Badge>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/ServerSettings/ConnectionForm.tsx` around lines 116 - 118,
The VRAM badge is currently gated by a truthy check so a valid value of 0 won’t
render; update the condition in ConnectionForm (the check around
health.vram_used_mb before rendering <Badge>) to explicitly allow zero by
checking for null/undefined (e.g., health.vram_used_mb !== null &&
health.vram_used_mb !== undefined or Number.isFinite(health.vram_used_mb)) so 0
MB displays while still avoiding rendering when the value is absent.

</div>
) : null}
</div>

<div className="mt-6 pt-6 border-t">
<div className="flex items-start space-x-3">
<Checkbox
Expand Down
71 changes: 71 additions & 0 deletions app/src/components/ServerSettings/GenerationSettings.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card role="region" aria-label="Generation Settings" tabIndex={0}>
<CardHeader>
<CardTitle>Generation Settings</CardTitle>
<CardDescription>
Controls for long text generation. These settings apply to all engines.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="space-y-3">
<div className="flex items-center justify-between">
<label htmlFor="maxChunkChars" className="text-sm font-medium leading-none">
Auto-chunking limit
</label>
<span className="text-sm tabular-nums text-muted-foreground">
{maxChunkChars} chars
</span>
</div>
<Slider
id="maxChunkChars"
value={[maxChunkChars]}
onValueChange={([value]) => setMaxChunkChars(value)}
min={100}
max={2000}
step={50}
aria-label="Auto-chunking character limit"
/>
<p className="text-sm text-muted-foreground">
Long text is split into chunks at sentence boundaries before generating. Lower values
can improve quality for long outputs.
</p>
</div>

<div className="space-y-3">
<div className="flex items-center justify-between">
<label htmlFor="crossfadeMs" className="text-sm font-medium leading-none">
Chunk crossfade
</label>
<span className="text-sm tabular-nums text-muted-foreground">
{crossfadeMs === 0 ? 'Cut' : `${crossfadeMs}ms`}
</span>
</div>
<Slider
id="crossfadeMs"
value={[crossfadeMs]}
onValueChange={([value]) => setCrossfadeMs(value)}
min={0}
max={200}
step={10}
aria-label="Chunk crossfade duration"
/>
<p className="text-sm text-muted-foreground">
Blends audio between chunks to smooth transitions. Set to 0 for a hard cut.
</p>
</div>
</div>
</CardContent>
</Card>
);
}
41 changes: 10 additions & 31 deletions app/src/components/ServerSettings/GpuAcceleration.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -216,31 +215,19 @@ export function GpuAcceleration() {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="h-4 w-4" />
GPU Acceleration
</CardTitle>
<CardTitle>GPU Acceleration</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Current status */}
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="text-sm font-medium">Backend</div>
<div className="text-sm text-muted-foreground">
{isCurrentlyCuda ? 'CUDA (GPU accelerated)' : 'CPU'}
</div>
<div className="space-y-1">
<div className="text-sm font-medium">Backend</div>
<div className="text-sm text-muted-foreground">
{isCurrentlyCuda
? 'CUDA (GPU accelerated)'
: hasNativeGpu
? `${health.backend_type === 'mlx' ? 'MLX' : 'PyTorch'} (GPU accelerated)`
: 'CPU'}
</div>
<Badge variant={isCurrentlyCuda ? 'default' : 'secondary'}>
{isCurrentlyCuda ? (
<>
<Zap className="h-3 w-3 mr-1" /> CUDA
</>
) : (
<>
<Cpu className="h-3 w-3 mr-1" /> CPU
</>
)}
</Badge>
</div>

{/* GPU info from health */}
Expand All @@ -257,14 +244,6 @@ export function GpuAcceleration() {
)}

{/* Native GPU detected - no CUDA download needed */}
{hasNativeGpu && (
<div className="p-3 rounded-lg bg-accent/10 border border-accent/20">
<div className="text-sm">
Your system uses <strong>{health.gpu_type}</strong> for acceleration. No additional
downloads needed.
</div>
</div>
)}

{/* CUDA download section - only show when native GPU is NOT detected (i.e., Windows/Linux NVIDIA users) */}
{!hasNativeGpu && (
Expand Down
59 changes: 27 additions & 32 deletions app/src/components/ServerSettings/ModelManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
];

Expand Down Expand Up @@ -564,12 +565,6 @@ export function ModelManagement() {
Loaded
</Badge>
)}
{freshSelectedModel.downloaded && !freshSelectedModel.loaded && (
<Badge variant="secondary" className="text-xs">
<CircleCheck className="h-3 w-3 mr-1" />
Downloaded
</Badge>
)}
{selectedState?.hasError && (
<Badge variant="destructive" className="text-xs">
<CircleX className="h-3 w-3 mr-1" />
Expand All @@ -595,24 +590,6 @@ export function ModelManagement() {

{hfModelInfo && (
<div className="space-y-3">
{/* Stats row */}
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1" title="Downloads">
<Download className="h-3.5 w-3.5" />
{formatDownloads(hfModelInfo.downloads)}
</span>
<span className="flex items-center gap-1" title="Likes">
<Heart className="h-3.5 w-3.5" />
{formatDownloads(hfModelInfo.likes)}
</span>
{license && (
<span className="flex items-center gap-1" title="License">
<Scale className="h-3.5 w-3.5" />
{formatLicense(license)}
</span>
)}
</div>

{/* Pipeline tag + author */}
<div className="flex flex-wrap gap-1.5">
{hfModelInfo.pipeline_tag && (
Expand All @@ -632,6 +609,24 @@ export function ModelManagement() {
)}
</div>

{/* Stats row */}
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1" title="Downloads">
<Download className="h-3.5 w-3.5" />
{formatDownloads(hfModelInfo.downloads)}
</span>
<span className="flex items-center gap-1" title="Likes">
<Heart className="h-3.5 w-3.5" />
{formatDownloads(hfModelInfo.likes)}
Comment on lines +615 to +620
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify local typing and current call-site assumptions for HuggingFace stats fields.

# 1) Check HuggingFaceModelInfo field nullability in frontend types
fd "types.ts" app/src -t f -x rg -n "interface HuggingFaceModelInfo|downloads|likes" {}

# 2) Inspect all formatDownloads call sites in this file
rg -n "formatDownloads\\(" app/src/components/ServerSettings/ModelManagement.tsx -C 2

Repository: jamiepine/voicebox

Length of output: 914


🏁 Script executed:

# Get more context around the problematic lines
sed -n '590,650p' app/src/components/ServerSettings/ModelManagement.tsx | cat -n

Repository: jamiepine/voicebox

Length of output: 3353


🏁 Script executed:

# Check where hfModelInfo is defined/assigned
rg -n "hfModelInfo\s*=" app/src/components/ServerSettings/ModelManagement.tsx -B 2 -A 2

Repository: jamiepine/voicebox

Length of output: 44


🏁 Script executed:

# Look for where this JSX is rendered and how hfModelInfo is obtained
rg -n "hfModelInfo" app/src/components/ServerSettings/ModelManagement.tsx | head -20

Repository: jamiepine/voicebox

Length of output: 1215


🏁 Script executed:

# Get the complete HuggingFaceModelInfo interface definition
sed -n '131,150p' app/src/components/ServerSettings/ModelManagement.tsx | cat -n

Repository: jamiepine/voicebox

Length of output: 824


🏁 Script executed:

# Also check the formatDownloads function to see if it guards against undefined
sed -n '51,60p' app/src/components/ServerSettings/ModelManagement.tsx | cat -n

Repository: jamiepine/voicebox

Length of output: 459


🏁 Script executed:

# Get HuggingFaceModelInfo interface from the file
rg -n "export interface HuggingFaceModelInfo" app/src/components/ServerSettings/ModelManagement.tsx -A 20

Repository: jamiepine/voicebox

Length of output: 44


🏁 Script executed:

# Find types.ts files in the project
fd "types.ts" app/src -type f

Repository: jamiepine/voicebox

Length of output: 232


🏁 Script executed:

# Check if HuggingFaceModelInfo is imported or defined elsewhere
rg -n "interface HuggingFaceModelInfo" app/src --type ts

Repository: jamiepine/voicebox

Length of output: 130


🏁 Script executed:

# Get the full HuggingFaceModelInfo interface definition
sed -n '131,165p' app/src/lib/api/types.ts | cat -n

Repository: jamiepine/voicebox

Length of output: 1030


Guard HF stats formatting against missing numeric fields.

At line 616 and 620, unguarded hfModelInfo.downloads and hfModelInfo.likes lack defensive checks despite receiving external API data that may not include these fields. While the type definition marks them as required, the codebase defensively guards other optional API fields. Add null-safe defaults to prevent crashes if the HuggingFace API response omits these fields.

🛡️ Proposed fix
-                        {formatDownloads(hfModelInfo.downloads)}
+                        {formatDownloads(hfModelInfo.downloads ?? 0)}
...
-                        {formatDownloads(hfModelInfo.likes)}
+                        {formatDownloads(hfModelInfo.likes ?? 0)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Download className="h-3.5 w-3.5" />
{formatDownloads(hfModelInfo.downloads)}
</span>
<span className="flex items-center gap-1" title="Likes">
<Heart className="h-3.5 w-3.5" />
{formatDownloads(hfModelInfo.likes)}
<Download className="h-3.5 w-3.5" />
{formatDownloads(hfModelInfo.downloads ?? 0)}
</span>
<span className="flex items-center gap-1" title="Likes">
<Heart className="h-3.5 w-3.5" />
{formatDownloads(hfModelInfo.likes ?? 0)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/ServerSettings/ModelManagement.tsx` around lines 615 -
620, hfModelInfo.downloads and hfModelInfo.likes are used without null-safety
which can crash if the HF API omits them; in the render where Download and Heart
icons are shown, pass guarded values to formatDownloads (e.g., use optional
chaining or nullish coalescing) so formatDownloads(hfModelInfo.downloads ?? 0)
and formatDownloads(hfModelInfo.likes ?? 0) (or equivalent) are used; update the
ModelManagement component render around the Download/Heart spans to default
missing numeric fields to 0 before calling formatDownloads.

</span>
{license && (
<span className="flex items-center gap-1" title="License">
<Scale className="h-3.5 w-3.5" />
{formatLicense(license)}
</span>
)}
</div>

{/* Languages */}
{hfModelInfo.cardData?.language && hfModelInfo.cardData.language.length > 0 && (
<div>
Expand All @@ -647,8 +642,8 @@ export function ModelManagement() {

{/* Disk size */}
{freshSelectedModel.downloaded && freshSelectedModel.size_mb && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<HardDrive className="h-4 w-4" />
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<HardDrive className="h-3.5 w-3.5" />
<span>{formatSize(freshSelectedModel.size_mb)} on disk</span>
</div>
)}
Expand All @@ -661,7 +656,7 @@ export function ModelManagement() {
)}

{/* Actions */}
<div className="flex items-center gap-2 pt-2 border-t">
<div className="flex items-center gap-2 pt-2">
{selectedState?.hasError ? (
<>
<Button
Expand Down
17 changes: 1 addition & 16 deletions app/src/components/ServerSettings/ServerStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,13 @@ import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useServerHealth } from '@/lib/hooks/useServer';
import { useServerStore } from '@/stores/serverStore';
import { ModelProgress } from './ModelProgress';

export function ServerStatus() {
const { data: health, isLoading, error } = useServerHealth();
const serverUrl = useServerStore((state) => state.serverUrl);

return (
<Card
role="region"
aria-label="Server Status"
tabIndex={0}
>
<Card role="region" aria-label="Server Status" tabIndex={0}>
<CardHeader>
<CardTitle>Server Status</CardTitle>
</CardHeader>
Expand All @@ -24,16 +19,6 @@ export function ServerStatus() {
<div className="font-mono text-sm">{serverUrl}</div>
</div>

{/* Model download progress */}
<div className="space-y-2">
<ModelProgress modelName="qwen-tts-1.7B" displayName="Qwen TTS 1.7B" />
<ModelProgress modelName="qwen-tts-0.6B" displayName="Qwen TTS 0.6B" />
<ModelProgress modelName="whisper-base" displayName="Whisper Base" />
<ModelProgress modelName="whisper-small" displayName="Whisper Small" />
<ModelProgress modelName="whisper-medium" displayName="Whisper Medium" />
<ModelProgress modelName="whisper-large" displayName="Whisper Large" />
</div>

{isLoading ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Expand Down
10 changes: 5 additions & 5 deletions app/src/components/ServerTab/ServerTab.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="space-y-4 overflow-y-auto flex flex-col">
<div className="overflow-y-auto flex flex-col">
<div className="grid gap-4 md:grid-cols-2">
<ConnectionForm />
<ServerStatus />
<GenerationSettings />
{platform.metadata.isTauri && <GpuAcceleration />}
{platform.metadata.isTauri && <UpdateStatus />}
</div>
{platform.metadata.isTauri && <GpuAcceleration />}
{platform.metadata.isTauri && <UpdateStatus />}
<div className="py-8 text-center text-sm text-muted-foreground">
Created by{' '}
<a
Expand Down
Loading