feat: redesign UI with clean, modern SaaS aesthetic

- Remove excessive animations and gradients
- Implement clean card-based layout
- Add professional progress stepper with checkmarks
- Simplify color scheme to gray/indigo/purple
- Create minimal, elegant component designs
- Add subtle hover effects and transitions
- Improve typography and spacing
- Remove emoji overload, keep minimal icons
- Create professional button styles
- Implement clean form inputs with focus states
- Add proper visual hierarchy
- Simplify loading states and messages
- Create cohesive, modern user experience
This commit is contained in:
2025-12-09 13:24:47 +00:00
parent fb294252c7
commit 2f200aa61b
7 changed files with 222 additions and 406 deletions

View File

@@ -1,53 +1,11 @@
/* Mobile-first responsive styles */
#root {
width: 100%;
margin: 0 auto;
padding: 0;
position: relative;
z-index: 1;
}
/* Ensure touch-friendly interactions */
.touch-manipulation {
touch-action: manipulation;
}
/* Smooth animations */
button, input, select, textarea {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Advanced button hover effects */
button:not(:disabled) {
position: relative;
overflow: hidden;
}
button:not(:disabled)::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
button:not(:disabled):hover::before {
width: 300px;
height: 300px;
}
button:not(:disabled):hover {
transform: translateY(-4px) scale(1.02);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
}
button:not(:disabled):active {
transform: translateY(-2px) scale(1.01);
transition: all 0.2s ease;
}
/* Custom range slider styling for better mobile support */

View File

@@ -158,39 +158,52 @@ function App() {
const allRolesAssigned = roles.length > 0 && roles.every(role => voiceAssignments.has(role.id))
return (
<div className="min-h-screen relative">
<div className="container mx-auto py-6 px-4 sm:py-12 sm:px-6 lg:px-8 relative z-10">
{/* Header with gradient text and icon */}
<div className="text-center mb-8 sm:mb-14">
<div className="inline-block mb-6 animate-bounce">
<div className="w-20 h-20 sm:w-24 sm:h-24 mx-auto bg-white rounded-3xl shadow-glow-strong flex items-center justify-center relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-purple-100 to-indigo-100 opacity-50"></div>
<span className="text-4xl sm:text-5xl relative z-10">🎧</span>
</div>
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
<div className="container mx-auto py-8 px-4 sm:py-12 sm:px-6 lg:px-8 max-w-5xl">
{/* Header */}
<div className="text-center mb-12">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl mb-6 shadow-lg">
<span className="text-3xl">🎧</span>
</div>
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-extrabold text-white mb-4 drop-shadow-2xl tracking-tight">
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold text-gray-900 mb-4">
Text to Audiobook
</h1>
<p className="text-white/95 text-base sm:text-lg md:text-xl font-medium max-w-2xl mx-auto leading-relaxed">
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
Transform your stories into immersive audio experiences with AI-powered voices
</p>
{/* Progress indicator */}
<div className="mt-8 flex justify-center items-center gap-2 sm:gap-3">
<div className={`w-2 h-2 sm:w-3 sm:h-3 rounded-full transition-all duration-300 ${phase === 'input' ? 'bg-white scale-125' : 'bg-white/40'}`}></div>
<div className={`w-2 h-2 sm:w-3 sm:h-3 rounded-full transition-all duration-300 ${phase === 'assignment' ? 'bg-white scale-125' : 'bg-white/40'}`}></div>
<div className={`w-2 h-2 sm:w-3 sm:h-3 rounded-full transition-all duration-300 ${phase === 'review' ? 'bg-white scale-125' : 'bg-white/40'}`}></div>
<div className={`w-2 h-2 sm:w-3 sm:h-3 rounded-full transition-all duration-300 ${phase === 'generated' ? 'bg-white scale-125' : 'bg-white/40'}`}></div>
{/* Progress Steps */}
<div className="mt-10 flex justify-center items-center gap-3">
{['Input', 'Voices', 'Review', 'Done'].map((step, index) => {
const phases = ['input', 'assignment', 'review', 'generated'];
const isActive = phase === phases[index];
const isPast = phases.indexOf(phase) > index;
return (
<div key={step} className="flex items-center">
<div className={`flex items-center justify-center w-10 h-10 rounded-full font-semibold text-sm transition-all ${
isActive ? 'bg-indigo-600 text-white scale-110' :
isPast ? 'bg-green-500 text-white' :
'bg-gray-200 text-gray-500'
}`}>
{isPast ? '✓' : index + 1}
</div>
{index < 3 && (
<div className={`w-12 h-1 mx-2 rounded ${isPast ? 'bg-green-500' : 'bg-gray-200'}`}></div>
)}
</div>
);
})}
</div>
</div>
{error && (
<div className="max-w-4xl mx-auto mb-6 p-4 sm:p-5 glass-effect border-2 border-red-400 text-red-700 rounded-2xl shadow-glow text-sm sm:text-base animate-pulse">
<div className="mb-6 p-4 bg-red-50 border border-red-200 text-red-700 rounded-xl">
<div className="flex items-start gap-3">
<span className="text-2xl">⚠️</span>
<span className="text-xl">⚠️</span>
<div>
<p className="font-bold text-lg mb-1">Error</p>
<p>{error}</p>
<p className="font-semibold mb-1">Error</p>
<p className="text-sm">{error}</p>
</div>
</div>
</div>
@@ -209,10 +222,10 @@ function App() {
{phase === 'assignment' && (
<>
{isLongText && (
<div className="max-w-4xl mx-auto mb-6 p-4 sm:p-5 glass-effect border-2 border-yellow-400 text-yellow-900 rounded-2xl shadow-glow text-sm sm:text-base">
<div className="flex items-center gap-3">
<span className="text-2xl">⏱️</span>
<p className="font-semibold">Generation may take several minutes for very long content.</p>
<div className="mb-6 p-4 bg-yellow-50 border border-yellow-200 text-yellow-800 rounded-xl">
<div className="flex items-center gap-2">
<span>⏱️</span>
<p className="text-sm font-medium">Generation may take several minutes for very long content.</p>
</div>
</div>
)}
@@ -236,10 +249,10 @@ function App() {
{phase === 'review' && (
<>
{isLongText && (
<div className="max-w-4xl mx-auto mb-6 p-4 sm:p-5 glass-effect border-2 border-yellow-400 text-yellow-900 rounded-2xl shadow-glow text-sm sm:text-base">
<div className="flex items-center gap-3">
<span className="text-2xl">⏱️</span>
<p className="font-semibold">Generation may take several minutes for very long content.</p>
<div className="mb-6 p-4 bg-yellow-50 border border-yellow-200 text-yellow-800 rounded-xl">
<div className="flex items-center gap-2">
<span>⏱️</span>
<p className="text-sm font-medium">Generation may take several minutes for very long content.</p>
</div>
</div>
)}
@@ -262,30 +275,24 @@ function App() {
)}
{isGenerating && isLongText && (
<div className="max-w-4xl mx-auto mt-6 p-5 sm:p-6 glass-effect border-2 border-blue-400 text-blue-900 rounded-2xl shadow-glow-strong text-center text-sm sm:text-base">
<div className="flex flex-col items-center justify-center gap-4">
<div className="relative">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-200"></div>
<div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-600 border-t-transparent absolute top-0 left-0"></div>
</div>
<div className="mt-6 p-6 bg-blue-50 border border-blue-200 rounded-xl text-center">
<div className="flex flex-col items-center gap-4">
<div className="animate-spin rounded-full h-10 w-10 border-3 border-indigo-600 border-t-transparent"></div>
<div>
<p className="font-bold text-lg mb-1">Generating Your Audiobook</p>
<p className="text-sm">This may take several minutes for very long content. Please be patient...</p>
<p className="font-semibold text-gray-900 mb-1">Generating Your Audiobook</p>
<p className="text-sm text-gray-600">This may take several minutes. Please be patient...</p>
</div>
</div>
</div>
)}
{isGeneratingScript && (
<div className="max-w-4xl mx-auto mt-6 p-5 sm:p-6 glass-effect border-2 border-purple-400 text-purple-900 rounded-2xl shadow-glow-strong text-center text-sm sm:text-base">
<div className="flex flex-col items-center justify-center gap-4">
<div className="relative">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-purple-200"></div>
<div className="animate-spin rounded-full h-12 w-12 border-4 border-purple-600 border-t-transparent absolute top-0 left-0"></div>
</div>
<div className="mt-6 p-6 bg-purple-50 border border-purple-200 rounded-xl text-center">
<div className="flex flex-col items-center gap-4">
<div className="animate-spin rounded-full h-10 w-10 border-3 border-purple-600 border-t-transparent"></div>
<div>
<p className="font-bold text-lg mb-1">Generating Script Preview</p>
<p className="text-sm">AI is analyzing your text and assigning speakers...</p>
<p className="font-semibold text-gray-900 mb-1">Generating Script Preview</p>
<p className="text-sm text-gray-600">AI is analyzing your text...</p>
</div>
</div>
</div>

View File

@@ -8,68 +8,47 @@ export default function AudioPlayerSection({
onDownload
}: AudioPlayerSectionProps) {
return (
<div className="w-full max-w-4xl mx-auto p-6 sm:p-8 glass-effect rounded-3xl shadow-glow-strong card-hover">
<div className="card-modern p-8 space-y-6">
{/* Success Header */}
<div className="text-center mb-8">
<div className="inline-block mb-6">
<div className="w-24 h-24 sm:w-28 sm:h-28 mx-auto bg-gradient-to-br from-green-400 via-emerald-500 to-teal-600 rounded-3xl shadow-2xl flex items-center justify-center animate-bounce shadow-green-500/50">
<span className="text-5xl sm:text-6xl">🎉</span>
</div>
<div className="text-center">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-green-400 to-emerald-500 rounded-2xl mb-4">
<span className="text-3xl">🎉</span>
</div>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-extrabold gradient-text-animated mb-3">
<h2 className="text-3xl font-bold text-gray-900 mb-2">
Your Audiobook is Ready!
</h2>
<p className="text-gray-700 text-base sm:text-lg font-medium">
<p className="text-gray-600">
Listen below or download to enjoy offline
</p>
</div>
{/* Audio Player Card */}
<div className="bg-gradient-to-br from-purple-50 via-indigo-50 to-pink-50 rounded-3xl shadow-2xl p-6 sm:p-10 mb-8 border-2 border-purple-300 hover:shadow-3xl transition-all">
<div className="flex items-center gap-4 mb-6">
<div className="w-14 h-14 bg-gradient-to-br from-purple-500 via-purple-600 to-indigo-600 rounded-2xl flex items-center justify-center shadow-lg shadow-purple-500/50">
<span className="text-2xl">🎧</span>
</div>
<div>
<h3 className="text-xl sm:text-2xl font-extrabold text-gray-800">Listen Now</h3>
<p className="text-sm text-gray-600">Your personalized audiobook</p>
</div>
</div>
{/* Audio Player */}
<div className="p-6 bg-gradient-to-br from-indigo-50 to-purple-50 rounded-xl border border-indigo-100">
<audio
controls
src={audioUrl}
className="w-full min-h-[54px] rounded-2xl shadow-xl"
className="w-full"
preload="metadata"
style={{
filter: 'drop-shadow(0 8px 16px rgba(0, 0, 0, 0.15))'
}}
>
Your browser does not support the audio element.
</audio>
</div>
{/* Download Button */}
<div className="flex justify-center mb-6">
<div className="flex justify-center">
<button
onClick={onDownload}
className="px-12 py-6 min-h-[60px] bg-gradient-to-r from-green-500 via-emerald-600 to-teal-600 hover:from-green-600 hover:via-emerald-700 hover:to-teal-700 text-white font-extrabold rounded-2xl transition-all cursor-pointer touch-manipulation shadow-2xl text-lg sm:text-xl shadow-green-500/50 relative overflow-hidden"
className="btn-primary flex items-center gap-2"
>
<span className="flex items-center justify-center gap-4 relative z-10">
<span className="text-2xl">⬇️</span>
Download Audiobook
</span>
<span>⬇️</span>
Download Audiobook
</button>
</div>
{/* Success Message */}
<div className="p-6 bg-gradient-to-r from-green-50 via-emerald-50 to-teal-50 border-2 border-green-300 text-green-900 rounded-2xl shadow-lg text-center text-sm sm:text-base">
<div className="flex items-center justify-center gap-3 mb-2">
<span className="text-3xl">✅</span>
<p className="font-extrabold text-lg">Success!</p>
</div>
<div className="p-4 bg-green-50 border border-green-200 text-green-800 rounded-xl text-center">
<p className="font-medium">
Your audiobook has been successfully generated with AI-powered voices!
✅ Your audiobook has been successfully generated!
</p>
</div>
</div>

View File

@@ -22,52 +22,37 @@ export default function ScriptReviewSection({
};
return (
<div className="w-full max-w-4xl mx-auto p-6 sm:p-8 glass-effect rounded-3xl shadow-glow card-hover">
<div className="flex items-center gap-4 mb-6">
<div className="w-14 h-14 bg-gradient-to-br from-purple-500 via-purple-600 to-indigo-600 rounded-2xl flex items-center justify-center shadow-lg shadow-purple-500/50">
<span className="text-2xl">📋</span>
</div>
<div>
<h2 className="text-2xl sm:text-3xl font-extrabold gradient-text-animated">Review Script</h2>
<p className="text-gray-600 text-sm mt-1">Fine-tune speaker assignments</p>
</div>
</div>
<div className="text-sm sm:text-base text-gray-700 mb-8 bg-gradient-to-r from-purple-50 to-indigo-50 p-5 rounded-2xl border-2 border-purple-200 shadow-inner">
<div className="flex items-start gap-3">
<span className="text-xl">💡</span>
<p className="font-medium">Review the AI-generated script and adjust speaker assignments as needed.</p>
</div>
<div className="card-modern p-8 space-y-6">
<div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Review Script</h2>
<p className="text-gray-600">Review and adjust speaker assignments before generating your audiobook.</p>
</div>
{/* Script Segments */}
<div className="space-y-5 mb-8">
<div className="space-y-4">
{segments.map((segment, index) => (
<div
key={segment.id}
className="border-2 border-purple-300 rounded-2xl p-5 sm:p-6 bg-gradient-to-br from-white to-purple-50/30 shadow-lg hover:shadow-2xl transition-all card-hover"
style={{ animationDelay: `${index * 0.05}s` }}
className="p-5 bg-gray-50 rounded-xl border border-gray-200"
>
{/* Segment Header */}
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-5 mb-4">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center gap-3 mb-3">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center text-white font-extrabold shadow-md text-lg">
<div className="flex items-center justify-center w-8 h-8 bg-indigo-100 text-indigo-700 rounded-lg font-semibold text-sm">
{index + 1}
</div>
<span className="text-xs font-bold text-purple-600 bg-purple-100 px-3 py-1 rounded-lg">
SEGMENT
</span>
<span className="text-xs font-semibold text-gray-500 uppercase">Segment</span>
</div>
{/* Speaker Dropdown */}
<div className="flex items-center gap-3 flex-1">
<label className="text-sm font-bold text-gray-700 whitespace-nowrap flex items-center gap-2">
<span className="text-lg">🎤</span> Speaker:
<div className="flex items-center gap-2 flex-1">
<label className="text-sm font-semibold text-gray-700 whitespace-nowrap">
Speaker:
</label>
<select
value={segment.speaker}
onChange={(e) => handleSpeakerChange(segment.id, e.target.value)}
className="flex-1 p-4 min-h-[52px] border-2 border-purple-300 rounded-xl focus:border-purple-600 focus:ring-4 focus:ring-purple-300 focus:outline-none text-sm sm:text-base touch-manipulation bg-white shadow-sm font-medium"
className="flex-1 p-2 border-2 border-gray-200 rounded-lg focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:outline-none bg-white"
disabled={isGenerating}
>
{roles.map((role) => (
@@ -80,7 +65,7 @@ export default function ScriptReviewSection({
</div>
{/* Segment Text */}
<div className="w-full min-h-[100px] p-5 border-2 border-indigo-200 rounded-xl bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 text-sm sm:text-base whitespace-pre-wrap shadow-inner leading-relaxed">
<div className="p-4 bg-white rounded-lg border border-gray-200 text-gray-800 whitespace-pre-wrap leading-relaxed">
{segment.text}
</div>
</div>
@@ -88,56 +73,33 @@ export default function ScriptReviewSection({
</div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 justify-center">
<div className="flex flex-col sm:flex-row gap-3 justify-end pt-4">
<button
onClick={onSkipReview}
disabled={isGenerating}
className={`px-8 py-5 min-h-[56px] rounded-2xl font-bold transition-all touch-manipulation shadow-2xl text-lg relative overflow-hidden ${
isGenerating
? 'bg-gray-300 text-gray-500 cursor-not-allowed opacity-60'
: 'bg-gradient-to-r from-gray-500 via-gray-600 to-gray-700 text-white hover:from-gray-600 hover:via-gray-700 hover:to-gray-800 cursor-pointer shadow-gray-500/50'
}`}
className="px-6 py-3 border-2 border-gray-300 text-gray-700 font-semibold rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
<span className="relative z-10">Skip Review</span>
Back
</button>
<button
onClick={onGenerate}
disabled={isGenerating}
className={`px-10 py-5 min-h-[56px] rounded-2xl font-bold text-white text-lg transition-all touch-manipulation shadow-2xl relative overflow-hidden ${
isGenerating
? 'bg-gray-400 cursor-not-allowed opacity-60'
: 'bg-gradient-to-r from-green-500 via-emerald-600 to-teal-600 hover:from-green-600 hover:via-emerald-700 hover:to-teal-700 cursor-pointer shadow-green-500/50'
}`}
className="btn-primary"
>
{isGenerating ? (
<span className="flex items-center justify-center gap-3 relative z-10">
<div className="relative">
<div className="animate-spin rounded-full h-6 w-6 border-3 border-white/30"></div>
<div className="animate-spin rounded-full h-6 w-6 border-3 border-white border-t-transparent absolute top-0 left-0"></div>
</div>
<span className="flex items-center gap-2">
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
Generating...
</span>
) : (
<span className="flex items-center justify-center gap-3 relative z-10">
<span className="text-xl">✨</span>
<span className="flex items-center gap-2">
Generate Audiobook
<span>✨</span>
</span>
)}
</button>
</div>
{/* Info Message */}
{!isGenerating && (
<div className="mt-8 p-6 bg-gradient-to-r from-blue-50 via-indigo-50 to-purple-50 border-2 border-blue-300 text-blue-900 rounded-2xl shadow-lg text-sm sm:text-base">
<p className="font-extrabold mb-3 flex items-center gap-3 text-lg">
<span className="text-2xl">💡</span> Pro Tip
</p>
<p className="leading-relaxed">
Review the AI-generated speaker assignments and make any necessary corrections before generating the audiobook. This ensures the best possible listening experience!
</p>
</div>
)}
</div>
);
}

View File

@@ -36,65 +36,53 @@ export default function TextInputSection({
const isButtonDisabled = disabled || isLoading || text.trim() === '';
return (
<div className="w-full max-w-4xl mx-auto p-6 sm:p-8 glass-effect rounded-3xl shadow-glow card-hover">
<div className="flex items-center gap-4 mb-6 sm:mb-8">
<div className="w-14 h-14 bg-gradient-to-br from-purple-500 via-purple-600 to-indigo-600 rounded-2xl flex items-center justify-center shadow-lg shadow-purple-500/50">
<span className="text-2xl">📝</span>
</div>
<div>
<h2 className="text-2xl sm:text-3xl font-extrabold gradient-text-animated">Enter Your Text</h2>
<p className="text-gray-600 text-sm mt-1">Paste or type your story below</p>
</div>
<div className="card-modern p-8">
<div className="mb-6">
<h2 className="text-2xl font-bold text-gray-900 mb-2">Enter Your Text</h2>
<p className="text-gray-600">Paste or type your story below. Use patterns like "Name:" to mark different speakers.</p>
</div>
<div className="mb-6 relative">
<textarea
value={text}
onChange={handleTextChange}
placeholder="Enter your text here. Use patterns like 'Name:' or '[Name]' to mark different speakers...
placeholder="Example:
Example:
Sarah: Hello! How are you today?
John: I'm doing great, thanks for asking!"
className="w-full h-72 sm:h-80 md:h-96 p-5 sm:p-6 border-2 border-purple-300 rounded-2xl focus:border-purple-600 focus:ring-4 focus:ring-purple-300 focus:outline-none resize-y text-sm sm:text-base touch-manipulation shadow-inner bg-gradient-to-br from-white to-purple-50/30 font-mono leading-relaxed"
John: I'm doing great, thanks for asking!
Sarah: That's wonderful to hear."
className="w-full h-80 p-4 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:outline-none resize-y text-base leading-relaxed"
disabled={disabled || isLoading}
/>
<div className="absolute bottom-4 right-4 text-xs text-gray-500 bg-white/80 px-3 py-1 rounded-lg backdrop-blur-sm">
<div className="absolute bottom-3 right-3 text-xs text-gray-500 bg-white px-2 py-1 rounded-md border border-gray-200">
{text.length} characters
</div>
</div>
{showValidation && text.trim() === '' && (
<div className="mb-6 p-4 sm:p-5 bg-gradient-to-r from-red-50 to-pink-50 border-2 border-red-400 text-red-700 rounded-2xl shadow-lg text-sm sm:text-base animate-pulse">
<div className="flex items-center gap-3">
<span className="text-2xl">⚠️</span>
<p className="font-bold">Please enter some text before proceeding.</p>
<div className="mb-6 p-4 bg-red-50 border border-red-200 text-red-700 rounded-xl">
<div className="flex items-center gap-2">
<span>⚠️</span>
<p className="font-medium">Please enter some text before proceeding.</p>
</div>
</div>
)}
<div className="flex justify-center">
<div className="flex justify-end">
<button
onClick={handleNext}
disabled={isButtonDisabled}
className={`px-10 py-5 min-h-[56px] rounded-2xl font-bold text-white text-lg transition-all touch-manipulation shadow-2xl relative overflow-hidden ${
isButtonDisabled
? 'bg-gray-400 cursor-not-allowed opacity-60'
: 'bg-gradient-to-r from-purple-600 via-purple-700 to-indigo-600 hover:from-purple-700 hover:via-purple-800 hover:to-indigo-700 cursor-pointer shadow-purple-500/50'
}`}
className="btn-primary"
>
{isLoading ? (
<span className="flex items-center justify-center gap-3 relative z-10">
<div className="relative">
<div className="animate-spin rounded-full h-6 w-6 border-3 border-white/30"></div>
<div className="animate-spin rounded-full h-6 w-6 border-3 border-white border-t-transparent absolute top-0 left-0"></div>
</div>
<span className="flex items-center gap-2">
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
Processing...
</span>
) : (
<span className="flex items-center justify-center gap-3 relative z-10">
Next: Assign Voices
<span className="text-xl">→</span>
<span className="flex items-center gap-2">
Continue
<span>→</span>
</span>
)}
</button>

View File

@@ -159,66 +159,51 @@ export default function VoiceAssignmentSection({
const isGenerateDisabled = !allRolesAssigned || isGenerating;
return (
<div className="w-full max-w-4xl mx-auto p-6 sm:p-8 glass-effect rounded-3xl shadow-glow card-hover">
<div className="flex items-center gap-4 mb-8">
<div className="w-14 h-14 bg-gradient-to-br from-purple-500 via-purple-600 to-indigo-600 rounded-2xl flex items-center justify-center shadow-lg shadow-purple-500/50">
<span className="text-2xl">🎤</span>
</div>
<div>
<h2 className="text-2xl sm:text-3xl font-extrabold gradient-text-animated">Assign Voices to Roles</h2>
<p className="text-gray-600 text-sm mt-1">Choose the perfect voice for each character</p>
</div>
<div className="card-modern p-8 space-y-8">
<div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Assign Voices</h2>
<p className="text-gray-600">Choose a voice for each character in your story.</p>
</div>
{/* Role-to-Voice Mapping Section */}
<div className="mb-8">
<h3 className="text-xl font-bold mb-5 text-gray-800 flex items-center gap-3">
<span className="text-2xl">👥</span>
<span>Voice Assignments</span>
</h3>
<div className="space-y-4">
{roles.map((role, index) => (
<div
key={role.id}
className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-5 bg-gradient-to-r from-white to-purple-50/50 p-5 rounded-2xl shadow-md border-2 border-purple-200 hover:border-purple-400 hover:shadow-xl transition-all card-hover"
style={{ animationDelay: `${index * 0.1}s` }}
>
<div className="flex items-center gap-3 sm:w-48">
<div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center text-white font-bold shadow-md">
{index + 1}
</div>
<label className="font-bold text-gray-800 text-base sm:text-lg">
{role.label}
</label>
{/* Voice Assignments */}
<div className="space-y-4">
{roles.map((role, index) => (
<div key={role.id} className="flex flex-col sm:flex-row sm:items-center gap-3 p-4 bg-gray-50 rounded-xl border border-gray-200">
<div className="flex items-center gap-3 sm:w-48">
<div className="flex items-center justify-center w-8 h-8 bg-indigo-100 text-indigo-700 rounded-lg font-semibold text-sm">
{index + 1}
</div>
<select
value={voiceAssignments.get(role.id) || ''}
onChange={(e) => handleVoiceSelect(role.id, e.target.value)}
className="flex-1 p-4 min-h-[52px] border-2 border-purple-300 rounded-xl focus:border-purple-600 focus:ring-4 focus:ring-purple-300 focus:outline-none text-sm sm:text-base touch-manipulation bg-white shadow-sm font-medium"
>
<option value="">🎭 Select a voice...</option>
{KOKORO_VOICES.map((voice) => (
<option key={voice.id} value={voice.id}>
{voice.label}
</option>
))}
</select>
<label className="font-semibold text-gray-900">
{role.label}
</label>
</div>
))}
</div>
<select
value={voiceAssignments.get(role.id) || ''}
onChange={(e) => handleVoiceSelect(role.id, e.target.value)}
className="flex-1 p-3 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:outline-none bg-white"
>
<option value="">Select a voice...</option>
{KOKORO_VOICES.map((voice) => (
<option key={voice.id} value={voice.id}>
{voice.label}
</option>
))}
</select>
</div>
))}
</div>
{/* Language and Speed Settings */}
<div className="grid sm:grid-cols-2 gap-6 mb-8">
{/* Language Selector */}
<div className="bg-gradient-to-br from-white to-indigo-50/50 p-6 rounded-2xl shadow-md border-2 border-indigo-200 hover:shadow-xl transition-all">
<h3 className="text-xl font-bold mb-4 text-gray-800 flex items-center gap-3">
<span className="text-2xl">🌍</span> Language
</h3>
{/* Settings Grid */}
<div className="grid sm:grid-cols-2 gap-6">
{/* Language */}
<div>
<label className="block text-sm font-semibold text-gray-900 mb-2">
Language
</label>
<select
value={language}
onChange={handleLanguageSelect}
className="w-full p-4 min-h-[52px] border-2 border-indigo-300 rounded-xl focus:border-indigo-600 focus:ring-4 focus:ring-indigo-300 focus:outline-none text-sm sm:text-base touch-manipulation bg-white shadow-sm font-medium"
className="w-full p-3 border-2 border-gray-200 rounded-xl focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 focus:outline-none bg-white"
>
{LANGUAGES.map((lang) => (
<option key={lang.code} value={lang.code}>
@@ -228,86 +213,63 @@ export default function VoiceAssignmentSection({
</select>
</div>
{/* Speed Slider */}
<div className="bg-gradient-to-br from-white to-purple-50/50 p-6 rounded-2xl shadow-md border-2 border-purple-200 hover:shadow-xl transition-all">
<h3 className="text-xl font-bold mb-4 text-gray-800 flex items-center gap-3">
<span className="text-2xl">⚡</span> Playback Speed
</h3>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600 font-semibold">0.5x</span>
<input
type="range"
min="0.5"
max="1.5"
step="0.1"
value={speed}
onChange={handleSpeedChange}
className="flex-1 h-8 touch-manipulation"
/>
<span className="text-sm text-gray-600 font-semibold">1.5x</span>
</div>
<div className="mt-4 text-center">
<span className="inline-block font-extrabold text-purple-700 text-2xl bg-gradient-to-r from-purple-100 to-indigo-100 px-6 py-2 rounded-xl shadow-inner">
{speed.toFixed(1)}x
</span>
{/* Speed */}
<div>
<label className="block text-sm font-semibold text-gray-900 mb-2">
Playback Speed: {speed.toFixed(1)}x
</label>
<input
type="range"
min="0.5"
max="1.5"
step="0.1"
value={speed}
onChange={handleSpeedChange}
className="w-full"
/>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>0.5x</span>
<span>1.5x</span>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row justify-center gap-4 sm:gap-5">
<button
onClick={onGenerate}
disabled={isGenerateDisabled}
className={`px-10 py-5 min-h-[56px] rounded-2xl font-bold text-white text-lg transition-all touch-manipulation shadow-2xl relative overflow-hidden ${
isGenerateDisabled
? 'bg-gray-400 cursor-not-allowed opacity-60'
: 'bg-gradient-to-r from-purple-600 via-purple-700 to-indigo-600 hover:from-purple-700 hover:via-purple-800 hover:to-indigo-700 cursor-pointer shadow-purple-500/50'
}`}
>
{isGenerating ? (
<span className="flex items-center justify-center gap-3 relative z-10">
<div className="relative">
<div className="animate-spin rounded-full h-6 w-6 border-3 border-white/30"></div>
<div className="animate-spin rounded-full h-6 w-6 border-3 border-white border-t-transparent absolute top-0 left-0"></div>
</div>
Generating Script...
</span>
) : (
<span className="flex items-center justify-center gap-3 relative z-10">
Next: Review Script
<span className="text-xl">→</span>
</span>
)}
</button>
{onSkipReview && (
<button
onClick={onSkipReview}
disabled={isGenerateDisabled}
className={`px-10 py-5 min-h-[56px] rounded-2xl font-bold text-white text-lg transition-all touch-manipulation shadow-2xl relative overflow-hidden ${
isGenerateDisabled
? 'bg-gray-400 cursor-not-allowed opacity-60'
: 'bg-gradient-to-r from-green-500 via-emerald-600 to-teal-600 hover:from-green-600 hover:via-emerald-700 hover:to-teal-700 cursor-pointer shadow-green-500/50'
}`}
>
<span className="flex items-center justify-center gap-3 relative z-10">
<span className="text-xl">⚡</span>
Skip Review & Generate
</span>
</button>
)}
</div>
{/* Validation Message */}
{!allRolesAssigned && !isGenerating && (
<div className="mt-6 p-5 bg-gradient-to-r from-yellow-50 to-orange-50 border-2 border-yellow-400 text-yellow-900 rounded-2xl text-center text-sm sm:text-base shadow-lg">
<div className="flex items-center justify-center gap-3">
<span className="text-2xl">⚠️</span>
<p className="font-bold">Please assign voices to all roles before generating the audiobook.</p>
</div>
<div className="p-4 bg-yellow-50 border border-yellow-200 text-yellow-800 rounded-xl">
<p className="text-sm font-medium">Please assign voices to all roles before continuing.</p>
</div>
)}
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-3 justify-end">
{onSkipReview && (
<button
onClick={onSkipReview}
disabled={isGenerateDisabled}
className="px-6 py-3 border-2 border-gray-300 text-gray-700 font-semibold rounded-xl hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
Skip Review & Generate
</button>
)}
<button
onClick={onGenerate}
disabled={isGenerateDisabled}
className="btn-primary"
>
{isGenerating ? (
<span className="flex items-center gap-2">
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
Generating...
</span>
) : (
<span className="flex items-center gap-2">
Continue
<span>→</span>
</span>
)}
</button>
</div>
</div>
);
}

View File

@@ -14,82 +14,42 @@ body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-attachment: fixed;
position: relative;
overflow-x: hidden;
}
/* Animated background particles */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
radial-gradient(circle at 20% 50%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 20%, rgba(255, 255, 255, 0.08) 0%, transparent 50%);
animation: float 20s ease-in-out infinite;
pointer-events: none;
z-index: 0;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 20px rgba(102, 126, 234, 0.4), 0 0 40px rgba(118, 75, 162, 0.2); }
50% { box-shadow: 0 0 30px rgba(102, 126, 234, 0.6), 0 0 60px rgba(118, 75, 162, 0.3); }
}
@keyframes shimmer {
0% { background-position: -1000px 0; }
100% { background-position: 1000px 0; }
background: #f8f9fa;
}
@layer utilities {
.glass-effect {
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.3);
.card-modern {
background: white;
border-radius: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
border: 1px solid #e5e7eb;
transition: all 0.2s ease;
}
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
.card-modern:hover {
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
.gradient-text-animated {
background: linear-gradient(90deg, #667eea, #764ba2, #667eea);
background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: shimmer 3s linear infinite;
.btn-primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
font-weight: 600;
padding: 12px 32px;
border-radius: 12px;
border: none;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
.shadow-glow {
box-shadow: 0 20px 60px rgba(102, 126, 234, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1);
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.4);
}
.shadow-glow-strong {
animation: pulse-glow 3s ease-in-out infinite;
}
.card-hover {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 30px 80px rgba(102, 126, 234, 0.4);
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}