PaperStack / components /BlogView.tsx
Akhil-Theerthala's picture
Upload 26 files
57a85c6 verified
raw
history blame
21.4 kB
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, RotateCcw } from 'lucide-react';
// Loading placeholder for sections being generated
const SectionLoadingPlaceholder: React.FC<{
title: string;
index: number;
isCurrentlyGenerating: boolean;
statusMessage?: string;
}> = ({
title,
index,
isCurrentlyGenerating,
statusMessage
}) => (
<section className="relative scroll-mt-32 animate-in fade-in duration-500 mb-24">
<div className="flex flex-col md:flex-row gap-8">
<article className="flex-1 min-w-0">
<header className="mb-12">
<div className="flex items-start gap-6 mb-6">
<span className={`
flex-shrink-0 w-12 h-12 rounded-2xl flex items-center justify-center text-white font-bold text-xl shadow-lg
${isCurrentlyGenerating
? 'bg-gradient-to-br from-brand-500 to-purple-600 animate-pulse shadow-brand-500/20'
: 'bg-gray-200 dark:bg-gray-800 text-gray-400 dark:text-gray-600'
}
`}>
{isCurrentlyGenerating ? (
<Loader2 size={24} className="animate-spin" />
) : (
index + 1
)}
</span>
<div className="flex-1 pt-1">
<h2 className={`text-3xl md:text-4xl font-display font-bold leading-tight mb-4 ${
isCurrentlyGenerating ? 'text-gray-900 dark:text-gray-50' : 'text-gray-300 dark:text-gray-700'
}`}>
{title}
</h2>
<div className={`h-1 rounded-full max-w-[100px] ${
isCurrentlyGenerating
? 'bg-gradient-to-r from-brand-500 to-purple-500 animate-pulse'
: 'bg-gray-100 dark:bg-gray-800'
}`} />
</div>
</div>
</header>
{/* Loading skeleton */}
<div className="space-y-8 pl-0 md:pl-[4.5rem]">
{isCurrentlyGenerating ? (
<>
<div className="flex items-center gap-3 text-sm font-medium text-brand-600 dark:text-brand-400 mb-6 px-4 py-2 rounded-lg bg-brand-50 dark:bg-brand-900/10 w-fit">
<Loader2 size={16} className="animate-spin" />
<span className="font-mono">{statusMessage || 'Generating content...'}</span>
</div>
<div className="space-y-4 max-w-3xl">
<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-[98%] animate-pulse delay-75" />
<div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-[95%] animate-pulse delay-100" />
<div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-[90%] animate-pulse delay-150" />
<div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-[92%] animate-pulse delay-200" />
</div>
<div className="mt-8 p-8 rounded-2xl bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800">
<div className="h-40 bg-gray-200 dark:bg-gray-800 rounded-xl animate-pulse" />
</div>
</>
) : (
<div className="p-12 rounded-3xl border-2 border-dashed border-gray-200 dark:border-gray-800 text-center bg-gray-50/50 dark:bg-gray-900/20">
<p className="text-gray-400 dark:text-gray-600 font-medium">
Waiting to be analyzed...
</p>
</div>
)}
</div>
</article>
</div>
</section>
);
// Error state for failed sections
const SectionErrorState: React.FC<{
title: string;
error: string;
index: number;
onRetry?: () => void;
isRetrying?: boolean;
}> = ({ title, error, index, onRetry, isRetrying }) => (
<section className="relative scroll-mt-32 mb-24">
<div className="flex gap-8">
<article className="flex-1 min-w-0">
<header className="mb-8">
<div className="flex items-start gap-6 mb-4">
<span className="flex-shrink-0 w-12 h-12 rounded-2xl bg-red-500 flex items-center justify-center text-white font-bold text-lg shadow-lg">
{isRetrying ? <Loader2 size={24} className="animate-spin" /> : <AlertCircle size={24} />}
</span>
<div className="flex-1">
<h2 className="text-3xl md:text-4xl font-display font-bold text-gray-900 dark:text-gray-50 leading-tight mb-2">
{title}
</h2>
<div className="h-1 w-20 bg-red-500/30 rounded-full" />
</div>
</div>
</header>
<div className="p-6 rounded-2xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
<div className="flex items-start gap-4">
<AlertCircle size={24} className="text-red-500 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="font-semibold text-red-700 dark:text-red-300 text-lg">
Failed to generate this section
</p>
<p className="mt-2 text-sm text-red-600 dark:text-red-400 leading-relaxed">
{error}
</p>
{onRetry && (
<button
onClick={onRetry}
disabled={isRetrying}
className="mt-4 inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-red-100 dark:bg-red-900/30 hover:bg-red-200 dark:hover:bg-red-800/40 text-red-700 dark:text-red-300 font-semibold text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isRetrying ? (
<>
<Loader2 size={16} className="animate-spin" />
<span>Retrying...</span>
</>
) : (
<>
<RotateCcw size={16} />
<span>Try Again</span>
</>
)}
</button>
)}
</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' | 'validating';
currentSection?: number;
paperStructure?: PaperStructure | null;
sectionStatus?: string;
onRetrySection?: (sectionIndex: number) => Promise<void>;
retryingSectionIndex?: number;
contentRef?: React.RefObject<HTMLDivElement>;
}
const BlogView: React.FC<Props> = ({
sections,
paperTitle,
theme,
onExport,
onShare,
isLoading = false,
loadingStage = 'idle',
currentSection = -1,
paperStructure = null,
sectionStatus = '',
onRetrySection,
retryingSectionIndex = -1,
contentRef
}) => {
const [activeSection, setActiveSection] = useState<string>(sections[0]?.id || '');
const [readProgress, setReadProgress] = useState(0);
const internalContentRef = useRef<HTMLDivElement>(null);
const effectiveContentRef = contentRef || internalContentRef;
// 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={effectiveContentRef} className="max-w-4xl mx-auto px-6 py-12 md:py-20">
{/* 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 shadow-2xl shadow-brand-500/30">
<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-3xl font-display font-bold text-gray-900 dark:text-white text-center">
Deconstructing Research
</h2>
<p className="mt-4 text-lg text-gray-500 dark:text-gray-400 text-center max-w-md font-light">
Analyzing the paper's structure to craft a comprehensive narrative...
</p>
</div>
)}
{/* Generation Progress Banner */}
{(loadingStage === 'generating' || loadingStage === 'validating') && (
<div className={`mb-12 p-6 rounded-2xl border animate-in slide-in-from-top duration-500 backdrop-blur-sm ${
loadingStage === 'validating'
? 'bg-amber-50/80 dark:bg-amber-900/10 border-amber-200 dark:border-amber-800'
: 'bg-brand-50/80 dark:bg-brand-900/10 border-brand-200 dark:border-brand-800'
}`}>
<div className="flex items-center gap-5">
<div className="flex-shrink-0">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shadow-sm ${
loadingStage === 'validating' ? 'bg-amber-100 dark:bg-amber-900/30 text-amber-600' : 'bg-brand-100 dark:bg-brand-900/30 text-brand-600'
}`}>
{loadingStage === 'validating' ? (
<CheckCircle2 size={24} className="animate-pulse" />
) : (
<Loader2 size={24} className="animate-spin" />
)}
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-2">
<span className={`text-base font-semibold ${
loadingStage === 'validating'
? 'text-amber-800 dark:text-amber-300'
: 'text-brand-800 dark:text-brand-300'
}`}>
{loadingStage === 'validating' ? 'Validating scientific accuracy...' : 'Generating insights...'}
</span>
<span className={`text-sm font-medium font-mono ${
loadingStage === 'validating'
? 'text-amber-600 dark:text-amber-400'
: 'text-brand-600 dark:text-brand-400'
}`}>
{completedCount} / {sections.length}
</span>
</div>
<div className={`h-2 rounded-full overflow-hidden ${
loadingStage === 'validating'
? 'bg-amber-200 dark:bg-amber-900/30'
: 'bg-brand-200 dark:bg-brand-900/30'
}`}>
<div
className={`h-full rounded-full transition-all duration-500 ${
loadingStage === 'validating'
? 'bg-gradient-to-r from-amber-500 to-orange-500'
: 'bg-gradient-to-r from-brand-500 to-purple-500'
}`}
style={{ width: `${(completedCount / sections.length) * 100}%` }}
/>
</div>
{sectionStatus ? (
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400 font-mono">
{sectionStatus}
</p>
) : currentSection >= 0 && sections[currentSection] && (
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400 truncate">
Currently analyzing: <span className="font-medium text-gray-900 dark:text-gray-200">{sections[currentSection].title}</span>
</p>
)}
</div>
</div>
</div>
)}
{/* Article Header */}
{(sections.length > 0 || paperStructure) && (
<header className="mb-20 animate-in fade-in slide-in-from-bottom-8 duration-700">
{/* Paper Badge */}
<div className="flex items-center gap-3 mb-8">
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-md bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 text-xs font-bold uppercase tracking-widest border border-gray-200 dark:border-gray-700">
<FileText size={12} />
Research Paper
</span>
<span className="h-px w-8 bg-gray-200 dark:bg-gray-700"></span>
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{new Date().toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}
</span>
</div>
{/* Title */}
<h1 className="text-4xl md:text-6xl lg:text-7xl font-display font-bold text-gray-900 dark:text-white leading-[1.1] mb-10 tracking-tight">
{(paperStructure?.paperTitle || paperTitle).replace('.pdf', '')}
</h1>
{/* Abstract Preview */}
{paperStructure?.paperAbstract && (
<div className="relative">
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand-500 rounded-full opacity-30"></div>
<p className="text-xl md:text-2xl text-gray-600 dark:text-gray-300 leading-relaxed pl-8 font-serif italic text-opacity-90">
{paperStructure.paperAbstract}
</p>
</div>
)}
{/* Meta Info & Actions */}
<div className="mt-12 flex flex-wrap items-center justify-between gap-6 pt-8 border-t border-gray-100 dark:border-gray-800">
<div className="flex items-center gap-8 text-sm font-medium text-gray-500 dark:text-gray-400">
<div className="flex items-center gap-2">
<Clock size={18} className="text-brand-500" />
<span>{readingTime} min read</span>
</div>
<div className="flex items-center gap-2">
<BookOpen size={18} className="text-purple-500" />
<span>{sections.length} sections</span>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={onShare}
className="flex items-center gap-2 px-4 py-2 rounded-full bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-sm font-medium text-gray-700 dark:text-gray-300"
>
<Share2 size={16} />
Share
</button>
<button
onClick={onExport}
className="flex items-center gap-2 px-4 py-2 rounded-full bg-gray-900 dark:bg-white text-white dark:text-gray-900 hover:opacity-90 transition-opacity text-sm font-medium"
>
<Download size={16} />
Export
</button>
</div>
</div>
</header>
)}
{/* Key Contribution Highlight */}
{paperStructure?.mainContribution && (
<div className="mb-24 relative overflow-hidden rounded-3xl bg-slate-900 dark:bg-slate-900 text-white p-8 md:p-12 shadow-2xl shadow-slate-900/20 animate-in fade-in slide-in-from-bottom-4 duration-700 group">
<div className="absolute top-0 right-0 p-32 bg-brand-500/20 rounded-full blur-3xl -mr-16 -mt-16 group-hover:bg-brand-500/30 transition-colors duration-1000"></div>
<div className="absolute bottom-0 left-0 p-24 bg-purple-500/20 rounded-full blur-3xl -ml-12 -mb-12 group-hover:bg-purple-500/30 transition-colors duration-1000"></div>
<div className="relative z-10">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 rounded-lg bg-white/10 backdrop-blur-md border border-white/10">
<Sparkles size={20} className="text-brand-300" />
</div>
<span className="text-sm font-bold uppercase tracking-widest text-brand-200">Core Contribution</span>
</div>
<p className="text-2xl md:text-3xl leading-relaxed font-display font-medium text-slate-50">
{paperStructure.mainContribution}
</p>
</div>
</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}
statusMessage={index === currentSection ? sectionStatus : undefined}
/>
) : section.error ? (
<SectionErrorState
title={section.title}
error={section.error}
index={index}
onRetry={onRetrySection ? () => onRetrySection(index) : undefined}
isRetrying={retryingSectionIndex === 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;