Akhil-Theerthala commited on
Commit
57a85c6
·
verified ·
1 Parent(s): ade3003

Upload 26 files

Browse files
App.tsx CHANGED
@@ -1,18 +1,17 @@
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
13
  const [settings, setSettings] = useState<AppSettings>({
14
  apiKey: '',
15
- model: 'gemini-flash-latest',
16
  theme: 'light',
17
  layoutMode: 'auto',
18
  useThinking: false
@@ -28,11 +27,13 @@ const App: React.FC = () => {
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>('');
 
36
 
37
  // Chat
38
  const [isChatOpen, setIsChatOpen] = useState(false);
@@ -40,6 +41,7 @@ const App: React.FC = () => {
40
  const [isChatProcessing, setIsChatProcessing] = useState(false);
41
 
42
  const gridRef = useRef<HTMLDivElement>(null);
 
43
 
44
  const toggleTheme = () => {
45
  const newTheme = settings.theme === 'dark' ? 'light' : 'dark';
@@ -59,7 +61,7 @@ const App: React.FC = () => {
59
 
60
  const handleProcess = async () => {
61
  if (!settings.apiKey) {
62
- setStatus({ state: 'error', message: "Please enter your Gemini API Key." });
63
  return;
64
  }
65
 
@@ -162,7 +164,7 @@ const App: React.FC = () => {
162
  }));
163
  setBlogSections(placeholderSections);
164
 
165
- // Step 2: Generate each section progressively
166
  setBlogLoadingStage('generating');
167
  const paperContextInfo = {
168
  title: structure.paperTitle,
@@ -175,7 +177,7 @@ const App: React.FC = () => {
175
  setCurrentGeneratingSection(i);
176
 
177
  try {
178
- const generatedSection = await generateSingleBlogSection(
179
  settings.apiKey,
180
  settings.model,
181
  paperContext,
@@ -184,7 +186,17 @@ const App: React.FC = () => {
184
  structure.sections.length,
185
  paperContextInfo,
186
  true,
187
- settings.useThinking
 
 
 
 
 
 
 
 
 
 
188
  );
189
 
190
  // Update the specific section in state
@@ -205,6 +217,7 @@ const App: React.FC = () => {
205
  }
206
 
207
  setCurrentGeneratingSection(-1);
 
208
  setBlogLoadingStage('idle');
209
 
210
  } catch (error: any) {
@@ -216,6 +229,59 @@ const App: React.FC = () => {
216
  }
217
  };
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  const handleExpandCard = async (card: BentoCardData) => {
220
  if (card.expandedContent || card.isLoadingDetails) return;
221
 
@@ -254,22 +320,70 @@ const App: React.FC = () => {
254
  };
255
 
256
  const handleExport = async () => {
257
- if (!gridRef.current) return;
 
 
 
 
 
 
 
 
258
  // @ts-ignore
259
- if (window.html2canvas) {
260
- // @ts-ignore
261
- const canvas = await window.html2canvas(gridRef.current, {
262
- backgroundColor: settings.theme === 'dark' ? '#0f172a' : '#f8fafc',
263
- scale: 2,
264
- useCORS: true,
265
- logging: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  const link = document.createElement('a');
268
  link.download = 'bento-summary.png';
269
  link.href = canvas.toDataURL();
270
  link.click();
271
- } else {
272
- alert("Export module not loaded yet.");
273
  }
274
  };
275
 
@@ -355,68 +469,79 @@ const App: React.FC = () => {
355
  <p className="text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-2xl mx-auto font-light">
356
  Upload your research paper (PDF) and instantly transform it into a rich, interactive Bento grid.
357
  <br className="hidden md:block" />
358
- Powered by <span className="font-semibold text-brand-600 dark:text-brand-400">Gemini 3.0 Pro</span>.
359
  </p>
360
  </div>
361
 
362
  <div className="w-full max-w-2xl space-y-6 p-8 glass-panel rounded-3xl shadow-2xl border border-gray-200 dark:border-white/10 backdrop-blur-xl bg-white/80 dark:bg-black/40">
363
- {/* API Key */}
364
- <div className="relative">
365
- <div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
366
- <span className="text-xs font-bold bg-gray-200 dark:bg-gray-800 px-2 py-0.5 rounded text-gray-600 dark:text-gray-400">KEY</span>
367
- </div>
368
- <input
369
- type="password"
370
- placeholder="Enter Gemini API Key"
371
- value={settings.apiKey}
372
- onChange={(e) => setSettings({...settings, apiKey: e.target.value})}
373
- className="w-full bg-white dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-xl py-4 pl-16 pr-4 focus:ring-2 focus:ring-brand-500 outline-none transition-all text-sm font-mono"
374
- />
375
- </div>
 
 
 
 
 
 
 
 
376
 
377
- {/* Model Select & Thinking Toggle */}
378
- <div className="flex flex-col md:flex-row gap-3">
379
- <div className="grid grid-cols-2 gap-3 flex-grow">
380
- <button
381
- onClick={() => setSettings({...settings, model: 'gemini-flash-latest'})}
382
- className={`py-3 px-4 rounded-xl text-sm font-semibold transition-all flex flex-col md:flex-row items-center justify-center gap-2 border ${settings.model === 'gemini-flash-latest' ? 'bg-brand-50 dark:bg-brand-900/20 border-brand-500 text-brand-700 dark:text-brand-400' : 'bg-gray-50 dark:bg-gray-800 border-transparent hover:bg-gray-100 dark:hover:bg-gray-700'}`}
383
- >
384
- <span>⚡ Flash</span>
385
- <span className="text-xs opacity-60 font-normal">(Fast)</span>
386
- </button>
387
- <button
388
- onClick={() => setSettings({...settings, model: 'gemini-3-pro-preview'})}
389
- className={`py-3 px-4 rounded-xl text-sm font-semibold transition-all flex flex-col md:flex-row items-center justify-center gap-2 border ${settings.model === 'gemini-3-pro-preview' ? 'bg-purple-50 dark:bg-purple-900/20 border-purple-500 text-purple-700 dark:text-purple-400' : 'bg-gray-50 dark:bg-gray-800 border-transparent hover:bg-gray-100 dark:hover:bg-gray-700'}`}
390
- >
391
- <span>🧠 Pro</span>
392
- <span className="text-xs opacity-60 font-normal">(Smart)</span>
393
- </button>
394
- </div>
 
 
 
 
395
 
396
- {/* Thinking Toggle Switch */}
397
- <div
398
- className={`
399
- flex items-center justify-between px-4 py-2 rounded-xl border transition-all md:min-w-[180px]
400
- ${settings.useThinking ? 'bg-indigo-50 dark:bg-indigo-900/20 border-indigo-200 dark:border-indigo-800' : 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700'}
401
- `}
402
- >
403
- <div className="flex items-center gap-3 mr-4">
404
- <BrainCircuit size={20} className={settings.useThinking ? 'text-indigo-500 animate-pulse' : 'text-gray-400'} />
405
- <div className="flex flex-col leading-none">
406
- <span className={`text-sm font-bold ${settings.useThinking ? 'text-indigo-700 dark:text-indigo-300' : 'text-gray-600 dark:text-gray-400'}`}>Thinking</span>
407
- <span className="text-[10px] opacity-60">32k Budget</span>
408
- </div>
409
- </div>
410
-
411
- <button
412
- onClick={() => setSettings(s => ({ ...s, useThinking: !s.useThinking }))}
413
- className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${settings.useThinking ? 'bg-indigo-600' : 'bg-gray-300 dark:bg-gray-600'}`}
414
- >
415
- <span
416
- className={`${settings.useThinking ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-200 ease-in-out`}
417
- />
418
- </button>
419
- </div>
420
  </div>
421
 
422
  {/* PDF Upload - Main Action */}
@@ -606,6 +731,10 @@ const App: React.FC = () => {
606
  loadingStage={blogLoadingStage}
607
  currentSection={currentGeneratingSection}
608
  paperStructure={paperStructure}
 
 
 
 
609
  />
610
  )}
611
 
 
 
1
  import React, { useState, useRef } from 'react';
2
+ import { Moon, Sun, Upload, FileText, Download, Share2, MessageSquare, AlertCircle, LayoutGrid, List, Grid, ChevronLeft, ArrowRight, X, BrainCircuit, BookOpen, Layers, Key, ChevronDown } from 'lucide-react';
3
  import Background from './components/Background';
4
  import BentoCard from './components/BentoCard';
5
  import ChatBot from './components/ChatBot';
6
  import BlogView from './components/BlogView';
7
+ import { BentoCardData, BlogSection, ChatMessage, AppSettings, ProcessingStatus, ViewMode, PaperStructure, GeminiModel } from './types';
8
+ import { generateBentoCards, expandBentoCard, chatWithDocument, analyzePaperStructure, generateAndValidateSection, MODEL_INFO } from './services/aiService';
9
 
10
  const App: React.FC = () => {
11
  // Settings
12
  const [settings, setSettings] = useState<AppSettings>({
13
  apiKey: '',
14
+ model: 'gemini-2.5-flash',
15
  theme: 'light',
16
  layoutMode: 'auto',
17
  useThinking: false
 
27
  const [blogSections, setBlogSections] = useState<BlogSection[]>([]);
28
  const [paperStructure, setPaperStructure] = useState<PaperStructure | null>(null);
29
  const [isBlogLoading, setIsBlogLoading] = useState(false);
30
+ const [blogLoadingStage, setBlogLoadingStage] = useState<'idle' | 'analyzing' | 'generating' | 'validating'>('idle');
31
  const [currentGeneratingSection, setCurrentGeneratingSection] = useState<number>(-1);
32
+ const [sectionStatus, setSectionStatus] = useState<string>('');
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>('');
36
+ const [retryingSectionIndex, setRetryingSectionIndex] = useState<number>(-1);
37
 
38
  // Chat
39
  const [isChatOpen, setIsChatOpen] = useState(false);
 
41
  const [isChatProcessing, setIsChatProcessing] = useState(false);
42
 
43
  const gridRef = useRef<HTMLDivElement>(null);
44
+ const blogRef = useRef<HTMLDivElement>(null);
45
 
46
  const toggleTheme = () => {
47
  const newTheme = settings.theme === 'dark' ? 'light' : 'dark';
 
61
 
62
  const handleProcess = async () => {
63
  if (!settings.apiKey) {
64
+ setStatus({ state: 'error', message: 'Please enter your Gemini API Key.' });
65
  return;
66
  }
67
 
 
164
  }));
165
  setBlogSections(placeholderSections);
166
 
167
+ // Step 2: Generate, validate, and repair each section progressively
168
  setBlogLoadingStage('generating');
169
  const paperContextInfo = {
170
  title: structure.paperTitle,
 
177
  setCurrentGeneratingSection(i);
178
 
179
  try {
180
+ const generatedSection = await generateAndValidateSection(
181
  settings.apiKey,
182
  settings.model,
183
  paperContext,
 
186
  structure.sections.length,
187
  paperContextInfo,
188
  true,
189
+ settings.useThinking,
190
+ 2, // max repair attempts
191
+ (stage, message) => {
192
+ // Update UI with current status
193
+ if (stage === 'validating') {
194
+ setBlogLoadingStage('validating');
195
+ } else if (stage === 'generating' || stage === 'repairing') {
196
+ setBlogLoadingStage('generating');
197
+ }
198
+ setSectionStatus(message);
199
+ }
200
  );
201
 
202
  // Update the specific section in state
 
217
  }
218
 
219
  setCurrentGeneratingSection(-1);
220
+ setSectionStatus('');
221
  setBlogLoadingStage('idle');
222
 
223
  } catch (error: any) {
 
229
  }
230
  };
231
 
232
+ // Retry a failed section
233
+ const handleRetrySection = async (sectionIndex: number) => {
234
+ if (!paperStructure || !paperStructure.sections[sectionIndex]) {
235
+ console.error('Cannot retry: missing paper structure or section plan');
236
+ return;
237
+ }
238
+
239
+ setRetryingSectionIndex(sectionIndex);
240
+
241
+ try {
242
+ const sectionPlan = paperStructure.sections[sectionIndex];
243
+ const paperContextInfo = {
244
+ title: paperStructure.paperTitle,
245
+ abstract: paperStructure.paperAbstract,
246
+ mainContribution: paperStructure.mainContribution,
247
+ keyTerms: paperStructure.keyTerms
248
+ };
249
+
250
+ const generatedSection = await generateAndValidateSection(
251
+ settings.apiKey,
252
+ settings.model,
253
+ paperContext,
254
+ sectionPlan,
255
+ sectionIndex,
256
+ paperStructure.sections.length,
257
+ paperContextInfo,
258
+ true,
259
+ settings.useThinking,
260
+ 2,
261
+ (stage, message) => {
262
+ setSectionStatus(message);
263
+ }
264
+ );
265
+
266
+ // Update the section in state
267
+ setBlogSections(prev => prev.map((section, idx) =>
268
+ idx === sectionIndex ? { ...generatedSection, isLoading: false, error: undefined } : section
269
+ ));
270
+ setSectionStatus('');
271
+ } catch (error: any) {
272
+ console.error(`Failed to retry section ${sectionIndex}:`, error);
273
+ setBlogSections(prev => prev.map((section, idx) =>
274
+ idx === sectionIndex ? {
275
+ ...section,
276
+ isLoading: false,
277
+ error: error.message || 'Retry failed. Please try again.'
278
+ } : section
279
+ ));
280
+ } finally {
281
+ setRetryingSectionIndex(-1);
282
+ }
283
+ };
284
+
285
  const handleExpandCard = async (card: BentoCardData) => {
286
  if (card.expandedContent || card.isLoadingDetails) return;
287
 
 
320
  };
321
 
322
  const handleExport = async () => {
323
+ const targetRef = resultViewMode === 'blog' ? blogRef.current : gridRef.current;
324
+ if (!targetRef) return;
325
+
326
+ // @ts-ignore
327
+ if (!window.html2canvas) {
328
+ alert("Export module not loaded yet.");
329
+ return;
330
+ }
331
+
332
  // @ts-ignore
333
+ const canvas = await window.html2canvas(targetRef, {
334
+ backgroundColor: settings.theme === 'dark' ? '#0f172a' : '#f8fafc',
335
+ scale: 2,
336
+ useCORS: true,
337
+ logging: false,
338
+ windowWidth: targetRef.scrollWidth,
339
+ windowHeight: targetRef.scrollHeight,
340
+ });
341
+
342
+ if (resultViewMode === 'blog') {
343
+ // Export as PDF for blog view
344
+ // @ts-ignore
345
+ if (!window.jspdf) {
346
+ alert("PDF export module not loaded yet.");
347
+ return;
348
+ }
349
+ // @ts-ignore
350
+ const { jsPDF } = window.jspdf;
351
+ const imgData = canvas.toDataURL('image/png');
352
+
353
+ // A4 dimensions in mm
354
+ const pdfWidth = 210;
355
+ const pdfHeight = 297;
356
+
357
+ // Calculate the scaled dimensions to fit width
358
+ const ratio = pdfWidth / (canvas.width / 2); // Divide by scale factor (2)
359
+ const scaledHeight = (canvas.height / 2) * ratio;
360
+
361
+ // Create PDF
362
+ const pdf = new jsPDF({
363
+ orientation: 'portrait',
364
+ unit: 'mm',
365
+ format: 'a4'
366
  });
367
+
368
+ // Calculate how many pages we need
369
+ const pageCount = Math.ceil(scaledHeight / pdfHeight);
370
+
371
+ for (let page = 0; page < pageCount; page++) {
372
+ if (page > 0) {
373
+ pdf.addPage();
374
+ }
375
+ // Position the image so the correct portion shows on each page
376
+ const yOffset = -(page * pdfHeight);
377
+ pdf.addImage(imgData, 'PNG', 0, yOffset, pdfWidth, scaledHeight);
378
+ }
379
+
380
+ pdf.save('paper-blog.pdf');
381
+ } else {
382
+ // Export as PNG for grid view
383
  const link = document.createElement('a');
384
  link.download = 'bento-summary.png';
385
  link.href = canvas.toDataURL();
386
  link.click();
 
 
387
  }
388
  };
389
 
 
469
  <p className="text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-2xl mx-auto font-light">
470
  Upload your research paper (PDF) and instantly transform it into a rich, interactive Bento grid.
471
  <br className="hidden md:block" />
472
+ Powered by <span className="font-semibold text-brand-600 dark:text-brand-400">Google Gemini</span>.
473
  </p>
474
  </div>
475
 
476
  <div className="w-full max-w-2xl space-y-6 p-8 glass-panel rounded-3xl shadow-2xl border border-gray-200 dark:border-white/10 backdrop-blur-xl bg-white/80 dark:bg-black/40">
477
+ {/* API Keys & Model Selection */}
478
+ <div className="space-y-6">
479
+ {/* Model Selection Dropdown */}
480
+ <div className="space-y-2">
481
+ <label className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide ml-1">
482
+ Select Model
483
+ </label>
484
+ <div className="relative">
485
+ <select
486
+ value={settings.model}
487
+ onChange={(e) => setSettings({ ...settings, model: e.target.value as GeminiModel })}
488
+ className="w-full appearance-none bg-white dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-xl py-4 pl-4 pr-10 focus:ring-2 focus:ring-brand-500 outline-none transition-all text-sm font-medium text-gray-700 dark:text-gray-200"
489
+ >
490
+ <option value="gemini-2.5-flash">⚡ Gemini Flash (Fastest)</option>
491
+ <option value="gemini-3-pro-preview">🧠 Gemini Pro (Best Reasoning)</option>
492
+ </select>
493
+ <div className="absolute inset-y-0 right-0 flex items-center px-4 pointer-events-none text-gray-500">
494
+ <ChevronDown size={16} />
495
+ </div>
496
+ </div>
497
+ </div>
498
 
499
+ {/* API Key Input */}
500
+ <div className="space-y-2">
501
+ <label className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide ml-1">
502
+ Gemini API Key
503
+ </label>
504
+ <div className="relative">
505
+ <div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
506
+ <Key size={16} className="text-blue-500" />
507
+ </div>
508
+ <input
509
+ type="password"
510
+ placeholder="Enter your Gemini API Key"
511
+ value={settings.apiKey}
512
+ onChange={(e) => setSettings({ ...settings, apiKey: e.target.value })}
513
+ className="w-full bg-white dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-xl py-4 pl-12 pr-4 outline-none transition-all text-sm font-mono focus:ring-2 focus:ring-blue-500"
514
+ />
515
+ </div>
516
+ <p className="text-xs text-gray-400 px-1">
517
+ Get your key from Google AI Studio
518
+ </p>
519
+ </div>
520
+ </div>
521
 
522
+ {/* Thinking Toggle Switch */}
523
+ <div
524
+ className={`
525
+ flex items-center justify-between px-4 py-2 rounded-xl border transition-all
526
+ ${settings.useThinking ? 'bg-indigo-50 dark:bg-indigo-900/20 border-indigo-200 dark:border-indigo-800' : 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700'}
527
+ `}
528
+ >
529
+ <div className="flex items-center gap-3 mr-4">
530
+ <BrainCircuit size={20} className={settings.useThinking ? 'text-indigo-500 animate-pulse' : 'text-gray-400'} />
531
+ <div className="flex flex-col leading-none">
532
+ <span className={`text-sm font-bold ${settings.useThinking ? 'text-indigo-700 dark:text-indigo-300' : 'text-gray-600 dark:text-gray-400'}`}>Thinking</span>
533
+ <span className="text-[10px] opacity-60">32k Budget</span>
534
+ </div>
535
+ </div>
536
+
537
+ <button
538
+ onClick={() => setSettings(s => ({ ...s, useThinking: !s.useThinking }))}
539
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 ${settings.useThinking ? 'bg-indigo-600' : 'bg-gray-300 dark:bg-gray-600'}`}
540
+ >
541
+ <span
542
+ className={`${settings.useThinking ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-200 ease-in-out`}
543
+ />
544
+ </button>
 
545
  </div>
546
 
547
  {/* PDF Upload - Main Action */}
 
731
  loadingStage={blogLoadingStage}
732
  currentSection={currentGeneratingSection}
733
  paperStructure={paperStructure}
734
+ sectionStatus={sectionStatus}
735
+ onRetrySection={handleRetrySection}
736
+ retryingSectionIndex={retryingSectionIndex}
737
+ contentRef={blogRef}
738
  />
739
  )}
740
 
components/BlogSection.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React from 'react';
2
  import ReactMarkdown from 'react-markdown';
3
  import { BlogSection as BlogSectionType } from '../types';
4
  import MermaidDiagram from './MermaidDiagram';
@@ -6,7 +6,7 @@ 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;
@@ -14,6 +14,108 @@ interface Props {
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) {
@@ -42,10 +144,13 @@ const BlogSectionComponent: React.FC<Props> = ({ section, theme, index }) => {
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}>
@@ -71,39 +176,58 @@ const BlogSectionComponent: React.FC<Props> = ({ section, theme, index }) => {
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
  )}
@@ -113,7 +237,9 @@ const BlogSectionComponent: React.FC<Props> = ({ section, theme, index }) => {
113
  )}
114
 
115
  {section.visualizationType === 'equation' && section.visualizationData && (
116
- <EquationBlock equation={section.visualizationData} label={`${index + 1}`} />
 
 
117
  )}
118
  </div>
119
  </div>
@@ -133,6 +259,9 @@ const BlogSectionComponent: React.FC<Props> = ({ section, theme, index }) => {
133
  ))}
134
  </div>
135
  )}
 
 
 
136
  </article>
137
 
138
  {/* Margin Notes */}
 
1
+ import React, { useState } from 'react';
2
  import ReactMarkdown from 'react-markdown';
3
  import { BlogSection as BlogSectionType } from '../types';
4
  import MermaidDiagram from './MermaidDiagram';
 
6
  import EquationBlock from './EquationBlock';
7
  import Collapsible from './Collapsible';
8
  import Tooltip from './Tooltip';
9
+ import { Info, Lightbulb, AlertTriangle, BookOpen, CheckCircle, XCircle, Shield, ChevronDown, Wrench } from 'lucide-react';
10
 
11
  interface Props {
12
  section: BlogSectionType;
 
14
  index: number;
15
  }
16
 
17
+ // Validation Badge Component
18
+ const ValidationBadge: React.FC<{ section: BlogSectionType }> = ({ section }) => {
19
+ const [isExpanded, setIsExpanded] = useState(false);
20
+ const validation = section.validationStatus;
21
+
22
+ if (!validation?.isValidated) return null;
23
+
24
+ const getScoreColor = (score: number) => {
25
+ if (score >= 80) return 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800';
26
+ if (score >= 60) return 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800';
27
+ return 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800';
28
+ };
29
+
30
+ const getScoreIcon = (score: number) => {
31
+ if (score >= 80) return <CheckCircle size={12} />;
32
+ if (score >= 60) return <AlertTriangle size={12} />;
33
+ return <XCircle size={12} />;
34
+ };
35
+
36
+ return (
37
+ <div className="mt-6">
38
+ <button
39
+ onClick={() => setIsExpanded(!isExpanded)}
40
+ className={`
41
+ inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium border transition-all
42
+ ${getScoreColor(validation.overallScore)}
43
+ hover:opacity-80
44
+ `}
45
+ >
46
+ <Shield size={12} />
47
+ <span>Quality Score: {validation.overallScore}/100</span>
48
+ {validation.wasRepaired && (
49
+ <span className="flex items-center gap-1 ml-1 px-1.5 py-0.5 bg-white/50 dark:bg-black/20 rounded">
50
+ <Wrench size={10} />
51
+ Repaired
52
+ </span>
53
+ )}
54
+ <ChevronDown size={12} className={`transition-transform ${isExpanded ? 'rotate-180' : ''}`} />
55
+ </button>
56
+
57
+ {isExpanded && (
58
+ <div className="mt-3 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-top-2 duration-200">
59
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
60
+ {/* Content Relevance */}
61
+ <div className={`p-3 rounded-lg border ${getScoreColor(validation.contentRelevance.score)}`}>
62
+ <div className="flex items-center gap-2 mb-2">
63
+ {getScoreIcon(validation.contentRelevance.score)}
64
+ <span className="font-semibold">Content Relevance</span>
65
+ <span className="ml-auto">{validation.contentRelevance.score}/100</span>
66
+ </div>
67
+ {validation.contentRelevance.issues.length > 0 && (
68
+ <ul className="text-xs space-y-1 opacity-80">
69
+ {validation.contentRelevance.issues.slice(0, 3).map((issue, i) => (
70
+ <li key={i} className="flex items-start gap-1">
71
+ <span className="mt-1">•</span>
72
+ <span>{issue}</span>
73
+ </li>
74
+ ))}
75
+ </ul>
76
+ )}
77
+ {validation.contentRelevance.passed && validation.contentRelevance.issues.length === 0 && (
78
+ <p className="text-xs opacity-80">✓ Content verified against source paper</p>
79
+ )}
80
+ </div>
81
+
82
+ {/* Visualization Validity */}
83
+ <div className={`p-3 rounded-lg border ${getScoreColor(validation.visualizationValidity.score)}`}>
84
+ <div className="flex items-center gap-2 mb-2">
85
+ {getScoreIcon(validation.visualizationValidity.score)}
86
+ <span className="font-semibold">Visualization</span>
87
+ <span className="ml-auto">{validation.visualizationValidity.score}/100</span>
88
+ </div>
89
+ {validation.visualizationValidity.issues.length > 0 && (
90
+ <ul className="text-xs space-y-1 opacity-80">
91
+ {validation.visualizationValidity.issues.slice(0, 3).map((issue, i) => (
92
+ <li key={i} className="flex items-start gap-1">
93
+ <span className="mt-1">•</span>
94
+ <span>{issue}</span>
95
+ </li>
96
+ ))}
97
+ </ul>
98
+ )}
99
+ {validation.visualizationValidity.passed && validation.visualizationValidity.issues.length === 0 && (
100
+ <p className="text-xs opacity-80">✓ Visualization syntax valid</p>
101
+ )}
102
+ </div>
103
+ </div>
104
+
105
+ {validation.wasRepaired && (
106
+ <div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
107
+ <p className="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1">
108
+ <Wrench size={12} />
109
+ This section was automatically repaired ({validation.repairAttempts} attempt{validation.repairAttempts !== 1 ? 's' : ''})
110
+ </p>
111
+ </div>
112
+ )}
113
+ </div>
114
+ )}
115
+ </div>
116
+ );
117
+ };
118
+
119
  const BlogSectionComponent: React.FC<Props> = ({ section, theme, index }) => {
120
  const getMarginNoteIcon = (icon?: 'info' | 'warning' | 'tip' | 'note') => {
121
  switch (icon) {
 
144
 
145
  {/* Technical Terms Legend */}
146
  {section.technicalTerms.length > 0 && (
147
+ <div className="mt-8 p-6 bg-gradient-to-br from-gray-50 to-white dark:from-gray-800/50 dark:to-gray-900/30 rounded-2xl border border-gray-200 dark:border-gray-700">
148
+ <div className="flex items-center gap-2 mb-4">
149
+ <div className="w-1 h-4 bg-brand-500 rounded-full"></div>
150
+ <h5 className="text-sm font-bold uppercase tracking-wider text-gray-600 dark:text-gray-400">
151
+ Key Terms
152
+ </h5>
153
+ </div>
154
  <div className="flex flex-wrap gap-2">
155
  {section.technicalTerms.map((term, idx) => (
156
  <Tooltip key={idx} term={term.term} definition={term.definition}>
 
176
  {/* Main Content */}
177
  <article className="flex-1 min-w-0">
178
  {/* Section Header */}
179
+ <header className="mb-12">
180
+ <div className="flex items-start gap-6 mb-6">
181
+ <span className="flex-shrink-0 w-12 h-12 rounded-2xl bg-gradient-to-br from-brand-500 to-purple-600 flex items-center justify-center text-white font-bold text-xl shadow-lg shadow-brand-500/20 mt-1">
182
  {index + 1}
183
  </span>
184
+ <div className="flex-1">
185
+ <h2 className="text-3xl md:text-4xl font-display font-bold text-gray-900 dark:text-gray-50 leading-tight mb-4 tracking-tight">
186
+ {section.title}
187
+ </h2>
188
+ <div className="h-1 w-24 bg-gradient-to-r from-brand-500 to-purple-500 rounded-full opacity-80" />
189
+ </div>
190
  </div>
 
191
  </header>
192
 
193
  {/* Content */}
194
  <div className="prose prose-lg dark:prose-invert max-w-none
195
+ prose-headings:font-display prose-headings:font-bold prose-headings:tracking-tight
196
+ prose-h3:text-2xl prose-h3:mt-10 prose-h3:mb-4 prose-h3:text-gray-900 prose-h3:dark:text-gray-100
197
+ prose-h4:text-xl prose-h4:mt-8 prose-h4:mb-3 prose-h4:text-gray-800 prose-h4:dark:text-gray-200
198
+ prose-p:font-serif prose-p:text-[1.125rem] prose-p:text-gray-700 prose-p:dark:text-gray-300 prose-p:leading-[1.85] prose-p:mb-6
199
+ prose-strong:font-semibold prose-strong:text-gray-900 prose-strong:dark:text-white
200
+ prose-em:text-gray-700 prose-em:dark:text-gray-300
201
+ prose-a:text-brand-600 prose-a:dark:text-brand-400 prose-a:no-underline prose-a:border-b prose-a:border-brand-300 prose-a:dark:border-brand-700 hover:prose-a:border-brand-500 hover:prose-a:dark:border-brand-400 prose-a:transition-colors
202
+ prose-blockquote:border-l-4 prose-blockquote:border-brand-500 prose-blockquote:bg-gradient-to-r prose-blockquote:from-brand-50 prose-blockquote:to-transparent prose-blockquote:dark:from-brand-900/20 prose-blockquote:dark:to-transparent prose-blockquote:py-4 prose-blockquote:px-6 prose-blockquote:my-8 prose-blockquote:rounded-r-xl prose-blockquote:font-serif prose-blockquote:italic prose-blockquote:text-xl prose-blockquote:leading-relaxed prose-blockquote:text-gray-700 prose-blockquote:dark:text-gray-300
203
+ prose-code:font-mono prose-code:bg-gray-100 prose-code:dark:bg-gray-800 prose-code:px-2 prose-code:py-1 prose-code:rounded-md prose-code:text-sm prose-code:text-brand-600 prose-code:dark:text-brand-400 prose-code:before:content-none prose-code:after:content-none prose-code:font-medium
204
+ prose-pre:bg-gray-900 prose-pre:dark:bg-black prose-pre:border prose-pre:border-gray-800 prose-pre:rounded-xl prose-pre:shadow-lg prose-pre:my-8
205
+ prose-li:font-serif prose-li:text-[1.1rem] prose-li:text-gray-700 prose-li:dark:text-gray-300 prose-li:leading-relaxed prose-li:my-2
206
+ prose-ul:my-6 prose-ul:pl-0
207
+ prose-ol:my-6 prose-ol:pl-0
208
+ prose-li:marker:text-brand-500 prose-li:marker:dark:text-brand-400
209
+ prose-img:rounded-2xl prose-img:shadow-xl prose-img:border prose-img:border-gray-200 prose-img:dark:border-gray-800 prose-img:my-10
210
+ prose-hr:my-12 prose-hr:border-gray-200 prose-hr:dark:border-gray-800
211
  ">
212
  {renderContent()}
213
  </div>
214
 
215
  {/* Visualization */}
216
  {section.visualizationType && section.visualizationType !== 'none' && (
217
+ <div className="my-12">
218
+ <div className="p-8 rounded-2xl bg-gradient-to-br from-white to-gray-50 dark:from-gray-900 dark:to-gray-900/50 border border-gray-200 dark:border-gray-700 shadow-lg shadow-gray-200/50 dark:shadow-none overflow-hidden">
219
+ {/* Visualization Header */}
220
+ <div className="flex items-center gap-2 mb-6 pb-4 border-b border-gray-100 dark:border-gray-800">
221
+ <div className="w-2 h-2 rounded-full bg-brand-500"></div>
222
+ <span className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
223
+ {section.visualizationType === 'mermaid' ? 'Diagram' :
224
+ section.visualizationType === 'chart' ? 'Data Visualization' :
225
+ section.visualizationType === 'equation' ? 'Mathematical Formulation' : 'Visual'}
226
+ </span>
227
+ </div>
228
+
229
  {section.visualizationType === 'mermaid' && section.visualizationData && (
230
+ <div className="min-h-[200px] flex items-center justify-center">
231
  <MermaidDiagram chart={section.visualizationData} theme={theme} />
232
  </div>
233
  )}
 
237
  )}
238
 
239
  {section.visualizationType === 'equation' && section.visualizationData && (
240
+ <div className="py-4">
241
+ <EquationBlock equation={section.visualizationData} label={`${index + 1}`} />
242
+ </div>
243
  )}
244
  </div>
245
  </div>
 
259
  ))}
260
  </div>
261
  )}
262
+
263
+ {/* Validation Badge */}
264
+ <ValidationBadge section={section} />
265
  </article>
266
 
267
  {/* Margin Notes */}
components/BlogView.tsx CHANGED
@@ -2,108 +2,142 @@ 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>
@@ -123,9 +157,13 @@ interface Props {
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> = ({
@@ -137,11 +175,16 @@ const BlogView: React.FC<Props> = ({
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);
@@ -212,53 +255,83 @@ const BlogView: React.FC<Props> = ({
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>
@@ -268,85 +341,84 @@ const BlogView: React.FC<Props> = ({
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
 
@@ -359,9 +431,16 @@ const BlogView: React.FC<Props> = ({
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}
 
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, RotateCcw } from 'lucide-react';
6
 
7
  // Loading placeholder for sections being generated
8
+ const SectionLoadingPlaceholder: React.FC<{
9
+ title: string;
10
+ index: number;
11
+ isCurrentlyGenerating: boolean;
12
+ statusMessage?: string;
13
+ }> = ({
14
  title,
15
  index,
16
+ isCurrentlyGenerating,
17
+ statusMessage
18
  }) => (
19
+ <section className="relative scroll-mt-32 animate-in fade-in duration-500 mb-24">
20
+ <div className="flex flex-col md:flex-row gap-8">
21
  <article className="flex-1 min-w-0">
22
+ <header className="mb-12">
23
+ <div className="flex items-start gap-6 mb-6">
24
  <span className={`
25
+ flex-shrink-0 w-12 h-12 rounded-2xl flex items-center justify-center text-white font-bold text-xl shadow-lg
26
  ${isCurrentlyGenerating
27
+ ? 'bg-gradient-to-br from-brand-500 to-purple-600 animate-pulse shadow-brand-500/20'
28
+ : 'bg-gray-200 dark:bg-gray-800 text-gray-400 dark:text-gray-600'
29
  }
30
  `}>
31
  {isCurrentlyGenerating ? (
32
+ <Loader2 size={24} className="animate-spin" />
33
  ) : (
34
  index + 1
35
  )}
36
  </span>
37
+ <div className="flex-1 pt-1">
38
+ <h2 className={`text-3xl md:text-4xl font-display font-bold leading-tight mb-4 ${
39
+ isCurrentlyGenerating ? 'text-gray-900 dark:text-gray-50' : 'text-gray-300 dark:text-gray-700'
40
+ }`}>
41
+ {title}
42
+ </h2>
43
+ <div className={`h-1 rounded-full max-w-[100px] ${
44
+ isCurrentlyGenerating
45
+ ? 'bg-gradient-to-r from-brand-500 to-purple-500 animate-pulse'
46
+ : 'bg-gray-100 dark:bg-gray-800'
47
+ }`} />
48
+ </div>
49
  </div>
 
 
 
 
 
50
  </header>
51
 
52
  {/* Loading skeleton */}
53
+ <div className="space-y-8 pl-0 md:pl-[4.5rem]">
54
  {isCurrentlyGenerating ? (
55
  <>
56
+ <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">
57
+ <Loader2 size={16} className="animate-spin" />
58
+ <span className="font-mono">{statusMessage || 'Generating content...'}</span>
59
  </div>
60
+
61
+ <div className="space-y-4 max-w-3xl">
62
  <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-full animate-pulse" />
63
+ <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-[98%] animate-pulse delay-75" />
64
+ <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-[95%] animate-pulse delay-100" />
65
+ <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-[90%] animate-pulse delay-150" />
66
+ <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg w-[92%] animate-pulse delay-200" />
67
  </div>
68
+
69
+ <div className="mt-8 p-8 rounded-2xl bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800">
70
+ <div className="h-40 bg-gray-200 dark:bg-gray-800 rounded-xl animate-pulse" />
71
  </div>
72
  </>
73
  ) : (
74
+ <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">
75
+ <p className="text-gray-400 dark:text-gray-600 font-medium">
76
+ Waiting to be analyzed...
77
  </p>
78
  </div>
79
  )}
80
  </div>
81
  </article>
82
  </div>
 
 
 
 
 
83
  </section>
84
  );
85
 
86
  // Error state for failed sections
87
+ const SectionErrorState: React.FC<{
88
+ title: string;
89
+ error: string;
90
+ index: number;
91
+ onRetry?: () => void;
92
+ isRetrying?: boolean;
93
+ }> = ({ title, error, index, onRetry, isRetrying }) => (
94
+ <section className="relative scroll-mt-32 mb-24">
95
  <div className="flex gap-8">
96
  <article className="flex-1 min-w-0">
97
  <header className="mb-8">
98
+ <div className="flex items-start gap-6 mb-4">
99
+ <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">
100
+ {isRetrying ? <Loader2 size={24} className="animate-spin" /> : <AlertCircle size={24} />}
101
  </span>
102
+ <div className="flex-1">
103
+ <h2 className="text-3xl md:text-4xl font-display font-bold text-gray-900 dark:text-gray-50 leading-tight mb-2">
104
+ {title}
105
+ </h2>
106
+ <div className="h-1 w-20 bg-red-500/30 rounded-full" />
107
+ </div>
108
  </div>
109
  </header>
110
 
111
+ <div className="p-6 rounded-2xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
112
+ <div className="flex items-start gap-4">
113
+ <AlertCircle size={24} className="text-red-500 flex-shrink-0 mt-0.5" />
114
+ <div className="flex-1">
115
+ <p className="font-semibold text-red-700 dark:text-red-300 text-lg">
116
  Failed to generate this section
117
  </p>
118
+ <p className="mt-2 text-sm text-red-600 dark:text-red-400 leading-relaxed">
119
  {error}
120
  </p>
121
+
122
+ {onRetry && (
123
+ <button
124
+ onClick={onRetry}
125
+ disabled={isRetrying}
126
+ 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"
127
+ >
128
+ {isRetrying ? (
129
+ <>
130
+ <Loader2 size={16} className="animate-spin" />
131
+ <span>Retrying...</span>
132
+ </>
133
+ ) : (
134
+ <>
135
+ <RotateCcw size={16} />
136
+ <span>Try Again</span>
137
+ </>
138
+ )}
139
+ </button>
140
+ )}
141
  </div>
142
  </div>
143
  </div>
 
157
  onExport: () => void;
158
  onShare: () => void;
159
  isLoading?: boolean;
160
+ loadingStage?: 'idle' | 'analyzing' | 'generating' | 'validating';
161
  currentSection?: number;
162
  paperStructure?: PaperStructure | null;
163
+ sectionStatus?: string;
164
+ onRetrySection?: (sectionIndex: number) => Promise<void>;
165
+ retryingSectionIndex?: number;
166
+ contentRef?: React.RefObject<HTMLDivElement>;
167
  }
168
 
169
  const BlogView: React.FC<Props> = ({
 
175
  isLoading = false,
176
  loadingStage = 'idle',
177
  currentSection = -1,
178
+ paperStructure = null,
179
+ sectionStatus = '',
180
+ onRetrySection,
181
+ retryingSectionIndex = -1,
182
+ contentRef
183
  }) => {
184
  const [activeSection, setActiveSection] = useState<string>(sections[0]?.id || '');
185
  const [readProgress, setReadProgress] = useState(0);
186
+ const internalContentRef = useRef<HTMLDivElement>(null);
187
+ const effectiveContentRef = contentRef || internalContentRef;
188
 
189
  // Calculate reading time (rough estimate: 200 words per minute)
190
  const completedSections = sections.filter(s => !s.isLoading && s.content);
 
255
 
256
  {/* Main Content */}
257
  <div className="lg:ml-72 xl:mr-8">
258
+ <div ref={effectiveContentRef} className="max-w-4xl mx-auto px-6 py-12 md:py-20">
259
 
260
  {/* Loading State - Paper Analysis */}
261
  {loadingStage === 'analyzing' && (
262
  <div className="flex flex-col items-center justify-center min-h-[60vh] animate-in fade-in duration-500">
263
  <div className="relative">
264
+ <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">
265
  <Sparkles size={40} className="text-white animate-bounce" />
266
  </div>
267
  <div className="absolute inset-0 rounded-full bg-gradient-to-r from-brand-500 to-purple-600 animate-ping opacity-20" />
268
  </div>
269
+ <h2 className="mt-8 text-3xl font-display font-bold text-gray-900 dark:text-white text-center">
270
+ Deconstructing Research
271
  </h2>
272
+ <p className="mt-4 text-lg text-gray-500 dark:text-gray-400 text-center max-w-md font-light">
273
+ Analyzing the paper's structure to craft a comprehensive narrative...
274
  </p>
275
  </div>
276
  )}
277
 
278
  {/* Generation Progress Banner */}
279
+ {(loadingStage === 'generating' || loadingStage === 'validating') && (
280
+ <div className={`mb-12 p-6 rounded-2xl border animate-in slide-in-from-top duration-500 backdrop-blur-sm ${
281
+ loadingStage === 'validating'
282
+ ? 'bg-amber-50/80 dark:bg-amber-900/10 border-amber-200 dark:border-amber-800'
283
+ : 'bg-brand-50/80 dark:bg-brand-900/10 border-brand-200 dark:border-brand-800'
284
+ }`}>
285
+ <div className="flex items-center gap-5">
286
  <div className="flex-shrink-0">
287
+ <div className={`w-12 h-12 rounded-xl flex items-center justify-center shadow-sm ${
288
+ loadingStage === 'validating' ? 'bg-amber-100 dark:bg-amber-900/30 text-amber-600' : 'bg-brand-100 dark:bg-brand-900/30 text-brand-600'
289
+ }`}>
290
+ {loadingStage === 'validating' ? (
291
+ <CheckCircle2 size={24} className="animate-pulse" />
292
+ ) : (
293
+ <Loader2 size={24} className="animate-spin" />
294
+ )}
295
  </div>
296
  </div>
297
  <div className="flex-1 min-w-0">
298
  <div className="flex items-center justify-between mb-2">
299
+ <span className={`text-base font-semibold ${
300
+ loadingStage === 'validating'
301
+ ? 'text-amber-800 dark:text-amber-300'
302
+ : 'text-brand-800 dark:text-brand-300'
303
+ }`}>
304
+ {loadingStage === 'validating' ? 'Validating scientific accuracy...' : 'Generating insights...'}
305
  </span>
306
+ <span className={`text-sm font-medium font-mono ${
307
+ loadingStage === 'validating'
308
+ ? 'text-amber-600 dark:text-amber-400'
309
+ : 'text-brand-600 dark:text-brand-400'
310
+ }`}>
311
  {completedCount} / {sections.length}
312
  </span>
313
  </div>
314
+ <div className={`h-2 rounded-full overflow-hidden ${
315
+ loadingStage === 'validating'
316
+ ? 'bg-amber-200 dark:bg-amber-900/30'
317
+ : 'bg-brand-200 dark:bg-brand-900/30'
318
+ }`}>
319
  <div
320
+ className={`h-full rounded-full transition-all duration-500 ${
321
+ loadingStage === 'validating'
322
+ ? 'bg-gradient-to-r from-amber-500 to-orange-500'
323
+ : 'bg-gradient-to-r from-brand-500 to-purple-500'
324
+ }`}
325
  style={{ width: `${(completedCount / sections.length) * 100}%` }}
326
  />
327
  </div>
328
+ {sectionStatus ? (
329
+ <p className="mt-2 text-sm text-gray-600 dark:text-gray-400 font-mono">
330
+ {sectionStatus}
331
+ </p>
332
+ ) : currentSection >= 0 && sections[currentSection] && (
333
+ <p className="mt-2 text-sm text-gray-600 dark:text-gray-400 truncate">
334
+ Currently analyzing: <span className="font-medium text-gray-900 dark:text-gray-200">{sections[currentSection].title}</span>
335
  </p>
336
  )}
337
  </div>
 
341
 
342
  {/* Article Header */}
343
  {(sections.length > 0 || paperStructure) && (
344
+ <header className="mb-20 animate-in fade-in slide-in-from-bottom-8 duration-700">
345
  {/* Paper Badge */}
346
+ <div className="flex items-center gap-3 mb-8">
347
+ <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">
348
  <FileText size={12} />
349
+ Research Paper
350
+ </span>
351
+ <span className="h-px w-8 bg-gray-200 dark:bg-gray-700"></span>
352
+ <span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
353
+ {new Date().toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}
354
  </span>
 
 
 
 
 
 
355
  </div>
356
 
357
  {/* Title */}
358
+ <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">
359
  {(paperStructure?.paperTitle || paperTitle).replace('.pdf', '')}
360
  </h1>
361
 
362
  {/* Abstract Preview */}
363
  {paperStructure?.paperAbstract && (
364
+ <div className="relative">
365
+ <div className="absolute left-0 top-0 bottom-0 w-1 bg-brand-500 rounded-full opacity-30"></div>
366
+ <p className="text-xl md:text-2xl text-gray-600 dark:text-gray-300 leading-relaxed pl-8 font-serif italic text-opacity-90">
367
+ {paperStructure.paperAbstract}
368
+ </p>
369
+ </div>
370
  )}
371
 
372
+ {/* Meta Info & Actions */}
373
+ <div className="mt-12 flex flex-wrap items-center justify-between gap-6 pt-8 border-t border-gray-100 dark:border-gray-800">
374
+ <div className="flex items-center gap-8 text-sm font-medium text-gray-500 dark:text-gray-400">
375
+ <div className="flex items-center gap-2">
376
+ <Clock size={18} className="text-brand-500" />
377
+ <span>{readingTime} min read</span>
378
+ </div>
379
+ <div className="flex items-center gap-2">
380
+ <BookOpen size={18} className="text-purple-500" />
381
+ <span>{sections.length} sections</span>
382
+ </div>
383
  </div>
384
+
385
  <div className="flex items-center gap-2">
 
 
 
 
 
 
 
 
 
 
386
  <button
387
  onClick={onShare}
388
+ 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"
 
389
  >
390
+ <Share2 size={16} />
391
+ Share
392
  </button>
393
  <button
394
  onClick={onExport}
395
+ 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"
 
396
  >
397
+ <Download size={16} />
398
+ Export
399
  </button>
400
  </div>
401
  </div>
 
 
 
 
 
 
402
  </header>
403
  )}
404
 
405
  {/* Key Contribution Highlight */}
406
  {paperStructure?.mainContribution && (
407
+ <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">
408
+ <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>
409
+ <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>
410
+
411
+ <div className="relative z-10">
412
+ <div className="flex items-center gap-3 mb-6">
413
+ <div className="p-2 rounded-lg bg-white/10 backdrop-blur-md border border-white/10">
414
+ <Sparkles size={20} className="text-brand-300" />
415
+ </div>
416
+ <span className="text-sm font-bold uppercase tracking-widest text-brand-200">Core Contribution</span>
417
+ </div>
418
+ <p className="text-2xl md:text-3xl leading-relaxed font-display font-medium text-slate-50">
419
+ {paperStructure.mainContribution}
420
+ </p>
421
  </div>
 
 
 
422
  </div>
423
  )}
424
 
 
431
  title={section.title}
432
  index={index}
433
  isCurrentlyGenerating={index === currentSection}
434
+ statusMessage={index === currentSection ? sectionStatus : undefined}
435
  />
436
  ) : section.error ? (
437
+ <SectionErrorState
438
+ title={section.title}
439
+ error={section.error}
440
+ index={index}
441
+ onRetry={onRetrySection ? () => onRetrySection(index) : undefined}
442
+ isRetrying={retryingSectionIndex === index}
443
+ />
444
  ) : (
445
  <BlogSectionComponent
446
  section={section}
components/InteractiveChart.tsx CHANGED
@@ -1,5 +1,6 @@
1
  import React from 'react';
2
  import { ChartData } from '../types';
 
3
 
4
  interface Props {
5
  data: ChartData;
@@ -10,6 +11,25 @@ interface Props {
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
 
1
  import React from 'react';
2
  import { ChartData } from '../types';
3
+ import { AlertTriangle } from 'lucide-react';
4
 
5
  interface Props {
6
  data: ChartData;
 
11
  const InteractiveChart: React.FC<Props> = ({ data, theme }) => {
12
  const isDark = theme === 'dark';
13
 
14
+ // Validate data
15
+ if (!data || !data.data || data.data.length === 0) {
16
+ return (
17
+ <div className="w-full p-6 rounded-xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800">
18
+ <div className="flex items-start gap-3">
19
+ <AlertTriangle size={20} className="text-amber-500 flex-shrink-0 mt-0.5" />
20
+ <div>
21
+ <p className="font-semibold text-amber-700 dark:text-amber-300 text-sm">
22
+ No chart data available
23
+ </p>
24
+ <p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
25
+ The chart could not be rendered because no data was provided.
26
+ </p>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ );
31
+ }
32
+
33
  // Default colors palette
34
  const defaultColors = [
35
  '#0ea5e9', // brand blue
components/MermaidDiagram.tsx CHANGED
@@ -1,6 +1,7 @@
1
 
2
  import React, { useEffect, useRef, useState } from 'react';
3
  import mermaid from 'mermaid';
 
4
 
5
  interface Props {
6
  chart: string;
@@ -10,6 +11,7 @@ interface Props {
10
  const MermaidDiagram: React.FC<Props> = ({ chart, theme = 'light' }) => {
11
  const containerRef = useRef<HTMLDivElement>(null);
12
  const [svg, setSvg] = useState<string>('');
 
13
 
14
  useEffect(() => {
15
  mermaid.initialize({
@@ -24,25 +26,68 @@ const MermaidDiagram: React.FC<Props> = ({ chart, theme = 'light' }) => {
24
  const renderChart = async () => {
25
  if (!containerRef.current) return;
26
 
 
 
27
  try {
 
 
 
 
 
 
28
  const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`;
29
  // Mermaid render returns an object with svg property in newer versions
30
- const { svg } = await mermaid.render(id, chart);
31
  setSvg(svg);
32
- } catch (error) {
33
- console.error('Mermaid failed to render:', error);
34
- setSvg(''); // Clear on error
 
35
  }
36
  };
37
 
38
- renderChart();
 
 
39
  }, [chart, theme]);
40
 
41
- if (!svg) return null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
  return (
44
  <div
45
- className="w-full h-full flex items-center justify-center p-4 mermaid-container overflow-hidden"
46
  ref={containerRef}
47
  dangerouslySetInnerHTML={{ __html: svg }}
48
  />
 
1
 
2
  import React, { useEffect, useRef, useState } from 'react';
3
  import mermaid from 'mermaid';
4
+ import { AlertTriangle } from 'lucide-react';
5
 
6
  interface Props {
7
  chart: string;
 
11
  const MermaidDiagram: React.FC<Props> = ({ chart, theme = 'light' }) => {
12
  const containerRef = useRef<HTMLDivElement>(null);
13
  const [svg, setSvg] = useState<string>('');
14
+ const [error, setError] = useState<string>('');
15
 
16
  useEffect(() => {
17
  mermaid.initialize({
 
26
  const renderChart = async () => {
27
  if (!containerRef.current) return;
28
 
29
+ setError('');
30
+
31
  try {
32
+ // Clean up the chart string - remove any markdown code fences
33
+ let cleanChart = chart.trim();
34
+ if (cleanChart.startsWith('```')) {
35
+ cleanChart = cleanChart.replace(/^```(?:mermaid)?\n?/, '').replace(/\n?```$/, '');
36
+ }
37
+
38
  const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`;
39
  // Mermaid render returns an object with svg property in newer versions
40
+ const { svg } = await mermaid.render(id, cleanChart);
41
  setSvg(svg);
42
+ } catch (err: any) {
43
+ console.error('Mermaid failed to render:', err);
44
+ setError(err.message || 'Failed to render diagram');
45
+ setSvg('');
46
  }
47
  };
48
 
49
+ if (chart) {
50
+ renderChart();
51
+ }
52
  }, [chart, theme]);
53
 
54
+ if (error) {
55
+ return (
56
+ <div className="w-full p-6 rounded-xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800">
57
+ <div className="flex items-start gap-3">
58
+ <AlertTriangle size={20} className="text-amber-500 flex-shrink-0 mt-0.5" />
59
+ <div>
60
+ <p className="font-semibold text-amber-700 dark:text-amber-300 text-sm">
61
+ Diagram could not be rendered
62
+ </p>
63
+ <p className="mt-1 text-xs text-amber-600 dark:text-amber-400 font-mono">
64
+ {error}
65
+ </p>
66
+ <details className="mt-3">
67
+ <summary className="text-xs text-amber-600 dark:text-amber-400 cursor-pointer hover:underline">
68
+ Show raw diagram code
69
+ </summary>
70
+ <pre className="mt-2 p-3 bg-white dark:bg-gray-900 rounded-lg text-xs overflow-auto max-h-40 border border-amber-100 dark:border-amber-900">
71
+ {chart}
72
+ </pre>
73
+ </details>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ if (!svg) {
81
+ return (
82
+ <div className="w-full h-40 flex items-center justify-center">
83
+ <div className="w-8 h-8 border-2 border-gray-300 border-t-brand-500 rounded-full animate-spin" />
84
+ </div>
85
+ );
86
+ }
87
 
88
  return (
89
  <div
90
+ className="w-full h-full flex items-center justify-center p-4 mermaid-container overflow-hidden [&_svg]:max-w-full [&_svg]:h-auto"
91
  ref={containerRef}
92
  dangerouslySetInnerHTML={{ __html: svg }}
93
  />
components/Sidebar.tsx CHANGED
@@ -83,7 +83,7 @@ const Sidebar: React.FC<Props> = ({ sections, activeSection, onSectionClick }) =
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'
@@ -95,11 +95,18 @@ const Sidebar: React.FC<Props> = ({ sections, activeSection, onSectionClick }) =
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'
@@ -114,13 +121,29 @@ const Sidebar: React.FC<Props> = ({ sections, activeSection, onSectionClick }) =
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>
 
83
 
84
  {isCollapsed ? (
85
  <div className={`
86
+ w-8 h-8 rounded-lg flex items-center justify-center text-sm font-bold relative
87
  ${isLoading ? 'bg-gray-200 dark:bg-gray-700' : ''}
88
  ${isActive && !isLoading
89
  ? 'bg-brand-500 text-white'
 
95
  ) : (
96
  index + 1
97
  )}
98
+ {/* Validation indicator dot */}
99
+ {section.validationStatus?.isValidated && (
100
+ <span className={`absolute -top-1 -right-1 w-2.5 h-2.5 rounded-full border-2 border-white dark:border-gray-900 ${
101
+ section.validationStatus.overallScore >= 80 ? 'bg-green-500' :
102
+ section.validationStatus.overallScore >= 60 ? 'bg-amber-500' : 'bg-red-500'
103
+ }`} />
104
+ )}
105
  </div>
106
  ) : (
107
  <div className="flex items-start gap-3">
108
  <span className={`
109
+ flex-shrink-0 w-6 h-6 rounded-md flex items-center justify-center text-xs font-bold mt-0.5 relative
110
  ${isLoading ? 'bg-gray-200 dark:bg-gray-700 animate-pulse' : ''}
111
  ${isActive && !isLoading
112
  ? 'bg-brand-500 text-white'
 
121
  index + 1
122
  )}
123
  </span>
124
+ <div className="flex-1 min-w-0">
125
+ <span className={`
126
+ text-sm leading-snug transition-colors block
127
+ ${isLoading ? 'text-gray-400 dark:text-gray-600' : ''}
128
+ ${isActive && !isLoading ? 'font-semibold' : 'font-medium'}
129
+ `}>
130
+ {section.title}
131
+ </span>
132
+ {/* Validation score badge */}
133
+ {section.validationStatus?.isValidated && (
134
+ <span className={`inline-flex items-center gap-1 text-[10px] mt-0.5 px-1.5 py-0.5 rounded ${
135
+ section.validationStatus.overallScore >= 80
136
+ ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400'
137
+ : section.validationStatus.overallScore >= 60
138
+ ? 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400'
139
+ : 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400'
140
+ }`}>
141
+ {section.validationStatus.overallScore >= 80 ? '✓' : section.validationStatus.overallScore >= 60 ? '!' : '✗'}
142
+ {section.validationStatus.overallScore}%
143
+ {section.validationStatus.wasRepaired && ' 🔧'}
144
+ </span>
145
+ )}
146
+ </div>
147
  </div>
148
  )}
149
  </button>
index.css ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Base Typography Improvements */
6
+ body {
7
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
8
+ font-feature-settings: 'cv11', 'ss01';
9
+ -webkit-font-smoothing: antialiased;
10
+ -moz-osx-font-smoothing: grayscale;
11
+ }
12
+
13
+ /* Improved serif font for content */
14
+ .font-serif {
15
+ font-family: 'Georgia', 'Times New Roman', serif;
16
+ font-feature-settings: 'liga', 'kern';
17
+ }
18
+
19
+ /* Glass Panel Effect */
20
+ .glass-panel {
21
+ background: rgba(255, 255, 255, 0.85);
22
+ backdrop-filter: blur(20px) saturate(180%);
23
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
24
+ border: 1px solid rgba(255, 255, 255, 0.3);
25
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.08);
26
+ }
27
+
28
+ .dark .glass-panel {
29
+ background: rgba(15, 23, 42, 0.75);
30
+ border: 1px solid rgba(255, 255, 255, 0.08);
31
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.4);
32
+ }
33
+
34
+ /* Smooth Focus States */
35
+ *:focus-visible {
36
+ outline: 2px solid rgba(99, 102, 241, 0.5);
37
+ outline-offset: 2px;
38
+ }
39
+
40
+ /* Better Text Selection */
41
+ ::selection {
42
+ background-color: rgba(99, 102, 241, 0.3);
43
+ }
44
+
45
+ .dark ::selection {
46
+ background-color: rgba(99, 102, 241, 0.4);
47
+ }
48
+
49
+ /* Improved Scrollbar */
50
+ ::-webkit-scrollbar {
51
+ width: 8px;
52
+ height: 8px;
53
+ }
54
+
55
+ ::-webkit-scrollbar-track {
56
+ background: transparent;
57
+ }
58
+
59
+ ::-webkit-scrollbar-thumb {
60
+ background: rgba(156, 163, 175, 0.4);
61
+ border-radius: 4px;
62
+ }
63
+
64
+ ::-webkit-scrollbar-thumb:hover {
65
+ background: rgba(156, 163, 175, 0.6);
66
+ }
67
+
68
+ .dark ::-webkit-scrollbar-thumb {
69
+ background: rgba(75, 85, 99, 0.5);
70
+ }
71
+
72
+ .dark ::-webkit-scrollbar-thumb:hover {
73
+ background: rgba(75, 85, 99, 0.7);
74
+ }
75
+
76
+ /* Blog Content Improvements */
77
+ .prose {
78
+ --tw-prose-bullets: theme('colors.brand.500');
79
+ --tw-prose-counters: theme('colors.brand.600');
80
+ }
81
+
82
+ .dark .prose {
83
+ --tw-prose-invert-bullets: theme('colors.brand.400');
84
+ --tw-prose-invert-counters: theme('colors.brand.400');
85
+ }
86
+
87
+ /* List styling for better readability */
88
+ .prose ul > li::marker {
89
+ color: var(--tw-prose-bullets);
90
+ }
91
+
92
+ .prose ol > li::marker {
93
+ color: var(--tw-prose-counters);
94
+ font-weight: 600;
95
+ }
96
+
97
+ /* Blockquote improvements */
98
+ .prose blockquote {
99
+ position: relative;
100
+ quotes: none;
101
+ }
102
+
103
+ .prose blockquote::before {
104
+ content: '"';
105
+ position: absolute;
106
+ left: -0.5rem;
107
+ top: -0.5rem;
108
+ font-size: 3rem;
109
+ color: theme('colors.brand.200');
110
+ font-family: Georgia, serif;
111
+ line-height: 1;
112
+ }
113
+
114
+ .dark .prose blockquote::before {
115
+ color: theme('colors.brand.800');
116
+ }
117
+
118
+ /* Smooth animations */
119
+ @keyframes fade-in-up {
120
+ from {
121
+ opacity: 0;
122
+ transform: translateY(10px);
123
+ }
124
+ to {
125
+ opacity: 1;
126
+ transform: translateY(0);
127
+ }
128
+ }
129
+
130
+ .animate-fade-in-up {
131
+ animation: fade-in-up 0.5s ease-out;
132
+ }
133
+
134
+ /* Pulse effect for loading states */
135
+ @keyframes gentle-pulse {
136
+ 0%, 100% {
137
+ opacity: 1;
138
+ }
139
+ 50% {
140
+ opacity: 0.7;
141
+ }
142
+ }
143
+
144
+ .animate-gentle-pulse {
145
+ animation: gentle-pulse 2s ease-in-out infinite;
146
+ }
147
+
148
+ /* Better link hover effect */
149
+ .prose a {
150
+ transition: all 0.2s ease;
151
+ }
152
+
153
+ /* Code block improvements */
154
+ .prose pre {
155
+ position: relative;
156
+ }
157
+
158
+ .prose pre code {
159
+ font-size: 0.875rem;
160
+ line-height: 1.7;
161
+ }
162
+
163
+ /* Improved heading spacing in blog content */
164
+ .prose h2 {
165
+ margin-top: 2.5em;
166
+ margin-bottom: 0.75em;
167
+ }
168
+
169
+ .prose h3 {
170
+ margin-top: 2em;
171
+ margin-bottom: 0.5em;
172
+ }
173
+
174
+ .prose h4 {
175
+ margin-top: 1.5em;
176
+ margin-bottom: 0.5em;
177
+ }
178
+
179
+ /* First paragraph after heading - no margin top */
180
+ .prose h2 + p,
181
+ .prose h3 + p,
182
+ .prose h4 + p {
183
+ margin-top: 0;
184
+ }
185
+
186
+ /* Better paragraph spacing */
187
+ .prose p + p {
188
+ margin-top: 1.25em;
189
+ }
190
+
191
+ /* Responsive improvements */
192
+ @media (max-width: 768px) {
193
+ .prose {
194
+ font-size: 1rem;
195
+ }
196
+
197
+ .prose blockquote {
198
+ padding-left: 1rem;
199
+ padding-right: 1rem;
200
+ }
201
+
202
+ .prose blockquote::before {
203
+ font-size: 2rem;
204
+ left: -0.25rem;
205
+ }
206
+ }
207
+
208
+ /* Print styles */
209
+ @media print {
210
+ .glass-panel {
211
+ background: white;
212
+ backdrop-filter: none;
213
+ box-shadow: none;
214
+ border: 1px solid #e5e7eb;
215
+ }
216
+
217
+ nav, aside, .no-print {
218
+ display: none !important;
219
+ }
220
+ }
index.html CHANGED
@@ -6,9 +6,10 @@
6
  <title>PaperStack</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
 
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
12
  <style>
13
  body {
14
  font-family: 'Inter', sans-serif;
@@ -16,6 +17,12 @@
16
  h1, h2, h3, h4, h5, h6, .font-display {
17
  font-family: 'Space Grotesk', sans-serif;
18
  }
 
 
 
 
 
 
19
  /* Custom Scrollbar */
20
  ::-webkit-scrollbar {
21
  width: 8px;
@@ -150,15 +157,70 @@
150
  darkMode: 'class',
151
  theme: {
152
  extend: {
 
 
 
 
 
 
153
  colors: {
154
  brand: {
155
- 50: '#f0f9ff',
156
- 100: '#e0f2fe',
157
- 500: '#0ea5e9',
158
- 600: '#0284c7',
159
- 900: '#0c4a6e',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  }
161
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  animation: {
163
  'spin-slow': 'spin 20s linear infinite',
164
  'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite',
 
6
  <title>PaperStack</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
10
  <link rel="preconnect" href="https://fonts.googleapis.com">
11
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700;1,900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
13
  <style>
14
  body {
15
  font-family: 'Inter', sans-serif;
 
17
  h1, h2, h3, h4, h5, h6, .font-display {
18
  font-family: 'Space Grotesk', sans-serif;
19
  }
20
+ .font-serif {
21
+ font-family: 'Merriweather', serif;
22
+ }
23
+ .font-mono {
24
+ font-family: 'JetBrains Mono', monospace;
25
+ }
26
  /* Custom Scrollbar */
27
  ::-webkit-scrollbar {
28
  width: 8px;
 
157
  darkMode: 'class',
158
  theme: {
159
  extend: {
160
+ fontFamily: {
161
+ sans: ['Inter', 'sans-serif'],
162
+ serif: ['Merriweather', 'serif'],
163
+ display: ['Space Grotesk', 'sans-serif'],
164
+ mono: ['JetBrains Mono', 'monospace'],
165
+ },
166
  colors: {
167
  brand: {
168
+ 50: '#eff6ff',
169
+ 100: '#dbeafe',
170
+ 200: '#bfdbfe',
171
+ 300: '#93c5fd',
172
+ 400: '#60a5fa',
173
+ 500: '#3b82f6', // Blue-500
174
+ 600: '#2563eb',
175
+ 700: '#1d4ed8',
176
+ 800: '#1e40af',
177
+ 900: '#1e3a8a',
178
+ 950: '#172554',
179
+ },
180
+ gray: {
181
+ // Cool grays for a cleaner look
182
+ 50: '#f9fafb',
183
+ 100: '#f3f4f6',
184
+ 200: '#e5e7eb',
185
+ 300: '#d1d5db',
186
+ 400: '#9ca3af',
187
+ 500: '#6b7280',
188
+ 600: '#4b5563',
189
+ 700: '#374151',
190
+ 800: '#1f2937',
191
+ 900: '#111827',
192
+ 950: '#030712',
193
  }
194
  },
195
+ typography: (theme) => ({
196
+ DEFAULT: {
197
+ css: {
198
+ color: theme('colors.gray.700'),
199
+ fontFamily: theme('fontFamily.serif'),
200
+ a: {
201
+ color: theme('colors.brand.600'),
202
+ '&:hover': {
203
+ color: theme('colors.brand.700'),
204
+ },
205
+ },
206
+ h1: { fontFamily: theme('fontFamily.display') },
207
+ h2: { fontFamily: theme('fontFamily.display') },
208
+ h3: { fontFamily: theme('fontFamily.display') },
209
+ h4: { fontFamily: theme('fontFamily.display') },
210
+ },
211
+ },
212
+ dark: {
213
+ css: {
214
+ color: theme('colors.gray.300'),
215
+ a: {
216
+ color: theme('colors.brand.400'),
217
+ '&:hover': {
218
+ color: theme('colors.brand.300'),
219
+ },
220
+ },
221
+ }
222
+ }
223
+ }),
224
  animation: {
225
  'spin-slow': 'spin 20s linear infinite',
226
  'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite',
package-lock.json CHANGED
@@ -11,6 +11,7 @@
11
  "@google/genai": "^1.30.0",
12
  "lucide-react": "^0.554.0",
13
  "mermaid": "11.4.0",
 
14
  "react": "^19.2.0",
15
  "react-dom": "^19.2.0",
16
  "react-markdown": "^10.1.0"
@@ -1622,12 +1623,21 @@
1622
  "version": "22.19.1",
1623
  "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
1624
  "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
1625
- "dev": true,
1626
  "license": "MIT",
1627
  "dependencies": {
1628
  "undici-types": "~6.21.0"
1629
  }
1630
  },
 
 
 
 
 
 
 
 
 
 
1631
  "node_modules/@types/react": {
1632
  "version": "19.2.7",
1633
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
@@ -1677,6 +1687,18 @@
1677
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1678
  }
1679
  },
 
 
 
 
 
 
 
 
 
 
 
 
1680
  "node_modules/acorn": {
1681
  "version": "8.15.0",
1682
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -1698,6 +1720,18 @@
1698
  "node": ">= 14"
1699
  }
1700
  },
 
 
 
 
 
 
 
 
 
 
 
 
1701
  "node_modules/ansi-regex": {
1702
  "version": "6.2.2",
1703
  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -1722,6 +1756,12 @@
1722
  "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1723
  }
1724
  },
 
 
 
 
 
 
1725
  "node_modules/bail": {
1726
  "version": "2.0.2",
1727
  "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@@ -1826,6 +1866,19 @@
1826
  "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
1827
  "license": "BSD-3-Clause"
1828
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
1829
  "node_modules/caniuse-lite": {
1830
  "version": "1.0.30001757",
1831
  "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
@@ -1941,6 +1994,18 @@
1941
  "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1942
  "license": "MIT"
1943
  },
 
 
 
 
 
 
 
 
 
 
 
 
1944
  "node_modules/comma-separated-tokens": {
1945
  "version": "2.0.3",
1946
  "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@@ -2556,6 +2621,15 @@
2556
  "robust-predicates": "^3.0.2"
2557
  }
2558
  },
 
 
 
 
 
 
 
 
 
2559
  "node_modules/dequal": {
2560
  "version": "2.0.3",
2561
  "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -2584,6 +2658,20 @@
2584
  "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==",
2585
  "license": "(MPL-2.0 OR Apache-2.0)"
2586
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2587
  "node_modules/eastasianwidth": {
2588
  "version": "0.2.0",
2589
  "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -2612,6 +2700,51 @@
2612
  "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
2613
  "license": "MIT"
2614
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2615
  "node_modules/esbuild": {
2616
  "version": "0.25.12",
2617
  "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -2674,6 +2807,15 @@
2674
  "url": "https://opencollective.com/unified"
2675
  }
2676
  },
 
 
 
 
 
 
 
 
 
2677
  "node_modules/exsolve": {
2678
  "version": "1.0.8",
2679
  "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
@@ -2743,6 +2885,50 @@
2743
  "url": "https://github.com/sponsors/isaacs"
2744
  }
2745
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2746
  "node_modules/formdata-polyfill": {
2747
  "version": "4.0.10",
2748
  "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -2770,6 +2956,15 @@
2770
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
2771
  }
2772
  },
 
 
 
 
 
 
 
 
 
2773
  "node_modules/gaxios": {
2774
  "version": "7.1.3",
2775
  "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
@@ -2809,6 +3004,43 @@
2809
  "node": ">=6.9.0"
2810
  }
2811
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2812
  "node_modules/glob": {
2813
  "version": "10.5.0",
2814
  "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
@@ -2868,6 +3100,18 @@
2868
  "node": ">=14"
2869
  }
2870
  },
 
 
 
 
 
 
 
 
 
 
 
 
2871
  "node_modules/gtoken": {
2872
  "version": "8.0.0",
2873
  "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
@@ -2887,6 +3131,45 @@
2887
  "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
2888
  "license": "MIT"
2889
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2890
  "node_modules/hast-util-to-jsx-runtime": {
2891
  "version": "2.3.6",
2892
  "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@@ -2950,6 +3233,15 @@
2950
  "node": ">= 14"
2951
  }
2952
  },
 
 
 
 
 
 
 
 
 
2953
  "node_modules/iconv-lite": {
2954
  "version": "0.6.3",
2955
  "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -3248,6 +3540,15 @@
3248
  "node": ">= 18"
3249
  }
3250
  },
 
 
 
 
 
 
 
 
 
3251
  "node_modules/mdast-util-from-markdown": {
3252
  "version": "2.0.2",
3253
  "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
@@ -3872,6 +4173,27 @@
3872
  ],
3873
  "license": "MIT"
3874
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3875
  "node_modules/minimatch": {
3876
  "version": "9.0.5",
3877
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -3995,6 +4317,71 @@
3995
  "dev": true,
3996
  "license": "MIT"
3997
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3998
  "node_modules/package-json-from-dist": {
3999
  "version": "1.0.1",
4000
  "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -4604,6 +4991,12 @@
4604
  "url": "https://github.com/sponsors/SuperchupuDev"
4605
  }
4606
  },
 
 
 
 
 
 
4607
  "node_modules/trim-lines": {
4608
  "version": "3.0.1",
4609
  "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -4657,7 +5050,6 @@
4657
  "version": "6.21.0",
4658
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
4659
  "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
4660
- "dev": true,
4661
  "license": "MIT"
4662
  },
4663
  "node_modules/unified": {
@@ -4952,6 +5344,22 @@
4952
  "node": ">= 8"
4953
  }
4954
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4955
  "node_modules/which": {
4956
  "version": "2.0.2",
4957
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
 
11
  "@google/genai": "^1.30.0",
12
  "lucide-react": "^0.554.0",
13
  "mermaid": "11.4.0",
14
+ "openai": "^4.77.0",
15
  "react": "^19.2.0",
16
  "react-dom": "^19.2.0",
17
  "react-markdown": "^10.1.0"
 
1623
  "version": "22.19.1",
1624
  "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
1625
  "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
 
1626
  "license": "MIT",
1627
  "dependencies": {
1628
  "undici-types": "~6.21.0"
1629
  }
1630
  },
1631
+ "node_modules/@types/node-fetch": {
1632
+ "version": "2.6.13",
1633
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
1634
+ "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
1635
+ "license": "MIT",
1636
+ "dependencies": {
1637
+ "@types/node": "*",
1638
+ "form-data": "^4.0.4"
1639
+ }
1640
+ },
1641
  "node_modules/@types/react": {
1642
  "version": "19.2.7",
1643
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
 
1687
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1688
  }
1689
  },
1690
+ "node_modules/abort-controller": {
1691
+ "version": "3.0.0",
1692
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
1693
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
1694
+ "license": "MIT",
1695
+ "dependencies": {
1696
+ "event-target-shim": "^5.0.0"
1697
+ },
1698
+ "engines": {
1699
+ "node": ">=6.5"
1700
+ }
1701
+ },
1702
  "node_modules/acorn": {
1703
  "version": "8.15.0",
1704
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
 
1720
  "node": ">= 14"
1721
  }
1722
  },
1723
+ "node_modules/agentkeepalive": {
1724
+ "version": "4.6.0",
1725
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
1726
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
1727
+ "license": "MIT",
1728
+ "dependencies": {
1729
+ "humanize-ms": "^1.2.1"
1730
+ },
1731
+ "engines": {
1732
+ "node": ">= 8.0.0"
1733
+ }
1734
+ },
1735
  "node_modules/ansi-regex": {
1736
  "version": "6.2.2",
1737
  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
 
1756
  "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1757
  }
1758
  },
1759
+ "node_modules/asynckit": {
1760
+ "version": "0.4.0",
1761
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
1762
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
1763
+ "license": "MIT"
1764
+ },
1765
  "node_modules/bail": {
1766
  "version": "2.0.2",
1767
  "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
 
1866
  "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
1867
  "license": "BSD-3-Clause"
1868
  },
1869
+ "node_modules/call-bind-apply-helpers": {
1870
+ "version": "1.0.2",
1871
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
1872
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
1873
+ "license": "MIT",
1874
+ "dependencies": {
1875
+ "es-errors": "^1.3.0",
1876
+ "function-bind": "^1.1.2"
1877
+ },
1878
+ "engines": {
1879
+ "node": ">= 0.4"
1880
+ }
1881
+ },
1882
  "node_modules/caniuse-lite": {
1883
  "version": "1.0.30001757",
1884
  "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
 
1994
  "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1995
  "license": "MIT"
1996
  },
1997
+ "node_modules/combined-stream": {
1998
+ "version": "1.0.8",
1999
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
2000
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
2001
+ "license": "MIT",
2002
+ "dependencies": {
2003
+ "delayed-stream": "~1.0.0"
2004
+ },
2005
+ "engines": {
2006
+ "node": ">= 0.8"
2007
+ }
2008
+ },
2009
  "node_modules/comma-separated-tokens": {
2010
  "version": "2.0.3",
2011
  "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
 
2621
  "robust-predicates": "^3.0.2"
2622
  }
2623
  },
2624
+ "node_modules/delayed-stream": {
2625
+ "version": "1.0.0",
2626
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
2627
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
2628
+ "license": "MIT",
2629
+ "engines": {
2630
+ "node": ">=0.4.0"
2631
+ }
2632
+ },
2633
  "node_modules/dequal": {
2634
  "version": "2.0.3",
2635
  "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
 
2658
  "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==",
2659
  "license": "(MPL-2.0 OR Apache-2.0)"
2660
  },
2661
+ "node_modules/dunder-proto": {
2662
+ "version": "1.0.1",
2663
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
2664
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
2665
+ "license": "MIT",
2666
+ "dependencies": {
2667
+ "call-bind-apply-helpers": "^1.0.1",
2668
+ "es-errors": "^1.3.0",
2669
+ "gopd": "^1.2.0"
2670
+ },
2671
+ "engines": {
2672
+ "node": ">= 0.4"
2673
+ }
2674
+ },
2675
  "node_modules/eastasianwidth": {
2676
  "version": "0.2.0",
2677
  "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
 
2700
  "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
2701
  "license": "MIT"
2702
  },
2703
+ "node_modules/es-define-property": {
2704
+ "version": "1.0.1",
2705
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
2706
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
2707
+ "license": "MIT",
2708
+ "engines": {
2709
+ "node": ">= 0.4"
2710
+ }
2711
+ },
2712
+ "node_modules/es-errors": {
2713
+ "version": "1.3.0",
2714
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
2715
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
2716
+ "license": "MIT",
2717
+ "engines": {
2718
+ "node": ">= 0.4"
2719
+ }
2720
+ },
2721
+ "node_modules/es-object-atoms": {
2722
+ "version": "1.1.1",
2723
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
2724
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
2725
+ "license": "MIT",
2726
+ "dependencies": {
2727
+ "es-errors": "^1.3.0"
2728
+ },
2729
+ "engines": {
2730
+ "node": ">= 0.4"
2731
+ }
2732
+ },
2733
+ "node_modules/es-set-tostringtag": {
2734
+ "version": "2.1.0",
2735
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
2736
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
2737
+ "license": "MIT",
2738
+ "dependencies": {
2739
+ "es-errors": "^1.3.0",
2740
+ "get-intrinsic": "^1.2.6",
2741
+ "has-tostringtag": "^1.0.2",
2742
+ "hasown": "^2.0.2"
2743
+ },
2744
+ "engines": {
2745
+ "node": ">= 0.4"
2746
+ }
2747
+ },
2748
  "node_modules/esbuild": {
2749
  "version": "0.25.12",
2750
  "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
 
2807
  "url": "https://opencollective.com/unified"
2808
  }
2809
  },
2810
+ "node_modules/event-target-shim": {
2811
+ "version": "5.0.1",
2812
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
2813
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
2814
+ "license": "MIT",
2815
+ "engines": {
2816
+ "node": ">=6"
2817
+ }
2818
+ },
2819
  "node_modules/exsolve": {
2820
  "version": "1.0.8",
2821
  "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
 
2885
  "url": "https://github.com/sponsors/isaacs"
2886
  }
2887
  },
2888
+ "node_modules/form-data": {
2889
+ "version": "4.0.5",
2890
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
2891
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
2892
+ "license": "MIT",
2893
+ "dependencies": {
2894
+ "asynckit": "^0.4.0",
2895
+ "combined-stream": "^1.0.8",
2896
+ "es-set-tostringtag": "^2.1.0",
2897
+ "hasown": "^2.0.2",
2898
+ "mime-types": "^2.1.12"
2899
+ },
2900
+ "engines": {
2901
+ "node": ">= 6"
2902
+ }
2903
+ },
2904
+ "node_modules/form-data-encoder": {
2905
+ "version": "1.7.2",
2906
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
2907
+ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
2908
+ "license": "MIT"
2909
+ },
2910
+ "node_modules/formdata-node": {
2911
+ "version": "4.4.1",
2912
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
2913
+ "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
2914
+ "license": "MIT",
2915
+ "dependencies": {
2916
+ "node-domexception": "1.0.0",
2917
+ "web-streams-polyfill": "4.0.0-beta.3"
2918
+ },
2919
+ "engines": {
2920
+ "node": ">= 12.20"
2921
+ }
2922
+ },
2923
+ "node_modules/formdata-node/node_modules/web-streams-polyfill": {
2924
+ "version": "4.0.0-beta.3",
2925
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
2926
+ "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
2927
+ "license": "MIT",
2928
+ "engines": {
2929
+ "node": ">= 14"
2930
+ }
2931
+ },
2932
  "node_modules/formdata-polyfill": {
2933
  "version": "4.0.10",
2934
  "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
 
2956
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
2957
  }
2958
  },
2959
+ "node_modules/function-bind": {
2960
+ "version": "1.1.2",
2961
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
2962
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
2963
+ "license": "MIT",
2964
+ "funding": {
2965
+ "url": "https://github.com/sponsors/ljharb"
2966
+ }
2967
+ },
2968
  "node_modules/gaxios": {
2969
  "version": "7.1.3",
2970
  "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
 
3004
  "node": ">=6.9.0"
3005
  }
3006
  },
3007
+ "node_modules/get-intrinsic": {
3008
+ "version": "1.3.0",
3009
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
3010
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
3011
+ "license": "MIT",
3012
+ "dependencies": {
3013
+ "call-bind-apply-helpers": "^1.0.2",
3014
+ "es-define-property": "^1.0.1",
3015
+ "es-errors": "^1.3.0",
3016
+ "es-object-atoms": "^1.1.1",
3017
+ "function-bind": "^1.1.2",
3018
+ "get-proto": "^1.0.1",
3019
+ "gopd": "^1.2.0",
3020
+ "has-symbols": "^1.1.0",
3021
+ "hasown": "^2.0.2",
3022
+ "math-intrinsics": "^1.1.0"
3023
+ },
3024
+ "engines": {
3025
+ "node": ">= 0.4"
3026
+ },
3027
+ "funding": {
3028
+ "url": "https://github.com/sponsors/ljharb"
3029
+ }
3030
+ },
3031
+ "node_modules/get-proto": {
3032
+ "version": "1.0.1",
3033
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
3034
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
3035
+ "license": "MIT",
3036
+ "dependencies": {
3037
+ "dunder-proto": "^1.0.1",
3038
+ "es-object-atoms": "^1.0.0"
3039
+ },
3040
+ "engines": {
3041
+ "node": ">= 0.4"
3042
+ }
3043
+ },
3044
  "node_modules/glob": {
3045
  "version": "10.5.0",
3046
  "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
 
3100
  "node": ">=14"
3101
  }
3102
  },
3103
+ "node_modules/gopd": {
3104
+ "version": "1.2.0",
3105
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
3106
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
3107
+ "license": "MIT",
3108
+ "engines": {
3109
+ "node": ">= 0.4"
3110
+ },
3111
+ "funding": {
3112
+ "url": "https://github.com/sponsors/ljharb"
3113
+ }
3114
+ },
3115
  "node_modules/gtoken": {
3116
  "version": "8.0.0",
3117
  "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
 
3131
  "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
3132
  "license": "MIT"
3133
  },
3134
+ "node_modules/has-symbols": {
3135
+ "version": "1.1.0",
3136
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
3137
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
3138
+ "license": "MIT",
3139
+ "engines": {
3140
+ "node": ">= 0.4"
3141
+ },
3142
+ "funding": {
3143
+ "url": "https://github.com/sponsors/ljharb"
3144
+ }
3145
+ },
3146
+ "node_modules/has-tostringtag": {
3147
+ "version": "1.0.2",
3148
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
3149
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
3150
+ "license": "MIT",
3151
+ "dependencies": {
3152
+ "has-symbols": "^1.0.3"
3153
+ },
3154
+ "engines": {
3155
+ "node": ">= 0.4"
3156
+ },
3157
+ "funding": {
3158
+ "url": "https://github.com/sponsors/ljharb"
3159
+ }
3160
+ },
3161
+ "node_modules/hasown": {
3162
+ "version": "2.0.2",
3163
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
3164
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
3165
+ "license": "MIT",
3166
+ "dependencies": {
3167
+ "function-bind": "^1.1.2"
3168
+ },
3169
+ "engines": {
3170
+ "node": ">= 0.4"
3171
+ }
3172
+ },
3173
  "node_modules/hast-util-to-jsx-runtime": {
3174
  "version": "2.3.6",
3175
  "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
 
3233
  "node": ">= 14"
3234
  }
3235
  },
3236
+ "node_modules/humanize-ms": {
3237
+ "version": "1.2.1",
3238
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
3239
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
3240
+ "license": "MIT",
3241
+ "dependencies": {
3242
+ "ms": "^2.0.0"
3243
+ }
3244
+ },
3245
  "node_modules/iconv-lite": {
3246
  "version": "0.6.3",
3247
  "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
 
3540
  "node": ">= 18"
3541
  }
3542
  },
3543
+ "node_modules/math-intrinsics": {
3544
+ "version": "1.1.0",
3545
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
3546
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
3547
+ "license": "MIT",
3548
+ "engines": {
3549
+ "node": ">= 0.4"
3550
+ }
3551
+ },
3552
  "node_modules/mdast-util-from-markdown": {
3553
  "version": "2.0.2",
3554
  "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
 
4173
  ],
4174
  "license": "MIT"
4175
  },
4176
+ "node_modules/mime-db": {
4177
+ "version": "1.52.0",
4178
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
4179
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
4180
+ "license": "MIT",
4181
+ "engines": {
4182
+ "node": ">= 0.6"
4183
+ }
4184
+ },
4185
+ "node_modules/mime-types": {
4186
+ "version": "2.1.35",
4187
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
4188
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
4189
+ "license": "MIT",
4190
+ "dependencies": {
4191
+ "mime-db": "1.52.0"
4192
+ },
4193
+ "engines": {
4194
+ "node": ">= 0.6"
4195
+ }
4196
+ },
4197
  "node_modules/minimatch": {
4198
  "version": "9.0.5",
4199
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
 
4317
  "dev": true,
4318
  "license": "MIT"
4319
  },
4320
+ "node_modules/openai": {
4321
+ "version": "4.104.0",
4322
+ "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
4323
+ "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
4324
+ "license": "Apache-2.0",
4325
+ "dependencies": {
4326
+ "@types/node": "^18.11.18",
4327
+ "@types/node-fetch": "^2.6.4",
4328
+ "abort-controller": "^3.0.0",
4329
+ "agentkeepalive": "^4.2.1",
4330
+ "form-data-encoder": "1.7.2",
4331
+ "formdata-node": "^4.3.2",
4332
+ "node-fetch": "^2.6.7"
4333
+ },
4334
+ "bin": {
4335
+ "openai": "bin/cli"
4336
+ },
4337
+ "peerDependencies": {
4338
+ "ws": "^8.18.0",
4339
+ "zod": "^3.23.8"
4340
+ },
4341
+ "peerDependenciesMeta": {
4342
+ "ws": {
4343
+ "optional": true
4344
+ },
4345
+ "zod": {
4346
+ "optional": true
4347
+ }
4348
+ }
4349
+ },
4350
+ "node_modules/openai/node_modules/@types/node": {
4351
+ "version": "18.19.130",
4352
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
4353
+ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
4354
+ "license": "MIT",
4355
+ "dependencies": {
4356
+ "undici-types": "~5.26.4"
4357
+ }
4358
+ },
4359
+ "node_modules/openai/node_modules/node-fetch": {
4360
+ "version": "2.7.0",
4361
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
4362
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
4363
+ "license": "MIT",
4364
+ "dependencies": {
4365
+ "whatwg-url": "^5.0.0"
4366
+ },
4367
+ "engines": {
4368
+ "node": "4.x || >=6.0.0"
4369
+ },
4370
+ "peerDependencies": {
4371
+ "encoding": "^0.1.0"
4372
+ },
4373
+ "peerDependenciesMeta": {
4374
+ "encoding": {
4375
+ "optional": true
4376
+ }
4377
+ }
4378
+ },
4379
+ "node_modules/openai/node_modules/undici-types": {
4380
+ "version": "5.26.5",
4381
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
4382
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
4383
+ "license": "MIT"
4384
+ },
4385
  "node_modules/package-json-from-dist": {
4386
  "version": "1.0.1",
4387
  "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
 
4991
  "url": "https://github.com/sponsors/SuperchupuDev"
4992
  }
4993
  },
4994
+ "node_modules/tr46": {
4995
+ "version": "0.0.3",
4996
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
4997
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
4998
+ "license": "MIT"
4999
+ },
5000
  "node_modules/trim-lines": {
5001
  "version": "3.0.1",
5002
  "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
 
5050
  "version": "6.21.0",
5051
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
5052
  "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
 
5053
  "license": "MIT"
5054
  },
5055
  "node_modules/unified": {
 
5344
  "node": ">= 8"
5345
  }
5346
  },
5347
+ "node_modules/webidl-conversions": {
5348
+ "version": "3.0.1",
5349
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
5350
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
5351
+ "license": "BSD-2-Clause"
5352
+ },
5353
+ "node_modules/whatwg-url": {
5354
+ "version": "5.0.0",
5355
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
5356
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
5357
+ "license": "MIT",
5358
+ "dependencies": {
5359
+ "tr46": "~0.0.3",
5360
+ "webidl-conversions": "^3.0.0"
5361
+ }
5362
+ },
5363
  "node_modules/which": {
5364
  "version": "2.0.2",
5365
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
package.json CHANGED
@@ -12,6 +12,7 @@
12
  "react-dom": "^19.2.0",
13
  "react": "^19.2.0",
14
  "@google/genai": "^1.30.0",
 
15
  "lucide-react": "^0.554.0",
16
  "react-markdown": "^10.1.0",
17
  "mermaid": "11.4.0"
 
12
  "react-dom": "^19.2.0",
13
  "react": "^19.2.0",
14
  "@google/genai": "^1.30.0",
15
+ "openai": "^4.77.0",
16
  "lucide-react": "^0.554.0",
17
  "react-markdown": "^10.1.0",
18
  "mermaid": "11.4.0"
services/aiService.ts ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AI Service - Gemini-based implementation
3
+ */
4
+
5
+ import {
6
+ GeminiModel,
7
+ BentoCardData,
8
+ BlogSection,
9
+ ChatMessage,
10
+ PaperStructure,
11
+ SectionPlan,
12
+ ValidationStatus,
13
+ ChartData
14
+ } from '../types';
15
+
16
+ // Gemini functions
17
+ import {
18
+ generateBentoCards as geminiGenerateBentoCards,
19
+ expandBentoCard as geminiExpandBentoCard,
20
+ chatWithDocument as geminiChatWithDocument,
21
+ analyzePaperStructure as geminiAnalyzePaperStructure,
22
+ generateSingleBlogSection as geminiGenerateSingleBlogSection,
23
+ validateVisualization as geminiValidateVisualization,
24
+ repairVisualization as geminiRepairVisualization,
25
+ generateAndValidateSection as geminiGenerateAndValidateSection,
26
+ // Legacy exports
27
+ validateBlogSection as geminiValidateBlogSection,
28
+ repairBlogSection as geminiRepairBlogSection
29
+ } from './geminiService';
30
+
31
+ /**
32
+ * Generate Bento Cards
33
+ */
34
+ export const generateBentoCards = async (
35
+ apiKey: string,
36
+ model: GeminiModel,
37
+ content: string,
38
+ isPdf: boolean = false,
39
+ useThinking: boolean = false
40
+ ): Promise<BentoCardData[]> => {
41
+ return geminiGenerateBentoCards(apiKey, model, content, isPdf, useThinking);
42
+ };
43
+
44
+ /**
45
+ * Expand Bento Card
46
+ */
47
+ export const expandBentoCard = async (
48
+ apiKey: string,
49
+ model: GeminiModel,
50
+ topic: string,
51
+ detailPrompt: string,
52
+ originalContext: string,
53
+ useThinking: boolean = false
54
+ ): Promise<string> => {
55
+ return geminiExpandBentoCard(apiKey, model, topic, detailPrompt, originalContext, useThinking);
56
+ };
57
+
58
+ /**
59
+ * Chat with Document
60
+ */
61
+ export const chatWithDocument = async (
62
+ apiKey: string,
63
+ model: GeminiModel,
64
+ history: ChatMessage[],
65
+ newMessage: string,
66
+ context: string
67
+ ): Promise<string> => {
68
+ return geminiChatWithDocument(apiKey, model, history, newMessage, context);
69
+ };
70
+
71
+ /**
72
+ * Analyze Paper Structure
73
+ */
74
+ export const analyzePaperStructure = async (
75
+ apiKey: string,
76
+ model: GeminiModel,
77
+ content: string,
78
+ isPdf: boolean = false,
79
+ useThinking: boolean = false
80
+ ): Promise<PaperStructure> => {
81
+ return geminiAnalyzePaperStructure(apiKey, model, content, isPdf, useThinking);
82
+ };
83
+
84
+ /**
85
+ * Generate Single Blog Section
86
+ */
87
+ export const generateSingleBlogSection = async (
88
+ apiKey: string,
89
+ model: GeminiModel,
90
+ content: string,
91
+ sectionPlan: SectionPlan,
92
+ sectionIndex: number,
93
+ totalSections: number,
94
+ paperContext: { title: string; abstract: string; mainContribution: string; keyTerms: string[] },
95
+ isPdf: boolean = false,
96
+ useThinking: boolean = false
97
+ ): Promise<BlogSection> => {
98
+ return geminiGenerateSingleBlogSection(
99
+ apiKey, model, content, sectionPlan, sectionIndex,
100
+ totalSections, paperContext, isPdf, useThinking
101
+ );
102
+ };
103
+
104
+ /**
105
+ * Validate visualization only (local syntax check)
106
+ */
107
+ export const validateVisualization = (section: BlogSection): ValidationStatus => {
108
+ return geminiValidateVisualization(section);
109
+ };
110
+
111
+ /**
112
+ * Repair visualization using AI
113
+ */
114
+ export const repairVisualization = async (
115
+ apiKey: string,
116
+ model: GeminiModel,
117
+ section: BlogSection,
118
+ validationErrors: string[],
119
+ paperContent: string,
120
+ isPdf: boolean = false
121
+ ): Promise<{ visualizationData?: string; chartData?: ChartData }> => {
122
+ return geminiRepairVisualization(apiKey, model, section, validationErrors, paperContent, isPdf);
123
+ };
124
+
125
+ /**
126
+ * Validate Blog Section (legacy - now just validates visualization)
127
+ */
128
+ export const validateBlogSection = async (
129
+ apiKey: string,
130
+ model: GeminiModel,
131
+ section: BlogSection,
132
+ sectionPlan: SectionPlan,
133
+ paperContext: { title: string; abstract: string; mainContribution: string; keyTerms: string[] },
134
+ paperContent: string,
135
+ isPdf: boolean = false
136
+ ): Promise<ValidationStatus> => {
137
+ return geminiValidateBlogSection(
138
+ apiKey, model, section, sectionPlan, paperContext, paperContent, isPdf
139
+ );
140
+ };
141
+
142
+ /**
143
+ * Repair Blog Section
144
+ */
145
+ export const repairBlogSection = async (
146
+ apiKey: string,
147
+ model: GeminiModel,
148
+ section: BlogSection,
149
+ validationStatus: ValidationStatus & { correctedVisualization?: string },
150
+ sectionPlan: SectionPlan,
151
+ paperContext: { title: string; abstract: string; mainContribution: string; keyTerms: string[] },
152
+ paperContent: string,
153
+ isPdf: boolean = false
154
+ ): Promise<BlogSection> => {
155
+ return geminiRepairBlogSection(
156
+ apiKey, model, section, validationStatus, sectionPlan, paperContext, paperContent, isPdf
157
+ );
158
+ };
159
+
160
+ /**
161
+ * Generate and Validate Section
162
+ */
163
+ export const generateAndValidateSection = async (
164
+ apiKey: string,
165
+ model: GeminiModel,
166
+ content: string,
167
+ sectionPlan: SectionPlan,
168
+ sectionIndex: number,
169
+ totalSections: number,
170
+ paperContext: { title: string; abstract: string; mainContribution: string; keyTerms: string[] },
171
+ isPdf: boolean = false,
172
+ useThinking: boolean = false,
173
+ maxRepairAttempts: number = 2,
174
+ onStatusUpdate?: (status: 'generating' | 'validating' | 'repairing' | 'complete', message: string) => void
175
+ ): Promise<BlogSection> => {
176
+ return geminiGenerateAndValidateSection(
177
+ apiKey, model, content, sectionPlan, sectionIndex, totalSections,
178
+ paperContext, isPdf, useThinking, maxRepairAttempts, onStatusUpdate
179
+ );
180
+ };
181
+
182
+ // Model info
183
+ export const MODEL_INFO: Record<GeminiModel, { name: string; description: string; icon: string }> = {
184
+ 'gemini-2.5-flash': {
185
+ name: 'Gemini 2.5 Flash',
186
+ description: 'Fast & efficient',
187
+ icon: '⚡'
188
+ },
189
+ 'gemini-3-pro-preview': {
190
+ name: 'Gemini 3 Pro',
191
+ description: 'Advanced reasoning',
192
+ icon: '🧠'
193
+ }
194
+ };
services/geminiService.ts CHANGED
@@ -1,6 +1,134 @@
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,
@@ -21,7 +149,7 @@ const RESPONSE_SCHEMA = {
21
 
22
  export const generateBentoCards = async (
23
  apiKey: string,
24
- model: ModelType,
25
  content: string,
26
  isPdf: boolean = false,
27
  useThinking: boolean = false
@@ -92,7 +220,7 @@ export const generateBentoCards = async (
92
  // but ensuring responseSchema is present usually works.
93
  }
94
 
95
- try {
96
  const response = await ai.models.generateContent({
97
  model: effectiveModel,
98
  contents: { parts: promptParts },
@@ -121,16 +249,12 @@ export const generateBentoCards = async (
121
  expandedContent: undefined,
122
  isLoadingDetails: false
123
  }));
124
-
125
- } catch (error: any) {
126
- console.error("Gemini Bento Error:", error);
127
- throw new Error(error.message || "Failed to generate bento cards");
128
- }
129
  };
130
 
131
  export const expandBentoCard = async (
132
  apiKey: string,
133
- model: ModelType,
134
  topic: string,
135
  detailPrompt: string,
136
  originalContext: string,
@@ -160,22 +284,26 @@ export const expandBentoCard = async (
160
  requestConfig.thinkingConfig = { thinkingBudget: 32768 };
161
  }
162
 
163
- const response = await ai.models.generateContent({
164
- model: effectiveModel,
165
- contents: prompt,
166
- config: requestConfig
167
- });
 
168
 
169
- return response.text || "Could not generate details.";
 
170
  };
171
 
172
  export const chatWithDocument = async (
173
  apiKey: string,
174
- model: ModelType,
175
  history: ChatMessage[],
176
  newMessage: string,
177
  context: string
178
  ): Promise<string> => {
 
 
179
  const ai = new GoogleGenAI({ apiKey });
180
 
181
  const chatHistory = history.map(h => ({
@@ -183,16 +311,18 @@ export const chatWithDocument = async (
183
  parts: [{ text: h.text }]
184
  }));
185
 
186
- const chat = ai.chats.create({
187
- model: model,
188
- history: chatHistory,
189
- config: {
190
- systemInstruction: `You are a helpful research assistant. You have read the following paper content/summary: ${context.substring(0, 20000)}. Answer the user's questions accurately based on this context.`
191
- }
192
- });
 
193
 
194
- const result = await chat.sendMessage({ message: newMessage });
195
- return result.text || "";
 
196
  };
197
 
198
  // Paper Structure Analysis Schema
@@ -315,7 +445,7 @@ const SINGLE_SECTION_SCHEMA = {
315
  */
316
  export const analyzePaperStructure = async (
317
  apiKey: string,
318
- model: ModelType,
319
  content: string,
320
  isPdf: boolean = false,
321
  useThinking: boolean = false
@@ -385,7 +515,7 @@ export const analyzePaperStructure = async (
385
  requestConfig.thinkingConfig = { thinkingBudget: 16384 };
386
  }
387
 
388
- try {
389
  const response = await ai.models.generateContent({
390
  model: effectiveModel,
391
  contents: { parts: promptParts },
@@ -410,11 +540,7 @@ export const analyzePaperStructure = async (
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
  /**
@@ -422,7 +548,7 @@ export const analyzePaperStructure = async (
422
  */
423
  export const generateSingleBlogSection = async (
424
  apiKey: string,
425
- model: ModelType,
426
  content: string,
427
  sectionPlan: SectionPlan,
428
  sectionIndex: number,
@@ -525,7 +651,7 @@ export const generateSingleBlogSection = async (
525
  requestConfig.thinkingConfig = { thinkingBudget: 8192 };
526
  }
527
 
528
- try {
529
  const response = await ai.models.generateContent({
530
  model: effectiveModel,
531
  contents: { parts: promptParts },
@@ -560,11 +686,7 @@ export const generateSingleBlogSection = async (
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
  /**
@@ -572,7 +694,7 @@ export const generateSingleBlogSection = async (
572
  */
573
  export const generateBlogContent = async (
574
  apiKey: string,
575
- model: ModelType,
576
  content: string,
577
  isPdf: boolean = false,
578
  useThinking: boolean = false
@@ -606,3 +728,394 @@ export const generateBlogContent = async (
606
 
607
  return sections;
608
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
  import { GoogleGenAI, Type, Schema } from "@google/genai";
3
+ import { BentoCardData, BlogSection, ChatMessage, GeminiModel, ChartData, PaperStructure, SectionPlan, ValidationStatus, ValidationResult } from "../types";
4
+
5
+ // ============================================================================
6
+ // RETRY UTILITY WITH EXPONENTIAL BACKOFF
7
+ // ============================================================================
8
+
9
+ interface RetryConfig {
10
+ maxRetries: number;
11
+ baseDelay: number; // in ms
12
+ maxDelay: number; // in ms
13
+ retryableErrors: string[];
14
+ }
15
+
16
+ const DEFAULT_RETRY_CONFIG: RetryConfig = {
17
+ maxRetries: 3,
18
+ baseDelay: 1000,
19
+ maxDelay: 10000,
20
+ retryableErrors: [
21
+ 'RESOURCE_EXHAUSTED',
22
+ 'UNAVAILABLE',
23
+ 'DEADLINE_EXCEEDED',
24
+ 'INTERNAL',
25
+ 'rate limit',
26
+ 'quota',
27
+ '429',
28
+ '500',
29
+ '502',
30
+ '503',
31
+ '504',
32
+ 'timeout',
33
+ 'network',
34
+ 'ECONNRESET',
35
+ 'ETIMEDOUT'
36
+ ]
37
+ };
38
+
39
+ /**
40
+ * Check if an error is retryable
41
+ */
42
+ const isRetryableError = (error: any, config: RetryConfig = DEFAULT_RETRY_CONFIG): boolean => {
43
+ const errorMessage = error?.message?.toLowerCase() || '';
44
+ const errorCode = error?.code?.toLowerCase() || '';
45
+ const statusCode = error?.status?.toString() || '';
46
+
47
+ return config.retryableErrors.some(pattern =>
48
+ errorMessage.includes(pattern.toLowerCase()) ||
49
+ errorCode.includes(pattern.toLowerCase()) ||
50
+ statusCode.includes(pattern)
51
+ );
52
+ };
53
+
54
+ /**
55
+ * Sleep for a given duration
56
+ */
57
+ const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
58
+
59
+ /**
60
+ * Calculate delay with exponential backoff and jitter
61
+ */
62
+ const calculateBackoff = (attempt: number, config: RetryConfig): number => {
63
+ const exponentialDelay = config.baseDelay * Math.pow(2, attempt);
64
+ const jitter = Math.random() * 0.3 * exponentialDelay; // Add 0-30% jitter
65
+ return Math.min(exponentialDelay + jitter, config.maxDelay);
66
+ };
67
+
68
+ /**
69
+ * Execute a function with retry logic
70
+ */
71
+ async function withRetry<T>(
72
+ fn: () => Promise<T>,
73
+ operationName: string,
74
+ config: RetryConfig = DEFAULT_RETRY_CONFIG
75
+ ): Promise<T> {
76
+ let lastError: any;
77
+
78
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
79
+ try {
80
+ return await fn();
81
+ } catch (error: any) {
82
+ lastError = error;
83
+
84
+ if (attempt === config.maxRetries || !isRetryableError(error, config)) {
85
+ // Don't retry if max attempts reached or error is not retryable
86
+ console.error(`[${operationName}] Failed after ${attempt + 1} attempt(s):`, error.message);
87
+ throw new Error(formatUserFriendlyError(error, operationName));
88
+ }
89
+
90
+ const delay = calculateBackoff(attempt, config);
91
+ console.warn(`[${operationName}] Attempt ${attempt + 1} failed, retrying in ${Math.round(delay)}ms...`, error.message);
92
+ await sleep(delay);
93
+ }
94
+ }
95
+
96
+ throw lastError;
97
+ }
98
+
99
+ /**
100
+ * Format error message for user display
101
+ */
102
+ const formatUserFriendlyError = (error: any, operation: string): string => {
103
+ const errorMsg = error?.message?.toLowerCase() || '';
104
+
105
+ if (errorMsg.includes('api key') || errorMsg.includes('invalid key') || errorMsg.includes('unauthorized')) {
106
+ return 'Invalid API key. Please check your Gemini API key and try again.';
107
+ }
108
+
109
+ if (errorMsg.includes('rate limit') || errorMsg.includes('quota') || errorMsg.includes('429')) {
110
+ return 'API rate limit exceeded. Please wait a moment and try again.';
111
+ }
112
+
113
+ if (errorMsg.includes('timeout') || errorMsg.includes('deadline')) {
114
+ return `Request timed out during ${operation}. The paper may be too large or complex. Try again or use a smaller document.`;
115
+ }
116
+
117
+ if (errorMsg.includes('network') || errorMsg.includes('econnreset') || errorMsg.includes('etimedout')) {
118
+ return 'Network error. Please check your connection and try again.';
119
+ }
120
+
121
+ if (errorMsg.includes('resource_exhausted')) {
122
+ return 'Server resources are temporarily exhausted. Please wait a few seconds and try again.';
123
+ }
124
+
125
+ if (errorMsg.includes('unavailable') || errorMsg.includes('500') || errorMsg.includes('502') || errorMsg.includes('503')) {
126
+ return 'The AI service is temporarily unavailable. Please try again in a moment.';
127
+ }
128
+
129
+ // Default message with original error
130
+ return error.message || `An error occurred during ${operation}. Please try again.`;
131
+ };
132
 
133
  const RESPONSE_SCHEMA = {
134
  type: Type.ARRAY,
 
149
 
150
  export const generateBentoCards = async (
151
  apiKey: string,
152
+ model: GeminiModel,
153
  content: string,
154
  isPdf: boolean = false,
155
  useThinking: boolean = false
 
220
  // but ensuring responseSchema is present usually works.
221
  }
222
 
223
+ return withRetry(async () => {
224
  const response = await ai.models.generateContent({
225
  model: effectiveModel,
226
  contents: { parts: promptParts },
 
249
  expandedContent: undefined,
250
  isLoadingDetails: false
251
  }));
252
+ }, 'generateBentoCards');
 
 
 
 
253
  };
254
 
255
  export const expandBentoCard = async (
256
  apiKey: string,
257
+ model: GeminiModel,
258
  topic: string,
259
  detailPrompt: string,
260
  originalContext: string,
 
284
  requestConfig.thinkingConfig = { thinkingBudget: 32768 };
285
  }
286
 
287
+ return withRetry(async () => {
288
+ const response = await ai.models.generateContent({
289
+ model: effectiveModel,
290
+ contents: prompt,
291
+ config: requestConfig
292
+ });
293
 
294
+ return response.text || "Could not generate details.";
295
+ }, 'expandBentoCard');
296
  };
297
 
298
  export const chatWithDocument = async (
299
  apiKey: string,
300
+ model: GeminiModel,
301
  history: ChatMessage[],
302
  newMessage: string,
303
  context: string
304
  ): Promise<string> => {
305
+ if (!apiKey) throw new Error("API Key is missing");
306
+
307
  const ai = new GoogleGenAI({ apiKey });
308
 
309
  const chatHistory = history.map(h => ({
 
311
  parts: [{ text: h.text }]
312
  }));
313
 
314
+ return withRetry(async () => {
315
+ const chat = ai.chats.create({
316
+ model: model,
317
+ history: chatHistory,
318
+ config: {
319
+ systemInstruction: `You are a helpful research assistant. You have read the following paper content/summary: ${context.substring(0, 20000)}. Answer the user's questions accurately based on this context.`
320
+ }
321
+ });
322
 
323
+ const result = await chat.sendMessage({ message: newMessage });
324
+ return result.text || "";
325
+ }, 'chatWithDocument');
326
  };
327
 
328
  // Paper Structure Analysis Schema
 
445
  */
446
  export const analyzePaperStructure = async (
447
  apiKey: string,
448
+ model: GeminiModel,
449
  content: string,
450
  isPdf: boolean = false,
451
  useThinking: boolean = false
 
515
  requestConfig.thinkingConfig = { thinkingBudget: 16384 };
516
  }
517
 
518
+ return withRetry(async () => {
519
  const response = await ai.models.generateContent({
520
  model: effectiveModel,
521
  contents: { parts: promptParts },
 
540
  id: `plan-${index}-${Date.now()}`
541
  }))
542
  };
543
+ }, 'analyzePaperStructure');
 
 
 
 
544
  };
545
 
546
  /**
 
548
  */
549
  export const generateSingleBlogSection = async (
550
  apiKey: string,
551
+ model: GeminiModel,
552
  content: string,
553
  sectionPlan: SectionPlan,
554
  sectionIndex: number,
 
651
  requestConfig.thinkingConfig = { thinkingBudget: 8192 };
652
  }
653
 
654
+ return withRetry(async () => {
655
  const response = await ai.models.generateContent({
656
  model: effectiveModel,
657
  contents: { parts: promptParts },
 
686
  id: `collapse-${sectionIndex}-${secIdx}-${Date.now()}`
687
  }))
688
  };
689
+ }, `generateSection:${sectionPlan.title}`);
 
 
 
 
690
  };
691
 
692
  /**
 
694
  */
695
  export const generateBlogContent = async (
696
  apiKey: string,
697
+ model: GeminiModel,
698
  content: string,
699
  isPdf: boolean = false,
700
  useThinking: boolean = false
 
728
 
729
  return sections;
730
  };
731
+
732
+ // ============================================================================
733
+ // VALIDATION SYSTEM - Focused on Coded Visualizations Only
734
+ // ============================================================================
735
+
736
+ /**
737
+ * Validate Mermaid syntax locally (basic check)
738
+ */
739
+ const validateMermaidSyntax = (mermaidCode: string): { valid: boolean; errors: string[] } => {
740
+ const errors: string[] = [];
741
+
742
+ if (!mermaidCode || mermaidCode.trim() === '') {
743
+ return { valid: true, errors: [] };
744
+ }
745
+
746
+ const code = mermaidCode.trim();
747
+
748
+ // Check for valid start
749
+ const validStarts = ['graph ', 'flowchart ', 'sequenceDiagram', 'classDiagram', 'stateDiagram', 'erDiagram', 'gantt', 'pie', 'mindmap'];
750
+ const hasValidStart = validStarts.some(start => code.toLowerCase().startsWith(start.toLowerCase()));
751
+
752
+ if (!hasValidStart) {
753
+ errors.push('Mermaid diagram must start with a valid type (graph, flowchart, sequenceDiagram, etc.)');
754
+ }
755
+
756
+ // Check for basic structure in graph/flowchart
757
+ if (code.toLowerCase().startsWith('graph') || code.toLowerCase().startsWith('flowchart')) {
758
+ const hasNodes = /\w+\[.+\]/.test(code) || /\w+\(.+\)/.test(code) || /\w+\{.+\}/.test(code);
759
+ if (!hasNodes && !code.includes('-->') && !code.includes('---')) {
760
+ errors.push('Graph should contain node definitions or connections');
761
+ }
762
+
763
+ // Check for unbalanced brackets
764
+ const openBrackets = (code.match(/\[/g) || []).length;
765
+ const closeBrackets = (code.match(/\]/g) || []).length;
766
+ if (openBrackets !== closeBrackets) {
767
+ errors.push(`Unbalanced square brackets: ${openBrackets} open, ${closeBrackets} close`);
768
+ }
769
+ }
770
+
771
+ // Check for markdown code fences (common error)
772
+ if (code.includes('```')) {
773
+ errors.push('Mermaid code should not contain markdown code fences');
774
+ }
775
+
776
+ return { valid: errors.length === 0, errors };
777
+ };
778
+
779
+ /**
780
+ * Validate chart data structure
781
+ */
782
+ const validateChartData = (chartData: ChartData | undefined): { valid: boolean; errors: string[] } => {
783
+ const errors: string[] = [];
784
+
785
+ if (!chartData) {
786
+ return { valid: true, errors: [] };
787
+ }
788
+
789
+ const validTypes = ['bar', 'line', 'pie', 'area', 'scatter'];
790
+ if (!validTypes.includes(chartData.type)) {
791
+ errors.push(`Invalid chart type: ${chartData.type}. Must be one of: ${validTypes.join(', ')}`);
792
+ }
793
+
794
+ if (!chartData.data || !Array.isArray(chartData.data)) {
795
+ errors.push('Chart data must be an array');
796
+ } else if (chartData.data.length === 0) {
797
+ errors.push('Chart data cannot be empty');
798
+ } else {
799
+ chartData.data.forEach((point, index) => {
800
+ if (typeof point.label !== 'string') {
801
+ errors.push(`Data point ${index}: label must be a string`);
802
+ }
803
+ if (typeof point.value !== 'number' || isNaN(point.value)) {
804
+ errors.push(`Data point ${index}: value must be a valid number`);
805
+ }
806
+ });
807
+
808
+ if (chartData.type === 'pie') {
809
+ const total = chartData.data.reduce((sum, p) => sum + (p.value || 0), 0);
810
+ if (total <= 0) {
811
+ errors.push('Pie chart values must sum to a positive number');
812
+ }
813
+ }
814
+ }
815
+
816
+ return { valid: errors.length === 0, errors };
817
+ };
818
+
819
+ /**
820
+ * Validate equation syntax (basic LaTeX check)
821
+ */
822
+ const validateEquationSyntax = (equation: string): { valid: boolean; errors: string[] } => {
823
+ const errors: string[] = [];
824
+
825
+ if (!equation || equation.trim() === '') {
826
+ return { valid: true, errors: [] };
827
+ }
828
+
829
+ // Check for unbalanced braces
830
+ let braceCount = 0;
831
+ for (const char of equation) {
832
+ if (char === '{') braceCount++;
833
+ if (char === '}') braceCount--;
834
+ if (braceCount < 0) {
835
+ errors.push('Unbalanced curly braces: closing brace without opening');
836
+ break;
837
+ }
838
+ }
839
+ if (braceCount > 0) {
840
+ errors.push(`Unbalanced curly braces: ${braceCount} unclosed`);
841
+ }
842
+
843
+ return { valid: errors.length === 0, errors };
844
+ };
845
+
846
+ /**
847
+ * Validate visualization only - no content validation needed
848
+ */
849
+ export const validateVisualization = (section: BlogSection): ValidationStatus => {
850
+ // Only validate if there's a coded visualization
851
+ if (section.visualizationType === 'none' || !section.visualizationType) {
852
+ return {
853
+ isValidated: true,
854
+ contentRelevance: { passed: true, score: 100, issues: [] },
855
+ visualizationValidity: { passed: true, score: 100, issues: [] },
856
+ overallScore: 100
857
+ };
858
+ }
859
+
860
+ let vizValidation = { valid: true, errors: [] as string[] };
861
+
862
+ switch (section.visualizationType) {
863
+ case 'mermaid':
864
+ vizValidation = validateMermaidSyntax(section.visualizationData || '');
865
+ break;
866
+ case 'chart':
867
+ vizValidation = validateChartData(section.chartData);
868
+ break;
869
+ case 'equation':
870
+ vizValidation = validateEquationSyntax(section.visualizationData || '');
871
+ break;
872
+ }
873
+
874
+ return {
875
+ isValidated: true,
876
+ contentRelevance: { passed: true, score: 100, issues: [] },
877
+ visualizationValidity: {
878
+ passed: vizValidation.valid,
879
+ score: vizValidation.valid ? 100 : 30,
880
+ issues: vizValidation.errors
881
+ },
882
+ overallScore: vizValidation.valid ? 100 : 50
883
+ };
884
+ };
885
+
886
+ // Visualization Repair Schema
887
+ const VISUALIZATION_REPAIR_SCHEMA = {
888
+ type: Type.OBJECT,
889
+ properties: {
890
+ visualizationData: {
891
+ type: Type.STRING,
892
+ description: "The corrected visualization code (Mermaid, LaTeX equation, etc.)"
893
+ },
894
+ chartData: {
895
+ type: Type.OBJECT,
896
+ nullable: true,
897
+ properties: {
898
+ type: { type: Type.STRING, enum: ['bar', 'line', 'pie', 'area'] },
899
+ title: { type: Type.STRING },
900
+ data: {
901
+ type: Type.ARRAY,
902
+ items: {
903
+ type: Type.OBJECT,
904
+ properties: {
905
+ label: { type: Type.STRING },
906
+ value: { type: Type.NUMBER }
907
+ },
908
+ required: ['label', 'value']
909
+ }
910
+ },
911
+ xAxis: { type: Type.STRING },
912
+ yAxis: { type: Type.STRING }
913
+ }
914
+ }
915
+ },
916
+ required: ['visualizationData']
917
+ };
918
+
919
+ /**
920
+ * Repair only the visualization - focused and direct
921
+ */
922
+ export const repairVisualization = async (
923
+ apiKey: string,
924
+ model: GeminiModel,
925
+ section: BlogSection,
926
+ validationErrors: string[],
927
+ paperContent: string,
928
+ isPdf: boolean = false
929
+ ): Promise<{ visualizationData?: string; chartData?: ChartData }> => {
930
+ if (!apiKey) throw new Error("API Key is missing");
931
+
932
+ const ai = new GoogleGenAI({ apiKey });
933
+
934
+ let promptParts: any[] = [];
935
+
936
+ if (isPdf) {
937
+ promptParts.push({
938
+ inlineData: { data: paperContent, mimeType: "application/pdf" },
939
+ });
940
+ } else {
941
+ promptParts.push({ text: `Paper context: ${paperContent.substring(0, 20000)}` });
942
+ }
943
+
944
+ promptParts.push({
945
+ text: `
946
+ FIX THIS ${section.visualizationType?.toUpperCase()} VISUALIZATION.
947
+
948
+ ORIGINAL CODE:
949
+ """
950
+ ${section.visualizationType === 'chart' ? JSON.stringify(section.chartData, null, 2) : section.visualizationData}
951
+ """
952
+
953
+ ERRORS FOUND:
954
+ ${validationErrors.map(e => `- ${e}`).join('\n')}
955
+
956
+ ${section.visualizationType === 'mermaid' ? `
957
+ MERMAID REQUIREMENTS:
958
+ - Start with 'graph TD' or 'flowchart LR'
959
+ - Use proper syntax: A[Label] --> B[Label]
960
+ - No markdown code fences
961
+ - Balance all brackets
962
+ ` : ''}
963
+
964
+ ${section.visualizationType === 'chart' ? `
965
+ CHART REQUIREMENTS:
966
+ - Valid type: bar, line, pie, or area
967
+ - Data array with {label: string, value: number} objects
968
+ - Non-empty data array
969
+ ` : ''}
970
+
971
+ ${section.visualizationType === 'equation' ? `
972
+ EQUATION REQUIREMENTS:
973
+ - Valid LaTeX syntax
974
+ - Balanced curly braces
975
+ - Use \\frac{}{}, \\sum, \\alpha, etc.
976
+ ` : ''}
977
+
978
+ Return ONLY the fixed visualization.
979
+ `
980
+ });
981
+
982
+ const requestConfig: any = {
983
+ responseMimeType: "application/json",
984
+ responseSchema: VISUALIZATION_REPAIR_SCHEMA as any,
985
+ systemInstruction: "You fix visualization syntax errors. Return only the corrected code.",
986
+ };
987
+
988
+ return withRetry(async () => {
989
+ const response = await ai.models.generateContent({
990
+ model: model,
991
+ contents: { parts: promptParts },
992
+ config: requestConfig
993
+ });
994
+
995
+ const text = response.text;
996
+ if (!text) throw new Error("No repair response");
997
+
998
+ let jsonStr = text;
999
+ const jsonMatch = text.match(/\{.*\}/s);
1000
+ if (jsonMatch) {
1001
+ jsonStr = jsonMatch[0];
1002
+ }
1003
+
1004
+ return JSON.parse(jsonStr);
1005
+ }, 'repairVisualization');
1006
+ };
1007
+
1008
+ /**
1009
+ * Generate section content and validate/repair visualization only
1010
+ */
1011
+ export const generateAndValidateSection = async (
1012
+ apiKey: string,
1013
+ model: GeminiModel,
1014
+ content: string,
1015
+ sectionPlan: SectionPlan,
1016
+ sectionIndex: number,
1017
+ totalSections: number,
1018
+ paperContext: { title: string; abstract: string; mainContribution: string; keyTerms: string[] },
1019
+ isPdf: boolean = false,
1020
+ useThinking: boolean = false,
1021
+ maxRepairAttempts: number = 2,
1022
+ onStatusUpdate?: (status: 'generating' | 'validating' | 'repairing' | 'complete', message: string) => void
1023
+ ): Promise<BlogSection> => {
1024
+
1025
+ // Step 1: Generate section content
1026
+ onStatusUpdate?.('generating', `Generating section: ${sectionPlan.title}`);
1027
+ let section = await generateSingleBlogSection(
1028
+ apiKey, model, content, sectionPlan, sectionIndex, totalSections, paperContext, isPdf, useThinking
1029
+ );
1030
+
1031
+ // Step 2: Validate visualization only (no content validation - trust the LLM)
1032
+ const validation = validateVisualization(section);
1033
+ section.validationStatus = validation;
1034
+
1035
+ // Step 3: Repair visualization if needed
1036
+ if (!validation.visualizationValidity.passed && section.visualizationType !== 'none') {
1037
+ let attempts = 0;
1038
+ while (!validation.visualizationValidity.passed && attempts < maxRepairAttempts) {
1039
+ attempts++;
1040
+ onStatusUpdate?.('repairing', `Fixing visualization (attempt ${attempts}/${maxRepairAttempts})...`);
1041
+
1042
+ try {
1043
+ const repaired = await repairVisualization(
1044
+ apiKey, model, section, validation.visualizationValidity.issues, content, isPdf
1045
+ );
1046
+
1047
+ // Apply the repaired visualization
1048
+ if (section.visualizationType === 'chart' && repaired.chartData) {
1049
+ section.chartData = repaired.chartData;
1050
+ } else if (repaired.visualizationData) {
1051
+ section.visualizationData = repaired.visualizationData;
1052
+ }
1053
+
1054
+ // Re-validate
1055
+ const newValidation = validateVisualization(section);
1056
+ section.validationStatus = {
1057
+ ...newValidation,
1058
+ wasRepaired: true,
1059
+ repairAttempts: attempts
1060
+ };
1061
+
1062
+ if (newValidation.visualizationValidity.passed) break;
1063
+ } catch (error) {
1064
+ console.error('Visualization repair failed:', error);
1065
+ break;
1066
+ }
1067
+ }
1068
+ }
1069
+
1070
+ onStatusUpdate?.('complete', `Section complete`);
1071
+ return section;
1072
+ };
1073
+
1074
+ // Legacy exports for backward compatibility
1075
+ export const validateBlogSection = async (
1076
+ apiKey: string,
1077
+ model: GeminiModel,
1078
+ section: BlogSection,
1079
+ _sectionPlan: SectionPlan,
1080
+ _paperContext: { title: string; abstract: string; mainContribution: string; keyTerms: string[] },
1081
+ _paperContent: string,
1082
+ _isPdf: boolean = false
1083
+ ): Promise<ValidationStatus> => {
1084
+ // Simplified: just do local validation
1085
+ return validateVisualization(section);
1086
+ };
1087
+
1088
+ export const repairBlogSection = async (
1089
+ apiKey: string,
1090
+ model: GeminiModel,
1091
+ section: BlogSection,
1092
+ validationStatus: ValidationStatus,
1093
+ _sectionPlan: SectionPlan,
1094
+ _paperContext: { title: string; abstract: string; mainContribution: string; keyTerms: string[] },
1095
+ paperContent: string,
1096
+ isPdf: boolean = false
1097
+ ): Promise<BlogSection> => {
1098
+ // Only repair visualization
1099
+ if (validationStatus.visualizationValidity.passed) {
1100
+ return section;
1101
+ }
1102
+
1103
+ try {
1104
+ const repaired = await repairVisualization(
1105
+ apiKey, model, section, validationStatus.visualizationValidity.issues, paperContent, isPdf
1106
+ );
1107
+
1108
+ return {
1109
+ ...section,
1110
+ visualizationData: repaired.visualizationData || section.visualizationData,
1111
+ chartData: repaired.chartData || section.chartData,
1112
+ validationStatus: {
1113
+ ...validationStatus,
1114
+ wasRepaired: true,
1115
+ repairAttempts: (section.validationStatus?.repairAttempts || 0) + 1
1116
+ }
1117
+ };
1118
+ } catch (error) {
1119
+ return section;
1120
+ }
1121
+ };
style.css CHANGED
@@ -1,28 +1,22 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
 
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
 
 
28
  }
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
 
4
 
5
+ body {
6
+ font-family: 'Inter', sans-serif;
 
 
 
 
 
 
 
 
7
  }
8
 
9
+ /* Custom utilities if needed, but prefer Tailwind classes */
10
+ .glass-panel {
11
+ background: rgba(255, 255, 255, 0.7);
12
+ backdrop-filter: blur(20px);
13
+ -webkit-backdrop-filter: blur(20px);
14
+ border: 1px solid rgba(255, 255, 255, 0.3);
15
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
16
  }
17
 
18
+ .dark .glass-panel {
19
+ background: rgba(15, 23, 42, 0.6);
20
+ border: 1px solid rgba(255, 255, 255, 0.1);
21
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
22
  }
types.ts CHANGED
@@ -6,7 +6,7 @@ export interface BentoCardData {
6
  type: 'stat' | 'concept' | 'quote' | 'insight' | 'process';
7
  colSpan: number; // 1 to 4
8
  rowSpan: number; // 1 to 2
9
- detailPrompt: string; // The prompt to send to Gemini to get more details
10
  mermaid?: string; // Mermaid JS diagram definition
11
  expandedContent?: string;
12
  isLoadingDetails?: boolean;
@@ -21,11 +21,12 @@ export interface ChatMessage {
21
  timestamp: number;
22
  }
23
 
24
- export type ModelType = 'gemini-flash-latest' | 'gemini-3-pro-preview';
 
25
 
26
  export interface AppSettings {
27
  apiKey: string;
28
- model: ModelType;
29
  theme: 'light' | 'dark';
30
  layoutMode: 'auto' | 'grid' | 'list';
31
  useThinking: boolean;
@@ -49,6 +50,24 @@ export interface BlogSection {
49
  collapsibleSections?: CollapsibleContent[];
50
  isLoading?: boolean;
51
  error?: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  }
53
 
54
  // Structure plan from paper analysis
 
6
  type: 'stat' | 'concept' | 'quote' | 'insight' | 'process';
7
  colSpan: number; // 1 to 4
8
  rowSpan: number; // 1 to 2
9
+ detailPrompt: string; // The prompt to send to the AI to get more details
10
  mermaid?: string; // Mermaid JS diagram definition
11
  expandedContent?: string;
12
  isLoadingDetails?: boolean;
 
21
  timestamp: number;
22
  }
23
 
24
+ // Model types
25
+ export type GeminiModel = 'gemini-2.5-flash' | 'gemini-3-pro-preview';
26
 
27
  export interface AppSettings {
28
  apiKey: string;
29
+ model: GeminiModel;
30
  theme: 'light' | 'dark';
31
  layoutMode: 'auto' | 'grid' | 'list';
32
  useThinking: boolean;
 
50
  collapsibleSections?: CollapsibleContent[];
51
  isLoading?: boolean;
52
  error?: string;
53
+ // Validation status
54
+ validationStatus?: ValidationStatus;
55
+ }
56
+
57
+ export interface ValidationStatus {
58
+ isValidated: boolean;
59
+ contentRelevance: ValidationResult;
60
+ visualizationValidity: ValidationResult;
61
+ overallScore: number; // 0-100
62
+ wasRepaired?: boolean;
63
+ repairAttempts?: number;
64
+ }
65
+
66
+ export interface ValidationResult {
67
+ passed: boolean;
68
+ score: number; // 0-100
69
+ issues: string[];
70
+ suggestions?: string[];
71
  }
72
 
73
  // Structure plan from paper analysis