您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); })();