Spaces:
Sleeping
Sleeping
| import React, { useEffect, useState, useRef } from 'react'; | |
| import { BlogSection as BlogSectionType, PaperStructure } from '../types'; | |
| import BlogSectionComponent from './BlogSection'; | |
| import Sidebar from './Sidebar'; | |
| import { Clock, BookOpen, FileText, Share2, Download, Sparkles, CheckCircle2, Loader2, AlertCircle, RefreshCw } from 'lucide-react'; | |
| // Loading placeholder for sections being generated | |
| const SectionLoadingPlaceholder: React.FC<{ title: string; index: number; isCurrentlyGenerating: boolean }> = ({ | |
| title, | |
| index, | |
| isCurrentlyGenerating | |
| }) => ( | |
| <section className="relative scroll-mt-32 animate-in fade-in duration-500"> | |
| <div className="flex gap-8"> | |
| <article className="flex-1 min-w-0"> | |
| <header className="mb-8"> | |
| <div className="flex items-center gap-4 mb-4"> | |
| <span className={` | |
| flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center text-white font-bold text-lg shadow-lg | |
| ${isCurrentlyGenerating | |
| ? 'bg-gradient-to-br from-brand-500 to-purple-600 animate-pulse' | |
| : 'bg-gray-300 dark:bg-gray-700' | |
| } | |
| `}> | |
| {isCurrentlyGenerating ? ( | |
| <Loader2 size={20} className="animate-spin" /> | |
| ) : ( | |
| index + 1 | |
| )} | |
| </span> | |
| <h2 className={`text-2xl md:text-3xl font-display font-bold leading-tight ${ | |
| isCurrentlyGenerating ? 'text-gray-900 dark:text-gray-50' : 'text-gray-400 dark:text-gray-600' | |
| }`}> | |
| {title} | |
| </h2> | |
| </div> | |
| <div className={`w-20 h-1 rounded-full ${ | |
| isCurrentlyGenerating | |
| ? 'bg-gradient-to-r from-brand-500 to-purple-500 animate-pulse' | |
| : 'bg-gray-200 dark:bg-gray-800' | |
| }`} /> | |
| </header> | |
| {/* Loading skeleton */} | |
| <div className="space-y-4"> | |
| {isCurrentlyGenerating ? ( | |
| <> | |
| <div className="flex items-center gap-2 text-sm text-brand-600 dark:text-brand-400 mb-4"> | |
| <Loader2 size={14} className="animate-spin" /> | |
| <span>Generating content...</span> | |
| </div> | |
| <div className="space-y-3"> | |
| <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-full animate-pulse" /> | |
| <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-11/12 animate-pulse delay-75" /> | |
| <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-4/5 animate-pulse delay-100" /> | |
| <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-full animate-pulse delay-150" /> | |
| <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-3/4 animate-pulse delay-200" /> | |
| </div> | |
| <div className="mt-6 p-4 rounded-xl bg-gray-100 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700"> | |
| <div className="h-32 bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse" /> | |
| </div> | |
| </> | |
| ) : ( | |
| <div className="p-8 rounded-xl border-2 border-dashed border-gray-200 dark:border-gray-800 text-center"> | |
| <p className="text-gray-400 dark:text-gray-600 text-sm"> | |
| Waiting to be generated... | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| </article> | |
| </div> | |
| {/* Section Divider */} | |
| <div className="my-16 flex items-center gap-4"> | |
| <div className="flex-1 h-px bg-gradient-to-r from-transparent via-gray-200 dark:via-gray-800 to-transparent" /> | |
| </div> | |
| </section> | |
| ); | |
| // Error state for failed sections | |
| const SectionErrorState: React.FC<{ title: string; error: string; index: number }> = ({ title, error, index }) => ( | |
| <section className="relative scroll-mt-32"> | |
| <div className="flex gap-8"> | |
| <article className="flex-1 min-w-0"> | |
| <header className="mb-8"> | |
| <div className="flex items-center gap-4 mb-4"> | |
| <span className="flex-shrink-0 w-10 h-10 rounded-xl bg-red-500 flex items-center justify-center text-white font-bold text-lg shadow-lg"> | |
| <AlertCircle size={20} /> | |
| </span> | |
| <h2 className="text-2xl md:text-3xl font-display font-bold text-gray-900 dark:text-gray-50 leading-tight"> | |
| {title} | |
| </h2> | |
| </div> | |
| </header> | |
| <div className="p-6 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800"> | |
| <div className="flex items-start gap-3"> | |
| <AlertCircle size={20} className="text-red-500 flex-shrink-0 mt-0.5" /> | |
| <div> | |
| <p className="font-semibold text-red-700 dark:text-red-300"> | |
| Failed to generate this section | |
| </p> | |
| <p className="mt-1 text-sm text-red-600 dark:text-red-400"> | |
| {error} | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </article> | |
| </div> | |
| <div className="my-16 flex items-center gap-4"> | |
| <div className="flex-1 h-px bg-gradient-to-r from-transparent via-gray-300 dark:via-gray-700 to-transparent" /> | |
| </div> | |
| </section> | |
| ); | |
| interface Props { | |
| sections: BlogSectionType[]; | |
| paperTitle: string; | |
| theme: 'light' | 'dark'; | |
| onExport: () => void; | |
| onShare: () => void; | |
| isLoading?: boolean; | |
| loadingStage?: 'idle' | 'analyzing' | 'generating'; | |
| currentSection?: number; | |
| paperStructure?: PaperStructure | null; | |
| } | |
| const BlogView: React.FC<Props> = ({ | |
| sections, | |
| paperTitle, | |
| theme, | |
| onExport, | |
| onShare, | |
| isLoading = false, | |
| loadingStage = 'idle', | |
| currentSection = -1, | |
| paperStructure = null | |
| }) => { | |
| const [activeSection, setActiveSection] = useState<string>(sections[0]?.id || ''); | |
| const [readProgress, setReadProgress] = useState(0); | |
| const contentRef = useRef<HTMLDivElement>(null); | |
| // Calculate reading time (rough estimate: 200 words per minute) | |
| const completedSections = sections.filter(s => !s.isLoading && s.content); | |
| const totalWords = completedSections.reduce((acc, section) => { | |
| return acc + (section.content?.split(/\s+/).length || 0); | |
| }, 0); | |
| const readingTime = Math.max(1, Math.ceil(totalWords / 200)); | |
| // Count completed sections | |
| const completedCount = sections.filter(s => !s.isLoading && !s.error).length; | |
| // Intersection Observer for active section tracking | |
| useEffect(() => { | |
| const options = { | |
| root: null, | |
| rootMargin: '-20% 0px -60% 0px', | |
| threshold: 0 | |
| }; | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach((entry) => { | |
| if (entry.isIntersecting) { | |
| const id = entry.target.id.replace('section-', ''); | |
| setActiveSection(id); | |
| } | |
| }); | |
| }, options); | |
| sections.forEach((section) => { | |
| const element = document.getElementById(`section-${section.id}`); | |
| if (element) observer.observe(element); | |
| }); | |
| return () => observer.disconnect(); | |
| }, [sections]); | |
| // Scroll progress tracking | |
| useEffect(() => { | |
| const handleScroll = () => { | |
| const winScroll = document.documentElement.scrollTop; | |
| const height = document.documentElement.scrollHeight - document.documentElement.clientHeight; | |
| const scrolled = (winScroll / height) * 100; | |
| setReadProgress(scrolled); | |
| }; | |
| window.addEventListener('scroll', handleScroll); | |
| return () => window.removeEventListener('scroll', handleScroll); | |
| }, []); | |
| return ( | |
| <div className="min-h-screen"> | |
| {/* Reading Progress Bar */} | |
| <div className="fixed top-0 left-0 right-0 h-1 bg-gray-200 dark:bg-gray-800 z-50"> | |
| <div | |
| className="h-full bg-gradient-to-r from-brand-500 via-purple-500 to-pink-500 transition-all duration-150" | |
| style={{ width: `${readProgress}%` }} | |
| /> | |
| </div> | |
| {/* Sidebar Navigation */} | |
| {sections.length > 0 && ( | |
| <Sidebar | |
| sections={sections} | |
| activeSection={activeSection} | |
| onSectionClick={setActiveSection} | |
| /> | |
| )} | |
| {/* Main Content */} | |
| <div className="lg:ml-72 xl:mr-8"> | |
| <div ref={contentRef} className="max-w-3xl mx-auto px-6 py-8"> | |
| {/* Loading State - Paper Analysis */} | |
| {loadingStage === 'analyzing' && ( | |
| <div className="flex flex-col items-center justify-center min-h-[60vh] animate-in fade-in duration-500"> | |
| <div className="relative"> | |
| <div className="w-24 h-24 rounded-full bg-gradient-to-r from-brand-500 to-purple-600 animate-pulse flex items-center justify-center"> | |
| <Sparkles size={40} className="text-white animate-bounce" /> | |
| </div> | |
| <div className="absolute inset-0 rounded-full bg-gradient-to-r from-brand-500 to-purple-600 animate-ping opacity-20" /> | |
| </div> | |
| <h2 className="mt-8 text-2xl font-display font-bold text-gray-900 dark:text-white"> | |
| Analyzing Paper Structure | |
| </h2> | |
| <p className="mt-3 text-gray-500 dark:text-gray-400 text-center max-w-md"> | |
| Understanding the paper's key contributions, methodology, and findings to create the optimal narrative structure... | |
| </p> | |
| </div> | |
| )} | |
| {/* Generation Progress Banner */} | |
| {loadingStage === 'generating' && ( | |
| <div className="mb-8 p-4 rounded-2xl bg-gradient-to-r from-brand-50 to-purple-50 dark:from-brand-900/20 dark:to-purple-900/20 border border-brand-200 dark:border-brand-800 animate-in slide-in-from-top duration-500"> | |
| <div className="flex items-center gap-4"> | |
| <div className="flex-shrink-0"> | |
| <div className="w-10 h-10 rounded-full bg-brand-500 flex items-center justify-center"> | |
| <Loader2 size={20} className="text-white animate-spin" /> | |
| </div> | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-sm font-semibold text-brand-700 dark:text-brand-300"> | |
| Generating sections... | |
| </span> | |
| <span className="text-sm text-brand-600 dark:text-brand-400"> | |
| {completedCount} / {sections.length} | |
| </span> | |
| </div> | |
| <div className="h-2 bg-brand-100 dark:bg-brand-900/30 rounded-full overflow-hidden"> | |
| <div | |
| className="h-full bg-gradient-to-r from-brand-500 to-purple-500 rounded-full transition-all duration-500" | |
| style={{ width: `${(completedCount / sections.length) * 100}%` }} | |
| /> | |
| </div> | |
| {currentSection >= 0 && sections[currentSection] && ( | |
| <p className="mt-2 text-xs text-gray-600 dark:text-gray-400 truncate"> | |
| Currently writing: <span className="font-medium">{sections[currentSection].title}</span> | |
| </p> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Article Header */} | |
| {(sections.length > 0 || paperStructure) && ( | |
| <header className="mb-16 animate-in fade-in slide-in-from-bottom-8 duration-700"> | |
| {/* Paper Badge */} | |
| <div className="flex items-center gap-2 mb-6"> | |
| <span className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300 text-xs font-semibold uppercase tracking-wider"> | |
| <FileText size={12} /> | |
| Research Summary | |
| </span> | |
| {loadingStage === 'generating' && ( | |
| <span className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300 text-xs font-semibold"> | |
| <Loader2 size={12} className="animate-spin" /> | |
| Generating... | |
| </span> | |
| )} | |
| </div> | |
| {/* Title */} | |
| <h1 className="text-4xl md:text-5xl lg:text-6xl font-display font-bold text-gray-900 dark:text-white leading-[1.1] mb-8"> | |
| {(paperStructure?.paperTitle || paperTitle).replace('.pdf', '')} | |
| </h1> | |
| {/* Abstract Preview */} | |
| {paperStructure?.paperAbstract && ( | |
| <p className="text-xl text-gray-600 dark:text-gray-300 leading-relaxed mb-8 italic border-l-4 border-brand-500 pl-4"> | |
| {paperStructure.paperAbstract} | |
| </p> | |
| )} | |
| {/* Meta Info */} | |
| <div className="flex flex-wrap items-center gap-6 text-sm text-gray-500 dark:text-gray-400"> | |
| <div className="flex items-center gap-2"> | |
| <Clock size={16} /> | |
| <span>{readingTime} min read</span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <BookOpen size={16} /> | |
| <span>{sections.length} sections</span> | |
| </div> | |
| {completedCount === sections.length && sections.length > 0 && ( | |
| <div className="flex items-center gap-2 text-green-600 dark:text-green-400"> | |
| <CheckCircle2 size={16} /> | |
| <span>Complete</span> | |
| </div> | |
| )} | |
| <div className="flex items-center gap-1 ml-auto"> | |
| <button | |
| onClick={onShare} | |
| className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" | |
| title="Share" | |
| > | |
| <Share2 size={18} /> | |
| </button> | |
| <button | |
| onClick={onExport} | |
| className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" | |
| title="Export" | |
| > | |
| <Download size={18} /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Decorative Line */} | |
| <div className="mt-10 flex items-center gap-4"> | |
| <div className="flex-1 h-px bg-gradient-to-r from-brand-500 via-purple-500 to-transparent" /> | |
| <div className="w-2 h-2 rounded-full bg-brand-500 animate-pulse" /> | |
| </div> | |
| </header> | |
| )} | |
| {/* Key Contribution Highlight */} | |
| {paperStructure?.mainContribution && ( | |
| <div className="mb-16 p-8 rounded-2xl bg-gradient-to-br from-brand-50 to-purple-50 dark:from-brand-900/20 dark:to-purple-900/20 border border-brand-200 dark:border-brand-800 shadow-xl shadow-brand-200/20 dark:shadow-none animate-in fade-in slide-in-from-bottom-4 duration-700"> | |
| <div className="flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-brand-600 dark:text-brand-400 mb-4"> | |
| <Sparkles size={14} /> | |
| Key Contribution | |
| </div> | |
| <p className="text-xl md:text-2xl leading-relaxed text-gray-800 dark:text-gray-200 font-medium"> | |
| {paperStructure.mainContribution} | |
| </p> | |
| </div> | |
| )} | |
| {/* Sections */} | |
| <div className="space-y-4"> | |
| {sections.map((section, index) => ( | |
| <div key={section.id}> | |
| {section.isLoading ? ( | |
| <SectionLoadingPlaceholder | |
| title={section.title} | |
| index={index} | |
| isCurrentlyGenerating={index === currentSection} | |
| /> | |
| ) : section.error ? ( | |
| <SectionErrorState title={section.title} error={section.error} index={index} /> | |
| ) : ( | |
| <BlogSectionComponent | |
| section={section} | |
| theme={theme} | |
| index={index} | |
| /> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| {/* Footer */} | |
| <footer className="mt-20 pt-10 border-t border-gray-200 dark:border-gray-800"> | |
| <div className="text-center"> | |
| <p className="text-sm text-gray-500 dark:text-gray-400 mb-4"> | |
| Generated with PaperStack • Powered by Gemini AI | |
| </p> | |
| <div className="flex justify-center gap-4"> | |
| <button | |
| onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} | |
| className="px-6 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-sm font-semibold transition-colors" | |
| > | |
| Back to Top ↑ | |
| </button> | |
| </div> | |
| </div> | |
| </footer> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default BlogView; | |