mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-18 06:10:43 -07:00
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:
@@ -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...
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user