`;
document.body.insertAdjacentHTML('beforeend', sidebarHTML);
}
initElements() {
this.domElements = {
sidebar: document.getElementById('linuxdo-enhancer-sidebar'),
toggleButton: document.getElementById('linuxdo-enhancer-toggle-button'),
postList: document.getElementById('linuxdo-enhancer-posts'),
aiAnalysisDiv: document.getElementById('linuxdo-enhancer-ai-analysis'),
// Removed search input: searchInput: document.getElementById('linuxdo-enhancer-search'),
postCount: document.getElementById('linuxdo-enhancer-post-count'),
aiStatus: document.getElementById('linuxdo-enhancer-ai-status'),
updateTime: document.getElementById('linuxdo-enhancer-update-time'),
settingsButton: document.getElementById('linuxdo-enhancer-settings-button'),
pauseScrollButton: document.getElementById('linuxdo-enhancer-pause-scroll'),
configModal: document.getElementById('linuxdo-enhancer-config-modal'),
apiKeyInput: document.getElementById('linuxdo-enhancer-gemini-api-key-input'),
saveConfigButton: document.getElementById('linuxdo-enhancer-save-config-button'),
apiUrlSelect: document.getElementById('linuxdo-enhancer-api-url-select'), // Get select element
fetchIntervalInput: document.getElementById('linuxdo-enhancer-fetch-interval-input'), // Get new input
analyzeIntervalInput: document.getElementById('linuxdo-enhancer-analyze-interval-input') // Get new input
};
// Check if all critical elements were found
const criticalElements = [
'sidebar', 'toggleButton', 'postList', 'aiAnalysisDiv',
'postCount', 'aiStatus', 'updateTime',
'settingsButton', 'pauseScrollButton', 'configModal',
'apiKeyInput', 'saveConfigButton', 'apiUrlSelect',
'fetchIntervalInput', 'analyzeIntervalInput' // Added new elements
];
for (const key of criticalElements) {
if (!this.domElements[key]) {
console.error(`LinuxDoEnhancer Error: Critical DOM element "${key}" not found! Script may not function correctly.`);
// Potentially display a persistent error message on the page if sidebar exists
if(this.domElements.sidebar) {
this.domElements.sidebar.innerHTML = `
脚本初始化失败:找不到关键UI元素 (${key})。请尝试刷新页面或检查脚本是否正确安装。
`;
} else {
alert(`LinuxDoEnhancer Error: Critical DOM element "${key}" not found! Script initialization failed.`);
}
// Stop initialization process
throw new Error(`Missing critical DOM element: ${key}`);
}
}
// Initial state for pause button text/icon
this.domElements.pauseScrollButton.innerHTML = ' 暂停'; // Pause icon
}
setupEventListeners() {
this.domElements.toggleButton.addEventListener('click', () => this.toggleSidebar());
this.domElements.pauseScrollButton.addEventListener('click', () => this.toggleScroll());
this.domElements.settingsButton.addEventListener('click', () => this.showConfigModal());
// Enable save button if API key and intervals are valid
const checkSaveButtonState = () => {
const key = this.domElements.apiKeyInput.value.trim();
const fetchInterval = parseInt(this.domElements.fetchIntervalInput.value, 10);
const analyzeInterval = parseInt(this.domElements.analyzeIntervalInput.value, 10);
this.domElements.saveConfigButton.disabled = !(key && fetchInterval >= 10 && analyzeInterval >= 60);
};
this.domElements.apiKeyInput.addEventListener('input', checkSaveButtonState);
this.domElements.fetchIntervalInput.addEventListener('input', checkSaveButtonState);
this.domElements.analyzeIntervalInput.addEventListener('input', checkSaveButtonState);
this.domElements.saveConfigButton.addEventListener('click', () => this.handleSaveConfig());
// Pause/resume scrolling on post list hover
this.domElements.postList.addEventListener('mouseenter', () => this.stopScrolling());
this.domElements.postList.addEventListener('mouseleave', () => {
if (!this.state.isScrollingPaused) {
this.startScrolling();
}
});
}
loadConfigAndStart() {
// Load API Key
const savedApiKey = GM_getValue(this.config.API_KEY_STORAGE_KEY);
// Load API URL preference
const savedApiUrl = GM_getValue(this.config.API_URL_STORAGE_KEY);
if (savedApiUrl) {
this.config.GEMINI_API_BASE_URL = savedApiUrl;
} // Default is already set in constructor
// Load intervals preference
const savedFetchInterval = GM_getValue(this.config.FETCH_INTERVAL_STORAGE_KEY);
const savedAnalyzeInterval = GM_getValue(this.config.ANALYZE_INTERVAL_STORAGE_KEY);
// Use saved value if valid, otherwise use default
this.config.POST_FETCH_INTERVAL = savedFetchInterval && savedFetchInterval >= 10 * 1000 ? savedFetchInterval : this.config.DEFAULT_POST_FETCH_INTERVAL;
this.config.AI_ANALYZE_INTERVAL = savedAnalyzeInterval && savedAnalyzeInterval >= 60 * 1000 ? savedAnalyzeInterval : this.config.DEFAULT_AI_ANALYZE_INTERVAL;
if (savedApiKey) {
console.log("API key found. Starting fetch and analysis with loaded config.");
this.startFetchingAndAnalyzing();
} else {
console.log("API key not found. Showing config modal.");
this.showConfigModal();
}
}
toggleSidebar() {
this.state.isSidebarVisible = !this.state.isSidebarVisible;
this.domElements.sidebar.classList.toggle('hidden', !this.state.isSidebarVisible);
document.body.classList.toggle('linuxdo-enhancer-sidebar-hidden', !this.state.isSidebarVisible);
// Adjust header position immediately on toggle
const header = document.querySelector('.d-header-wrap');
if (header) {
if (this.state.isSidebarVisible) {
header.style.left = this.config.SIDEBAR_WIDTH;
header.style.width = `calc(100% - ${this.config.SIDEBAR_WIDTH})`;
// When showing, restart intervals if not paused
if (!this.state.isScrollingPaused) {
this.startFetchingAndAnalyzing();
} else {
// If paused, only start scrolling
this.startScrolling();
}
} else {
header.style.left = '0';
header.style.width = '100%';
// When hiding, stop all intervals
this.stopFetchingAndAnalyzing();
}
}
}
toggleScroll() {
this.state.isScrollingPaused = !this.state.isScrollingPaused;
this.domElements.pauseScrollButton.classList.toggle('active', this.state.isScrollingPaused);
if (this.state.isScrollingPaused) {
this.domElements.pauseScrollButton.innerHTML = ' 继续'; // Play icon (reused pause icon path)
this.stopScrolling();
} else {
this.domElements.pauseScrollButton.innerHTML = ' 暂停'; // Pause icon
this.startScrolling();
}
}
startScrolling() {
// Only start scrolling if sidebar is visible and scrolling is not paused
if (this.state.scrollIntervalId || this.state.isScrollingPaused || !this.state.isSidebarVisible) return;
this.state.scrollIntervalId = setInterval(() => {
const list = this.domElements.postList;
if (!list || list.scrollHeight <= list.clientHeight) { // Defensive check & stop if no overflow
this.stopScrolling();
return;
}
const { scrollTop, scrollHeight, clientHeight } = list;
// If we are at the bottom or near bottom, jump back to top
if (scrollTop + clientHeight >= scrollHeight - this.config.SCROLL_AMOUNT) {
// Small delay before jumping to top
setTimeout(() => { list.scrollTop = 0; }, 500);
} else {
list.scrollTop += this.config.SCROLL_AMOUNT;
}
}, this.config.SCROLL_INTERVAL);
}
stopScrolling() {
if (this.state.scrollIntervalId) {
clearInterval(this.state.scrollIntervalId);
this.state.scrollIntervalId = null;
}
}
startFetchingAndAnalyzing() {
// Only start if sidebar is visible
if (!this.state.isSidebarVisible) {
console.log("Sidebar is hidden, skipping start of intervals.");
return;
}
// Clear existing intervals before starting new ones
this.stopFetchingAndAnalyzing();
// Initial fetch
this.fetchPosts().then(() => {
// Start initial analysis a bit after fetch completes and posts are rendered
setTimeout(() => {
if (this.state.rawPostData.length > 0) {
this.analyzePostsWithAI();
} else {
console.log("Initial fetch yielded no posts. Skipping first analysis.");
if(this.domElements.aiAnalysisDiv) {
this.domElements.aiAnalysisDiv.innerHTML = '
未能加载帖子,等待下一次尝试...
';
}
if(this.domElements.aiStatus) {
this.domElements.aiStatus.textContent = '等待';
}
}
}, 2000); // Wait 2 seconds after fetch attempts
});
// Set up intervals using configured frequencies
this.intervals.fetchInterval = setInterval(() => this.fetchPosts(), this.config.POST_FETCH_INTERVAL);
this.intervals.analyzeInterval = setInterval(() => {
if (this.state.rawPostData.length > 0) {
this.analyzePostsWithAI();
} else {
console.log("No posts available for analysis this cycle.");
if(this.domElements.aiAnalysisDiv) {
this.domElements.aiAnalysisDiv.innerHTML = '
等待加载最新帖子进行分析...
';
}
if(this.domElements.aiStatus) {
this.domElements.aiStatus.textContent = '等待';
}
}
}, this.config.AI_ANALYZE_INTERVAL);
console.log(`Enhancer started: fetching every ${this.config.POST_FETCH_INTERVAL/1000}s, analyzing every ${this.config.AI_ANALYZE_INTERVAL/1000}s.`);
// Adjust header position on start (redundant if called by toggle, but safe)
const header = document.querySelector('.d-header-wrap');
if (header) {
header.style.left = this.config.SIDEBAR_WIDTH;
header.style.width = `calc(100% - ${this.config.SIDEBAR_WIDTH})`;
}
// Ensure scrolling starts if not paused
this.startScrolling();
}
stopFetchingAndAnalyzing() {
if (this.intervals.fetchInterval) {
clearInterval(this.intervals.fetchInterval);
this.intervals.fetchInterval = null;
}
if (this.intervals.analyzeInterval) {
clearInterval(this.intervals.analyzeInterval);
this.intervals.analyzeInterval = null;
}
// Stop scrolling when intervals are stopped (except if explicitly paused?)
// Decided to let toggleScroll handle scroll pausing state separately
// this.stopScrolling(); // Do not stop scrolling here, let toggle handle it
console.log("Enhancer intervals cleared.");
// Reset header position (redundant if called by toggle, but safe)
const header = document.querySelector('.d-header-wrap');
if (header && !this.state.isSidebarVisible) { // Only reset if sidebar is actually hidden
header.style.left = '0';
header.style.width = '100%';
}
}
async fetchPosts() {
if (this.state.isFetchingPosts) {
console.log("Fetch in progress, skipping.");
return;
}
// Only fetch if sidebar is visible
if (!this.state.isSidebarVisible) {
console.log("Sidebar is hidden, skipping fetch.");
return;
}
this.state.isFetchingPosts = true;
console.log('Fetching posts...');
const postListElement = this.domElements.postList;
const updateTimeElement = this.domElements.updateTime;
try {
// Only show full loading state if list is currently empty or has error
if (this.state.rawPostData.length === 0 || (postListElement && postListElement.querySelector('.linuxdo-enhancer-error'))) {
this.showLoading(postListElement);
} else {
console.log("Fetching new posts...");
}
if(this.domElements.aiStatus) this.domElements.aiStatus.textContent = '更新中';
const response = await fetch(this.config.POSTS_URL);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText.substring(0, 100)}...`);
}
const data = await response.json();
// Check if data format is as expected for latest_posts
if (Array.isArray(data.latest_posts)) {
const newPosts = data.latest_posts.map(post => ({
id: post.id, // Post ID
topic_id: post.topic_id,
topic_slug: post.topic_slug,
topic_title: post.topic_title,
fancy_title: post.topic_html_title || post.topic_title, // Use HTML title which might have emojis
post_number: post.post_number, // Floor number
created_at: post.created_at, // UTC time
updated_at: post.updated_at, // UTC time
views: post.views, // Views for the TOPIC
posts_count: post.posts_count, // Total posts in the TOPIC
author: post.username || '未知', // Use username directly from JSON
display_author_name: post.display_username, // Store display_username separately
// --- Simplified Avatar Path ---
author_avatar_template: post.avatar_template, // Store the template directly
// --- End Simplified Avatar Path ---
user_title: post.user_title, // User's custom title
link: `https://linux.do/t/${post.topic_slug}/${post.topic_id}${post.post_number ? `/${post.post_number}` : ''}`, // Link directly to the post, handle first post edge case
raw: post.raw, // Raw markdown
cooked: post.cooked, // Cooked HTML (can contain images, links etc.)
excerpt: post.excerpt, // Short text excerpt
post_likes: post.reaction_users_count || 0 // Likes for THIS POST - Use this for per-post likes
}));
// Simple check if posts have changed significantly (first few IDs)
const currentTopIds = this.state.rawPostData.slice(0, 5).map(p => p.id).join(',');
const newTopIds = newPosts.slice(0, 5).map(p => p.id).join(',');
if (!this.state.rawPostData.length || newTopIds !== currentTopIds) {
console.log('New posts received. Updating display.');
this.state.rawPostData = newPosts;
// No filtering needed after removing search
this.state.filteredPostData = [...this.state.rawPostData];
this.renderPosts();
} else {
console.log('No significant change in top posts.');
// Ensure filteredData is still a copy of rawData
this.state.filteredPostData = [...this.state.rawPostData];
// Re-render is needed even if no new posts arrived if there are existing posts
if(this.state.rawPostData.length > 0) {
this.renderPosts(); // Render existing data
} else {
// If no raw data, ensure empty state is rendered
this.renderPosts();
}
}
this.updatePostCount();
this.updateLastUpdateTime();
} else {
console.error('Unexpected data format from posts.json:', data);
// Show error only if we couldn't process the data
if(postListElement) this.showError(postListElement, '获取的帖子数据格式异常!');
}
} catch (error) {
console.error('Error fetching posts:', error);
// Show error only if fetch failed and no previous data exists
if (this.state.rawPostData.length === 0) {
if(postListElement) this.showError(postListElement, `加载帖子失败: ${error.message}`);
} else {
// If we have old data, maybe just log the error subtly and indicate connection issue
console.warn("Failed to fetch new posts, displaying old data.", error);
if(updateTimeElement) {
updateTimeElement.textContent = `最后更新: ${this.state.lastUpdateTime ? this.state.lastUpdateTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false }) : '--'} (连接失败)`;
}
}
} finally {
this.state.isFetchingPosts = false;
}
}
renderPosts() {
// We now render directly from rawPostData as there's no filtering
const postsToDisplay = this.state.rawPostData.slice(0, this.config.MAX_DISPLAY_POSTS);
const postListElement = this.domElements.postList;
if (!postListElement) { // Defensive check
console.error("Attempted to render posts, but postList element is null.");
this.stopScrolling(); // Stop scrolling if element is gone
return;
}
postListElement.innerHTML = ''; // Clear current list
if (postsToDisplay.length === 0) {
postListElement.innerHTML = `
暂无帖子。
`;
return;
}
postsToDisplay.forEach(post => {
const postElement = document.createElement('div');
postElement.classList.add('linuxdo-enhancer-post-item');
const postDate = new Date(post.created_at);
// Convert UTC to Beijing Time (UTC+8)
const options = {
// year: 'numeric', // Removed year for brevity
month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', // second: '2-digit', // Removed seconds for brevity
timeZone: 'Asia/Shanghai', // Beijing Time
hour12: false // Use 24-hour format
};
const formattedDate = postDate.toLocaleString('zh-CN', options);
// Use display_author_name if available, fallback to username
const displayUsername = post.display_author_name && post.display_author_name.trim() !== '' ? post.display_author_name : post.author;
// --- Simplified Avatar Path Construction ---
// Directly use the template if available, replace size
const avatarUrl = post.author_avatar_template ? `https://linux.do${post.author_avatar_template.replace('{size}', '40')}` : null;
// Fallback avatar if template is null or invalid (Discourse letter avatars are predictable)
// Use the post.author for fallback avatar
const defaultAvatarUrl = `https://linux.do/letter_avatar/u/${post.author.substring(0,1).toLowerCase()}/40/1.png`;
postElement.innerHTML = `
`;
postListElement.appendChild(postElement);
});
// Reset scroll position only if not filtering (search removed) and not paused
// Also, only reset if there are posts to scroll
if (!this.state.isScrollingPaused && postsToDisplay.length > 0) {
postListElement.scrollTop = 0;
}
}
updatePostCount() {
if (this.domElements.postCount) {
// Count from rawPostData since filtering is removed
this.domElements.postCount.textContent = this.state.rawPostData.length;
}
}
updateLastUpdateTime() {
const updateTimeElement = this.domElements.updateTime;
if (!updateTimeElement) return;
const now = new Date();
// Convert to Beijing Time (UTC+8) for display
const options = {
hour: '2-digit', minute: '2-digit', // Removed seconds for brevity
timeZone: 'Asia/Shanghai',
hour12: false // Use 24-hour format
// day and month will be part of the main date if we add it back
};
// For "最后更新", maybe just time is sufficient
const timeString = now.toLocaleTimeString('zh-CN', options);
// For date, keep month-day
const dateOptions = {
month: '2-digit', day: '2-digit',
timeZone: 'Asia/Shanghai',
}
const dateString = now.toLocaleDateString('zh-CN', dateOptions).replace(/\//g, '-'); // Format like MM-DD
updateTimeElement.textContent = `最后更新: ${dateString} ${timeString}`;
this.state.lastUpdateTime = now; // Store JS Date object
}
async analyzePostsWithAI() {
// Check if AI Analysis section or status badge exist
const aiAnalysisDiv = this.domElements.aiAnalysisDiv;
const aiStatus = this.domElements.aiStatus;
// Only analyze if sidebar is visible and there's data
if (!this.state.isSidebarVisible || this.state.isAnalyzing || !this.state.rawPostData.length) {
console.log("Sidebar hidden, analysis in progress, or no data, skipping analysis.");
if (aiStatus) aiStatus.textContent = this.state.isAnalyzing ? '分析中' : (this.state.rawPostData.length > 0 ? aiStatus.textContent : '等待');
return;
}
this.state.isAnalyzing = true;
console.log('Starting AI analysis...');
const apiKey = GM_getValue(this.config.API_KEY_STORAGE_KEY);
if (!apiKey) {
if (aiAnalysisDiv) this.showError(aiAnalysisDiv, 'Gemini API 密钥未配置');
if (aiStatus) aiStatus.textContent = '密钥缺失';
this.state.isAnalyzing = false;
return;
}
try {
if (aiAnalysisDiv) {
this.showLoading(aiAnalysisDiv);
}
if (aiStatus) aiStatus.textContent = '分析中';
// Use recent posts for analysis, ensure data exists
const postsForPrompt = this.state.rawPostData.slice(0, this.config.AI_ANALYSIS_POST_COUNT);
if (postsForPrompt.length === 0) {
console.log("No recent posts to send to AI.");
if (aiAnalysisDiv) aiAnalysisDiv.innerHTML = '
暂无帖子可供分析。
';
if (aiStatus) aiStatus.textContent = '无数据';
this.state.isAnalyzing = false;
return;
}
// Use the 'author' field which now consistently holds the username from JSON
const postSummaryText = postsForPrompt.map(post =>
`标题: ${post.fancy_title || post.topic_title}\n作者: ${post.author}${post.display_author_name && post.display_author_name !== post.author ? ` (${post.display_author_name})` : ''}${post.user_title ? ` [${post.user_title}]` : ''}\n回复数(话题): ${post.posts_count - 1}\n浏览量(话题): ${post.views}\n点赞数(此贴): ${post.post_likes}\n链接: ${post.link}\n`
).join('\n---\n'); // Join with separator
// Refined prompt for better structure and parsing
const prompt = `你是一个经验丰富的论坛内容分析助手,请基于以下最新的论坛帖子列表,严格按照指定格式输出分析结果。优先分析帖子标题和元信息,如果excerpt或cooked内容简洁明确也可以参考。
帖子列表数据(每项由---分隔):
${postSummaryText}
请根据以上数据,完成以下任务,并严格按照以下格式输出,每个部分前加上对应的中文标题和冒号,并在每个部分结束后换行,不包含额外的解释或Markdown格式(除了推荐阅读的链接格式 [标题](链接),标题使用帖子原标题):
总结: 简要总结这些帖子主要讨论了哪些话题,提炼核心内容。
真假分析: 基于帖子标题和你的知识,分析其中某些话题(如果涉及声明、优惠、传闻等)的可能性或真实性。请谨慎,如果无法确定,请说明这仅仅是基于有限信息进行的推测。
推荐阅读: 从列表中挑选1-2个你认为最有趣或最有价值的帖子(基于标题、回复、浏览量、点赞等信息)推荐给读者,并说明理由。请务必使用 Markdown 链接格式 [标题](链接),标题使用帖子原标题。
辣评: 用一句话或简短的一段话,对这些帖子或论坛的整体氛围进行一个辛辣、幽默或直接的点评。
`;
const GEMINI_API_URL = `${this.config.GEMINI_API_BASE_URL}${this.config.GEMINI_MODEL}:generateContent?key=${apiKey}`;
// Use GM_xmlhttpRequest for cross-origin API call
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: GEMINI_API_URL,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
temperature: 0.7, // Adjust creativity
maxOutputTokens: 1000, // Limit response length
}
}),
timeout: 60000, // 60 seconds timeout
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
resolve({ status: response.status, response: JSON.parse(response.responseText) });
} catch (e) {
reject(new Error(`Failed to parse API response JSON: ${e.message}`));
}
} else {
// Provide more details in the error message
let errorDetails = `Status: ${response.status}`;
if (response.statusText) errorDetails += ` ${response.statusText}`;
if (response.responseText) {
try {
const errorJson = JSON.parse(response.responseText);
if (errorJson.error && errorJson.error.message) {
errorDetails += ` - ${errorJson.error.message}`;
// Check for specific "suspended" message
if (errorJson.error.message.includes("suspended") || errorJson.error.message.includes("denied")) {
errorDetails += "\n您的API密钥可能无效或已被暂停使用,请检查。";
}
} else {
errorDetails += `\nBody: ${response.responseText.substring(0, 150)}...`;
}
} catch (_) {
errorDetails += `\nBody: ${response.responseText.substring(0, 150)}...`;
}
}
reject(new Error(`API request failed: ${errorDetails}`));
}
},
onerror: function(error) {
// The error object from GM_xmlhttpRequest might have different properties
const errorMsg = error.statusText || error.responseText || error.message || 'Unknown error';
reject(new Error(`GM_xmlhttpRequest error: ${errorMsg}`));
},
ontimeout: function() {
reject(new Error('API request timed out.'));
}
});
});
const result = response.response;
console.log('Gemini API result:', result);
if (result.error) {
console.error('Gemini API returned error in body:', result.error);
let displayError = result.error.message || '未知错误';
if (displayError.includes("suspended") || displayError.includes("denied")) {
displayError += "\n您的API密钥可能无效或已被暂停使用,请检查。";
}
if (aiAnalysisDiv) this.showError(aiAnalysisDiv, `AI返回错误: ${displayError}`);
if (aiStatus) aiStatus.textContent = 'API错误';
} else {
const text = result.candidates?.[0]?.content?.parts?.[0]?.text;
if (text) {
console.log('Gemini Raw Output:', text);
this.displayAIAnalysis(text);
if (aiStatus) aiStatus.textContent = '已更新';
} else {
console.warn('Gemini API returned no text content or unexpected structure.', result);
if (aiAnalysisDiv) this.showError(aiAnalysisDiv, 'AI 生成内容为空或格式异常。');
if (aiStatus) aiStatus.textContent = '生成失败';
}
}
} catch (error) {
console.error('AI analysis error:', error);
const displayError = error.message.length > 200 ? error.message.substring(0, 200) + '...' : error.message;
if (aiAnalysisDiv) this.showError(aiAnalysisDiv, `AI分析失败: ${displayError}`);
if (aiStatus) aiStatus.textContent = '错误';
} finally {
this.state.isAnalyzing = false;
}
}
displayAIAnalysis(rawText) {
const aiAnalysisDiv = this.domElements.aiAnalysisDiv;
if (!aiAnalysisDiv) return; // Defensive check
// Function to robustly extract content between sections
const getSectionContent = (text, sectionTitle) => {
// Escape regex special characters in the title
const escapedTitle = sectionTitle.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
// Regex: look for the title followed by :, capture content until the next known title or end of string
// Using lookahead for the next section title or end of string ($)
// Adding \s* after the colon to handle optional whitespace
const regex = new RegExp(`${escapedTitle}:\\s*([\\s\\S]*?)(?=\\n(?:总结|真假分析|推荐阅读|辣评):|$)$`, 'm'); // 'm' flag for multiline ^ $
const match = text.match(regex);
return match?.[1]?.trim() ?? `未能解析${sectionTitle}内容。`; // Provide fallback if not found
};
// Extract content for each section
const summary = getSectionContent(rawText, '总结');
const truthAnalysis = getSectionContent(rawText, '真假分析');
const recommendations = getSectionContent(rawText, '推荐阅读');
const spicyComment = getSectionContent(rawText, '辣评');
// Clear previous content and add structure back
aiAnalysisDiv.innerHTML = `
总结
${this.marked.parse(summary)}
真假分析
${this.marked.parse(truthAnalysis)}
推荐阅读
${this.marked.parse(recommendations)}
辣评
${this.marked.parse(spicyComment)}
`;
// Log if parsing failed for debugging
if (summary.includes('未能解析') || truthAnalysis.includes('未能解析') || recommendations.includes('未能解析') || spicyComment.includes('未能解析')) {
console.warn("Partial or failed parsing of Gemini output. Raw text:", rawText);
}
}
showLoading(element) {
if (!element) return; // Defensive check
element.innerHTML = `