aibanking.dev / views /Broadcast.tsx
admin08077's picture
Upload 26 files
b8b3edf verified
import React, { useState, useEffect, useMemo } from 'react';
import {
Globe,
Volume2,
Mic2,
Settings2,
Loader2,
Zap,
Sparkles,
MessageSquare,
ArrowRight,
Users,
ChevronRight,
Activity,
ShieldCheck,
Search,
Filter,
X
} from 'lucide-react';
import { synthesizeSpeech, TTS_VOICES, TTS_LANGUAGES, callGemini } from '../services/geminiService';
const Broadcast: React.FC = () => {
const [activeTab, setActiveTab] = useState<'single' | 'multi'>('single');
const [selectedLang, setSelectedLang] = useState(TTS_LANGUAGES.find(l => l.code === 'en')!);
const [selectedVoice, setSelectedVoice] = useState(TTS_VOICES[0]);
const [directorNotes, setDirectorNotes] = useState('Professional, authoritative corporate tone.');
const [transcript, setTranscript] = useState("Greetings, this is a global institutional broadcast via the Lumina Quantum Node. System parity achieved.");
const [isSynthesizing, setIsSynthesizing] = useState(false);
// Multi-speaker state
const [msConfig, setMsConfig] = useState({
speaker1: 'Alex',
voice1: 'Kore',
speaker2: 'Lumina',
voice2: 'Zephyr'
});
const [searchTerm, setSearchTerm] = useState('');
const filteredLangs = useMemo(() =>
TTS_LANGUAGES.filter(l => l.name.toLowerCase().includes(searchTerm.toLowerCase())),
[searchTerm]);
const handleGenerateNotes = async () => {
try {
const response = await callGemini('gemini-3-flash-preview',
`Generate professional, detailed director's notes for a TTS reading in ${selectedLang.name}. The theme is "Quantum Financial Hub Status Update". Keep it to 2 sentences. tone should be ${selectedVoice.style}.`
);
setDirectorNotes(response.text || '');
} catch (e) {
console.error(e);
}
};
const handleBroadcast = async () => {
setIsSynthesizing(true);
const success = await synthesizeSpeech({
text: transcript,
voiceName: selectedVoice.name,
directorNotes: directorNotes,
multiSpeaker: activeTab === 'multi' ? msConfig : undefined
});
setIsSynthesizing(false);
};
return (
<div className="space-y-10 animate-in fade-in duration-700 pb-20 max-w-7xl mx-auto">
{/* Header */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
<div>
<h2 className="text-4xl font-black text-white italic tracking-tighter uppercase mb-2">Global <span className="text-blue-500 not-italic">Broadcast</span></h2>
<p className="text-zinc-500 text-[10px] font-black uppercase tracking-[0.3em] flex items-center gap-2">
<Globe size={12} className="text-blue-500" />
Neural Voice Synthesis • 80+ Protocols Online
</p>
</div>
<div className="flex bg-black p-1.5 rounded-2xl border border-zinc-900">
<button
onClick={() => setActiveTab('single')}
className={`px-8 py-2.5 rounded-xl text-[10px] font-black transition-all uppercase tracking-widest flex items-center gap-3 ${activeTab === 'single' ? 'bg-zinc-100 text-black' : 'text-zinc-500 hover:text-zinc-300'}`}
>
<Mic2 size={14} /> Single Node
</button>
<button
onClick={() => setActiveTab('multi')}
className={`px-8 py-2.5 rounded-xl text-[10px] font-black transition-all uppercase tracking-widest flex items-center gap-3 ${activeTab === 'multi' ? 'bg-zinc-100 text-black' : 'text-zinc-500 hover:text-zinc-300'}`}
>
<Users size={14} /> Multi Speaker
</button>
</div>
</div>
<div className="grid grid-cols-12 gap-8">
{/* Left: Language & Voice Registry */}
<div className="col-span-12 lg:col-span-4 space-y-8">
<div className="bg-zinc-950 border border-zinc-900 rounded-[3rem] p-8 shadow-2xl h-[calc(100vh-350px)] flex flex-col relative overflow-hidden">
<div className="absolute top-0 right-0 p-6 opacity-[0.02]">
<Globe size={180} />
</div>
<div className="relative z-10 mb-8 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-white font-black text-xs uppercase tracking-widest italic">Protocol Registry</h3>
<span className="text-[9px] font-black text-zinc-500 bg-zinc-900 px-3 py-1 rounded-full">{filteredLangs.length} Nodes</span>
</div>
<div className="relative">
<Search size={14} className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-600" />
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search Language Node..."
className="w-full bg-black border border-zinc-800 rounded-xl py-3 pl-11 pr-4 text-white text-[10px] font-black uppercase tracking-widest outline-none focus:border-blue-500 transition-all"
/>
</div>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar pr-2 space-y-2 relative z-10">
{filteredLangs.map(lang => (
<button
key={lang.code}
onClick={() => setSelectedLang(lang)}
className={`w-full flex items-center justify-between p-4 rounded-2xl border transition-all ${selectedLang.code === lang.code ? 'bg-blue-600/10 border-blue-500/50 text-blue-400' : 'bg-black border-zinc-900 text-zinc-600 hover:border-zinc-700'}`}
>
<span className="text-[10px] font-black uppercase tracking-widest">{lang.name}</span>
<span className="text-[9px] mono font-bold opacity-40">{lang.code.toUpperCase()}</span>
</button>
))}
</div>
</div>
</div>
{/* Center: Command Center */}
<div className="col-span-12 lg:col-span-8 space-y-8">
<div className="bg-zinc-950 border border-zinc-900 rounded-[3.5rem] p-10 shadow-2xl relative overflow-hidden group">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_0%,_#1e1b4b_0%,_transparent_60%)] opacity-30"></div>
<div className="relative z-10 space-y-10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-6">
<div className="w-16 h-16 bg-blue-600/10 text-blue-500 border border-blue-500/20 rounded-3xl flex items-center justify-center">
<Activity size={32} className="animate-pulse" />
</div>
<div>
<h4 className="text-2xl font-black text-white italic tracking-tighter uppercase leading-none">{selectedLang.name} Link</h4>
<p className="text-[10px] text-zinc-500 font-black uppercase tracking-widest mt-2">Active Node: {selectedLang.code.toUpperCase()}_PARITY</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="px-6 py-2 bg-black border border-zinc-900 rounded-full flex items-center gap-3">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="text-[9px] font-black text-zinc-500 uppercase tracking-widest">Neural Sync Stable</span>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-6">
<div className="space-y-3">
<label className="text-[10px] font-black text-zinc-600 uppercase tracking-widest ml-1">Voice Profile</label>
<select
value={selectedVoice.name}
onChange={(e) => setSelectedVoice(TTS_VOICES.find(v => v.name === e.target.value)!)}
className="w-full bg-black border border-zinc-900 rounded-2xl py-4 px-6 text-white text-[11px] font-black uppercase tracking-widest outline-none focus:border-blue-500 transition-all appearance-none cursor-pointer"
>
{TTS_VOICES.map(v => (
<option key={v.name} value={v.name}>{v.name} — {v.style}</option>
))}
</select>
</div>
<div className="space-y-3">
<div className="flex justify-between items-end mb-1">
<label className="text-[10px] font-black text-zinc-600 uppercase tracking-widest ml-1">Directorial Notes</label>
<button onClick={handleGenerateNotes} className="text-[9px] font-black text-blue-500 uppercase tracking-widest hover:text-white transition-colors flex items-center gap-2">
<Sparkles size={10} /> Neural Architect
</button>
</div>
<textarea
value={directorNotes}
onChange={(e) => setDirectorNotes(e.target.value)}
className="w-full bg-black border border-zinc-900 rounded-2xl p-6 text-zinc-300 text-[11px] font-medium leading-relaxed italic h-32 outline-none focus:border-blue-500 transition-all resize-none"
/>
</div>
</div>
<div className="space-y-3">
<label className="text-[10px] font-black text-zinc-600 uppercase tracking-widest ml-1">Transmission Script</label>
<div className="relative">
<textarea
value={transcript}
onChange={(e) => setTranscript(e.target.value)}
className="w-full bg-black border-2 border-zinc-900 rounded-[2.5rem] p-8 text-white text-base font-bold leading-relaxed tracking-tight h-[280px] outline-none focus:border-blue-500 transition-all resize-none placeholder:text-zinc-800"
placeholder="Input the global broadcast content..."
/>
<div className="absolute bottom-6 right-8 text-[9px] font-black text-zinc-700 uppercase tracking-widest">
{transcript.length} Characters
</div>
</div>
</div>
</div>
<div className="pt-4">
<button
onClick={handleBroadcast}
disabled={isSynthesizing || !transcript.trim()}
className="w-full py-7 bg-blue-600 hover:bg-blue-500 disabled:bg-zinc-900 disabled:text-zinc-700 text-white rounded-[2.5rem] font-black text-sm uppercase tracking-[0.4em] transition-all flex items-center justify-center gap-6 shadow-2xl shadow-blue-900/40 group"
>
{isSynthesizing ? (
<>
<Loader2 className="animate-spin" size={24} />
<span>Generating Global Waveform...</span>
</>
) : (
<>
<Zap size={24} className="group-hover:scale-125 transition-transform" />
<span>Execute Multi-Node Broadcast</span>
<ArrowRight size={20} className="group-hover:translate-x-3 transition-transform" />
</>
)}
</button>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="bg-zinc-950 border border-zinc-900 rounded-[2.5rem] p-8 shadow-xl flex items-center gap-6 group hover:border-blue-500/20 transition-all">
<div className="p-4 bg-blue-600/10 text-blue-500 rounded-2xl group-hover:scale-110 transition-transform">
<ShieldCheck size={24} />
</div>
<div>
<h5 className="text-white font-black text-xs uppercase italic">Verified Logic</h5>
<p className="text-[10px] text-zinc-600 font-bold uppercase mt-1">Zero-persistence audio packet shredding enabled.</p>
</div>
</div>
<div className="bg-zinc-950 border border-zinc-900 rounded-[2.5rem] p-8 shadow-xl flex items-center gap-6 group hover:border-emerald-500/20 transition-all">
<div className="p-4 bg-emerald-500/10 text-emerald-500 rounded-2xl group-hover:scale-110 transition-transform">
<Users size={24} />
</div>
<div>
<h5 className="text-white font-black text-xs uppercase italic">Directorial Access</h5>
<p className="text-[10px] text-zinc-600 font-bold uppercase mt-1">Fine-grained accent and pace control active.</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Broadcast;