PageSourceSearch

https://dynamic-salmiakki-095693.netlify.app/js/utils/pdf-generator.js

js dynamic-salmiakki-095693.netlify.app collected 2026-10-03 10:40:41 UTC 19,209 bytes, 474 lines download raw bytes

1// PDF Generation Module
2// Extracted from main.js for better modularity
3
4/**
5 * Dynamically loads the jsPDF library if not already present.
6 * @returns {Promise<void>}
7 */
8async function ensureJsPDFLibrary() {
9    if (!window.jsPDF && !window.jspdf) {
10        console.log('[PDF Generator] jsPDF library not found, loading dynamically...');
11        const script = document.createElement('script');
12        script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
13        document.head.appendChild(script);
14        
15        await new Promise((resolve, reject) => {
16            script.onload = () => {
17                // Wait a bit for the library to initialize
18                setTimeout(() => {
19                    if (window.jsPDF || window.jspdf) {
20                        console.log('[PDF Generator] jsPDF library loaded successfully.');
21                        resolve();
22                    } else {
23                        console.error('[PDF Generator] jsPDF library loaded but not accessible.');
24                        reject(new Error('jsPDF library loaded but not accessible'));
25                    }
26                }, 100);
27            };
28            script.onerror = () => {
29                console.error('[PDF Generator] Failed to load jsPDF library.');
30                reject(new Error('Failed to load jsPDF library'));
31            };
32        });
33    }
34}
35
36/**
37 * Helper function to clean text content from a DOM element.
38 * @param {HTMLElement} element - The DOM element to clean.
39 * @returns {string} - The cleaned text content.
40 */
41function cleanTextForPdf(element) {
42    if (!element) return '';
43    // Clone the element to avoid modifying the original
44    const clone = element.cloneNode(true);
45    
46    // Remove script tags, style tags, and other non-content elements like buttons
47    const unwantedSelectors = 'script, style, .pdf-button, button, .no-print, [data-pdf-ignore="true"]';
48    clone.querySelectorAll(unwantedSelectors).forEach(el => el.remove());
49    
50    // Get text content and clean it
51    let text = clone.textContent || clone.innerText || '';
52    
53    // Replace multiple spaces/newlines with single spaces, trim
54    text = text.replace(/\s+/g, ' ').trim();
55    
56    return text;
57}
58
59/**
60 * Generates a PDF document from the chat conversation and infoboxes.
61 * @param {HTMLElement} chatContainer - The DOM element containing chat messages.
62 * @param {HTMLElement} infoboxesContainer - The DOM element containing infoboxes.
63 */
64async function generateWikiLivePDF(chatContainer, infoboxesContainer) {
65    console.log('🔄 Starting PDF generation with image support...');
66    
67    // Ensure jsPDF library is loaded
68    await ensureJsPDFLibrary();
69    
70    // Get jsPDF constructor - try multiple possible locations
71    let jsPDF;
72    if (window.jsPDF) {
73        jsPDF = window.jsPDF;
74    } else if (window.jspdf && window.jspdf.jsPDF) {
75        jsPDF = window.jspdf.jsPDF;
76    } else if (window.jspdf) {
77        jsPDF = window.jspdf;
78    } else {
79        throw new Error('jsPDF library not found after loading');
80    }
81    
82    console.log('[PDF Generator] Using jsPDF constructor:', typeof jsPDF);
83    const doc = new jsPDF();
84    
85    // Get current date for filename
86    const now = new Date();
87    const dateStr = now.toISOString().split('T')[0];
88    const timeStr = now.toTimeString().split(' ')[0].replace(/:/g, '-');
89    
90    // Set up PDF styling
91    doc.setFontSize(14);
92    doc.setFont(undefined, 'bold');
93    doc.text('WikiLive Research Report', 20, 20);
94    
95    doc.setFontSize(10);
96    doc.setFont(undefined, 'normal');
97    doc.text('Wikipedia RealTime Search Tool', 20, 30);
98    
99    doc.setFontSize(9);
100    doc.text('www.mylivesearch.com', 20, 38);
101    doc.text('Helping you use Wikipedia even better!!', 20, 46);
102    doc.text('All Data is from Wikipedia in realtime supported by AI Systems', 20, 54);
103    
104    doc.setFontSize(8);
105    doc.text(`Generated on: ${now.toLocaleString()}`, 20, 64);
106    
107    let yPosition = 80;
108    const pageHeight = doc.internal.pageSize.height;
109    const margin = 20;
110    const lineHeight = 7;
111    const contentWidth = doc.internal.pageSize.width - (margin * 2);
112
113    // Extract conversation content
114    if (chatContainer) {
115        console.log('[PDF Generator] Searching for messages in chatContainer:', chatContainer);
116        const messages = chatContainer.querySelectorAll('.message.user, .message.bot');
117        console.log('[PDF Generator] Found messages:', messages.length);
118        
119        if (messages.length > 0) {
120            doc.setFontSize(14);
121            doc.setFont(undefined, 'bold');
122            doc.text('Conversation History', margin, yPosition);
123            yPosition += lineHeight * 2;
124            
125            messages.forEach((message, index) => {
126                const isUser = message.classList.contains('user');
127                const content = cleanTextForPdf(message);
128                console.log(`[PDF Generator] Processing message ${index + 1}:`, { isUser, contentLength: content?.length });
129                
130                if (content && content.trim().length > 0) {
131                    // Add speaker label
132                    doc.setFontSize(10);
133                    doc.setFont(undefined, 'bold');
134                    doc.text(isUser ? 'User:' : 'Assistant:', margin, yPosition);
135                    yPosition += lineHeight + 2;
136                    
137                    // Add message content
138                    doc.setFont(undefined, 'normal');
139                    doc.setFontSize(8);
140                    const lines = doc.splitTextToSize(content.trim(), contentWidth);
141                    lines.forEach((line) => {
142                        if (yPosition > pageHeight - 20) {
143                            doc.addPage();
144                            yPosition = 20;
145                        }
146                        doc.text(line, margin, yPosition);
147                        yPosition += lineHeight;
148                    });
149                    yPosition += lineHeight; // Space between messages
150                }
151            });
152        } else {
153            console.log('[PDF Generator] No messages found - trying alternative selectors');
154            // Fallback: try to find any elements with message-like content
155            const fallbackMessages = chatContainer.querySelectorAll('.message');
156            console.log('[PDF Generator] Fallback search found:', fallbackMessages.length, 'elements');
157        }
158    }
159
160    // Process infoboxes with images
161    if (infoboxesContainer) {
162        const infoboxes = infoboxesContainer.querySelectorAll('.infobox');
163        
164        if (infoboxes.length > 0) {
165            if (yPosition > 80) {
166                yPosition += lineHeight * 2;
167            }
168            
169            doc.setFontSize(14);
170            doc.setFont(undefined, 'bold');
171            doc.text('Wikipedia Information', margin, yPosition);
172            yPosition += lineHeight * 2;
173            
174            // First, collect all images and load them
175            console.log('📸 Loading images for PDF...');
176            const imagePromises = [];
177            const entityData = [];
178            
179            let entityCount = 1;
180            infoboxes.forEach((infobox) => {
181                // Extract title and content
182                const { title, summary, relatedLinks } = extractEntityData(infobox, entityCount);
183                
184                // Extract image
185                const images = infobox.querySelectorAll('img');
186                let imagePromise = null;
187                
188                if (images.length > 0) {
189                    // Find the best image
190                    let bestImage = images[0];
191                    for (const img of images) {
192                        const width = parseInt(img.width) || 0;
193                        const height = parseInt(img.height) || 0;
194                        const currentWidth = parseInt(bestImage.width) || 0;
195                        const currentHeight = parseInt(bestImage.height) || 0;
196                        
197                        if (width * height > currentWidth * currentHeight) {
198                            bestImage = img;
199                        }
200                    }
201                    
202                    imagePromise = loadImageAsBase64(bestImage.src, title);
203                }
204                
205                entityData.push({
206                    title,
207                    summary,
208                    relatedLinks,
209                    imagePromise,
210                    entityCount
211                });
212                
213                if (imagePromise) {
214                    imagePromises.push(imagePromise);
215                }
216                
217                entityCount++;
218            });
219            
220            // Wait for all images to load
221            const loadedImages = await Promise.allSettled(imagePromises);
222            console.log(`📸 Loaded ${loadedImages.filter(r => r.status === 'fulfilled').length}/${imagePromises.length} images`);
223            
224            // Now generate PDF with loaded images
225            for (const entity of entityData) {
226                if (yPosition > pageHeight - 50) {
227                    doc.addPage();
228                    yPosition = 20;
229                }
230                
231                const hasImage = entity.imagePromise !== null;
232                const textWidth = hasImage ? contentWidth - 85 : contentWidth;
233                const headerStartY = yPosition;
234                
235                // Add entity header
236                doc.setFontSize(12);
237                doc.setFont(undefined, 'bold');
238                doc.text(`${entity.entityCount}. ${entity.title}`, margin, yPosition);
239                yPosition += lineHeight + 2;
240                
241                // Add image if loaded successfully
242                if (hasImage) {
243                    const imageResult = await entity.imagePromise;
244                    if (imageResult && imageResult.dataURL) {
245                        try {
246                            const imageX = margin + textWidth + 10;
247                            const imageY = headerStartY + 5;
248                            const imageWidth = 75;
249                            const imageHeight = Math.min(imageWidth * (imageResult.height / imageResult.width), 100);
250                            
251                            doc.addImage(imageResult.dataURL, 'JPEG', imageX, imageY, imageWidth, imageHeight);
252                            console.log(`✅ Added image for ${entity.title}`);
253                        } catch (error) {
254                            console.log(`❌ Failed to add image for ${entity.title}:`, error);
255                        }
256                    }
257                }
258                
259                // Add summary
260                if (entity.summary && entity.summary.length > 10) {
261                    doc.setFont(undefined, 'normal');
262                    doc.setFontSize(8);
263                    const summaryLines = doc.splitTextToSize(entity.summary, textWidth);
264                    summaryLines.forEach((line) => {
265                        if (yPosition > pageHeight - 20) {
266                            doc.addPage();
267                            yPosition = 20;
268                        }
269                        doc.text(line, margin, yPosition);
270                        yPosition += lineHeight;
271                    });
272                }
273                
274                // Add related topics
275                if (entity.relatedLinks.length > 0) {
276                    yPosition += lineHeight;
277                    doc.setFont(undefined, 'bold');
278                    doc.setFontSize(8);
279                    doc.text('Related Topics:', margin, yPosition);
280                    yPosition += lineHeight;
281                    
282                    doc.setFont(undefined, 'normal');
283                    const relatedText = entity.relatedLinks.join(', ');
284                    const relatedLines = doc.splitTextToSize(relatedText, textWidth);
285                    relatedLines.forEach((line) => {
286                        if (yPosition > pageHeight - 20) {
287                            doc.addPage();
288                            yPosition = 20;
289                        }
290                        doc.text(line, margin, yPosition);
291                        yPosition += lineHeight;
292                    });
293                }
294                
295                // Add Wikipedia link
296                yPosition += lineHeight;
297                doc.setFont(undefined, 'italic');
298                doc.setFontSize(8);
299                const wikiUrl = `https://en.wikipedia.org/wiki/${encodeURIComponent(entity.title.replace(/ /g, '_'))}`;
300                doc.text(`Wikipedia: ${wikiUrl}`, margin, yPosition);
301                yPosition += lineHeight * 2;
302            }
303        }
304    }
305
306    // Generate and download PDF
307    const filename = `WikiLive_Research_${dateStr}_${timeStr}.pdf`;
308    doc.save(filename);
309    console.log(`[PDF Generator] PDF '${filename}' generated and download initiated.`);
310}
311
312// Helper function to extract entity data from an infobox
313function extractEntityData(infobox, entityCount) {
314    // Extract title
315    let title = '';
316    
317    // First, try to get title from the infobox text content
318    const infoboxText = cleanTextForPdf(infobox).trim();
319    
320    // Look for patterns like "#1Tom Cruise" or "Tom Cruise" at the beginning
321    // Improved regex to capture full titles without truncation
322    const titleMatch = infoboxText.match(/^#?\d*\s*([A-Za-z][^#\n\r]*?)(?:\s*(?:is\s|was\s|are\s|were\s|\(|Thomas\s|The\s+following|List\s+of))/i);
323    if (titleMatch && titleMatch[1]) {
324        title = titleMatch[1].trim();
325        // Remove any trailing punctuation or extra characters
326        title = title.replace(/[.,;:!?]+$/, '').trim();
327    }
328    
329    // Alternative pattern for titles that don't have descriptive text immediately after
330    if (!title || title.length < 3) {
331        const simpleMatch = infoboxText.match(/^#?\d*\s*([A-Za-z][A-Za-z\s\-'.:()]+?)(?:\s*(?:Related|Read\s+more|\n))/i);
332        if (simpleMatch && simpleMatch[1]) {
333            title = simpleMatch[1].trim().replace(/[.,;:!?]+$/, '').trim();
334        }
335    }
336    
337    // If no match, try to extract from HTML elements
338    if (!title || title.length < 3) {
339        const titleElement = infobox.querySelector('h3, .infobox-title, .title, .entity-title');
340        if (titleElement) {
341            title = cleanTextForPdf(titleElement).trim();
342        }
343    }
344    
345    // Fallback to first bold text
346    if (!title || title.toLowerCase() === 'unknown entity' || title.length < 3) {
347        const firstBold = infobox.querySelector('b, strong');
348        if (firstBold) {
349            const boldText = cleanTextForPdf(firstBold).trim();
350            // Clean up bold text that might have extra characters
351            const cleanBold = boldText.replace(/^#?\d*\s*/, '').trim();
352            if (cleanBold && cleanBold.length > 2) {
353                title = cleanBold.replace(/[.,;:!?]+$/, '').trim();
354            }
355        }
356    }
357    
358    // Try to extract from the beginning of the text content (line by line)
359    if (!title || title.toLowerCase() === 'unknown entity' || title.length < 3) {
360        const lines = infoboxText.split('\n');
361        for (const line of lines) {
362            const cleanLine = line.replace(/^#?\d*\s*/, '').trim();
363            if (cleanLine && cleanLine.length > 2 && !cleanLine.toLowerCase().includes('unknown')) {
364                // Take the first meaningful part before common descriptive words
365                const titlePart = cleanLine.split(/\s+(?:is\s|was\s|are\s|were\s|\(|Thomas\s|The\s+following|List\s+of)/i)[0];
366                if (titlePart && titlePart.length > 2) {
367                    title = titlePart.trim().replace(/[.,;:!?]+$/, '').trim();
368                    break;
369                }
370            }
371        }
372    }
373    
374    // Final fallback
375    if (!title || title.toLowerCase() === 'unknown entity' || title.length < 2) {
376        title = `Wikipedia Entity #${entityCount}`;
377    }
378
379    // Extract summary
380    let summary = '';
381    const summaryElement = infobox.querySelector('.summary, .description, .content, p');
382    if (summaryElement) {
383        summary = cleanTextForPdf(summaryElement).trim();
384    } else {
385        // Get all text and clean it
386        const allText = cleanTextForPdf(infobox).trim();
387        // Remove the title from the beginning if it appears there
388        summary = allText.replace(new RegExp(`^${title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*`, 'i'), '').trim();
389    }
390    
391    // Extract related links
392    const relatedLinks = infobox.querySelectorAll('.related-links a, .related a, [class*="related"] a');
393    const relatedLinksText = Array.from(relatedLinks)
394        .map(link => cleanTextForPdf(link).trim())
395        .filter(text => text.length > 0);
396
397    return {
398        title,
399        summary,
400        relatedLinks: relatedLinksText
401    };
402}
403
404// Helper function to load an image as base64
405function loadImageAsBase64(url, title) {
406    return new Promise((resolve, reject) => {
407        const img = new Image();
408        img.crossOrigin = 'anonymous'; // Enable CORS
409        
410        img.onload = function() {
411            const canvas = document.createElement('canvas');
412            const ctx = canvas.getContext('2d');
413            canvas.width = img.width;
414            canvas.height = img.height;
415            ctx.drawImage(img, 0, 0);
416            
417            try {
418                const dataURL = canvas.toDataURL('image/jpeg', 0.8);
419                resolve({
420                    dataURL,
421                    width: img.width,
422                    height: img.height
423                });
424            } catch (error) {
425                reject(error);
426            }
427        };
428        
429        img.onerror = function() {
430            reject(new Error(`Failed to load image: ${url}`));
431        };
432        
433        img.src = url;
434    });
435}
436
437// Helper function to load a script dynamically
438function loadScript(src) {
439    return new Promise((resolve, reject) => {
440        const script = document.createElement('script');
441        script.src = src;
442        document.head.appendChild(script);
443        
444        script.onload = () => {
445            console.log(`✅ Loaded script: ${src}`);
446            resolve();
447        };
448        script.onerror = () => {
449            console.error(`❌ Failed to load script: ${src}`);
450            reject(new Error(`Failed to load script: ${src}`));
451        };
452    });
453}
454
455// Export for use in other modules or globally
456if (typeof module !== 'undefined' && module.exports) {
457    // Node.js environment (less likely for this type of utility)
458    module.exports = {
459        generateWikiLivePDF,
460        ensureJsPDFLibrary,
461        cleanTextForPdf
462    };
463} else {
464    // Browser environment - attach to window
465    window.PdfGenerator = {
466        generate: generateWikiLivePDF,
467        _ensureJsPDF: ensureJsPDFLibrary, // Expose for testing or manual pre-loading if needed
468        _cleanText: cleanTextForPdf
469    };
470    // For direct backward compatibility with existing calls to generatePDF()
471    window.generatePDF = generateWikiLivePDF;
472}
473
474console.log('PDF Generation Module - Loaded and Ready');

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.