Slack AI Assistant - Minimal Dark Theme

Gemini-powered Slack assistant with modern dark theme, Roboto font, and enhanced animations

// ==UserScript==
// @name         Slack AI Assistant - Minimal Dark Theme
// @version      7.6
// @description  Gemini-powered Slack assistant with modern dark theme, Roboto font, and enhanced animations
// @author       Shawon
// @icon         https://www.slack.com/favicon.ico
// @match        https://app.slack.com/client/*
// @grant        MIT
// @namespace https://greasyfork.org/users/1392874
// ==/UserScript==

(function () {
    'use strict';

    // === Color Palette ===
    const colors = {
        background: 'linear-gradient(135deg, #0e1111, #1a1f1f)',
        surface: 'linear-gradient(135deg, #1c2525, #2e3a3a)',
        primary: 'linear-gradient(90deg, #7F00FF, #E100FF)',
        secondary: 'linear-gradient(135deg, #00bfa5, #1de9b6)',
        error: '#ff5252',
        text: '#f5f5f5',
        muted: '#a0a0a0',
        border: '#3a4a4a',
        glow: 'rgba(127, 0, 255, 0.3)'
    };

    // === Animation Variables ===
    const animations = {
        buttonHoverDuration: '0.2s',
        transitionEasing: 'cubic-bezier(0.4, 0, 0.2, 1)',
        tooltipFadeDuration: '0.25s', // Slightly longer for smoother tooltip
        dropdownSlideDuration: '0.3s',
        modalScaleDuration: '0.4s',
        popupMoveDuration: '0.6s', // Slightly longer for smoother popup
        popupFadeDuration: '4s'   // Extended for better visibility
    };

    // === Button Configurations ===
    const buttons = [
        { id: 'goodMorning', icon: '🌞', tooltip: 'Send Good Morning', message: 'Good morning!', ariaLabel: 'Send Good Morning message' },
        { id: 'ok', icon: '👍', tooltip: 'Send Ok', message: 'Ok', ariaLabel: 'Send Ok message' },
        { id: 'quickMessage', icon: '🎯', tooltip: 'Quick Messages', dropdown: true, ariaLabel: 'Toggle quick messages' },
        { id: 'smartReply', icon: '✨', tooltip: 'Smart Reply (Gemini)', ariaLabel: 'Generate smart reply' },
        { id: 'polish', icon: '✏️', tooltip: 'Polish Message', ariaLabel: 'Open message polish modal' }
    ];

    // === Quick Messages ===
    const quickMessages = [
        { text: 'Thanks!', action: 'insert' },
        { text: 'Will do!', action: 'insert' },
        { text: 'On it!', action: 'send' },
        { text: 'Got it, thanks!', action: 'send' },
        { text: 'Looks good!', action: 'insert' },
        { text: 'I’ll follow up.', action: 'send' },
        { text: 'Can we discuss?', action: 'insert' },
        { text: 'Great work!', action: 'send' }
    ];

    // === Gemini API Client ===
    class GeminiClient {
        constructor() {
            this.baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
            this.model = 'gemini-1.5-flash';
        }

        async init() {
            let apiKey = localStorage.getItem('gemini_api_key');
            if (!apiKey) {
                apiKey = prompt('Enter your Gemini API key:');
                if (!apiKey) throw new Error('API key required');
                localStorage.setItem('gemini_api_key', apiKey);
            }
            this.apiKey = apiKey;
        }

        async generateContent(prompt) {
            try {
                if (!this.apiKey) await this.init();
                const response = await fetch(`${this.baseUrl}/${this.model}:generateContent?key=${this.apiKey}`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] })
                });
                if (!response.ok) throw new Error(`HTTP ${response.status}`);
                const data = await response.json();
                return data.candidates?.[0]?.content?.parts?.[0]?.text || 'No response';
            } catch (err) {
                throw new Error(`Gemini API failed: ${err.message}`);
            }
        }
    }

    // === Wait for Slack Interface ===
    function waitForSlackInterface(maxRetries = 30) {
        let retries = 0;
        const interval = setInterval(() => {
            const textBox = document.querySelector('.ql-editor[contenteditable="true"]') ||
                           document.querySelector('[data-qa="message_input"] .ql-editor') ||
                           document.querySelector('[data-message-input="true"] .ql-editor');
            if (textBox || retries >= maxRetries) {
                clearInterval(interval);
                if (textBox) addControlBox();
                else console.warn('Slack AI Assistant: Message input not found after max retries');
            }
            retries++;
        }, 1000);
    }

    // === Add Control Box ===
    function addControlBox() {
        const controlBox = document.createElement('div');
        controlBox.id = 'slackAssistant';
        Object.assign(controlBox.style, {
            position: 'fixed',
            bottom: '2rem',
            right: '25rem',
            background: colors.surface,
            borderRadius: '14px',
            boxShadow: `0 6px 16px rgba(0,0,0,0.3), 0 0 8px ${colors.glow}`,
            padding: '10px 12px',
            display: 'flex',
            alignItems: 'center',
            gap: '10px',
            zIndex: '100000000',
            cursor: 'move',
            animation: `fadeIn ${animations.modalScaleDuration} ${animations.transitionEasing}`,
            border: `1px solid ${colors.border}`,
            transition: `transform ${animations.buttonHoverDuration} ${animations.transitionEasing}`
        });

        // Load Roboto font
        const fontLink = document.createElement('link');
        fontLink.href = 'https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap';
        fontLink.rel = 'stylesheet';
        document.head.appendChild(fontLink);

        buttons.forEach(btn => controlBox.appendChild(createButton(btn)));

        document.body.appendChild(controlBox);

        // Draggable functionality
        let isDragging = false, offsetX, offsetY;
        controlBox.addEventListener('mousedown', e => {
            isDragging = true;
            offsetX = e.clientX - controlBox.getBoundingClientRect().left;
            offsetY = e.clientY - controlBox.getBoundingClientRect().top;
            controlBox.style.cursor = 'grabbing';
            document.body.style.userSelect = 'none';
        });
        document.addEventListener('mousemove', e => {
            if (isDragging) {
                controlBox.style.left = `${e.clientX - offsetX}px`;
                controlBox.style.top = `${e.clientY - offsetY}px`;
                controlBox.style.right = controlBox.style.bottom = 'auto';
            }
        }, { passive: true });
        document.addEventListener('mouseup', () => {
            isDragging = false;
            controlBox.style.cursor = 'move';
            document.body.style.userSelect = '';
        }, { passive: true });
    }

    // === Create Button ===
    function createButton({ id, icon, tooltip, message, dropdown, ariaLabel }) {
        const btn = document.createElement('button');
        btn.id = `btn-${id}`;
        btn.textContent = icon;
        btn.title = tooltip;
        btn.setAttribute('data-toggled', 'false');
        btn.setAttribute('aria-label', ariaLabel);
        Object.assign(btn.style, {
            fontSize: '16px',
            background: 'transparent',
            border: `1px solid ${colors.border}`,
            borderRadius: '50%',
            cursor: 'pointer',
            padding: '8px',
            transition: `transform ${animations.buttonHoverDuration} ${animations.transitionEasing}, background ${animations.buttonHoverDuration} ${animations.transitionEasing}, box-shadow ${animations.buttonHoverDuration} ${animations.transitionEasing}`,
            position: 'relative',
            color: colors.text,
            width: '36px',
            height: '36px',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center'
        });

        btn.onmouseenter = () => {
            btn.style.background = colors.border;
            btn.style.transform = 'scale(1.05)'; // Reduced scale for subtler effect
            btn.style.boxShadow = `0 0 6px ${colors.glow}`; // Softer glow
        };
        btn.onmouseleave = () => {
            btn.style.background = 'transparent';
            btn.style.transform = 'scale(1)';
            btn.style.boxShadow = 'none';
        };

        btn.onclick = () => {
            if (dropdown) return toggleQuickMessages(btn);
            if (id === 'smartReply') return generateSmartReply();
            if (id === 'polish') return showPolishModal();
            sendMessage(message);
        };

        return btn;
    }

    // === Toggle Quick Messages Dropdown ===
    function toggleQuickMessages(button) {
        const isToggled = button.getAttribute('data-toggled') === 'true';
        const existing = document.querySelector('#quickMessages');
        if (isToggled && existing) {
            existing.remove();
            button.setAttribute('data-toggled', 'false');
            return;
        }
        if (existing) existing.remove();

        const dropdown = document.createElement('div');
        dropdown.id = 'quickMessages';
        Object.assign(dropdown.style, {
            position: 'absolute',
            bottom: 'calc(100% + 10px)',
            right: '0',
            background: colors.surface,
            borderRadius: '12px',
            boxShadow: `0 6px 16px rgba(0,0,0,0.3), 0 0 8px ${colors.glow}`,
            padding: '14px',
            zIndex: '100000001',
            minWidth: '220px',
            maxWidth: '320px',
            maxHeight: '260px',
            overflowY: 'auto',
            animation: `slideDown ${animations.dropdownSlideDuration} ${animations.transitionEasing}`,
            border: `1px solid ${colors.border}`
        });

        quickMessages.forEach((msg, index) => {
            const item = document.createElement('div');
            item.textContent = msg.text;
            item.setAttribute('role', 'button');
            item.setAttribute('tabindex', '0');
            Object.assign(item.style, {
                padding: '10px 12px',
                cursor: 'pointer',
                borderRadius: '10px',
                transition: `background ${animations.buttonHoverDuration} ${animations.transitionEasing}, transform ${animations.buttonHoverDuration} ${animations.transitionEasing}`,
                fontSize: '14px',
                color: colors.text,
                background: 'transparent',
                fontWeight: '500',
                whiteSpace: 'normal',
                lineHeight: '1.5',
                transform: 'translateX(-20px)',
                opacity: '0',
                animation: `slideIn ${animations.dropdownSlideDuration} ${animations.transitionEasing} ${index * 0.05}s forwards`
            });

            item.onmouseenter = () => {
                item.style.background = colors.border;
                item.style.transform = 'translateX(0) scale(1.02)';
                item.style.boxShadow = `0 0 8px ${colors.glow}`;
            };
            item.onmouseleave = () => {
                item.style.background = 'transparent';
                item.style.transform = 'translateX(0)';
                item.style.boxShadow = 'none';
            };

            item.onclick = () => {
                if (msg.action === 'insert') insertMessage(msg.text);
                else sendMessage(msg.text);
                dropdown.remove();
                button.setAttribute('data-toggled', 'false');
            };

            item.onkeydown = e => {
                if (e.key === 'Enter' || e.key === ' ') {
                    item.click();
                    e.preventDefault();
                }
            };

            dropdown.appendChild(item);
        });

        button.parentNode.appendChild(dropdown);
        button.setAttribute('data-toggled', 'true');

        document.addEventListener('click', e => {
            if (!dropdown.contains(e.target) && e.target !== button) {
                dropdown.remove();
                button.setAttribute('data-toggled', 'false');
            }
        }, { once: true });
    }

    // === Show Polish Modal ===
    function showPolishModal() {
        const modal = document.createElement('div');
        modal.setAttribute('role', 'dialog');
        modal.setAttribute('aria-label', 'Message Refinement Modal');
        Object.assign(modal.style, {
            position: 'fixed',
            top: '0',
            left: '0',
            width: '100vw',
            height: '100vh',
            background: 'rgba(18, 18, 18, 0.95)',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            zIndex: '100000001',
            animation: `fadeIn ${animations.modalScaleDuration} ${animations.transitionEasing}`
        });

        const box = document.createElement('div');
        Object.assign(box.style, {
            background: colors.surface,
            padding: '28px',
            borderRadius: '16px',
            display: 'flex',
            flexDirection: 'column',
            gap: '16px',
            width: '440px',
            maxWidth: '92%',
            boxShadow: `0 8px 20px rgba(0,0,0,0.4), 0 0 10px ${colors.glow}`,
            border: `1px solid ${colors.border}`,
            position: 'relative',
            transform: 'scale(0.9)',
            animation: `scaleIn ${animations.modalScaleDuration} ${animations.transitionEasing} forwards`
        });

        const crossBtn = document.createElement('button');
        crossBtn.textContent = '×';
        crossBtn.setAttribute('aria-label', 'Close message refinement modal');
        Object.assign(crossBtn.style, {
            position: 'absolute',
            top: '12px',
            right: '12px',
            background: colors.error,
            color: colors.text,
            border: 'none',
            borderRadius: '50%',
            width: '28px',
            height: '28px',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            fontSize: '18px',
            cursor: 'pointer',
            transition: `transform ${animations.buttonHoverDuration} ${animations.transitionEasing}, background ${animations.buttonHoverDuration} ${animations.transitionEasing}`
        });
        crossBtn.onmouseenter = () => {
            crossBtn.style.background = '#b71c1c';
            crossBtn.style.transform = 'scale(1.1)';
        };
        crossBtn.onmouseleave = () => {
            crossBtn.style.background = colors.error;
            crossBtn.style.transform = 'scale(1)';
        };
        crossBtn.onclick = () => {
            modal.remove();
            const textBox = document.querySelector('.ql-editor[contenteditable="true"]') ||
                           document.querySelector('[data-qa="message_input"] .ql-editor');
            if (textBox) textBox.focus();
        };

        const title = document.createElement('div');
        title.textContent = 'Refine Message';
        Object.assign(title.style, {
            fontSize: '18px',
            fontWeight: '700',
            color: colors.text,
            paddingBottom: '12px',
            borderBottom: `1px solid ${colors.border}`
        });

        const textarea = document.createElement('textarea');
        textarea.placeholder = 'Type your message here...';
        textarea.setAttribute('aria-label', 'Message input for polishing or grammar fixing');
        Object.assign(textarea.style, {
            resize: 'none',
            minHeight: '140px',
            fontSize: '14px',
            padding: '14px',
            borderRadius: '12px',
            border: `1px solid ${colors.border}`,
            transition: `border ${animations.buttonHoverDuration} ${animations.transitionEasing}, box-shadow ${animations.buttonHoverDuration} ${animations.transitionEasing}`,
            outline: 'none',
            background: colors.background,
            color: colors.text,
            lineHeight: '1.6'
        });
        textarea.onfocus = () => {
            textarea.style.border = `1px solid ${colors.primary}`;
            textarea.style.boxShadow = `0 0 8px ${colors.glow}`;
        };
        textarea.onblur = () => {
            textarea.style.border = `1px solid ${colors.border}`;
            textarea.style.boxShadow = 'none';
        };
        textarea.focus();

        const buttonRow = document.createElement('div');
        Object.assign(buttonRow.style, {
            display: 'flex',
            justifyContent: 'flex-end',
            gap: '12px',
            flexWrap: 'wrap'
        });

        const modalButtons = [
            {
                text: 'Fix Grammar & Insert',
                bg: colors.secondary,
                color: colors.text,
                hoverBg: colors.secondary,
                ariaLabel: 'Fix grammar and insert the message',
                action: async () => {
                    const text = textarea.value.trim();
                    if (!text) return showPopup('Message empty!');
                    showPopup('Fixing grammar...', false);
                    try {
                        const gemini = new GeminiClient();
                        const prompt = `Fix grammar and clarity of this Slack message:\n"${text}"\nKeep tone and intent unchanged. Return only the corrected message.`;
                        const result = await gemini.generateContent(prompt);
                        insertMessage(result);
                        modal.remove();
                        showPopup('Grammar fixed and inserted!', true);
                    } catch (err) {
                        showPopup(err.message);
                    }
                }
            },
            {
                text: 'Polish & Insert',
                bg: colors.primary,
                color: colors.text,
                hoverBg: colors.primary,
                ariaLabel: 'Polish and insert the message',
                action: async () => {
                    const text = textarea.value.trim();
                    if (!text) return showPopup('Message empty!');
                    showPopup('Polishing...', false);
                    try {
                        const gemini = new GeminiClient();
                        const prompt = `Polish this Slack message to be polite, friendly, and professional:\n"${text}"\nPreserve original intent. Return only the improved message.`;
                        const result = await gemini.generateContent(prompt);
                        insertMessage(result);
                        modal.remove();
                        showPopup('Inserted!', true);
                    } catch (err) {
                        showPopup(err.message);
                    }
                }
            }
        ];

        modalButtons.forEach(({ text, bg, color, hoverBg, ariaLabel, action }) => {
            const btn = document.createElement('button');
            btn.textContent = text;
            btn.setAttribute('aria-label', ariaLabel);
            Object.assign(btn.style, {
                padding: '12px 18px',
                borderRadius: '12px',
                background: bg,
                color,
                border: 'none',
                cursor: 'pointer',
                transition: `transform ${animations.buttonHoverDuration} ${animations.transitionEasing}, box-shadow ${animations.buttonHoverDuration} ${animations.transitionEasing}`,
                fontSize: '14px',
                fontWeight: '500',
                position: 'relative',
                overflow: 'hidden'
            });
            btn.onmouseenter = () => {
                btn.style.transform = 'scale(1.05)';
                btn.style.boxShadow = `0 0 12px ${colors.glow}`;
                btn.style.animation = `pulse ${animations.buttonHoverDuration} ${animations.transitionEasing} infinite`;
            };
            btn.onmouseleave = () => {
                btn.style.transform = 'scale(1)';
                btn.style.boxShadow = 'none';
                btn.style.animation = 'none';
            };
            btn.onclick = action;
            buttonRow.appendChild(btn);
        });

        box.append(crossBtn, title, textarea, buttonRow);
        modal.appendChild(box);
        document.body.appendChild(modal);

        modal.addEventListener('keydown', e => {
            if (e.key === 'Escape') crossBtn.click();
        });
    }

    // === Generate Smart Reply ===
    async function generateSmartReply() {
        const messages = Array.from(document.querySelectorAll('.c-message_kit__background')).slice(-2);
        let convo = '', lastSender = null;
        messages.forEach(msg => {
            const sender = msg.querySelector('.c-message__sender_button')?.textContent.trim() || lastSender;
            if (sender) lastSender = sender;
            const text = Array.from(msg.querySelectorAll('.p-rich_text_section')).map(el => el.textContent.trim()).join(' ');
            if (text) convo += `${lastSender}: ${text}\n\n`;
        });

        if (!convo) return showPopup('No messages found');
        showPopup('Generating reply...', false);

        try {
            const gemini = new GeminiClient();
            const prompt = `You are Shawon. Reply to the other person in this Slack conversation in a friendly, professional tone. Do not mention Shawon. No emojis.\n\n${convo}\n\nReturn a short, natural reply.`;
            const reply = await gemini.generateContent(prompt);
            insertMessage(reply);
            showPopup('Reply inserted!', true);
        } catch (err) {
            showPopup(err.message);
        }
    }

    // === Insert Message ===
    function insertMessage(message) {
        const textBox = document.querySelector('.ql-editor[contenteditable="true"]') ||
                        document.querySelector('[data-qa="message_input"] .ql-editor') ||
                        document.querySelector('[data-message-input="true"] .ql-editor');
        if (textBox) {
            textBox.focus();
            document.execCommand('insertText', false, message);
        } else {
            showPopup('Message input not found');
        }
    }

    // === Send Message ===
    function sendMessage(message) {
        const textBox = document.querySelector('.ql-editor[contenteditable="true"]') ||
                        document.querySelector('[data-qa="message_input"] .ql-editor') ||
                        document.querySelector('[data-message-input="true"] .ql-editor');
        const sendBtn = document.querySelector('[data-qa="texty_send_button"]') ||
                        document.querySelector('button[aria-label*="Send message"]');
        if (textBox && sendBtn) {
            textBox.focus();
            document.execCommand('insertText', false, message);
            setTimeout(() => sendBtn.click(), 500);
        } else {
            showPopup('Unable to send message');
        }
    }

    // === Show Popup ===
    function showPopup(message, isFollowUp = false) {
        const popup = document.createElement('div');
        const controlBox = document.querySelector('#slackAssistant');
        const controlBoxRect = controlBox?.getBoundingClientRect();
        const baseBottom = controlBoxRect ? `${window.innerHeight - controlBoxRect.top + 20}px` : '7rem'; // Position above control box
        Object.assign(popup.style, {
            position: 'fixed',
            bottom: isFollowUp ? `calc(${baseBottom} + 3.5rem)` : baseBottom, // Stack follow-up popup with spacing
            right: controlBoxRect ? `${window.innerWidth - controlBoxRect.right + 10}px` : '28rem', // Align with control box
            background: colors.primary,
            color: colors.text,
            padding: '12px 18px',
            borderRadius: '16px',
            fontSize: '14px',
            boxShadow: `0 6px 16px rgba(0,0,0,0.3), 0 0 8px ${colors.glow}`,
            fontWeight: '500',
            zIndex: '100000002', // Higher than control box
            animation: isFollowUp
                ? `fadeInOut ${animations.popupFadeDuration} ${animations.transitionEasing}`
                : `fadeInOut ${animations.popupFadeDuration} ${animations.transitionEasing}, moveUp ${animations.popupMoveDuration} ${animations.transitionEasing} forwards`,
            transform: 'translateY(10px)',
            opacity: '0',
            maxWidth: '300px',
            textAlign: 'center'
        });
        popup.textContent = message;
        document.body.appendChild(popup);
        setTimeout(() => popup.remove(), 4000); // Matches longer fade duration
    }

    // === Inject Styles ===
    const style = document.createElement('style');
    style.textContent = `
        /* === Animations === */
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }

        @keyframes slideDown {
            from { opacity: 0; transform: translateY(-10px); }
            to { opacity: 1; transform: translateY(0); }
        }

        @keyframes fadeInOut {
            0% { opacity: 0; transform: translateY(10px); }
            15% { opacity: 1; transform: translateY(0); }
            85% { opacity: 1; transform: translateY(0); }
            100% { opacity: 0; transform: translateY(10px); }
        }

        @keyframes moveUp {
            0% { transform: translateY(10px); }
            80% { transform: translateY(-5px); }
            100% { transform: translateY(0); }
        }

        @keyframes scaleIn {
            from { transform: scale(0.9); opacity: 0; }
            to { transform: scale(1); opacity: 1; }
        }

        @keyframes slideIn {
            from { transform: translateX(-20px); opacity: 0; }
            to { transform: translateX(0); opacity: 1; }
        }

        @keyframes pulse {
            0% { box-shadow: 0 0 0 ${colors.glow}; }
            50% { box-shadow: 0 0 12px ${colors.glow}; }
            100% { box-shadow: 0 0 0 ${colors.glow}; }
        }

        /* === Global Styles === */
        #slackAssistant, #quickMessages, [role="button"], textarea {
            font-family: 'Roboto', sans-serif;
            transition: all ${animations.buttonHoverDuration} ${animations.transitionEasing};
        }

        /* === Scrollbar for Quick Messages === */
        #quickMessages::-webkit-scrollbar {
            width: 8px;
        }
        #quickMessages::-webkit-scrollbar-track {
            background: transparent;
            border-radius: 4px;
        }
        #quickMessages::-webkit-scrollbar-thumb {
            background: ${colors.border};
            border-radius: 4px;
        }
        #quickMessages::-webkit-scrollbar-thumb:hover {
            background: ${colors.muted};
        }

        /* === Textarea Placeholder === */
        textarea::placeholder {
            color: ${colors.muted};
            opacity: 1;
        }

        /* === Tooltip Styles === */
        [title]:hover:after {
            content: attr(title);
            position: absolute;
            bottom: calc(100% + 12px);
            left: 50%;
            transform: translateX(-50%);
            background: ${colors.surface};
            color: ${colors.text};
            padding: 4px 8px;
            width:8rem;
            border-radius: 12px !important; /* Rounded shape */
            font-size: 11px !important; /* Slightly larger for readability */
            font-weight: 500;
            whiteSpace: nowrap;
            z-index: 100000002;
            border: 1px solid ${colors.border};
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            animation: fadeTooltip ${animations.tooltipFadeDuration} ${animations.transitionEasing} forwards;
        }

        @keyframes fadeTooltip {
            from { opacity: 0; transform: translateX(-50%) translateY(5px); }
            to { opacity: 1; transform: translateX(-50%) translateY(0); }
        }
    `;
    document.head.appendChild(style);

    waitForSlackInterface();
})();