您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Advanced ChatGPT automation with dynamic templating
// ==UserScript== // @name ChatGPT Automation Pro // @namespace http://tampermonkey.net/ // @version 1.6 // @description Advanced ChatGPT automation with dynamic templating // @author Henry Russell // @match https://chatgpt.com/* // @match https://chat.openai.com/* // @grant unsafeWindow // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @connect * // @inject-into content // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; // Configuration const CONFIG = { DEBUG_MODE: false, RESPONSE_TIMEOUT: 3000000, // 5 minutes MIN_WIDTH: 300, MIN_HEIGHT: 200, MAX_WIDTH: 600, MAX_HEIGHT: 800, DEFAULT_VISIBLE: false }; // State management let isProcessing = false; let isLooping = false; let currentBatchIndex = 0; let dynamicElements = []; let lastResponseElement = null; let responseObserver = null; let isMinimized = false; let isDarkMode = false; let uiVisible = CONFIG.DEFAULT_VISIBLE; let headerObserverStarted = false; let batchWaitTime = 2000; // Default wait time between batch items let autoRemoveProcessed = true; // Whether to remove processed items from textbox let autoScrollLogs = true; // Whether to auto-scroll logs let newChatPerItem = false; // Whether to start new chat for each item // Storage keys const STORAGE_KEYS = { messageInput: 'messageInput', templateInput: 'templateInput', dynamicElementsInput: 'dynamicElementsInput', customCodeInput: 'customCodeInput', loop: 'looping', autoRemove: 'autoRemoveProcessed', newChat: 'newChatPerItem', autoScroll: 'autoScrollLogs', waitTime: 'batchWaitTime', activeTab: 'activeTab', uiState: 'uiState', logHeight: 'logContentHeight', // Config keys configDebug: 'config.debugMode', configTimeout: 'config.responseTimeout', configMinWidth: 'config.minWidth', configMinHeight: 'config.minHeight', configMaxWidth: 'config.maxWidth', configMaxHeight: 'config.maxHeight', configDefaultVisible: 'config.defaultVisible' }; // UI Elements let mainContainer = null; let messageInput = null; let customCodeInput = null; let templateInput = null; let dynamicElementsInput = null; let statusIndicator = null; let logContainer = null; let progressBar = null; let resizeHandle = null; // Utility functions const log = (message, type = 'info') => { const timestamp = new Date().toLocaleTimeString(); const logMessage = `[${timestamp}] ${message}`; if (CONFIG.DEBUG_MODE) { console.log(logMessage); } if (logContainer) { const logEntry = document.createElement('div'); logEntry.className = `log-entry log-${type}`; logEntry.textContent = logMessage; logContainer.appendChild(logEntry); // Auto-scroll logs if enabled if (autoScrollLogs) { logContainer.scrollTop = logContainer.scrollHeight; } // Keep only last 50 log entries while (logContainer.children.length > 50) { logContainer.removeChild(logContainer.firstChild); } } }; // Detect dark mode const detectDarkMode = () => { const htmlElement = document.documentElement; const bodyElement = document.body; // Check various indicators for dark mode const darkIndicators = [ htmlElement.classList.contains('dark'), bodyElement.classList.contains('dark'), htmlElement.getAttribute('data-theme') === 'dark', bodyElement.getAttribute('data-theme') === 'dark', getComputedStyle(bodyElement).backgroundColor.includes('rgb(0, 0, 0)') || getComputedStyle(bodyElement).backgroundColor.includes('rgb(17, 24, 39)') || getComputedStyle(bodyElement).backgroundColor.includes('rgb(31, 41, 55)') ]; return darkIndicators.some(indicator => indicator); }; // Cross-origin HTTP helper using GM_xmlhttpRequest const http = { request: (opts) => new Promise((resolve, reject) => { try { const { method = 'GET', url, headers = {}, data, responseType = 'text', timeout = 30000 } = opts || {}; if (!url) throw new Error('Missing url'); GM_xmlhttpRequest({ method, url, headers, data, responseType, timeout, anonymous: false, onload: (res) => resolve(res), onerror: (err) => reject(err), ontimeout: () => reject(new Error('Request timeout')) }); } catch (e) { reject(e); } }), postForm: (url, formObj, extraHeaders = {}) => { const body = Object.entries(formObj || {}) .map(([k,v]) => encodeURIComponent(k) + '=' + encodeURIComponent(String(v))) .join('&'); return http.request({ method: 'POST', url, headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', ...extraHeaders }, data: body }); }, postMultipart: (url, formObj, extraHeaders = {}) => { return http.postForm(url, formObj, extraHeaders); } }; const executeCustomCode = async (code, responseText, templateData = null) => { if (!code || code.trim() === '') return; try { // Resolve context from templateData first let item = templateData?.elementData ?? null; let index = templateData?.index ?? null; let total = templateData?.total ?? null; // Fallback: if item is missing and the Dynamic Elements input contains exactly one element, // use it so custom code depending on { item } can still run outside Template mode. if (!item && dynamicElementsInput && typeof dynamicElementsInput.value === 'string' && dynamicElementsInput.value.trim()) { try { const arr = await parseDynamicElements(dynamicElementsInput.value.trim()); if (Array.isArray(arr) && arr.length === 1) { item = arr[0]; if (index == null) index = 1; if (total == null) total = 1; log('Context fallback applied: using single dynamic element for custom code'); } else if (Array.isArray(arr) && arr.length > 1 && CONFIG.DEBUG_MODE) { log('Context note: multiple dynamic elements detected but Template mode is off; no auto-selection applied', 'warning'); } } catch { /* ignore fallback errors */ } } // Debug logging to help troubleshoot if (CONFIG.DEBUG_MODE) { log(`Custom code context: item=${item ? JSON.stringify(item).slice(0,100) : 'null'}, index=${index}, total=${total}`); } // Create and execute the user function, properly awaiting any returned promise // Handle both regular code and async IIFE patterns let result; // Check if the code is wrapped in an async IIFE pattern const asyncIIFEPattern = /^\s*\(\s*async\s*\(\s*\)\s*=>\s*\{[\s\S]*\}\s*\)\s*\(\s*\)\s*;?\s*$/; const isAsyncIIFE = asyncIIFEPattern.test(code.trim()); if (isAsyncIIFE) { // For async IIFE, execute directly and the result will be a Promise log('Detected async IIFE pattern, executing with proper await...'); const fn = new Function('response', 'log', 'console', 'item', 'index', 'total', 'http', `return ${code}`); result = fn( responseText, (msg, type = 'info') => log(msg, type), console, item, index, total, http ); } else { // For regular code, wrap in function as before const fn = new Function('response', 'log', 'console', 'item', 'index', 'total', 'http', code); result = fn( responseText, (msg, type = 'info') => log(msg, type), console, item, index, total, http ); } // Properly await the result whether it's a promise or not await Promise.resolve(result); log('Custom code executed successfully'); } catch (error) { log(`Custom code execution error: ${error.message}`, 'error'); // Re-throw the error so the calling code can handle retries throw error; } }; // Note: executeCustomCode is the primary API used throughout this script. // Template processing const processDynamicTemplate = (template, dynamicData) => { if (!template) return ''; const getByPath = (obj, path) => { try { return path.split('.').reduce((acc, part) => acc != null ? acc[part] : undefined, obj); } catch { return undefined; } }; const regex = /\{\{\s*([\w$.]+)\s*\}\}|\{\s*([\w$.]+)\s*\}/g; return template.replace(regex, (_, g1, g2) => { const keyPath = g1 || g2; let value = getByPath(dynamicData, keyPath); if (value === undefined) return ''; if (typeof value === 'object') { try { return JSON.stringify(value); } catch { return String(value); } } return String(value); }); }; // Parse dynamic elements (can be array or function) const parseDynamicElements = async (input) => { const raw = (input || '').trim(); if (!raw) return []; // Strict array JSON if (raw.startsWith('[')) { try { return JSON.parse(raw); } catch (e) { log(`Invalid JSON: ${e.message}`, 'error'); return []; } } // Support single-object JSON if (raw.startsWith('{')) { try { const obj = JSON.parse(raw); return [obj]; } catch (e) { /* fall through to eval */ } } // Evaluate expression or function in userscript context try { const fn = new Function('return ( ' + raw + ' )'); const v = fn(); const res = (typeof v === 'function') ? v() : v; if (Array.isArray(res)) return res; if (res && typeof res === 'object') return [res]; if (typeof res === 'string') { try { const parsed = JSON.parse(res); if (Array.isArray(parsed)) return parsed; if (parsed && typeof parsed === 'object') return [parsed]; } catch { /* ignore */ } } throw new Error('Result is not an array/object'); } catch (error) { log(`Error parsing dynamic elements: ${error.message}`, 'error'); return []; } }; // Storage helper to reduce repetitive try-catch blocks const saveToStorage = (key, value) => { try { GM_setValue(key, value); } catch {} }; // Save UI state (simplified with debouncing) let saveTimeout; const saveUIState = (immediate = false) => { if (!mainContainer) return; const doSave = () => { const state = { left: mainContainer.style.left, top: mainContainer.style.top, right: mainContainer.style.right, width: mainContainer.style.width, height: mainContainer.style.height, minimized: isMinimized, visible: uiVisible }; GM_setValue(STORAGE_KEYS.uiState, JSON.stringify(state)); }; if (immediate) { clearTimeout(saveTimeout); doSave(); } else { clearTimeout(saveTimeout); saveTimeout = setTimeout(doSave, 100); // Debounce saves } }; // Load UI state (simplified) const loadUIState = () => { try { const saved = GM_getValue(STORAGE_KEYS.uiState, null); return saved ? JSON.parse(saved) : {}; } catch { return {}; } }; const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // Function to start a new chat const startNewChat = async () => { try { log('Starting new chat...'); // Method 1: Try the "New chat" button (language independent, uses data-testid) const newChatButton = document.querySelector('a[data-testid="create-new-chat-button"]'); if (newChatButton) { log('Using new chat button...'); newChatButton.click(); await sleep(1000); return true; } const homeLink = document.querySelector('a[href="/"]'); if (homeLink && homeLink.textContent.trim() !== '') { log('Using home link...'); homeLink.click(); await sleep(1000); return true; } log('Using programmatic navigation...'); const currentUrl = window.location.href; const baseUrl = window.location.origin; // Only navigate if we're not already on the home page if (currentUrl !== baseUrl && currentUrl !== baseUrl + '/') { // Use history.pushState to avoid full page reload window.history.pushState({}, '', '/'); // Trigger a popstate event to simulate navigation window.dispatchEvent(new PopStateEvent('popstate')); await sleep(1500); return true; } log('Already on home page or all methods failed', 'warning'); return false; } catch (error) { log(`Error starting new chat: ${error.message}`, 'error'); return false; } }; // Function to update dynamic elements in real-time const updateDynamicElementsDisplay = (remainingElements) => { if (dynamicElementsInput && autoRemoveProcessed) { try { const newValue = JSON.stringify(remainingElements, null, 2); dynamicElementsInput.value = newValue; // Persist queue text so we can resume after refresh GM_setValue(STORAGE_KEYS.dynamicElementsInput, newValue); log(`Updated queue: ${remainingElements.length} items remaining`); } catch (error) { log(`Error updating display: ${error.message}`, 'warning'); } } }; // ChatGPT interaction functions const getChatInput = () => { const selectors = [ '#prompt-textarea', 'div[contenteditable="true"]', 'textarea[placeholder*="Message"]', 'div.ProseMirror' ]; for (const selector of selectors) { const element = document.querySelector(selector); if (element && element.isContentEditable !== false) { return element; } } return null; }; const getSendButton = () => { const selectors = [ '#composer-submit-button', 'button[data-testid="send-button"]', 'button[aria-label*="Send"]', 'button[aria-label*="submit"]' ]; for (const selector of selectors) { const button = document.querySelector(selector); if (button && !button.disabled) { return button; } } return null; }; const typeMessage = async (message) => { const input = getChatInput(); if (!input) { throw new Error('Chat input not found'); } // Clear existing content if (input.tagName === 'DIV') { input.innerHTML = ''; input.focus(); // For contenteditable divs, we need to insert text properly const paragraph = document.createElement('p'); paragraph.textContent = message; input.appendChild(paragraph); // Trigger input events input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); } else { input.value = message; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); } await sleep(100); log(`Message typed: "${message.substring(0, 50)}${message.length > 50 ? '...' : ''}"`); }; const sendMessage = async () => { const sendButton = getSendButton(); if (!sendButton || sendButton.disabled) { throw new Error('Send button not available'); } sendButton.click(); log('Message sent'); await sleep(500); }; const waitForResponse = async () => { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { if (responseObserver) { responseObserver.disconnect(); } reject(new Error('Response timeout')); }, CONFIG.RESPONSE_TIMEOUT); const checkForNewResponse = () => { // Look for the latest assistant message const assistantMessages = document.querySelectorAll('[data-message-author-role="assistant"]'); const latestMessage = assistantMessages[assistantMessages.length - 1]; if (latestMessage && latestMessage !== lastResponseElement) { // Check if the message is complete (not generating) const isGenerating = document.querySelector('[data-testid="stop-button"]') || document.querySelector('.result-thinking') || latestMessage.querySelector('.typing-indicator'); if (!isGenerating) { clearTimeout(timeout); if (responseObserver) { responseObserver.disconnect(); } lastResponseElement = latestMessage; resolve(latestMessage); } } }; // Initial check checkForNewResponse(); // Set up observer for DOM changes responseObserver = new MutationObserver(() => { checkForNewResponse(); }); responseObserver.observe(document.body, { childList: true, subtree: true, characterData: true }); }); }; const extractResponseText = (responseElement) => { if (!responseElement) return ''; // Try different selectors for response content const contentSelectors = [ '.markdown', '.prose', '[data-message-id]', '.whitespace-pre-wrap' ]; for (const selector of contentSelectors) { const contentElement = responseElement.querySelector(selector); if (contentElement) { return contentElement.textContent.trim(); } } return responseElement.textContent.trim(); }; // Main automation function with batch processing const processMessage = async (message, customCode = '', isTemplate = false) => { if (isProcessing && !isLooping) { log('Already processing a message', 'warning'); return; } if (!isLooping) { isProcessing = true; currentBatchIndex = 0; } updateStatus('processing'); try { let messagesToProcess = []; if (isTemplate && dynamicElements.length > 0) { // Process template with dynamic elements messagesToProcess = dynamicElements.map((element, index) => ({ message: processDynamicTemplate(message, { item: element, index: index + 1, total: dynamicElements.length }), customCode, elementData: element, index: index + 1 })); if (CONFIG.DEBUG_MODE) { log(`Processing ${messagesToProcess.length} elements. First element: ${JSON.stringify(dynamicElements[0])}`); } } else { messagesToProcess = [{ message, customCode }]; } for (let i = 0; i < messagesToProcess.length; i++) { const { message: processedMessage, customCode: code, elementData, index } = messagesToProcess[i]; updateProgress(i + 1, messagesToProcess.length); if (isTemplate) { log(`Processing item ${index}/${messagesToProcess.length}: ${JSON.stringify(elementData)}`); } let success = false; let retryCount = 0; const maxRetries = 3; while (!success && retryCount <= maxRetries) { try { if (retryCount > 0) { log(`Retry attempt ${retryCount} for item ${index}...`); await sleep(batchWaitTime); // Wait before retry } // Start new chat if option is enabled and not the first item if (newChatPerItem && (i > 0 || retryCount > 0)) { const chatSuccess = await startNewChat(); if (!chatSuccess) { log('Failed to start new chat, continuing in current chat', 'warning'); } await sleep(1000); // Additional wait after new chat } log(`Starting message processing...`); // Type the message await typeMessage(processedMessage); await sleep(500); // Send the message await sendMessage(); updateStatus('waiting'); // Wait for response log('Waiting for ChatGPT response...'); const responseElement = await waitForResponse(); const responseText = extractResponseText(responseElement); log('Response received'); console.log('ChatGPT Response:', responseText); // Execute custom code if provided if (code && code.trim() !== '') { if (isTemplate) { try { log(`Custom code context -> index: ${index ?? 'null'}/${messagesToProcess.length}, item: ${elementData ? JSON.stringify(elementData).slice(0,200) : 'null'}`); } catch { /* no-op */ } } log('Executing custom code...'); await executeCustomCode(code, responseText, { elementData, index, total: messagesToProcess.length }); } // Item processed successfully - remove from queue text if auto-remove is enabled if (isTemplate && autoRemoveProcessed) { const idx = dynamicElements.indexOf(elementData); if (idx >= 0) { dynamicElements.splice(idx, 1); updateDynamicElementsDisplay(dynamicElements); } } log(`Item ${index} processed successfully`); success = true; } catch (itemError) { retryCount++; log(`Error processing item ${index} (attempt ${retryCount}): ${itemError.message}`, 'error'); if (retryCount > maxRetries) { log(`Item ${index} failed after ${maxRetries} retries, skipping...`, 'error'); } } } // Add delay between batch items (user configurable) if (i < messagesToProcess.length - 1) { log(`Waiting ${batchWaitTime}ms before next item...`); await sleep(batchWaitTime); } // Check if loop should continue if (!isLooping) break; } updateStatus('complete'); log('Message processing completed'); updateProgress(0, 0); // Reset progress } catch (error) { log(`Batch error: ${error.message}`, 'error'); updateStatus('error'); updateProgress(0, 0); } finally { if (!isLooping) { isProcessing = false; currentBatchIndex = 0; } setTimeout(() => updateStatus('idle'), 2000); } }; // Stop batch processing const stopBatchProcessing = () => { isLooping = false; isProcessing = false; currentBatchIndex = 0; updateStatus('idle'); updateProgress(0, 0); log('Batch processing stopped'); }; // Update progress bar const updateProgress = (current, total) => { if (!progressBar) return; if (total === 0) { progressBar.style.display = 'none'; return; } progressBar.style.display = 'block'; const percentage = (current / total) * 100; progressBar.querySelector('.progress-fill').style.width = `${percentage}%`; progressBar.querySelector('.progress-text').textContent = `${current}/${total}`; }; // UI Creation const createUI = () => { isDarkMode = detectDarkMode(); // Main container mainContainer = document.createElement('div'); mainContainer.id = 'chatgpt-automation-ui'; mainContainer.className = isDarkMode ? 'dark-mode' : 'light-mode'; mainContainer.innerHTML = ` <div class="automation-header" id="automation-header"> <h3>ChatGPT Automation Pro</h3> <div class="header-controls"> <div class="status-indicator" id="status-indicator"> <span class="status-dot"></span> <span class="status-text">Ready</span> </div> <button class="header-btn" id="minimize-btn" title="Minimize" aria-label="Minimize"> <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> <path d="M6 12h12v2H6z"/> </svg> </button> <button class="header-btn" id="close-btn" title="Close" aria-label="Close"> <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> <path d="M18.3 5.71L12 12.01L5.7 5.71L4.29 7.12L10.59 13.42L4.29 19.72L5.7 21.13L12 14.83L18.3 21.13L19.71 19.72L13.41 13.42L19.71 7.12L18.3 5.71Z"/> </svg> </button> </div> </div> <div class="automation-content" id="automation-content"> <div class="progress-container" id="progress-container" style="display: none;"> <div class="progress-bar"> <div class="progress-fill"></div> </div> <div class="progress-text">0/0</div> </div> <div class="automation-form"> <div class="tab-container"> <button class="tab-btn active" data-tab="simple">Simple</button> <button class="tab-btn" data-tab="template">Template</button> <button class="tab-btn" data-tab="advanced">Response (JS)</button> <button class="tab-btn" data-tab="settings">Settings</button> </div> <div class="tab-content active" id="simple-tab"> <div class="form-group"> <label for="message-input">Message:</label> <textarea id="message-input" placeholder="Enter your message for ChatGPT..." rows="3"></textarea> </div> </div> <div class="tab-content" id="template-tab"> <div class="form-group"> <label for="template-input">Message Template:</label> <textarea id="template-input" placeholder="Template with placeholders like {{item}}, {{index}}, {{total}} or {item.name}..." rows="3"></textarea> <div class="help-text">Use {{item}} / {item}, {{index}} / {index}, {{total}} / {total}. Nested paths supported, e.g. {item.name} or {{item.orderId}}</div> </div> <div class="form-group"> <label for="dynamic-elements-input">Dynamic Elements (JSON array or function):</label> <div class="code-editor"> <textarea id="dynamic-elements-input" placeholder='["item1", "item2", "item3"] or () => ["generated", "items"]' rows="4"></textarea> <div class="editor-tools"> <button class="tool-btn" id="format-json-btn" title="Format JSON">{ }</button> <button class="tool-btn" id="validate-elements-btn" title="Validate">✓</button> <button class="tool-btn" id="elements-syntax-check-btn" title="Check JS">JS</button> <button class="tool-btn" id="elements-insert-fn-btn" title="Insert Snippet">📝</button> </div> </div> </div> <div class="batch-controls"> <div class="batch-settings"> <label class="checkbox-label"> <input type="checkbox" id="loop-checkbox"> <span class="checkmark"></span> Process all items in batch </label> <label class="checkbox-label"> <input type="checkbox" id="auto-remove-checkbox" checked> <span class="checkmark"></span> Remove processed items from queue </label> <label class="checkbox-label"> <input type="checkbox" id="new-chat-checkbox"> <span class="checkmark"></span> Start new chat for each item </label> <div class="wait-time-control"> <label for="wait-time-input">Wait between items (ms):</label> <input type="number" id="wait-time-input" min="100" max="30000" value="2000" step="100"> </div> </div> <div class="batch-actions"> <button id="stop-batch-btn" class="btn btn-danger" style="display: none;">Stop Batch</button> </div> </div> </div> <div class="tab-content" id="advanced-tab"> <div class="form-group"> <label for="custom-code-input">Custom Code (JavaScript):</label> <div class="code-editor"> <textarea id="custom-code-input" placeholder="// Custom code to run after response (optional) // Available variables: response, log, console, item, index, total, http // http: cross-origin helper (GM_xmlhttpRequest) // await http.postForm('https://api.example.com/submit', { foo: 'bar' }) // Example: log('Response length: ' + response.length);" rows="6"></textarea> <div class="editor-tools"> <button class="tool-btn" id="syntax-check-btn" title="Check Syntax">JS</button> <button class="tool-btn" id="insert-template-btn" title="Insert Template">📝</button> </div> </div> <div class="help-text">Runs your JavaScript after ChatGPT finishes. Use <code>response</code> (string), <code>log()</code>, and <code>http</code> (CORS-capable) to integrate with any website's API.</div> </div> </div> <div class="tab-content" id="settings-tab"> <div class="form-group"> <label>Debug mode:</label> <label class="checkbox-label"> <input type="checkbox" id="debug-mode-checkbox"> <span class="checkmark"></span> Enable debug logging </label> </div> <div class="form-group"> <label for="response-timeout-input">Response timeout (ms):</label> <input type="number" id="response-timeout-input" min="10000" max="6000000" step="1000" class="settings-input timeout"> </div> <div class="form-group"> <label>Panel size limits (px):</label> <div class="size-inputs-grid"> <div class="size-input-group"> <label for="min-width-input">Min width</label> <input type="number" id="min-width-input" min="200" max="1200" step="10" class="settings-input size"> </div> <div class="size-input-group"> <label for="min-height-input">Min height</label> <input type="number" id="min-height-input" min="120" max="1200" step="10" class="settings-input size"> </div> <div class="size-input-group"> <label for="max-width-input">Max width</label> <input type="number" id="max-width-input" min="200" max="2000" step="10" class="settings-input size"> </div> <div class="size-input-group"> <label for="max-height-input">Max height</label> <input type="number" id="max-height-input" min="120" max="2000" step="10" class="settings-input size"> </div> </div> </div> <div class="form-group"> <label>Visibility:</label> <label class="checkbox-label"> <input type="checkbox" id="default-visible-checkbox"> <span class="checkmark"></span> Show panel by default </label> <div class="help-text">Controls default visibility on page load. You can still toggle from the header button.</div> </div> </div> <div class="form-actions"> <button id="send-btn" class="btn btn-primary"> <span class="btn-text">Send Message</span> <span class="btn-loader" style="display: none;"> <div class="spinner"></div> </span> </button> <button id="clear-btn" class="btn btn-secondary">Clear</button> <button id="toggle-log-btn" class="btn btn-secondary">Toggle Log</button> </div> </div> <div class="automation-log" id="log-container" style="display: none;"> <div class="log-header"> <span>Activity Log</span> <div class="log-header-controls"> <button class="tool-btn" id="toggle-auto-scroll-btn" title="Toggle Auto-scroll">📜</button> <button class="tool-btn" id="clear-log-btn" title="Clear Log">🗑️</button> </div> </div> <div class="log-content"></div> </div> </div> <div class="resize-handle" id="resize-handle"></div> `; // Add styles with ChatGPT-inspired design (guard against duplicates) let style = document.getElementById('chatgpt-automation-style'); if (!style) { style = document.createElement('style'); style.id = 'chatgpt-automation-style'; style.textContent = ` /* Base styles that adapt to ChatGPT's theme (scoped) */ #chatgpt-automation-ui { position: fixed; top: 20px; right: 20px; width: 380px; min-width: ${CONFIG.MIN_WIDTH}px; max-width: ${CONFIG.MAX_WIDTH}px; min-height: ${CONFIG.MIN_HEIGHT}px; max-height: ${CONFIG.MAX_HEIGHT}px; background: var(--main-surface-primary, #ffffff); border: 1px solid var(--border-medium, rgba(0,0,0,0.1)); border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); z-index: 10000; resize: both; overflow: hidden; backdrop-filter: blur(10px); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } #chatgpt-automation-ui.dark-mode { background: var(--main-surface-primary, #2d2d30); border-color: var(--border-medium, rgba(255,255,255,0.1)); color: var(--text-primary, #ffffff); } #chatgpt-automation-ui.minimized { resize: none; height: auto !important; min-height: auto !important; } #chatgpt-automation-ui.minimized .automation-content { max-height: 250px; } #chatgpt-automation-ui.minimized .progress-container, #chatgpt-automation-ui.minimized .automation-form { display: none; } #chatgpt-automation-ui.minimized .automation-log { display: block !important; } #chatgpt-automation-ui.minimized .log-content { height: 120px; min-height: 80px; max-height: 400px; overflow-y: auto; resize: vertical; border-bottom: 2px solid var(--border-light, rgba(0,0,0,0.1)); } #chatgpt-automation-ui.dark-mode.minimized .log-content { border-bottom-color: var(--border-light, rgba(255,255,255,0.1)); } #chatgpt-automation-ui.minimized .automation-header { position: sticky; top: 0; z-index: 1; } #chatgpt-automation-ui .automation-header { background: linear-gradient(135deg, var(--brand-purple, #6366f1) 0%, var(--brand-purple-darker, #4f46e5) 100%); color: white; padding: 12px 16px; border-radius: 12px 12px 0 0; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; } #chatgpt-automation-ui .automation-header h3 { margin: 0; font-size: 15px; font-weight: 600; flex: 1; } #chatgpt-automation-ui .header-controls { display: flex; align-items: center; gap: 12px; } #chatgpt-automation-ui .status-indicator { display: flex; align-items: center; gap: 6px; font-size: 11px; opacity: 0.9; } #chatgpt-automation-ui .status-dot { width: 6px; height: 6px; border-radius: 50%; background: #10b981; animation: pulse-idle 2s infinite; } #chatgpt-automation-ui .header-btn { background: rgba(255, 255, 255, 0.1); border: none; border-radius: 4px; padding: 4px; color: white; cursor: pointer; transition: background 0.2s; display: flex; align-items: center; justify-content: center; } #chatgpt-automation-ui .header-btn:hover { background: rgba(255, 255, 255, 0.2); } #chatgpt-automation-ui .automation-content { max-height: calc(100% - 60px); } #chatgpt-automation-ui .progress-container { padding: 12px 16px; border-bottom: 1px solid var(--border-light, rgba(0,0,0,0.06)); background: var(--surface-secondary, #f8fafc); } #chatgpt-automation-ui.dark-mode .progress-container { background: var(--surface-secondary, #1e1e20); border-color: var(--border-light, rgba(255,255,255,0.06)); } #chatgpt-automation-ui .progress-bar { width: 100%; height: 4px; background: var(--border-light, rgba(0,0,0,0.1)); border-radius: 2px; overflow: hidden; margin-bottom: 4px; } #chatgpt-automation-ui .progress-fill { height: 100%; background: var(--brand-purple, #6366f1); transition: width 0.3s ease; } #chatgpt-automation-ui .progress-text { font-size: 11px; color: var(--text-secondary, #6b7280); text-align: center; } #chatgpt-automation-ui .automation-form { padding: 16px; } #chatgpt-automation-ui .tab-container { display: flex; border-bottom: 1px solid var(--border-light, rgba(0,0,0,0.06)); margin-bottom: 16px; } #chatgpt-automation-ui.dark-mode .tab-container { border-color: var(--border-light, rgba(255,255,255,0.06)); } #chatgpt-automation-ui .tab-btn { background: none; border: none; padding: 8px 16px; cursor: pointer; color: var(--text-secondary, #6b7280); font-size: 13px; font-weight: 500; border-bottom: 2px solid transparent; transition: all 0.2s; } #chatgpt-automation-ui .tab-btn.active { color: var(--brand-purple, #6366f1); border-color: var(--brand-purple, #6366f1); } #chatgpt-automation-ui .tab-content { display: none; } #chatgpt-automation-ui .tab-content.active { display: block; } #chatgpt-automation-ui .form-group { margin-bottom: 16px; } #chatgpt-automation-ui .form-group label { display: block; margin-bottom: 6px; font-weight: 500; color: var(--text-primary, #374151); font-size: 13px; } #chatgpt-automation-ui.dark-mode .form-group label { color: var(--text-primary, #f3f4f6); } #chatgpt-automation-ui .form-group textarea { width: 100%; padding: 10px 12px; border: 1px solid var(--border-medium, rgba(0,0,0,0.1)); border-radius: 8px; font-size: 13px; resize: vertical; font-family: 'SF Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; box-sizing: border-box; background: var(--input-background, #ffffff); color: var(--text-primary, #374151); transition: border-color 0.2s, box-shadow 0.2s; } #chatgpt-automation-ui.dark-mode .form-group textarea { background: var(--input-background, #1e1e20); color: var(--text-primary, #f3f4f6); border-color: var(--border-medium, rgba(255,255,255,0.1)); } #chatgpt-automation-ui .form-group textarea:focus { outline: none; border-color: var(--brand-purple, #6366f1); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); } #chatgpt-automation-ui .code-editor { position: relative; } #chatgpt-automation-ui .editor-tools { position: absolute; top: 8px; right: 8px; display: flex; gap: 4px; opacity: 0; transition: opacity 0.2s; } #chatgpt-automation-ui .code-editor:hover .editor-tools { opacity: 1; } #chatgpt-automation-ui .tool-btn { background: var(--surface-secondary, rgba(0,0,0,0.05)); border: none; border-radius: 4px; padding: 4px 6px; font-size: 10px; cursor: pointer; color: var(--text-secondary, #6b7280); transition: background 0.2s; } #chatgpt-automation-ui .tool-btn:hover { background: var(--surface-secondary, rgba(0,0,0,0.1)); } #chatgpt-automation-ui .help-text { font-size: 11px; color: var(--text-secondary, #6b7280); margin-top: 4px; font-style: italic; } #chatgpt-automation-ui .batch-controls { margin-top: 12px; padding: 12px; background: var(--surface-secondary, #f8fafc); border-radius: 6px; } #chatgpt-automation-ui.dark-mode .batch-controls { background: var(--surface-secondary, #1e1e20); } #chatgpt-automation-ui .batch-settings { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; } #chatgpt-automation-ui .batch-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; } #chatgpt-automation-ui .wait-time-control { display: flex; align-items: center; gap: 8px; margin-top: 4px; } #chatgpt-automation-ui .wait-time-control label { font-size: 12px; margin: 0; white-space: nowrap; color: var(--text-primary, #374151); } #chatgpt-automation-ui.dark-mode .wait-time-control label { color: var(--text-primary, #f3f4f6); } #chatgpt-automation-ui .wait-time-control input[type="number"] { width: 80px; padding: 4px 8px; border: 1px solid var(--border-medium, rgba(0,0,0,0.1)); border-radius: 4px; font-size: 12px; background: var(--input-background, #ffffff); color: var(--text-primary, #374151); } #chatgpt-automation-ui.dark-mode .wait-time-control input[type="number"] { background: var(--input-background, #1e1e20); color: var(--text-primary, #f3f4f6); border-color: var(--border-medium, rgba(255,255,255,0.1)); } #chatgpt-automation-ui .wait-time-control input[type="number"]:focus { outline: none; border-color: var(--brand-purple, #6366f1); box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1); } /* Settings input styles */ #chatgpt-automation-ui .settings-input { padding: 6px 8px; border: 1px solid var(--border-medium, rgba(0,0,0,0.1)); border-radius: 6px; font-size: 13px; background: var(--input-background, #ffffff); color: var(--text-primary, #374151); } #chatgpt-automation-ui.dark-mode .settings-input { background: var(--input-background, #1e1e20); color: var(--text-primary, #f3f4f6); border-color: var(--border-medium, rgba(255,255,255,0.1)); } #chatgpt-automation-ui .settings-input:focus { outline: none; border-color: var(--brand-purple, #6366f1); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); } #chatgpt-automation-ui .settings-input.timeout { width: 140px; } #chatgpt-automation-ui .size-inputs-grid { display: flex; gap: 8px; flex-wrap: wrap; } #chatgpt-automation-ui .size-input-group { display: flex; flex-direction: column; gap: 4px; } #chatgpt-automation-ui .size-input-group label { font-size: 12px; margin: 0; color: var(--text-primary, #374151); } #chatgpt-automation-ui.dark-mode .size-input-group label { color: var(--text-primary, #f3f4f6); } #chatgpt-automation-ui .settings-input.size { width: 120px; } #chatgpt-automation-ui .checkbox-label { display: flex; align-items: center; cursor: pointer; font-size: 13px; color: var(--text-primary, #374151); } #chatgpt-automation-ui.dark-mode .checkbox-label { color: var(--text-primary, #f3f4f6); } #chatgpt-automation-ui .checkbox-label input[type="checkbox"] { margin-right: 8px; } #chatgpt-automation-ui .form-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; } #chatgpt-automation-ui .btn { padding: 8px 16px; border: none; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 6px; position: relative; } #chatgpt-automation-ui .btn-primary { background: var(--brand-purple, #6366f1); color: white; } #chatgpt-automation-ui .btn-primary:hover { background: var(--brand-purple-darker, #4f46e5); } #chatgpt-automation-ui .btn-secondary { background: var(--surface-secondary, #f3f4f6); color: var(--text-primary, #374151); border: 1px solid var(--border-light, rgba(0,0,0,0.06)); } #chatgpt-automation-ui.dark-mode .btn-secondary { background: var(--surface-secondary, #1e1e20); color: var(--text-primary, #f3f4f6); border-color: var(--border-light, rgba(255,255,255,0.06)); } #chatgpt-automation-ui .btn-secondary:hover { background: var(--surface-secondary, #e5e7eb); } #chatgpt-automation-ui.dark-mode .btn-secondary:hover { background: var(--surface-secondary, #2a2a2d); } #chatgpt-automation-ui .btn-danger { background: #ef4444; color: white; } #chatgpt-automation-ui .btn-danger:hover { background: #dc2626; } #chatgpt-automation-ui .btn-warning { background: #f59e0b; color: white; } #chatgpt-automation-ui .btn-warning:hover { background: #d97706; } #chatgpt-automation-ui .btn:disabled { opacity: 0.5; cursor: not-allowed; } #chatgpt-automation-ui .spinner { width: 12px; height: 12px; border: 2px solid rgba(255, 255, 255, 0.3); border-top: 2px solid white; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } #chatgpt-automation-ui .automation-log { border-top: 1px solid var(--border-light, rgba(0,0,0,0.06)); } #chatgpt-automation-ui.dark-mode .automation-log { border-color: var(--border-light, rgba(255,255,255,0.06)); } #chatgpt-automation-ui .log-header { padding: 10px 16px; background: var(--surface-secondary, #f8fafc); font-weight: 500; font-size: 13px; color: var(--text-primary, #374151); display: flex; justify-content: space-between; align-items: center; } #chatgpt-automation-ui.dark-mode .log-header { background: var(--surface-secondary, #1e1e20); color: var(--text-primary, #f3f4f6); } #chatgpt-automation-ui .log-header-controls { display: flex; gap: 4px; } #chatgpt-automation-ui .log-content { padding: 12px; overflow-y: auto; scroll-behavior: smooth; } #chatgpt-automation-ui .log-entry { padding: 4px 0; font-size: 11px; font-family: 'SF Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; border-bottom: 1px solid var(--border-light, rgba(0,0,0,0.03)); line-height: 1.4; } #chatgpt-automation-ui .log-entry:last-child { border-bottom: none; } #chatgpt-automation-ui .log-info { color: var(--text-primary, #374151); } #chatgpt-automation-ui.dark-mode .log-info { color: var(--text-primary, #d1d5db); } #chatgpt-automation-ui .log-warning { color: #f59e0b; } #chatgpt-automation-ui .log-error { color: #ef4444; } #chatgpt-automation-ui .resize-handle { position: absolute; bottom: 0; right: 0; width: 20px; height: 20px; cursor: nw-resize; background: linear-gradient(-45deg, transparent 0%, transparent 40%, var(--border-medium, rgba(0,0,0,0.1)) 40%, var(--border-medium, rgba(0,0,0,0.1)) 60%, transparent 60%, transparent 100%); } /* Responsive design */ @media (max-width: 768px) { #chatgpt-automation-ui { width: 320px; right: 10px; top: 10px; } } /* Animation keyframes */ @keyframes pulse-idle { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } @keyframes pulse-processing { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(1.2); } } #chatgpt-automation-ui .status-processing .status-dot { background: #f59e0b; animation: pulse-processing 1s infinite; } #chatgpt-automation-ui .status-waiting .status-dot { background: #3b82f6; animation: pulse-processing 1.5s infinite; } #chatgpt-automation-ui .status-complete .status-dot { background: #10b981; animation: none; } #chatgpt-automation-ui .status-error .status-dot { background: #ef4444; animation: pulse-processing 0.5s infinite; } `; document.head.appendChild(style); } document.body.appendChild(mainContainer); // Get UI elements messageInput = document.getElementById('message-input'); customCodeInput = document.getElementById('custom-code-input'); templateInput = document.getElementById('template-input'); dynamicElementsInput = document.getElementById('dynamic-elements-input'); statusIndicator = document.getElementById('status-indicator'); logContainer = document.querySelector('.log-content'); progressBar = document.getElementById('progress-container'); resizeHandle = document.getElementById('resize-handle'); // Restore saved inputs, toggles and config try { // Textareas messageInput.value = GM_getValue(STORAGE_KEYS.messageInput, '') || ''; templateInput.value = GM_getValue(STORAGE_KEYS.templateInput, '') || ''; const savedDyn = GM_getValue(STORAGE_KEYS.dynamicElementsInput, ''); if (typeof savedDyn === 'string') dynamicElementsInput.value = savedDyn; customCodeInput.value = GM_getValue(STORAGE_KEYS.customCodeInput, '') || ''; // Checkboxes and switches const loopEl = document.getElementById('loop-checkbox'); const autoRemoveEl = document.getElementById('auto-remove-checkbox'); const newChatEl = document.getElementById('new-chat-checkbox'); if (loopEl) { loopEl.checked = !!GM_getValue(STORAGE_KEYS.loop, false); isLooping = loopEl.checked; } if (autoRemoveEl) { autoRemoveEl.checked = GM_getValue(STORAGE_KEYS.autoRemove, true); autoRemoveProcessed = autoRemoveEl.checked; } if (newChatEl) { newChatEl.checked = !!GM_getValue(STORAGE_KEYS.newChat, false); newChatPerItem = newChatEl.checked; } // Auto-scroll state (button only, no checkbox) autoScrollLogs = GM_getValue(STORAGE_KEYS.autoScroll, true); // Wait time const waitInput = document.getElementById('wait-time-input'); const savedWait = parseInt(GM_getValue(STORAGE_KEYS.waitTime, batchWaitTime)); if (!Number.isNaN(savedWait)) { batchWaitTime = savedWait; if (waitInput) waitInput.value = String(savedWait); } // Active tab const savedTab = GM_getValue(STORAGE_KEYS.activeTab, 'simple'); const tabBtn = document.querySelector(`.tab-btn[data-tab="${savedTab}"]`); if (tabBtn) tabBtn.click(); // Restore saved log content height const savedLogHeight = parseInt(GM_getValue(STORAGE_KEYS.logHeight, 120)); if (!Number.isNaN(savedLogHeight)) { const logContent = document.querySelector('.log-content'); if (logContent) { logContent.style.height = `${savedLogHeight}px`; } } // Config - apply saved values and reflect in UI const dbgVal = !!GM_getValue(STORAGE_KEYS.configDebug, CONFIG.DEBUG_MODE); CONFIG.DEBUG_MODE = dbgVal; const dbgEl = document.getElementById('debug-mode-checkbox'); if (dbgEl) dbgEl.checked = dbgVal; const toVal = parseInt(GM_getValue(STORAGE_KEYS.configTimeout, CONFIG.RESPONSE_TIMEOUT)); if (!Number.isNaN(toVal)) CONFIG.RESPONSE_TIMEOUT = toVal; const toEl = document.getElementById('response-timeout-input'); if (toEl) toEl.value = String(CONFIG.RESPONSE_TIMEOUT); const minW = parseInt(GM_getValue(STORAGE_KEYS.configMinWidth, CONFIG.MIN_WIDTH)); const minH = parseInt(GM_getValue(STORAGE_KEYS.configMinHeight, CONFIG.MIN_HEIGHT)); const maxW = parseInt(GM_getValue(STORAGE_KEYS.configMaxWidth, CONFIG.MAX_WIDTH)); const maxH = parseInt(GM_getValue(STORAGE_KEYS.configMaxHeight, CONFIG.MAX_HEIGHT)); if (!Number.isNaN(minW)) CONFIG.MIN_WIDTH = minW; if (!Number.isNaN(minH)) CONFIG.MIN_HEIGHT = minH; if (!Number.isNaN(maxW)) CONFIG.MAX_WIDTH = maxW; if (!Number.isNaN(maxH)) CONFIG.MAX_HEIGHT = maxH; const minWEl = document.getElementById('min-width-input'); const minHEl = document.getElementById('min-height-input'); const maxWEl = document.getElementById('max-width-input'); const maxHEl = document.getElementById('max-height-input'); if (minWEl) minWEl.value = String(CONFIG.MIN_WIDTH); if (minHEl) minHEl.value = String(CONFIG.MIN_HEIGHT); if (maxWEl) maxWEl.value = String(CONFIG.MAX_WIDTH); if (maxHEl) maxHEl.value = String(CONFIG.MAX_HEIGHT); // Override CSS min/max with inline styles so changes take effect immediately mainContainer.style.minWidth = CONFIG.MIN_WIDTH + 'px'; mainContainer.style.minHeight = CONFIG.MIN_HEIGHT + 'px'; mainContainer.style.maxWidth = CONFIG.MAX_WIDTH + 'px'; mainContainer.style.maxHeight = CONFIG.MAX_HEIGHT + 'px'; const defVis = !!GM_getValue(STORAGE_KEYS.configDefaultVisible, CONFIG.DEFAULT_VISIBLE); CONFIG.DEFAULT_VISIBLE = defVis; const dvEl = document.getElementById('default-visible-checkbox'); if (dvEl) dvEl.checked = defVis; } catch {} // Load saved state const savedState = loadUIState(); if (savedState.left) { mainContainer.style.left = savedState.left; mainContainer.style.right = 'auto'; } if (savedState.top) { mainContainer.style.top = savedState.top; } if (savedState.width) { mainContainer.style.width = savedState.width; } if (savedState.height) { mainContainer.style.height = savedState.height; } if (savedState.minimized) { isMinimized = true; mainContainer.classList.add('minimized'); } if (typeof savedState.visible === 'boolean') { uiVisible = savedState.visible; } // Default hidden based on persisted/CONFIG if (!uiVisible) { mainContainer.style.display = 'none'; } // Bind events bindEvents(); // Initialize auto-scroll button state const autoScrollBtn = document.getElementById('toggle-auto-scroll-btn'); if (autoScrollBtn) { autoScrollBtn.style.opacity = autoScrollLogs ? '1' : '0.5'; autoScrollBtn.title = autoScrollLogs ? 'Auto-scroll: ON' : 'Auto-scroll: OFF'; } // Watch for theme changes const observer = new MutationObserver(() => { const newDarkMode = detectDarkMode(); if (newDarkMode !== isDarkMode) { isDarkMode = newDarkMode; mainContainer.className = isDarkMode ? 'dark-mode' : 'light-mode'; if (isMinimized) mainContainer.classList.add('minimized'); } }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] }); observer.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] }); // Add persistent header launcher mountHeaderLauncher(); startHeaderObserver(); log('UI initialized successfully'); // Auto-resize container to fit initial content setTimeout(() => autoResizeContainer(), 200); }; // Header launcher utilities const createLauncherButton = () => { const btn = document.createElement('button'); btn.id = 'chatgpt-automation-launcher'; btn.type = 'button'; btn.title = 'Open Automation'; btn.setAttribute('aria-label', 'Open Automation'); btn.className = 'btn relative btn-ghost text-token-text-primary'; btn.innerHTML = `<div class="flex w-full items-center justify-center gap-1.5"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="20" height="20" fill="currentColor" class="icon"><path d="M273 151.1L288 171.8L303 151.1C328 116.5 368.2 96 410.9 96C484.4 96 544 155.6 544 229.1L544 231.7C544 249.3 540.6 267.3 534.5 285.4C512.7 276.8 488.9 272 464 272C358 272 272 358 272 464C272 492.5 278.2 519.6 289.4 544C288.9 544 288.5 544 288 544C272.5 544 257.2 539.4 244.9 529.9C171.9 474.2 32 343.9 32 231.7L32 229.1C32 155.6 91.6 96 165.1 96C207.8 96 248 116.5 273 151.1zM320 464C320 384.5 384.5 320 464 320C543.5 320 608 384.5 608 464C608 543.5 543.5 608 464 608C384.5 608 320 543.5 320 464zM497.4 387C491.6 382.8 483.6 383 478 387.5L398 451.5C392.7 455.7 390.6 462.9 392.9 469.3C395.2 475.7 401.2 480 408 480L440.9 480L425 522.4C422.5 529.1 424.8 536.7 430.6 541C436.4 545.3 444.4 545 450 540.5L530 476.5C535.3 472.3 537.4 465.1 535.1 458.7C532.8 452.3 526.8 448 520 448L487.1 448L503 405.6C505.5 398.9 503.2 391.3 497.4 387z"/></svg><span class="max-md:hidden">Automation</span></div>`; btn.addEventListener('click', () => { // If UI was removed by a re-render, recreate it let ui = document.getElementById('chatgpt-automation-ui'); if (!ui) { createUI(); ui = document.getElementById('chatgpt-automation-ui'); } if (!ui) return; const show = ui.style.display === 'none'; ui.style.display = show ? 'block' : 'none'; mainContainer = ui; uiVisible = show; saveUIState(); }); return btn; }; const mountHeaderLauncher = () => { const header = document.getElementById('page-header'); if (!header) return false; let target = header.querySelector('#conversation-header-actions'); if (!target) target = header; if (!target.querySelector('#chatgpt-automation-launcher')) { const btn = createLauncherButton(); target.appendChild(btn); } // Also ensure the UI exists if it should be visible const savedState = loadUIState(); const shouldShow = savedState.visible === true || CONFIG.DEFAULT_VISIBLE; if (shouldShow && !document.getElementById('chatgpt-automation-ui')) { createUI(); } return true; }; const startHeaderObserver = () => { if (headerObserverStarted) return; headerObserverStarted = true; const ensure = () => { try { // Recreate launcher if missing mountHeaderLauncher(); // Ensure UI matches persisted visibility const savedState = loadUIState(); const ui = document.getElementById('chatgpt-automation-ui'); const shouldShow = savedState.visible === true || CONFIG.DEFAULT_VISIBLE; if (ui) { ui.style.display = shouldShow ? 'block' : 'none'; } else if (shouldShow) { createUI(); } } catch (e) { /* noop */ } }; ensure(); const obs = new MutationObserver(() => ensure()); obs.observe(document.body, { childList: true, subtree: true }); }; const updateStatus = (status) => { if (!statusIndicator) return; const statusTexts = { idle: 'Ready', processing: 'Typing...', waiting: 'Waiting for response...', complete: 'Complete', error: 'Error' }; statusIndicator.className = `status-indicator status-${status}`; statusIndicator.querySelector('.status-text').textContent = statusTexts[status] || 'Unknown'; }; // Auto-resize container to fit content const autoResizeContainer = () => { if (!mainContainer || isMinimized) return; try { // Get the automation content container const contentContainer = document.querySelector('#automation-content'); if (!contentContainer) return; // Temporarily remove height constraints to measure natural height const originalHeight = mainContainer.style.height; const originalMaxHeight = mainContainer.style.maxHeight; mainContainer.style.height = 'auto'; mainContainer.style.maxHeight = 'none'; // Force layout recalculation contentContainer.style.height = 'auto'; // Wait for next frame to get accurate measurements requestAnimationFrame(() => { const contentHeight = contentContainer.scrollHeight; const headerHeight = 60; // Header height const logHeaderHeight = 45; // Log header when visible const padding = 20; // Some padding let targetHeight = contentHeight + headerHeight + padding; // Add log header height if log is visible const logContainer = document.getElementById('log-container'); if (logContainer && logContainer.style.display !== 'none') { targetHeight += logHeaderHeight; } // Apply min/max constraints targetHeight = Math.max(targetHeight, CONFIG.MIN_HEIGHT); targetHeight = Math.min(targetHeight, CONFIG.MAX_HEIGHT); // Apply the calculated height mainContainer.style.height = `${targetHeight}px`; mainContainer.style.maxHeight = `${CONFIG.MAX_HEIGHT}px`; // Reset content container height contentContainer.style.height = ''; log(`Container auto-resized to ${targetHeight}px`); }); } catch (error) { // Restore original height on error if (originalHeight) mainContainer.style.height = originalHeight; if (originalMaxHeight) mainContainer.style.maxHeight = originalMaxHeight; log(`Auto-resize error: ${error.message}`, 'warning'); } }; const bindEvents = () => { // Tab switching document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { const tabName = btn.dataset.tab; // Update active tab button document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); // Update active tab content document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); document.getElementById(`${tabName}-tab`).classList.add('active'); // Auto-resize container to fit new content setTimeout(() => autoResizeContainer(), 100); // Persist active tab saveToStorage(STORAGE_KEYS.activeTab, tabName); }); }); // Send button document.getElementById('send-btn').addEventListener('click', async () => { const activeTab = document.querySelector('.tab-btn.active').dataset.tab; const sendBtn = document.getElementById('send-btn'); const btnText = sendBtn.querySelector('.btn-text'); const btnLoader = sendBtn.querySelector('.btn-loader'); let message = ''; let customCode = customCodeInput.value.trim(); let isTemplate = false; if (activeTab === 'simple') { message = messageInput.value.trim(); } else if (activeTab === 'template') { message = templateInput.value.trim(); isTemplate = true; // Parse dynamic elements const elementsInput = dynamicElementsInput.value.trim(); if (elementsInput) { dynamicElements = await parseDynamicElements(elementsInput); if (!Array.isArray(dynamicElements) || dynamicElements.length === 0) { log('No valid dynamic elements found', 'warning'); return; } } // Check if batch processing is enabled isLooping = document.getElementById('loop-checkbox').checked; if (isLooping) { document.getElementById('stop-batch-btn').style.display = 'inline-block'; } } else { message = messageInput.value.trim() || templateInput.value.trim(); } if (!message) { log('Please enter a message', 'warning'); return; } // Update button state sendBtn.disabled = true; btnText.style.display = 'none'; btnLoader.style.display = 'inline-flex'; try { await processMessage(message, customCode, isTemplate); } finally { sendBtn.disabled = false; btnText.style.display = 'inline'; btnLoader.style.display = 'none'; if (!isLooping) { document.getElementById('stop-batch-btn').style.display = 'none'; } } }); // Stop batch button document.getElementById('stop-batch-btn').addEventListener('click', () => { stopBatchProcessing(); document.getElementById('stop-batch-btn').style.display = 'none'; }); // Auto-remove processed items checkbox document.getElementById('auto-remove-checkbox').addEventListener('change', (e) => { autoRemoveProcessed = e.target.checked; log(`Auto-remove processed items: ${autoRemoveProcessed ? 'enabled' : 'disabled'}`); saveToStorage(STORAGE_KEYS.autoRemove, autoRemoveProcessed); }); // New chat per item checkbox document.getElementById('new-chat-checkbox').addEventListener('change', (e) => { newChatPerItem = e.target.checked; log(`New chat per item: ${newChatPerItem ? 'enabled' : 'disabled'}`); saveToStorage(STORAGE_KEYS.newChat, newChatPerItem); }); // Wait time input document.getElementById('wait-time-input').addEventListener('change', (e) => { const value = parseInt(e.target.value); if (value >= 0 && value <= 30000) { batchWaitTime = value; log(`Wait time between items set to ${value}ms`); saveToStorage(STORAGE_KEYS.waitTime, batchWaitTime); } else { e.target.value = batchWaitTime; log('Invalid wait time, keeping current value', 'warning'); } }); // Clear button document.getElementById('clear-btn').addEventListener('click', () => { messageInput.value = ''; customCodeInput.value = ''; templateInput.value = ''; dynamicElementsInput.value = ''; document.getElementById('loop-checkbox').checked = false; log('Form cleared'); // Persist cleared state try { GM_setValue(STORAGE_KEYS.messageInput, ''); GM_setValue(STORAGE_KEYS.customCodeInput, ''); GM_setValue(STORAGE_KEYS.templateInput, ''); GM_setValue(STORAGE_KEYS.dynamicElementsInput, ''); GM_setValue(STORAGE_KEYS.loop, false); } catch {} }); // Toggle log button document.getElementById('toggle-log-btn').addEventListener('click', () => { const logElement = document.getElementById('log-container'); if (logElement.style.display === 'none') { logElement.style.display = 'block'; } else { logElement.style.display = 'none'; } // Auto-resize container after log visibility change setTimeout(() => autoResizeContainer(), 100); }); // Clear log button document.getElementById('clear-log-btn').addEventListener('click', () => { logContainer.innerHTML = ''; log('Log cleared'); }); // Save log content height when user resizes it const logContent = document.querySelector('.log-content'); if (logContent) { let resizeTimeout; const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { const height = Math.round(entry.contentRect.height); if (height >= 80 && height <= 400) { // Within our constraints saveToStorage(STORAGE_KEYS.logHeight, height); log(`Log height saved: ${height}px`); } }, 500); // Debounce to avoid excessive saves during dragging } }); resizeObserver.observe(logContent); } // Toggle auto-scroll button document.getElementById('toggle-auto-scroll-btn').addEventListener('click', () => { autoScrollLogs = !autoScrollLogs; const btn = document.getElementById('toggle-auto-scroll-btn'); btn.style.opacity = autoScrollLogs ? '1' : '0.5'; btn.title = autoScrollLogs ? 'Auto-scroll: ON' : 'Auto-scroll: OFF'; log(`Auto-scroll logs: ${autoScrollLogs ? 'enabled' : 'disabled'}`); // If enabling auto-scroll, scroll to bottom immediately if (autoScrollLogs && logContainer) { logContainer.scrollTop = logContainer.scrollHeight; } // Save state to storage saveToStorage(STORAGE_KEYS.autoScroll, autoScrollLogs); }); // Minimize button document.getElementById('minimize-btn').addEventListener('click', () => { isMinimized = !isMinimized; if (isMinimized) { mainContainer.classList.add('minimized'); } else { mainContainer.classList.remove('minimized'); // Auto-resize when restoring from minimized state setTimeout(() => autoResizeContainer(), 100); } saveUIState(true); // Immediate save for user action }); // Close button document.getElementById('close-btn').addEventListener('click', () => { mainContainer.style.display = 'none'; uiVisible = false; saveUIState(true); // Immediate save for user action log('UI closed'); }); // Tool buttons document.getElementById('format-json-btn').addEventListener('click', () => { try { const input = dynamicElementsInput.value.trim(); if (input.startsWith('[')) { const parsed = JSON.parse(input); dynamicElementsInput.value = JSON.stringify(parsed, null, 2); log('JSON formatted'); saveToStorage(STORAGE_KEYS.dynamicElementsInput, dynamicElementsInput.value); } } catch (error) { log('Invalid JSON format', 'warning'); } }); document.getElementById('validate-elements-btn').addEventListener('click', async () => { const elements = await parseDynamicElements(dynamicElementsInput.value.trim()); if (Array.isArray(elements) && elements.length > 0) { log(`Valid! Found ${elements.length} elements: ${JSON.stringify(elements.slice(0, 3))}${elements.length > 3 ? '...' : ''}`, 'info'); } else { log('No valid elements found', 'warning'); } }); // Template tab JS check and snippet const elSyntaxBtn = document.getElementById('elements-syntax-check-btn'); if (elSyntaxBtn) { elSyntaxBtn.addEventListener('click', async () => { const code = dynamicElementsInput.value.trim(); if (!code) return log('Nothing to check', 'warning'); try { // Attempt to parse expression in userscript context new Function('return ( ' + code + ' )'); log('Dynamic elements JS syntax is valid', 'info'); } catch (err) { log(`Syntax error: ${err.message}`, 'error'); } }); } const elSnippetBtn = document.getElementById('elements-insert-fn-btn'); if (elSnippetBtn) { elSnippetBtn.addEventListener('click', () => { const sample = `() => [ { name: "soup", orderId: "123" }, { name: "salad", orderId: "124" } ]`; dynamicElementsInput.value = sample; log('Inserted sample dynamic elements function'); }); } document.getElementById('syntax-check-btn').addEventListener('click', async () => { try { new Function(customCodeInput.value); log('Syntax is valid', 'info'); } catch (error) { log(`Syntax error: ${error.message}`, 'error'); } }); document.getElementById('insert-template-btn').addEventListener('click', () => { const template = `// Example custom code template if (response.includes('error')) { log('Detected error in response', 'warning'); } else { log('Response looks good: ' + response.length + ' characters'); // Extract specific information const matches = response.match(/\\d+/g); if (matches) { log('Found numbers: ' + matches.join(', ')); } }`; customCodeInput.value = template; try { GM_setValue(STORAGE_KEYS.customCodeInput, customCodeInput.value); } catch {} }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { // Ctrl/Cmd + Enter to send if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { if ([messageInput, customCodeInput, templateInput, dynamicElementsInput].includes(document.activeElement)) { document.getElementById('send-btn').click(); e.preventDefault(); } } // Escape to minimize if (e.key === 'Escape' && mainContainer.contains(document.activeElement)) { document.getElementById('minimize-btn').click(); e.preventDefault(); } }); // Dragging functionality let isDragging = false; let dragOffset = { x: 0, y: 0 }; // Resizing functionality let isResizing = false; let resizeStartX, resizeStartY, resizeStartWidth, resizeStartHeight; const header = document.getElementById('automation-header'); header.addEventListener('mousedown', (e) => { if (e.target.closest('.header-btn')) return; // Don't drag when clicking buttons isDragging = true; const rect = mainContainer.getBoundingClientRect(); dragOffset.x = e.clientX - rect.left; dragOffset.y = e.clientY - rect.top; header.style.userSelect = 'none'; e.preventDefault(); }); resizeHandle.addEventListener('mousedown', (e) => { isResizing = true; resizeStartX = e.clientX; resizeStartY = e.clientY; resizeStartWidth = mainContainer.offsetWidth; resizeStartHeight = mainContainer.offsetHeight; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (isDragging) { const x = e.clientX - dragOffset.x; const y = e.clientY - dragOffset.y; mainContainer.style.left = `${Math.max(0, Math.min(x, window.innerWidth - mainContainer.offsetWidth))}px`; mainContainer.style.top = `${Math.max(0, Math.min(y, window.innerHeight - mainContainer.offsetHeight))}px`; mainContainer.style.right = 'auto'; saveUIState(); // Debounced for drag operations } else if (isResizing) { const newWidth = Math.max(CONFIG.MIN_WIDTH, Math.min(CONFIG.MAX_WIDTH, resizeStartWidth + (e.clientX - resizeStartX))); const newHeight = Math.max(CONFIG.MIN_HEIGHT, Math.min(CONFIG.MAX_HEIGHT, resizeStartHeight + (e.clientY - resizeStartY))); mainContainer.style.width = `${newWidth}px`; mainContainer.style.height = `${newHeight}px`; saveUIState(); // Debounced for resize operations } }); document.addEventListener('mouseup', () => { if (isDragging) { saveUIState(true); // Immediate save when drag ends isDragging = false; header.style.userSelect = ''; } if (isResizing) { saveUIState(true); // Immediate save when resize ends isResizing = false; } }); // Consolidated input persistence const persistInputs = [ { element: messageInput, key: STORAGE_KEYS.messageInput }, { element: templateInput, key: STORAGE_KEYS.templateInput }, { element: dynamicElementsInput, key: STORAGE_KEYS.dynamicElementsInput }, { element: customCodeInput, key: STORAGE_KEYS.customCodeInput } ]; persistInputs.forEach(({ element, key }) => { element.addEventListener('input', () => saveToStorage(key, element.value)); }); // Persist loop checkbox when used const loopEl = document.getElementById('loop-checkbox'); loopEl.addEventListener('change', (e) => { isLooping = e.target.checked; saveToStorage(STORAGE_KEYS.loop, isLooping); }); // Settings: Debug mode const debugEl = document.getElementById('debug-mode-checkbox'); if (debugEl) { debugEl.addEventListener('change', (e) => { CONFIG.DEBUG_MODE = !!e.target.checked; saveToStorage(STORAGE_KEYS.configDebug, CONFIG.DEBUG_MODE); log(`Debug mode ${CONFIG.DEBUG_MODE ? 'enabled' : 'disabled'}`); }); } // Settings: Response timeout const timeoutEl = document.getElementById('response-timeout-input'); if (timeoutEl) { timeoutEl.addEventListener('change', (e) => { const v = parseInt(e.target.value); if (!Number.isNaN(v) && v >= 10000 && v <= 6000000) { CONFIG.RESPONSE_TIMEOUT = v; saveToStorage(STORAGE_KEYS.configTimeout, v); log(`Response timeout set to ${v}ms`); } else { e.target.value = String(CONFIG.RESPONSE_TIMEOUT); log('Invalid response timeout', 'warning'); } }); } // Settings: Size bounds (consolidated) const applySizeLimits = () => { mainContainer.style.minWidth = CONFIG.MIN_WIDTH + 'px'; mainContainer.style.minHeight = CONFIG.MIN_HEIGHT + 'px'; mainContainer.style.maxWidth = CONFIG.MAX_WIDTH + 'px'; mainContainer.style.maxHeight = CONFIG.MAX_HEIGHT + 'px'; }; // Data-driven size input handlers const sizeInputs = [ { id: 'min-width-input', configKey: 'MIN_WIDTH', storageKey: STORAGE_KEYS.configMinWidth, min: 200, max: 1200 }, { id: 'min-height-input', configKey: 'MIN_HEIGHT', storageKey: STORAGE_KEYS.configMinHeight, min: 120, max: 1200 }, { id: 'max-width-input', configKey: 'MAX_WIDTH', storageKey: STORAGE_KEYS.configMaxWidth, min: 200, max: 2000 }, { id: 'max-height-input', configKey: 'MAX_HEIGHT', storageKey: STORAGE_KEYS.configMaxHeight, min: 120, max: 2000 } ]; sizeInputs.forEach(({ id, configKey, storageKey, min, max }) => { const element = document.getElementById(id); if (element) { element.addEventListener('change', (e) => { const v = parseInt(e.target.value); if (!Number.isNaN(v) && v >= min && v <= max) { CONFIG[configKey] = v; saveToStorage(storageKey, v); applySizeLimits(); } else { e.target.value = String(CONFIG[configKey]); } }); } }); // Settings: default visible const defVisEl = document.getElementById('default-visible-checkbox'); if (defVisEl) defVisEl.addEventListener('change', (e) => { CONFIG.DEFAULT_VISIBLE = !!e.target.checked; try { GM_setValue(STORAGE_KEYS.configDefaultVisible, CONFIG.DEFAULT_VISIBLE); } catch {} log(`Default visibility ${CONFIG.DEFAULT_VISIBLE ? 'ON' : 'OFF'}`); }); }; // Initialize the script const init = () => { if (document.getElementById('chatgpt-automation-ui')) { return; // Already initialized } log('Initializing ChatGPT Automation Pro...'); // Wait for page to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createUI); } else { createUI(); } }; // Auto-start init(); // Export for external access window.ChatGPTAutomation = { processMessage, stopBatchProcessing, log, updateStatus, CONFIG, toggleUI: () => { if (mainContainer) { const show = mainContainer.style.display === 'none'; mainContainer.style.display = show ? 'block' : 'none'; uiVisible = show; saveUIState(true); // Immediate save for user action } }, show: () => { if (mainContainer) { mainContainer.style.display = 'block'; uiVisible = true; saveUIState(true); // Immediate save for user action } }, hide: () => { if (mainContainer) { mainContainer.style.display = 'none'; uiVisible = false; saveUIState(true); // Immediate save for user action } } }; })();