// ==UserScript== // @name Gemini Prompt Queue // @description Queue multiple prompts for Gemini // @author nihaltp // @namespace https://github.com/nihaltp/uscripts // @supportURL https://github.com/nihaltp/uscripts/issues // @homepageURL https://github.com/nihaltp/uscripts // @homepage https://github.com/nihaltp/uscripts // @license MIT // @match https://gemini.google.com/* // @icon https://gemini.google.com/favicon.ico // @version 1.0.0 // @grant none // @downloadURL https://raw.githubusercontent.com/nihaltp/uscripts/main/AI Queue/gemini.user.js // @updateURL https://raw.githubusercontent.com/nihaltp/uscripts/main/AI Queue/gemini.user.js // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const queue = []; let running = false; let editingId = null; let draggedId = null; window.aiQueueDebug = true; // set to true to enable debug logs // ----------------------------- // MARK: UI // ----------------------------- let panel; let isPanelVisible = false; // MARK: logging helpers function log(...args) { if (!window.aiQueueDebug) return; console.log("[AI QUEUE]", ...args); } function error(...args) { console.error("[AI QUEUE]", ...args); } function throwError(...args) { error(...args); throw new Error(args.join(' ')); } // MARK: create panel function createPanel() { panel = document.createElement('div'); panel.id = 'pq-panel'; Object.assign(panel.style, { position: 'fixed', top: '100px', left: '100px', bottom: 'auto', right: 'auto', width: '320px', minHeight: '200px', maxHeight: '70vh', overflowY: 'auto', background: '#202123', color: 'white', border: '1px solid #444', borderRadius: '16px', padding: '12px', zIndex: '2147483647', boxShadow: '0 10px 40px rgba(0,0,0,0.6)', display: 'none', // outline: '2px solid #555', }); panel.innerHTML = `
Prompt Queue
Idle
    `; // document.documentElement.appendChild(panel); document.body.appendChild(panel); setupPanelEvents(); } // MARK: setupPanelEvents function setupPanelEvents() { const input = panel.querySelector('#pq-input'); const addBtn = panel.querySelector('#pq-add'); const startBtn = panel.querySelector('#pq-start'); window.pqInput = input; window.pqAddBtn = addBtn; addBtn.addEventListener('click', () => { const text = input.value.trim(); if (!text) { error('Empty prompt, not adding to queue'); return; } // editing existing item if (editingId !== null) { const item = queue.find(item => item.id === editingId); if (!item) { error('Editing item not found in queue:', editingId); return; } item.prompt = text; editingId = null; addBtn.textContent = 'Add To Queue'; } else { // add new item queue.push({ id: crypto.randomUUID(), prompt: text, }); } updateToolbarButton(); input.value = ''; renderQueue(); }); startBtn.addEventListener('click', async () => { if (running) return; running = true; updateToolbarButton(); processQueue(); }); } // MARK: renderQueue function renderQueue() { const list = panel.querySelector('#pq-list'); list.innerHTML = ''; queue.forEach((item, index) => { const li = document.createElement('li'); li.style.marginBottom = '10px'; li.draggable = true; const row = document.createElement('div'); row.style.display = 'flex'; row.style.gap = '6px'; row.style.alignItems = 'flex-start'; if (editingId == item.id) { row.style.background = '#333'; row.style.padding = '6px'; row.style.borderRadius = '6px'; row.style.outline = '1px solid #7dd3fc'; } const text = document.createElement('div'); text.textContent = item.prompt; text.style.flex = '1'; text.style.wordBreak = 'break-word'; text.style.fontSize = '14px'; text.addEventListener('dblclick', () => { editQueueItem(item.id); }); const editBtn = document.createElement('button'); editBtn.textContent = '🖉'; editBtn.title = 'Edit'; editBtn.style.cursor = 'pointer'; editBtn.style.color = '#7dd3fc'; editBtn.addEventListener('click', () => { editQueueItem(item.id); }); const deleteBtn = document.createElement('button'); deleteBtn.textContent = '✕'; deleteBtn.title = 'Delete'; deleteBtn.style.cursor = 'pointer'; deleteBtn.style.color = '#ff6b6b'; deleteBtn.addEventListener('click', () => { const preview = item.prompt.length > 80 ? item.prompt.slice(0, 80) + '...' : item.prompt; const confirmed = confirm( `Delete this prompt?\n\n${preview}` ); if (!confirmed) { error('Deletion cancelled for item:', item.id); return; } deleteQueueItem(item.id); }); row.appendChild(text); row.appendChild(editBtn); row.appendChild(deleteBtn); li.appendChild(row); li.addEventListener('dragstart', () => { draggedId = item.id; li.style.opacity = '0.5'; }); li.addEventListener('dragend', () => { draggedId = null; li.style.opacity = '1'; }); li.addEventListener('dragover', e => { e.preventDefault(); li.style.borderTop = '2px solid #888'; }); li.addEventListener('dragleave', () => { li.style.borderTop = ''; }); li.addEventListener('drop', e => { e.preventDefault(); li.style.borderTop = ''; if (draggedId === item.id) { error('Dropped on itself, ignoring:', item.id); return; } moveQueueItem(draggedId, item.id); }); list.appendChild(li); }); updateToolbarButton(); } // MARK: deleteQueueItem function deleteQueueItem(id) { const index = queue.findIndex(item => item.id === id); if (index === -1) { error('Item to delete not found in queue:', id); return; } queue.splice(index, 1); renderQueue(); } // MARK: editQueueItem function editQueueItem(id) { const item = queue.find(item => item.id === id); if (!item) { error('Item to edit not found in queue:', id); return; } editingId = id; window.pqInput.value = item.prompt; window.pqAddBtn.textContent = 'Save Changes'; window.pqInput.focus(); // move cursor to end window.pqInput.selectionStart = window.pqInput.selectionEnd = window.pqInput.value.length; } // MARK: moveQueueItem function moveQueueItem(fromId, toId) { const fromIndex = queue.findIndex(item => item.id === fromId); const toIndex = queue.findIndex(item => item.id === toId); if (fromIndex === -1 || toIndex === -1) { return; } const [movedItem] = queue.splice(fromIndex, 1); queue.splice(toIndex, 0, movedItem); renderQueue(); } // MARK: setStatus function setStatus(text) { const status = panel.querySelector('#pq-status'); if (status) { status.textContent = text; } } // MARK: togglePanel function togglePanel() { isPanelVisible = !isPanelVisible; panel.style.display = isPanelVisible ? 'block' : 'none'; panel.style.pointerEvents = isPanelVisible ? 'auto' : 'none'; panel.style.visibility = 'visible'; panel.style.opacity = '1'; panel.style.top = '100px'; panel.style.left = '100px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; panel.style.inset = 'unset'; log('Panel element:', panel); log('Panel visible:', isPanelVisible); } // MARK: createToolbarButton function createToolbarButton() { const host = getComposerHost(); let button = document.querySelector('#pq-toolbar-button'); if (!button) { button = document.createElement('button'); button.id = 'pq-toolbar-button'; button.type = 'button'; button.textContent = 'Queue'; button.addEventListener('click', togglePanel); // add animation styles once if (!document.querySelector('#pq-styles')) { const style = document.createElement('style'); style.id = 'pq-styles'; style.textContent = ` @keyframes pq-pulse { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.06); opacity: 0.75; } 100% { transform: scale(1); opacity: 1; } } `; document.head.appendChild(style); } } if (host) { button.className = 'composer-btn h-9 min-h-9'; button.style.position = ''; button.style.bottom = ''; button.style.right = ''; button.style.left = ''; button.style.zIndex = ''; button.style.padding = '0 12px'; button.style.borderRadius = '9999px'; button.style.marginInlineStart = '8px'; if (button.parentElement !== host) { host.prepend(button); } } else { button.className = ''; button.style.position = 'fixed'; button.style.bottom = '24px'; button.style.right = '24px'; button.style.padding = '10px 14px'; button.style.borderRadius = '9999px'; button.style.background = '#1f1f1f'; button.style.color = '#fff'; button.style.border = '1px solid #555'; button.style.boxShadow = '0 10px 30px rgba(0,0,0,0.35)'; button.style.zIndex = '2147483647'; if (button.parentElement !== document.body) { document.body.appendChild(button); } } } // MARK: updateToolbarButton function updateToolbarButton() { const button = document.querySelector('#pq-toolbar-button'); if (!button) { error('Toolbar button not found, cannot update'); return; } const count = queue.length; button.textContent = count > 0 ? `Queue (${count})` : 'Queue'; // running animation if (running) { button.style.animation = 'pq-pulse 1.2s infinite'; button.style.opacity = '1'; } else { button.style.animation = ''; button.style.opacity = count > 0 ? '1' : '0.8'; } } createPanel(); // continuously reattach button because Gemini rerenders UI const observer = new MutationObserver(() => { createToolbarButton(); log('DOM mutated, ensured toolbar button and panel exist'); }); observer.observe(document.body, { childList: true, subtree: true, }); // ----------------------------- // MARK: Gemini Helpers // ----------------------------- function getComposerEditor() { return document.querySelector( '#prompt-textarea, textarea:not(#pq-input), [contenteditable="true"][role="textbox"], [contenteditable="true"]' ); } function getComposerHost() { const editor = getComposerEditor(); if (!editor) { return null; } return ( editor.closest('form') || editor.closest('[role="toolbar"]') || editor.closest('div') || editor.parentElement ); } // MARK: getSendButton function getSendButton() { const selectors = [ 'button[data-testid="send-button"]', 'button[aria-label*="Send"]', 'button[title*="Send"]', 'button[aria-label*="Submit"]', '[role="button"][aria-label*="Send"]', ]; for (const selector of selectors) { const button = document.querySelector(selector); if (button && !button.disabled && button.offsetParent !== null) { return button; } } const host = getComposerHost(); if (!host) { return null; } const buttons = [...host.querySelectorAll('button,[role="button"]')]; return ( buttons.find(btn => !btn.disabled && btn.offsetParent !== null && /send|submit/i.test((btn.getAttribute('aria-label') || '') + ' ' + (btn.textContent || '') + ' ' + (btn.getAttribute('title') || '')) ) || null ); } function isGenerating() { return !!document.querySelector( 'button[data-testid="stop-button"], button[aria-label*="Stop"], [role="button"][aria-label*="Stop"]' ); } async function waitForIdle() { while (isGenerating()) { await sleep(1000); } // extra delay for stability await sleep(1500); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function setEditorValue(editor, prompt) { if (!editor) { throwError('Editor not found'); } editor.focus(); if ('value' in editor) { editor.value = prompt; editor.dispatchEvent( new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'insertText', data: prompt, }) ); return; } if (editor.isContentEditable) { editor.textContent = prompt; editor.dispatchEvent( new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'insertText', data: prompt, }) ); return; } throwError('Unsupported editor type'); } // MARK: sendPrompt async function sendPrompt(prompt) { const editor = getComposerEditor(); if (!editor) { throwError('Editor not found'); } setEditorValue(editor, prompt); await sleep(800); const sendButton = getSendButton(); if (sendButton) { sendButton.click(); return; } editor.dispatchEvent( new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter', }) ); } // MARK: processQueue async function processQueue() { setStatus('Running'); while (queue.length > 0) { await waitForIdle(); const item = queue.shift(); const prompt = item.prompt; updateToolbarButton(); renderQueue(); setStatus(`Sending: ${prompt.slice(0, 40)}...`); try { await sendPrompt(prompt); await sleep(1000); await waitForIdle(); } catch (err) { error('Error processing prompt:', err); setStatus('Error: ' + err.message); running = false; updateToolbarButton(); return; } } setStatus('Finished'); running = false; updateToolbarButton(); } })();