Spaces:
Running
Running
Upload 14 files
Browse files- App.tsx +475 -0
- README.md +20 -12
- components/Background.tsx +111 -0
- components/BentoCard.tsx +263 -0
- components/ChatBot.tsx +101 -0
- components/MermaidDiagram.tsx +52 -0
- index.html +95 -19
- index.tsx +15 -0
- metadata.json +5 -0
- package.json +25 -0
- services/geminiService.ts +196 -0
- tsconfig.json +29 -0
- types.ts +37 -0
- vite.config.ts +23 -0
App.tsx
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import React, { useState, useRef } from 'react';
|
| 3 |
+
import { Moon, Sun, Upload, FileText, Download, Share2, MessageSquare, AlertCircle, LayoutGrid, List, Grid, ChevronLeft, ArrowRight, X, BrainCircuit } from 'lucide-react';
|
| 4 |
+
import Background from './components/Background';
|
| 5 |
+
import BentoCard from './components/BentoCard';
|
| 6 |
+
import ChatBot from './components/ChatBot';
|
| 7 |
+
import { BentoCardData, ChatMessage, AppSettings, ProcessingStatus } from './types';
|
| 8 |
+
import { generateBentoCards, expandBentoCard, chatWithDocument } from './services/geminiService';
|
| 9 |
+
|
| 10 |
+
const App: React.FC = () => {
|
| 11 |
+
// Settings
|
| 12 |
+
const [settings, setSettings] = useState<AppSettings>({
|
| 13 |
+
apiKey: '',
|
| 14 |
+
model: 'gemini-flash-latest',
|
| 15 |
+
theme: 'light',
|
| 16 |
+
layoutMode: 'auto',
|
| 17 |
+
useThinking: false
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
// Inputs
|
| 21 |
+
const [file, setFile] = useState<File | null>(null);
|
| 22 |
+
|
| 23 |
+
// State
|
| 24 |
+
const [view, setView] = useState<'input' | 'results'>('input');
|
| 25 |
+
const [cards, setCards] = useState<BentoCardData[]>([]);
|
| 26 |
+
const [status, setStatus] = useState<ProcessingStatus>({ state: 'idle' });
|
| 27 |
+
const [paperContext, setPaperContext] = useState<string>(''); // Stores the raw text/base64
|
| 28 |
+
const [paperTitle, setPaperTitle] = useState<string>('');
|
| 29 |
+
|
| 30 |
+
// Chat
|
| 31 |
+
const [isChatOpen, setIsChatOpen] = useState(false);
|
| 32 |
+
const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);
|
| 33 |
+
const [isChatProcessing, setIsChatProcessing] = useState(false);
|
| 34 |
+
|
| 35 |
+
const gridRef = useRef<HTMLDivElement>(null);
|
| 36 |
+
|
| 37 |
+
const toggleTheme = () => {
|
| 38 |
+
const newTheme = settings.theme === 'dark' ? 'light' : 'dark';
|
| 39 |
+
setSettings(prev => ({ ...prev, theme: newTheme }));
|
| 40 |
+
if (newTheme === 'dark') {
|
| 41 |
+
document.documentElement.classList.add('dark');
|
| 42 |
+
} else {
|
| 43 |
+
document.documentElement.classList.remove('dark');
|
| 44 |
+
}
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 48 |
+
if (e.target.files && e.target.files[0]) {
|
| 49 |
+
setFile(e.target.files[0]);
|
| 50 |
+
}
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
const handleProcess = async () => {
|
| 54 |
+
if (!settings.apiKey) {
|
| 55 |
+
setStatus({ state: 'error', message: "Please enter your Gemini API Key." });
|
| 56 |
+
return;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
if (!file) {
|
| 60 |
+
setStatus({ state: 'error', message: "Please upload a PDF file." });
|
| 61 |
+
return;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
setStatus({ state: 'reading', message: 'Reading PDF file...' });
|
| 65 |
+
|
| 66 |
+
try {
|
| 67 |
+
let contentToProcess = "";
|
| 68 |
+
const contextTitle = file.name;
|
| 69 |
+
|
| 70 |
+
// PDF UPLOAD FLOW
|
| 71 |
+
contentToProcess = await new Promise((resolve, reject) => {
|
| 72 |
+
const reader = new FileReader();
|
| 73 |
+
reader.onload = () => {
|
| 74 |
+
const result = reader.result as string;
|
| 75 |
+
const base64 = result.split(',')[1];
|
| 76 |
+
resolve(base64);
|
| 77 |
+
};
|
| 78 |
+
reader.onerror = reject;
|
| 79 |
+
reader.readAsDataURL(file);
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
if (file.type !== 'application/pdf') {
|
| 83 |
+
throw new Error("File must be a PDF.");
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
setPaperContext(contentToProcess);
|
| 87 |
+
setPaperTitle(contextTitle);
|
| 88 |
+
|
| 89 |
+
setStatus({ state: 'analyzing', message: settings.useThinking ? 'Thinking deeply about paper structure...' : 'Analyzing paper structure...' });
|
| 90 |
+
|
| 91 |
+
const generatedCards = await generateBentoCards(
|
| 92 |
+
settings.apiKey,
|
| 93 |
+
settings.model,
|
| 94 |
+
contentToProcess,
|
| 95 |
+
true,
|
| 96 |
+
settings.useThinking
|
| 97 |
+
);
|
| 98 |
+
|
| 99 |
+
setCards(generatedCards);
|
| 100 |
+
setStatus({ state: 'complete', message: 'Visualization ready.' });
|
| 101 |
+
setView('results');
|
| 102 |
+
|
| 103 |
+
} catch (error: any) {
|
| 104 |
+
console.error(error);
|
| 105 |
+
setStatus({ state: 'error', message: error.message || "An error occurred processing the paper." });
|
| 106 |
+
}
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
const handleReset = () => {
|
| 110 |
+
setView('input');
|
| 111 |
+
setCards([]);
|
| 112 |
+
setStatus({ state: 'idle' });
|
| 113 |
+
setChatHistory([]);
|
| 114 |
+
setIsChatOpen(false);
|
| 115 |
+
setPaperTitle('');
|
| 116 |
+
setPaperContext('');
|
| 117 |
+
setFile(null);
|
| 118 |
+
};
|
| 119 |
+
|
| 120 |
+
const handleExpandCard = async (card: BentoCardData) => {
|
| 121 |
+
if (card.expandedContent || card.isLoadingDetails) return;
|
| 122 |
+
|
| 123 |
+
setCards(prev => prev.map(c => c.id === card.id ? { ...c, isLoadingDetails: true } : c));
|
| 124 |
+
|
| 125 |
+
try {
|
| 126 |
+
const details = await expandBentoCard(
|
| 127 |
+
settings.apiKey,
|
| 128 |
+
settings.model,
|
| 129 |
+
card.title,
|
| 130 |
+
card.detailPrompt,
|
| 131 |
+
paperContext,
|
| 132 |
+
settings.useThinking
|
| 133 |
+
);
|
| 134 |
+
|
| 135 |
+
setCards(prev => prev.map(c => c.id === card.id ? { ...c, expandedContent: details, isLoadingDetails: false } : c));
|
| 136 |
+
} catch (error) {
|
| 137 |
+
setCards(prev => prev.map(c => c.id === card.id ? { ...c, isLoadingDetails: false, expandedContent: "Failed to load details." } : c));
|
| 138 |
+
}
|
| 139 |
+
};
|
| 140 |
+
|
| 141 |
+
const handleResizeCard = (id: string, deltaCol: number, deltaRow: number) => {
|
| 142 |
+
setCards(prev => prev.map(c => {
|
| 143 |
+
if (c.id === id) {
|
| 144 |
+
const newCol = Math.max(1, Math.min(4, c.colSpan + deltaCol));
|
| 145 |
+
const newRow = Math.max(1, Math.min(3, c.rowSpan + deltaRow));
|
| 146 |
+
return { ...c, colSpan: newCol, rowSpan: newRow };
|
| 147 |
+
}
|
| 148 |
+
return c;
|
| 149 |
+
}));
|
| 150 |
+
};
|
| 151 |
+
|
| 152 |
+
const handleRateCard = (id: string, rating: number) => {
|
| 153 |
+
setCards(prev => prev.map(c => c.id === id ? { ...c, rating } : c));
|
| 154 |
+
console.log(`Rated card ${id}: ${rating}`);
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
const handleExport = async () => {
|
| 158 |
+
if (!gridRef.current) return;
|
| 159 |
+
// @ts-ignore
|
| 160 |
+
if (window.html2canvas) {
|
| 161 |
+
// @ts-ignore
|
| 162 |
+
const canvas = await window.html2canvas(gridRef.current, {
|
| 163 |
+
backgroundColor: settings.theme === 'dark' ? '#0f172a' : '#f8fafc',
|
| 164 |
+
scale: 2,
|
| 165 |
+
useCORS: true,
|
| 166 |
+
logging: false
|
| 167 |
+
});
|
| 168 |
+
const link = document.createElement('a');
|
| 169 |
+
link.download = 'bento-summary.png';
|
| 170 |
+
link.href = canvas.toDataURL();
|
| 171 |
+
link.click();
|
| 172 |
+
} else {
|
| 173 |
+
alert("Export module not loaded yet.");
|
| 174 |
+
}
|
| 175 |
+
};
|
| 176 |
+
|
| 177 |
+
const handleShare = async () => {
|
| 178 |
+
if (navigator.share) {
|
| 179 |
+
try {
|
| 180 |
+
await navigator.share({
|
| 181 |
+
title: 'Paper Summary',
|
| 182 |
+
text: 'Check out this visual summary generated by BentoMind!',
|
| 183 |
+
url: window.location.href
|
| 184 |
+
});
|
| 185 |
+
} catch (err) {
|
| 186 |
+
console.error("Share failed", err);
|
| 187 |
+
}
|
| 188 |
+
} else {
|
| 189 |
+
alert("Sharing is not supported on this browser.");
|
| 190 |
+
}
|
| 191 |
+
};
|
| 192 |
+
|
| 193 |
+
const handleSendMessage = async (text: string) => {
|
| 194 |
+
const newUserMsg: ChatMessage = { id: Date.now().toString(), role: 'user', text, timestamp: Date.now() };
|
| 195 |
+
setChatHistory(prev => [...prev, newUserMsg]);
|
| 196 |
+
setIsChatProcessing(true);
|
| 197 |
+
|
| 198 |
+
try {
|
| 199 |
+
const responseText = await chatWithDocument(settings.apiKey, settings.model, chatHistory, text, paperContext || JSON.stringify(cards));
|
| 200 |
+
const newBotMsg: ChatMessage = { id: (Date.now()+1).toString(), role: 'model', text: responseText, timestamp: Date.now() };
|
| 201 |
+
setChatHistory(prev => [...prev, newBotMsg]);
|
| 202 |
+
} catch (error) {
|
| 203 |
+
const errorMsg: ChatMessage = { id: (Date.now()+1).toString(), role: 'system', text: "Failed to get response.", timestamp: Date.now() };
|
| 204 |
+
setChatHistory(prev => [...prev, errorMsg]);
|
| 205 |
+
} finally {
|
| 206 |
+
setIsChatProcessing(false);
|
| 207 |
+
}
|
| 208 |
+
};
|
| 209 |
+
|
| 210 |
+
return (
|
| 211 |
+
<div className={`min-h-screen w-full text-gray-900 dark:text-gray-100 overflow-x-hidden transition-colors duration-500 ${settings.theme}`}>
|
| 212 |
+
<Background theme={settings.theme} />
|
| 213 |
+
|
| 214 |
+
{/* Navbar */}
|
| 215 |
+
<nav className="fixed top-0 w-full z-50 px-6 py-4 flex justify-between items-center glass-panel border-b border-gray-200 dark:border-white/5">
|
| 216 |
+
<div className="flex items-center gap-4">
|
| 217 |
+
{view === 'results' && (
|
| 218 |
+
<button
|
| 219 |
+
onClick={handleReset}
|
| 220 |
+
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 transition-colors"
|
| 221 |
+
title="Start Over"
|
| 222 |
+
>
|
| 223 |
+
<ChevronLeft size={20} />
|
| 224 |
+
</button>
|
| 225 |
+
)}
|
| 226 |
+
<div className="flex items-center gap-2">
|
| 227 |
+
<div className="w-8 h-8 bg-gradient-to-br from-brand-500 to-purple-600 rounded-lg flex items-center justify-center text-white font-bold shadow-lg">
|
| 228 |
+
B
|
| 229 |
+
</div>
|
| 230 |
+
<span className="text-xl font-display font-bold tracking-tight hidden md:block">BentoMind</span>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
<div className="flex gap-4 items-center">
|
| 235 |
+
{view === 'results' && (
|
| 236 |
+
<div className="hidden md:flex items-center gap-2 px-4 py-1.5 bg-gray-100 dark:bg-gray-800 rounded-full border border-gray-200 dark:border-gray-700">
|
| 237 |
+
<FileText size={14} className="text-gray-500" />
|
| 238 |
+
<span className="text-sm font-medium max-w-[200px] truncate opacity-80">{paperTitle || 'Analysis'}</span>
|
| 239 |
+
</div>
|
| 240 |
+
)}
|
| 241 |
+
<button onClick={toggleTheme} className="p-2 rounded-full bg-gray-100 dark:bg-white/10 hover:bg-gray-200 dark:hover:bg-white/20 transition-colors shadow-sm">
|
| 242 |
+
{settings.theme === 'dark' ? <Sun size={20} /> : <Moon size={20} />}
|
| 243 |
+
</button>
|
| 244 |
+
</div>
|
| 245 |
+
</nav>
|
| 246 |
+
|
| 247 |
+
<main className="pt-32 pb-20 px-4 md:px-8 max-w-7xl mx-auto relative z-10 min-h-[80vh]">
|
| 248 |
+
|
| 249 |
+
{/* Input View */}
|
| 250 |
+
{view === 'input' && (
|
| 251 |
+
<section className="flex flex-col gap-8 animate-in fade-in zoom-in-95 duration-700 items-center justify-center min-h-[60vh]">
|
| 252 |
+
<div className="text-center space-y-6 mb-4 max-w-3xl">
|
| 253 |
+
<h1 className="text-5xl md:text-7xl font-display font-bold bg-clip-text text-transparent bg-gradient-to-r from-gray-900 via-brand-600 to-purple-600 dark:from-white dark:via-brand-400 dark:to-purple-400 leading-tight pb-2">
|
| 254 |
+
Visual Research Summaries
|
| 255 |
+
</h1>
|
| 256 |
+
<p className="text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-2xl mx-auto font-light">
|
| 257 |
+
Upload your research paper (PDF) and instantly transform it into a rich, interactive Bento grid.
|
| 258 |
+
<br className="hidden md:block" />
|
| 259 |
+
Powered by <span className="font-semibold text-brand-600 dark:text-brand-400">Gemini 3.0 Pro</span>.
|
| 260 |
+
</p>
|
| 261 |
+
</div>
|
| 262 |
+
|
| 263 |
+
<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">
|
| 264 |
+
{/* API Key */}
|
| 265 |
+
<div className="relative">
|
| 266 |
+
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
| 267 |
+
<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>
|
| 268 |
+
</div>
|
| 269 |
+
<input
|
| 270 |
+
type="password"
|
| 271 |
+
placeholder="Enter Gemini API Key"
|
| 272 |
+
value={settings.apiKey}
|
| 273 |
+
onChange={(e) => setSettings({...settings, apiKey: e.target.value})}
|
| 274 |
+
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"
|
| 275 |
+
/>
|
| 276 |
+
</div>
|
| 277 |
+
|
| 278 |
+
{/* Model Select & Thinking Toggle */}
|
| 279 |
+
<div className="flex flex-col md:flex-row gap-3">
|
| 280 |
+
<div className="grid grid-cols-2 gap-3 flex-grow">
|
| 281 |
+
<button
|
| 282 |
+
onClick={() => setSettings({...settings, model: 'gemini-flash-latest'})}
|
| 283 |
+
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'}`}
|
| 284 |
+
>
|
| 285 |
+
<span>⚡ Flash</span>
|
| 286 |
+
<span className="text-xs opacity-60 font-normal">(Fast)</span>
|
| 287 |
+
</button>
|
| 288 |
+
<button
|
| 289 |
+
onClick={() => setSettings({...settings, model: 'gemini-3-pro-preview'})}
|
| 290 |
+
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'}`}
|
| 291 |
+
>
|
| 292 |
+
<span>🧠 Pro</span>
|
| 293 |
+
<span className="text-xs opacity-60 font-normal">(Smart)</span>
|
| 294 |
+
</button>
|
| 295 |
+
</div>
|
| 296 |
+
|
| 297 |
+
{/* Thinking Toggle Switch */}
|
| 298 |
+
<div
|
| 299 |
+
className={`
|
| 300 |
+
flex items-center justify-between px-4 py-2 rounded-xl border transition-all md:min-w-[180px]
|
| 301 |
+
${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'}
|
| 302 |
+
`}
|
| 303 |
+
>
|
| 304 |
+
<div className="flex items-center gap-3 mr-4">
|
| 305 |
+
<BrainCircuit size={20} className={settings.useThinking ? 'text-indigo-500 animate-pulse' : 'text-gray-400'} />
|
| 306 |
+
<div className="flex flex-col leading-none">
|
| 307 |
+
<span className={`text-sm font-bold ${settings.useThinking ? 'text-indigo-700 dark:text-indigo-300' : 'text-gray-600 dark:text-gray-400'}`}>Thinking</span>
|
| 308 |
+
<span className="text-[10px] opacity-60">32k Budget</span>
|
| 309 |
+
</div>
|
| 310 |
+
</div>
|
| 311 |
+
|
| 312 |
+
<button
|
| 313 |
+
onClick={() => setSettings(s => ({ ...s, useThinking: !s.useThinking }))}
|
| 314 |
+
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'}`}
|
| 315 |
+
>
|
| 316 |
+
<span
|
| 317 |
+
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`}
|
| 318 |
+
/>
|
| 319 |
+
</button>
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
|
| 323 |
+
{/* PDF Upload - Main Action */}
|
| 324 |
+
<div className="relative">
|
| 325 |
+
<input
|
| 326 |
+
type="file"
|
| 327 |
+
accept=".pdf"
|
| 328 |
+
onChange={handleFileChange}
|
| 329 |
+
className="hidden"
|
| 330 |
+
id="file-upload"
|
| 331 |
+
/>
|
| 332 |
+
<label
|
| 333 |
+
htmlFor="file-upload"
|
| 334 |
+
className={`
|
| 335 |
+
flex flex-col items-center justify-center gap-3 cursor-pointer rounded-2xl border-2 border-dashed transition-all py-12
|
| 336 |
+
${file
|
| 337 |
+
? 'bg-brand-50 dark:bg-brand-900/10 border-brand-500 text-brand-600 dark:text-brand-400'
|
| 338 |
+
: 'bg-gray-50/50 dark:bg-white/5 border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-white/10 text-gray-500 hover:border-brand-400 dark:hover:border-brand-400'}
|
| 339 |
+
`}
|
| 340 |
+
>
|
| 341 |
+
{file ? (
|
| 342 |
+
<>
|
| 343 |
+
<div className="p-3 bg-brand-100 dark:bg-brand-900/30 rounded-full">
|
| 344 |
+
<FileText size={32} className="text-brand-600 dark:text-brand-400" />
|
| 345 |
+
</div>
|
| 346 |
+
<div className="text-center">
|
| 347 |
+
<span className="font-bold text-lg">{file.name}</span>
|
| 348 |
+
<p className="text-sm opacity-70 mt-1">{(file.size / 1024 / 1024).toFixed(2)} MB</p>
|
| 349 |
+
</div>
|
| 350 |
+
<div className="mt-2 text-xs bg-brand-200 dark:bg-brand-800/40 px-2 py-1 rounded">
|
| 351 |
+
Click to change file
|
| 352 |
+
</div>
|
| 353 |
+
</>
|
| 354 |
+
) : (
|
| 355 |
+
<>
|
| 356 |
+
<div className="p-3 bg-gray-100 dark:bg-gray-800 rounded-full">
|
| 357 |
+
<Upload size={32} />
|
| 358 |
+
</div>
|
| 359 |
+
<div className="text-center">
|
| 360 |
+
<span className="font-bold text-lg">Upload PDF Paper</span>
|
| 361 |
+
<p className="text-sm opacity-70 mt-1">Max size 10MB</p>
|
| 362 |
+
</div>
|
| 363 |
+
</>
|
| 364 |
+
)}
|
| 365 |
+
</label>
|
| 366 |
+
</div>
|
| 367 |
+
|
| 368 |
+
{/* Generate Button */}
|
| 369 |
+
<button
|
| 370 |
+
onClick={handleProcess}
|
| 371 |
+
disabled={status.state !== 'idle' && status.state !== 'error'}
|
| 372 |
+
className={`
|
| 373 |
+
w-full py-4 rounded-xl font-bold text-lg shadow-xl hover:shadow-2xl hover:scale-[1.01] active:scale-[0.99] transition-all disabled:opacity-70 disabled:cursor-not-allowed flex items-center justify-center gap-3
|
| 374 |
+
${settings.useThinking
|
| 375 |
+
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white'
|
| 376 |
+
: 'bg-gradient-to-r from-gray-900 to-gray-800 dark:from-brand-600 dark:to-purple-600 text-white'
|
| 377 |
+
}
|
| 378 |
+
`}
|
| 379 |
+
>
|
| 380 |
+
{status.state !== 'idle' && status.state !== 'error' && status.state !== 'complete' ? (
|
| 381 |
+
<>
|
| 382 |
+
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
| 383 |
+
<span>{status.message}</span>
|
| 384 |
+
</>
|
| 385 |
+
) : (
|
| 386 |
+
<>
|
| 387 |
+
<span>{settings.useThinking ? 'Deep Analyze' : 'Generate Bento Grid'}</span>
|
| 388 |
+
<ArrowRight size={20} />
|
| 389 |
+
</>
|
| 390 |
+
)}
|
| 391 |
+
</button>
|
| 392 |
+
|
| 393 |
+
{status.state === 'error' && (
|
| 394 |
+
<div className="flex items-center gap-2 text-red-500 bg-red-50 dark:bg-red-900/20 p-3 rounded-lg text-sm animate-in fade-in slide-in-from-top-2">
|
| 395 |
+
<AlertCircle size={16} />
|
| 396 |
+
{status.message}
|
| 397 |
+
</div>
|
| 398 |
+
)}
|
| 399 |
+
</div>
|
| 400 |
+
</section>
|
| 401 |
+
)}
|
| 402 |
+
|
| 403 |
+
{/* Results View */}
|
| 404 |
+
{view === 'results' && (
|
| 405 |
+
<div className="animate-in fade-in slide-in-from-bottom-8 duration-700">
|
| 406 |
+
{/* Controls */}
|
| 407 |
+
<div className="flex justify-between items-center mb-6">
|
| 408 |
+
<h2 className="text-2xl font-display font-bold flex items-center gap-3">
|
| 409 |
+
Summary Grid
|
| 410 |
+
{settings.useThinking && (
|
| 411 |
+
<span className="text-xs font-medium bg-indigo-100 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 px-2 py-1 rounded-full border border-indigo-200 dark:border-indigo-800 flex items-center gap-1">
|
| 412 |
+
<BrainCircuit size={12} /> Deep Thought
|
| 413 |
+
</span>
|
| 414 |
+
)}
|
| 415 |
+
</h2>
|
| 416 |
+
<div className="flex gap-2">
|
| 417 |
+
<button onClick={handleShare} className="p-2 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 border border-gray-200 dark:border-gray-700 transition-colors text-gray-600 dark:text-gray-300">
|
| 418 |
+
<Share2 size={18} />
|
| 419 |
+
</button>
|
| 420 |
+
<button onClick={handleExport} className="p-2 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 border border-gray-200 dark:border-gray-700 transition-colors text-gray-600 dark:text-gray-300">
|
| 421 |
+
<Download size={18} />
|
| 422 |
+
</button>
|
| 423 |
+
<div className="w-px h-8 bg-gray-200 dark:bg-gray-700 mx-2"></div>
|
| 424 |
+
<button onClick={() => setSettings({...settings, layoutMode: 'grid'})} className={`p-2 rounded-lg border transition-colors ${settings.layoutMode === 'grid' ? 'bg-brand-50 dark:bg-brand-900/30 border-brand-500 text-brand-600' : 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-500'}`}>
|
| 425 |
+
<Grid size={18} />
|
| 426 |
+
</button>
|
| 427 |
+
<button onClick={() => setSettings({...settings, layoutMode: 'list'})} className={`p-2 rounded-lg border transition-colors ${settings.layoutMode === 'list' ? 'bg-brand-50 dark:bg-brand-900/30 border-brand-500 text-brand-600' : 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-500'}`}>
|
| 428 |
+
<List size={18} />
|
| 429 |
+
</button>
|
| 430 |
+
</div>
|
| 431 |
+
</div>
|
| 432 |
+
|
| 433 |
+
{/* Grid */}
|
| 434 |
+
<div
|
| 435 |
+
ref={gridRef}
|
| 436 |
+
className="grid grid-cols-1 md:grid-cols-4 auto-rows-[minmax(180px,auto)] gap-4 md:gap-6 p-1 grid-flow-dense"
|
| 437 |
+
>
|
| 438 |
+
{cards.map((card) => (
|
| 439 |
+
<BentoCard
|
| 440 |
+
key={card.id}
|
| 441 |
+
card={card}
|
| 442 |
+
onExpand={handleExpandCard}
|
| 443 |
+
onRate={handleRateCard}
|
| 444 |
+
onResize={handleResizeCard}
|
| 445 |
+
layoutMode={settings.layoutMode}
|
| 446 |
+
/>
|
| 447 |
+
))}
|
| 448 |
+
</div>
|
| 449 |
+
|
| 450 |
+
{/* Floating Chat Trigger */}
|
| 451 |
+
<button
|
| 452 |
+
onClick={() => setIsChatOpen(true)}
|
| 453 |
+
className="fixed bottom-8 right-8 p-4 bg-brand-600 hover:bg-brand-500 text-white rounded-full shadow-2xl hover:scale-110 transition-all z-40 group"
|
| 454 |
+
>
|
| 455 |
+
<MessageSquare size={24} />
|
| 456 |
+
<span className="absolute right-full mr-4 top-1/2 -translate-y-1/2 px-3 py-1 bg-gray-900 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
|
| 457 |
+
Chat with Paper
|
| 458 |
+
</span>
|
| 459 |
+
</button>
|
| 460 |
+
</div>
|
| 461 |
+
)}
|
| 462 |
+
</main>
|
| 463 |
+
|
| 464 |
+
<ChatBot
|
| 465 |
+
isOpen={isChatOpen}
|
| 466 |
+
onClose={() => setIsChatOpen(false)}
|
| 467 |
+
messages={chatHistory}
|
| 468 |
+
onSendMessage={handleSendMessage}
|
| 469 |
+
isProcessing={isChatProcessing}
|
| 470 |
+
/>
|
| 471 |
+
</div>
|
| 472 |
+
);
|
| 473 |
+
};
|
| 474 |
+
|
| 475 |
+
export default App;
|
README.md
CHANGED
|
@@ -1,12 +1,20 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div align="center">
|
| 2 |
+
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
| 3 |
+
</div>
|
| 4 |
+
|
| 5 |
+
# Run and deploy your AI Studio app
|
| 6 |
+
|
| 7 |
+
This contains everything you need to run your app locally.
|
| 8 |
+
|
| 9 |
+
View your app in AI Studio: https://ai.studio/apps/drive/10fTu3NHihfpsNHHaQcKJcKfCbWtD-ydg
|
| 10 |
+
|
| 11 |
+
## Run Locally
|
| 12 |
+
|
| 13 |
+
**Prerequisites:** Node.js
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
1. Install dependencies:
|
| 17 |
+
`npm install`
|
| 18 |
+
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
| 19 |
+
3. Run the app:
|
| 20 |
+
`npm run dev`
|
components/Background.tsx
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import React, { useEffect, useRef } from 'react';
|
| 3 |
+
|
| 4 |
+
const Background: React.FC<{ theme: 'light' | 'dark' }> = ({ theme }) => {
|
| 5 |
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
| 6 |
+
|
| 7 |
+
useEffect(() => {
|
| 8 |
+
const canvas = canvasRef.current;
|
| 9 |
+
if (!canvas) return;
|
| 10 |
+
const ctx = canvas.getContext('2d');
|
| 11 |
+
if (!ctx) return;
|
| 12 |
+
|
| 13 |
+
let animationFrameId: number;
|
| 14 |
+
let time = 0;
|
| 15 |
+
|
| 16 |
+
const resize = () => {
|
| 17 |
+
canvas.width = window.innerWidth;
|
| 18 |
+
canvas.height = window.innerHeight;
|
| 19 |
+
};
|
| 20 |
+
window.addEventListener('resize', resize);
|
| 21 |
+
resize();
|
| 22 |
+
|
| 23 |
+
const points: { x: number; y: number; z: number }[] = [];
|
| 24 |
+
const numPoints = 50; // Reduced count slightly
|
| 25 |
+
const size = 300;
|
| 26 |
+
|
| 27 |
+
// Create a cube of dots
|
| 28 |
+
for (let i = 0; i < numPoints; i++) {
|
| 29 |
+
points.push({
|
| 30 |
+
x: (Math.random() - 0.5) * size,
|
| 31 |
+
y: (Math.random() - 0.5) * size,
|
| 32 |
+
z: (Math.random() - 0.5) * size,
|
| 33 |
+
});
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const draw = () => {
|
| 37 |
+
// Varied, slower speed
|
| 38 |
+
time += 0.001 + Math.sin(Date.now() * 0.0005) * 0.0005;
|
| 39 |
+
|
| 40 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 41 |
+
|
| 42 |
+
const cx = canvas.width / 2;
|
| 43 |
+
const cy = canvas.height / 2;
|
| 44 |
+
|
| 45 |
+
// Much lower opacity for better contrast with content
|
| 46 |
+
const color = theme === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.05)';
|
| 47 |
+
const connectionColor = theme === 'dark' ? 'rgba(255, 255, 255, 0.02)' : 'rgba(0, 0, 0, 0.02)';
|
| 48 |
+
|
| 49 |
+
ctx.fillStyle = color;
|
| 50 |
+
ctx.strokeStyle = connectionColor;
|
| 51 |
+
|
| 52 |
+
// Rotate points
|
| 53 |
+
const rotatedPoints = points.map(p => {
|
| 54 |
+
// Rotate Y
|
| 55 |
+
let x = p.x * Math.cos(time) - p.z * Math.sin(time);
|
| 56 |
+
let z = p.x * Math.sin(time) + p.z * Math.cos(time);
|
| 57 |
+
// Rotate X
|
| 58 |
+
let y = p.y * Math.cos(time * 0.5) - z * Math.sin(time * 0.5);
|
| 59 |
+
z = p.y * Math.sin(time * 0.5) + z * Math.cos(time * 0.5);
|
| 60 |
+
|
| 61 |
+
// Perspective projection
|
| 62 |
+
const scale = 800 / (800 + z);
|
| 63 |
+
return {
|
| 64 |
+
x: cx + x * scale,
|
| 65 |
+
y: cy + y * scale,
|
| 66 |
+
scale
|
| 67 |
+
};
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
// Draw connections
|
| 71 |
+
ctx.beginPath();
|
| 72 |
+
for (let i = 0; i < rotatedPoints.length; i++) {
|
| 73 |
+
for (let j = i + 1; j < rotatedPoints.length; j++) {
|
| 74 |
+
const dx = rotatedPoints[i].x - rotatedPoints[j].x;
|
| 75 |
+
const dy = rotatedPoints[i].y - rotatedPoints[j].y;
|
| 76 |
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
| 77 |
+
if (dist < 120) {
|
| 78 |
+
ctx.moveTo(rotatedPoints[i].x, rotatedPoints[i].y);
|
| 79 |
+
ctx.lineTo(rotatedPoints[j].x, rotatedPoints[j].y);
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
ctx.stroke();
|
| 84 |
+
|
| 85 |
+
// Draw points
|
| 86 |
+
rotatedPoints.forEach(p => {
|
| 87 |
+
ctx.beginPath();
|
| 88 |
+
ctx.arc(p.x, p.y, 2 * p.scale, 0, Math.PI * 2);
|
| 89 |
+
ctx.fill();
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
animationFrameId = requestAnimationFrame(draw);
|
| 93 |
+
};
|
| 94 |
+
|
| 95 |
+
draw();
|
| 96 |
+
|
| 97 |
+
return () => {
|
| 98 |
+
window.removeEventListener('resize', resize);
|
| 99 |
+
cancelAnimationFrame(animationFrameId);
|
| 100 |
+
};
|
| 101 |
+
}, [theme]);
|
| 102 |
+
|
| 103 |
+
return (
|
| 104 |
+
<canvas
|
| 105 |
+
ref={canvasRef}
|
| 106 |
+
className="fixed top-0 left-0 w-full h-full -z-10 pointer-events-none transition-opacity duration-1000 opacity-20"
|
| 107 |
+
/>
|
| 108 |
+
);
|
| 109 |
+
};
|
| 110 |
+
|
| 111 |
+
export default Background;
|
components/BentoCard.tsx
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import React, { useState } from 'react';
|
| 3 |
+
import { BentoCardData } from '../types';
|
| 4 |
+
import { Maximize2, Minimize2, BookOpen, MessageSquare, Activity, Quote, Zap, ThumbsUp, ThumbsDown, ArrowRight, FileText, MoveHorizontal, MoveVertical, Plus, Minus } from 'lucide-react';
|
| 5 |
+
import ReactMarkdown from 'react-markdown';
|
| 6 |
+
import MermaidDiagram from './MermaidDiagram';
|
| 7 |
+
|
| 8 |
+
interface Props {
|
| 9 |
+
card: BentoCardData;
|
| 10 |
+
onExpand: (card: BentoCardData) => void;
|
| 11 |
+
onRate: (id: string, rating: number) => void;
|
| 12 |
+
onResize: (id: string, deltaCol: number, deltaRow: number) => void;
|
| 13 |
+
layoutMode: 'auto' | 'grid' | 'list';
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const BentoCard: React.FC<Props> = ({ card, onExpand, onRate, onResize, layoutMode }) => {
|
| 17 |
+
const [isExpanded, setIsExpanded] = useState(false);
|
| 18 |
+
|
| 19 |
+
// Visual Configurations based on Card Type
|
| 20 |
+
const getTypeConfig = () => {
|
| 21 |
+
switch (card.type) {
|
| 22 |
+
case 'stat':
|
| 23 |
+
return {
|
| 24 |
+
icon: <Activity className="w-5 h-5" />,
|
| 25 |
+
color: 'text-emerald-600 dark:text-emerald-400',
|
| 26 |
+
bg: 'bg-emerald-100 dark:bg-emerald-900/30',
|
| 27 |
+
border: 'border-emerald-200 dark:border-emerald-800',
|
| 28 |
+
gradient: 'from-emerald-500/10 to-teal-500/5',
|
| 29 |
+
graphic: (
|
| 30 |
+
<svg viewBox="0 0 100 40" className="w-full h-full opacity-20 text-emerald-500" fill="none" stroke="currentColor" strokeWidth="2">
|
| 31 |
+
<path d="M0 35 C 20 35, 20 15, 40 15 S 60 25, 80 5 L 100 0" />
|
| 32 |
+
<path d="M0 40 L 100 40 L 100 0 L 80 5 C 60 25, 60 15, 40 15 C 20 15, 20 35, 0 35 Z" fill="currentColor" stroke="none" opacity="0.2" />
|
| 33 |
+
</svg>
|
| 34 |
+
)
|
| 35 |
+
};
|
| 36 |
+
case 'quote':
|
| 37 |
+
return {
|
| 38 |
+
icon: <Quote className="w-5 h-5" />,
|
| 39 |
+
color: 'text-amber-600 dark:text-amber-400',
|
| 40 |
+
bg: 'bg-amber-100 dark:bg-amber-900/30',
|
| 41 |
+
border: 'border-amber-200 dark:border-amber-800',
|
| 42 |
+
gradient: 'from-amber-500/10 to-orange-500/5',
|
| 43 |
+
graphic: (
|
| 44 |
+
<svg width="120" height="120" viewBox="0 0 24 24" className="absolute -right-2 -bottom-6 text-amber-500 opacity-10 transform rotate-12" fill="currentColor">
|
| 45 |
+
<path d="M14.017 21L14.017 18C14.017 16.8954 14.9124 16 16.017 16H19.017C19.5693 16 20.017 15.5523 20.017 15V9C20.017 8.44772 19.5693 8 19.017 8H15.017C14.4647 8 14.017 7.55228 14.017 7V3H19.017C20.6739 3 22.017 4.34315 22.017 6V15C22.017 16.6569 20.6739 18 19.017 18H16.017V21H14.017ZM5.0166 21L5.0166 18C5.0166 16.8954 5.91203 16 7.0166 16H10.0166C10.5689 16 11.0166 15.5523 11.0166 15V9C11.0166 8.44772 10.5689 8 10.0166 8H6.0166C5.46432 8 5.0166 7.55228 5.0166 7V3H10.0166C11.6735 3 13.0166 4.34315 13.0166 6V15C13.0166 16.6569 11.6735 18 10.0166 18H7.0166V21H5.0166Z" />
|
| 46 |
+
</svg>
|
| 47 |
+
)
|
| 48 |
+
};
|
| 49 |
+
case 'process':
|
| 50 |
+
return {
|
| 51 |
+
icon: <Zap className="w-5 h-5" />,
|
| 52 |
+
color: 'text-purple-600 dark:text-purple-400',
|
| 53 |
+
bg: 'bg-purple-100 dark:bg-purple-900/30',
|
| 54 |
+
border: 'border-purple-200 dark:border-purple-800',
|
| 55 |
+
gradient: 'from-purple-500/10 to-indigo-500/5',
|
| 56 |
+
graphic: (
|
| 57 |
+
<svg viewBox="0 0 100 100" className="w-full h-full opacity-10 text-purple-500 absolute right-0 top-0">
|
| 58 |
+
<circle cx="80" cy="20" r="15" stroke="currentColor" strokeWidth="2" fill="none" />
|
| 59 |
+
<circle cx="60" cy="50" r="10" stroke="currentColor" strokeWidth="2" fill="none" />
|
| 60 |
+
<circle cx="80" cy="80" r="20" stroke="currentColor" strokeWidth="2" fill="none" />
|
| 61 |
+
<line x1="75" y1="33" x2="65" y2="42" stroke="currentColor" strokeWidth="2" />
|
| 62 |
+
<line x1="65" y1="58" x2="75" y2="65" stroke="currentColor" strokeWidth="2" />
|
| 63 |
+
</svg>
|
| 64 |
+
)
|
| 65 |
+
};
|
| 66 |
+
case 'insight':
|
| 67 |
+
return {
|
| 68 |
+
icon: <MessageSquare className="w-5 h-5" />,
|
| 69 |
+
color: 'text-blue-600 dark:text-blue-400',
|
| 70 |
+
bg: 'bg-blue-100 dark:bg-blue-900/30',
|
| 71 |
+
border: 'border-blue-200 dark:border-blue-800',
|
| 72 |
+
gradient: 'from-blue-500/10 to-cyan-500/5',
|
| 73 |
+
graphic: (
|
| 74 |
+
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/10 rounded-bl-full pointer-events-none blur-xl" />
|
| 75 |
+
)
|
| 76 |
+
};
|
| 77 |
+
default:
|
| 78 |
+
return {
|
| 79 |
+
icon: <BookOpen className="w-5 h-5" />,
|
| 80 |
+
color: 'text-gray-600 dark:text-gray-400',
|
| 81 |
+
bg: 'bg-gray-100 dark:bg-gray-800',
|
| 82 |
+
border: 'border-gray-200 dark:border-gray-700',
|
| 83 |
+
gradient: 'from-gray-500/5 to-slate-500/5',
|
| 84 |
+
graphic: null
|
| 85 |
+
};
|
| 86 |
+
}
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
const config = getTypeConfig();
|
| 90 |
+
|
| 91 |
+
const displayRow = card.rowSpan > 3 ? 3 : card.rowSpan < 1 ? 1 : card.rowSpan;
|
| 92 |
+
|
| 93 |
+
const getGridClass = () => {
|
| 94 |
+
if (layoutMode === 'list') return 'col-span-4 row-span-1';
|
| 95 |
+
if (isExpanded) return 'col-span-4 row-span-2 md:col-span-4 md:row-span-3 z-30';
|
| 96 |
+
|
| 97 |
+
const displayCol = card.colSpan;
|
| 98 |
+
|
| 99 |
+
// Clamping to grid limits
|
| 100 |
+
const cols = displayCol > 4 ? 4 : displayCol < 1 ? 1 : displayCol;
|
| 101 |
+
|
| 102 |
+
const colClass = cols === 4 ? 'md:col-span-4' : cols === 3 ? 'md:col-span-3' : cols === 2 ? 'md:col-span-2' : 'md:col-span-1';
|
| 103 |
+
const rowClass = displayRow === 3 ? 'md:row-span-3' : displayRow === 2 ? 'md:row-span-2' : 'md:row-span-1';
|
| 104 |
+
|
| 105 |
+
return `col-span-4 ${colClass} ${rowClass}`;
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
const getLineClampClass = () => {
|
| 109 |
+
if (isExpanded) return '';
|
| 110 |
+
switch (displayRow) {
|
| 111 |
+
case 1: return 'line-clamp-4';
|
| 112 |
+
case 2: return 'line-clamp-[12]';
|
| 113 |
+
case 3: return 'line-clamp-[20]';
|
| 114 |
+
default: return 'line-clamp-4';
|
| 115 |
+
}
|
| 116 |
+
};
|
| 117 |
+
|
| 118 |
+
const handleExpandClick = (e: React.MouseEvent) => {
|
| 119 |
+
if ((e.target as HTMLElement).closest('.control-btn')) return;
|
| 120 |
+
e.stopPropagation();
|
| 121 |
+
if (!isExpanded && !card.expandedContent) {
|
| 122 |
+
onExpand(card);
|
| 123 |
+
}
|
| 124 |
+
setIsExpanded(!isExpanded);
|
| 125 |
+
};
|
| 126 |
+
|
| 127 |
+
return (
|
| 128 |
+
<div
|
| 129 |
+
className={`
|
| 130 |
+
relative group transition-all duration-500 ease-[cubic-bezier(0.25,0.1,0.25,1.0)]
|
| 131 |
+
${getGridClass()}
|
| 132 |
+
${isExpanded
|
| 133 |
+
? 'scale-[1.02] z-40 shadow-2xl -translate-y-2 h-auto min-h-[400px]'
|
| 134 |
+
: 'hover:scale-[1.02] hover:-translate-y-2 hover:shadow-xl hover:shadow-brand-500/10 hover:z-20 h-full'
|
| 135 |
+
}
|
| 136 |
+
`}
|
| 137 |
+
onClick={handleExpandClick}
|
| 138 |
+
>
|
| 139 |
+
<div className={`
|
| 140 |
+
h-full w-full rounded-2xl overflow-hidden flex flex-col
|
| 141 |
+
bg-white dark:bg-slate-900
|
| 142 |
+
border
|
| 143 |
+
${isExpanded ? 'border-brand-500 ring-1 ring-brand-500' : `${config.border} group-hover:border-opacity-0`}
|
| 144 |
+
shadow-sm transition-all duration-300
|
| 145 |
+
relative
|
| 146 |
+
group-hover:ring-1 group-hover:ring-black/5 dark:group-hover:ring-white/10
|
| 147 |
+
`}>
|
| 148 |
+
|
| 149 |
+
{/* Mermaid Diagram (Takes priority over default graphics if present) */}
|
| 150 |
+
{card.mermaid && (
|
| 151 |
+
<div className={`absolute inset-0 z-0 opacity-20 dark:opacity-30 ${isExpanded ? 'opacity-100 dark:opacity-100 z-10 bg-white/90 dark:bg-slate-900/90' : ''} transition-all duration-500`}>
|
| 152 |
+
<MermaidDiagram chart={card.mermaid} theme={document.documentElement.classList.contains('dark') ? 'dark' : 'light'} />
|
| 153 |
+
</div>
|
| 154 |
+
)}
|
| 155 |
+
|
| 156 |
+
{/* Default Graphic / Fallback */}
|
| 157 |
+
{!card.mermaid && (
|
| 158 |
+
<div className="absolute inset-0 overflow-hidden pointer-events-none z-0">
|
| 159 |
+
<div className={`absolute inset-0 bg-gradient-to-br ${config.gradient} opacity-100`} />
|
| 160 |
+
{config.graphic}
|
| 161 |
+
</div>
|
| 162 |
+
)}
|
| 163 |
+
|
| 164 |
+
{/* Layout & Action Controls (Visible on Hover) */}
|
| 165 |
+
<div className="absolute top-3 right-12 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-20 control-btn">
|
| 166 |
+
{/* Resize Width */}
|
| 167 |
+
<div className="flex items-center bg-white/90 dark:bg-black/60 rounded-lg backdrop-blur-sm border border-gray-200 dark:border-gray-700 p-1 shadow-sm">
|
| 168 |
+
<button onClick={(e) => { e.stopPropagation(); onResize(card.id, -1, 0); }} className="p-1 hover:bg-black/10 dark:hover:bg-white/20 rounded" title="Decrease Width">
|
| 169 |
+
<Minus size={12} />
|
| 170 |
+
</button>
|
| 171 |
+
<span className="px-1.5 text-[10px] font-mono text-gray-500">W</span>
|
| 172 |
+
<button onClick={(e) => { e.stopPropagation(); onResize(card.id, 1, 0); }} className="p-1 hover:bg-black/10 dark:hover:bg-white/20 rounded" title="Increase Width">
|
| 173 |
+
<Plus size={12} />
|
| 174 |
+
</button>
|
| 175 |
+
</div>
|
| 176 |
+
{/* Resize Height */}
|
| 177 |
+
<div className="flex items-center bg-white/90 dark:bg-black/60 rounded-lg backdrop-blur-sm border border-gray-200 dark:border-gray-700 p-1 shadow-sm">
|
| 178 |
+
<button onClick={(e) => { e.stopPropagation(); onResize(card.id, 0, -1); }} className="p-1 hover:bg-black/10 dark:hover:bg-white/20 rounded" title="Decrease Height">
|
| 179 |
+
<Minus size={12} />
|
| 180 |
+
</button>
|
| 181 |
+
<span className="px-1.5 text-[10px] font-mono text-gray-500">H</span>
|
| 182 |
+
<button onClick={(e) => { e.stopPropagation(); onResize(card.id, 0, 1); }} className="p-1 hover:bg-black/10 dark:hover:bg-white/20 rounded" title="Increase Height">
|
| 183 |
+
<Plus size={12} />
|
| 184 |
+
</button>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
<div className={`relative p-6 flex flex-col h-full z-10 ${isExpanded && card.mermaid ? 'bg-white/80 dark:bg-slate-900/80 backdrop-blur-sm' : ''}`}>
|
| 189 |
+
{/* Header */}
|
| 190 |
+
<div className="flex items-start justify-between mb-4">
|
| 191 |
+
<div className="flex items-center gap-3">
|
| 192 |
+
<div className={`p-2 rounded-lg ${config.bg} ${config.color} backdrop-blur-md shadow-sm transition-transform duration-300 group-hover:scale-110`}>
|
| 193 |
+
{config.icon}
|
| 194 |
+
</div>
|
| 195 |
+
<h3 className="font-display font-bold text-lg leading-tight text-gray-900 dark:text-gray-50 pr-8 drop-shadow-sm">{card.title}</h3>
|
| 196 |
+
</div>
|
| 197 |
+
<button
|
| 198 |
+
onClick={handleExpandClick}
|
| 199 |
+
className="control-btn p-2 rounded-full bg-white/50 dark:bg-black/20 hover:bg-white dark:hover:bg-black/40 transition-all opacity-0 group-hover:opacity-100 absolute right-4 top-4 backdrop-blur-md border border-white/20 shadow-sm"
|
| 200 |
+
>
|
| 201 |
+
{isExpanded ? <Minimize2 size={16} className="text-gray-700 dark:text-gray-200" /> : <Maximize2 size={16} className="text-gray-700 dark:text-gray-200" />}
|
| 202 |
+
</button>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
{/* Main Content (Summary) */}
|
| 206 |
+
<div className="flex-grow overflow-y-auto custom-scrollbar relative">
|
| 207 |
+
{/* Use ReactMarkdown for summary to handle bolding/italics if model sends it, but style as simple text */}
|
| 208 |
+
<div className={`text-base leading-relaxed text-gray-700 dark:text-gray-200 font-medium ${getLineClampClass()} drop-shadow-sm prose prose-sm dark:prose-invert max-w-none prose-p:my-0`}>
|
| 209 |
+
<ReactMarkdown>{card.summary}</ReactMarkdown>
|
| 210 |
+
</div>
|
| 211 |
+
|
| 212 |
+
{!isExpanded && (
|
| 213 |
+
<div className="mt-4 flex items-center gap-2 text-xs font-medium uppercase tracking-wider opacity-60 text-gray-600 dark:text-gray-300 group-hover:translate-x-1 transition-transform">
|
| 214 |
+
Click to explore <ArrowRight size={12} />
|
| 215 |
+
</div>
|
| 216 |
+
)}
|
| 217 |
+
</div>
|
| 218 |
+
|
| 219 |
+
{/* Expanded Content Area */}
|
| 220 |
+
{isExpanded && (
|
| 221 |
+
<div
|
| 222 |
+
className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700 animate-in fade-in slide-in-from-bottom-4 duration-500 overflow-y-auto custom-scrollbar max-h-[400px]"
|
| 223 |
+
onClick={(e) => e.stopPropagation()}
|
| 224 |
+
>
|
| 225 |
+
<div className="flex justify-between items-center mb-4">
|
| 226 |
+
<div className="flex items-center gap-2 text-brand-600 dark:text-brand-400">
|
| 227 |
+
<FileText size={16} />
|
| 228 |
+
<h4 className="text-sm uppercase tracking-wider font-bold">First Principles Deep Dive</h4>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
|
| 232 |
+
{card.isLoadingDetails ? (
|
| 233 |
+
<div className="space-y-3 py-2">
|
| 234 |
+
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full animate-pulse"></div>
|
| 235 |
+
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded w-5/6 animate-pulse delay-75"></div>
|
| 236 |
+
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded w-4/6 animate-pulse delay-150"></div>
|
| 237 |
+
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full animate-pulse delay-100"></div>
|
| 238 |
+
</div>
|
| 239 |
+
) : (
|
| 240 |
+
<div className="prose prose-sm dark:prose-invert max-w-none text-gray-700 dark:text-gray-300 bg-gray-50/50 dark:bg-black/20 p-4 rounded-xl">
|
| 241 |
+
<ReactMarkdown>{card.expandedContent || "No detailed content available."}</ReactMarkdown>
|
| 242 |
+
</div>
|
| 243 |
+
)}
|
| 244 |
+
|
| 245 |
+
{/* Feedback */}
|
| 246 |
+
<div className="flex items-center justify-end gap-2 mt-4 pt-2">
|
| 247 |
+
<span className="text-xs font-medium text-gray-500">Helpful?</span>
|
| 248 |
+
<button onClick={() => onRate(card.id, 1)} className={`p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${card.rating === 1 ? 'text-green-600 bg-green-50 dark:bg-green-900/20' : 'text-gray-400'}`}>
|
| 249 |
+
<ThumbsUp size={14} />
|
| 250 |
+
</button>
|
| 251 |
+
<button onClick={() => onRate(card.id, -1)} className={`p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${card.rating === -1 ? 'text-red-600 bg-red-50 dark:bg-red-900/20' : 'text-gray-400'}`}>
|
| 252 |
+
<ThumbsDown size={14} />
|
| 253 |
+
</button>
|
| 254 |
+
</div>
|
| 255 |
+
</div>
|
| 256 |
+
)}
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
</div>
|
| 260 |
+
);
|
| 261 |
+
};
|
| 262 |
+
|
| 263 |
+
export default BentoCard;
|
components/ChatBot.tsx
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
+
import { MessageSquare, X, Send, Loader2 } from 'lucide-react';
|
| 3 |
+
import { ChatMessage } from '../types';
|
| 4 |
+
|
| 5 |
+
interface Props {
|
| 6 |
+
isOpen: boolean;
|
| 7 |
+
onClose: () => void;
|
| 8 |
+
messages: ChatMessage[];
|
| 9 |
+
onSendMessage: (text: string) => void;
|
| 10 |
+
isProcessing: boolean;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const ChatBot: React.FC<Props> = ({ isOpen, onClose, messages, onSendMessage, isProcessing }) => {
|
| 14 |
+
const [input, setInput] = useState('');
|
| 15 |
+
const scrollRef = useRef<HTMLDivElement>(null);
|
| 16 |
+
|
| 17 |
+
useEffect(() => {
|
| 18 |
+
if (scrollRef.current) {
|
| 19 |
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
| 20 |
+
}
|
| 21 |
+
}, [messages, isOpen]);
|
| 22 |
+
|
| 23 |
+
const handleSubmit = (e: React.FormEvent) => {
|
| 24 |
+
e.preventDefault();
|
| 25 |
+
if (!input.trim() || isProcessing) return;
|
| 26 |
+
onSendMessage(input);
|
| 27 |
+
setInput('');
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
if (!isOpen) return null;
|
| 31 |
+
|
| 32 |
+
return (
|
| 33 |
+
<div className="fixed bottom-6 right-6 w-80 md:w-96 h-[500px] glass-panel rounded-2xl shadow-2xl flex flex-col z-50 overflow-hidden border border-gray-200 dark:border-gray-700 animate-in fade-in slide-in-from-bottom-10">
|
| 34 |
+
{/* Header */}
|
| 35 |
+
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center bg-gray-50/50 dark:bg-gray-900/50">
|
| 36 |
+
<div className="flex items-center gap-2">
|
| 37 |
+
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
| 38 |
+
<h3 className="font-bold text-sm">Paper Assistant</h3>
|
| 39 |
+
</div>
|
| 40 |
+
<button onClick={onClose} className="hover:bg-gray-200 dark:hover:bg-gray-700 p-1 rounded">
|
| 41 |
+
<X size={16} />
|
| 42 |
+
</button>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
{/* Messages */}
|
| 46 |
+
<div ref={scrollRef} className="flex-grow overflow-y-auto p-4 space-y-4 bg-white/50 dark:bg-black/20">
|
| 47 |
+
{messages.length === 0 && (
|
| 48 |
+
<div className="text-center text-xs opacity-50 mt-10">
|
| 49 |
+
Ask me anything about the paper!
|
| 50 |
+
</div>
|
| 51 |
+
)}
|
| 52 |
+
{messages.map((msg) => (
|
| 53 |
+
<div
|
| 54 |
+
key={msg.id}
|
| 55 |
+
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
| 56 |
+
>
|
| 57 |
+
<div
|
| 58 |
+
className={`
|
| 59 |
+
max-w-[80%] p-3 rounded-2xl text-sm shadow-sm
|
| 60 |
+
${msg.role === 'user'
|
| 61 |
+
? 'bg-brand-600 text-white rounded-br-none'
|
| 62 |
+
: 'bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-bl-none'}
|
| 63 |
+
`}
|
| 64 |
+
>
|
| 65 |
+
{msg.text}
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
))}
|
| 69 |
+
{isProcessing && (
|
| 70 |
+
<div className="flex justify-start">
|
| 71 |
+
<div className="bg-white dark:bg-gray-800 p-3 rounded-2xl rounded-bl-none border border-gray-100 dark:border-gray-700">
|
| 72 |
+
<Loader2 className="animate-spin w-4 h-4 opacity-50" />
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
)}
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
{/* Input */}
|
| 79 |
+
<form onSubmit={handleSubmit} className="p-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-900/50">
|
| 80 |
+
<div className="relative">
|
| 81 |
+
<input
|
| 82 |
+
type="text"
|
| 83 |
+
value={input}
|
| 84 |
+
onChange={(e) => setInput(e.target.value)}
|
| 85 |
+
placeholder="Type a question..."
|
| 86 |
+
className="w-full bg-white dark:bg-black/30 border border-gray-200 dark:border-gray-700 rounded-xl pl-4 pr-10 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500/50"
|
| 87 |
+
/>
|
| 88 |
+
<button
|
| 89 |
+
type="submit"
|
| 90 |
+
disabled={!input.trim() || isProcessing}
|
| 91 |
+
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 rounded-lg bg-brand-500 text-white hover:bg-brand-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
| 92 |
+
>
|
| 93 |
+
<Send size={14} />
|
| 94 |
+
</button>
|
| 95 |
+
</div>
|
| 96 |
+
</form>
|
| 97 |
+
</div>
|
| 98 |
+
);
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
export default ChatBot;
|
components/MermaidDiagram.tsx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import React, { useEffect, useRef, useState } from 'react';
|
| 3 |
+
import mermaid from 'mermaid';
|
| 4 |
+
|
| 5 |
+
interface Props {
|
| 6 |
+
chart: string;
|
| 7 |
+
theme?: 'light' | 'dark';
|
| 8 |
+
}
|
| 9 |
+
|
| 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({
|
| 16 |
+
startOnLoad: false,
|
| 17 |
+
theme: theme === 'dark' ? 'dark' : 'default',
|
| 18 |
+
securityLevel: 'loose',
|
| 19 |
+
fontFamily: 'Inter, sans-serif',
|
| 20 |
+
});
|
| 21 |
+
}, [theme]);
|
| 22 |
+
|
| 23 |
+
useEffect(() => {
|
| 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 |
+
/>
|
| 49 |
+
);
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
export default MermaidDiagram;
|
index.html
CHANGED
|
@@ -1,19 +1,95 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>BentoMind</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;
|
| 15 |
+
}
|
| 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;
|
| 22 |
+
height: 8px;
|
| 23 |
+
}
|
| 24 |
+
::-webkit-scrollbar-track {
|
| 25 |
+
background: transparent;
|
| 26 |
+
}
|
| 27 |
+
::-webkit-scrollbar-thumb {
|
| 28 |
+
background: #888;
|
| 29 |
+
border-radius: 4px;
|
| 30 |
+
}
|
| 31 |
+
::-webkit-scrollbar-thumb:hover {
|
| 32 |
+
background: #555;
|
| 33 |
+
}
|
| 34 |
+
.glass-panel {
|
| 35 |
+
background: rgba(255, 255, 255, 0.7); /* Increased opacity for light mode */
|
| 36 |
+
backdrop-filter: blur(20px);
|
| 37 |
+
-webkit-backdrop-filter: blur(20px);
|
| 38 |
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
| 39 |
+
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
| 40 |
+
}
|
| 41 |
+
.dark .glass-panel {
|
| 42 |
+
background: rgba(15, 23, 42, 0.6); /* Darker background for dark mode contrast */
|
| 43 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 44 |
+
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* Gradient text helper */
|
| 48 |
+
.text-gradient {
|
| 49 |
+
background-clip: text;
|
| 50 |
+
-webkit-background-clip: text;
|
| 51 |
+
-webkit-text-fill-color: transparent;
|
| 52 |
+
}
|
| 53 |
+
</style>
|
| 54 |
+
<script>
|
| 55 |
+
tailwind.config = {
|
| 56 |
+
darkMode: 'class',
|
| 57 |
+
theme: {
|
| 58 |
+
extend: {
|
| 59 |
+
colors: {
|
| 60 |
+
brand: {
|
| 61 |
+
50: '#f0f9ff',
|
| 62 |
+
100: '#e0f2fe',
|
| 63 |
+
500: '#0ea5e9',
|
| 64 |
+
600: '#0284c7',
|
| 65 |
+
900: '#0c4a6e',
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
animation: {
|
| 69 |
+
'spin-slow': 'spin 20s linear infinite',
|
| 70 |
+
'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
</script>
|
| 76 |
+
<script type="importmap">
|
| 77 |
+
{
|
| 78 |
+
"imports": {
|
| 79 |
+
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
|
| 80 |
+
"react/": "https://aistudiocdn.com/react@^19.2.0/",
|
| 81 |
+
"react": "https://aistudiocdn.com/react@^19.2.0",
|
| 82 |
+
"@google/genai": "https://aistudiocdn.com/@google/genai@^1.30.0",
|
| 83 |
+
"lucide-react": "https://aistudiocdn.com/lucide-react@^0.554.0",
|
| 84 |
+
"react-markdown": "https://aistudiocdn.com/react-markdown@^10.1.0",
|
| 85 |
+
"mermaid": "https://cdn.jsdelivr.net/npm/[email protected]/dist/mermaid.esm.min.mjs"
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
</script>
|
| 89 |
+
<link rel="stylesheet" href="/index.css">
|
| 90 |
+
</head>
|
| 91 |
+
<body class="antialiased transition-colors duration-300 bg-slate-50 dark:bg-slate-950">
|
| 92 |
+
<div id="root"></div>
|
| 93 |
+
<script type="module" src="/index.tsx"></script>
|
| 94 |
+
</body>
|
| 95 |
+
</html>
|
index.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import ReactDOM from 'react-dom/client';
|
| 3 |
+
import App from './App';
|
| 4 |
+
|
| 5 |
+
const rootElement = document.getElementById('root');
|
| 6 |
+
if (!rootElement) {
|
| 7 |
+
throw new Error("Could not find root element to mount to");
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const root = ReactDOM.createRoot(rootElement);
|
| 11 |
+
root.render(
|
| 12 |
+
<React.StrictMode>
|
| 13 |
+
<App />
|
| 14 |
+
</React.StrictMode>
|
| 15 |
+
);
|
metadata.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "BentoMind - Research Summarizer",
|
| 3 |
+
"description": "A visually stunning, intelligent research paper summarizer using Gemini API to generate dynamic Bento-style cards with deep-dive capabilities.",
|
| 4 |
+
"requestFramePermissions": []
|
| 5 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "bentomind---research-summarizer",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 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"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@types/node": "^22.14.0",
|
| 21 |
+
"@vitejs/plugin-react": "^5.0.0",
|
| 22 |
+
"typescript": "~5.8.2",
|
| 23 |
+
"vite": "^6.2.0"
|
| 24 |
+
}
|
| 25 |
+
}
|
services/geminiService.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import { GoogleGenAI, Type, Schema } from "@google/genai";
|
| 3 |
+
import { BentoCardData, ChatMessage, ModelType } from "../types";
|
| 4 |
+
|
| 5 |
+
const RESPONSE_SCHEMA = {
|
| 6 |
+
type: Type.ARRAY,
|
| 7 |
+
items: {
|
| 8 |
+
type: Type.OBJECT,
|
| 9 |
+
properties: {
|
| 10 |
+
title: { type: Type.STRING },
|
| 11 |
+
summary: { type: Type.STRING, description: "Concise summary using Markdown for emphasis (bold/italic) but no headings." },
|
| 12 |
+
type: { type: Type.STRING, enum: ['stat', 'concept', 'quote', 'insight', 'process'] },
|
| 13 |
+
colSpan: { type: Type.INTEGER },
|
| 14 |
+
rowSpan: { type: Type.INTEGER },
|
| 15 |
+
detailPrompt: { type: Type.STRING },
|
| 16 |
+
mermaid: { type: Type.STRING, description: "A valid Mermaid.js graph definition (e.g. 'graph TD...') if this card describes a process, workflow, or architecture found in the PDF. Leave empty otherwise." }
|
| 17 |
+
},
|
| 18 |
+
required: ['title', 'summary', 'type', 'colSpan', 'rowSpan', 'detailPrompt']
|
| 19 |
+
}
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
export const generateBentoCards = async (
|
| 23 |
+
apiKey: string,
|
| 24 |
+
model: ModelType,
|
| 25 |
+
content: string,
|
| 26 |
+
isPdf: boolean = false,
|
| 27 |
+
useThinking: boolean = false
|
| 28 |
+
): Promise<BentoCardData[]> => {
|
| 29 |
+
if (!apiKey) throw new Error("API Key is missing");
|
| 30 |
+
|
| 31 |
+
const ai = new GoogleGenAI({ apiKey });
|
| 32 |
+
|
| 33 |
+
let promptParts: any[] = [];
|
| 34 |
+
|
| 35 |
+
if (isPdf) {
|
| 36 |
+
promptParts.push({
|
| 37 |
+
inlineData: {
|
| 38 |
+
data: content, // content is base64 here
|
| 39 |
+
mimeType: "application/pdf",
|
| 40 |
+
},
|
| 41 |
+
});
|
| 42 |
+
promptParts.push({ text: "Analyze this research paper, paying close attention to any embedded figures, diagrams, and charts." });
|
| 43 |
+
} else {
|
| 44 |
+
promptParts.push({ text: `Analyze the following research paper text/content: \n\n${content.substring(0, 40000)}...` });
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
const thinkingInstruction = useThinking
|
| 48 |
+
? "You are in THINKING MODE. Use your extended reasoning budget to deeply analyze the methodology and theoretical underpinnings before summarizing. Identify subtle connections and foundational axioms."
|
| 49 |
+
: "";
|
| 50 |
+
|
| 51 |
+
promptParts.push({
|
| 52 |
+
text: `
|
| 53 |
+
${thinkingInstruction}
|
| 54 |
+
Create a highly structured, dynamic summary of this paper designed for a rich Bento Grid interface.
|
| 55 |
+
|
| 56 |
+
RULES:
|
| 57 |
+
1. Generate between 6 to 9 unique cards. Do not generate fewer than 6.
|
| 58 |
+
2. The 'colSpan' (1-4) and 'rowSpan' (1-2) must vary based on the visual hierarchy and importance of the point.
|
| 59 |
+
- Use 2x2 or 3x2 for the "Main Result" or "Core Concept".
|
| 60 |
+
- Use 1x1 for specific stats or quick quotes.
|
| 61 |
+
- Use 2x1 or 4x2 for process steps, architectures, or lists.
|
| 62 |
+
- IF you provide a 'mermaid' diagram, you MUST set colSpan >= 2 and rowSpan >= 2.
|
| 63 |
+
- Ensure the total grid packs densely (Total grid width is 4 columns).
|
| 64 |
+
3. 'summary' field: Use clear Markdown. You can use **bold** for emphasis. Keep it concise.
|
| 65 |
+
4. 'detailPrompt' must be a technical follow-up prompt that asks to explain the "First Principles" of this card's content.
|
| 66 |
+
5. 'mermaid' field: Look at the figures in the PDF. If there is a flowchart, architecture diagram, or framework overview, translate it into a valid Mermaid.js graph ('graph TD' or 'flowchart LR'). This is highly preferred for 'process' or 'concept' cards.
|
| 67 |
+
|
| 68 |
+
CONTENT TO EXTRACT:
|
| 69 |
+
- The Main Innovation/Contribution (High importance).
|
| 70 |
+
- Key Quantitative Results (Accuracy, Speedup, etc.).
|
| 71 |
+
- The "Secret Sauce" (Methodology details) -> Visualize this with Mermaid if a diagram exists in the PDF.
|
| 72 |
+
- A provoking Quote from the text.
|
| 73 |
+
- Limitations or Future Work.
|
| 74 |
+
- First Principles or Theory involved.
|
| 75 |
+
|
| 76 |
+
Return a valid JSON array.
|
| 77 |
+
`
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
// Thinking Mode Configuration
|
| 81 |
+
let effectiveModel = model;
|
| 82 |
+
let requestConfig: any = {
|
| 83 |
+
responseMimeType: "application/json",
|
| 84 |
+
responseSchema: RESPONSE_SCHEMA as any,
|
| 85 |
+
systemInstruction: "You are a senior research scientist summarizing papers for a high-tech dashboard. You prioritize density of information, visual hierarchy, and architectural clarity.",
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
if (useThinking) {
|
| 89 |
+
effectiveModel = 'gemini-3-pro-preview';
|
| 90 |
+
requestConfig.thinkingConfig = { thinkingBudget: 32768 };
|
| 91 |
+
// IMPORTANT: Do not set maxOutputTokens when thinking is enabled for 2.5/3.0 models in this context if not strictly needed,
|
| 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 },
|
| 99 |
+
config: requestConfig
|
| 100 |
+
});
|
| 101 |
+
|
| 102 |
+
const text = response.text;
|
| 103 |
+
if (!text) throw new Error("No response from Gemini");
|
| 104 |
+
|
| 105 |
+
// If thinking text is included in response.text (depending on SDK version/flags), we might need to clean it,
|
| 106 |
+
// but typically response.text returns the model output.
|
| 107 |
+
// For JSON schema mode, it should be just JSON.
|
| 108 |
+
|
| 109 |
+
// Attempt to find JSON if the model outputted extra chat:
|
| 110 |
+
let jsonStr = text;
|
| 111 |
+
const jsonMatch = text.match(/\[.*\]/s);
|
| 112 |
+
if (jsonMatch) {
|
| 113 |
+
jsonStr = jsonMatch[0];
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
const parsed = JSON.parse(jsonStr);
|
| 117 |
+
|
| 118 |
+
return parsed.map((item: any, index: number) => ({
|
| 119 |
+
...item,
|
| 120 |
+
id: `card-${index}-${Date.now()}`,
|
| 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,
|
| 137 |
+
useThinking: boolean = false
|
| 138 |
+
): Promise<string> => {
|
| 139 |
+
const ai = new GoogleGenAI({ apiKey });
|
| 140 |
+
|
| 141 |
+
const prompt = `
|
| 142 |
+
Context: The user is reading a summary card about "${topic}".
|
| 143 |
+
Original Paper Context (Excerpt): ${originalContext.substring(0, 5000)}...
|
| 144 |
+
|
| 145 |
+
Task: ${detailPrompt}
|
| 146 |
+
|
| 147 |
+
CRITICAL INSTRUCTION: Explain this concept from FIRST PRINCIPLES.
|
| 148 |
+
- Return the response in RICH MARKDOWN format.
|
| 149 |
+
- Use Headers (###), Lists, Bold text, and Code blocks if necessary.
|
| 150 |
+
- Derive the conclusion from fundamental axioms, physical laws, or basic mathematical truths.
|
| 151 |
+
- Explain the "mechanistic why" — how does it actually work at the lowest level?
|
| 152 |
+
- If it's a result, explain the statistical validity and the dataset composition.
|
| 153 |
+
`;
|
| 154 |
+
|
| 155 |
+
let effectiveModel = model;
|
| 156 |
+
let requestConfig: any = {};
|
| 157 |
+
|
| 158 |
+
if (useThinking) {
|
| 159 |
+
effectiveModel = 'gemini-3-pro-preview';
|
| 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 => ({
|
| 182 |
+
role: h.role === 'user' ? 'user' : 'model',
|
| 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 |
+
};
|
tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"experimentalDecorators": true,
|
| 5 |
+
"useDefineForClassFields": false,
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"lib": [
|
| 8 |
+
"ES2022",
|
| 9 |
+
"DOM",
|
| 10 |
+
"DOM.Iterable"
|
| 11 |
+
],
|
| 12 |
+
"skipLibCheck": true,
|
| 13 |
+
"types": [
|
| 14 |
+
"node"
|
| 15 |
+
],
|
| 16 |
+
"moduleResolution": "bundler",
|
| 17 |
+
"isolatedModules": true,
|
| 18 |
+
"moduleDetection": "force",
|
| 19 |
+
"allowJs": true,
|
| 20 |
+
"jsx": "react-jsx",
|
| 21 |
+
"paths": {
|
| 22 |
+
"@/*": [
|
| 23 |
+
"./*"
|
| 24 |
+
]
|
| 25 |
+
},
|
| 26 |
+
"allowImportingTsExtensions": true,
|
| 27 |
+
"noEmit": true
|
| 28 |
+
}
|
| 29 |
+
}
|
types.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
export interface BentoCardData {
|
| 3 |
+
id: string;
|
| 4 |
+
title: string;
|
| 5 |
+
summary: string;
|
| 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;
|
| 13 |
+
rating?: number;
|
| 14 |
+
feedback?: string;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export interface ChatMessage {
|
| 18 |
+
id: string;
|
| 19 |
+
role: 'user' | 'model' | 'system';
|
| 20 |
+
text: string;
|
| 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;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export interface ProcessingStatus {
|
| 35 |
+
state: 'idle' | 'reading' | 'analyzing' | 'generating' | 'complete' | 'error';
|
| 36 |
+
message?: string;
|
| 37 |
+
}
|
vite.config.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import path from 'path';
|
| 2 |
+
import { defineConfig, loadEnv } from 'vite';
|
| 3 |
+
import react from '@vitejs/plugin-react';
|
| 4 |
+
|
| 5 |
+
export default defineConfig(({ mode }) => {
|
| 6 |
+
const env = loadEnv(mode, '.', '');
|
| 7 |
+
return {
|
| 8 |
+
server: {
|
| 9 |
+
port: 3000,
|
| 10 |
+
host: '0.0.0.0',
|
| 11 |
+
},
|
| 12 |
+
plugins: [react()],
|
| 13 |
+
define: {
|
| 14 |
+
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
| 15 |
+
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
| 16 |
+
},
|
| 17 |
+
resolve: {
|
| 18 |
+
alias: {
|
| 19 |
+
'@': path.resolve(__dirname, '.'),
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
};
|
| 23 |
+
});
|