Jira Activity Module Fullscreen Toggle with AI Analysis

Add fullscreen toggle and AI-powered analysis for Jira activity log module.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         Jira Activity Module Fullscreen Toggle with AI Analysis
// @namespace    http://tampermonkey.net/
// @version      2.0.0
// @description  Add fullscreen toggle and AI-powered analysis for Jira activity log module.
// @description  为 Jira 活动日志模块添加全屏切换和 AI 分析功能。
// @author       Will
// @license      MIT
// @homepage     https://github.com/jwq2011/TamperMonkey-Scripts
// @supportURL   https://github.com/jwq2011/TamperMonkey-Scripts/issues
// @match        *://jira-sh.xxxxauto.com:8080/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @connect      dashscope.aliyuncs.com
// @require      https://update.greasyfork.org/scripts/506699/marked.js
// @compatible   tampermonkey
// @compatible   violentmonkey
// @compatible   greasemonkey
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    if (window.activityFullscreenScriptLoaded) return;
    window.activityFullscreenScriptLoaded = true;

    const DEFAULT_MODELS = ['qwen-plus', 'qwen-max', 'qwen-turbo', 'qwen-flash-2025-07-28', 'qwen-flash', 'qwen-plus-2025-07-14', 'qwen3-30b-a3b-instruct-2507'];
    const DASHSCOPE_API_KEY = GM_getValue('DASHSCOPE_API_KEY', '');
    const MODEL_NAME = GM_getValue('MODEL_NAME', 'qwen-plus');

    GM_addStyle(`
        .activity-fullscreen-btn, .activity-analyze-btn, .activity-config-btn {
            margin-left: 10px;
            padding: 6px 12px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 13px;
            color: white;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .activity-fullscreen-btn { background-color: #0052cc; }
        .activity-fullscreen-btn.exit { background-color: #d04437; }
        .activity-analyze-btn { background-color: #008000; }
        .activity-config-btn { background-color: #ff9900; }

        #exit-fullscreen-btn {
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 10000;
            background-color: #d04437;
            color: white;
            padding: 10px 15px;
            border-radius: 4px;
            cursor: pointer;
            box-shadow: 0 2px 8px rgba(0,0,0,0.3);
        }

        #ai-config-modal {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            z-index: 10001;
            width: 400px;
        }
        #ai-config-modal label {
            display: block;
            margin-top: 10px;
        }
        #ai-config-modal input, #ai-config-modal select {
            width: 100%;
            padding: 6px;
            margin-top: 4px;
        }
        #ai-config-modal button {
            margin-top: 15px;
            padding: 8px 12px;
            background-color: #0052cc;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        #overlay {
            position: fixed;
            top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(0,0,0,0.5);
            z-index: 10000;
        }

        /* AI Result Modal */
        #ai-result-modal {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            z-index: 10002;
            width: 80%;
            max-width: 800px;
            max-height: 80vh;
            overflow: hidden;
            resize: both;
            display: flex;
            flex-direction: column;
        }

        #ai-result-header {
            padding: 10px 15px;
            background: #0052cc;
            color: white;
            cursor: move;
            user-select: none;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        #ai-result-close {
            background: none;
            border: none;
            color: white;
            font-size: 18px;
            cursor: pointer;
        }

        #ai-result-content {
            padding: 15px;
            overflow-y: auto;
            flex-grow: 1;
        }

        #ai-result-actions {
            padding: 10px;
            text-align: right;
            border-top: 1px solid #eee;
        }

        #ai-result-actions button {
            margin-left: 10px;
            padding: 6px 12px;
            background-color: #0052cc;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
    `);

    function createButtons() {
        const activityModule = document.getElementById('activitymodule');
        if (!activityModule) return;

        const heading = activityModule.querySelector('#activitymodule_heading');
        if (!heading) return;

        [...heading.querySelectorAll('.activity-fullscreen-btn, .activity-analyze-btn, .activity-config-btn')].forEach(btn => btn.remove());

        const toggleBtn = document.createElement('button');
        toggleBtn.className = 'activity-fullscreen-btn';
        toggleBtn.textContent = '全屏模式';
        toggleBtn.addEventListener('click', toggleFullscreen);
        heading.appendChild(toggleBtn);

        const analyzeBtn = document.createElement('button');
        analyzeBtn.className = 'activity-analyze-btn';
        analyzeBtn.textContent = 'AI 分析';
        analyzeBtn.addEventListener('click', analyzeContent);
        heading.appendChild(analyzeBtn);

        const configBtn = document.createElement('button');
        configBtn.className = 'activity-config-btn';
        configBtn.textContent = '配置';
        configBtn.addEventListener('click', openConfigModal);
        heading.appendChild(configBtn);
    }

    function toggleFullscreen() {
        const activityModule = document.getElementById('activitymodule');
        const toggleBtn = document.querySelector('.activity-fullscreen-btn');
        const isFullscreen = activityModule.classList.contains('fullscreen-active');

        if (!isFullscreen) {
            activityModule.classList.add('fullscreen-active');
            activityModule.style.cssText = `
                position: fixed !important;
                top: 70px !important;
                left: 0 !important;
                right: 0 !important;
                bottom: 0 !important;
                z-index: 9999 !important;
                width: 100% !important;
                height: calc(100% - 70px) !important;
                margin: 0 !important;
                padding: 20px !important;
                overflow-y: auto !important;
                background-color: #fff !important;
                box-shadow: 0 0 15px rgba(0,0,0,0.2) !important;
                box-sizing: border-box !important;
            `;
            document.querySelector('.aui-sidebar')?.style.setProperty('display', 'none', 'important');

            const content = document.querySelector('.issue-body-content');
            content?.querySelectorAll('.module').forEach(module => {
                if (module.id !== 'activitymodule' && module.id !== 'stalker') {
                    module.style.display = 'none';
                    module.dataset.wasHidden = 'true';
                }
            });

            toggleBtn.textContent = '退出全屏';
            toggleBtn.classList.add('exit');

            const exitBtn = document.createElement('button');
            exitBtn.id = 'exit-fullscreen-btn';
            exitBtn.textContent = '退出全屏';
            exitBtn.addEventListener('click', toggleFullscreen);
            document.body.appendChild(exitBtn);

        } else {
            activityModule.classList.remove('fullscreen-active');
            activityModule.style.cssText = '';
            document.querySelector('.aui-sidebar')?.style.removeProperty('display');

            const content = document.querySelector('.issue-body-content');
            content?.querySelectorAll('.module').forEach(module => {
                if (module.dataset.wasHidden === 'true') {
                    module.style.display = '';
                    delete module.dataset.wasHidden;
                }
            });

            toggleBtn.textContent = '全屏模式';
            toggleBtn.classList.remove('exit');
            document.getElementById('exit-fullscreen-btn')?.remove();
        }
    }

    async function analyzeContent() {
        const contentDiv = document.querySelector('.mod-content');
        if (!contentDiv) {
            showResultModal("未找到内容区域");
            return;
        }

        const contentText = contentDiv.innerText.trim();
        if (!contentText) {
            showResultModal("内容为空");
            return;
        }

        showResultModal("正在分析中,请稍候...");

        const prompt = `
你是一名资深项目经理兼技术分析师,请仔细阅读以下 JIRA 活动日志内容,分析并总结以下几点:

1. **关键讨论点**:有哪些重要的讨论、意见分歧或建议?
2. **变更记录**:涉及哪些字段修改、状态流转或责任人调整?
3. **风险与问题**:是否存在延期风险、技术难点、资源瓶颈?
4. **结论与行动项**:最终达成什么共识?谁负责下一步?

日志内容如下:

${contentText}
`;

        try {
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
                    headers: {
                        'Authorization': `Bearer ${GM_getValue('DASHSCOPE_API_KEY')}`,
                        'Content-Type': 'application/json'
                    },
                    data: JSON.stringify({
                        model: GM_getValue('MODEL_NAME'),
                        messages: [
                            { role: 'system', content: '你是一位经验丰富的项目管理与技术分析专家。' },
                            { role: 'user', content: prompt }
                        ]
                    }),
                    onload: res => resolve(JSON.parse(res.responseText)),
                    onerror: reject
                });
            });

            const result = response.choices?.[0]?.message?.content || '分析失败';
            showResultModal(result);
        } catch (err) {
            console.error(err);
            showResultModal("分析失败,请检查网络或 API 配置");
        }
    }

    function showResultModal(content) {
        // 移除已有弹窗
        const oldOverlay = document.getElementById('overlay');
        const oldModal = document.getElementById('ai-result-modal');
        if (oldOverlay) oldOverlay.remove();
        if (oldModal) oldModal.remove();

        // 创建遮罩层
        const overlay = document.createElement('div');
        overlay.id = 'overlay';
        document.body.appendChild(overlay);

        // 创建弹窗主体
        const modal = document.createElement('div');
        modal.id = 'ai-result-modal';

        const header = document.createElement('div');
        header.id = 'ai-result-header';
        header.textContent = 'AI 分析结果';

        const closeBtn = document.createElement('button');
        closeBtn.id = 'ai-result-close';
        closeBtn.innerHTML = '×';
        closeBtn.onclick = () => {
            document.body.removeChild(overlay);
            document.body.removeChild(modal);
        };
        header.appendChild(closeBtn);

        const contentBox = document.createElement('div');
        contentBox.id = 'ai-result-content';
        contentBox.innerHTML = marked.parse(content);

        const actions = document.createElement('div');
        actions.id = 'ai-result-actions';

        const copyBtn = document.createElement('button');
        copyBtn.textContent = '复制';
        copyBtn.onclick = () => {
            navigator.clipboard.writeText(content).then(() => {
                alert("已复制到剪贴板");
            });
        };

        const newWindowBtn = document.createElement('button');
        newWindowBtn.textContent = '新窗口打开';
        newWindowBtn.onclick = () => {
            const win = window.open('', '_blank');
            win.document.write(`
                <!DOCTYPE html>
                <html>
                <head>
                    <meta charset="UTF-8">
                    <title>AI 分析结果</title>
                    <style>
                        body { font-family: sans-serif; padding: 20px; }
                        pre { white-space: pre-wrap; }
                    </style>
                </head>
                <body>
                    ${marked.parse(content)}
                </body>
                </html>
            `);
            win.document.close();
        };

        actions.appendChild(copyBtn);
        actions.appendChild(newWindowBtn);

        modal.appendChild(header);
        modal.appendChild(contentBox);
        modal.appendChild(actions);
        document.body.appendChild(modal);

        // 拖拽功能
        let isDragging = false;
        let offsetX, offsetY;

        header.onmousedown = (e) => {
            isDragging = true;
            offsetX = e.clientX - modal.getBoundingClientRect().left;
            offsetY = e.clientY - modal.getBoundingClientRect().top;
            modal.style.cursor = 'grabbing';
        };

        document.onmousemove = (e) => {
            if (!isDragging) return;
            modal.style.left = `${e.clientX - offsetX}px`;
            modal.style.top = `${e.clientY - offsetY}px`;
            modal.style.transform = 'none';
        };

        document.onmouseup = () => {
            isDragging = false;
            modal.style.cursor = 'default';
        };
    }

    function openConfigModal() {
        const overlay = document.createElement('div');
        overlay.id = 'overlay';
        document.body.appendChild(overlay);

        const modal = document.createElement('div');
        modal.id = 'ai-config-modal';

        const keyLabel = document.createElement('label');
        keyLabel.textContent = 'API Key';
        const keyInput = document.createElement('input');
        keyInput.type = 'password';
        keyInput.value = GM_getValue('DASHSCOPE_API_KEY', '');
        keyLabel.appendChild(keyInput);

        const modelLabel = document.createElement('label');
        modelLabel.textContent = '模型名称';
        const modelSelect = document.createElement('select');
        DEFAULT_MODELS.forEach(model => {
            const option = document.createElement('option');
            option.value = model;
            option.textContent = model;
            if (model === GM_getValue('MODEL_NAME')) option.selected = true;
            modelSelect.appendChild(option);
        });
        const customOption = document.createElement('option');
        customOption.value = 'custom';
        customOption.textContent = '自定义...';
        modelSelect.appendChild(customOption);
        modelLabel.appendChild(modelSelect);

        const customInput = document.createElement('input');
        customInput.placeholder = '输入模型名称';
        customInput.style.display = modelSelect.value === 'custom' ? 'block' : 'none';
        modelSelect.addEventListener('change', () => {
            customInput.style.display = modelSelect.value === 'custom' ? 'block' : 'none';
        });

        const saveBtn = document.createElement('button');
        saveBtn.textContent = '保存配置';
        saveBtn.onclick = () => {
            GM_setValue('DASHSCOPE_API_KEY', keyInput.value);
            const selectedModel = modelSelect.value === 'custom' ? customInput.value : modelSelect.value;
            GM_setValue('MODEL_NAME', selectedModel);
            alert('配置已保存');
            document.body.removeChild(overlay);
            document.body.removeChild(modal);
        };

        modal.appendChild(keyLabel);
        modal.appendChild(modelLabel);
        modal.appendChild(modelSelect);
        modal.appendChild(customInput);
        modal.appendChild(saveBtn);
        document.body.appendChild(modal);
    }

    function initialize() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                createButtons();
            });
        } else {
            createButtons();
        }

        setTimeout(() => {
            if (!document.querySelector('.activity-fullscreen-btn')) {
                createButtons();
            }
        }, 3000);
    }

    initialize();
})();