import React, { useState, useRef } from 'react'; import { Moon, Sun, Upload, FileText, Download, Share2, MessageSquare, AlertCircle, LayoutGrid, List, Grid, ChevronLeft, ArrowRight, X, BrainCircuit, BookOpen, Layers, Key, ChevronDown } from 'lucide-react'; import Background from './components/Background'; import BentoCard from './components/BentoCard'; import ChatBot from './components/ChatBot'; import BlogView from './components/BlogView'; import { BentoCardData, BlogSection, ChatMessage, AppSettings, ProcessingStatus, ViewMode, PaperStructure, GeminiModel, ReadingPreferences } from './types'; import { generateBentoCards, expandBentoCard, chatWithDocument, analyzePaperStructure, generateAndValidateSection, MODEL_INFO } from './services/aiService'; // Default reading preferences for ADHD-friendly features const defaultReadingPreferences: ReadingPreferences = { bionicReading: false, eli5Mode: false, highlightKeyTerms: true, ttsEnabled: false, ttsSpeed: 1.0 }; const App: React.FC = () => { // Settings const [settings, setSettings] = useState({ apiKey: '', model: 'gemini-2.5-flash', theme: 'light', layoutMode: 'auto', useThinking: false, readingPreferences: defaultReadingPreferences }); // Inputs const [file, setFile] = useState(null); // State const [view, setView] = useState<'input' | 'results'>('input'); const [resultViewMode, setResultViewMode] = useState('grid'); const [cards, setCards] = useState([]); const [blogSections, setBlogSections] = useState([]); const [paperStructure, setPaperStructure] = useState(null); const [isBlogLoading, setIsBlogLoading] = useState(false); const [blogLoadingStage, setBlogLoadingStage] = useState<'idle' | 'analyzing' | 'generating' | 'validating'>('idle'); const [currentGeneratingSection, setCurrentGeneratingSection] = useState(-1); const [sectionStatus, setSectionStatus] = useState(''); const [status, setStatus] = useState({ state: 'idle' }); const [paperContext, setPaperContext] = useState(''); // Stores the raw text/base64 const [paperTitle, setPaperTitle] = useState(''); const [retryingSectionIndex, setRetryingSectionIndex] = useState(-1); // Chat const [isChatOpen, setIsChatOpen] = useState(false); const [chatHistory, setChatHistory] = useState([]); const [isChatProcessing, setIsChatProcessing] = useState(false); const gridRef = useRef(null); const blogRef = useRef(null); const toggleTheme = () => { const newTheme = settings.theme === 'dark' ? 'light' : 'dark'; setSettings(prev => ({ ...prev, theme: newTheme })); if (newTheme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } }; const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { setFile(e.target.files[0]); } }; const handleProcess = async () => { if (!settings.apiKey) { setStatus({ state: 'error', message: 'Please enter your Gemini API Key.' }); return; } if (!file) { setStatus({ state: 'error', message: "Please upload a PDF file." }); return; } setStatus({ state: 'reading', message: 'Reading PDF file...' }); try { let contentToProcess = ""; const contextTitle = file.name; // PDF UPLOAD FLOW contentToProcess = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const result = reader.result as string; const base64 = result.split(',')[1]; resolve(base64); }; reader.onerror = reject; reader.readAsDataURL(file); }); if (file.type !== 'application/pdf') { throw new Error("File must be a PDF."); } setPaperContext(contentToProcess); setPaperTitle(contextTitle); setStatus({ state: 'analyzing', message: settings.useThinking ? 'Thinking deeply about paper structure...' : 'Analyzing paper structure...' }); const generatedCards = await generateBentoCards( settings.apiKey, settings.model, contentToProcess, true, settings.useThinking ); setCards(generatedCards); setStatus({ state: 'complete', message: 'Visualization ready.' }); setView('results'); } catch (error: any) { console.error(error); setStatus({ state: 'error', message: error.message || "An error occurred processing the paper." }); } }; const handleReset = () => { setView('input'); setResultViewMode('grid'); setCards([]); setBlogSections([]); setPaperStructure(null); setBlogLoadingStage('idle'); setCurrentGeneratingSection(-1); setStatus({ state: 'idle' }); setChatHistory([]); setIsChatOpen(false); setPaperTitle(''); setPaperContext(''); setFile(null); }; const handleSwitchToBlogView = async () => { // If we already have fully generated sections, just switch view if (blogSections.length > 0 && blogSections.every(s => !s.isLoading)) { setResultViewMode('blog'); return; } setIsBlogLoading(true); setResultViewMode('blog'); try { // Step 1: Analyze paper structure setBlogLoadingStage('analyzing'); const structure = await analyzePaperStructure( settings.apiKey, settings.model, paperContext, true, settings.useThinking ); setPaperStructure(structure); setPaperTitle(structure.paperTitle || paperTitle); // Create placeholder sections with loading state const placeholderSections: BlogSection[] = structure.sections.map((plan, index) => ({ id: plan.id, title: plan.title, content: '', isLoading: true, visualizationType: plan.suggestedVisualization })); setBlogSections(placeholderSections); // Step 2: Generate, validate, and repair each section progressively setBlogLoadingStage('generating'); const paperContextInfo = { title: structure.paperTitle, abstract: structure.paperAbstract, mainContribution: structure.mainContribution, keyTerms: structure.keyTerms }; for (let i = 0; i < structure.sections.length; i++) { setCurrentGeneratingSection(i); try { const generatedSection = await generateAndValidateSection( settings.apiKey, settings.model, paperContext, structure.sections[i], i, structure.sections.length, paperContextInfo, true, settings.useThinking, 2, // max repair attempts (stage, message) => { // Update UI with current status if (stage === 'validating') { setBlogLoadingStage('validating'); } else if (stage === 'generating' || stage === 'repairing') { setBlogLoadingStage('generating'); } setSectionStatus(message); } ); // Update the specific section in state setBlogSections(prev => prev.map((section, idx) => idx === i ? { ...generatedSection, isLoading: false } : section )); } catch (sectionError: any) { // Mark section as errored but continue with others setBlogSections(prev => prev.map((section, idx) => idx === i ? { ...section, isLoading: false, error: sectionError.message, content: `Failed to generate this section: ${sectionError.message}` } : section )); } } setCurrentGeneratingSection(-1); setSectionStatus(''); setBlogLoadingStage('idle'); } catch (error: any) { console.error('Failed to generate blog view:', error); setStatus({ state: 'error', message: 'Failed to analyze paper: ' + error.message }); setBlogLoadingStage('idle'); } finally { setIsBlogLoading(false); } }; // Retry a failed section const handleRetrySection = async (sectionIndex: number) => { if (!paperStructure || !paperStructure.sections[sectionIndex]) { console.error('Cannot retry: missing paper structure or section plan'); return; } setRetryingSectionIndex(sectionIndex); try { const sectionPlan = paperStructure.sections[sectionIndex]; const paperContextInfo = { title: paperStructure.paperTitle, abstract: paperStructure.paperAbstract, mainContribution: paperStructure.mainContribution, keyTerms: paperStructure.keyTerms }; const generatedSection = await generateAndValidateSection( settings.apiKey, settings.model, paperContext, sectionPlan, sectionIndex, paperStructure.sections.length, paperContextInfo, true, settings.useThinking, 2, (stage, message) => { setSectionStatus(message); } ); // Update the section in state setBlogSections(prev => prev.map((section, idx) => idx === sectionIndex ? { ...generatedSection, isLoading: false, error: undefined } : section )); setSectionStatus(''); } catch (error: any) { console.error(`Failed to retry section ${sectionIndex}:`, error); setBlogSections(prev => prev.map((section, idx) => idx === sectionIndex ? { ...section, isLoading: false, error: error.message || 'Retry failed. Please try again.' } : section )); } finally { setRetryingSectionIndex(-1); } }; const handleExpandCard = async (card: BentoCardData) => { if (card.expandedContent || card.isLoadingDetails) return; setCards(prev => prev.map(c => c.id === card.id ? { ...c, isLoadingDetails: true } : c)); try { const details = await expandBentoCard( settings.apiKey, settings.model, card.title, card.detailPrompt, paperContext, settings.useThinking ); setCards(prev => prev.map(c => c.id === card.id ? { ...c, expandedContent: details, isLoadingDetails: false } : c)); } catch (error) { setCards(prev => prev.map(c => c.id === card.id ? { ...c, isLoadingDetails: false, expandedContent: "Failed to load details." } : c)); } }; const handleResizeCard = (id: string, deltaCol: number, deltaRow: number) => { setCards(prev => prev.map(c => { if (c.id === id) { const newCol = Math.max(1, Math.min(4, c.colSpan + deltaCol)); const newRow = Math.max(1, Math.min(3, c.rowSpan + deltaRow)); return { ...c, colSpan: newCol, rowSpan: newRow }; } return c; })); }; const handleRateCard = (id: string, rating: number) => { setCards(prev => prev.map(c => c.id === id ? { ...c, rating } : c)); console.log(`Rated card ${id}: ${rating}`); }; const handleExport = async () => { const targetRef = resultViewMode === 'blog' ? blogRef.current : gridRef.current; if (!targetRef) return; // @ts-ignore if (!window.html2canvas) { alert("Export module not loaded yet."); return; } // @ts-ignore const canvas = await window.html2canvas(targetRef, { backgroundColor: settings.theme === 'dark' ? '#0f172a' : '#f8fafc', scale: 2, useCORS: true, logging: false, windowWidth: targetRef.scrollWidth, windowHeight: targetRef.scrollHeight, }); if (resultViewMode === 'blog') { // Export as PDF for blog view // @ts-ignore if (!window.jspdf) { alert("PDF export module not loaded yet."); return; } // @ts-ignore const { jsPDF } = window.jspdf; const imgData = canvas.toDataURL('image/png'); // A4 dimensions in mm const pdfWidth = 210; const pdfHeight = 297; // Calculate the scaled dimensions to fit width const ratio = pdfWidth / (canvas.width / 2); // Divide by scale factor (2) const scaledHeight = (canvas.height / 2) * ratio; // Create PDF const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); // Calculate how many pages we need const pageCount = Math.ceil(scaledHeight / pdfHeight); for (let page = 0; page < pageCount; page++) { if (page > 0) { pdf.addPage(); } // Position the image so the correct portion shows on each page const yOffset = -(page * pdfHeight); pdf.addImage(imgData, 'PNG', 0, yOffset, pdfWidth, scaledHeight); } pdf.save('paper-blog.pdf'); } else { // Export as PNG for grid view const link = document.createElement('a'); link.download = 'bento-summary.png'; link.href = canvas.toDataURL(); link.click(); } }; const handleShare = async () => { if (navigator.share) { try { await navigator.share({ title: 'Paper Summary', text: 'Check out this visual summary generated by PaperStack!', url: window.location.href }); } catch (err) { console.error("Share failed", err); } } else { alert("Sharing is not supported on this browser."); } }; const handleSendMessage = async (text: string) => { const newUserMsg: ChatMessage = { id: Date.now().toString(), role: 'user', text, timestamp: Date.now() }; setChatHistory(prev => [...prev, newUserMsg]); setIsChatProcessing(true); try { const responseText = await chatWithDocument(settings.apiKey, settings.model, chatHistory, text, paperContext || JSON.stringify(cards)); const newBotMsg: ChatMessage = { id: (Date.now()+1).toString(), role: 'model', text: responseText, timestamp: Date.now() }; setChatHistory(prev => [...prev, newBotMsg]); } catch (error) { const errorMsg: ChatMessage = { id: (Date.now()+1).toString(), role: 'system', text: "Failed to get response.", timestamp: Date.now() }; setChatHistory(prev => [...prev, errorMsg]); } finally { setIsChatProcessing(false); } }; return (
{/* Navbar */}
{/* Input View */} {view === 'input' && (

Visual Research Summaries

Upload your research paper (PDF) and instantly transform it into a rich, interactive Bento grid.
Powered by Google Gemini.

{/* API Keys & Model Selection */}
{/* Model Selection Dropdown */}
{/* API Key Input */}
setSettings({ ...settings, apiKey: e.target.value })} 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" />

Get your key from Google AI Studio

{/* Thinking Toggle Switch */}
Thinking 32k Budget
{/* PDF Upload - Main Action */}
{/* Generate Button */} {status.state === 'error' && (
{status.message}
)}
)} {/* Results View */} {view === 'results' && (
{/* View Mode Toggle - Grid vs Blog */}
{/* Grid View */} {resultViewMode === 'grid' && ( <> {/* Controls */}

Summary Grid {settings.useThinking && ( Deep Thought )}

{/* Grid */}
{cards.map((card) => ( ))}
)} {/* Blog View */} {resultViewMode === 'blog' && ( setSettings(prev => ({ ...prev, readingPreferences: prefs }))} /> )} {/* Floating Chat Trigger */}
)}
setIsChatOpen(false)} messages={chatHistory} onSendMessage={handleSendMessage} isProcessing={isChatProcessing} />
); }; export default App;