Add Docker support and update dependencies

- Introduced Docker support with CPU-only and GPU-enabled configurations via Dockerfiles and docker-compose files.
- Added a .dockerignore file to exclude unnecessary files from Docker images.
- Updated bun.lock and package.json to include new dependencies for icon handling.
- Enhanced README with Docker usage instructions and deployment options.
- Refactored components to utilize new icon libraries for improved UI consistency.
This commit is contained in:
Jamie Pine
2026-02-02 02:19:05 -08:00
parent 4c4b3e5463
commit f090759d8f
61 changed files with 5982 additions and 394 deletions
@@ -460,7 +460,7 @@ export function AudioPlayer() {
// Use double requestAnimationFrame to ensure DOM is fully rendered
let rafId1: number;
let rafId2: number;
let timeoutId: number | null = null;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
rafId1 = requestAnimationFrame(() => {
rafId2 = requestAnimationFrame(() => {
@@ -1,7 +1,8 @@
import { SparklesIcon, TextSquareIcon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Icon } from '@iconify/react';
import { useMatchRoute } from '@tanstack/react-router';
import { AnimatePresence, motion } from 'framer-motion';
import { HugeiconsIcon } from '@hugeicons/react';
import { Loading01Icon, TextSquareIcon, SparklesIcon } from '@hugeicons/core-free-icons';
import { useEffect, useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
@@ -302,7 +303,7 @@ export function FloatingGenerateBox({
size="icon"
>
{isPending ? (
<HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={SparklesIcon} size={16} className="h-4 w-4" />
)}
@@ -1,5 +1,6 @@
import { Mic01Icon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Loading01Icon, Mic01Icon } from '@hugeicons/core-free-icons';
import { Icon } from '@iconify/react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
@@ -47,7 +48,11 @@ export function GenerationForm() {
<FormLabel>Voice Profile</FormLabel>
{selectedProfile ? (
<div className="mt-2 p-3 border rounded-md bg-muted/50 flex items-center gap-2">
<HugeiconsIcon icon={Mic01Icon} size={16} className="h-4 w-4 text-muted-foreground" />
<HugeiconsIcon
icon={Mic01Icon}
size={16}
className="h-4 w-4 text-muted-foreground"
/>
<span className="font-medium">{selectedProfile.name}</span>
<span className="text-sm text-muted-foreground">{selectedProfile.language}</span>
</div>
@@ -171,14 +176,10 @@ export function GenerationForm() {
/>
</div>
<Button
type="submit"
className="w-full"
disabled={isPending || !selectedProfileId}
>
<Button type="submit" className="w-full" disabled={isPending || !selectedProfileId}>
{isPending ? (
<>
<HugeiconsIcon icon={Loading01Icon} size={16} className="mr-2 h-4 w-4 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="mr-2 h-4 w-4 animate-spin" />
Generating...
</>
) : (
+25 -10
View File
@@ -1,13 +1,13 @@
import { HugeiconsIcon } from '@hugeicons/react';
import {
WaveIcon,
Download01Icon,
Archive01Icon,
Loading01Icon,
Delete01Icon,
Download01Icon,
MoreHorizontalIcon,
PlayIcon,
Delete01Icon,
WaveIcon,
} from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Icon } from '@iconify/react';
import { useEffect, useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import {
@@ -55,7 +55,9 @@ export function HistoryTable() {
const [importDialogOpen, setImportDialogOpen] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [generationToDelete, setGenerationToDelete] = useState<{ id: string; name: string } | null>(null);
const [generationToDelete, setGenerationToDelete] = useState<{ id: string; name: string } | null>(
null,
);
const limit = 20;
const { toast } = useToast();
@@ -223,7 +225,10 @@ export function HistoryTable() {
if (isLoading && page === 0) {
return (
<div className="flex items-center justify-center h-full">
<HugeiconsIcon icon={Loading01Icon} size={32} className="h-8 w-8 animate-spin text-muted-foreground" />
<Icon
icon="svg-spinners:ring-resize"
className="h-8 w-8 animate-spin text-muted-foreground"
/>
</div>
);
}
@@ -269,7 +274,11 @@ export function HistoryTable() {
>
{/* Waveform icon */}
<div className="flex items-center shrink-0">
<HugeiconsIcon icon={WaveIcon} size={20} className="h-5 w-5 text-muted-foreground" />
<HugeiconsIcon
icon={WaveIcon}
size={20}
className="h-5 w-5 text-muted-foreground"
/>
</div>
{/* Left side - Meta information */}
@@ -353,7 +362,12 @@ export function HistoryTable() {
{/* Load more trigger element */}
{hasMore && (
<div ref={loadMoreRef} className="flex items-center justify-center py-4">
{isFetching && <HugeiconsIcon icon={Loading01Icon} size={24} className="h-6 w-6 animate-spin text-muted-foreground" />}
{isFetching && (
<Icon
icon="svg-spinners:ring-resize"
className="h-6 w-6 animate-spin text-muted-foreground"
/>
)}
</div>
)}
@@ -372,7 +386,8 @@ export function HistoryTable() {
<DialogHeader>
<DialogTitle>Delete Generation</DialogTitle>
<DialogDescription>
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.
</DialogDescription>
</DialogHeader>
<DialogFooter>
@@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Delete01Icon, Download01Icon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Download01Icon, Loading01Icon, Delete01Icon } from '@hugeicons/core-free-icons';
import { Icon } from '@iconify/react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useState } from 'react';
import {
AlertDialog,
@@ -154,7 +155,10 @@ export function ModelManagement() {
<CardContent className="space-y-4">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<HugeiconsIcon icon={Loading01Icon} size={24} className="h-6 w-6 animate-spin text-muted-foreground" />
<Icon
icon="svg-spinners:ring-resize"
className="h-6 w-6 animate-spin text-muted-foreground"
/>
</div>
) : modelStatus ? (
<div className="space-y-4">
@@ -246,7 +250,7 @@ export function ModelManagement() {
>
{deleteMutation.isPending ? (
<>
<HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 mr-2 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 mr-2 animate-spin" />
Deleting...
</>
) : (
@@ -320,7 +324,7 @@ function ModelItem({ model, onDownload, onDelete, isDownloading, formatSize }: M
</div>
) : showDownloading ? (
<Button size="sm" variant="outline" disabled>
<HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 mr-2 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 mr-2 animate-spin" />
Downloading...
</Button>
) : (
@@ -1,5 +1,6 @@
import { CancelCircleIcon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Loading01Icon, CancelCircleIcon } from '@hugeicons/core-free-icons';
import { Icon } from '@iconify/react';
import { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
@@ -13,7 +14,11 @@ interface ModelProgressProps {
isDownloading?: boolean;
}
export function ModelProgress({ modelName, displayName, isDownloading = false }: ModelProgressProps) {
export function ModelProgress({
modelName,
displayName,
isDownloading = false,
}: ModelProgressProps) {
const [progress, setProgress] = useState<ModelProgressType | null>(null);
const serverUrl = useServerStore((state) => state.serverUrl);
@@ -75,10 +80,12 @@ export function ModelProgress({ modelName, displayName, isDownloading = false }:
const getStatusIcon = () => {
switch (progress.status) {
case 'error':
return <HugeiconsIcon icon={CancelCircleIcon} size={16} className="h-4 w-4 text-destructive" />;
return (
<HugeiconsIcon icon={CancelCircleIcon} size={16} className="h-4 w-4 text-destructive" />
);
case 'downloading':
case 'extracting':
return <HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" />;
return <Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />;
default:
return null;
}
@@ -1,5 +1,6 @@
import { Delete01Icon, Download01Icon, Loading01Icon } from '@hugeicons/core-free-icons';
import { Delete01Icon, Download01Icon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Icon } from '@iconify/react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useState } from 'react';
import {
@@ -162,7 +163,7 @@ export function ProviderSettings() {
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-8">
<HugeiconsIcon icon={Loading01Icon} size={24} className="h-6 w-6 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-6 w-6 animate-spin" />
</div>
</CardContent>
</Card>
@@ -210,11 +211,7 @@ export function ProviderSettings() {
disabled={downloadingProvider === 'pytorch-cuda'}
>
{downloadingProvider === 'pytorch-cuda' ? (
<HugeiconsIcon
icon={Loading01Icon}
size={16}
className="h-4 w-4 animate-spin"
/>
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
) : (
<>
<HugeiconsIcon icon={Download01Icon} size={16} className="h-4 w-4 mr-1" />
@@ -257,11 +254,7 @@ export function ProviderSettings() {
disabled={downloadingProvider === 'pytorch-cpu'}
>
{downloadingProvider === 'pytorch-cpu' ? (
<HugeiconsIcon
icon={Loading01Icon}
size={16}
className="h-4 w-4 animate-spin"
/>
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
) : (
<>
<HugeiconsIcon icon={Download01Icon} size={16} className="h-4 w-4 mr-1" />
@@ -1,5 +1,6 @@
import { CancelCircleIcon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { Loading01Icon, CancelCircleIcon } from '@hugeicons/core-free-icons';
import { Icon } from '@iconify/react';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useServerHealth } from '@/lib/hooks/useServer';
@@ -33,7 +34,7 @@ export function ServerStatus() {
{isLoading ? (
<div className="flex items-center gap-2">
<HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
<span className="text-sm">Checking connection...</span>
</div>
) : error ? (
+13 -7
View File
@@ -1,6 +1,14 @@
import { Link, useMatchRoute } from '@tanstack/react-router';
import {
Book01Icon,
Mic01Icon,
PackageIcon,
ServerStack01Icon,
SpeakerIcon,
VolumeHighIcon,
} from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { PackageIcon, Book01Icon, Loading01Icon, Mic01Icon, McpServerIcon, SpeakerIcon, VolumeHighIcon } from '@hugeicons/core-free-icons';
import { Icon } from '@iconify/react';
import { Link, useMatchRoute } from '@tanstack/react-router';
import voiceboxLogo from '@/assets/voicebox-logo.png';
import { cn } from '@/lib/utils/cn';
import { useGenerationStore } from '@/stores/generationStore';
@@ -16,7 +24,7 @@ const tabs = [
{ id: 'voices', path: '/voices', icon: Mic01Icon, label: 'Voices' },
{ id: 'audio', path: '/audio', icon: SpeakerIcon, label: 'Audio' },
{ id: 'models', path: '/models', icon: PackageIcon, label: 'Models' },
{ id: 'server', path: '/server', icon: McpServerIcon, label: 'Server' },
{ id: 'server', path: '/server', icon: ServerStack01Icon, label: 'Server' },
];
export function Sidebar({ isMacOS }: SidebarProps) {
@@ -43,9 +51,7 @@ export function Sidebar({ isMacOS }: SidebarProps) {
const Icon = tab.icon;
// For index route, use exact match; for others, use default matching
const isActive =
tab.path === '/'
? matchRoute({ to: '/' })
: matchRoute({ to: tab.path });
tab.path === '/' ? matchRoute({ to: '/' }) : matchRoute({ to: tab.path });
return (
<Link
@@ -76,7 +82,7 @@ export function Sidebar({ isMacOS }: SidebarProps) {
isPlayerVisible ? 'mb-[120px]' : 'mb-0',
)}
>
<HugeiconsIcon icon={Loading01Icon} size={24} className="h-6 w-6 text-accent animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-6 w-6 text-accent animate-spin" />
</div>
)}
</div>
@@ -1,8 +1,7 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { HugeiconsIcon } from '@hugeicons/react';
import { DragDropVerticalIcon, Mic01Icon, MoreHorizontalIcon, PlayIcon, Delete01Icon } from '@hugeicons/core-free-icons';
import { useState } from 'react';
import { DragDropVerticalIcon, MoreHorizontalIcon, PlayIcon, Delete01Icon } from '@hugeicons/core-free-icons';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@@ -11,10 +10,10 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Textarea } from '@/components/ui/textarea';
import { ProfileAvatar } from '@/components/VoiceProfiles/ProfileAvatar';
import type { StoryItemDetail } from '@/lib/api/types';
import { cn } from '@/lib/utils/cn';
import { useStoryStore } from '@/stores/storyStore';
import { useServerStore } from '@/stores/serverStore';
interface StoryChatItemProps {
item: StoryItemDetail;
@@ -36,10 +35,6 @@ export function StoryChatItem({
isDragging,
}: StoryChatItemProps) {
const seek = useStoryStore((state) => state.seek);
const serverUrl = useServerStore((state) => state.serverUrl);
const [avatarError, setAvatarError] = useState(false);
const avatarUrl = `${serverUrl}/profiles/${item.profile_id}/avatar`;
// Check if this item is currently playing based on timecode
const itemStartMs = item.start_time_ms;
@@ -81,21 +76,12 @@ export function StoryChatItem({
{/* Voice Avatar */}
<div className="shrink-0">
<div className="h-10 w-10 rounded-full bg-muted flex items-center justify-center overflow-hidden">
{!avatarError ? (
<img
src={avatarUrl}
alt={`${item.profile_name} avatar`}
className={cn(
'h-full w-full object-cover transition-all duration-200',
!isCurrentlyPlaying && 'grayscale'
)}
onError={() => setAvatarError(true)}
/>
) : (
<HugeiconsIcon icon={Mic01Icon} size={20} className="h-5 w-5 text-muted-foreground" />
)}
</div>
<ProfileAvatar
profileId={item.profile_id}
size="lg"
grayscale={!isCurrentlyPlaying}
alt={`${item.profile_name} avatar`}
/>
</div>
{/* Content */}
@@ -0,0 +1,80 @@
import { HugeiconsIcon } from '@hugeicons/react';
import { Mic01Icon } from '@hugeicons/core-free-icons';
import { useState } from 'react';
import { cn } from '@/lib/utils/cn';
import { useServerStore } from '@/stores/serverStore';
interface ProfileAvatarProps {
profileId: string;
avatarPath?: string | null;
size?: 'sm' | 'md' | 'lg' | 'xl';
grayscale?: boolean;
className?: string;
alt?: string;
}
const sizeClasses = {
sm: 'h-6 w-6',
md: 'h-8 w-8',
lg: 'h-10 w-10',
xl: 'h-24 w-24',
};
const iconSizes = {
sm: 14,
md: 16,
lg: 20,
xl: 40,
};
const iconClassNames = {
sm: 'h-3.5 w-3.5',
md: 'h-4 w-4',
lg: 'h-5 w-5',
xl: 'h-10 w-10',
};
export function ProfileAvatar({
profileId,
avatarPath,
size = 'md',
grayscale = false,
className,
alt = 'Profile avatar',
}: ProfileAvatarProps) {
const [avatarError, setAvatarError] = useState(false);
const serverUrl = useServerStore((state) => state.serverUrl);
// If avatarPath is explicitly null or empty string, don't try to load avatar
// Otherwise, always try to load (avatarPath might not be available in all contexts)
const avatarUrl =
avatarPath === null || avatarPath === '' ? null : `${serverUrl}/profiles/${profileId}/avatar`;
return (
<div
className={cn(
sizeClasses[size],
'rounded-full bg-muted flex items-center justify-center shrink-0 overflow-hidden',
className,
)}
>
{avatarUrl && !avatarError ? (
<img
src={avatarUrl}
alt={alt}
className={cn(
'h-full w-full object-cover transition-all duration-200',
grayscale && 'grayscale',
)}
onError={() => setAvatarError(true)}
/>
) : (
<HugeiconsIcon
icon={Mic01Icon}
size={iconSizes[size]}
className={cn(iconClassNames[size], 'text-muted-foreground')}
/>
)}
</div>
);
}
@@ -1,5 +1,5 @@
import { HugeiconsIcon } from '@hugeicons/react';
import { Download01Icon, Edit01Icon, Mic01Icon, Delete01Icon } from '@hugeicons/core-free-icons';
import { Download01Icon, Edit01Icon, Delete01Icon } from '@hugeicons/core-free-icons';
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -13,10 +13,10 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { ProfileAvatar } from '@/components/VoiceProfiles/ProfileAvatar';
import type { VoiceProfileResponse } from '@/lib/api/types';
import { useDeleteProfile, useExportProfile } from '@/lib/hooks/useProfiles';
import { cn } from '@/lib/utils/cn';
import { useServerStore } from '@/stores/serverStore';
import { useUIStore } from '@/stores/uiStore';
interface ProfileCardProps {
@@ -25,19 +25,15 @@ interface ProfileCardProps {
export function ProfileCard({ profile }: ProfileCardProps) {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [avatarError, setAvatarError] = useState(false);
const deleteProfile = useDeleteProfile();
const exportProfile = useExportProfile();
const setEditingProfileId = useUIStore((state) => state.setEditingProfileId);
const setProfileDialogOpen = useUIStore((state) => state.setProfileDialogOpen);
const selectedProfileId = useUIStore((state) => state.selectedProfileId);
const setSelectedProfileId = useUIStore((state) => state.setSelectedProfileId);
const serverUrl = useServerStore((state) => state.serverUrl);
const isSelected = selectedProfileId === profile.id;
const avatarUrl = profile.avatar_path ? `${serverUrl}/profiles/${profile.id}/avatar` : null;
const handleSelect = () => {
setSelectedProfileId(isSelected ? null : profile.id);
};
@@ -73,21 +69,13 @@ export function ProfileCard({ profile }: ProfileCardProps) {
>
<CardHeader className="p-3 pb-2">
<CardTitle className="flex items-center gap-1.5 text-base font-medium">
<div className="h-6 w-6 rounded-full bg-muted flex items-center justify-center shrink-0 overflow-hidden">
{avatarUrl && !avatarError ? (
<img
src={avatarUrl}
alt={`${profile.name} avatar`}
className={cn(
'h-full w-full object-cover transition-all duration-200',
!isSelected && 'grayscale',
)}
onError={() => setAvatarError(true)}
/>
) : (
<HugeiconsIcon icon={Mic01Icon} size={14} className="h-3.5 w-3.5 text-muted-foreground" />
)}
</div>
<ProfileAvatar
profileId={profile.id}
avatarPath={profile.avatar_path}
size="sm"
grayscale={!isSelected}
alt={`${profile.name} avatar`}
/>
<span className="break-words">{profile.name}</span>
</CardTitle>
</CardHeader>
+8 -4
View File
@@ -1,6 +1,6 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { HugeiconsIcon } from '@hugeicons/react';
import { Edit01Icon, MoreHorizontalIcon, Add01Icon, Delete01Icon, Mic01Icon } from '@hugeicons/core-free-icons';
import { Edit01Icon, MoreHorizontalIcon, Add01Icon, Delete01Icon } from '@hugeicons/core-free-icons';
import { useMemo, useRef } from 'react';
import { Button } from '@/components/ui/button';
import {
@@ -19,6 +19,7 @@ import {
TableRow,
} from '@/components/ui/table';
import { ProfileForm } from '@/components/VoiceProfiles/ProfileForm';
import { ProfileAvatar } from '@/components/VoiceProfiles/ProfileAvatar';
import { apiClient } from '@/lib/api/client';
import type { VoiceProfileResponse } from '@/lib/api/types';
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
@@ -185,9 +186,12 @@ function VoiceRow({
<TableRow className="cursor-pointer" onClick={onEdit}>
<TableCell>
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-lg bg-muted flex items-center justify-center shrink-0">
<HugeiconsIcon icon={Mic01Icon} size={16} className="h-4 w-4 text-muted-foreground" />
</div>
<ProfileAvatar
profileId={profile.id}
avatarPath={profile.avatar_path}
size="md"
alt={`${profile.name} avatar`}
/>
<div>
<div className="font-medium">{profile.name}</div>
{profile.description && (
+19 -21
View File
@@ -1,25 +1,19 @@
"use client"
'use client';
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { HugeiconsIcon } from '@hugeicons/react';
import { CircleIcon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import * as React from 'react';
import { cn } from "@/lib/utils/cn"
import { cn } from '@/lib/utils/cn';
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
return <RadioGroupPrimitive.Root className={cn('grid gap-2', className)} {...props} ref={ref} />;
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
@@ -29,17 +23,21 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-accent text-accent ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
'aspect-square h-4 w-4 rounded-full border border-accent text-accent ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<HugeiconsIcon icon={CircleIcon} size={10} className="h-2.5 w-2.5 fill-current text-current" />
<HugeiconsIcon
icon={CircleIcon}
size={10}
className="h-2.5 w-2.5 fill-current text-current"
/>
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
);
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem }
export { RadioGroup, RadioGroupItem };
-5
View File
@@ -91,11 +91,6 @@ export function useGenerationForm(options: UseGenerationFormOptions = {}) {
instruct: data.instruct || undefined,
});
toast({
title: 'Generation complete!',
description: `Audio generated (${result.duration.toFixed(2)}s)`,
});
const audioUrl = apiClient.getAudioUrl(result.id);
setAudioWithAutoPlay(audioUrl, result.id, selectedProfileId, data.text.substring(0, 50));
+28 -7
View File
@@ -1,5 +1,6 @@
import { CancelCircleIcon, CheckmarkCircle02Icon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';
import { CheckmarkCircle02Icon, Loading01Icon, CancelCircleIcon } from '@hugeicons/core-free-icons';
import { Icon } from '@iconify/react';
import { useCallback, useEffect, useRef } from 'react';
import { Progress } from '@/components/ui/progress';
import { useToast } from '@/components/ui/use-toast';
@@ -60,7 +61,7 @@ export function useModelDownloadToast({
title: displayName,
description: (
<div className="flex items-center gap-2">
<HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" />
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
<span>Connecting to download...</span>
</div>
),
@@ -97,19 +98,35 @@ export function useModelDownloadToast({
switch (progress.status) {
case 'complete':
statusIcon = <HugeiconsIcon icon={CheckmarkCircle02Icon} size={16} className="h-4 w-4 text-green-500" />;
statusIcon = (
<HugeiconsIcon
icon={CheckmarkCircle02Icon}
size={16}
className="h-4 w-4 text-green-500"
/>
);
statusText = 'Download complete';
break;
case 'error':
statusIcon = <HugeiconsIcon icon={CancelCircleIcon} size={16} className="h-4 w-4 text-destructive" />;
statusIcon = (
<HugeiconsIcon
icon={CancelCircleIcon}
size={16}
className="h-4 w-4 text-destructive"
/>
);
statusText = `Error: ${progress.error || 'Unknown error'}`;
break;
case 'downloading':
statusIcon = <HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" />;
statusIcon = (
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
);
statusText = progress.filename || 'Downloading...';
break;
case 'extracting':
statusIcon = <HugeiconsIcon icon={Loading01Icon} size={16} className="h-4 w-4 animate-spin" />;
statusIcon = (
<Icon icon="svg-spinners:ring-resize" className="h-4 w-4 animate-spin" />
);
statusText = 'Extracting...';
break;
}
@@ -155,7 +172,11 @@ export function useModelDownloadToast({
toastUpdateRef.current({
title: (
<div className="flex items-center gap-2">
<HugeiconsIcon icon={CheckmarkCircle02Icon} size={16} className="h-4 w-4 text-green-500" />
<HugeiconsIcon
icon={CheckmarkCircle02Icon}
size={16}
className="h-4 w-4 text-green-500"
/>
<span>{displayName}</span>
</div>
),