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.