PageSourceSearch

https://dynamic-salmiakki-095693.netlify.app/biographical-relationship-viewer.js

js dynamic-salmiakki-095693.netlify.app collected 2026-10-03 10:40:23 UTC 19,379 bytes, 531 lines download raw bytes

1// 🌳 Biographical Relationship Viewer for WikiLive
2// Interactive tree/card view showing all biographical properties from infoboxes
3
4/**
5 * Creates an interactive biographical relationship viewer
6 * Displays above the infoboxes with expandable sections for each entity
7 */
8function createBiographicalRelationshipViewer(data, chatContainer) {
9    console.log('[BiographicalViewer] 🌳 ========== FUNCTION CALLED ==========');
10    console.log('[BiographicalViewer] Data received:', {
11        hasData: !!data,
12        hasContainer: !!chatContainer,
13        dataKeys: data ? Object.keys(data) : [],
14        hasWikipediaData: !!(data?.wikipediaData),
15        wikipediaDataCount: data?.wikipediaData?.length || 0
16    });
17    
18    if (!data || !chatContainer) {
19        console.error('[BiographicalViewer] ❌ Missing required data or container');
20        return;
21    }
22    
23    // Extract all entities with biographical data
24    const entitiesWithBio = extractBiographicalEntities(data);
25    
26    console.log('[BiographicalViewer] Entities with bio data:', {
27        count: entitiesWithBio.length,
28        entities: entitiesWithBio.map(e => ({ title: e.title, propCount: Object.keys(e.bioProperties).length }))
29    });
30    
31    if (entitiesWithBio.length === 0) {
32        console.log('[BiographicalViewer] ⚠️ No biographical data found - viewer will not be created');
33        return;
34    }
35    
36    console.log(`[BiographicalViewer] Found ${entitiesWithBio.length} entities with biographical data`);
37    
38    // Remove existing viewer if present
39    const existingViewer = chatContainer.querySelector('.biographical-relationship-viewer');
40    if (existingViewer) {
41        existingViewer.remove();
42    }
43    
44    // Create main container
45    const viewerContainer = document.createElement('div');
46    viewerContainer.className = 'biographical-relationship-viewer';
47    viewerContainer.style.cssText = `
48        margin: 20px 0;
49        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
50        border-radius: 16px;
51        padding: 24px;
52        box-shadow: 0 10px 30px rgba(0,0,0,0.2);
53        animation: slideInFromTop 0.5s ease-out;
54    `;
55    
56    // Add animation keyframes
57    if (!document.getElementById('bio-viewer-animations')) {
58        const style = document.createElement('style');
59        style.id = 'bio-viewer-animations';
60        style.textContent = `
61            @keyframes slideInFromTop {
62                from {
63                    opacity: 0;
64                    transform: translateY(-20px);
65                }
66                to {
67                    opacity: 1;
68                    transform: translateY(0);
69                }
70            }
71            
72            @keyframes expandSection {
73                from {
74                    max-height: 0;
75                    opacity: 0;
76                }
77                to {
78                    max-height: 1000px;
79                    opacity: 1;
80                }
81            }
82            
83            .bio-property-item:hover {
84                transform: translateX(5px);
85                background: rgba(255,255,255,0.15);
86            }
87        `;
88        document.head.appendChild(style);
89    }
90    
91    // Create header
92    const header = document.createElement('div');
93    header.style.cssText = `
94        display: flex;
95        justify-content: space-between;
96        align-items: center;
97        margin-bottom: 20px;
98    `;
99    
100    const headerTitle = document.createElement('h3');
101    headerTitle.innerHTML = '🌳 Biographical Relationships';
102    headerTitle.style.cssText = `
103        margin: 0;
104        color: white;
105        font-size: 22px;
106        font-weight: 600;
107        text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
108    `;
109    
110    const toggleAllBtn = document.createElement('button');
111    toggleAllBtn.innerHTML = '📖 Expand All';
112    toggleAllBtn.style.cssText = `
113        background: rgba(255,255,255,0.2);
114        border: 1px solid rgba(255,255,255,0.3);
115        color: white;
116        padding: 8px 16px;
117        border-radius: 20px;
118        cursor: pointer;
119        font-size: 14px;
120        font-weight: 500;
121        transition: all 0.3s ease;
122        backdrop-filter: blur(10px);
123    `;
124    
125    let allExpanded = false;
126    toggleAllBtn.addEventListener('click', () => {
127        allExpanded = !allExpanded;
128        toggleAllBtn.innerHTML = allExpanded ? '📕 Collapse All' : '📖 Expand All';
129        
130        const allSections = viewerContainer.querySelectorAll('.bio-properties-section');
131        allSections.forEach(section => {
132            if (allExpanded) {
133                section.style.display = 'block';
134                section.style.animation = 'expandSection 0.3s ease-out';
135            } else {
136                section.style.display = 'none';
137            }
138        });
139        
140        const allIcons = viewerContainer.querySelectorAll('.expand-icon');
141        allIcons.forEach(icon => {
142            icon.textContent = allExpanded ? '▼' : '▶';
143        });
144    });
145    
146    toggleAllBtn.addEventListener('mouseenter', () => {
147        toggleAllBtn.style.background = 'rgba(255,255,255,0.3)';
148        toggleAllBtn.style.transform = 'scale(1.05)';
149    });
150    
151    toggleAllBtn.addEventListener('mouseleave', () => {
152        toggleAllBtn.style.background = 'rgba(255,255,255,0.2)';
153        toggleAllBtn.style.transform = 'scale(1)';
154    });
155    
156    header.appendChild(headerTitle);
157    header.appendChild(toggleAllBtn);
158    viewerContainer.appendChild(header);
159    
160    // Create stats summary
161    const stats = createStatsSummary(entitiesWithBio);
162    viewerContainer.appendChild(stats);
163    
164    // Create entity cards
165    const cardsContainer = document.createElement('div');
166    cardsContainer.style.cssText = `
167        display: grid;
168        grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
169        gap: 16px;
170        margin-top: 20px;
171    `;
172    
173    entitiesWithBio.forEach((entity, index) => {
174        const card = createEntityBioCard(entity, index);
175        cardsContainer.appendChild(card);
176    });
177    
178    viewerContainer.appendChild(cardsContainer);
179    
180    // Insert before the fancy UI preview or at the beginning
181    const fancyPreview = chatContainer.querySelector('.fancy-ui-preview');
182    if (fancyPreview) {
183        chatContainer.insertBefore(viewerContainer, fancyPreview);
184    } else {
185        chatContainer.appendChild(viewerContainer);
186    }
187    
188    console.log('[BiographicalViewer] ✅ Viewer created successfully');
189}
190
191/**
192 * Extract entities with biographical data from the response
193 */
194function extractBiographicalEntities(data) {
195    console.log('[BiographicalViewer] 📊 Extracting biographical entities...');
196    const entities = [];
197    const bioFields = ['spouse', 'spouses', 'children', 'born', 'died', 'birthDate', 'deathDate', 
198                       'birthPlace', 'deathPlace', 'family', 'parents', 'relatives', 'siblings',
199                       'education', 'occupation', 'occupations', 'party', 'nationality', 'citizenship'];
200    
201    console.log('[BiographicalViewer] Looking for fields:', bioFields);
202    
203    // Check wikipediaData
204    if (data.wikipediaData && Array.isArray(data.wikipediaData)) {
205        console.log('[BiographicalViewer] Checking wikipediaData:', data.wikipediaData.length, 'entities');
206        data.wikipediaData.forEach((entity, index) => {
207            // Check for both pageData and infobox properties
208            const dataSource = entity.pageData || entity.infobox;
209            
210            console.log(`[BiographicalViewer] Entity ${index + 1}:`, {
211                title: entity.pageTitle || entity.title,
212                hasPageData: !!entity.pageData,
213                hasInfobox: !!entity.infobox,
214                dataSourceKeys: dataSource ? Object.keys(dataSource) : []
215            });
216            
217            if (dataSource) {
218                const bioProps = {};
219                Object.keys(dataSource).forEach(key => {
220                    const lowerKey = key.toLowerCase();
221                    if (bioFields.some(field => lowerKey.includes(field))) {
222                        bioProps[key] = dataSource[key];
223                        console.log(`[BiographicalViewer] ✅ Found bio field: ${key} = ${dataSource[key]}`);
224                    }
225                });
226                
227                if (Object.keys(bioProps).length > 0) {
228                    console.log(`[BiographicalViewer] ✅ Adding entity with ${Object.keys(bioProps).length} bio properties:`, entity.pageTitle || entity.title);
229                    entities.push({
230                        title: entity.pageTitle || entity.title,
231                        type: entity.type || 'Unknown',
232                        bioProperties: bioProps,
233                        allProperties: dataSource,
234                        imageUrl: entity.imageUrl || entity.main_image_url,
235                        summary: entity.summary || entity.extract
236                    });
237                }
238            }
239        });
240    }
241    
242    // Check infoboxes
243    if (data.infoboxes && Array.isArray(data.infoboxes)) {
244        data.infoboxes.forEach(entity => {
245            if (entity.properties) {
246                const bioProps = {};
247                Object.keys(entity.properties).forEach(key => {
248                    const lowerKey = key.toLowerCase();
249                    if (bioFields.some(field => lowerKey.includes(field))) {
250                        bioProps[key] = entity.properties[key];
251                    }
252                });
253                
254                if (Object.keys(bioProps).length > 0) {
255                    entities.push({
256                        title: entity.title,
257                        type: entity.type || 'Unknown',
258                        bioProperties: bioProps,
259                        allProperties: entity.properties,
260                        imageUrl: entity.imageUrl,
261                        summary: entity.summary
262                    });
263                }
264            }
265        });
266    }
267    
268    return entities;
269}
270
271/**
272 * Create stats summary bar
273 */
274function createStatsSummary(entities) {
275    const statsContainer = document.createElement('div');
276    statsContainer.style.cssText = `
277        display: flex;
278        gap: 16px;
279        flex-wrap: wrap;
280        padding: 16px;
281        background: rgba(255,255,255,0.1);
282        border-radius: 12px;
283        backdrop-filter: blur(10px);
284    `;
285    
286    const totalEntities = entities.length;
287    const totalProperties = entities.reduce((sum, e) => sum + Object.keys(e.bioProperties).length, 0);
288    const uniquePropertyTypes = new Set();
289    entities.forEach(e => Object.keys(e.bioProperties).forEach(k => uniquePropertyTypes.add(k)));
290    
291    const stats = [
292        { icon: '👥', label: 'Entities', value: totalEntities },
293        { icon: '🔗', label: 'Relationships', value: totalProperties },
294        { icon: '📊', label: 'Property Types', value: uniquePropertyTypes.size }
295    ];
296    
297    stats.forEach(stat => {
298        const statItem = document.createElement('div');
299        statItem.style.cssText = `
300            flex: 1;
301            min-width: 120px;
302            text-align: center;
303            padding: 12px;
304            background: rgba(255,255,255,0.05);
305            border-radius: 8px;
306            border: 1px solid rgba(255,255,255,0.1);
307        `;
308        
309        statItem.innerHTML = `
310            <div style="font-size: 24px; margin-bottom: 4px;">${stat.icon}</div>
311            <div style="font-size: 20px; font-weight: 600; color: white; margin-bottom: 2px;">${stat.value}</div>
312            <div style="font-size: 12px; color: rgba(255,255,255,0.7); text-transform: uppercase; letter-spacing: 0.5px;">${stat.label}</div>
313        `;
314        
315        statsContainer.appendChild(statItem);
316    });
317    
318    return statsContainer;
319}
320
321/**
322 * Create individual entity biographical card
323 */
324function createEntityBioCard(entity, index) {
325    const card = document.createElement('div');
326    card.className = 'bio-entity-card';
327    card.style.cssText = `
328        background: rgba(255,255,255,0.95);
329        border-radius: 12px;
330        overflow: hidden;
331        box-shadow: 0 4px 12px rgba(0,0,0,0.15);
332        transition: all 0.3s ease;
333        animation: slideInFromTop 0.5s ease-out ${index * 0.1}s backwards;
334    `;
335    
336    card.addEventListener('mouseenter', () => {
337        card.style.transform = 'translateY(-4px)';
338        card.style.boxShadow = '0 8px 24px rgba(0,0,0,0.2)';
339    });
340    
341    card.addEventListener('mouseleave', () => {
342        card.style.transform = 'translateY(0)';
343        card.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
344    });
345    
346    // Card header
347    const cardHeader = document.createElement('div');
348    cardHeader.style.cssText = `
349        padding: 16px;
350        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
351        color: white;
352        cursor: pointer;
353        display: flex;
354        align-items: center;
355        gap: 12px;
356    `;
357    
358    const expandIcon = document.createElement('span');
359    expandIcon.className = 'expand-icon';
360    expandIcon.textContent = '▶';
361    expandIcon.style.cssText = `
362        font-size: 12px;
363        transition: transform 0.3s ease;
364    `;
365    
366    const cardTitle = document.createElement('div');
367    cardTitle.style.cssText = `
368        flex: 1;
369    `;
370    
371    const titleText = document.createElement('h4');
372    titleText.textContent = entity.title;
373    titleText.style.cssText = `
374        margin: 0 0 4px 0;
375        font-size: 16px;
376        font-weight: 600;
377    `;
378    
379    const propertyCount = document.createElement('div');
380    propertyCount.textContent = `${Object.keys(entity.bioProperties).length} biographical properties`;
381    propertyCount.style.cssText = `
382        font-size: 12px;
383        opacity: 0.9;
384    `;
385    
386    cardTitle.appendChild(titleText);
387    cardTitle.appendChild(propertyCount);
388    cardHeader.appendChild(expandIcon);
389    cardHeader.appendChild(cardTitle);
390    
391    // Properties section (initially hidden)
392    const propertiesSection = document.createElement('div');
393    propertiesSection.className = 'bio-properties-section';
394    propertiesSection.style.cssText = `
395        display: none;
396        padding: 16px;
397        background: white;
398    `;
399    
400    // Group properties by category
401    const categorizedProps = categorizeProperties(entity.bioProperties);
402    
403    Object.entries(categorizedProps).forEach(([category, props]) => {
404        if (props.length === 0) return;
405        
406        const categorySection = document.createElement('div');
407        categorySection.style.cssText = `
408            margin-bottom: 16px;
409        `;
410        
411        const categoryTitle = document.createElement('div');
412        categoryTitle.textContent = category;
413        categoryTitle.style.cssText = `
414            font-size: 13px;
415            font-weight: 600;
416            color: #667eea;
417            text-transform: uppercase;
418            letter-spacing: 0.5px;
419            margin-bottom: 8px;
420            padding-bottom: 4px;
421            border-bottom: 2px solid #667eea;
422        `;
423        
424        categorySection.appendChild(categoryTitle);
425        
426        props.forEach(([key, value]) => {
427            const propItem = document.createElement('div');
428            propItem.className = 'bio-property-item';
429            propItem.style.cssText = `
430                padding: 8px 12px;
431                margin: 4px 0;
432                border-radius: 6px;
433                background: #f8f9fa;
434                transition: all 0.2s ease;
435                border-left: 3px solid #667eea;
436            `;
437            
438            const propLabel = document.createElement('div');
439            propLabel.textContent = formatPropertyLabel(key);
440            propLabel.style.cssText = `
441                font-size: 12px;
442                font-weight: 600;
443                color: #495057;
444                margin-bottom: 2px;
445            `;
446            
447            const propValue = document.createElement('div');
448            propValue.textContent = value;
449            propValue.style.cssText = `
450                font-size: 14px;
451                color: #212529;
452                line-height: 1.4;
453            `;
454            
455            propItem.appendChild(propLabel);
456            propItem.appendChild(propValue);
457            categorySection.appendChild(propItem);
458        });
459        
460        propertiesSection.appendChild(categorySection);
461    });
462    
463    // Toggle functionality
464    let isExpanded = false;
465    cardHeader.addEventListener('click', () => {
466        isExpanded = !isExpanded;
467        propertiesSection.style.display = isExpanded ? 'block' : 'none';
468        expandIcon.textContent = isExpanded ? '▼' : '▶';
469        expandIcon.style.transform = isExpanded ? 'rotate(0deg)' : 'rotate(0deg)';
470        
471        if (isExpanded) {
472            propertiesSection.style.animation = 'expandSection 0.3s ease-out';
473        }
474    });
475    
476    card.appendChild(cardHeader);
477    card.appendChild(propertiesSection);
478    
479    return card;
480}
481
482/**
483 * Categorize properties into logical groups
484 */
485function categorizeProperties(properties) {
486    const categories = {
487        '👤 Personal': [],
488        '👨‍👩‍👧‍👦 Family': [],
489        '🎓 Education & Career': [],
490        '🏛️ Political & Social': [],
491        '📍 Location': [],
492        '🎯 Other': []
493    };
494    
495    Object.entries(properties).forEach(([key, value]) => {
496        const lowerKey = key.toLowerCase();
497        
498        if (lowerKey.includes('born') || lowerKey.includes('birth') || lowerKey.includes('died') || lowerKey.includes('death') || lowerKey.includes('age')) {
499            categories['👤 Personal'].push([key, value]);
500        } else if (lowerKey.includes('spouse') || lowerKey.includes('children') || lowerKey.includes('parent') || lowerKey.includes('family') || lowerKey.includes('relative') || lowerKey.includes('sibling')) {
501            categories['👨‍👩‍👧‍👦 Family'].push([key, value]);
502        } else if (lowerKey.includes('education') || lowerKey.includes('occupation') || lowerKey.includes('career') || lowerKey.includes('field') || lowerKey.includes('work')) {
503            categories['🎓 Education & Career'].push([key, value]);
504        } else if (lowerKey.includes('party') || lowerKey.includes('political') || lowerKey.includes('president') || lowerKey.includes('senator') || lowerKey.includes('office')) {
505            categories['🏛️ Political & Social'].push([key, value]);
506        } else if (lowerKey.includes('place') || lowerKey.includes('location') || lowerKey.includes('city') || lowerKey.includes('country') || lowerKey.includes('nationality') || lowerKey.includes('citizenship')) {
507            categories['📍 Location'].push([key, value]);
508        } else {
509            categories['🎯 Other'].push([key, value]);
510        }
511    });
512    
513    return categories;
514}
515
516/**
517 * Format property label for display
518 */
519function formatPropertyLabel(key) {
520    // Convert camelCase to Title Case with spaces
521    return key
522        .replace(/([A-Z])/g, ' $1')
523        .replace(/^./, str => str.toUpperCase())
524        .trim();
525}
526
527// Expose to global scope
528if (typeof window !== 'undefined') {
529    window.createBiographicalRelationshipViewer = createBiographicalRelationshipViewer;
530    console.log('🌳 [Global] createBiographicalRelationshipViewer function exposed to window scope');
531}

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.