diff --git a/giscus-comments.js b/giscus-comments.js index 33bc7a8fc..ed916f11b 100644 --- a/giscus-comments.js +++ b/giscus-comments.js @@ -3,6 +3,10 @@ 'use strict'; let currentPath = ''; + let giscusLoaded = false; + let intersectionObserver = null; + let themeObserver = null; + let loadingTimeout = null; // Add CSS styles once function addStyles() { @@ -59,6 +63,106 @@ .giscus-container.loaded { opacity: 1; } + + /* Lazy loading placeholder */ + .giscus-placeholder { + min-height: 200px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted, #666); + font-size: 0.875rem; + border: 1px dashed var(--border-color, #e5e7eb); + border-radius: 8px; + margin: 1rem 0; + cursor: pointer; + transition: all 0.2s ease; + } + + .giscus-placeholder:hover { + background: var(--hover-bg, #f9fafb); + border-color: var(--primary-color, #3b82f6); + } + + .giscus-placeholder-content { + text-align: center; + line-height: 1.5; + } + + .giscus-placeholder-icon { + font-size: 2rem; + margin-bottom: 0.5rem; + opacity: 0.5; + } + + /* Discussion notice styling */ + .giscus-notice { + background: var(--notice-bg, #f8fafc); + border: 1px solid var(--notice-border, #e2e8f0); + border-radius: 8px; + padding: 1rem; + margin: 1rem 0; + color: var(--notice-text, #475569); + font-size: 0.875rem; + line-height: 1.5; + } + + .giscus-notice-title { + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--notice-title, #334155); + } + + .giscus-notice-button { + background: var(--primary-color, #3b82f6); + color: white; + border: none; + border-radius: 4px; + padding: 0.5rem 1rem; + cursor: pointer; + font-size: 0.875rem; + text-decoration: none; + display: inline-flex; + align-items: center; + transition: background-color 0.2s ease; + } + + .giscus-notice-button:hover { + background: var(--primary-color-hover, #2563eb); + } + + .giscus-notice-button.secondary { + background: var(--secondary-color, #6b7280); + } + + .giscus-notice-button.secondary:hover { + background: var(--secondary-color-hover, #4b5563); + } + + /* Dark mode notice styling */ + @media (prefers-color-scheme: dark) { + .giscus-notice { + background: var(--notice-bg-dark, #1e293b); + border-color: var(--notice-border-dark, #334155); + color: var(--notice-text-dark, #94a3b8); + } + + .giscus-notice-title { + color: var(--notice-title-dark, #e2e8f0); + } + } + + [data-theme="dark"] .giscus-notice, + .dark .giscus-notice { + background: var(--notice-bg-dark, #1e293b); + border-color: var(--notice-border-dark, #334155); + color: var(--notice-text-dark, #94a3b8); + } + + [data-theme="dark"] .giscus-notice-title, + .dark .giscus-notice-title { + color: var(--notice-title-dark, #e2e8f0); + } `; document.head.appendChild(style); } @@ -75,6 +179,67 @@ el.remove(); } }); + + // Clean up observers + if (intersectionObserver) { + intersectionObserver.disconnect(); + intersectionObserver = null; + } + + if (themeObserver) { + themeObserver.disconnect(); + themeObserver = null; + } + + if (loadingTimeout) { + clearTimeout(loadingTimeout); + loadingTimeout = null; + } + + giscusLoaded = false; + } + + // Create placeholder for lazy loading + function createGiscusPlaceholder() { + const isChinesePage = window.location.pathname.includes('/zh-CN/') || window.location.pathname.includes('/cn/'); + const placeholderText = isChinesePage + ? '💬 点击或滚动到此处加载评论' + : '💬 Click or scroll here to load comments'; + + const placeholder = document.createElement('div'); + placeholder.className = 'giscus-placeholder'; + placeholder.innerHTML = ` +
+
💬
+
${placeholderText}
+
+ `; + + // Click to load + placeholder.addEventListener('click', () => { + loadGiscusContent(placeholder.parentElement); + }); + + return placeholder; + } + + // Setup intersection observer for lazy loading + function setupLazyLoading(container) { + if (!window.IntersectionObserver || giscusLoaded) return; + + intersectionObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting && !giscusLoaded) { + loadGiscusContent(container); + intersectionObserver.disconnect(); + } + }); + }, { + rootMargin: '200px 0px', // Start loading 200px before the element comes into view + threshold: 0 + }); + + intersectionObserver.observe(container); } // Find the best content container for giscus @@ -176,8 +341,13 @@ // Set up theme change observer function setupThemeObserver() { + // Don't create multiple observers + if (themeObserver) { + return; + } + // Watch for changes to the document element's attributes and classes - const observer = new MutationObserver((mutations) => { + themeObserver = new MutationObserver((mutations) => { let themeChanged = false; mutations.forEach((mutation) => { @@ -196,12 +366,12 @@ }); // Observe both html and body elements for theme changes - observer.observe(document.documentElement, { + themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme', 'data-color-scheme'] }); - observer.observe(document.body, { + themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme', 'data-color-scheme'] }); @@ -221,33 +391,239 @@ }); } - // Create and insert giscus - async function loadGiscus() { - const newPath = window.location.pathname; + // Generate GitHub Discussions URL for current page + function generateDiscussionUrl() { + const repoOwner = 'Comfy-Org'; + const repoName = 'docs'; + const currentPath = window.location.pathname; + const currentTitle = document.title || 'Discussion'; + const currentUrl = window.location.href; - // Exclude paths that should not have comments - const excludedPaths = ['/', '/zh-CN', '/zh-CN/']; - - // Skip if current path is in excluded list or contains API/search paths - if (excludedPaths.includes(newPath) || newPath.includes('/api/') || newPath.includes('/search')) { - return; + // Extract meaningful part of the path for search + const pathSegments = currentPath.split('/').filter(Boolean); + let searchQuery = ''; + + if (pathSegments.length > 0) { + // Use the complete path as search term, keeping language prefixes + // Example: /zh-CN/development/core-concepts/workflow -> "zh-CN development core concepts workflow" + searchQuery = pathSegments + .join(' ') + .replace(/[-_]/g, ' ') // Replace dashes/underscores with spaces + .replace(/\s+/g, ' ') // Normalize multiple spaces + .trim(); } - currentPath = newPath; - cleanupGiscus(); + // If no meaningful path, use page title + if (!searchQuery && currentTitle) { + searchQuery = currentTitle.replace(/[^\w\s]/g, '').trim(); + } - const contentContainer = await findContentContainer(); - if (!contentContainer) { - console.warn('Giscus: Could not find suitable content container'); - return; + // Create search URL to find existing discussions + if (searchQuery) { + const encodedQuery = encodeURIComponent(searchQuery); + return `https://github.com/${repoOwner}/${repoName}/discussions?discussions_q=${encodedQuery}`; } - // Create giscus container - const giscusContainer = document.createElement('div'); - giscusContainer.className = 'giscus-container loading'; + // Fallback to discussions main page + return `https://github.com/${repoOwner}/${repoName}/discussions`; + } + + // Generate URL for creating a new discussion with context + function generateNewDiscussionUrl() { + const repoOwner = 'Comfy-Org'; + const repoName = 'docs'; + const currentPath = window.location.pathname; + const currentTitle = document.title || 'Discussion'; + const currentUrl = window.location.href; + + // Create discussion title based on complete page path + let discussionTitle = currentTitle; + if (currentPath && currentPath !== '/') { + const pathSegments = currentPath.split('/').filter(Boolean); + + if (pathSegments.length > 0) { + // Create title from complete path, keeping language prefixes + // Example: /zh-CN/development/core-concepts/workflow -> "zh-CN/development/core-concepts/workflow" + const pathTitle = pathSegments.join('/'); + discussionTitle = pathTitle; + } + } + + // Create discussion body with context + const body = `Discussion for: ${currentTitle}\nPage: ${currentUrl}\nPath: ${currentPath}`; + const encodedTitle = encodeURIComponent(discussionTitle); + const encodedBody = encodeURIComponent(body); + + return `https://github.com/${repoOwner}/${repoName}/discussions/new?category=general&title=${encodedTitle}&body=${encodedBody}`; + } + + // Show friendly discussion notice + function showGiscusNotice(container, noticeType = 'rate_limit') { + const discussionUrl = generateDiscussionUrl(); + const newDiscussionUrl = generateNewDiscussionUrl(); - // Get current theme for initial load + const noticeMessages = { + rate_limit: { + en: { + title: '💬 Join the Discussion', + message: 'Comments are temporarily unavailable due to high traffic.', + suggestion: 'Please first check if there are existing discussions about this page. If you can\'t find any relevant discussions, then start a new one to connect your comments with this page.', + discussionLink: 'Find Related Discussions', + newDiscussionLink: 'Start New Discussion' + }, + zh: { + title: '💬 参与讨论', + message: '由于访问量较高,评论功能暂时不可用。', + suggestion: '请先查找是否有关于此页面的相关讨论。如果找不到相关讨论,再发起新的讨论以便将评论与此页面关联。', + discussionLink: '查找相关讨论', + newDiscussionLink: '发起新讨论' + } + }, + network: { + en: { + title: '💬 Join the Discussion', + message: 'Comments could not be loaded at this time.', + suggestion: 'Please first check if there are existing discussions about this page. If you can\'t find any relevant discussions, then start a new one to connect your comments with this page.', + discussionLink: 'Find Related Discussions', + newDiscussionLink: 'Start New Discussion' + }, + zh: { + title: '💬 参与讨论', + message: '评论暂时无法加载。', + suggestion: '请先查找是否有关于此页面的相关讨论。如果找不到相关讨论,再发起新的讨论以便将评论与此页面关联。', + discussionLink: '查找相关讨论', + newDiscussionLink: '发起新讨论' + } + } + }; + + const isChinesePage = window.location.pathname.includes('/zh-CN/') || window.location.pathname.includes('/cn/'); + const lang = isChinesePage ? 'zh' : 'en'; + const notice = noticeMessages[noticeType][lang]; + + const noticeDiv = document.createElement('div'); + noticeDiv.className = 'giscus-notice'; + noticeDiv.innerHTML = ` +
${notice.title}
+
${notice.message}
+
${notice.suggestion}
+
+ ${notice.discussionLink} + ${notice.newDiscussionLink} +
+ `; + + container.innerHTML = ''; + container.appendChild(noticeDiv); + container.classList.remove('loading'); + container.classList.add('loaded'); + } + + // Legacy retry function (may not be needed now) + window.retryGiscus = function() { + const container = document.querySelector('.giscus-container'); + if (!container) return; + + // Simply reload the giscus content + giscusLoaded = false; // Reset the loaded flag + loadGiscusContent(container); + }; + + // Listen for giscus messages to detect rate limit errors + function setupGiscusErrorListener() { + window.addEventListener('message', function(event) { + if (event.origin !== 'https://giscus.app') return; + + const message = event.data; + console.log('Giscus message received:', message); // Debug log + + const container = document.querySelector('.giscus-container'); + if (!container) return; + + // Check for various error patterns + if (message && message.giscus) { + // Standard giscus error format + if (message.giscus.error) { + const errorType = message.giscus.error.type; + console.warn('Giscus error detected:', errorType, message.giscus.error); + + if (errorType === 'rate_limit' || errorType === 'rate-limit' || errorType === 'RATE_LIMITED') { + showGiscusNotice(container, 'rate_limit'); + } else { + showGiscusNotice(container, 'network'); + } + } + + // Check for discussion data with error indicators + if (message.giscus.discussion === null && message.giscus.viewer === null) { + // This might indicate a rate limit or auth issue + console.warn('Giscus: No discussion data, possibly rate limited'); + setTimeout(() => { + if (container.querySelector('.giscus-frame')) { + const iframe = container.querySelector('.giscus-frame'); + if (iframe && iframe.contentDocument) { + const errorElements = iframe.contentDocument.querySelectorAll('[class*="error"], [class*="Error"]'); + if (errorElements.length > 0) { + console.warn('Giscus: Error elements found in iframe'); + showGiscusNotice(container, 'rate_limit'); + } + } + } + }, 3000); + } + } + + // Also check for direct error messages in the content + if (typeof message === 'string' && message.includes('rate limit')) { + console.warn('Giscus: Rate limit detected in string message'); + showGiscusNotice(container, 'rate_limit'); + } + }); + + // Additional error detection: Monitor iframe content changes + const checkForErrors = () => { + const container = document.querySelector('.giscus-container'); + const iframe = document.querySelector('.giscus-frame'); + + if (iframe && container) { + try { + // Check if iframe has loaded and has error content + setTimeout(() => { + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + if (iframeDoc) { + const bodyText = iframeDoc.body ? iframeDoc.body.textContent || '' : ''; + if (bodyText.includes('rate limit') || bodyText.includes('API rate limit') || bodyText.includes('exceeded')) { + console.warn('Giscus: Rate limit detected in iframe content'); + showGiscusNotice(container, 'rate_limit'); + } else if (bodyText.includes('error occurred')) { + console.warn('Giscus: Generic error detected in iframe content'); + showGiscusNotice(container, 'rate_limit'); // Assume it's rate limit for now + } + } + }, 5000); // Check after 5 seconds + } catch (e) { + // Cross-origin restrictions, can't access iframe content + console.log('Cannot access iframe content due to CORS'); + } + } + }; + + // Run error check periodically + setTimeout(checkForErrors, 6000); + } + + // Load actual giscus content + function loadGiscusContent(container) { + if (giscusLoaded) return; + giscusLoaded = true; + + // Clear placeholder + container.innerHTML = ''; + container.className = 'giscus-container loading'; + + // Get current theme and path info const currentTheme = getCurrentTheme(); + const newPath = window.location.pathname; // Create giscus script const script = document.createElement('script'); @@ -275,18 +651,95 @@ script.setAttribute('crossorigin', 'anonymous'); script.async = true; - // Handle giscus load event + // Handle giscus load event and errors script.onload = () => { + // Clear the loading timeout since script loaded successfully + if (loadingTimeout) { + clearTimeout(loadingTimeout); + loadingTimeout = null; + } + setTimeout(() => { - giscusContainer.classList.remove('loading'); - giscusContainer.classList.add('loaded'); + container.classList.remove('loading'); + container.classList.add('loaded'); // Set up theme observer after giscus loads setupThemeObserver(); }, 1000); }; - giscusContainer.appendChild(script); + script.onerror = () => { + // Clear the loading timeout since we got an error + if (loadingTimeout) { + clearTimeout(loadingTimeout); + loadingTimeout = null; + } + + console.error('Giscus: Failed to load giscus script'); + showGiscusNotice(container, 'network'); + }; + + // Set up a timeout to detect if giscus fails to load + loadingTimeout = setTimeout(() => { + // Check if giscus is still loading or showing an error + const iframe = container.querySelector('.giscus-frame'); + const hasError = container.querySelector('.giscus-error'); + + if (container.classList.contains('loading') || (iframe && !hasError)) { + // Check iframe content for errors if possible + try { + if (iframe && iframe.contentDocument) { + const iframeDoc = iframe.contentDocument; + const bodyText = iframeDoc.body ? iframeDoc.body.textContent || '' : ''; + + if (bodyText.includes('error occurred') || bodyText.includes('rate limit') || bodyText.includes('exceeded')) { + console.warn('Giscus: Error detected in iframe after timeout'); + showGiscusNotice(container, 'rate_limit'); + } + } else { + console.warn('Giscus: Loading timeout, possibly rate limited'); + showGiscusNotice(container, 'rate_limit'); + } + } catch (e) { + // Can't access iframe due to CORS, assume rate limit + console.warn('Giscus: Loading timeout, assuming rate limited'); + showGiscusNotice(container, 'rate_limit'); + } + } + loadingTimeout = null; + }, 8000); // Reduced to 8 second timeout for faster detection + + container.appendChild(script); + } + + // Create and insert giscus container with lazy loading + async function loadGiscus() { + const newPath = window.location.pathname; + + // Exclude paths that should not have comments + const excludedPaths = ['/', '/zh-CN', '/zh-CN/']; + + // Skip if current path is in excluded list or contains API/search paths + if (excludedPaths.includes(newPath) || newPath.includes('/api/') || newPath.includes('/search')) { + return; + } + + currentPath = newPath; + cleanupGiscus(); + + const contentContainer = await findContentContainer(); + if (!contentContainer) { + console.warn('Giscus: Could not find suitable content container'); + return; + } + + // Create giscus container with placeholder + const giscusContainer = document.createElement('div'); + giscusContainer.className = 'giscus-container'; + + // Add placeholder for lazy loading + const placeholder = createGiscusPlaceholder(); + giscusContainer.appendChild(placeholder); // Insert at the end of content, but before any navigation const navElements = contentContainer.querySelectorAll('nav, .pagination, [class*="nav"]:last-child'); @@ -296,6 +749,9 @@ } else { contentContainer.appendChild(giscusContainer); } + + // Setup lazy loading + setupLazyLoading(giscusContainer); } // Handle SPA navigation @@ -307,13 +763,13 @@ const url = location.href; if (url !== lastUrl) { lastUrl = url; - setTimeout(loadGiscus, 500); // Delay for content to load + setTimeout(loadGiscus, 800); // Increased delay for content to load } }).observe(document, {subtree: true, childList: true}); // Also listen for popstate events window.addEventListener('popstate', () => { - setTimeout(loadGiscus, 500); + setTimeout(loadGiscus, 800); }); // Listen for pushstate/replacestate (modern SPA frameworks) @@ -322,26 +778,79 @@ history.pushState = function() { originalPushState.apply(history, arguments); - setTimeout(loadGiscus, 500); + setTimeout(loadGiscus, 800); }; history.replaceState = function() { originalReplaceState.apply(history, arguments); - setTimeout(loadGiscus, 500); + setTimeout(loadGiscus, 800); }; } + // Debug functions for testing notice messages + window.giscusDebug = { + // Force show rate limit notice + showRateLimitNotice: () => { + const container = document.querySelector('.giscus-container'); + if (container) { + showGiscusNotice(container, 'rate_limit'); + } else { + console.warn('No giscus container found. Wait for page to load giscus placeholder first.'); + } + }, + + // Force show network notice + showNetworkNotice: () => { + const container = document.querySelector('.giscus-container'); + if (container) { + showGiscusNotice(container, 'network'); + } else { + console.warn('No giscus container found. Wait for page to load giscus placeholder first.'); + } + }, + + // Reset to placeholder + resetToPlaceholder: () => { + const container = document.querySelector('.giscus-container'); + if (container) { + container.innerHTML = ''; + const placeholder = createGiscusPlaceholder(); + container.appendChild(placeholder); + setupLazyLoading(container); + giscusLoaded = false; + } + }, + + // Test URL generation + testUrls: () => { + const searchUrl = generateDiscussionUrl(); + const newUrl = generateNewDiscussionUrl(); + console.log('Current page path:', window.location.pathname); + console.log('Search URL:', searchUrl); + console.log('New Discussion URL:', newUrl); + return { searchUrl, newUrl }; + } + }; + // Initialize function init() { addStyles(); setupSPAHandling(); + setupGiscusErrorListener(); + + // Add debug info + console.log('Giscus Debug Commands Available:'); + console.log('- giscusDebug.showRateLimitNotice() - Show rate limit notice'); + console.log('- giscusDebug.showNetworkNotice() - Show network notice'); + console.log('- giscusDebug.resetToPlaceholder() - Reset to placeholder'); + console.log('- giscusDebug.testUrls() - Test URL generation for current page'); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { - setTimeout(loadGiscus, 1000); + setTimeout(loadGiscus, 1500); // Longer delay on initial load }); } else { - setTimeout(loadGiscus, 1000); + setTimeout(loadGiscus, 1500); // Longer delay on initial load } }