mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 00:55:14 -07:00
Enhance App component with loading messages and UI improvements
- Added a loading message feature that cycles through various messages while the server is starting in Tauri. - Improved the loading screen UI with a new layout and animations for the voicebox logo and loading text. - Refactored the App component to include necessary imports and state management for loading messages. - Updated styles for better visual appeal and user experience during the loading phase.
This commit is contained in:
@@ -40,6 +40,7 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"framer-motion": "^12.29.0",
|
"framer-motion": "^12.29.0",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
|
"motion": "^12.29.0",
|
||||||
"react": "^18.3.0",
|
"react": "^18.3.0",
|
||||||
"react-dom": "^18.3.0",
|
"react-dom": "^18.3.0",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
|
|||||||
+58
-11
@@ -1,24 +1,49 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import voiceboxLogo from '@/assets/voicebox-logo.png';
|
||||||
|
import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer';
|
||||||
import { GenerationForm } from '@/components/Generation/GenerationForm';
|
import { GenerationForm } from '@/components/Generation/GenerationForm';
|
||||||
import { HistoryTable } from '@/components/History/HistoryTable';
|
import { HistoryTable } from '@/components/History/HistoryTable';
|
||||||
import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm';
|
import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm';
|
||||||
|
import { ModelManagement } from '@/components/ServerSettings/ModelManagement';
|
||||||
import { ServerStatus } from '@/components/ServerSettings/ServerStatus';
|
import { ServerStatus } from '@/components/ServerSettings/ServerStatus';
|
||||||
import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus';
|
import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus';
|
||||||
import { ModelManagement } from '@/components/ServerSettings/ModelManagement';
|
import ShinyText from '@/components/ShinyText';
|
||||||
|
import { Sidebar } from '@/components/Sidebar';
|
||||||
|
import { UpdateNotification } from '@/components/UpdateNotification';
|
||||||
import { Toaster } from '@/components/ui/toaster';
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
import { ProfileList } from '@/components/VoiceProfiles/ProfileList';
|
import { ProfileList } from '@/components/VoiceProfiles/ProfileList';
|
||||||
import { Sidebar } from '@/components/Sidebar';
|
import { isTauri, setupWindowCloseHandler, startServer } from '@/lib/tauri';
|
||||||
import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer';
|
|
||||||
import { UpdateNotification } from '@/components/UpdateNotification';
|
|
||||||
import { isTauri, startServer, setupWindowCloseHandler } from '@/lib/tauri';
|
|
||||||
import voiceboxLogo from '@/assets/voicebox-logo.png';
|
|
||||||
|
|
||||||
// Track if server is starting to prevent duplicate starts
|
// Track if server is starting to prevent duplicate starts
|
||||||
let serverStarting = false;
|
let serverStarting = false;
|
||||||
|
|
||||||
|
const LOADING_MESSAGES = [
|
||||||
|
'Warming up tensors...',
|
||||||
|
'Calibrating synthesizer engine...',
|
||||||
|
'Initializing voice models...',
|
||||||
|
'Loading neural networks...',
|
||||||
|
'Preparing audio pipelines...',
|
||||||
|
'Optimizing waveform generators...',
|
||||||
|
'Tuning frequency analyzers...',
|
||||||
|
'Building voice embeddings...',
|
||||||
|
'Configuring text-to-speech cores...',
|
||||||
|
'Syncing audio buffers...',
|
||||||
|
'Establishing model connections...',
|
||||||
|
'Preprocessing training data...',
|
||||||
|
'Validating voice samples...',
|
||||||
|
'Compiling inference engines...',
|
||||||
|
'Mapping phoneme sequences...',
|
||||||
|
'Aligning prosody parameters...',
|
||||||
|
'Activating speech synthesis...',
|
||||||
|
'Fine-tuning acoustic models...',
|
||||||
|
'Preparing voice cloning matrices...',
|
||||||
|
'Initializing Qwen TTS framework...',
|
||||||
|
];
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [activeTab, setActiveTab] = useState('main');
|
const [activeTab, setActiveTab] = useState('main');
|
||||||
const [serverReady, setServerReady] = useState(false);
|
const [serverReady, setServerReady] = useState(false);
|
||||||
|
const [loadingMessageIndex, setLoadingMessageIndex] = useState(0);
|
||||||
|
|
||||||
// Setup window close handler and auto-start server when running in Tauri (production only)
|
// Setup window close handler and auto-start server when running in Tauri (production only)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -74,21 +99,43 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Cycle through loading messages every 3 seconds
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isTauri() || serverReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setLoadingMessageIndex((prev) => (prev + 1) % LOADING_MESSAGES.length);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [serverReady]);
|
||||||
|
|
||||||
// Show loading screen while server is starting in Tauri
|
// Show loading screen while server is starting in Tauri
|
||||||
if (isTauri() && !serverReady) {
|
if (isTauri() && !serverReady) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||||
<div className="text-center space-y-6">
|
<div className="text-center space-y-6">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center relative">
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="w-48 h-48 rounded-full bg-accent/20 blur-3xl" />
|
||||||
|
</div>
|
||||||
<img
|
<img
|
||||||
src={voiceboxLogo}
|
src={voiceboxLogo}
|
||||||
alt="Voicebox"
|
alt="Voicebox"
|
||||||
className="w-16 h-16 object-contain animate-fade-in-scale"
|
className="w-48 h-48 object-contain animate-fade-in-scale relative z-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="animate-fade-in-delayed">
|
||||||
|
<ShinyText
|
||||||
|
text={LOADING_MESSAGES[loadingMessageIndex]}
|
||||||
|
className="text-lg font-medium text-muted-foreground"
|
||||||
|
speed={2}
|
||||||
|
color="hsl(var(--muted-foreground))"
|
||||||
|
shineColor="hsl(var(--foreground))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground text-lg animate-fade-in-delayed">
|
|
||||||
Starting voicebox...
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { motion, useAnimationFrame, useMotionValue, useTransform } from 'motion/react';
|
||||||
|
import type React from 'react';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
interface ShinyTextProps {
|
||||||
|
text: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
speed?: number;
|
||||||
|
className?: string;
|
||||||
|
color?: string;
|
||||||
|
shineColor?: string;
|
||||||
|
spread?: number;
|
||||||
|
yoyo?: boolean;
|
||||||
|
pauseOnHover?: boolean;
|
||||||
|
direction?: 'left' | 'right';
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShinyText: React.FC<ShinyTextProps> = ({
|
||||||
|
text,
|
||||||
|
disabled = false,
|
||||||
|
speed = 2,
|
||||||
|
className = '',
|
||||||
|
color = '#b5b5b5',
|
||||||
|
shineColor = '#ffffff',
|
||||||
|
spread = 120,
|
||||||
|
yoyo = false,
|
||||||
|
pauseOnHover = false,
|
||||||
|
direction = 'left',
|
||||||
|
delay = 0,
|
||||||
|
}) => {
|
||||||
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
|
const progress = useMotionValue(0);
|
||||||
|
const elapsedRef = useRef(0);
|
||||||
|
const lastTimeRef = useRef<number | null>(null);
|
||||||
|
const directionRef = useRef(direction === 'left' ? 1 : -1);
|
||||||
|
|
||||||
|
const animationDuration = speed * 1000;
|
||||||
|
const delayDuration = delay * 1000;
|
||||||
|
|
||||||
|
useAnimationFrame((time) => {
|
||||||
|
if (disabled || isPaused) {
|
||||||
|
lastTimeRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastTimeRef.current === null) {
|
||||||
|
lastTimeRef.current = time;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaTime = time - lastTimeRef.current;
|
||||||
|
lastTimeRef.current = time;
|
||||||
|
|
||||||
|
elapsedRef.current += deltaTime;
|
||||||
|
|
||||||
|
// Animation goes from 0 to 100
|
||||||
|
if (yoyo) {
|
||||||
|
const cycleDuration = animationDuration + delayDuration;
|
||||||
|
const fullCycle = cycleDuration * 2;
|
||||||
|
const cycleTime = elapsedRef.current % fullCycle;
|
||||||
|
|
||||||
|
if (cycleTime < animationDuration) {
|
||||||
|
// Forward animation: 0 -> 100
|
||||||
|
const p = (cycleTime / animationDuration) * 100;
|
||||||
|
progress.set(directionRef.current === 1 ? p : 100 - p);
|
||||||
|
} else if (cycleTime < cycleDuration) {
|
||||||
|
// Delay at end
|
||||||
|
progress.set(directionRef.current === 1 ? 100 : 0);
|
||||||
|
} else if (cycleTime < cycleDuration + animationDuration) {
|
||||||
|
// Reverse animation: 100 -> 0
|
||||||
|
const reverseTime = cycleTime - cycleDuration;
|
||||||
|
const p = 100 - (reverseTime / animationDuration) * 100;
|
||||||
|
progress.set(directionRef.current === 1 ? p : 100 - p);
|
||||||
|
} else {
|
||||||
|
// Delay at start
|
||||||
|
progress.set(directionRef.current === 1 ? 0 : 100);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const cycleDuration = animationDuration + delayDuration;
|
||||||
|
const cycleTime = elapsedRef.current % cycleDuration;
|
||||||
|
|
||||||
|
if (cycleTime < animationDuration) {
|
||||||
|
// Animation phase: 0 -> 100
|
||||||
|
const p = (cycleTime / animationDuration) * 100;
|
||||||
|
progress.set(directionRef.current === 1 ? p : 100 - p);
|
||||||
|
} else {
|
||||||
|
// Delay phase - hold at end (shine off-screen)
|
||||||
|
progress.set(directionRef.current === 1 ? 100 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
directionRef.current = direction === 'left' ? 1 : -1;
|
||||||
|
elapsedRef.current = 0;
|
||||||
|
progress.set(0);
|
||||||
|
// eslint-d, progress.setisable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [direction]);
|
||||||
|
|
||||||
|
// Transform: p=0 -> 150% (shine off right), p=100 -> -50% (shine off left)
|
||||||
|
const backgroundPosition = useTransform(progress, (p) => `${150 - p * 2}% center`);
|
||||||
|
|
||||||
|
const handleMouseEnter = useCallback(() => {
|
||||||
|
if (pauseOnHover) setIsPaused(true);
|
||||||
|
}, [pauseOnHover]);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
if (pauseOnHover) setIsPaused(false);
|
||||||
|
}, [pauseOnHover]);
|
||||||
|
|
||||||
|
const gradientStyle: React.CSSProperties = {
|
||||||
|
backgroundImage: `linear-gradient(${spread}deg, ${color} 0%, ${color} 35%, ${shineColor} 50%, ${color} 65%, ${color} 100%)`,
|
||||||
|
backgroundSize: '200% auto',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
backgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.span
|
||||||
|
className={`inline-block ${className}`}
|
||||||
|
style={{ ...gradientStyle, backgroundPosition }}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</motion.span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShinyText;
|
||||||
|
// plugins: [],
|
||||||
|
// };
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"framer-motion": "^12.29.0",
|
"framer-motion": "^12.29.0",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
|
"motion": "^12.29.0",
|
||||||
"react": "^18.3.0",
|
"react": "^18.3.0",
|
||||||
"react-dom": "^18.3.0",
|
"react-dom": "^18.3.0",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
@@ -859,6 +860,8 @@
|
|||||||
|
|
||||||
"minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
"minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
|
"motion": ["[email protected]", "", { "dependencies": { "framer-motion": "^12.29.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rjB5CP2N9S2ESAyEFnAFMgTec6X8yvfxLNcz8n12gPq3M48R7ZbBeVYkDOTj8SPMwfvGIFI801SiPSr1+HCr9g=="],
|
||||||
|
|
||||||
"motion-dom": ["[email protected]", "", { "dependencies": { "motion-utils": "^12.27.2" } }, "sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA=="],
|
"motion-dom": ["[email protected]", "", { "dependencies": { "motion-utils": "^12.27.2" } }, "sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA=="],
|
||||||
|
|
||||||
"motion-utils": ["[email protected]", "", {}, "sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q=="],
|
"motion-utils": ["[email protected]", "", {}, "sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q=="],
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user