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