Discourse AI

Elon Musk recently open-sourced the Twitter recommendation algorithm. Can this be implemented in this plugin?

Below is a Tampermonkey script written by a forum user to implement the Twitter recommendation algorithm. Theoretically, it can be used on any Discourse forum:

// ==UserScript==
// @name         *** Algorithm
// @namespace    http://tampermonkey.net/
// @version      0.9
// @description  Intelligent recommendation system integrating the X algorithm and DeepSeek AI, providing personalized topic recommendations
// @author       ***
// @match        ****
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @connect      2c2ch1u11-share-api-0.hf.space
// ==/UserScript==

(function() {
    'use strict';

    const API_KEY = '***';
    const API_URL = '***/v1/chat/completions';
    const MODEL = 'deepseek-chat';

    // ========== Algorithm Configuration Parameters (Adjustable for Optimization) ==========
    const CONFIG = {
        // X Algorithm Weight Parameters
        WEIGHT_LIKES: 0.5,
        WEIGHT_REPLIES: 13.5,
        WEIGHT_VIEWS: 0.015,
        PINNED_BOOST: 2.0,
        TIME_DECAY_FACTOR: 1.5,
        TIME_DECAY_OFFSET: 2,

        // Maximum Score Parameters
        X_SCORE_MAX: 45,
        AI_SCORE_MAX: 55,

        // Score Threshold Parameters
        MAX_SCORE: 100,
        MIN_DISPLAY_SCORE: 20,

        // Cache Parameters
        PROFILE_CACHE_TTL: 86400000,      // User profile cache: 24 hours
        TOPIC_SCORE_CACHE_TTL: 3600000,   // Single topic AI score cache: 1 hour

        // API Parameters
        MAX_TOPICS_PER_BATCH: 30,
        MAX_LIKED_TITLES: 15,       // Number of liked topics
        MAX_REPLIED_TITLES: 10,     // Number of replied topics
        MAX_CREATED_TITLES: 5,      // Number of created topics
        API_RETRY_COUNT: 2,
        API_RETRY_DELAY: 1000,

        // User Profile Analysis Dimensions
        PROFILE_CATEGORIES: ['Technical Interests', 'Content Preferences', 'Interaction Habits', 'Professional Fields', 'Reading Depth'],
    };

    let isRecommendMode = false;
    let scoreMap = {};
    let allTopicsData = {};
    let sortTimeout = null;
    let userProfile = "";
    let isLoading = false;
    let isProcessingNewTopics = false;

    // ========== CSS Styles ==========
    GM_addStyle(`
        .nav-pills > li > a,
        .nav-pills > li.ember-view > a,
        .navigation-container .nav-pills > li > a {
            border-bottom: 3px solid transparent !important;
            transition: all 0.2s ease;
        }

        body.recommend-mode-active .nav-pills > li:not(#nav-item-recommend) > a,
        body.recommend-mode-active .nav-pills > li.ember-view:not(#nav-item-recommend) > a,
        body.recommend-mode-active .navigation-container .nav-pills > li:not(#nav-item-recommend) > a {
            color: var(--primary-medium) !important;
            border-bottom-color: transparent !important;
        }

        .nav-pills li#nav-item-recommend.active > a,
        body.recommend-mode-active .nav-pills li#nav-item-recommend > a {
            color: var(--tertiary) !important;
            border-bottom: 3px solid var(--tertiary) !important;
            font-weight: 600;
        }

        .nav-pills li#nav-item-recommend > a:hover {
            color: var(--tertiary) !important;
            opacity: 0.8;
        }

        .recommend-loading-container {
            width: 100%;
            padding: 60px 20px;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            min-height: 300px;
        }

        .recommend-loading-text {
            color: var(--primary-medium);
            font-size: 15px;
            margin-bottom: 24px;
            text-align: center;
        }

        .recommend-spinner {
            width: 36px;
            height: 36px;
            border: 3px solid var(--primary-low);
            border-top: 3px solid var(--primary-medium);
            border-radius: 50%;
            animation: spin 0.8s linear infinite;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .manus-score-badge {
            font-size: 0.75em;
            color: var(--tertiary);
            margin-left: 8px;
            padding: 2px 6px;
            background: var(--tertiary-very-low);
            border-radius: 4px;
            font-weight: 500;
            transition: all 0.3s ease;
        }

        .manus-score-badge.calculating {
            color: var(--primary-medium);
            background: var(--primary-very-low);
            animation: pulse 1.5s ease-in-out infinite;
        }

        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.5; }
        }

        .manus-score-loading {
            font-size: 0.75em;
            color: var(--primary-medium);
            margin-left: 8px;
        }

        .recommend-full-overlay {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: var(--secondary);
            z-index: 100;
            min-height: 500px;
        }

        .recommend-loading-row {
            position: relative;
            z-index: 101;
        }
    `);

    // ========== Topic AI Score Cache Functions ==========
    function getTopicScoreCache() {
        return GM_getValue('topic_ai_scores_cache', {});
    }

    function setTopicScoreCache(topicId, aiScore) {
        const cache = getTopicScoreCache();
        cache[topicId] = { score: aiScore, time: Date.now() };
        GM_setValue('topic_ai_scores_cache', cache);
    }

    function getCachedTopicScore(topicId) {
        const cache = getTopicScoreCache();
        const entry = cache[topicId];
        if (entry && (Date.now() - entry.time < CONFIG.TOPIC_SCORE_CACHE_TTL)) {
            return entry.score;
        }
        return null;
    }

    function cleanExpiredCache() {
        const cache = getTopicScoreCache();
        const now = Date.now();
        let cleaned = false;
        for (const [id, entry] of Object.entries(cache)) {
            if (now - entry.time > CONFIG.TOPIC_SCORE_CACHE_TTL) {
                delete cache[id];
                cleaned = true;
            }
        }
        if (cleaned) {
            GM_setValue('topic_ai_scores_cache', cache);
        }
    }

    // ========== Inject Recommendation Tab ==========
    function injectRecommendTab() {
        if (document.querySelector('#nav-item-recommend')) return;
        const navPills = document.querySelector('.nav-pills');
        if (!navPills) return;

        const recommendTab = document.createElement('li');
        recommendTab.id = 'nav-item-recommend';
        recommendTab.className = 'ember-view nav-item_recommend';
        recommendTab.innerHTML = `<a href="javascript:void(0)">Recommend</a>`;

        const latestTab = navPills.querySelector('.nav-item_latest') ||
                          navPills.querySelector('[data-filter-type="latest"]')?.parentElement ||
                          navPills.firstChild;
        navPills.insertBefore(recommendTab, latestTab);

        recommendTab.addEventListener('click', async (e) => {
            e.preventDefault();
            e.stopPropagation();

            if (isLoading) return;

            isRecommendMode = true;
            document.body.classList.add('recommend-mode-active');

            navPills.querySelectorAll('li').forEach(li => {
                li.classList.remove('active');
                li.querySelector('a')?.classList.remove('active');
            });
            recommendTab.classList.add('active');

            cleanExpiredCache();

            showLoading(true);
            await startRecommendation();
            showLoading(false);

            sortAndDisplayRows();
        });

        navPills.querySelectorAll('li:not(#nav-item-recommend)').forEach(tab => {
            tab.addEventListener('click', () => {
                isRecommendMode = false;
                document.body.classList.remove('recommend-mode-active');
                recommendTab.classList.remove('active');
                document.querySelectorAll('.manus-score-badge').forEach(b => b.remove());
                document.querySelectorAll('.manus-score-loading').forEach(b => b.remove());
            });
        });

        window.addEventListener('popstate', () => {
            if (isRecommendMode) {
                isRecommendMode = false;
                document.body.classList.remove('recommend-mode-active');
                recommendTab.classList.remove('active');
            }
        });
    }

    // ========== Loading Indicator ==========
    let originalTableContent = null;

    function showLoading(show, text = 'Integrating X Algorithm and DeepSeek AI recommendations...') {
        isLoading = show;

        const topicListContainer = document.querySelector('.topic-list-container') ||
                                   document.querySelector('.topic-list')?.parentElement ||
                                   document.querySelector('.topic-list');
        const tbody = document.querySelector('.topic-list tbody');

        if (!topicListContainer) return;

        if (show) {
            if (!originalTableContent && tbody) {
                originalTableContent = tbody.innerHTML;
            }

            if (tbody) {
                tbody.innerHTML = `
                    <tr class="recommend-loading-row">
                        <td colspan="100%">
                            <div class="recommend-loading-container">
                                <div class="recommend-loading-text">${text}</div>
                                <div class="recommend-spinner"></div>
                            </div>
                        </td>
                    </tr>
                `;
            }

            topicListContainer.style.position = 'relative';

            let overlay = topicListContainer.querySelector('.recommend-full-overlay');
            if (!overlay) {
                overlay = document.createElement('div');
                overlay.className = 'recommend-full-overlay';
                topicListContainer.appendChild(overlay);
            }
        } else {
            topicListContainer.querySelector('.recommend-full-overlay')?.remove();

            if (originalTableContent && tbody) {
                tbody.innerHTML = originalTableContent;
                originalTableContent = null;
            }
        }
    }

    // ========== Helper function to extract titles ==========
    function extractTitles(data, maxCount) {
        if (!data?.user_actions) return '';

        const titles = [...new Set(
            data.user_actions
                .filter(a => a.title)
                .map(a => a.title)
        )].slice(0, maxCount);

        return titles.length > 0 ? titles.join('\n') : '';
    }

    // ========== Optimization: More detailed user profile retrieval ==========
    async function fetchUserProfile(username) {
        if (!username) {
            console.log('[Recommendation] Not logged in, using default profile');
            return "Default";
        }

        const cacheKey = `user_profile_v9_${username}`;
        const cached = GM_getValue(cacheKey);
        if (cached && (Date.now() - cached.time < CONFIG.PROFILE_CACHE_TTL)) {
            console.log(`[Recommendation] User profile (cache):\n${cached.profile}`);
            return cached.profile;
        }

        try {
            // Retrieve various user behavior data
            const [likedData, repliedData, createdData] = await Promise.all([
                fetch(`/user_actions.json?username=${username}&filter=1`).then(r => r.json()).catch(() => null), // Likes
                fetch(`/user_actions.json?username=${username}&filter=5`).then(r => r.json()).catch(() => null), // Replies
                fetch(`/user_actions.json?username=${username}&filter=4`).then(r => r.json()).catch(() => null), // Created
            ]);

            // Extract titles of different types
            const likedTitles = extractTitles(likedData, CONFIG.MAX_LIKED_TITLES);
            const repliedTitles = extractTitles(repliedData, CONFIG.MAX_REPLIED_TITLES);
            const createdTitles = extractTitles(createdData, CONFIG.MAX_CREATED_TITLES);

            if (!likedTitles && !repliedTitles && !createdTitles) return "Default";

            // Build a more detailed analysis prompt
            const prompt = `You are a professional user behavior analyst. Please generate a detailed user profile based on the following user behavior data:

**Topics liked by the user** (best reflects interests):
${likedTitles || 'No data'}

**Topics replied to by the user** (reflects engagement and expertise):
${repliedTitles || 'No data'}

**Topics created by the user** (reflects active focus areas):
${createdTitles || 'No data'}

Please analyze the user profile from the following dimensions, summarizing each dimension in one sentence:
1. **Technical Interests**: Which tech stacks, tools, or platforms the user is interested in
2. **Content Preferences**: Preference for tutorials, discussions, news, or Q&A content
3. **Interaction Habits**: Whether the user is a deep participant or a shallow browser
4. **Professional Field**: Main technical fields or industry directions of focus
5. **Reading Depth**: Preference for beginner, intermediate, or expert-level content

Output format (no numbering, output 5 lines directly):
Technical Interests: [Analysis Result]
Content Preferences: [Analysis Result]
Interaction Habits: [Analysis Result]
Professional Field: [Analysis Result]
Reading Depth: [Analysis Result]`;

            const profile = await callDeepSeek(prompt, false);

            if (profile && profile.length > 0) {
                GM_setValue(cacheKey, { time: Date.now(), profile: profile });
                console.log(`[Recommendation] User profile:\n${profile}`);
                return profile;
            }
            return "Default";
        } catch (e) {
            console.error('[Recommendation] Failed to retrieve user profile:', e);
            return "Default";
        }
    }

    // ========== Start recommendation process ==========
    async function startRecommendation() {
        try {
            const currentUser = getCurrentUserInfo();

            if (!userProfile) {
                userProfile = await fetchUserProfile(currentUser?.username);
            }

            const response = await fetch('/latest.json');
            const data = await response.json();
            const topics = data.topic_list.topics;

            topics.forEach(t => {
                allTopicsData[t.id] = t;
            });

            const uncachedTopics = [];
            const xScoresRaw = {};
            const aiScoresMap = {};

            topics.forEach(topic => {
                xScoresRaw[topic.id] = calculateXScore(topic);

                const cachedScore = getCachedTopicScore(topic.id);
                if (cachedScore !== null) {
                    aiScoresMap[topic.id] = cachedScore;
                } else {
                    uncachedTopics.push(topic);
                }
            });

            if (uncachedTopics.length > 0) {
                await batchScoreTopics(uncachedTopics, aiScoresMap);
            }

            calculateFinalScores(topics, xScoresRaw, aiScoresMap);

        } catch (e) {
            console.error('[Recommendation] Recommendation process failed:', e);
        }
    }

    // ========== Batch score topics ==========
    async function batchScoreTopics(topics, aiScoresMap) {
        for (let i = 0; i < topics.length; i += CONFIG.MAX_TOPICS_PER_BATCH) {
            const batch = topics.slice(i, i + CONFIG.MAX_TOPICS_PER_BATCH);
            const aiScores = await getAIScoresForBatch(batch);

            batch.forEach(topic => {
                const aiScore = aiScores[topic.id] || 5;
                setTopicScoreCache(topic.id, aiScore);
                aiScoresMap[topic.id] = aiScore;
            });
        }
    }

    // ========== Optimization: More precise batch topic scoring ==========
    async function getAIScoresForBatch(topics) {
        const topicList = topics.map((t, idx) =>
            `${idx + 1}. [ID:${t.id}] ${t.title}`
        ).join('\n');

        const prompt = `You are a content recommendation expert. Please score the following topics based on the user profile.

**User Profile**:
${userProfile}

**Topics to Score**:
${topicList}

**Scoring Criteria** (0-10 points):
- **9-10 points**: Highly matches user profile, user is very likely to be interested
- **7-8 points**: Matches user profile well, user is likely to be interested
- **5-6 points**: Partially matches user profile, user might be interested
- **3-4 points**: Weakly related to user profile, user is unlikely to be interested
- **0-2 points**: Unrelated or opposite to user profile, user is not interested

**Scoring Considerations**:
1. Match between topic content and user technical interests
2. Consistency between topic type and user content preferences
3. Adaptability between topic depth and user reading depth
4. Relevance between topic field and user professional field

Please return in JSON format, with keys as topic IDs (pure numbers) and values as scores (0-10 numbers).
Example: {"123": 8.5, "456": 5.0}

Return only JSON, no other text.`;

        const scores = await callDeepSeek(prompt, true);

        // Clean and validate scores
        const cleanedScores = {};
        for (const [id, score] of Object.entries(scores)) {
            const numId = parseInt(id, 10);
            const numScore = parseFloat(score);
            if (!isNaN(numId) && !isNaN(numScore) && numScore >= 0 && numScore <= 10) {
                cleanedScores[numId] = Math.round(numScore * 10) / 10;
            }
        }

        return cleanedScores;
    }

    // ========== Optimization: More robust final score calculation ==========
    function calculateFinalScores(topics, xScoresRaw, aiScoresMap) {
        const xScores = Object.values(xScoresRaw);
        if (xScores.length === 0) return;

        // Use percentile normalization to avoid extreme value impact
        const sortedX = [...xScores].sort((a, b) => a - b);
        const p5 = sortedX[Math.floor(sortedX.length * 0.05)];  // 5th percentile
        const p95 = sortedX[Math.floor(sortedX.length * 0.95)]; // 95th percentile
        const rangeX = p95 - p5;

        topics.forEach(topic => {
            const id = topic.id;

            let xScoreNormalized;
            if (rangeX === 0) {
                xScoreNormalized = CONFIG.X_SCORE_MAX / 2;
            } else {
                // Clamp within p5 to p95 range
                const clampedX = Math.max(p5, Math.min(p95, xScoresRaw[id]));
                xScoreNormalized = ((clampedX - p5) / rangeX) * CONFIG.X_SCORE_MAX;
            }

            const aiScore = aiScoresMap[id] || 5;
            const aiScoreNormalized = (aiScore / 10) * CONFIG.AI_SCORE_MAX;

            // Final Score: X Algorithm (45%) + AI Score (55%)
            const finalScore = xScoreNormalized + aiScoreNormalized;
            scoreMap[id] = Math.round(finalScore * 10) / 10;
        });
    }

    // ========== Calculate final scores for new topics ==========
    function calculateFinalScoresForNew(topicIds, xScoresRaw, aiScoresMap) {
        const allXScores = Object.values(scoreMap).length > 0
            ? [...Object.values(xScoresRaw), ...Object.keys(allTopicsData).map(id => calculateXScore(allTopicsData[id]))]
            : Object.values(xScoresRaw);

        if (allXScores.length === 0) return;

        const sortedX = [...allXScores].sort((a, b) => a - b);
        const p5 = sortedX[Math.floor(sortedX.length * 0.05)];
        const p95 = sortedX[Math.floor(sortedX.length * 0.95)];
        const rangeX = p95 - p5;

        topicIds.forEach(id => {
            if (xScoresRaw[id] === undefined) return;

            let xScoreNormalized;
            if (rangeX === 0) {
                xScoreNormalized = CONFIG.X_SCORE_MAX / 2;
            } else {
                const clampedX = Math.max(p5, Math.min(p95, xScoresRaw[id]));
                xScoreNormalized = ((clampedX - p5) / rangeX) * CONFIG.X_SCORE_MAX;
            }

            const aiScore = aiScoresMap[id] || 5;
            const aiScoreNormalized = (aiScore / 10) * CONFIG.AI_SCORE_MAX;

            const finalScore = xScoreNormalized + aiScoreNormalized;
            scoreMap[id] = Math.round(finalScore * 10) / 10;
        });
    }

    // ========== Handle newly loaded topics (scroll loading) ==========
    async function processNewTopics(newRows) {
        if (!isRecommendMode || isProcessingNewTopics) return;
        isProcessingNewTopics = true;

        try {
            const newTopicIds = [];
            newRows.forEach(row => {
                const id = row.getAttribute('data-topic-id');
                if (id && !scoreMap[id]) {
                    newTopicIds.push(id);
                }
            });

            if (newTopicIds.length === 0) {
                isProcessingNewTopics = false;
                return;
            }

            newTopicIds.forEach(id => {
                const row = document.querySelector(`tr[data-topic-id="${id}"]`);
                if (row) {
                    addCalculatingBadge(row);
                }
            });

            const uncachedTopics = [];
            const xScoresRaw = {};
            const aiScoresMap = {};

            for (const id of newTopicIds) {
                const topicData = allTopicsData[id] || await fetchTopicData(id);
                if (!topicData) continue;

                allTopicsData[id] = topicData;
                xScoresRaw[id] = calculateXScore(topicData);

                const cachedScore = getCachedTopicScore(id);
                if (cachedScore !== null) {
                    aiScoresMap[id] = cachedScore;
                } else {
                    uncachedTopics.push(topicData);
                }
            }

            if (uncachedTopics.length > 0) {
                await batchScoreTopics(uncachedTopics, aiScoresMap);
            }

            calculateFinalScoresForNew(newTopicIds, xScoresRaw, aiScoresMap);

            updateRowBadges();

        } catch (e) {
            console.error('[Recommendation] Failed to process new topics:', e);
        }

        isProcessingNewTopics = false;
    }

    // ========== Get single topic data ==========
    async function fetchTopicData(topicId) {
        try {
            const row = document.querySelector(`tr[data-topic-id="${topicId}"]`);
            if (row) {
                const title = row.querySelector('.title')?.textContent?.trim() || '';
                const replies = parseInt(row.querySelector('.posts')?.textContent) || 0;
                const views = parseInt(row.querySelector('.views')?.textContent?.replace(/[k,]/gi, '')) || 0;
                const likes = parseInt(row.querySelector('.likes')?.textContent) || 0;

                return {
                    id: topicId,
                    title: title,
                    posts_count: replies,
                    views: views,
                    like_count: likes,
                    created_at: new Date().toISOString(),
                    pinned: row.classList.contains('pinned')
                };
            }
            return null;
        } catch (e) {
            return null;
        }
    }

    // ========== Sort and display ==========
    function sortAndDisplayRows() {
        if (!isRecommendMode) return;
        const tbody = document.querySelector('.topic-list tbody');
        if (!tbody) return;

        const rows = Array.from(tbody.querySelectorAll('tr.topic-list-item'));

        rows.sort((a, b) => {
            const idA = a.getAttribute('data-topic-id');
            const idB = b.getAttribute('data-topic-id');
            const scoreA = scoreMap[idA] || 0;
            const scoreB = scoreMap[idB] || 0;
            return scoreB - scoreA;
        });

        let hiddenCount = 0;
        rows.forEach(row => {
            const id = row.getAttribute('data-topic-id');
            const score = scoreMap[id];

            if (score === undefined) {
                row.style.display = '';
                addCalculatingBadge(row);
            } else if (score < CONFIG.MIN_DISPLAY_SCORE) {
                row.style.display = 'none';
                hiddenCount++;
            } else {
                row.style.display = '';
                addScoreBadge(row);
            }
            tbody.appendChild(row);
        });
    }

    // ========== Update badges for all rows ==========
    function updateRowBadges() {
        if (!isRecommendMode) return;
        const rows = document.querySelectorAll('tr.topic-list-item');
        rows.forEach(row => {
            const id = row.getAttribute('data-topic-id');
            const score = scoreMap[id];

            if (score === undefined) {
                addCalculatingBadge(row);
            } else if (score < CONFIG.MIN_DISPLAY_SCORE) {
                row.style.display = 'none';
            } else {
                row.style.display = '';
                addScoreBadge(row);
            }
        });
    }

    // ========== Add score badge ==========
    function addScoreBadge(row) {
        const id = row.getAttribute('data-topic-id');
        const score = scoreMap[id];

        row.querySelector('.manus-score-loading')?.remove();

        if (score !== undefined) {
            let badge = row.querySelector('.manus-score-badge');
            if (!badge) {
                badge = document.createElement('span');
                badge.className = 'manus-score-badge';
                const titleContainer = row.querySelector('.link-top-line') ||
                                      row.querySelector('.title')?.parentElement ||
                                      row.querySelector('.main-link') ||
                                      row.querySelector('td:first-child');
                if (titleContainer) {
                    titleContainer.appendChild(badge);
                }
            }
            if (badge) {
                badge.textContent = `🔥 ${score.toFixed(1)}`;
                badge.classList.remove('calculating');
            }
        }
    }

    // ========== Add "Calculating" badge ==========
    function addCalculatingBadge(row) {
        const id = row.getAttribute('data-topic-id');

        if (scoreMap[id] !== undefined) return;

        let badge = row.querySelector('.manus-score-badge');
        if (!badge) {
            badge = document.createElement('span');
            badge.className = 'manus-score-badge calculating';
            const titleContainer = row.querySelector('.link-top-line') ||
                                  row.querySelector('.title')?.parentElement ||
                                  row.querySelector('.main-link') ||
                                  row.querySelector('td:first-child');
            if (titleContainer) {
                titleContainer.appendChild(badge);
            }
        }
        if (badge) {
            badge.textContent = '⏳ Calculating...';
            badge.classList.add('calculating');
        }
    }

    // ========== Optimization: Improved JSON cleaning logic ==========
    function cleanJsonResponse(content) {
        if (!content) return content;

        let cleaned = content.trim();

        // Remove Markdown code block markers
        cleaned = cleaned.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '');

        // Remove possible text explanations
        const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
        if (jsonMatch) {
            cleaned = jsonMatch[0];
        }

        return cleaned.trim();
    }

    // ========== API Call ==========
    async function callDeepSeek(prompt, isJson = true, retryCount = 0) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: API_URL,
                headers: {
                    "Content-Type": "application/json",
                    "Authorization": `Bearer ${API_KEY}`
                },
                data: JSON.stringify({
                    model: MODEL,
                    messages: [{ role: "user", content: prompt }],
                    temperature: 0.3,
                    max_tokens: 1000
                }),
                timeout: 30000,
                onload: (res) => {
                    try {
                        if (res.status >= 200 && res.status < 300) {
                            let content = JSON.parse(res.responseText).choices[0].message.content;

                            if (isJson) {
                                content = cleanJsonResponse(content);
                                resolve(JSON.parse(content));
                            } else {
                                resolve(content);
                            }
                        } else {
                            throw new Error(`HTTP ${res.status}`);
                        }
                    } catch(e) {
                        console.error('[Recommendation] API response parsing failed:', e);
                        if (retryCount < CONFIG.API_RETRY_COUNT) {
                            setTimeout(() => {
                                callDeepSeek(prompt, isJson, retryCount + 1).then(resolve);
                            }, CONFIG.API_RETRY_DELAY);
                        } else {
                            resolve(isJson ? {} : "");
                        }
                    }
                },
                onerror: (err) => {
                    console.error('[Recommendation] API call failed:', err);
                    if (retryCount < CONFIG.API_RETRY_COUNT) {
                        setTimeout(() => {
                            callDeepSeek(prompt, isJson, retryCount + 1).then(resolve);
                        }, CONFIG.API_RETRY_DELAY);
                    } else {
                        resolve(isJson ? {} : "");
                    }
                },
                ontimeout: () => {
                    console.error('[Recommendation] API call timed out');
                    if (retryCount < CONFIG.API_RETRY_COUNT) {
                        setTimeout(() => {
                            callDeepSeek(prompt, isJson, retryCount + 1).then(resolve);
                        }, CONFIG.API_RETRY_DELAY);
                    } else {
                        resolve(isJson ? {} : "");
                    }
                }
            });
        });
    }

    // ========== Optimization: Smarter X Algorithm Scoring ==========
    function calculateXScore(topic) {
        const now = new Date();
        const created = new Date(topic.created_at);
        const hoursOld = Math.max(0, (now - created) / (1000 * 60 * 60));

        // Base engagement score
        const likeScore = (topic.like_count || 0) * CONFIG.WEIGHT_LIKES;
        const replyScore = (topic.posts_count || 0) * CONFIG.WEIGHT_REPLIES;
        const viewScore = (topic.views || 0) * CONFIG.WEIGHT_VIEWS;

        // Calculate engagement rate bonus (high engagement rate indicates higher content quality)
        const views = topic.views || 1;
        const engagementRate = ((topic.like_count || 0) + (topic.posts_count || 0)) / views;
        const engagementBoost = 1 + Math.min(engagementRate * 10, 2); // Max 2x bonus

        let score = (likeScore + replyScore + viewScore) * engagementBoost;

        // Pin bonus
        if (topic.pinned) {
            score *= CONFIG.PINNED_BOOST;
        }

        // Time decay (freshness)
        const timeFactor = Math.pow(hoursOld + CONFIG.TIME_DECAY_OFFSET, CONFIG.TIME_DECAY_FACTOR);

        return score / timeFactor;
    }

    // ========== Get current user info ==========
    function getCurrentUserInfo() {
        try {
            // Method 1: Discourse container
            const container = window.Discourse?.__container__;
            if (container) {
                const currentUser = container.lookup('service:current-user');
                if (currentUser?.username) {
                    return {
                        username: currentUser.username,
                        id: currentUser.id,
                        name: currentUser.name,
                        trust_level: currentUser.trust_level
                    };
                }
            }

            // Method 2: User.current()
            if (window.User?.current?.()?.username) {
                const user = window.User.current();
                return { username: user.username, id: user.id };
            }

            // Method 3: #current-user
            const userLink = document.querySelector('#current-user a[data-user-card]');
            if (userLink) {
                return { username: userLink.getAttribute('data-user-card') };
            }

            // Method 4: header
            const headerUser = document.querySelector('.header-dropdown-toggle.current-user button');
            if (headerUser) {
                const img = headerUser.querySelector('img');
                if (img?.alt) {
                    return { username: img.alt };
                }
            }

            // Method 5: d-header
            const anyUserCard = document.querySelector('.d-header [data-user-card]');
            if (anyUserCard) {
                return { username: anyUserCard.getAttribute('data-user-card') };
            }

            // Method 6: preload data
            const preloadData = document.querySelector('#data-preloaded');
            if (preloadData) {
                try {
                    const data = JSON.parse(preloadData.dataset.preloaded || '{}');
                    const currentUser = JSON.parse(data.currentUser || '{}');
                    if (currentUser.username) {
                        return { username: currentUser.username };
                    }
                } catch (e) {}
            }

            return null;
        } catch (e) {
            return null;
        }
    }

    // ========== Listen for DOM changes ==========
    const listObserver = new MutationObserver((mutations) => {
        if (!isRecommendMode || isLoading) return;

        const newRows = [];
        mutations.forEach(m => {
            m.addedNodes.forEach(node => {
                if (node.nodeType === 1) {
                    if (node.classList?.contains('topic-list-item')) {
                        newRows.push(node);
                    } else if (node.querySelectorAll) {
                        node.querySelectorAll('.topic-list-item').forEach(row => newRows.push(row));
                    }
                }
            });
        });

        if (newRows.length > 0) {
            newRows.forEach(row => {
                const id = row.getAttribute('data-topic-id');
                if (id && scoreMap[id] === undefined) {
                    addCalculatingBadge(row);
                } else if (scoreMap[id] !== undefined) {
                    if (scoreMap[id] >= CONFIG.MIN_DISPLAY_SCORE) {
                        addScoreBadge(row);
                    } else {
                        row.style.display = 'none';
                    }
                }
            });

            clearTimeout(sortTimeout);
            sortTimeout = setTimeout(() => processNewTopics(newRows), 500);
        }
    });

    const navObserver = new MutationObserver(() => {
        injectRecommendTab();
        const tbody = document.querySelector('.topic-list tbody');
        if (tbody && !tbody.dataset.observing) {
            tbody.dataset.observing = 'true';
            listObserver.observe(tbody, { childList: true });
        }
    });

    // ========== Start script ==========
    navObserver.observe(document.body, { childList: true, subtree: true });
    injectRecommendTab();

    console.log('[Recommendation System] Loaded - Integrating X Algorithm and DeepSeek AI');

})();

Here is the effect on the Discourse forum:


לייק 1