mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 00:55:14 -07:00
refactor: restructure server settings and models UI
- Split chunking/crossfade sliders into dedicated GenerationSettings card - Merge connection status badges into ConnectionForm (remove ServerStatus card) - 2-column grid layout for the entire settings page - GPU Acceleration: remove icon, badge, and MLX info card - Models: merge 'Other Voice Models' into single 'Voice Generation' list - Model detail: remove 'Downloaded' badge, border above actions, swap badges above stats row, match disk size font to stats
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Loader2, XCircle } from 'lucide-react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
@@ -15,8 +17,8 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Slider } from '@/components/ui/slider';
|
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
|
import { useServerHealth } from '@/lib/hooks/useServer';
|
||||||
import { usePlatform } from '@/platform/PlatformContext';
|
import { usePlatform } from '@/platform/PlatformContext';
|
||||||
import { useServerStore } from '@/stores/serverStore';
|
import { useServerStore } from '@/stores/serverStore';
|
||||||
|
|
||||||
@@ -34,11 +36,8 @@ export function ConnectionForm() {
|
|||||||
const setKeepServerRunningOnClose = useServerStore((state) => state.setKeepServerRunningOnClose);
|
const setKeepServerRunningOnClose = useServerStore((state) => state.setKeepServerRunningOnClose);
|
||||||
const mode = useServerStore((state) => state.mode);
|
const mode = useServerStore((state) => state.mode);
|
||||||
const setMode = useServerStore((state) => state.setMode);
|
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 { toast } = useToast();
|
||||||
|
const { data: health, isLoading, error: healthError } = useServerHealth();
|
||||||
|
|
||||||
const form = useForm<ConnectionFormValues>({
|
const form = useForm<ConnectionFormValues>({
|
||||||
resolver: zodResolver(connectionSchema),
|
resolver: zodResolver(connectionSchema),
|
||||||
@@ -56,7 +55,7 @@ export function ConnectionForm() {
|
|||||||
|
|
||||||
function onSubmit(data: ConnectionFormValues) {
|
function onSubmit(data: ConnectionFormValues) {
|
||||||
setServerUrl(data.serverUrl);
|
setServerUrl(data.serverUrl);
|
||||||
form.reset(data); // Reset form state after successful submission
|
form.reset(data);
|
||||||
toast({
|
toast({
|
||||||
title: 'Server URL updated',
|
title: 'Server URL updated',
|
||||||
description: `Connected to ${data.serverUrl}`,
|
description: `Connected to ${data.serverUrl}`,
|
||||||
@@ -90,6 +89,37 @@ export function ConnectionForm() {
|
|||||||
</form>
|
</form>
|
||||||
</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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 pt-6 border-t">
|
<div className="mt-6 pt-6 border-t">
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -154,55 +184,6 @@ export function ConnectionForm() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-6 pt-6 border-t">
|
|
||||||
<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. Default is 800.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3 mt-4">
|
|
||||||
<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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
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 { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
@@ -216,32 +215,20 @@ export function GpuAcceleration() {
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle>GPU Acceleration</CardTitle>
|
||||||
<Zap className="h-4 w-4" />
|
|
||||||
GPU Acceleration
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* Current status */}
|
{/* Current status */}
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="text-sm font-medium">Backend</div>
|
<div className="text-sm font-medium">Backend</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{isCurrentlyCuda ? 'CUDA (GPU accelerated)' : 'CPU'}
|
{isCurrentlyCuda
|
||||||
|
? 'CUDA (GPU accelerated)'
|
||||||
|
: hasNativeGpu
|
||||||
|
? `${health.backend_type === 'mlx' ? 'MLX' : 'PyTorch'} (GPU accelerated)`
|
||||||
|
: 'CPU'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 */}
|
{/* GPU info from health */}
|
||||||
{health.gpu_type && (
|
{health.gpu_type && (
|
||||||
@@ -257,14 +244,6 @@ export function GpuAcceleration() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Native GPU detected - no CUDA download needed */}
|
{/* 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) */}
|
{/* CUDA download section - only show when native GPU is NOT detected (i.e., Windows/Linux NVIDIA users) */}
|
||||||
{!hasNativeGpu && (
|
{!hasNativeGpu && (
|
||||||
|
|||||||
@@ -342,17 +342,18 @@ export function ModelManagement() {
|
|||||||
setDetailOpen(true);
|
setDetailOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ttsModels = modelStatus?.models.filter((m) => m.model_name.startsWith('qwen-tts')) ?? [];
|
const voiceModels =
|
||||||
const otherTtsModels =
|
|
||||||
modelStatus?.models.filter(
|
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')) ?? [];
|
const whisperModels = modelStatus?.models.filter((m) => m.model_name.startsWith('whisper')) ?? [];
|
||||||
|
|
||||||
// Build sections
|
// Build sections
|
||||||
const sections: { label: string; models: ModelStatus[] }[] = [
|
const sections: { label: string; models: ModelStatus[] }[] = [
|
||||||
{ label: 'Voice Generation', models: ttsModels },
|
{ label: 'Voice Generation', models: voiceModels },
|
||||||
...(otherTtsModels.length > 0 ? [{ label: 'Other Voice Models', models: otherTtsModels }] : []),
|
|
||||||
{ label: 'Transcription', models: whisperModels },
|
{ label: 'Transcription', models: whisperModels },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -564,12 +565,6 @@ export function ModelManagement() {
|
|||||||
Loaded
|
Loaded
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{freshSelectedModel.downloaded && !freshSelectedModel.loaded && (
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
<CircleCheck className="h-3 w-3 mr-1" />
|
|
||||||
Downloaded
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{selectedState?.hasError && (
|
{selectedState?.hasError && (
|
||||||
<Badge variant="destructive" className="text-xs">
|
<Badge variant="destructive" className="text-xs">
|
||||||
<CircleX className="h-3 w-3 mr-1" />
|
<CircleX className="h-3 w-3 mr-1" />
|
||||||
@@ -595,24 +590,6 @@ export function ModelManagement() {
|
|||||||
|
|
||||||
{hfModelInfo && (
|
{hfModelInfo && (
|
||||||
<div className="space-y-3">
|
<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 */}
|
{/* Pipeline tag + author */}
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{hfModelInfo.pipeline_tag && (
|
{hfModelInfo.pipeline_tag && (
|
||||||
@@ -632,6 +609,24 @@ export function ModelManagement() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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)}
|
||||||
|
</span>
|
||||||
|
{license && (
|
||||||
|
<span className="flex items-center gap-1" title="License">
|
||||||
|
<Scale className="h-3.5 w-3.5" />
|
||||||
|
{formatLicense(license)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Languages */}
|
{/* Languages */}
|
||||||
{hfModelInfo.cardData?.language && hfModelInfo.cardData.language.length > 0 && (
|
{hfModelInfo.cardData?.language && hfModelInfo.cardData.language.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
@@ -647,8 +642,8 @@ export function ModelManagement() {
|
|||||||
|
|
||||||
{/* Disk size */}
|
{/* Disk size */}
|
||||||
{freshSelectedModel.downloaded && freshSelectedModel.size_mb && (
|
{freshSelectedModel.downloaded && freshSelectedModel.size_mb && (
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
<HardDrive className="h-4 w-4" />
|
<HardDrive className="h-3.5 w-3.5" />
|
||||||
<span>{formatSize(freshSelectedModel.size_mb)} on disk</span>
|
<span>{formatSize(freshSelectedModel.size_mb)} on disk</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -661,7 +656,7 @@ export function ModelManagement() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex items-center gap-2 pt-2 border-t">
|
<div className="flex items-center gap-2 pt-2">
|
||||||
{selectedState?.hasError ? (
|
{selectedState?.hasError ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -3,18 +3,13 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { useServerHealth } from '@/lib/hooks/useServer';
|
import { useServerHealth } from '@/lib/hooks/useServer';
|
||||||
import { useServerStore } from '@/stores/serverStore';
|
import { useServerStore } from '@/stores/serverStore';
|
||||||
import { ModelProgress } from './ModelProgress';
|
|
||||||
|
|
||||||
export function ServerStatus() {
|
export function ServerStatus() {
|
||||||
const { data: health, isLoading, error } = useServerHealth();
|
const { data: health, isLoading, error } = useServerHealth();
|
||||||
const serverUrl = useServerStore((state) => state.serverUrl);
|
const serverUrl = useServerStore((state) => state.serverUrl);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card role="region" aria-label="Server Status" tabIndex={0}>
|
||||||
role="region"
|
|
||||||
aria-label="Server Status"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Server Status</CardTitle>
|
<CardTitle>Server Status</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -24,16 +19,6 @@ export function ServerStatus() {
|
|||||||
<div className="font-mono text-sm">{serverUrl}</div>
|
<div className="font-mono text-sm">{serverUrl}</div>
|
||||||
</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 ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm';
|
import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm';
|
||||||
|
import { GenerationSettings } from '@/components/ServerSettings/GenerationSettings';
|
||||||
import { GpuAcceleration } from '@/components/ServerSettings/GpuAcceleration';
|
import { GpuAcceleration } from '@/components/ServerSettings/GpuAcceleration';
|
||||||
import { ServerStatus } from '@/components/ServerSettings/ServerStatus';
|
|
||||||
import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus';
|
import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus';
|
||||||
import { usePlatform } from '@/platform/PlatformContext';
|
import { usePlatform } from '@/platform/PlatformContext';
|
||||||
|
|
||||||
export function ServerTab() {
|
export function ServerTab() {
|
||||||
const platform = usePlatform();
|
const platform = usePlatform();
|
||||||
return (
|
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">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<ConnectionForm />
|
<ConnectionForm />
|
||||||
<ServerStatus />
|
<GenerationSettings />
|
||||||
</div>
|
|
||||||
{platform.metadata.isTauri && <GpuAcceleration />}
|
{platform.metadata.isTauri && <GpuAcceleration />}
|
||||||
{platform.metadata.isTauri && <UpdateStatus />}
|
{platform.metadata.isTauri && <UpdateStatus />}
|
||||||
|
</div>
|
||||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||||
Created by{' '}
|
Created by{' '}
|
||||||
<a
|
<a
|
||||||
|
|||||||
Reference in New Issue
Block a user