This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend/modules/content_feed.py
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

1049 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
BreakPilot Studio - Content Feed Modul
Funktionen:
- Educational Content durchsuchen & entdecken
- Matrix Feed Integration
- Search & Filter (Kategorie, Alter, Lizenz, Tags)
- Content Preview & Download
- Rating System (5 Sterne + Kommentare)
- Favoriten & Collections
"""
class ContentFeedModule:
"""Modul für Content Feed & Discovery."""
@staticmethod
def get_css() -> str:
"""CSS für das Content Feed Modul."""
return """
/* =============================================
CONTENT FEED MODULE
============================================= */
.panel-content-feed {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bp-bg);
overflow-y: auto;
}
/* Header with Search */
.content-feed-header {
padding: 24px 40px;
background: var(--bp-surface);
border-bottom: 1px solid var(--bp-border);
position: sticky;
top: 0;
z-index: 10;
}
.feed-header-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.feed-header-top h2 {
font-size: 24px;
font-weight: 700;
color: var(--bp-text);
}
/* Search Bar */
.feed-search-bar {
position: relative;
}
.feed-search-input {
width: 100%;
padding: 14px 48px 14px 20px;
background: var(--bp-bg);
border: 1px solid var(--bp-border);
border-radius: 999px;
color: var(--bp-text);
font-size: 15px;
transition: all 0.2s;
}
.feed-search-input:focus {
outline: none;
border-color: var(--bp-primary);
box-shadow: 0 0 0 3px var(--bp-primary-soft);
}
.feed-search-icon {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
color: var(--bp-text-muted);
font-size: 18px;
}
/* Filters */
.feed-filters {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 16px;
}
.filter-group {
display: flex;
gap: 8px;
align-items: center;
}
.filter-label {
font-size: 13px;
font-weight: 500;
color: var(--bp-text-muted);
}
.filter-select {
padding: 8px 16px;
background: var(--bp-bg);
border: 1px solid var(--bp-border);
border-radius: 8px;
color: var(--bp-text);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.filter-select:hover {
border-color: var(--bp-primary);
}
.filter-chip {
padding: 8px 16px;
background: var(--bp-bg);
border: 1px solid var(--bp-border);
border-radius: 999px;
font-size: 13px;
color: var(--bp-text);
cursor: pointer;
transition: all 0.2s;
}
.filter-chip:hover {
background: var(--bp-primary-soft);
border-color: var(--bp-primary);
}
.filter-chip.active {
background: var(--bp-primary);
border-color: var(--bp-primary);
color: white;
}
/* View Toggle */
.view-toggle {
display: flex;
gap: 4px;
padding: 4px;
background: var(--bp-bg);
border-radius: 8px;
}
.view-btn {
padding: 8px 12px;
background: transparent;
border: none;
border-radius: 6px;
color: var(--bp-text-muted);
cursor: pointer;
transition: all 0.2s;
}
.view-btn:hover {
background: var(--bp-surface-elevated);
}
.view-btn.active {
background: var(--bp-primary);
color: white;
}
/* Content Area */
.content-feed-content {
padding: 32px 40px;
flex: 1;
}
/* Feed Grid View */
.feed-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 24px;
}
/* Feed List View */
.feed-list {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Feed Card (Grid View) */
.feed-card {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
overflow: hidden;
transition: all 0.3s;
cursor: pointer;
}
.feed-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
border-color: var(--bp-primary);
}
.feed-card-thumbnail {
width: 100%;
height: 200px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 64px;
color: white;
position: relative;
}
.feed-card-badge {
position: absolute;
top: 12px;
right: 12px;
padding: 6px 12px;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
border-radius: 6px;
font-size: 11px;
font-weight: 600;
color: white;
text-transform: uppercase;
}
.feed-card-body {
padding: 20px;
}
.feed-card-title {
font-size: 17px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 8px;
line-height: 1.4;
}
.feed-card-description {
font-size: 14px;
color: var(--bp-text-muted);
margin-bottom: 16px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.feed-card-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.feed-meta-badge {
padding: 4px 10px;
background: var(--bp-bg);
border-radius: 4px;
font-size: 11px;
color: var(--bp-text-muted);
display: inline-flex;
align-items: center;
gap: 4px;
}
.feed-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 16px;
border-top: 1px solid var(--bp-border);
}
.feed-rating {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--bp-text-muted);
}
.feed-stars {
color: var(--bp-gold);
}
.feed-actions {
display: flex;
gap: 8px;
}
.feed-action-btn {
padding: 8px 16px;
background: transparent;
border: 1px solid var(--bp-border);
border-radius: 6px;
color: var(--bp-text);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.feed-action-btn:hover {
background: var(--bp-primary);
border-color: var(--bp-primary);
color: white;
}
.feed-action-btn-primary {
background: var(--bp-primary);
border-color: var(--bp-primary);
color: white;
}
.feed-action-btn-primary:hover {
background: var(--bp-primary-hover);
}
/* Feed List Card */
.feed-list-card {
display: flex;
gap: 20px;
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
padding: 20px;
transition: all 0.3s;
cursor: pointer;
}
.feed-list-card:hover {
border-color: var(--bp-primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.feed-list-thumbnail {
width: 120px;
height: 120px;
flex-shrink: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: white;
}
.feed-list-body {
flex: 1;
}
/* Content Modal */
.content-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.content-modal {
background: var(--bp-surface);
border-radius: 16px;
max-width: 900px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.content-modal-header {
padding: 32px;
border-bottom: 1px solid var(--bp-border);
position: sticky;
top: 0;
background: var(--bp-surface);
z-index: 1;
}
.modal-close {
float: right;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid var(--bp-border);
border-radius: 6px;
color: var(--bp-text-muted);
cursor: pointer;
font-size: 18px;
transition: all 0.2s;
}
.modal-close:hover {
background: var(--bp-danger);
border-color: var(--bp-danger);
color: white;
}
.content-modal-body {
padding: 32px;
}
.modal-content-preview {
width: 100%;
max-height: 500px;
background: var(--bp-bg);
border-radius: 12px;
margin-bottom: 24px;
overflow: hidden;
}
.modal-content-preview iframe,
.modal-content-preview video,
.modal-content-preview img {
width: 100%;
height: 100%;
border: none;
}
.modal-content-info {
margin-top: 24px;
}
.modal-section-title {
font-size: 14px;
font-weight: 600;
color: var(--bp-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
.license-info {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--bp-bg);
border: 1px solid var(--bp-border);
border-radius: 8px;
margin-bottom: 24px;
}
.license-badge-large {
font-size: 32px;
}
.license-text {
flex: 1;
}
.license-name-large {
font-size: 16px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 4px;
}
.license-link {
font-size: 13px;
color: var(--bp-primary);
text-decoration: none;
}
.license-link:hover {
text-decoration: underline;
}
/* Rating Section */
.rating-section {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid var(--bp-border);
}
.rating-stars-interactive {
display: flex;
gap: 8px;
font-size: 32px;
margin-bottom: 16px;
}
.star-btn {
background: none;
border: none;
color: var(--bp-border);
cursor: pointer;
transition: all 0.2s;
padding: 0;
}
.star-btn:hover,
.star-btn.active {
color: var(--bp-gold);
transform: scale(1.1);
}
.rating-comment {
width: 100%;
padding: 12px;
background: var(--bp-bg);
border: 1px solid var(--bp-border);
border-radius: 8px;
color: var(--bp-text);
font-family: inherit;
font-size: 14px;
resize: vertical;
min-height: 80px;
}
.rating-submit {
margin-top: 12px;
padding: 12px 24px;
background: var(--bp-primary);
border: none;
border-radius: 8px;
color: white;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.rating-submit:hover {
background: var(--bp-primary-hover);
}
/* Empty State */
.feed-empty {
text-align: center;
padding: 80px 20px;
color: var(--bp-text-muted);
}
.feed-empty-icon {
font-size: 80px;
margin-bottom: 24px;
}
.feed-empty-title {
font-size: 20px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 12px;
}
.feed-empty-text {
font-size: 15px;
line-height: 1.6;
}
/* Loading State */
.feed-loading {
text-align: center;
padding: 60px 20px;
color: var(--bp-text-muted);
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid var(--bp-border);
border-top-color: var(--bp-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
"""
@staticmethod
def get_html() -> str:
"""HTML für das Content Feed Modul."""
return """
<!-- Content Feed Panel -->
<div id="panel-content-feed" class="panel panel-content-feed" style="display: none;">
<!-- Header with Search -->
<div class="content-feed-header">
<div class="feed-header-top">
<h2>📚 Content Feed</h2>
<div class="view-toggle">
<button class="view-btn active" data-view="grid">⬜</button>
<button class="view-btn" data-view="list">☰</button>
</div>
</div>
<div class="feed-search-bar">
<input type="text" class="feed-search-input" id="feed-search" placeholder="Suche nach Educational Content...">
<span class="feed-search-icon">🔍</span>
</div>
<div class="feed-filters">
<div class="filter-group">
<span class="filter-label">Kategorie:</span>
<select class="filter-select" id="filter-category">
<option value="">Alle</option>
<option value="movement">🏃 Bewegung</option>
<option value="math">🔢 Mathe</option>
<option value="steam">🔬 STEAM</option>
<option value="language">📖 Sprache</option>
<option value="arts">🎨 Kunst</option>
<option value="social">🤝 Sozial</option>
<option value="mindfulness">🧘 Achtsamkeit</option>
</select>
</div>
<div class="filter-group">
<span class="filter-label">Typ:</span>
<select class="filter-select" id="filter-type">
<option value="">Alle</option>
<option value="video">📹 Video</option>
<option value="pdf">📄 PDF</option>
<option value="h5p">🎓 H5P</option>
<option value="image_gallery">🖼️ Bilder</option>
<option value="audio">🎵 Audio</option>
</select>
</div>
<div class="filter-group">
<span class="filter-label">Alter:</span>
<select class="filter-select" id="filter-age">
<option value="">Alle</option>
<option value="3-6">3-6 Jahre</option>
<option value="6-10">6-10 Jahre</option>
<option value="10-14">10-14 Jahre</option>
<option value="14-18">14-18 Jahre</option>
</select>
</div>
<div class="filter-chip active" data-filter="all">
Alle
</div>
<div class="filter-chip" data-filter="favorites">
⭐ Favoriten
</div>
<div class="filter-chip" data-filter="recent">
🆕 Neu
</div>
</div>
</div>
<!-- Content Area -->
<div class="content-feed-content">
<!-- Loading State -->
<div class="feed-loading" id="feed-loading">
<div class="spinner"></div>
<div>Content wird geladen...</div>
</div>
<!-- Grid View -->
<div class="feed-grid" id="feed-grid" style="display: none;">
<!-- Content cards will be inserted here -->
</div>
<!-- List View -->
<div class="feed-list" id="feed-list" style="display: none;">
<!-- Content list items will be inserted here -->
</div>
<!-- Empty State -->
<div class="feed-empty" id="feed-empty" style="display: none;">
<div class="feed-empty-icon">📦</div>
<div class="feed-empty-title">Kein Content gefunden</div>
<div class="feed-empty-text">
Versuchen Sie andere Suchbegriffe oder Filter.<br>
Oder erstellen Sie selbst Educational Content!
</div>
</div>
</div>
</div>
<!-- Content Modal Template -->
<template id="content-modal-template">
<div class="content-modal-overlay">
<div class="content-modal">
<div class="content-modal-header">
<button class="modal-close">×</button>
<h2 class="modal-title">Content Title</h2>
</div>
<div class="content-modal-body">
<div class="modal-content-preview">
<!-- Preview will be inserted here -->
</div>
<div class="modal-content-info">
<p class="modal-description">Description...</p>
<div class="feed-card-meta" style="margin: 16px 0;">
<!-- Meta badges -->
</div>
<div class="modal-section-title">Creative Commons Lizenz</div>
<div class="license-info">
<div class="license-badge-large">⚖️</div>
<div class="license-text">
<div class="license-name-large">CC BY-SA 4.0</div>
<a href="#" class="license-link" target="_blank">Details anzeigen →</a>
</div>
</div>
<div class="rating-section">
<div class="modal-section-title">Bewerten Sie diesen Content</div>
<div class="rating-stars-interactive" data-rating="0">
<button class="star-btn" data-star="1">★</button>
<button class="star-btn" data-star="2">★</button>
<button class="star-btn" data-star="3">★</button>
<button class="star-btn" data-star="4">★</button>
<button class="star-btn" data-star="5">★</button>
</div>
<textarea class="rating-comment" placeholder="Teilen Sie Ihre Erfahrungen mit diesem Content..."></textarea>
<button class="rating-submit">Bewertung abgeben</button>
</div>
<div class="form-actions" style="margin-top: 24px;">
<button class="btn-primary">📥 Content herunterladen</button>
<button class="btn-secondary">⭐ Zu Favoriten</button>
</div>
</div>
</div>
</div>
</div>
</template>
"""
@staticmethod
def get_js() -> str:
"""JavaScript für das Content Feed Modul."""
return """
// =============================================
// CONTENT FEED MODULE
// =============================================
(function() {
'use strict';
const ContentFeed = {
currentView: 'grid',
allContent: [],
filteredContent: [],
currentFilters: {
search: '',
category: '',
type: '',
age: '',
preset: 'all'
},
init() {
this.bindViewToggle();
this.bindSearch();
this.bindFilters();
this.loadContent();
},
bindViewToggle() {
document.querySelectorAll('.view-btn').forEach(btn => {
btn.addEventListener('click', () => {
const view = btn.dataset.view;
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
this.currentView = view;
this.renderContent();
});
});
},
bindSearch() {
const searchInput = document.getElementById('feed-search');
let searchTimeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.currentFilters.search = e.target.value.toLowerCase();
this.filterContent();
}, 300);
});
},
bindFilters() {
// Select filters
['category', 'type', 'age'].forEach(filter => {
document.getElementById(`filter-${filter}`).addEventListener('change', (e) => {
this.currentFilters[filter] = e.target.value;
this.filterContent();
});
});
// Chip filters
document.querySelectorAll('.filter-chip').forEach(chip => {
chip.addEventListener('click', () => {
document.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
this.currentFilters.preset = chip.dataset.filter;
this.filterContent();
});
});
},
async loadContent() {
document.getElementById('feed-loading').style.display = 'block';
document.getElementById('feed-grid').style.display = 'none';
document.getElementById('feed-list').style.display = 'none';
document.getElementById('feed-empty').style.display = 'none';
try {
const response = await fetch('http://localhost:8002/api/v1/content?limit=50');
if (!response.ok) throw new Error('Failed to load content');
const data = await response.json();
this.allContent = data.content || [];
this.filteredContent = this.allContent;
this.renderContent();
} catch (error) {
console.error('Error loading content:', error);
document.getElementById('feed-loading').innerHTML = `
<div style="color: var(--bp-danger);">
❌ Fehler beim Laden des Contents<br>
<small>${error.message}</small>
</div>
`;
}
},
filterContent() {
let filtered = this.allContent;
// Search filter
if (this.currentFilters.search) {
filtered = filtered.filter(item =>
item.title.toLowerCase().includes(this.currentFilters.search) ||
(item.description && item.description.toLowerCase().includes(this.currentFilters.search))
);
}
// Category filter
if (this.currentFilters.category) {
filtered = filtered.filter(item => item.category === this.currentFilters.category);
}
// Type filter
if (this.currentFilters.type) {
filtered = filtered.filter(item => item.content_type === this.currentFilters.type);
}
// Age filter
if (this.currentFilters.age) {
const [min, max] = this.currentFilters.age.split('-').map(Number);
filtered = filtered.filter(item =>
item.age_min <= max && item.age_max >= min
);
}
// Preset filters
if (this.currentFilters.preset === 'favorites') {
// TODO: Filter favorites from localStorage
} else if (this.currentFilters.preset === 'recent') {
filtered = filtered.sort((a, b) =>
new Date(b.created_at) - new Date(a.created_at)
).slice(0, 20);
}
this.filteredContent = filtered;
this.renderContent();
},
renderContent() {
document.getElementById('feed-loading').style.display = 'none';
if (this.filteredContent.length === 0) {
document.getElementById('feed-grid').style.display = 'none';
document.getElementById('feed-list').style.display = 'none';
document.getElementById('feed-empty').style.display = 'block';
return;
}
document.getElementById('feed-empty').style.display = 'none';
if (this.currentView === 'grid') {
this.renderGridView();
} else {
this.renderListView();
}
},
renderGridView() {
const grid = document.getElementById('feed-grid');
grid.style.display = 'grid';
document.getElementById('feed-list').style.display = 'none';
grid.innerHTML = this.filteredContent.map(item => `
<div class="feed-card" data-id="${item.id}">
<div class="feed-card-thumbnail">
${this.getContentIcon(item.content_type)}
<div class="feed-card-badge">${item.content_type}</div>
</div>
<div class="feed-card-body">
<div class="feed-card-title">${item.title}</div>
<div class="feed-card-description">${item.description || 'Keine Beschreibung'}</div>
<div class="feed-card-meta">
<span class="feed-meta-badge">${this.getCategoryIcon(item.category)} ${this.getCategoryName(item.category)}</span>
<span class="feed-meta-badge">👥 ${item.age_min}-${item.age_max} Jahre</span>
<span class="feed-meta-badge">⚖️ ${item.license}</span>
</div>
<div class="feed-card-footer">
<div class="feed-rating">
<span class="feed-stars">⭐⭐⭐⭐⭐</span>
<span>${item.avg_rating || 0}/5</span>
</div>
<div class="feed-actions">
<button class="feed-action-btn feed-action-btn-primary" onclick="ContentFeed.openModal('${item.id}')">
Ansehen
</button>
</div>
</div>
</div>
</div>
`).join('');
},
renderListView() {
const list = document.getElementById('feed-list');
list.style.display = 'flex';
document.getElementById('feed-grid').style.display = 'none';
list.innerHTML = this.filteredContent.map(item => `
<div class="feed-list-card" data-id="${item.id}" onclick="ContentFeed.openModal('${item.id}')">
<div class="feed-list-thumbnail">
${this.getContentIcon(item.content_type)}
</div>
<div class="feed-list-body">
<div class="feed-card-title">${item.title}</div>
<div class="feed-card-description">${item.description || 'Keine Beschreibung'}</div>
<div class="feed-card-meta">
<span class="feed-meta-badge">${this.getCategoryIcon(item.category)} ${this.getCategoryName(item.category)}</span>
<span class="feed-meta-badge">👥 ${item.age_min}-${item.age_max} Jahre</span>
<span class="feed-meta-badge">⚖️ ${item.license}</span>
<span class="feed-meta-badge">📥 ${item.downloads || 0} Downloads</span>
</div>
</div>
</div>
`).join('');
},
openModal(contentId) {
const content = this.filteredContent.find(c => c.id === contentId);
if (!content) return;
const template = document.getElementById('content-modal-template');
const modal = template.content.cloneNode(true);
// Fill modal with content data
modal.querySelector('.modal-title').textContent = content.title;
modal.querySelector('.modal-description').textContent = content.description || 'Keine Beschreibung';
// Close button
modal.querySelector('.modal-close').addEventListener('click', (e) => {
e.target.closest('.content-modal-overlay').remove();
});
// Close on overlay click
modal.querySelector('.content-modal-overlay').addEventListener('click', (e) => {
if (e.target.classList.contains('content-modal-overlay')) {
e.target.remove();
}
});
// Add modal to body
document.body.appendChild(modal);
},
getContentIcon(type) {
const icons = {
video: '📹',
pdf: '📄',
h5p: '🎓',
image_gallery: '🖼️',
audio: '🎵',
markdown: '📝'
};
return icons[type] || '📦';
},
getCategoryIcon(category) {
const icons = {
movement: '🏃',
math: '🔢',
steam: '🔬',
language: '📖',
arts: '🎨',
social: '🤝',
mindfulness: '🧘'
};
return icons[category] || '📚';
},
getCategoryName(category) {
const names = {
movement: 'Bewegung',
math: 'Mathe',
steam: 'STEAM',
language: 'Sprache',
arts: 'Kunst',
social: 'Sozial',
mindfulness: 'Achtsamkeit'
};
return names[category] || category;
}
};
// Make ContentFeed globally accessible
window.ContentFeed = ContentFeed;
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => ContentFeed.init());
} else {
ContentFeed.init();
}
})();
"""