Akhil-Theerthala commited on
Commit
6fe3275
·
verified ·
1 Parent(s): 7450703

Upload 14 files

Browse files
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
- title: PaperStack
3
- emoji: 🚀
4
- colorFrom: indigo
5
- colorTo: gray
6
- sdk: static
7
- pinned: false
8
- license: apache-2.0
9
- short_description: A research paper summarizer using bentogrids
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
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
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ });