Spaces:
Sleeping
Sleeping
| import React, { useState, useRef } from 'react'; | |
| import { Layers, Database, Key, CheckCircle, FileJson, ArrowRight, ShieldCheck, Zap, Sparkles, Mail, Globe, Scale, Share2, Copy, FileText, MonitorPlay, Github, Terminal, Package, Rocket, Server, FolderTree, HelpCircle, AlertCircle, XCircle, Mic, Play, Pause, Loader2, Volume2, Info } from 'lucide-react'; | |
| import CodeBlock from './CodeBlock'; | |
| import { generateProposalScript, generateSpeech } from '../geminiService'; | |
| const DeveloperHub: React.FC = () => { | |
| const [activeSection, setActiveSection] = useState<'architecture' | 'deployment' | 'compliance' | 'repo' | 'pitch'>('pitch'); | |
| const [presentationMode, setPresentationMode] = useState(false); | |
| // Pitch Studio States | |
| const [pitchScript, setPitchScript] = useState<string>(''); | |
| const [isGeneratingPitch, setIsGeneratingPitch] = useState(false); | |
| const [isSpeaking, setIsSpeaking] = useState(false); | |
| const [pitchType, setPitchType] = useState<'executive' | 'walkthrough'>('executive'); | |
| const handleCreatePitch = async () => { | |
| setIsGeneratingPitch(true); | |
| const script = await generateProposalScript(pitchType); | |
| setPitchScript(script); | |
| setIsGeneratingPitch(false); | |
| }; | |
| const handlePlayPitch = async () => { | |
| if (!pitchScript) return; | |
| setIsSpeaking(true); | |
| const base64Audio = await generateSpeech(pitchScript); | |
| if (base64Audio) { | |
| const audioData = decode(base64Audio); | |
| const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)({ sampleRate: 24000 }); | |
| const audioBuffer = await decodeAudioData(audioData, audioCtx, 24000, 1); | |
| const source = audioCtx.createBufferSource(); | |
| source.buffer = audioBuffer; | |
| source.connect(audioCtx.destination); | |
| source.onended = () => setIsSpeaking(false); | |
| source.start(); | |
| } else { | |
| setIsSpeaking(false); | |
| } | |
| }; | |
| function decode(base64: string) { | |
| const binaryString = atob(base64); | |
| const len = binaryString.length; | |
| const bytes = new Uint8Array(len); | |
| for (let i = 0; i < len; i++) { | |
| bytes[i] = binaryString.charCodeAt(i); | |
| } | |
| return bytes; | |
| } | |
| async function decodeAudioData( | |
| data: Uint8Array, | |
| ctx: AudioContext, | |
| sampleRate: number, | |
| numChannels: number, | |
| ): Promise<AudioBuffer> { | |
| const dataInt16 = new Int16Array(data.buffer); | |
| const frameCount = dataInt16.length / numChannels; | |
| const buffer = ctx.createBuffer(numChannels, frameCount, sampleRate); | |
| for (let channel = 0; channel < numChannels; channel++) { | |
| const channelData = buffer.getChannelData(channel); | |
| for (let i = 0; i < frameCount; i++) { | |
| channelData[i] = dataInt16[i * numChannels + channel] / 32768.0; | |
| } | |
| } | |
| return buffer; | |
| } | |
| const dockerfileCode = `FROM node:18-alpine AS build | |
| WORKDIR /app | |
| COPY package*.json ./ | |
| RUN npm install | |
| COPY . . | |
| RUN npm run build | |
| FROM nginx:stable-alpine | |
| COPY --from=build /app/dist /usr/share/nginx/html | |
| EXPOSE 80 | |
| CMD ["nginx", "-g", "daemon off;"]`; | |
| const renderSectionContent = (section: string) => { | |
| switch(section) { | |
| case 'pitch': return ( | |
| <div className="space-y-8 animate-in fade-in duration-500"> | |
| <div className="flex items-start justify-between"> | |
| <div> | |
| <h2 className="text-3xl font-display font-bold text-white mb-2">Pitch Studio</h2> | |
| <p className="text-gray-400 max-w-2xl"> | |
| Generate a professional narrative for your proposal video. Use the AI Narrator to provide the voiceover. | |
| </p> | |
| </div> | |
| <div className="p-3 bg-brand-500/20 rounded-2xl border border-brand-500/30"> | |
| <Mic size={32} className="text-brand-500" /> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-8"> | |
| <div className="glass-panel p-6 rounded-2xl bg-white/[0.02] border-white/5 space-y-4"> | |
| <h3 className="text-lg font-bold text-white flex items-center gap-2"> | |
| <FileText size={18} className="text-brand-400" /> | |
| Script Config | |
| </h3> | |
| <div className="flex gap-2 p-1 bg-black/40 rounded-xl border border-white/5 mb-4"> | |
| <button | |
| onClick={() => setPitchType('executive')} | |
| className={`flex-1 py-2 text-[10px] font-black uppercase tracking-widest rounded-lg transition-all ${pitchType === 'executive' ? 'bg-brand-500 text-white' : 'text-gray-500 hover:text-white'}`} | |
| > | |
| Executive Pitch | |
| </button> | |
| <button | |
| onClick={() => setPitchType('walkthrough')} | |
| className={`flex-1 py-2 text-[10px] font-black uppercase tracking-widest rounded-lg transition-all ${pitchType === 'walkthrough' ? 'bg-brand-500 text-white' : 'text-gray-500 hover:text-white'}`} | |
| > | |
| Walkthrough V.O. | |
| </button> | |
| </div> | |
| <button | |
| onClick={handleCreatePitch} | |
| disabled={isGeneratingPitch} | |
| className="w-full py-4 bg-brand-600 hover:bg-brand-500 text-white rounded-xl font-black uppercase tracking-widest text-[10px] flex items-center justify-center gap-3 transition-all active:scale-95 disabled:opacity-50" | |
| > | |
| {isGeneratingPitch ? <Loader2 className="animate-spin" size={16} /> : <Sparkles size={16} />} | |
| Generate {pitchType === 'executive' ? 'Proposal' : 'Walkthrough'} Script | |
| </button> | |
| {pitchScript && ( | |
| <div className="mt-6 space-y-4 animate-in slide-in-from-top-2"> | |
| <div className="p-4 bg-black/40 rounded-xl border border-white/10 max-h-[250px] overflow-y-auto"> | |
| <p className="text-xs text-gray-400 leading-relaxed whitespace-pre-wrap italic"> | |
| "{pitchScript}" | |
| </p> | |
| </div> | |
| <button | |
| onClick={handlePlayPitch} | |
| disabled={isSpeaking} | |
| className="w-full py-4 bg-white text-black hover:bg-brand-500 hover:text-white rounded-xl font-black uppercase tracking-widest text-[10px] flex items-center justify-center gap-3 transition-all disabled:opacity-50" | |
| > | |
| {isSpeaking ? <Loader2 className="animate-spin" size={16} /> : <Volume2 size={16} />} | |
| Play Narrator | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| <div className="glass-panel p-6 rounded-2xl bg-white/[0.02] border-white/5 flex flex-col justify-center items-center text-center space-y-4"> | |
| <div className="w-16 h-16 rounded-full bg-blue-500/20 flex items-center justify-center border border-blue-500/30 mb-2"> | |
| <MonitorPlay size={32} className="text-blue-500" /> | |
| </div> | |
| <h3 className="text-lg font-bold text-white">Video Ready</h3> | |
| <p className="text-xs text-gray-500 max-w-xs"> | |
| Turn on your screen recorder, activate "Presentation Mode", and let the AI handle the narration while you navigate the tabs. | |
| </p> | |
| <div className="flex gap-2"> | |
| <div className="px-3 py-1 bg-white/5 rounded text-[10px] text-gray-400 font-mono">1080p Export</div> | |
| <div className="px-3 py-1 bg-white/5 rounded text-[10px] text-gray-400 font-mono">Dolby Audio</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| case 'repo': return ( | |
| <div className="space-y-8 animate-in fade-in duration-500"> | |
| <div> | |
| <h2 className="text-2xl font-display font-bold text-white mb-2">Repository Setup</h2> | |
| <p className="text-gray-400"> | |
| GitHub is the standard for hosting the Shib Insider technical ecosystem. | |
| </p> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div className="p-6 bg-gray-900/50 rounded-xl border border-gray-700"> | |
| <h3 className="text-brand-400 font-bold mb-3 flex items-center gap-2"> | |
| <Package size={18} /> Frontend Hub | |
| </h3> | |
| <p className="text-xs text-gray-500 mb-4">React 18 + Vite. Optimized for L2 wallet interactions.</p> | |
| <div className="text-[10px] font-mono bg-black/40 p-2 rounded border border-white/5 text-gray-400"> | |
| git init<br/> | |
| git add .<br/> | |
| git commit -m "initial protocol" | |
| </div> | |
| </div> | |
| <div className="p-6 bg-gray-900/50 rounded-xl border border-gray-700"> | |
| <h3 className="text-blue-400 font-bold mb-3 flex items-center gap-2"> | |
| <Terminal size={18} /> Backend Node | |
| </h3> | |
| <p className="text-xs text-gray-500 mb-4">Python + Flask. Handles Beehiiv RSS & DB verification.</p> | |
| <div className="text-[10px] font-mono bg-black/40 p-2 rounded border border-white/5 text-gray-400"> | |
| python -m venv venv<br/> | |
| pip install flask feedparser | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| case 'deployment': return ( | |
| <div className="space-y-8 animate-in fade-in duration-500"> | |
| <div> | |
| <h2 className="text-2xl font-display font-bold text-white mb-2">Deploying to Hugging Face</h2> | |
| <p className="text-gray-400">Host your dashboard on Hugging Face Spaces using the Docker SDK for maximum reliability.</p> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
| <div className="lg:col-span-2 space-y-6"> | |
| <div className="glass-panel p-6 rounded-2xl bg-white/[0.02] border-white/10"> | |
| <h3 className="text-lg font-bold text-white mb-4 flex items-center gap-2"> | |
| <FileCode2 size={18} className="text-blue-400" /> Dockerfile | |
| </h3> | |
| <CodeBlock language="dockerfile" code={dockerfileCode} filename="Dockerfile" /> | |
| </div> | |
| <div className="glass-panel p-6 rounded-2xl bg-white/[0.02] border-white/10"> | |
| <h3 className="text-lg font-bold text-white mb-4">Step-by-Step Instructions</h3> | |
| <ol className="space-y-4"> | |
| <li className="flex gap-4"> | |
| <div className="w-6 h-6 rounded-full bg-brand-500 flex items-center justify-center text-[10px] font-black shrink-0">1</div> | |
| <div> | |
| <p className="text-sm font-bold text-white">Create Space</p> | |
| <p className="text-xs text-gray-500">Go to HF Spaces, select "New Space", choose <strong>Docker</strong> as the SDK.</p> | |
| </div> | |
| </li> | |
| <li className="flex gap-4"> | |
| <div className="w-6 h-6 rounded-full bg-brand-500 flex items-center justify-center text-[10px] font-black shrink-0">2</div> | |
| <div> | |
| <p className="text-sm font-bold text-white">Config Secrets</p> | |
| <p className="text-xs text-gray-500">Under "Settings" → "Variables and Secrets", add your <code>API_KEY</code> from Google AI Studio.</p> | |
| </div> | |
| </li> | |
| <li className="flex gap-4"> | |
| <div className="w-6 h-6 rounded-full bg-brand-500 flex items-center justify-center text-[10px] font-black shrink-0">3</div> | |
| <div> | |
| <p className="text-sm font-bold text-white">Push Files</p> | |
| <p className="text-xs text-gray-500">Push your <code>App.tsx</code>, <code>index.html</code>, and <code>Dockerfile</code>. HF will build automatically.</p> | |
| </div> | |
| </li> | |
| </ol> | |
| </div> | |
| </div> | |
| <div className="space-y-6"> | |
| <div className="p-6 bg-blue-900/10 border border-blue-500/20 rounded-2xl"> | |
| <h4 className="text-blue-400 font-black text-[10px] uppercase tracking-widest mb-3 flex items-center gap-2"> | |
| <Info size={14} /> Deployment Tips | |
| </h4> | |
| <ul className="space-y-3 text-[11px] text-gray-400"> | |
| <li className="flex gap-2"><div className="w-1 h-1 rounded-full bg-blue-500 mt-1.5 shrink-0"></div> Ensure <code>dist</code> folder matches Docker config.</li> | |
| <li className="flex gap-2"><div className="w-1 h-1 rounded-full bg-blue-500 mt-1.5 shrink-0"></div> Set Space to "Public" for subscriber access.</li> | |
| <li className="flex gap-2"><div className="w-1 h-1 rounded-full bg-blue-500 mt-1.5 shrink-0"></div> Port 7860 is the default HF port.</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| case 'compliance': return ( | |
| <div className="space-y-8 animate-in fade-in duration-500"> | |
| <h2 className="text-2xl font-display font-bold text-white">Platform Compliance Strategy</h2> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div className="p-6 bg-gray-900/50 rounded-xl border border-gray-700"> | |
| <h3 className="text-lg font-bold text-brand-400 mb-2">1. Gamified Discovery</h3> | |
| <p className="text-sm text-gray-400 leading-relaxed">Frame activity as exploring the newsletter value rather than "pay-per-click" to maintain platform health.</p> | |
| </div> | |
| <div className="p-6 bg-gray-900/50 rounded-xl border border-gray-700"> | |
| <h3 className="text-lg font-bold text-green-400 mb-2">2. On-Chain Verifiability</h3> | |
| <p className="text-sm text-gray-400 leading-relaxed">Using Shibarium ensures that every reward given is transparent and traceable back to genuine engagement.</p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| default: return <div className="text-gray-500">Section details loaded in full codebase.</div>; | |
| } | |
| } | |
| const SidebarButton = ({ section, icon: Icon, label }: { section: typeof activeSection, icon: any, label: string }) => ( | |
| <button | |
| onClick={() => setActiveSection(section)} | |
| className={`w-full text-left px-4 py-3 rounded-xl transition-all flex items-center gap-3 ${ | |
| activeSection === section | |
| ? 'bg-brand-500/10 text-brand-400 border border-brand-500/20' | |
| : 'text-gray-400 hover:bg-gray-800' | |
| }`} | |
| > | |
| <Icon size={18} /> | |
| <span className="font-semibold">{label}</span> | |
| </button> | |
| ); | |
| return ( | |
| <div className="grid grid-cols-1 lg:grid-cols-4 gap-8 h-full"> | |
| <div className="lg:col-span-1 flex flex-col gap-6"> | |
| <div className="space-y-2"> | |
| {!presentationMode && ( | |
| <> | |
| <SidebarButton section="pitch" icon={Mic} label="Pitch Studio" /> | |
| <SidebarButton section="repo" icon={Github} label="Repository Setup" /> | |
| <SidebarButton section="deployment" icon={Rocket} label="Deploy to HF" /> | |
| <SidebarButton section="compliance" icon={Scale} label="Compliance" /> | |
| </> | |
| )} | |
| <div className="pt-6 border-t border-gray-800 space-y-3"> | |
| <button | |
| onClick={() => setPresentationMode(!presentationMode)} | |
| className={`w-full py-3 px-4 rounded-xl font-bold flex items-center justify-center gap-2 transition-all ${ | |
| presentationMode | |
| ? 'bg-brand-600 text-white shadow-lg' | |
| : 'bg-gray-800 text-gray-300 hover:bg-gray-700' | |
| }`} | |
| > | |
| <MonitorPlay size={18} /> | |
| {presentationMode ? 'Exit Presentation' : 'Presentation Mode'} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="lg:col-span-3"> | |
| <div className="glass-panel p-8 rounded-2xl min-h-[600px] relative overflow-y-auto max-h-[75vh]"> | |
| {presentationMode ? ( | |
| <div className="space-y-16"> | |
| <div className="pb-4 border-b border-gray-800"> | |
| <h1 className="text-3xl font-display font-bold text-white mb-2">Technical Specification: Weekly Trail</h1> | |
| <p className="text-gray-400">Project Overview and Presentation Mode active.</p> | |
| </div> | |
| {renderSectionContent('pitch')} | |
| <hr className="border-gray-800" /> | |
| {renderSectionContent('deployment')} | |
| <hr className="border-gray-800" /> | |
| {renderSectionContent('repo')} | |
| </div> | |
| ) : ( | |
| renderSectionContent(activeSection) | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // Internal icon fix for the file | |
| const FileCode2 = (props: any) => ( | |
| <svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 22h14a2 2 0 0 0 2-2V7.5L14.5 2H6a2 2 0 0 0-2 2v4"/><polyline points="14 2 14 8 20 8"/><path d="m3 15 2 2-2 2"/><path d="m9 15-2 2 2 2"/></svg> | |
| ); | |
| export default DeveloperHub; | |