Akhil-Theerthala commited on
Commit
ade3003
·
verified ·
1 Parent(s): 647d306

checkpoint-2

Browse files
App.tsx CHANGED
@@ -1,11 +1,12 @@
1
 
2
  import React, { useState, useRef } from 'react';
3
- import { Moon, Sun, Upload, FileText, Download, Share2, MessageSquare, AlertCircle, LayoutGrid, List, Grid, ChevronLeft, ArrowRight, X, BrainCircuit } from 'lucide-react';
4
  import Background from './components/Background';
5
  import BentoCard from './components/BentoCard';
6
  import ChatBot from './components/ChatBot';
7
- import { BentoCardData, ChatMessage, AppSettings, ProcessingStatus } from './types';
8
- import { generateBentoCards, expandBentoCard, chatWithDocument } from './services/geminiService';
 
9
 
10
  const App: React.FC = () => {
11
  // Settings
@@ -22,7 +23,13 @@ const App: React.FC = () => {
22
 
23
  // State
24
  const [view, setView] = useState<'input' | 'results'>('input');
 
25
  const [cards, setCards] = useState<BentoCardData[]>([]);
 
 
 
 
 
26
  const [status, setStatus] = useState<ProcessingStatus>({ state: 'idle' });
27
  const [paperContext, setPaperContext] = useState<string>(''); // Stores the raw text/base64
28
  const [paperTitle, setPaperTitle] = useState<string>('');
@@ -108,7 +115,12 @@ const App: React.FC = () => {
108
 
109
  const handleReset = () => {
110
  setView('input');
 
111
  setCards([]);
 
 
 
 
112
  setStatus({ state: 'idle' });
113
  setChatHistory([]);
114
  setIsChatOpen(false);
@@ -117,6 +129,93 @@ const App: React.FC = () => {
117
  setFile(null);
118
  };
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  const handleExpandCard = async (card: BentoCardData) => {
121
  if (card.expandedContent || card.isLoadingDetails) return;
122
 
@@ -403,49 +502,112 @@ const App: React.FC = () => {
403
  {/* Results View */}
404
  {view === 'results' && (
405
  <div className="animate-in fade-in slide-in-from-bottom-8 duration-700">
406
- {/* Controls */}
407
- <div className="flex justify-between items-center mb-6">
408
- <h2 className="text-2xl font-display font-bold flex items-center gap-3">
409
- Summary Grid
410
- {settings.useThinking && (
411
- <span className="text-xs font-medium bg-indigo-100 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 px-2 py-1 rounded-full border border-indigo-200 dark:border-indigo-800 flex items-center gap-1">
412
- <BrainCircuit size={12} /> Deep Thought
413
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  )}
415
- </h2>
416
- <div className="flex gap-2">
417
- <button onClick={handleShare} className="p-2 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 border border-gray-200 dark:border-gray-700 transition-colors text-gray-600 dark:text-gray-300">
418
- <Share2 size={18} />
419
- </button>
420
- <button onClick={handleExport} className="p-2 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 border border-gray-200 dark:border-gray-700 transition-colors text-gray-600 dark:text-gray-300">
421
- <Download size={18} />
422
- </button>
423
- <div className="w-px h-8 bg-gray-200 dark:bg-gray-700 mx-2"></div>
424
- <button onClick={() => setSettings({...settings, layoutMode: 'grid'})} className={`p-2 rounded-lg border transition-colors ${settings.layoutMode === 'grid' ? 'bg-brand-50 dark:bg-brand-900/30 border-brand-500 text-brand-600' : 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-500'}`}>
425
- <Grid size={18} />
426
- </button>
427
- <button onClick={() => setSettings({...settings, layoutMode: 'list'})} className={`p-2 rounded-lg border transition-colors ${settings.layoutMode === 'list' ? 'bg-brand-50 dark:bg-brand-900/30 border-brand-500 text-brand-600' : 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-500'}`}>
428
- <List size={18} />
429
- </button>
430
- </div>
431
  </div>
432
 
433
- {/* Grid */}
434
- <div
435
- ref={gridRef}
436
- className="grid grid-cols-1 md:grid-cols-4 auto-rows-[minmax(180px,auto)] gap-4 md:gap-6 p-1 grid-flow-dense"
437
- >
438
- {cards.map((card) => (
439
- <BentoCard
440
- key={card.id}
441
- card={card}
442
- onExpand={handleExpandCard}
443
- onRate={handleRateCard}
444
- onResize={handleResizeCard}
445
- layoutMode={settings.layoutMode}
446
- />
447
- ))}
448
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
 
450
  {/* Floating Chat Trigger */}
451
  <button
 
1
 
2
  import React, { useState, useRef } from 'react';
3
+ import { Moon, Sun, Upload, FileText, Download, Share2, MessageSquare, AlertCircle, LayoutGrid, List, Grid, ChevronLeft, ArrowRight, X, BrainCircuit, BookOpen, Layers } from 'lucide-react';
4
  import Background from './components/Background';
5
  import BentoCard from './components/BentoCard';
6
  import ChatBot from './components/ChatBot';
7
+ import BlogView from './components/BlogView';
8
+ import { BentoCardData, BlogSection, ChatMessage, AppSettings, ProcessingStatus, ViewMode, PaperStructure } from './types';
9
+ import { generateBentoCards, expandBentoCard, chatWithDocument, analyzePaperStructure, generateSingleBlogSection } from './services/geminiService';
10
 
11
  const App: React.FC = () => {
12
  // Settings
 
23
 
24
  // State
25
  const [view, setView] = useState<'input' | 'results'>('input');
26
+ const [resultViewMode, setResultViewMode] = useState<ViewMode>('grid');
27
  const [cards, setCards] = useState<BentoCardData[]>([]);
28
+ const [blogSections, setBlogSections] = useState<BlogSection[]>([]);
29
+ const [paperStructure, setPaperStructure] = useState<PaperStructure | null>(null);
30
+ const [isBlogLoading, setIsBlogLoading] = useState(false);
31
+ const [blogLoadingStage, setBlogLoadingStage] = useState<'idle' | 'analyzing' | 'generating'>('idle');
32
+ const [currentGeneratingSection, setCurrentGeneratingSection] = useState<number>(-1);
33
  const [status, setStatus] = useState<ProcessingStatus>({ state: 'idle' });
34
  const [paperContext, setPaperContext] = useState<string>(''); // Stores the raw text/base64
35
  const [paperTitle, setPaperTitle] = useState<string>('');
 
115
 
116
  const handleReset = () => {
117
  setView('input');
118
+ setResultViewMode('grid');
119
  setCards([]);
120
+ setBlogSections([]);
121
+ setPaperStructure(null);
122
+ setBlogLoadingStage('idle');
123
+ setCurrentGeneratingSection(-1);
124
  setStatus({ state: 'idle' });
125
  setChatHistory([]);
126
  setIsChatOpen(false);
 
129
  setFile(null);
130
  };
131
 
132
+ const handleSwitchToBlogView = async () => {
133
+ // If we already have fully generated sections, just switch view
134
+ if (blogSections.length > 0 && blogSections.every(s => !s.isLoading)) {
135
+ setResultViewMode('blog');
136
+ return;
137
+ }
138
+
139
+ setIsBlogLoading(true);
140
+ setResultViewMode('blog');
141
+
142
+ try {
143
+ // Step 1: Analyze paper structure
144
+ setBlogLoadingStage('analyzing');
145
+ const structure = await analyzePaperStructure(
146
+ settings.apiKey,
147
+ settings.model,
148
+ paperContext,
149
+ true,
150
+ settings.useThinking
151
+ );
152
+ setPaperStructure(structure);
153
+ setPaperTitle(structure.paperTitle || paperTitle);
154
+
155
+ // Create placeholder sections with loading state
156
+ const placeholderSections: BlogSection[] = structure.sections.map((plan, index) => ({
157
+ id: plan.id,
158
+ title: plan.title,
159
+ content: '',
160
+ isLoading: true,
161
+ visualizationType: plan.suggestedVisualization
162
+ }));
163
+ setBlogSections(placeholderSections);
164
+
165
+ // Step 2: Generate each section progressively
166
+ setBlogLoadingStage('generating');
167
+ const paperContextInfo = {
168
+ title: structure.paperTitle,
169
+ abstract: structure.paperAbstract,
170
+ mainContribution: structure.mainContribution,
171
+ keyTerms: structure.keyTerms
172
+ };
173
+
174
+ for (let i = 0; i < structure.sections.length; i++) {
175
+ setCurrentGeneratingSection(i);
176
+
177
+ try {
178
+ const generatedSection = await generateSingleBlogSection(
179
+ settings.apiKey,
180
+ settings.model,
181
+ paperContext,
182
+ structure.sections[i],
183
+ i,
184
+ structure.sections.length,
185
+ paperContextInfo,
186
+ true,
187
+ settings.useThinking
188
+ );
189
+
190
+ // Update the specific section in state
191
+ setBlogSections(prev => prev.map((section, idx) =>
192
+ idx === i ? { ...generatedSection, isLoading: false } : section
193
+ ));
194
+ } catch (sectionError: any) {
195
+ // Mark section as errored but continue with others
196
+ setBlogSections(prev => prev.map((section, idx) =>
197
+ idx === i ? {
198
+ ...section,
199
+ isLoading: false,
200
+ error: sectionError.message,
201
+ content: `Failed to generate this section: ${sectionError.message}`
202
+ } : section
203
+ ));
204
+ }
205
+ }
206
+
207
+ setCurrentGeneratingSection(-1);
208
+ setBlogLoadingStage('idle');
209
+
210
+ } catch (error: any) {
211
+ console.error('Failed to generate blog view:', error);
212
+ setStatus({ state: 'error', message: 'Failed to analyze paper: ' + error.message });
213
+ setBlogLoadingStage('idle');
214
+ } finally {
215
+ setIsBlogLoading(false);
216
+ }
217
+ };
218
+
219
  const handleExpandCard = async (card: BentoCardData) => {
220
  if (card.expandedContent || card.isLoadingDetails) return;
221
 
 
502
  {/* Results View */}
503
  {view === 'results' && (
504
  <div className="animate-in fade-in slide-in-from-bottom-8 duration-700">
505
+ {/* View Mode Toggle - Grid vs Blog */}
506
+ <div className="flex justify-center mb-8">
507
+ <div className="inline-flex items-center p-1.5 rounded-2xl bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
508
+ <button
509
+ onClick={() => setResultViewMode('grid')}
510
+ className={`
511
+ flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-semibold transition-all duration-300
512
+ ${resultViewMode === 'grid'
513
+ ? 'bg-white dark:bg-gray-900 text-brand-600 dark:text-brand-400 shadow-lg'
514
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
515
+ }
516
+ `}
517
+ >
518
+ <Layers size={18} />
519
+ <span>Bento Grid</span>
520
+ </button>
521
+ <button
522
+ onClick={handleSwitchToBlogView}
523
+ disabled={isBlogLoading && resultViewMode !== 'blog'}
524
+ className={`
525
+ flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-semibold transition-all duration-300
526
+ ${resultViewMode === 'blog'
527
+ ? 'bg-white dark:bg-gray-900 text-brand-600 dark:text-brand-400 shadow-lg'
528
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
529
+ }
530
+ disabled:opacity-50
531
+ `}
532
+ >
533
+ {isBlogLoading && resultViewMode !== 'blog' ? (
534
+ <>
535
+ <div className="w-4 h-4 border-2 border-gray-400 border-t-brand-500 rounded-full animate-spin" />
536
+ <span>Analyzing...</span>
537
+ </>
538
+ ) : (
539
+ <>
540
+ <BookOpen size={18} />
541
+ <span>Blog View</span>
542
+ </>
543
  )}
544
+ </button>
545
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
  </div>
547
 
548
+ {/* Grid View */}
549
+ {resultViewMode === 'grid' && (
550
+ <>
551
+ {/* Controls */}
552
+ <div className="flex justify-between items-center mb-6">
553
+ <h2 className="text-2xl font-display font-bold flex items-center gap-3">
554
+ Summary Grid
555
+ {settings.useThinking && (
556
+ <span className="text-xs font-medium bg-indigo-100 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 px-2 py-1 rounded-full border border-indigo-200 dark:border-indigo-800 flex items-center gap-1">
557
+ <BrainCircuit size={12} /> Deep Thought
558
+ </span>
559
+ )}
560
+ </h2>
561
+ <div className="flex gap-2">
562
+ <button onClick={handleShare} className="p-2 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 border border-gray-200 dark:border-gray-700 transition-colors text-gray-600 dark:text-gray-300">
563
+ <Share2 size={18} />
564
+ </button>
565
+ <button onClick={handleExport} className="p-2 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 border border-gray-200 dark:border-gray-700 transition-colors text-gray-600 dark:text-gray-300">
566
+ <Download size={18} />
567
+ </button>
568
+ <div className="w-px h-8 bg-gray-200 dark:bg-gray-700 mx-2"></div>
569
+ <button onClick={() => setSettings({...settings, layoutMode: 'grid'})} className={`p-2 rounded-lg border transition-colors ${settings.layoutMode === 'grid' ? 'bg-brand-50 dark:bg-brand-900/30 border-brand-500 text-brand-600' : 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-500'}`}>
570
+ <Grid size={18} />
571
+ </button>
572
+ <button onClick={() => setSettings({...settings, layoutMode: 'list'})} className={`p-2 rounded-lg border transition-colors ${settings.layoutMode === 'list' ? 'bg-brand-50 dark:bg-brand-900/30 border-brand-500 text-brand-600' : 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-500'}`}>
573
+ <List size={18} />
574
+ </button>
575
+ </div>
576
+ </div>
577
+
578
+ {/* Grid */}
579
+ <div
580
+ ref={gridRef}
581
+ className="grid grid-cols-1 md:grid-cols-4 auto-rows-[minmax(180px,auto)] gap-4 md:gap-6 p-1 grid-flow-dense"
582
+ >
583
+ {cards.map((card) => (
584
+ <BentoCard
585
+ key={card.id}
586
+ card={card}
587
+ onExpand={handleExpandCard}
588
+ onRate={handleRateCard}
589
+ onResize={handleResizeCard}
590
+ layoutMode={settings.layoutMode}
591
+ />
592
+ ))}
593
+ </div>
594
+ </>
595
+ )}
596
+
597
+ {/* Blog View */}
598
+ {resultViewMode === 'blog' && (
599
+ <BlogView
600
+ sections={blogSections}
601
+ paperTitle={paperStructure?.paperTitle || paperTitle}
602
+ theme={settings.theme}
603
+ onExport={handleExport}
604
+ onShare={handleShare}
605
+ isLoading={isBlogLoading}
606
+ loadingStage={blogLoadingStage}
607
+ currentSection={currentGeneratingSection}
608
+ paperStructure={paperStructure}
609
+ />
610
+ )}
611
 
612
  {/* Floating Chat Trigger */}
613
  <button
components/BlogSection.tsx ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import { BlogSection as BlogSectionType } from '../types';
4
+ import MermaidDiagram from './MermaidDiagram';
5
+ import InteractiveChart from './InteractiveChart';
6
+ import EquationBlock from './EquationBlock';
7
+ import Collapsible from './Collapsible';
8
+ import Tooltip from './Tooltip';
9
+ import { Info, Lightbulb, AlertTriangle, BookOpen } from 'lucide-react';
10
+
11
+ interface Props {
12
+ section: BlogSectionType;
13
+ theme: 'light' | 'dark';
14
+ index: number;
15
+ }
16
+
17
+ const BlogSectionComponent: React.FC<Props> = ({ section, theme, index }) => {
18
+ const getMarginNoteIcon = (icon?: 'info' | 'warning' | 'tip' | 'note') => {
19
+ switch (icon) {
20
+ case 'warning':
21
+ return <AlertTriangle size={14} className="text-amber-500" />;
22
+ case 'tip':
23
+ return <Lightbulb size={14} className="text-green-500" />;
24
+ case 'info':
25
+ return <Info size={14} className="text-blue-500" />;
26
+ default:
27
+ return <BookOpen size={14} className="text-gray-400" />;
28
+ }
29
+ };
30
+
31
+ // Apply tooltips to content
32
+ const renderContent = () => {
33
+ if (!section.technicalTerms || section.technicalTerms.length === 0) {
34
+ return <ReactMarkdown>{section.content}</ReactMarkdown>;
35
+ }
36
+
37
+ // For complex tooltip integration, we'll render ReactMarkdown
38
+ // and let users hover on specially marked terms
39
+ return (
40
+ <div className="relative">
41
+ <ReactMarkdown>{section.content}</ReactMarkdown>
42
+
43
+ {/* Technical Terms Legend */}
44
+ {section.technicalTerms.length > 0 && (
45
+ <div className="mt-6 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-xl border border-gray-200 dark:border-gray-700">
46
+ <h5 className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
47
+ Key Terms
48
+ </h5>
49
+ <div className="flex flex-wrap gap-2">
50
+ {section.technicalTerms.map((term, idx) => (
51
+ <Tooltip key={idx} term={term.term} definition={term.definition}>
52
+ <span className="inline-flex items-center px-2.5 py-1 rounded-lg bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300 text-sm font-medium cursor-help">
53
+ {term.term}
54
+ </span>
55
+ </Tooltip>
56
+ ))}
57
+ </div>
58
+ </div>
59
+ )}
60
+ </div>
61
+ );
62
+ };
63
+
64
+ return (
65
+ <section
66
+ id={`section-${section.id}`}
67
+ className="relative scroll-mt-32 animate-in fade-in slide-in-from-bottom-4 duration-700"
68
+ style={{ animationDelay: `${index * 100}ms` }}
69
+ >
70
+ <div className="flex gap-8">
71
+ {/* Main Content */}
72
+ <article className="flex-1 min-w-0">
73
+ {/* Section Header */}
74
+ <header className="mb-8">
75
+ <div className="flex items-center gap-4 mb-4">
76
+ <span className="flex-shrink-0 w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg shadow-lg shadow-brand-500/20">
77
+ {index + 1}
78
+ </span>
79
+ <h2 className="text-2xl md:text-3xl font-display font-bold text-gray-900 dark:text-gray-50 leading-tight">
80
+ {section.title}
81
+ </h2>
82
+ </div>
83
+ <div className="w-20 h-1 bg-gradient-to-r from-brand-500 to-purple-500 rounded-full" />
84
+ </header>
85
+
86
+ {/* Content */}
87
+ <div className="prose prose-lg dark:prose-invert max-w-none
88
+ prose-headings:font-display prose-headings:font-bold
89
+ prose-h3:text-xl prose-h3:mt-8 prose-h3:mb-4
90
+ prose-p:text-gray-700 prose-p:dark:text-gray-300 prose-p:leading-relaxed
91
+ prose-strong:text-gray-900 prose-strong:dark:text-white
92
+ prose-a:text-brand-600 prose-a:dark:text-brand-400 prose-a:no-underline hover:prose-a:underline
93
+ prose-blockquote:border-l-brand-500 prose-blockquote:bg-gray-50 prose-blockquote:dark:bg-gray-800/50 prose-blockquote:py-2 prose-blockquote:px-4 prose-blockquote:rounded-r-lg prose-blockquote:not-italic
94
+ prose-code:bg-gray-100 prose-code:dark:bg-gray-800 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:before:content-none prose-code:after:content-none
95
+ prose-pre:bg-gray-900 prose-pre:dark:bg-black prose-pre:border prose-pre:border-gray-800
96
+ prose-li:text-gray-700 prose-li:dark:text-gray-300
97
+ ">
98
+ {renderContent()}
99
+ </div>
100
+
101
+ {/* Visualization */}
102
+ {section.visualizationType && section.visualizationType !== 'none' && (
103
+ <div className="my-10">
104
+ <div className="p-6 rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 shadow-lg shadow-gray-200/50 dark:shadow-none">
105
+ {section.visualizationType === 'mermaid' && section.visualizationData && (
106
+ <div className="min-h-[200px]">
107
+ <MermaidDiagram chart={section.visualizationData} theme={theme} />
108
+ </div>
109
+ )}
110
+
111
+ {section.visualizationType === 'chart' && section.chartData && (
112
+ <InteractiveChart data={section.chartData} theme={theme} />
113
+ )}
114
+
115
+ {section.visualizationType === 'equation' && section.visualizationData && (
116
+ <EquationBlock equation={section.visualizationData} label={`${index + 1}`} />
117
+ )}
118
+ </div>
119
+ </div>
120
+ )}
121
+
122
+ {/* Collapsible Deep Dives */}
123
+ {section.collapsibleSections && section.collapsibleSections.length > 0 && (
124
+ <div className="mt-8 space-y-4">
125
+ {section.collapsibleSections.map((collapsible, idx) => (
126
+ <Collapsible
127
+ key={collapsible.id}
128
+ title={collapsible.title}
129
+ variant={idx === 0 ? 'deep-dive' : 'default'}
130
+ >
131
+ <ReactMarkdown>{collapsible.content}</ReactMarkdown>
132
+ </Collapsible>
133
+ ))}
134
+ </div>
135
+ )}
136
+ </article>
137
+
138
+ {/* Margin Notes */}
139
+ {section.marginNotes && section.marginNotes.length > 0 && (
140
+ <aside className="hidden xl:block w-64 flex-shrink-0">
141
+ <div className="sticky top-32 space-y-4">
142
+ {section.marginNotes.map((note, idx) => (
143
+ <div
144
+ key={note.id}
145
+ className="p-4 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700 animate-in fade-in slide-in-from-right-4 duration-500"
146
+ style={{ animationDelay: `${(index * 100) + (idx * 50)}ms` }}
147
+ >
148
+ <div className="flex items-start gap-3">
149
+ <div className="flex-shrink-0 mt-0.5">
150
+ {getMarginNoteIcon(note.icon)}
151
+ </div>
152
+ <p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed">
153
+ {note.text}
154
+ </p>
155
+ </div>
156
+ </div>
157
+ ))}
158
+ </div>
159
+ </aside>
160
+ )}
161
+ </div>
162
+
163
+ {/* Section Divider */}
164
+ <div className="my-16 flex items-center gap-4">
165
+ <div className="flex-1 h-px bg-gradient-to-r from-transparent via-gray-300 dark:via-gray-700 to-transparent" />
166
+ <div className="flex gap-1">
167
+ <div className="w-1.5 h-1.5 rounded-full bg-gray-300 dark:bg-gray-700" />
168
+ <div className="w-1.5 h-1.5 rounded-full bg-gray-400 dark:bg-gray-600" />
169
+ <div className="w-1.5 h-1.5 rounded-full bg-gray-300 dark:bg-gray-700" />
170
+ </div>
171
+ <div className="flex-1 h-px bg-gradient-to-r from-transparent via-gray-300 dark:via-gray-700 to-transparent" />
172
+ </div>
173
+ </section>
174
+ );
175
+ };
176
+
177
+ export default BlogSectionComponent;
178
+
components/BlogView.tsx ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { BlogSection as BlogSectionType, PaperStructure } from '../types';
3
+ import BlogSectionComponent from './BlogSection';
4
+ import Sidebar from './Sidebar';
5
+ import { Clock, BookOpen, FileText, Share2, Download, Sparkles, CheckCircle2, Loader2, AlertCircle, RefreshCw } from 'lucide-react';
6
+
7
+ // Loading placeholder for sections being generated
8
+ const SectionLoadingPlaceholder: React.FC<{ title: string; index: number; isCurrentlyGenerating: boolean }> = ({
9
+ title,
10
+ index,
11
+ isCurrentlyGenerating
12
+ }) => (
13
+ <section className="relative scroll-mt-32 animate-in fade-in duration-500">
14
+ <div className="flex gap-8">
15
+ <article className="flex-1 min-w-0">
16
+ <header className="mb-8">
17
+ <div className="flex items-center gap-4 mb-4">
18
+ <span className={`
19
+ flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center text-white font-bold text-lg shadow-lg
20
+ ${isCurrentlyGenerating
21
+ ? 'bg-gradient-to-br from-brand-500 to-purple-600 animate-pulse'
22
+ : 'bg-gray-300 dark:bg-gray-700'
23
+ }
24
+ `}>
25
+ {isCurrentlyGenerating ? (
26
+ <Loader2 size={20} className="animate-spin" />
27
+ ) : (
28
+ index + 1
29
+ )}
30
+ </span>
31
+ <h2 className={`text-2xl md:text-3xl font-display font-bold leading-tight ${
32
+ isCurrentlyGenerating ? 'text-gray-900 dark:text-gray-50' : 'text-gray-400 dark:text-gray-600'
33
+ }`}>
34
+ {title}
35
+ </h2>
36
+ </div>
37
+ <div className={`w-20 h-1 rounded-full ${
38
+ isCurrentlyGenerating
39
+ ? 'bg-gradient-to-r from-brand-500 to-purple-500 animate-pulse'
40
+ : 'bg-gray-200 dark:bg-gray-800'
41
+ }`} />
42
+ </header>
43
+
44
+ {/* Loading skeleton */}
45
+ <div className="space-y-4">
46
+ {isCurrentlyGenerating ? (
47
+ <>
48
+ <div className="flex items-center gap-2 text-sm text-brand-600 dark:text-brand-400 mb-4">
49
+ <Loader2 size={14} className="animate-spin" />
50
+ <span>Generating content...</span>
51
+ </div>
52
+ <div className="space-y-3">
53
+ <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-full animate-pulse" />
54
+ <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-11/12 animate-pulse delay-75" />
55
+ <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-4/5 animate-pulse delay-100" />
56
+ <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-full animate-pulse delay-150" />
57
+ <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-3/4 animate-pulse delay-200" />
58
+ </div>
59
+ <div className="mt-6 p-4 rounded-xl bg-gray-100 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700">
60
+ <div className="h-32 bg-gray-200 dark:bg-gray-700 rounded-lg animate-pulse" />
61
+ </div>
62
+ </>
63
+ ) : (
64
+ <div className="p-8 rounded-xl border-2 border-dashed border-gray-200 dark:border-gray-800 text-center">
65
+ <p className="text-gray-400 dark:text-gray-600 text-sm">
66
+ Waiting to be generated...
67
+ </p>
68
+ </div>
69
+ )}
70
+ </div>
71
+ </article>
72
+ </div>
73
+
74
+ {/* Section Divider */}
75
+ <div className="my-16 flex items-center gap-4">
76
+ <div className="flex-1 h-px bg-gradient-to-r from-transparent via-gray-200 dark:via-gray-800 to-transparent" />
77
+ </div>
78
+ </section>
79
+ );
80
+
81
+ // Error state for failed sections
82
+ const SectionErrorState: React.FC<{ title: string; error: string; index: number }> = ({ title, error, index }) => (
83
+ <section className="relative scroll-mt-32">
84
+ <div className="flex gap-8">
85
+ <article className="flex-1 min-w-0">
86
+ <header className="mb-8">
87
+ <div className="flex items-center gap-4 mb-4">
88
+ <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">
89
+ <AlertCircle size={20} />
90
+ </span>
91
+ <h2 className="text-2xl md:text-3xl font-display font-bold text-gray-900 dark:text-gray-50 leading-tight">
92
+ {title}
93
+ </h2>
94
+ </div>
95
+ </header>
96
+
97
+ <div className="p-6 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
98
+ <div className="flex items-start gap-3">
99
+ <AlertCircle size={20} className="text-red-500 flex-shrink-0 mt-0.5" />
100
+ <div>
101
+ <p className="font-semibold text-red-700 dark:text-red-300">
102
+ Failed to generate this section
103
+ </p>
104
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">
105
+ {error}
106
+ </p>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </article>
111
+ </div>
112
+
113
+ <div className="my-16 flex items-center gap-4">
114
+ <div className="flex-1 h-px bg-gradient-to-r from-transparent via-gray-300 dark:via-gray-700 to-transparent" />
115
+ </div>
116
+ </section>
117
+ );
118
+
119
+ interface Props {
120
+ sections: BlogSectionType[];
121
+ paperTitle: string;
122
+ theme: 'light' | 'dark';
123
+ onExport: () => void;
124
+ onShare: () => void;
125
+ isLoading?: boolean;
126
+ loadingStage?: 'idle' | 'analyzing' | 'generating';
127
+ currentSection?: number;
128
+ paperStructure?: PaperStructure | null;
129
+ }
130
+
131
+ const BlogView: React.FC<Props> = ({
132
+ sections,
133
+ paperTitle,
134
+ theme,
135
+ onExport,
136
+ onShare,
137
+ isLoading = false,
138
+ loadingStage = 'idle',
139
+ currentSection = -1,
140
+ paperStructure = null
141
+ }) => {
142
+ const [activeSection, setActiveSection] = useState<string>(sections[0]?.id || '');
143
+ const [readProgress, setReadProgress] = useState(0);
144
+ const contentRef = useRef<HTMLDivElement>(null);
145
+
146
+ // Calculate reading time (rough estimate: 200 words per minute)
147
+ const completedSections = sections.filter(s => !s.isLoading && s.content);
148
+ const totalWords = completedSections.reduce((acc, section) => {
149
+ return acc + (section.content?.split(/\s+/).length || 0);
150
+ }, 0);
151
+ const readingTime = Math.max(1, Math.ceil(totalWords / 200));
152
+
153
+ // Count completed sections
154
+ const completedCount = sections.filter(s => !s.isLoading && !s.error).length;
155
+
156
+ // Intersection Observer for active section tracking
157
+ useEffect(() => {
158
+ const options = {
159
+ root: null,
160
+ rootMargin: '-20% 0px -60% 0px',
161
+ threshold: 0
162
+ };
163
+
164
+ const observer = new IntersectionObserver((entries) => {
165
+ entries.forEach((entry) => {
166
+ if (entry.isIntersecting) {
167
+ const id = entry.target.id.replace('section-', '');
168
+ setActiveSection(id);
169
+ }
170
+ });
171
+ }, options);
172
+
173
+ sections.forEach((section) => {
174
+ const element = document.getElementById(`section-${section.id}`);
175
+ if (element) observer.observe(element);
176
+ });
177
+
178
+ return () => observer.disconnect();
179
+ }, [sections]);
180
+
181
+ // Scroll progress tracking
182
+ useEffect(() => {
183
+ const handleScroll = () => {
184
+ const winScroll = document.documentElement.scrollTop;
185
+ const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
186
+ const scrolled = (winScroll / height) * 100;
187
+ setReadProgress(scrolled);
188
+ };
189
+
190
+ window.addEventListener('scroll', handleScroll);
191
+ return () => window.removeEventListener('scroll', handleScroll);
192
+ }, []);
193
+
194
+ return (
195
+ <div className="min-h-screen">
196
+ {/* Reading Progress Bar */}
197
+ <div className="fixed top-0 left-0 right-0 h-1 bg-gray-200 dark:bg-gray-800 z-50">
198
+ <div
199
+ className="h-full bg-gradient-to-r from-brand-500 via-purple-500 to-pink-500 transition-all duration-150"
200
+ style={{ width: `${readProgress}%` }}
201
+ />
202
+ </div>
203
+
204
+ {/* Sidebar Navigation */}
205
+ {sections.length > 0 && (
206
+ <Sidebar
207
+ sections={sections}
208
+ activeSection={activeSection}
209
+ onSectionClick={setActiveSection}
210
+ />
211
+ )}
212
+
213
+ {/* Main Content */}
214
+ <div className="lg:ml-72 xl:mr-8">
215
+ <div ref={contentRef} className="max-w-3xl mx-auto px-6 py-8">
216
+
217
+ {/* Loading State - Paper Analysis */}
218
+ {loadingStage === 'analyzing' && (
219
+ <div className="flex flex-col items-center justify-center min-h-[60vh] animate-in fade-in duration-500">
220
+ <div className="relative">
221
+ <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">
222
+ <Sparkles size={40} className="text-white animate-bounce" />
223
+ </div>
224
+ <div className="absolute inset-0 rounded-full bg-gradient-to-r from-brand-500 to-purple-600 animate-ping opacity-20" />
225
+ </div>
226
+ <h2 className="mt-8 text-2xl font-display font-bold text-gray-900 dark:text-white">
227
+ Analyzing Paper Structure
228
+ </h2>
229
+ <p className="mt-3 text-gray-500 dark:text-gray-400 text-center max-w-md">
230
+ Understanding the paper's key contributions, methodology, and findings to create the optimal narrative structure...
231
+ </p>
232
+ </div>
233
+ )}
234
+
235
+ {/* Generation Progress Banner */}
236
+ {loadingStage === 'generating' && (
237
+ <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">
238
+ <div className="flex items-center gap-4">
239
+ <div className="flex-shrink-0">
240
+ <div className="w-10 h-10 rounded-full bg-brand-500 flex items-center justify-center">
241
+ <Loader2 size={20} className="text-white animate-spin" />
242
+ </div>
243
+ </div>
244
+ <div className="flex-1 min-w-0">
245
+ <div className="flex items-center justify-between mb-2">
246
+ <span className="text-sm font-semibold text-brand-700 dark:text-brand-300">
247
+ Generating sections...
248
+ </span>
249
+ <span className="text-sm text-brand-600 dark:text-brand-400">
250
+ {completedCount} / {sections.length}
251
+ </span>
252
+ </div>
253
+ <div className="h-2 bg-brand-100 dark:bg-brand-900/30 rounded-full overflow-hidden">
254
+ <div
255
+ className="h-full bg-gradient-to-r from-brand-500 to-purple-500 rounded-full transition-all duration-500"
256
+ style={{ width: `${(completedCount / sections.length) * 100}%` }}
257
+ />
258
+ </div>
259
+ {currentSection >= 0 && sections[currentSection] && (
260
+ <p className="mt-2 text-xs text-gray-600 dark:text-gray-400 truncate">
261
+ Currently writing: <span className="font-medium">{sections[currentSection].title}</span>
262
+ </p>
263
+ )}
264
+ </div>
265
+ </div>
266
+ </div>
267
+ )}
268
+
269
+ {/* Article Header */}
270
+ {(sections.length > 0 || paperStructure) && (
271
+ <header className="mb-16 animate-in fade-in slide-in-from-bottom-8 duration-700">
272
+ {/* Paper Badge */}
273
+ <div className="flex items-center gap-2 mb-6">
274
+ <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">
275
+ <FileText size={12} />
276
+ Research Summary
277
+ </span>
278
+ {loadingStage === 'generating' && (
279
+ <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">
280
+ <Loader2 size={12} className="animate-spin" />
281
+ Generating...
282
+ </span>
283
+ )}
284
+ </div>
285
+
286
+ {/* Title */}
287
+ <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">
288
+ {(paperStructure?.paperTitle || paperTitle).replace('.pdf', '')}
289
+ </h1>
290
+
291
+ {/* Abstract Preview */}
292
+ {paperStructure?.paperAbstract && (
293
+ <p className="text-xl text-gray-600 dark:text-gray-300 leading-relaxed mb-8 italic border-l-4 border-brand-500 pl-4">
294
+ {paperStructure.paperAbstract}
295
+ </p>
296
+ )}
297
+
298
+ {/* Meta Info */}
299
+ <div className="flex flex-wrap items-center gap-6 text-sm text-gray-500 dark:text-gray-400">
300
+ <div className="flex items-center gap-2">
301
+ <Clock size={16} />
302
+ <span>{readingTime} min read</span>
303
+ </div>
304
+ <div className="flex items-center gap-2">
305
+ <BookOpen size={16} />
306
+ <span>{sections.length} sections</span>
307
+ </div>
308
+ {completedCount === sections.length && sections.length > 0 && (
309
+ <div className="flex items-center gap-2 text-green-600 dark:text-green-400">
310
+ <CheckCircle2 size={16} />
311
+ <span>Complete</span>
312
+ </div>
313
+ )}
314
+ <div className="flex items-center gap-1 ml-auto">
315
+ <button
316
+ onClick={onShare}
317
+ className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
318
+ title="Share"
319
+ >
320
+ <Share2 size={18} />
321
+ </button>
322
+ <button
323
+ onClick={onExport}
324
+ className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
325
+ title="Export"
326
+ >
327
+ <Download size={18} />
328
+ </button>
329
+ </div>
330
+ </div>
331
+
332
+ {/* Decorative Line */}
333
+ <div className="mt-10 flex items-center gap-4">
334
+ <div className="flex-1 h-px bg-gradient-to-r from-brand-500 via-purple-500 to-transparent" />
335
+ <div className="w-2 h-2 rounded-full bg-brand-500 animate-pulse" />
336
+ </div>
337
+ </header>
338
+ )}
339
+
340
+ {/* Key Contribution Highlight */}
341
+ {paperStructure?.mainContribution && (
342
+ <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">
343
+ <div className="flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-brand-600 dark:text-brand-400 mb-4">
344
+ <Sparkles size={14} />
345
+ Key Contribution
346
+ </div>
347
+ <p className="text-xl md:text-2xl leading-relaxed text-gray-800 dark:text-gray-200 font-medium">
348
+ {paperStructure.mainContribution}
349
+ </p>
350
+ </div>
351
+ )}
352
+
353
+ {/* Sections */}
354
+ <div className="space-y-4">
355
+ {sections.map((section, index) => (
356
+ <div key={section.id}>
357
+ {section.isLoading ? (
358
+ <SectionLoadingPlaceholder
359
+ title={section.title}
360
+ index={index}
361
+ isCurrentlyGenerating={index === currentSection}
362
+ />
363
+ ) : section.error ? (
364
+ <SectionErrorState title={section.title} error={section.error} index={index} />
365
+ ) : (
366
+ <BlogSectionComponent
367
+ section={section}
368
+ theme={theme}
369
+ index={index}
370
+ />
371
+ )}
372
+ </div>
373
+ ))}
374
+ </div>
375
+
376
+ {/* Footer */}
377
+ <footer className="mt-20 pt-10 border-t border-gray-200 dark:border-gray-800">
378
+ <div className="text-center">
379
+ <p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
380
+ Generated with PaperStack • Powered by Gemini AI
381
+ </p>
382
+ <div className="flex justify-center gap-4">
383
+ <button
384
+ onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
385
+ 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"
386
+ >
387
+ Back to Top ↑
388
+ </button>
389
+ </div>
390
+ </div>
391
+ </footer>
392
+ </div>
393
+ </div>
394
+ </div>
395
+ );
396
+ };
397
+
398
+ export default BlogView;
399
+
components/Collapsible.tsx ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { ChevronDown, Lightbulb, BookOpen, Info } from 'lucide-react';
3
+
4
+ interface Props {
5
+ title: string;
6
+ children: React.ReactNode;
7
+ defaultOpen?: boolean;
8
+ variant?: 'default' | 'info' | 'deep-dive';
9
+ }
10
+
11
+ const Collapsible: React.FC<Props> = ({
12
+ title,
13
+ children,
14
+ defaultOpen = false,
15
+ variant = 'default'
16
+ }) => {
17
+ const [isOpen, setIsOpen] = useState(defaultOpen);
18
+
19
+ const getVariantStyles = () => {
20
+ switch (variant) {
21
+ case 'info':
22
+ return {
23
+ wrapper: 'border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-900/10',
24
+ header: 'hover:bg-blue-100/50 dark:hover:bg-blue-900/20',
25
+ icon: <Info size={16} className="text-blue-500" />,
26
+ title: 'text-blue-700 dark:text-blue-300'
27
+ };
28
+ case 'deep-dive':
29
+ return {
30
+ wrapper: 'border-purple-200 dark:border-purple-800 bg-purple-50/50 dark:bg-purple-900/10',
31
+ header: 'hover:bg-purple-100/50 dark:hover:bg-purple-900/20',
32
+ icon: <Lightbulb size={16} className="text-purple-500" />,
33
+ title: 'text-purple-700 dark:text-purple-300'
34
+ };
35
+ default:
36
+ return {
37
+ wrapper: 'border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-800/30',
38
+ header: 'hover:bg-gray-100/50 dark:hover:bg-gray-800/50',
39
+ icon: <BookOpen size={16} className="text-gray-500" />,
40
+ title: 'text-gray-700 dark:text-gray-300'
41
+ };
42
+ }
43
+ };
44
+
45
+ const styles = getVariantStyles();
46
+
47
+ return (
48
+ <div className={`rounded-xl border overflow-hidden transition-all duration-300 ${styles.wrapper}`}>
49
+ <button
50
+ onClick={() => setIsOpen(!isOpen)}
51
+ className={`
52
+ w-full flex items-center justify-between p-4 text-left
53
+ transition-colors duration-200 ${styles.header}
54
+ `}
55
+ >
56
+ <div className="flex items-center gap-3">
57
+ {styles.icon}
58
+ <span className={`font-semibold text-sm ${styles.title}`}>
59
+ {title}
60
+ </span>
61
+ </div>
62
+ <ChevronDown
63
+ size={18}
64
+ className={`
65
+ transition-transform duration-300 ease-out text-gray-400
66
+ ${isOpen ? 'rotate-180' : ''}
67
+ `}
68
+ />
69
+ </button>
70
+
71
+ <div
72
+ className={`
73
+ overflow-hidden transition-all duration-300 ease-out
74
+ ${isOpen ? 'max-h-[2000px] opacity-100' : 'max-h-0 opacity-0'}
75
+ `}
76
+ >
77
+ <div className="p-4 pt-0 border-t border-gray-100 dark:border-gray-800">
78
+ <div className="pt-4 prose prose-sm dark:prose-invert max-w-none">
79
+ {children}
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ );
85
+ };
86
+
87
+ export default Collapsible;
88
+
components/EquationBlock.tsx ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Copy, Check } from 'lucide-react';
3
+
4
+ interface Props {
5
+ equation: string;
6
+ label?: string;
7
+ inline?: boolean;
8
+ }
9
+
10
+ const EquationBlock: React.FC<Props> = ({ equation, label, inline = false }) => {
11
+ const [copied, setCopied] = React.useState(false);
12
+
13
+ const handleCopy = async () => {
14
+ await navigator.clipboard.writeText(equation);
15
+ setCopied(true);
16
+ setTimeout(() => setCopied(false), 2000);
17
+ };
18
+
19
+ // Simple LaTeX-like rendering (for demonstration)
20
+ // In production, integrate KaTeX for proper math rendering
21
+ const formatEquation = (eq: string): string => {
22
+ return eq
23
+ .replace(/\^(\{[^}]+\}|\d)/g, '<sup>$1</sup>')
24
+ .replace(/\_(\{[^}]+\}|\d)/g, '<sub>$1</sub>')
25
+ .replace(/\\frac\{([^}]+)\}\{([^}]+)\}/g, '<span class="frac"><span>$1</span><span>$2</span></span>')
26
+ .replace(/\\sqrt\{([^}]+)\}/g, '√($1)')
27
+ .replace(/\\sum/g, 'Σ')
28
+ .replace(/\\prod/g, 'Π')
29
+ .replace(/\\int/g, '∫')
30
+ .replace(/\\infty/g, '∞')
31
+ .replace(/\\alpha/g, 'α')
32
+ .replace(/\\beta/g, 'β')
33
+ .replace(/\\gamma/g, 'γ')
34
+ .replace(/\\delta/g, 'δ')
35
+ .replace(/\\theta/g, 'θ')
36
+ .replace(/\\lambda/g, 'λ')
37
+ .replace(/\\mu/g, 'μ')
38
+ .replace(/\\sigma/g, 'σ')
39
+ .replace(/\\pi/g, 'π')
40
+ .replace(/\\omega/g, 'ω')
41
+ .replace(/\\partial/g, '∂')
42
+ .replace(/\\nabla/g, '∇')
43
+ .replace(/\\cdot/g, '·')
44
+ .replace(/\\times/g, '×')
45
+ .replace(/\\leq/g, '≤')
46
+ .replace(/\\geq/g, '≥')
47
+ .replace(/\\neq/g, '≠')
48
+ .replace(/\\approx/g, '≈')
49
+ .replace(/\\rightarrow/g, '→')
50
+ .replace(/\\leftarrow/g, '←')
51
+ .replace(/\\Rightarrow/g, '⇒')
52
+ .replace(/\\in/g, '∈')
53
+ .replace(/\\forall/g, '∀')
54
+ .replace(/\\exists/g, '∃');
55
+ };
56
+
57
+ if (inline) {
58
+ return (
59
+ <span
60
+ className="inline-flex items-center px-2 py-0.5 mx-1 font-mono text-sm bg-gray-100 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700"
61
+ dangerouslySetInnerHTML={{ __html: formatEquation(equation) }}
62
+ />
63
+ );
64
+ }
65
+
66
+ return (
67
+ <div className="relative group my-6">
68
+ <div className="flex items-stretch rounded-xl overflow-hidden border border-gray-200 dark:border-gray-700 bg-gradient-to-r from-gray-50 to-white dark:from-gray-900 dark:to-gray-800">
69
+ {/* Equation Number/Label */}
70
+ {label && (
71
+ <div className="flex items-center px-4 bg-gray-100 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700">
72
+ <span className="text-xs font-mono text-gray-500 dark:text-gray-400">
73
+ ({label})
74
+ </span>
75
+ </div>
76
+ )}
77
+
78
+ {/* Equation Content */}
79
+ <div className="flex-1 flex items-center justify-center py-6 px-8">
80
+ <div
81
+ className="font-mono text-lg md:text-xl text-gray-800 dark:text-gray-200 tracking-wide"
82
+ dangerouslySetInnerHTML={{ __html: formatEquation(equation) }}
83
+ />
84
+ </div>
85
+
86
+ {/* Copy Button */}
87
+ <button
88
+ onClick={handleCopy}
89
+ className="absolute top-2 right-2 p-2 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700"
90
+ title="Copy equation"
91
+ >
92
+ {copied ? (
93
+ <Check size={14} className="text-green-500" />
94
+ ) : (
95
+ <Copy size={14} className="text-gray-400" />
96
+ )}
97
+ </button>
98
+ </div>
99
+
100
+ {/* Decorative elements */}
101
+ <div className="absolute -left-2 top-1/2 -translate-y-1/2 w-1 h-8 bg-gradient-to-b from-brand-400 to-purple-400 rounded-full opacity-60" />
102
+ </div>
103
+ );
104
+ };
105
+
106
+ export default EquationBlock;
107
+
components/InteractiveChart.tsx ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { ChartData } from '../types';
3
+
4
+ interface Props {
5
+ data: ChartData;
6
+ theme: 'light' | 'dark';
7
+ }
8
+
9
+ // Simple SVG-based chart implementation that works without external dependencies
10
+ const InteractiveChart: React.FC<Props> = ({ data, theme }) => {
11
+ const isDark = theme === 'dark';
12
+
13
+ // Default colors palette
14
+ const defaultColors = [
15
+ '#0ea5e9', // brand blue
16
+ '#8b5cf6', // purple
17
+ '#10b981', // emerald
18
+ '#f59e0b', // amber
19
+ '#ef4444', // red
20
+ '#ec4899', // pink
21
+ '#06b6d4', // cyan
22
+ '#84cc16', // lime
23
+ ];
24
+
25
+ const colors = data.colors || defaultColors;
26
+ const maxValue = Math.max(...data.data.map(d => d.value));
27
+ const chartHeight = 200;
28
+ const chartWidth = 400;
29
+ const barWidth = Math.min(60, (chartWidth - 40) / data.data.length - 10);
30
+
31
+ const renderBarChart = () => (
32
+ <svg viewBox={`0 0 ${chartWidth} ${chartHeight + 60}`} className="w-full h-auto">
33
+ {/* Y-axis labels */}
34
+ {[0, 0.25, 0.5, 0.75, 1].map((ratio, i) => (
35
+ <g key={i}>
36
+ <text
37
+ x="30"
38
+ y={chartHeight - (ratio * chartHeight) + 5}
39
+ className={`text-[10px] ${isDark ? 'fill-gray-400' : 'fill-gray-500'}`}
40
+ textAnchor="end"
41
+ >
42
+ {Math.round(maxValue * ratio)}
43
+ </text>
44
+ <line
45
+ x1="40"
46
+ y1={chartHeight - (ratio * chartHeight)}
47
+ x2={chartWidth - 10}
48
+ y2={chartHeight - (ratio * chartHeight)}
49
+ className={isDark ? 'stroke-gray-700' : 'stroke-gray-200'}
50
+ strokeDasharray="4,4"
51
+ />
52
+ </g>
53
+ ))}
54
+
55
+ {/* Bars */}
56
+ {data.data.map((item, index) => {
57
+ const barHeight = (item.value / maxValue) * chartHeight;
58
+ const x = 50 + index * ((chartWidth - 60) / data.data.length);
59
+ const y = chartHeight - barHeight;
60
+
61
+ return (
62
+ <g key={index} className="group cursor-pointer">
63
+ {/* Bar */}
64
+ <rect
65
+ x={x}
66
+ y={y}
67
+ width={barWidth}
68
+ height={barHeight}
69
+ fill={colors[index % colors.length]}
70
+ rx="4"
71
+ className="transition-all duration-300 hover:opacity-80"
72
+ />
73
+
74
+ {/* Value on hover */}
75
+ <text
76
+ x={x + barWidth / 2}
77
+ y={y - 8}
78
+ textAnchor="middle"
79
+ className={`text-[11px] font-bold opacity-0 group-hover:opacity-100 transition-opacity ${isDark ? 'fill-gray-200' : 'fill-gray-700'}`}
80
+ >
81
+ {item.value}
82
+ </text>
83
+
84
+ {/* Label */}
85
+ <text
86
+ x={x + barWidth / 2}
87
+ y={chartHeight + 20}
88
+ textAnchor="middle"
89
+ className={`text-[10px] ${isDark ? 'fill-gray-400' : 'fill-gray-600'}`}
90
+ >
91
+ {item.label.length > 10 ? item.label.substring(0, 10) + '...' : item.label}
92
+ </text>
93
+ </g>
94
+ );
95
+ })}
96
+ </svg>
97
+ );
98
+
99
+ const renderLineChart = () => {
100
+ const points = data.data.map((item, index) => {
101
+ const x = 50 + index * ((chartWidth - 80) / (data.data.length - 1 || 1));
102
+ const y = chartHeight - (item.value / maxValue) * chartHeight;
103
+ return { x, y, ...item };
104
+ });
105
+
106
+ const pathD = points
107
+ .map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`)
108
+ .join(' ');
109
+
110
+ // Area fill path
111
+ const areaD = `${pathD} L ${points[points.length - 1]?.x || 0} ${chartHeight} L ${points[0]?.x || 0} ${chartHeight} Z`;
112
+
113
+ return (
114
+ <svg viewBox={`0 0 ${chartWidth} ${chartHeight + 60}`} className="w-full h-auto">
115
+ {/* Grid lines */}
116
+ {[0, 0.25, 0.5, 0.75, 1].map((ratio, i) => (
117
+ <g key={i}>
118
+ <text
119
+ x="30"
120
+ y={chartHeight - (ratio * chartHeight) + 5}
121
+ className={`text-[10px] ${isDark ? 'fill-gray-400' : 'fill-gray-500'}`}
122
+ textAnchor="end"
123
+ >
124
+ {Math.round(maxValue * ratio)}
125
+ </text>
126
+ <line
127
+ x1="40"
128
+ y1={chartHeight - (ratio * chartHeight)}
129
+ x2={chartWidth - 10}
130
+ y2={chartHeight - (ratio * chartHeight)}
131
+ className={isDark ? 'stroke-gray-700' : 'stroke-gray-200'}
132
+ strokeDasharray="4,4"
133
+ />
134
+ </g>
135
+ ))}
136
+
137
+ {/* Area fill */}
138
+ <path
139
+ d={areaD}
140
+ fill={colors[0]}
141
+ fillOpacity="0.1"
142
+ />
143
+
144
+ {/* Line */}
145
+ <path
146
+ d={pathD}
147
+ fill="none"
148
+ stroke={colors[0]}
149
+ strokeWidth="3"
150
+ strokeLinecap="round"
151
+ strokeLinejoin="round"
152
+ />
153
+
154
+ {/* Points and labels */}
155
+ {points.map((point, index) => (
156
+ <g key={index} className="group cursor-pointer">
157
+ <circle
158
+ cx={point.x}
159
+ cy={point.y}
160
+ r="6"
161
+ fill={colors[0]}
162
+ className="transition-all duration-200 hover:r-8"
163
+ />
164
+ <circle
165
+ cx={point.x}
166
+ cy={point.y}
167
+ r="3"
168
+ fill={isDark ? '#1e293b' : 'white'}
169
+ />
170
+
171
+ {/* Tooltip on hover */}
172
+ <text
173
+ x={point.x}
174
+ y={point.y - 14}
175
+ textAnchor="middle"
176
+ className={`text-[11px] font-bold opacity-0 group-hover:opacity-100 transition-opacity ${isDark ? 'fill-gray-200' : 'fill-gray-700'}`}
177
+ >
178
+ {point.value}
179
+ </text>
180
+
181
+ {/* X-axis label */}
182
+ <text
183
+ x={point.x}
184
+ y={chartHeight + 20}
185
+ textAnchor="middle"
186
+ className={`text-[10px] ${isDark ? 'fill-gray-400' : 'fill-gray-600'}`}
187
+ >
188
+ {point.label.length > 8 ? point.label.substring(0, 8) + '..' : point.label}
189
+ </text>
190
+ </g>
191
+ ))}
192
+ </svg>
193
+ );
194
+ };
195
+
196
+ const renderPieChart = () => {
197
+ const total = data.data.reduce((sum, item) => sum + item.value, 0);
198
+ const centerX = 150;
199
+ const centerY = 120;
200
+ const radius = 80;
201
+ let startAngle = -90;
202
+
203
+ return (
204
+ <svg viewBox="0 0 300 280" className="w-full h-auto max-w-[300px] mx-auto">
205
+ {data.data.map((item, index) => {
206
+ const angle = (item.value / total) * 360;
207
+ const endAngle = startAngle + angle;
208
+
209
+ const startRad = (startAngle * Math.PI) / 180;
210
+ const endRad = (endAngle * Math.PI) / 180;
211
+
212
+ const x1 = centerX + radius * Math.cos(startRad);
213
+ const y1 = centerY + radius * Math.sin(startRad);
214
+ const x2 = centerX + radius * Math.cos(endRad);
215
+ const y2 = centerY + radius * Math.sin(endRad);
216
+
217
+ const largeArc = angle > 180 ? 1 : 0;
218
+
219
+ const pathD = `M ${centerX} ${centerY} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z`;
220
+
221
+ const midAngle = startAngle + angle / 2;
222
+ const midRad = (midAngle * Math.PI) / 180;
223
+ const labelRadius = radius + 25;
224
+ const labelX = centerX + labelRadius * Math.cos(midRad);
225
+ const labelY = centerY + labelRadius * Math.sin(midRad);
226
+
227
+ startAngle = endAngle;
228
+
229
+ return (
230
+ <g key={index} className="group cursor-pointer">
231
+ <path
232
+ d={pathD}
233
+ fill={colors[index % colors.length]}
234
+ className="transition-all duration-200 hover:opacity-80"
235
+ style={{ transformOrigin: `${centerX}px ${centerY}px` }}
236
+ />
237
+ </g>
238
+ );
239
+ })}
240
+
241
+ {/* Center circle for donut effect */}
242
+ <circle
243
+ cx={centerX}
244
+ cy={centerY}
245
+ r={radius * 0.5}
246
+ className={isDark ? 'fill-gray-900' : 'fill-white'}
247
+ />
248
+
249
+ {/* Legend */}
250
+ <g transform={`translate(10, ${centerY * 2 + 20})`}>
251
+ {data.data.map((item, index) => (
252
+ <g key={index} transform={`translate(${index * 90}, 0)`}>
253
+ <rect
254
+ x="0"
255
+ y="0"
256
+ width="12"
257
+ height="12"
258
+ rx="2"
259
+ fill={colors[index % colors.length]}
260
+ />
261
+ <text
262
+ x="18"
263
+ y="10"
264
+ className={`text-[10px] ${isDark ? 'fill-gray-300' : 'fill-gray-600'}`}
265
+ >
266
+ {item.label.length > 8 ? item.label.substring(0, 8) + '..' : item.label}
267
+ </text>
268
+ </g>
269
+ ))}
270
+ </g>
271
+ </svg>
272
+ );
273
+ };
274
+
275
+ const renderChart = () => {
276
+ switch (data.type) {
277
+ case 'bar':
278
+ return renderBarChart();
279
+ case 'line':
280
+ case 'area':
281
+ return renderLineChart();
282
+ case 'pie':
283
+ return renderPieChart();
284
+ default:
285
+ return renderBarChart();
286
+ }
287
+ };
288
+
289
+ return (
290
+ <div className="w-full">
291
+ {/* Chart Title */}
292
+ {data.title && (
293
+ <h4 className="text-center text-sm font-semibold text-gray-700 dark:text-gray-300 mb-4">
294
+ {data.title}
295
+ </h4>
296
+ )}
297
+
298
+ {/* Chart */}
299
+ <div className="relative">
300
+ {renderChart()}
301
+ </div>
302
+
303
+ {/* Axis Labels */}
304
+ {(data.xAxis || data.yAxis) && (
305
+ <div className="flex justify-between mt-2 text-xs text-gray-500 dark:text-gray-400">
306
+ {data.yAxis && <span className="italic">{data.yAxis}</span>}
307
+ {data.xAxis && <span className="italic">{data.xAxis}</span>}
308
+ </div>
309
+ )}
310
+ </div>
311
+ );
312
+ };
313
+
314
+ export default InteractiveChart;
315
+
components/Sidebar.tsx ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { BookOpen, ChevronRight, Loader2, CheckCircle } from 'lucide-react';
3
+ import { BlogSection } from '../types';
4
+
5
+ interface Props {
6
+ sections: BlogSection[];
7
+ activeSection: string;
8
+ onSectionClick: (id: string) => void;
9
+ }
10
+
11
+ const Sidebar: React.FC<Props> = ({ sections, activeSection, onSectionClick }) => {
12
+ const [isCollapsed, setIsCollapsed] = useState(false);
13
+
14
+ const handleClick = (id: string) => {
15
+ onSectionClick(id);
16
+ const element = document.getElementById(`section-${id}`);
17
+ if (element) {
18
+ element.scrollIntoView({ behavior: 'smooth', block: 'start' });
19
+ }
20
+ };
21
+
22
+ return (
23
+ <nav
24
+ className={`
25
+ hidden lg:block fixed left-0 top-24 h-[calc(100vh-8rem)] z-40
26
+ transition-all duration-300 ease-out
27
+ ${isCollapsed ? 'w-16' : 'w-72'}
28
+ `}
29
+ >
30
+ <div className="h-full flex flex-col ml-6">
31
+ {/* Header */}
32
+ <div className="flex items-center justify-between mb-6 pr-4">
33
+ {!isCollapsed && (
34
+ <div className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest text-gray-500 dark:text-gray-400">
35
+ <BookOpen size={14} />
36
+ <span>Contents</span>
37
+ </div>
38
+ )}
39
+ <button
40
+ onClick={() => setIsCollapsed(!isCollapsed)}
41
+ className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
42
+ >
43
+ <ChevronRight
44
+ size={16}
45
+ className={`transition-transform duration-300 ${isCollapsed ? '' : 'rotate-180'}`}
46
+ />
47
+ </button>
48
+ </div>
49
+
50
+ {/* Navigation Items */}
51
+ <div className="flex-1 overflow-y-auto custom-scrollbar pr-4 space-y-1">
52
+ {sections.map((section, index) => {
53
+ const isActive = activeSection === section.id;
54
+ const isLoading = section.isLoading;
55
+ const isComplete = !section.isLoading && section.content && !section.error;
56
+ const hasError = section.error;
57
+
58
+ return (
59
+ <button
60
+ key={section.id}
61
+ onClick={() => !isLoading && handleClick(section.id)}
62
+ disabled={isLoading}
63
+ className={`
64
+ w-full text-left transition-all duration-200 group relative
65
+ ${isCollapsed ? 'px-2 py-3' : 'px-4 py-3'}
66
+ rounded-xl
67
+ ${isLoading ? 'opacity-60 cursor-wait' : ''}
68
+ ${hasError ? 'opacity-70' : ''}
69
+ ${isActive
70
+ ? 'bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300'
71
+ : 'hover:bg-gray-50 dark:hover:bg-gray-800/50 text-gray-600 dark:text-gray-400'
72
+ }
73
+ `}
74
+ >
75
+ {/* Active Indicator */}
76
+ <div
77
+ className={`
78
+ absolute left-0 top-1/2 -translate-y-1/2 w-1 rounded-r-full
79
+ transition-all duration-300
80
+ ${isActive ? 'h-8 bg-brand-500' : 'h-0 bg-transparent'}
81
+ `}
82
+ />
83
+
84
+ {isCollapsed ? (
85
+ <div className={`
86
+ w-8 h-8 rounded-lg flex items-center justify-center text-sm font-bold
87
+ ${isLoading ? 'bg-gray-200 dark:bg-gray-700' : ''}
88
+ ${isActive && !isLoading
89
+ ? 'bg-brand-500 text-white'
90
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-500'
91
+ }
92
+ `}>
93
+ {isLoading ? (
94
+ <Loader2 size={14} className="animate-spin" />
95
+ ) : (
96
+ index + 1
97
+ )}
98
+ </div>
99
+ ) : (
100
+ <div className="flex items-start gap-3">
101
+ <span className={`
102
+ flex-shrink-0 w-6 h-6 rounded-md flex items-center justify-center text-xs font-bold mt-0.5
103
+ ${isLoading ? 'bg-gray-200 dark:bg-gray-700 animate-pulse' : ''}
104
+ ${isActive && !isLoading
105
+ ? 'bg-brand-500 text-white'
106
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-500 group-hover:bg-gray-200 dark:group-hover:bg-gray-700'
107
+ }
108
+ `}>
109
+ {isLoading ? (
110
+ <Loader2 size={10} className="animate-spin" />
111
+ ) : isComplete ? (
112
+ <CheckCircle size={10} className="text-green-500" />
113
+ ) : (
114
+ index + 1
115
+ )}
116
+ </span>
117
+ <span className={`
118
+ text-sm leading-snug transition-colors
119
+ ${isLoading ? 'text-gray-400 dark:text-gray-600' : ''}
120
+ ${isActive && !isLoading ? 'font-semibold' : 'font-medium'}
121
+ `}>
122
+ {section.title}
123
+ </span>
124
+ </div>
125
+ )}
126
+ </button>
127
+ );
128
+ })}
129
+ </div>
130
+
131
+ {/* Progress Indicator */}
132
+ {!isCollapsed && (
133
+ <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-800 pr-4 space-y-3">
134
+ {/* Generation Progress */}
135
+ {sections.some(s => s.isLoading) && (
136
+ <div>
137
+ <div className="flex items-center justify-between text-xs text-brand-600 dark:text-brand-400 mb-2">
138
+ <span className="flex items-center gap-1">
139
+ <Loader2 size={10} className="animate-spin" />
140
+ Generating
141
+ </span>
142
+ <span>
143
+ {sections.filter(s => !s.isLoading).length}/{sections.length}
144
+ </span>
145
+ </div>
146
+ <div className="h-1.5 bg-brand-100 dark:bg-brand-900/30 rounded-full overflow-hidden">
147
+ <div
148
+ className="h-full bg-gradient-to-r from-brand-500 to-purple-500 rounded-full transition-all duration-500"
149
+ style={{
150
+ width: `${(sections.filter(s => !s.isLoading).length / sections.length) * 100}%`
151
+ }}
152
+ />
153
+ </div>
154
+ </div>
155
+ )}
156
+
157
+ {/* Reading Progress */}
158
+ <div>
159
+ <div className="flex items-center justify-between text-xs text-gray-500 mb-2">
160
+ <span>Reading</span>
161
+ <span>
162
+ {sections.findIndex(s => s.id === activeSection) + 1}/{sections.length}
163
+ </span>
164
+ </div>
165
+ <div className="h-1.5 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden">
166
+ <div
167
+ className="h-full bg-gradient-to-r from-gray-400 to-gray-500 dark:from-gray-600 dark:to-gray-500 rounded-full transition-all duration-500"
168
+ style={{
169
+ width: `${((sections.findIndex(s => s.id === activeSection) + 1) / sections.length) * 100}%`
170
+ }}
171
+ />
172
+ </div>
173
+ </div>
174
+ </div>
175
+ )}
176
+ </div>
177
+ </nav>
178
+ );
179
+ };
180
+
181
+ export default Sidebar;
182
+
components/Tooltip.tsx ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+
3
+ interface Props {
4
+ term: string;
5
+ definition: string;
6
+ children: React.ReactNode;
7
+ }
8
+
9
+ const Tooltip: React.FC<Props> = ({ term, definition, children }) => {
10
+ const [isVisible, setIsVisible] = useState(false);
11
+ const [position, setPosition] = useState<'top' | 'bottom'>('top');
12
+ const triggerRef = useRef<HTMLSpanElement>(null);
13
+ const tooltipRef = useRef<HTMLDivElement>(null);
14
+
15
+ useEffect(() => {
16
+ if (isVisible && triggerRef.current && tooltipRef.current) {
17
+ const triggerRect = triggerRef.current.getBoundingClientRect();
18
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
19
+
20
+ // Check if tooltip would go off-screen at the top
21
+ if (triggerRect.top - tooltipRect.height - 12 < 0) {
22
+ setPosition('bottom');
23
+ } else {
24
+ setPosition('top');
25
+ }
26
+ }
27
+ }, [isVisible]);
28
+
29
+ return (
30
+ <span className="relative inline-block">
31
+ <span
32
+ ref={triggerRef}
33
+ onMouseEnter={() => setIsVisible(true)}
34
+ onMouseLeave={() => setIsVisible(false)}
35
+ className="border-b-2 border-dashed border-brand-400 dark:border-brand-500 cursor-help transition-colors hover:border-brand-600 dark:hover:border-brand-400 hover:text-brand-600 dark:hover:text-brand-400"
36
+ >
37
+ {children}
38
+ </span>
39
+
40
+ {isVisible && (
41
+ <div
42
+ ref={tooltipRef}
43
+ className={`
44
+ absolute z-50 w-72 p-4 rounded-xl shadow-2xl
45
+ bg-white dark:bg-gray-900
46
+ border border-gray-200 dark:border-gray-700
47
+ animate-in fade-in zoom-in-95 duration-200
48
+ ${position === 'top'
49
+ ? 'bottom-full mb-3 left-1/2 -translate-x-1/2'
50
+ : 'top-full mt-3 left-1/2 -translate-x-1/2'
51
+ }
52
+ `}
53
+ >
54
+ {/* Arrow */}
55
+ <div
56
+ className={`
57
+ absolute w-3 h-3 bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700
58
+ transform rotate-45
59
+ ${position === 'top'
60
+ ? 'bottom-0 left-1/2 -translate-x-1/2 translate-y-1.5 border-r border-b'
61
+ : 'top-0 left-1/2 -translate-x-1/2 -translate-y-1.5 border-l border-t'
62
+ }
63
+ `}
64
+ />
65
+
66
+ <div className="relative">
67
+ <div className="text-xs font-bold uppercase tracking-wider text-brand-600 dark:text-brand-400 mb-2">
68
+ Definition
69
+ </div>
70
+ <div className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1">
71
+ {term}
72
+ </div>
73
+ <div className="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
74
+ {definition}
75
+ </div>
76
+ </div>
77
+ </div>
78
+ )}
79
+ </span>
80
+ );
81
+ };
82
+
83
+ export default Tooltip;
84
+
index.html CHANGED
@@ -32,14 +32,14 @@
32
  background: #555;
33
  }
34
  .glass-panel {
35
- background: rgba(255, 255, 255, 0.7); /* Increased opacity for light mode */
36
  backdrop-filter: blur(20px);
37
  -webkit-backdrop-filter: blur(20px);
38
  border: 1px solid rgba(255, 255, 255, 0.3);
39
  box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
40
  }
41
  .dark .glass-panel {
42
- background: rgba(15, 23, 42, 0.6); /* Darker background for dark mode contrast */
43
  border: 1px solid rgba(255, 255, 255, 0.1);
44
  box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
45
  }
@@ -50,6 +50,100 @@
50
  -webkit-background-clip: text;
51
  -webkit-text-fill-color: transparent;
52
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  </style>
54
  <script>
55
  tailwind.config = {
 
32
  background: #555;
33
  }
34
  .glass-panel {
35
+ background: rgba(255, 255, 255, 0.7);
36
  backdrop-filter: blur(20px);
37
  -webkit-backdrop-filter: blur(20px);
38
  border: 1px solid rgba(255, 255, 255, 0.3);
39
  box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
40
  }
41
  .dark .glass-panel {
42
+ background: rgba(15, 23, 42, 0.6);
43
  border: 1px solid rgba(255, 255, 255, 0.1);
44
  box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
45
  }
 
50
  -webkit-background-clip: text;
51
  -webkit-text-fill-color: transparent;
52
  }
53
+
54
+ /* Blog View - Prose Enhancements */
55
+ .prose blockquote {
56
+ font-style: normal;
57
+ quotes: none;
58
+ }
59
+ .prose blockquote::before {
60
+ content: none;
61
+ }
62
+
63
+ /* Equation Fraction Styling */
64
+ .frac {
65
+ display: inline-flex;
66
+ flex-direction: column;
67
+ align-items: center;
68
+ vertical-align: middle;
69
+ margin: 0 0.2em;
70
+ }
71
+ .frac > span:first-child {
72
+ border-bottom: 1px solid currentColor;
73
+ padding: 0 0.3em 0.1em;
74
+ }
75
+ .frac > span:last-child {
76
+ padding: 0.1em 0.3em 0;
77
+ }
78
+
79
+ /* Smooth scroll behavior */
80
+ html {
81
+ scroll-behavior: smooth;
82
+ }
83
+
84
+ /* Animation utilities */
85
+ @keyframes fade-in-up {
86
+ from {
87
+ opacity: 0;
88
+ transform: translateY(20px);
89
+ }
90
+ to {
91
+ opacity: 1;
92
+ transform: translateY(0);
93
+ }
94
+ }
95
+
96
+ .animate-fade-in-up {
97
+ animation: fade-in-up 0.6s ease-out forwards;
98
+ }
99
+
100
+ /* Stagger delays */
101
+ .delay-100 { animation-delay: 100ms; }
102
+ .delay-200 { animation-delay: 200ms; }
103
+ .delay-300 { animation-delay: 300ms; }
104
+ .delay-400 { animation-delay: 400ms; }
105
+ .delay-500 { animation-delay: 500ms; }
106
+
107
+ /* Line clamp utilities */
108
+ .line-clamp-4 {
109
+ display: -webkit-box;
110
+ -webkit-line-clamp: 4;
111
+ -webkit-box-orient: vertical;
112
+ overflow: hidden;
113
+ }
114
+ .line-clamp-\[12\] {
115
+ display: -webkit-box;
116
+ -webkit-line-clamp: 12;
117
+ -webkit-box-orient: vertical;
118
+ overflow: hidden;
119
+ }
120
+ .line-clamp-\[20\] {
121
+ display: -webkit-box;
122
+ -webkit-line-clamp: 20;
123
+ -webkit-box-orient: vertical;
124
+ overflow: hidden;
125
+ }
126
+
127
+ /* Mermaid container */
128
+ .mermaid-container svg {
129
+ max-width: 100%;
130
+ height: auto;
131
+ }
132
+
133
+ /* Custom scrollbar for specific containers */
134
+ .custom-scrollbar::-webkit-scrollbar {
135
+ width: 6px;
136
+ }
137
+ .custom-scrollbar::-webkit-scrollbar-track {
138
+ background: transparent;
139
+ }
140
+ .custom-scrollbar::-webkit-scrollbar-thumb {
141
+ background: rgba(0,0,0,0.2);
142
+ border-radius: 3px;
143
+ }
144
+ .dark .custom-scrollbar::-webkit-scrollbar-thumb {
145
+ background: rgba(255,255,255,0.2);
146
+ }
147
  </style>
148
  <script>
149
  tailwind.config = {
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
services/geminiService.ts CHANGED
@@ -1,6 +1,6 @@
1
 
2
  import { GoogleGenAI, Type, Schema } from "@google/genai";
3
- import { BentoCardData, ChatMessage, ModelType } from "../types";
4
 
5
  const RESPONSE_SCHEMA = {
6
  type: Type.ARRAY,
@@ -194,3 +194,415 @@ export const chatWithDocument = async (
194
  const result = await chat.sendMessage({ message: newMessage });
195
  return result.text || "";
196
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
  import { GoogleGenAI, Type, Schema } from "@google/genai";
3
+ import { BentoCardData, BlogSection, ChatMessage, ModelType, ChartData, PaperStructure, SectionPlan } from "../types";
4
 
5
  const RESPONSE_SCHEMA = {
6
  type: Type.ARRAY,
 
194
  const result = await chat.sendMessage({ message: newMessage });
195
  return result.text || "";
196
  };
197
+
198
+ // Paper Structure Analysis Schema
199
+ const PAPER_STRUCTURE_SCHEMA = {
200
+ type: Type.OBJECT,
201
+ properties: {
202
+ paperTitle: { type: Type.STRING, description: "The title of the paper" },
203
+ paperAbstract: { type: Type.STRING, description: "A 2-3 sentence summary of the paper" },
204
+ mainContribution: { type: Type.STRING, description: "The key innovation or finding in one sentence" },
205
+ keyTerms: {
206
+ type: Type.ARRAY,
207
+ items: { type: Type.STRING },
208
+ description: "5-10 important technical terms used throughout the paper"
209
+ },
210
+ sections: {
211
+ type: Type.ARRAY,
212
+ items: {
213
+ type: Type.OBJECT,
214
+ properties: {
215
+ title: { type: Type.STRING, description: "Engaging section title" },
216
+ sectionType: {
217
+ type: Type.STRING,
218
+ enum: ['intro', 'background', 'methodology', 'results', 'analysis', 'applications', 'conclusion', 'custom']
219
+ },
220
+ focusPoints: {
221
+ type: Type.ARRAY,
222
+ items: { type: Type.STRING },
223
+ description: "3-5 key points this section should cover"
224
+ },
225
+ suggestedVisualization: {
226
+ type: Type.STRING,
227
+ enum: ['mermaid', 'chart', 'equation', 'none']
228
+ },
229
+ visualizationHint: {
230
+ type: Type.STRING,
231
+ description: "What specifically should be visualized (e.g., 'architecture diagram from Figure 2', 'accuracy comparison from Table 1')"
232
+ },
233
+ estimatedLength: { type: Type.STRING, enum: ['short', 'medium', 'long'] }
234
+ },
235
+ required: ['title', 'sectionType', 'focusPoints', 'suggestedVisualization', 'estimatedLength']
236
+ }
237
+ }
238
+ },
239
+ required: ['paperTitle', 'paperAbstract', 'mainContribution', 'sections', 'keyTerms']
240
+ };
241
+
242
+ // Single Blog Section Schema
243
+ const SINGLE_SECTION_SCHEMA = {
244
+ type: Type.OBJECT,
245
+ properties: {
246
+ content: { type: Type.STRING, description: "Rich Markdown content (300-500 words). Use **bold**, *italics*, bullet points, numbered lists, and ### subheadings." },
247
+ visualizationType: {
248
+ type: Type.STRING,
249
+ enum: ['mermaid', 'chart', 'equation', 'none']
250
+ },
251
+ visualizationData: {
252
+ type: Type.STRING,
253
+ description: "For 'mermaid': valid Mermaid.js graph. For 'equation': LaTeX-style string. Empty for 'chart' or 'none'."
254
+ },
255
+ chartData: {
256
+ type: Type.OBJECT,
257
+ nullable: true,
258
+ properties: {
259
+ type: { type: Type.STRING, enum: ['bar', 'line', 'pie', 'area'] },
260
+ title: { type: Type.STRING },
261
+ data: {
262
+ type: Type.ARRAY,
263
+ items: {
264
+ type: Type.OBJECT,
265
+ properties: {
266
+ label: { type: Type.STRING },
267
+ value: { type: Type.NUMBER }
268
+ },
269
+ required: ['label', 'value']
270
+ }
271
+ },
272
+ xAxis: { type: Type.STRING },
273
+ yAxis: { type: Type.STRING }
274
+ }
275
+ },
276
+ marginNotes: {
277
+ type: Type.ARRAY,
278
+ items: {
279
+ type: Type.OBJECT,
280
+ properties: {
281
+ text: { type: Type.STRING },
282
+ icon: { type: Type.STRING, enum: ['info', 'warning', 'tip', 'note'] }
283
+ },
284
+ required: ['text']
285
+ }
286
+ },
287
+ technicalTerms: {
288
+ type: Type.ARRAY,
289
+ items: {
290
+ type: Type.OBJECT,
291
+ properties: {
292
+ term: { type: Type.STRING },
293
+ definition: { type: Type.STRING }
294
+ },
295
+ required: ['term', 'definition']
296
+ }
297
+ },
298
+ collapsibleSections: {
299
+ type: Type.ARRAY,
300
+ items: {
301
+ type: Type.OBJECT,
302
+ properties: {
303
+ title: { type: Type.STRING },
304
+ content: { type: Type.STRING }
305
+ },
306
+ required: ['title', 'content']
307
+ }
308
+ }
309
+ },
310
+ required: ['content', 'visualizationType']
311
+ };
312
+
313
+ /**
314
+ * Step 1: Analyze the paper and determine the best structure
315
+ */
316
+ export const analyzePaperStructure = async (
317
+ apiKey: string,
318
+ model: ModelType,
319
+ content: string,
320
+ isPdf: boolean = false,
321
+ useThinking: boolean = false
322
+ ): Promise<PaperStructure> => {
323
+ if (!apiKey) throw new Error("API Key is missing");
324
+
325
+ const ai = new GoogleGenAI({ apiKey });
326
+
327
+ let promptParts: any[] = [];
328
+
329
+ if (isPdf) {
330
+ promptParts.push({
331
+ inlineData: {
332
+ data: content,
333
+ mimeType: "application/pdf",
334
+ },
335
+ });
336
+ promptParts.push({ text: "Carefully analyze this research paper, including all figures, tables, diagrams, and equations." });
337
+ } else {
338
+ promptParts.push({ text: `Analyze this research paper: \n\n${content.substring(0, 50000)}` });
339
+ }
340
+
341
+ const thinkingInstruction = useThinking
342
+ ? "Use deep reasoning to understand the paper's structure, contributions, and the best way to present it to a general technical audience."
343
+ : "";
344
+
345
+ promptParts.push({
346
+ text: `
347
+ ${thinkingInstruction}
348
+
349
+ You are planning a distill.pub-style interactive article. Analyze this paper and create an OPTIMAL STRUCTURE for presenting it.
350
+
351
+ YOUR TASK:
352
+ 1. Identify the paper's core contribution and why it matters
353
+ 2. Determine 5-8 sections that best tell the story of this paper
354
+ 3. For each section, identify what visualization would be most impactful
355
+
356
+ SECTION PLANNING GUIDELINES:
357
+ - Start with a hook that explains WHY this research matters
358
+ - Include background only if essential for understanding
359
+ - The methodology section should explain the "how" clearly
360
+ - Results sections should reference SPECIFIC figures/tables from the paper
361
+ - Include practical applications or implications
362
+ - End with limitations and future directions
363
+
364
+ VISUALIZATION PLANNING:
365
+ - 'mermaid': Use for architectures, pipelines, workflows, decision trees
366
+ - 'chart': Use when there are quantitative comparisons (accuracy, speed, etc.)
367
+ - 'equation': Use for key mathematical formulations
368
+ - 'none': Use for narrative sections
369
+
370
+ Look at the paper's figures and tables - reference them in visualizationHint so we can recreate them!
371
+
372
+ Return a structured JSON plan.
373
+ `
374
+ });
375
+
376
+ let effectiveModel = model;
377
+ let requestConfig: any = {
378
+ responseMimeType: "application/json",
379
+ responseSchema: PAPER_STRUCTURE_SCHEMA as any,
380
+ systemInstruction: "You are a senior science editor who plans engaging, accessible articles from complex research. You identify the narrative arc and visual opportunities in papers.",
381
+ };
382
+
383
+ if (useThinking) {
384
+ effectiveModel = 'gemini-3-pro-preview';
385
+ requestConfig.thinkingConfig = { thinkingBudget: 16384 };
386
+ }
387
+
388
+ try {
389
+ const response = await ai.models.generateContent({
390
+ model: effectiveModel,
391
+ contents: { parts: promptParts },
392
+ config: requestConfig
393
+ });
394
+
395
+ const text = response.text;
396
+ if (!text) throw new Error("No response from Gemini");
397
+
398
+ let jsonStr = text;
399
+ const jsonMatch = text.match(/\{.*\}/s);
400
+ if (jsonMatch) {
401
+ jsonStr = jsonMatch[0];
402
+ }
403
+
404
+ const parsed = JSON.parse(jsonStr);
405
+
406
+ return {
407
+ ...parsed,
408
+ sections: parsed.sections.map((section: any, index: number) => ({
409
+ ...section,
410
+ id: `plan-${index}-${Date.now()}`
411
+ }))
412
+ };
413
+
414
+ } catch (error: any) {
415
+ console.error("Paper Structure Analysis Error:", error);
416
+ throw new Error(error.message || "Failed to analyze paper structure");
417
+ }
418
+ };
419
+
420
+ /**
421
+ * Step 2: Generate a single section based on the plan
422
+ */
423
+ export const generateSingleBlogSection = async (
424
+ apiKey: string,
425
+ model: ModelType,
426
+ content: string,
427
+ sectionPlan: SectionPlan,
428
+ sectionIndex: number,
429
+ totalSections: number,
430
+ paperContext: { title: string; abstract: string; mainContribution: string; keyTerms: string[] },
431
+ isPdf: boolean = false,
432
+ useThinking: boolean = false
433
+ ): Promise<BlogSection> => {
434
+ if (!apiKey) throw new Error("API Key is missing");
435
+
436
+ const ai = new GoogleGenAI({ apiKey });
437
+
438
+ let promptParts: any[] = [];
439
+
440
+ if (isPdf) {
441
+ promptParts.push({
442
+ inlineData: {
443
+ data: content,
444
+ mimeType: "application/pdf",
445
+ },
446
+ });
447
+ } else {
448
+ promptParts.push({ text: `Paper content: \n\n${content.substring(0, 40000)}` });
449
+ }
450
+
451
+ const lengthGuide = {
452
+ 'short': '200-300 words',
453
+ 'medium': '300-450 words',
454
+ 'long': '450-600 words'
455
+ };
456
+
457
+ const sectionTypeGuide: Record<string, string> = {
458
+ 'intro': 'Write an engaging opening that hooks the reader. Explain WHY this research matters to the world. Be provocative and inspiring.',
459
+ 'background': 'Provide essential context. Explain prerequisite concepts clearly. Define technical terms for a smart but non-expert reader.',
460
+ 'methodology': 'Explain HOW it works step by step. Use clear analogies. Break down complex processes into digestible parts.',
461
+ 'results': 'Present the key findings with specifics. Use actual numbers from the paper. Compare to baselines meaningfully.',
462
+ 'analysis': 'Go deeper. Discuss implications, trade-offs, and surprising findings. Address limitations honestly.',
463
+ 'applications': 'Describe real-world use cases. Be concrete about how this could be applied. Include potential impact.',
464
+ 'conclusion': 'Synthesize the key takeaways. Discuss open questions and future directions. End with a forward-looking statement.',
465
+ 'custom': 'Write compelling content following the focus points provided.'
466
+ };
467
+
468
+ promptParts.push({
469
+ text: `
470
+ PAPER CONTEXT:
471
+ - Title: ${paperContext.title}
472
+ - Main Contribution: ${paperContext.mainContribution}
473
+ - Key Terms: ${paperContext.keyTerms.join(', ')}
474
+
475
+ SECTION TO GENERATE (${sectionIndex + 1} of ${totalSections}):
476
+ - Title: "${sectionPlan.title}"
477
+ - Type: ${sectionPlan.sectionType}
478
+ - Focus Points: ${sectionPlan.focusPoints.map((p, i) => `\n ${i + 1}. ${p}`).join('')}
479
+ - Target Length: ${lengthGuide[sectionPlan.estimatedLength]}
480
+ - Visualization: ${sectionPlan.suggestedVisualization}${sectionPlan.visualizationHint ? ` - ${sectionPlan.visualizationHint}` : ''}
481
+
482
+ WRITING INSTRUCTIONS:
483
+ ${sectionTypeGuide[sectionPlan.sectionType]}
484
+
485
+ CONTENT REQUIREMENTS:
486
+ - Write in distill.pub style: accessible yet technically accurate
487
+ - Use Markdown formatting: **bold** for emphasis, bullet points for lists, ### for subheadings
488
+ - Include 1-2 margin notes with helpful asides (use 'tip' for pro tips, 'info' for context, 'warning' for caveats)
489
+ - Define 2-3 technical terms that readers might not know
490
+ - Add 1 collapsible "deep dive" section for readers who want more detail
491
+
492
+ VISUALIZATION REQUIREMENTS:
493
+ ${sectionPlan.suggestedVisualization === 'mermaid' ? `
494
+ Create a Mermaid.js diagram. Use this format:
495
+ - Start with 'graph TD' (top-down) or 'graph LR' (left-right)
496
+ - Use descriptive node labels: A[Input Data] --> B[Process Step]
497
+ - Keep it clear and not too complex (5-10 nodes max)
498
+ ` : ''}
499
+ ${sectionPlan.suggestedVisualization === 'chart' ? `
500
+ Extract ACTUAL data from the paper to create a chart:
501
+ - Use real numbers from tables/figures
502
+ - Choose the right chart type (bar for comparisons, line for trends)
503
+ - Include axis labels
504
+ ` : ''}
505
+ ${sectionPlan.suggestedVisualization === 'equation' ? `
506
+ Write the key equation in LaTeX-style notation:
507
+ - Use \\frac{}{} for fractions
508
+ - Use \\sum, \\prod for summations/products
509
+ - Use Greek letters: \\alpha, \\beta, \\theta, etc.
510
+ ` : ''}
511
+
512
+ Generate the section content now.
513
+ `
514
+ });
515
+
516
+ let effectiveModel = model;
517
+ let requestConfig: any = {
518
+ responseMimeType: "application/json",
519
+ responseSchema: SINGLE_SECTION_SCHEMA as any,
520
+ systemInstruction: "You are an expert science writer for distill.pub. You write clear, engaging, and technically accurate content that makes complex research accessible.",
521
+ };
522
+
523
+ if (useThinking) {
524
+ effectiveModel = 'gemini-3-pro-preview';
525
+ requestConfig.thinkingConfig = { thinkingBudget: 8192 };
526
+ }
527
+
528
+ try {
529
+ const response = await ai.models.generateContent({
530
+ model: effectiveModel,
531
+ contents: { parts: promptParts },
532
+ config: requestConfig
533
+ });
534
+
535
+ const text = response.text;
536
+ if (!text) throw new Error("No response from Gemini");
537
+
538
+ let jsonStr = text;
539
+ const jsonMatch = text.match(/\{.*\}/s);
540
+ if (jsonMatch) {
541
+ jsonStr = jsonMatch[0];
542
+ }
543
+
544
+ const parsed = JSON.parse(jsonStr);
545
+
546
+ return {
547
+ id: sectionPlan.id,
548
+ title: sectionPlan.title,
549
+ content: parsed.content,
550
+ visualizationType: parsed.visualizationType,
551
+ visualizationData: parsed.visualizationData,
552
+ chartData: parsed.chartData,
553
+ marginNotes: (parsed.marginNotes || []).map((note: any, noteIdx: number) => ({
554
+ ...note,
555
+ id: `note-${sectionIndex}-${noteIdx}-${Date.now()}`
556
+ })),
557
+ technicalTerms: parsed.technicalTerms || [],
558
+ collapsibleSections: (parsed.collapsibleSections || []).map((section: any, secIdx: number) => ({
559
+ ...section,
560
+ id: `collapse-${sectionIndex}-${secIdx}-${Date.now()}`
561
+ }))
562
+ };
563
+
564
+ } catch (error: any) {
565
+ console.error(`Section ${sectionIndex + 1} Generation Error:`, error);
566
+ throw new Error(error.message || `Failed to generate section: ${sectionPlan.title}`);
567
+ }
568
+ };
569
+
570
+ /**
571
+ * Legacy function - kept for compatibility but now uses the progressive approach internally
572
+ */
573
+ export const generateBlogContent = async (
574
+ apiKey: string,
575
+ model: ModelType,
576
+ content: string,
577
+ isPdf: boolean = false,
578
+ useThinking: boolean = false
579
+ ): Promise<BlogSection[]> => {
580
+ // First, analyze the structure
581
+ const structure = await analyzePaperStructure(apiKey, model, content, isPdf, useThinking);
582
+
583
+ // Then generate each section
584
+ const sections: BlogSection[] = [];
585
+ const paperContext = {
586
+ title: structure.paperTitle,
587
+ abstract: structure.paperAbstract,
588
+ mainContribution: structure.mainContribution,
589
+ keyTerms: structure.keyTerms
590
+ };
591
+
592
+ for (let i = 0; i < structure.sections.length; i++) {
593
+ const section = await generateSingleBlogSection(
594
+ apiKey,
595
+ model,
596
+ content,
597
+ structure.sections[i],
598
+ i,
599
+ structure.sections.length,
600
+ paperContext,
601
+ isPdf,
602
+ useThinking
603
+ );
604
+ sections.push(section);
605
+ }
606
+
607
+ return sections;
608
+ };
types.ts CHANGED
@@ -35,3 +35,66 @@ export interface ProcessingStatus {
35
  state: 'idle' | 'reading' | 'analyzing' | 'generating' | 'complete' | 'error';
36
  message?: string;
37
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  state: 'idle' | 'reading' | 'analyzing' | 'generating' | 'complete' | 'error';
36
  message?: string;
37
  }
38
+
39
+ // Blog View Types
40
+ export interface BlogSection {
41
+ id: string;
42
+ title: string;
43
+ content: string; // Markdown
44
+ visualizationType?: 'mermaid' | 'chart' | 'equation' | 'none';
45
+ visualizationData?: string;
46
+ chartData?: ChartData;
47
+ marginNotes?: MarginNote[];
48
+ technicalTerms?: TechnicalTerm[];
49
+ collapsibleSections?: CollapsibleContent[];
50
+ isLoading?: boolean;
51
+ error?: string;
52
+ }
53
+
54
+ // Structure plan from paper analysis
55
+ export interface SectionPlan {
56
+ id: string;
57
+ title: string;
58
+ sectionType: 'intro' | 'background' | 'methodology' | 'results' | 'analysis' | 'applications' | 'conclusion' | 'custom';
59
+ focusPoints: string[]; // Key points to cover
60
+ suggestedVisualization: 'mermaid' | 'chart' | 'equation' | 'none';
61
+ visualizationHint?: string; // What to visualize
62
+ estimatedLength: 'short' | 'medium' | 'long';
63
+ }
64
+
65
+ export interface PaperStructure {
66
+ paperTitle: string;
67
+ paperAbstract: string;
68
+ mainContribution: string;
69
+ sections: SectionPlan[];
70
+ keyTerms: string[]; // Important terms across the paper
71
+ }
72
+
73
+ export interface MarginNote {
74
+ id: string;
75
+ text: string;
76
+ icon?: 'info' | 'warning' | 'tip' | 'note';
77
+ }
78
+
79
+ export interface TechnicalTerm {
80
+ term: string;
81
+ definition: string;
82
+ }
83
+
84
+ export interface CollapsibleContent {
85
+ id: string;
86
+ title: string;
87
+ content: string;
88
+ }
89
+
90
+ // Chart Types
91
+ export interface ChartData {
92
+ type: 'bar' | 'line' | 'pie' | 'scatter' | 'area';
93
+ title: string;
94
+ data: Array<{ label: string; value: number; [key: string]: any }>;
95
+ xAxis?: string;
96
+ yAxis?: string;
97
+ colors?: string[];
98
+ }
99
+
100
+ export type ViewMode = 'grid' | 'blog';