您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Advanced ChatGPT automation with dynamic templating
当前为
// ==UserScript== // @name ChatGPT Automation Pro // @namespace http://tampermonkey.net/ // @version 2.5 // @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 * // @run-at document-end // @inject-into auto // @license MIT // ==/UserScript== (function () { 'use strict'; const CONFIG = { DEBUG_MODE: false, RESPONSE_TIMEOUT: 3000000, DEFAULT_VISIBLE: false, RUN_LOCK_TTL_MS: 15000, RUN_LOCK_RENEW_MS: 5000, BATCH_WAIT_TIME: 2000, AUTO_REMOVE_PROCESSED: false, AUTO_SCROLL_LOGS: true, }; // --- Eval Capability Detection (Chrome MV3/Tampermonkey CSP) --- // Some environments (e.g., Chrome + Tampermonkey on CSP-heavy sites like chatgpt.com) block // dynamic code evaluation (`eval`/`Function`). Detect once and adapt behavior. const ENV = (() => { let canEval = true; try { // Using the Function constructor directly to test, same mechanism used by custom code runner // eslint-disable-next-line no-new-func new Function('return 1')(); } catch (e) { canEval = false; } return { CAN_EVAL: canEval, IS_CHROME: typeof navigator !== 'undefined' && /Chrome\//.test(navigator.userAgent), IS_FIREFOX: typeof navigator !== 'undefined' && /Firefox\//.test(navigator.userAgent), }; })(); const state = { isLooping: false, dynamicElements: [], lastResponseElement: null, responseObserver: null, isMinimized: false, isDarkMode: false, uiVisible: CONFIG.DEFAULT_VISIBLE, headerObserverStarted: false, autoScrollLogs: CONFIG.AUTO_SCROLL_LOGS, batchWaitTime: CONFIG.BATCH_WAIT_TIME, autoRemoveProcessed: CONFIG.AUTO_REMOVE_PROCESSED, isProcessing: false, currentBatchIndex: 0, processedCount: 0, chainDefinition: null, runLockId: null, runLockTimer: null, }; const STORAGE_KEYS = { messageInput: 'messageInput', templateInput: 'templateInput', dynamicElementsInput: 'dynamicElementsInput', customCodeInput: 'customCodeInput', loop: 'looping', autoRemove: 'autoRemoveProcessed', autoScroll: 'autoScrollLogs', waitTime: 'batchWaitTime', stepWaitTime: 'stepWaitTime', activeTab: 'activeTab', uiState: 'uiState', chainDef: 'chain.definition', presetsTemplates: 'presets.templates', presetsChains: 'presets.chains', presetsResponseJS: 'presets.responseJS', presetsSteps: 'presets.steps', logHistory: 'log.history', logVisible: 'log.visible', runLockKey: 'chatgptAutomation.runLock', configDebug: 'config.debugMode', configTimeout: 'config.responseTimeout', configDefaultVisible: 'config.defaultVisible', }; let ui = { mainContainer: null, statusIndicator: null, logContainer: null, progressBar: null, progressBarSub: null, resizeHandle: null, miniProgress: null, miniFill: null, miniLabel: null, miniSubProgress: null, miniSubFill: null, miniSubLabel: null, }; // Small helpers to keep calls consistent const saveUIState = (immediate = false) => uiState.save(immediate); const utils = { log: (message, type = 'info') => { const now = new Date(); const datePart = now.toLocaleDateString(undefined, { day: '2-digit', month: 'short' }); const timePart = now.toLocaleTimeString(); const timestamp = `${datePart} ${timePart}`; const logMessage = `[${timestamp}] ${message}`; if (CONFIG.DEBUG_MODE) console.log(logMessage); if (ui.logContainer) { const logEntry = document.createElement('div'); logEntry.className = `log-entry log-${type}`; logEntry.textContent = logMessage; ui.logContainer.appendChild(logEntry); if (state.autoScrollLogs) { ui.logContainer.scrollTop = ui.logContainer.scrollHeight; } const entries = Array.from(ui.logContainer.querySelectorAll('.log-entry')); while (entries.length > 200) { const first = entries.shift(); if (first?.parentNode) first.parentNode.removeChild(first); } } try { let history = GM_getValue(STORAGE_KEYS.logHistory, []); if (!Array.isArray(history)) history = []; history.push({ t: Date.now(), type, msg: logMessage }); if (history.length > 300) history = history.slice(-300); GM_setValue(STORAGE_KEYS.logHistory, history); } catch {} }, clip: (s, n = 300) => { try { const str = String(s ?? ''); return str.length > n ? str.slice(0, n) + '…' : str; } catch { return ''; } }, detectDarkMode: () => { const html = document.documentElement; const body = document.body; return [ html.classList.contains('dark'), body.classList.contains('dark'), html.getAttribute('data-theme') === 'dark', body.getAttribute('data-theme') === 'dark', getComputedStyle(body).backgroundColor.includes('rgb(0, 0, 0)') || getComputedStyle(body).backgroundColor.includes('rgb(17, 24, 39)') || getComputedStyle(body).backgroundColor.includes('rgb(31, 41, 55)'), ].some(Boolean); }, saveToStorage: (key, value) => { try { GM_setValue(key, value); } catch {} }, sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), getByPath: (obj, path) => { try { return path.split('.').reduce((acc, part) => acc?.[part], obj); } catch { return undefined; } }, queryFirst: (selectors) => { for (const s of selectors) { const el = document.querySelector(s); if (el) return el; } return null; }, loadFromStorage: (key, def) => { try { return GM_getValue(key, def); } catch { return def; } }, }; 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) => { try { const msg = err?.error || err?.message || 'Network error'; reject(new Error(msg)); } catch { reject(new Error('Network error')); } }, 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 processors = { executeCustomCode: async (code, responseText, templateData = null) => { if (!code || code.trim() === '') return; try { // Resolve context let item = templateData?.elementData ?? null; let index = templateData?.index ?? null; let total = templateData?.total ?? null; const stepsCtx = templateData?.steps ?? {}; const lastResponse = templateData?.lastResponse ?? responseText; // Fallback: single dynamic element → provide as item when not in template mode if (!item) { try { const dynInput = document.getElementById('dynamic-elements-input'); const val = dynInput && typeof dynInput.value === 'string' ? dynInput.value.trim() : ''; if (val) { const arr = await processors.parseDynamicElements(val); if (Array.isArray(arr) && arr.length === 1) { item = arr[0]; if (index == null) index = 1; if (total == null) total = 1; utils.log('Context fallback: using single dynamic element for custom code'); } } } catch {} } if (CONFIG.DEBUG_MODE) { utils.log( `Custom code context: item=${item ? JSON.stringify(item).slice(0, 100) : 'null'}, index=${index}, total=${total}` ); } if (ENV.CAN_EVAL) { // Use sandbox-safe Function constructor; await Promise if returned const Fn = function () {}.constructor; // constructor of a sandboxed function const fn = new Fn( 'response', 'log', 'console', 'item', 'index', 'total', 'http', 'steps', 'lastResponse', 'GM_getValue', 'GM_setValue', 'GM_xmlhttpRequest', 'unsafeWindow', 'utils', code ); const result = fn( responseText, (msg, type = 'info') => utils.log(msg, type), console, item, index, total, http, stepsCtx, lastResponse, GM_getValue, GM_setValue, GM_xmlhttpRequest, unsafeWindow, utils ); await Promise.resolve(result); utils.log('Custom code executed successfully'); return result; } else { // Safe fallback: no dynamic code evaluation due to CSP (Chrome MV3/Tampermonkey). // Attempt to auto-parse JSON and return a useful object. let data = responseText; if (typeof data === 'string') { const trimmed = data.trim(); if (trimmed.startsWith('{') || trimmed.startsWith('[')) { try { data = JSON.parse(trimmed); } catch {} } } utils.log('Custom JS is disabled here (CSP blocks unsafe-eval). Returned parsed response instead.', 'warn'); return data; } } catch (error) { utils.log(`Custom code execution error: ${error.message}`, 'error'); throw error; } }, processDynamicTemplate: (template, dynamicData) => { if (!template) return ''; const regex = /\{\{\s*([\w$.]+)\s*\}\}|\{\s*([\w$.]+)\s*\}/g; return template.replace(regex, (_, g1, g2) => { const keyPath = g1 || g2; let value = utils.getByPath(dynamicData, keyPath); if (value === undefined) return ''; if (typeof value === 'object') { try { return JSON.stringify(value); } catch { return String(value); } } return String(value); }); }, parseDynamicElements: async (input) => { const raw = (input || '').trim(); if (!raw) return []; // Primary: JSON arrays/objects if (raw.startsWith('[') || raw.startsWith('{')) { try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : [parsed]; } catch (e) { utils.log(`Invalid JSON: ${e.message}`, 'error'); return []; } } // Chrome-safe fallback when expressions are provided (no eval): // Support simple CSV / newline separated values and numeric ranges like 1..5 if (!ENV.CAN_EVAL) { const lines = raw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean); const parts = lines.length > 1 ? lines : raw.split(',').map((s) => s.trim()).filter(Boolean); const out = []; for (const p of parts) { const m = p.match(/^(\d+)\.\.(\d+)$/); if (m) { const a = parseInt(m[1], 10), b = parseInt(m[2], 10); const step = a <= b ? 1 : -1; for (let x = a; step > 0 ? x <= b : x >= b; x += step) out.push(x); } else if (/^\d+(?:\.\d+)?$/.test(p)) { out.push(Number(p)); } else if ((p.startsWith('"') && p.endsWith('"')) || (p.startsWith("'") && p.endsWith("'"))) { out.push(p.slice(1, -1)); } else { // treat as literal string out.push(p); } } return out; } // Legacy (Firefox): evaluate expression (may return array/object/stringified JSON) try { // eslint-disable-next-line no-new-func const v = new Function('return ( ' + raw + ' )')(); 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 {} } return [res]; } catch (e) { utils.log(`Dynamic element expression not supported here: ${e.message}`, 'warn'); return []; } }, }; const uiState = { saveTimeout: null, save: (immediate = false) => { if (!ui.mainContainer) return; const doSave = () => { const stateData = { left: ui.mainContainer.style.left, top: ui.mainContainer.style.top, right: ui.mainContainer.style.right, minimized: state.isMinimized, visible: state.uiVisible, }; utils.saveToStorage(STORAGE_KEYS.uiState, JSON.stringify(stateData)); }; if (immediate) { clearTimeout(uiState.saveTimeout); doSave(); } else { clearTimeout(uiState.saveTimeout); uiState.saveTimeout = setTimeout(doSave, 100); } }, load: () => { try { const saved = GM_getValue(STORAGE_KEYS.uiState, null); return saved ? JSON.parse(saved) : {}; } catch { return {}; } }, }; const chatGPT = { getChatInput: () => { const selectors = [ '#prompt-textarea', 'div[contenteditable="true"]', 'textarea[placeholder*="Message"]', 'div.ProseMirror', ]; const el = utils.queryFirst(selectors); return el && el.isContentEditable !== false ? el : null; }, getSendButton: () => { const selectors = [ '#composer-submit-button', 'button[data-testid="send-button"]', 'button[aria-label*="Send"]', 'button[aria-label*="submit"]', ]; const btn = utils.queryFirst(selectors); return btn && !btn.disabled ? btn : null; }, typeMessage: async (message) => { const input = chatGPT.getChatInput(); if (!input) throw new Error('Chat input not found'); if (input.tagName === 'DIV') { input.innerHTML = ''; input.focus(); const paragraph = document.createElement('p'); paragraph.textContent = message; input.appendChild(paragraph); 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 utils.sleep(100); utils.log(`Message typed: "${utils.clip(message, 50)}"`); }, sendMessage: async () => { const sendButton = chatGPT.getSendButton(); if (!sendButton) throw new Error('Send button not available'); sendButton.click(); utils.log('Message sent'); await utils.sleep(500); }, ask: async (message) => { await chatGPT.typeMessage(message); await utils.sleep(300); await chatGPT.sendMessage(); updateStatus('waiting'); const el = await chatGPT.waitForResponse(); return { el, text: chatGPT.extractResponseText(el) }; }, // ask with expectation option: { expect: 'image' | 'text' } askWith: async (message, options = { expect: 'text' }) => { await chatGPT.typeMessage(message); await utils.sleep(300); await chatGPT.sendMessage(); updateStatus('waiting'); const el = await chatGPT.waitForResponse(); if (options.expect === 'image') { // Allow brief time for images to attach await utils.sleep(500); let images = chatGPT.extractResponseImages(el); if (!images || images.length === 0) { // Retry scan for late-loading images await utils.sleep(800); images = chatGPT.extractResponseImages(el); } return { el, images }; } return { el, text: chatGPT.extractResponseText(el) }; }, waitForResponse: async () => { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { if (state.responseObserver) state.responseObserver.disconnect(); reject(new Error('Response timeout')); }, CONFIG.RESPONSE_TIMEOUT); const checkForNewResponse = () => { const assistantMessages = document.querySelectorAll( '[data-message-author-role="assistant"]' ); const latestMessage = assistantMessages[assistantMessages.length - 1]; if (latestMessage && latestMessage !== state.lastResponseElement) { const isGenerating = document.querySelector('[data-testid="stop-button"]') || document.querySelector('.result-thinking') || latestMessage.querySelector('.typing-indicator'); if (!isGenerating) { clearTimeout(timeout); if (state.responseObserver) state.responseObserver.disconnect(); // Prefer the full assistant turn container (article) which holds images/content const container = latestMessage.closest('article[data-turn="assistant"]') || latestMessage; state.lastResponseElement = container; resolve(container); } } }; checkForNewResponse(); state.responseObserver = new MutationObserver(checkForNewResponse); state.responseObserver.observe(document.body, { childList: true, subtree: true, characterData: true, }); }); }, extractResponseText: (responseElement) => { if (!responseElement) return ''; 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(); }, // Extract image URLs from an assistant response element extractResponseImages: (responseElement) => { if (!responseElement) return []; const urls = new Set(); try { // Search within the assistant article scope const scope = responseElement.closest && responseElement.closest('article[data-turn="assistant"]') ? responseElement.closest('article[data-turn="assistant"]') : responseElement; // Get all generated images, excluding blurred ones scope.querySelectorAll('div[id^="image-"] img[alt="Generated image"]').forEach((img) => { const src = img.getAttribute('src'); // Skip blurred backdrop images (they have blur-2xl or scale-110 in their parent) const isBlurred = img.closest('.blur-2xl') || img.closest('.scale-110'); if (src && !isBlurred) { utils.log('🖼️ Found image: ' + src); urls.add(src); } }); } catch (e) { utils.log('❌ Error in extractResponseImages: ' + e.message, 'error'); } return Array.from(urls); }, }; // UI Creation const createUI = () => { state.isDarkMode = utils.detectDarkMode(); // Main container ui.mainContainer = document.createElement('div'); ui.mainContainer.id = 'chatgpt-automation-ui'; ui.mainContainer.className = state.isDarkMode ? 'dark-mode' : 'light-mode'; ui.mainContainer.innerHTML = /*html*/ `<div class="automation-header" id="automation-header"> <h3>ChatGPT Automation Pro</h3> <div class="header-controls"> <div class="mini-progress" id="mini-progress" style="display: none" title="Batch progress" > <div class="mini-bar"><div class="mini-fill" id="mini-fill"></div></div> <div class="mini-label" id="mini-label">0/0</div> </div> <div class="mini-progress" id="mini-sub-progress" style="display: none" title="Inner batch progress" > <div class="mini-bar"> <div class="mini-fill" id="mini-sub-fill"></div> </div> <div class="mini-label" id="mini-sub-label">0/0</div> </div> <div class="status-indicator" id="status-indicator"> <span class="status-dot"></span> <span class="status-text">Ready</span> </div> <button class="header-btn" id="header-log-toggle" title="Show/Hide Log" aria-label="Show/Hide Log" > <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> <path d="M4 5h16v2H4V5zm0 6h16v2H4v-2zm0 6h10v2H4v-2z" /> </svg> </button> <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 class="progress-bar sub" id="progress-container-sub" style="display: none; margin-top: 6px" > <div class="progress-fill"></div> </div> <div class="progress-text sub" id="progress-text-sub" style="display: none"> 0/0 </div> </div> <div class="automation-form"> <div class="tab-container"> <button class="tab-btn active" data-tab="composer">Composer</button> <button class="tab-btn" data-tab="settings">Settings</button> </div> <div class="tab-content active" id="composer-tab"> <div class="form-group"> <label>Composer Canvas:</label> <div class="composer-presets"> <div class="preset-row"> <input type="text" id="composer-preset-name-input" class="settings-input" placeholder="Preset name" style="flex: 1" /> <select id="composer-preset-select" class="settings-input" style="flex: 2" > <option value="">Select preset...</option> </select> <button class="btn btn-secondary" id="save-composer-preset-btn" title="Save current configuration" > <svg width="14" height="14" viewBox="0 0 448 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-242.7c0-17-6.7-33.3-18.7-45.3L352 50.7C340 38.7 323.7 32 306.7 32L64 32zm32 96c0-17.7 14.3-32 32-32l160 0c17.7 0 32 14.3 32 32l0 64c0 17.7-14.3 32-32 32l-160 0c-17.7 0-32-14.3-32-32l0-64zM224 288a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"/></svg> </button> <button class="btn btn-primary" id="load-composer-preset-btn" title="Load selected preset" > <svg width="14" height="14" viewBox="0 0 576 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M56 225.6L32.4 296.2 32.4 96c0-35.3 28.7-64 64-64l138.7 0c13.8 0 27.3 4.5 38.4 12.8l38.4 28.8c5.5 4.2 12.3 6.4 19.2 6.4l117.3 0c35.3 0 64 28.7 64 64l0 16-365.4 0c-41.3 0-78 26.4-91.1 65.6zM477.8 448L99 448c-32.8 0-55.9-32.1-45.5-63.2l48-144C108 221.2 126.4 208 147 208l378.8 0c32.8 0 55.9 32.1 45.5 63.2l-48 144c-6.5 19.6-24.9 32.8-45.5 32.8z"/></svg> </button> <button class="btn btn-danger" id="delete-composer-preset-btn" title="Delete selected preset" > <svg width="14" height="14" viewBox="0 0 448 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M136.7 5.9C141.1-7.2 153.3-16 167.1-16l113.9 0c13.8 0 26 8.8 30.4 21.9L320 32 416 32c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 96C14.3 96 0 81.7 0 64S14.3 32 32 32l96 0 8.7-26.1zM32 144l384 0 0 304c0 35.3-28.7 64-64 64L96 512c-35.3 0-64-28.7-64-64l0-304zm88 64c-13.3 0-24 10.7-24 24l0 192c0 13.3 10.7 24 24 24s24-10.7 24-24l0-192c0-13.3-10.7-24-24-24zm104 0c-13.3 0-24 10.7-24 24l0 192c0 13.3 10.7 24 24 24s24-10.7 24-24l0-192c0-13.3-10.7-24-24-24zm104 0c-13.3 0-24 10.7-24 24l0 192c0 13.3 10.7 24 24 24s24-10.7 24-24l0-192c0-13.3-10.7-24-24-24z"/></svg> </button> </div> </div> <div id="chain-canvas" class="chain-canvas"> <div class="chain-toolbar"> <button class="btn btn-secondary" id="add-step-btn"> Add Step </button> <button class="btn btn-secondary" id="validate-chain-btn"> Validate Chain </button> <button class="btn btn-primary" id="run-chain-btn"> Run Chain </button> <button class="btn btn-danger" id="stop-run-btn" style="display: none" > Stop </button> </div> <div id="chain-cards" class="chain-cards"></div> </div> <div class="help-text"> Visual editor for multi-step automation chains. Steps connect in sequence; supports templates and custom JavaScript execution. </div> </div> <div class="form-group"> <label for="dynamic-elements-input" >Dynamic Elements (List, JSON, or function)</label > <div class="code-editor"> <div class="overlay-field"> <textarea id="dynamic-elements-input" rows="4" placeholder='["item1", "item2", "item3"] or () => ["generated", "items"]' ></textarea> <button class="tool-btn overlay" id="format-dyn-elements-btn" title="Format JSON" > { } </button> <button class="tool-btn overlay" id="apply-dyn-elements-btn" style="right: 36px;" title="Apply dynamic elements to runtime" > ▶ </button> </div> </div> </div> <div class="form-group"> <label for="chain-json-input">Chain JSON (advanced):</label> <div class="code-editor"> <textarea id="chain-json-input" rows="6" placeholder='{ "entryId": "step-1", "steps": [ { "id": "step-1", "type": "prompt", "title": "Create message", "template": "Hello {item}", "next": "step-2" }, { "id": "step-2", "type": "js", "title": "Process response", "code": "utils.log(\"Processing: \" + steps[\"step-1\"].response);" } ] }' ></textarea> <div class="editor-tools"> <button class="tool-btn" id="format-chain-json-btn" title="Format JSON" > { } </button> </div> </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>Batch settings:</label> <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> <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 class="wait-time-control"> <label for="step-wait-input">Wait between steps (ms):</label> <input type="number" id="step-wait-input" min="0" max="30000" value="0" 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="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> </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> <div class="automation-log" id="log-container"> <div class="log-header"> <span>Activity Log</span> <div class="log-header-controls"> <button class="tool-btn" id="stop-mini-btn" title="Stop" style="display: none"> <svg width="14" height="14" viewBox="0 0 448 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M64 32C77.3 32 88 42.7 88 56v400c0 13.3-10.7 24-24 24H40c-21.2 0-39.2-17.9-39.2-39.2V71.2C.8 50 18.8 32 40 32h24z"/></svg> </button> <button class="tool-btn" id="toggle-auto-scroll-btn" title="Toggle Auto-scroll" > <svg width="14" height="14" viewBox="0 0 512 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M64 256c0 106 86 192 192 192s192-86 192-192S362 64 256 64 64 150 64 256zm160-48h224v32H224v-32z"/></svg> </button> <button class="tool-btn" id="clear-log-btn" title="Clear Log"> <svg width="14" height="14" viewBox="0 0 448 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M136.7 5.9C141.1-7.2 153.3-16 167.1-16l113.9 0c13.8 0 26 8.8 30.4 21.9L320 32 416 32c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 96C14.3 96 0 81.7 0 64S14.3 32 32 32l96 0 8.7-26.1zM32 144l384 0 0 304c0 35.3-28.7 64-64 64L96 512c-35.3 0-64-28.7-64-64l0-304zm88 64c-13.3 0-24 10.7-24 24l0 192c0 13.3 10.7 24 24 24s24-10.7 24-24l0-192c0-13.3-10.7-24-24-24zm104 0c-13.3 0-24 10.7-24 24l0 192c0 13.3 10.7 24 24 24s24-10.7 24-24l0-192c0-13.3-10.7-24-24-24zm104 0c-13.3 0-24 10.7-24 24l0 192c0 13.3 10.7 24 24 24s24-10.7 24-24l0-192c0-13.3-10.7-24-24-24z"/></svg> </button> </div> </div> <div class="log-content"></div> </div> </div> <div class="resize-handle" id="resize-handle"></div> <!-- Modal for editing a chain step --> <div id="chain-step-modal" class="chain-modal" aria-hidden="true" style="display: none" > <div class="chain-modal-backdrop"></div> <div class="chain-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="chain-step-title" > <div class="chain-modal-header"> <h4 id="chain-step-title">Edit Step</h4> <div class="step-modal-presets"> <select id="step-preset-select" class="settings-input" style="min-width: 120px" > <option value="">Select preset...</option> </select> <button class="tool-btn" id="save-step-preset-btn" title="Save as preset" > <svg width="14" height="14" viewBox="0 0 448 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-242.7c0-17-6.7-33.3-18.7-45.3L352 50.7C340 38.7 323.7 32 306.7 32L64 32zm32 96c0-17.7 14.3-32 32-32l160 0c17.7 0 32 14.3 32 32l0 64c0 17.7-14.3 32-32 32l-160 0c-17.7 0-32-14.3-32-32l0-64zM224 288a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"/></svg> </button> <button class="tool-btn" id="delete-step-preset-btn" title="Delete preset" > <svg width="14" height="14" viewBox="0 0 448 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M136.7 5.9C141.1-7.2 153.3-16 167.1-16l113.9 0c13.8 0 26 8.8 30.4 21.9L320 32 416 32c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 96C14.3 96 0 81.7 0 64S14.3 32 32 32l96 0 8.7-26.1zM32 144l384 0 0 304c0 35.3-28.7 64-64 64L96 512c-35.3 0-64-28.7-64-64l0-304zm88 64c-13.3 0-24 10.7-24 24l0 192c0 13.3 10.7 24 24 24s24-10.7 24-24l0-192c0-13.3-10.7-24-24-24zm104 0c-13.3 0-24 10.7-24 24l0 192c0 13.3 10.7 24 24 24s24-10.7 24-24l0-192c0-13.3-10.7-24-24-24zm104 0c-13.3 0-24 10.7-24 24l0 192c0 13.3 10.7 24 24 24s24-10.7 24-24l0-192c0-13.3-10.7-24-24-24z"/></svg> </button> </div> <button class="header-btn" id="close-step-modal-btn" 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 class="chain-modal-body"> <div class="form-group"> <label for="step-id-input">ID</label> <input id="step-id-input" class="settings-input" placeholder="step-1" /> </div> <div class="form-group"> <label for="step-title-input">Title</label> <input id="step-title-input" class="settings-input" placeholder="Describe the step" /> </div> <div class="form-group"> <label for="step-type-select">Type</label> <select id="step-type-select" class="settings-input"> <option value="prompt">Prompt</option> <option value="template">Template (Batch)</option> <option value="js">JavaScript</option> <option value="http">HTTP Request</option> </select> </div> <div class="form-group" data-field="prompt"> <label for="step-response-type">Response Type</label> <select id="step-response-type" class="settings-input"> <option value="text">Text</option> <option value="image">Image</option> </select> <label class="checkbox-label" style="margin-top: 6px; display: block"> <input type="checkbox" id="step-newchat-checkbox" /> <span class="checkmark"></span> Open in new chat before this step </label> </div> <div class="form-group" data-field="prompt"> <label for="step-prompt-template">Message Template</label> <textarea id="step-prompt-template" rows="4" class="settings-input" placeholder="Send a message to ChatGPT. Use {steps.stepId.response} to access previous step data." ></textarea> <div class="help-text"> Access previous step data: {steps.stepId.response} for prompts, {steps.stepId.data} for HTTP, {steps.stepId.status} for HTTP status </div> </div> <div class="form-group" data-field="template"> <label for="step-template-input">Message Template</label> <textarea id="step-template-input" rows="4" class="settings-input" placeholder="Template with placeholders like {{item}}, {{index}}, {{total}} or {steps.stepId.data}..." ></textarea> <label for="step-template-elements" style="margin-top: 8px" >Dynamic Elements (JSON/function). Supports {placeholders}.</label > <div class="overlay-field"> <textarea id="step-template-elements" rows="3" class="settings-input" placeholder='["item1", "item2", "item3"] or () => ["generated", "items"]' ></textarea> <button class="tool-btn overlay" id="format-step-elements-btn" title="Format JSON" > { } </button> </div> <label class="checkbox-label" style="margin-top: 6px; display: block"> <input type="checkbox" id="step-use-dynamicelements-checkbox" /> <span class="checkmark"></span> Use chain.dynamicElements as elements </label> <div class="help-text"> Batch processing: {{item}} for current item, {steps.stepId.response} for previous step data </div> </div> <div class="form-group" data-field="http"> <label>HTTP Request</label> <input id="step-http-url" class="settings-input" placeholder="https://api.example.com/data or {steps.stepId.data.apiUrl}" /> <div style="display: flex; gap: 8px; margin-top: 6px"> <select id="step-http-method" class="settings-input"> <option>GET</option> <option>POST</option> <option>PUT</option> <option>DELETE</option> </select> <div class="overlay-field"> <input id="step-http-headers" class="settings-input" placeholder='{"Authorization": "Bearer {steps.authStep.data.token}"}' /> <button class="tool-btn overlay" id="format-http-headers-btn" title="Format JSON" > { } </button> </div> </div> <div class="overlay-field"> <textarea id="step-http-body" rows="3" class="settings-input" placeholder="Request body: {steps.stepId.response} or JSON data" ></textarea> <button class="tool-btn overlay" id="format-http-body-btn" title="Format JSON" > { } </button> </div> <div class="help-text"> Access response with {steps.thisStepId.data} or {steps.thisStepId.status}. Use previous step data in URL/headers/body. </div> </div> <div class="form-group" data-field="js"> <label for="step-js-code">JavaScript Code</label> <textarea id="step-js-code" rows="6" class="settings-input" placeholder="// Access previous steps with steps.stepId.data or steps.stepId.response // Available: response, log, console, item, index, total, http, steps, lastResponse // Example: utils.log('API response:', steps.httpStep.data);" ></textarea> <div class="help-text"> Access step data with <code>steps.stepId.data</code> or <code>steps.stepId.response</code>. Use <code>http</code> for API calls, <code>utils.log()</code> for output. </div> </div> <div class="form-group"> <label for="step-next-select">Next step</label> <select id="step-next-select" class="settings-input"></select> </div> </div> <div class="chain-modal-footer"> <button class="btn btn-secondary" id="delete-step-btn">Delete</button> <button class="btn btn-primary" id="save-step-btn">Save</button> </div> </div> </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 = /*css*/ `/* Base styles that adapt to ChatGPT's theme (scoped) */ #chatgpt-automation-ui { position: fixed; top: 20px; right: 20px; height: auto; width: auto; 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); /* Theme-driven accent colors sourced from ChatGPT theme tokens */ --accent: var(--theme-entity-accent, #6366f1); --accent-strong: color-mix(in oklab, var(--accent) 85%, #000); } #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: both; height: 46px; width: 600px; } #chatgpt-automation-ui.minimized.log-open { height: 300px; } /* Hide main form when minimized */ #chatgpt-automation-ui.minimized .automation-form { display: none; } /* Keep progress container visible state-controlled; mini bars appear in header */ /* Base content layout mirrors minimized behavior */ #chatgpt-automation-ui .automation-content { display: flex; flex-direction: column; min-height: 0; height: calc(100% - 60px); /* Header height offset */ } #chatgpt-automation-ui .automation-header { background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 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 .mini-progress { display: flex; align-items: center; gap: 6px; min-width: 80px; } #chatgpt-automation-ui .mini-progress .mini-bar { width: 60px; height: 4px; background: rgba(255, 255, 255, 0.3); border-radius: 2px; overflow: hidden; } #chatgpt-automation-ui .mini-progress .mini-fill { height: 100%; background: var(--accent); width: 0%; transition: width 0.3s ease; } #chatgpt-automation-ui .mini-progress .mini-label { font-size: 10px; opacity: 0.85; } #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 { display: flex; flex-direction: column; overflow: hidden; /* Allow children to shrink/scroll correctly inside flex */ min-height: 0; -webkit-overflow-scrolling: touch; } #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); } /* Hide main progress container when minimized (header mini bars take over) */ #chatgpt-automation-ui.minimized .progress-container { display: none !important; } #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-bar.sub { background: var(--border-light, rgba(0, 0, 0, 0.1)); } #chatgpt-automation-ui .progress-fill { height: 100%; background: var(--accent); transition: width 0.3s ease; } #chatgpt-automation-ui .progress-text { font-size: 11px; color: var(--text-secondary, #6b7280); text-align: center; } #chatgpt-automation-ui .progress-text.sub { opacity: 0.8; } #chatgpt-automation-ui .automation-form { padding: 16px; /* Keep natural height so logs fill remaining space */ flex: 0 0 auto; overflow: auto; } #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(--accent); border-color: var(--accent); } #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(--accent); box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 25%, transparent); } #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(--accent); box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 25%, transparent); } /* 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(--accent); box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 25%, transparent); } #chatgpt-automation-ui .settings-input.timeout { width: 140px; } /* Overlay format button on hover */ #chatgpt-automation-ui .overlay-field { position: relative; } #chatgpt-automation-ui .overlay-field .overlay { position: absolute; right: 6px; top: 6px; opacity: 0; transition: opacity 0.15s ease; } #chatgpt-automation-ui .overlay-field:hover .overlay { opacity: 1; } #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(--accent); color: white; } #chatgpt-automation-ui .btn-primary:hover { background: var(--accent-strong); } #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)); /* Base height when expanded; can be resized */ height: 150px; flex: 1 1 auto; overflow: hidden; display: flex; flex-direction: column; min-height: 0; } #chatgpt-automation-ui.dark-mode .automation-log { border-color: var(--border-light, rgba(255, 255, 255, 0.06)); } #chatgpt-automation-ui .log-header { padding: 12px 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: 16px; overflow-y: auto; scroll-behavior: smooth; flex: 1 1 auto; min-height: 0; } #chatgpt-automation-ui #step-next-select { width: 100%; } #chatgpt-automation-ui .log-entry { padding: 6px 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; margin-bottom: 6px; /* extra space below last entry */ } #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% ); } /* Chain canvas styles */ #chatgpt-automation-ui .chain-canvas { border: 1px dashed var(--border-light, rgba(0, 0, 0, 0.1)); border-radius: 8px; padding: 8px; min-height: 120px; } #chatgpt-automation-ui .chain-toolbar { display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; } #chatgpt-automation-ui .chain-cards { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-start; min-height: 80px; } /* Empty chain cards container */ #chatgpt-automation-ui .chain-cards:empty { display: flex; align-items: center; justify-content: center; background: var(--surface-secondary, #f8fafc); border: 2px dashed var(--border-medium, rgba(0, 0, 0, 0.15)); border-radius: 12px; padding: 32px 16px; text-align: center; color: var(--text-secondary, #6b7280); font-size: 14px; transition: all 0.2s ease; } #chatgpt-automation-ui.dark-mode .chain-cards:empty { background: var(--surface-secondary, #1e1e20); border-color: var(--border-medium, rgba(255, 255, 255, 0.15)); color: var(--text-secondary, #9ca3af); } #chatgpt-automation-ui .chain-cards:empty::before { content: "🔗 No steps yet. Click 'Add Step' to start building your automation chain."; font-weight: 500; } #chatgpt-automation-ui .chain-card { background: var(--surface-secondary, #f8fafc); border: 1px solid var(--border-light, rgba(0, 0, 0, 0.06)); border-radius: 8px; padding: 8px; min-width: 140px; max-width: 200px; position: relative; transition: all 0.2s ease; } #chatgpt-automation-ui .chain-card:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); transform: translateY(-1px); } #chatgpt-automation-ui.dark-mode .chain-card { background: var(--surface-secondary, #1e1e20); border-color: var(--border-light, rgba(255, 255, 255, 0.06)); } #chatgpt-automation-ui.dark-mode .chain-card:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } #chatgpt-automation-ui .chain-card .title { font-weight: 600; font-size: 12px; margin-bottom: 4px; } #chatgpt-automation-ui .chain-card .meta { font-size: 11px; opacity: 0.8; margin-bottom: 6px; } #chatgpt-automation-ui .chain-card .actions { display: flex; gap: 6px; } /* Composer presets */ #chatgpt-automation-ui .composer-presets { margin-bottom: 12px; padding: 8px; background: var(--surface-secondary, #f8fafc); border-radius: 8px; border: 1px solid var(--border-light, rgba(0, 0, 0, 0.06)); } #chatgpt-automation-ui.dark-mode .composer-presets { background: var(--surface-secondary, #1e1e20); border-color: var(--border-light, rgba(255, 255, 255, 0.06)); } #chatgpt-automation-ui .composer-presets .preset-row { display: flex; gap: 8px; align-items: center; } /* Modal */ #chatgpt-automation-ui .chain-modal { position: fixed; inset: 0; z-index: 10001; } #chatgpt-automation-ui .chain-modal-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.3); } #chatgpt-automation-ui .chain-modal-dialog { position: relative; background: var(--main-surface-primary, #fff); width: 520px; max-width: calc(100% - 32px); margin: 40px auto; border-radius: 10px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2); overflow: hidden; } #chatgpt-automation-ui.dark-mode .chain-modal-dialog { background: var(--main-surface-primary, #2d2d30); } #chatgpt-automation-ui .chain-modal-dialog .header-btn { /* Ensure the close button inside the modal inherits modal text color and uses a transparent background so it's visible in light mode */ background: transparent; color: inherit; padding: 6px; border: none; } #chatgpt-automation-ui .chain-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--border-light, rgba(0, 0, 0, 0.06)); gap: 12px; } #chatgpt-automation-ui .step-modal-presets { display: flex; gap: 8px; align-items: center; } #chatgpt-automation-ui .chain-modal-body { padding: 12px 16px; max-height: 60vh; overflow: auto; } #chatgpt-automation-ui .chain-modal-footer { display: flex; gap: 8px; justify-content: flex-end; padding: 12px 16px; border-top: 1px solid var(--border-light, rgba(0, 0, 0, 0.06)); } #chatgpt-automation-ui .presets-grid .preset-row { display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; } /* 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(ui.mainContainer); // Get UI elements ui.statusIndicator = document.getElementById('status-indicator'); ui.logContainer = document.querySelector('.log-content'); ui.progressBar = document.getElementById('progress-container'); ui.progressBarSub = document.getElementById('progress-container-sub'); ui.miniProgress = document.getElementById('mini-progress'); ui.miniFill = document.getElementById('mini-fill'); ui.miniLabel = document.getElementById('mini-label'); ui.miniSubProgress = document.getElementById('mini-sub-progress'); ui.miniSubFill = document.getElementById('mini-sub-fill'); ui.miniSubLabel = document.getElementById('mini-sub-label'); ui.resizeHandle = document.getElementById('resize-handle'); // Restore saved inputs, toggles and config try { // Chain JSON restored later with parsing // Checkboxes and switches const loopEl = document.getElementById('loop-checkbox'); const autoRemoveEl = document.getElementById('auto-remove-checkbox'); if (loopEl) { loopEl.checked = !!GM_getValue(STORAGE_KEYS.loop, true); state.isLooping = loopEl.checked; } if (autoRemoveEl) { autoRemoveEl.checked = GM_getValue(STORAGE_KEYS.autoRemove, true); state.autoRemoveProcessed = autoRemoveEl.checked; } // Auto-scroll state (button only, no checkbox) state.autoScrollLogs = GM_getValue(STORAGE_KEYS.autoScroll, true); // Wait time const waitInput = document.getElementById('wait-time-input'); const savedWait = parseInt(GM_getValue(STORAGE_KEYS.waitTime, state.batchWaitTime)); if (!Number.isNaN(savedWait)) { state.batchWaitTime = savedWait; if (waitInput) waitInput.value = String(savedWait); } // Per-step wait time const stepWaitInput = document.getElementById('step-wait-input'); const savedStepWait = parseInt(GM_getValue(STORAGE_KEYS.stepWaitTime, 0)); if (stepWaitInput && !Number.isNaN(savedStepWait)) { stepWaitInput.value = String(savedStepWait); } // Active tab const savedTab = GM_getValue(STORAGE_KEYS.activeTab, 'composer'); const tabBtn = document.querySelector(`.tab-btn[data-tab="${savedTab}"]`); if (tabBtn) { tabBtn.click(); } else { // Fallback to composer if saved tab doesn't exist const composerBtn = document.querySelector(`.tab-btn[data-tab="composer"]`); if (composerBtn) composerBtn.click(); } // 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 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; // Chain definition const savedChain = GM_getValue(STORAGE_KEYS.chainDef, ''); const chainInput = document.getElementById('chain-json-input'); if (savedChain && chainInput) { chainInput.value = typeof savedChain === 'string' ? savedChain : JSON.stringify(savedChain, null, 2); try { state.chainDefinition = JSON.parse(chainInput.value); } catch { state.chainDefinition = null; } } } catch {} // Load saved state const savedState = uiState.load(); if (savedState.left) { ui.mainContainer.style.left = savedState.left; ui.mainContainer.style.right = 'auto'; } if (savedState.top) { ui.mainContainer.style.top = savedState.top; } if (savedState.minimized) { state.isMinimized = true; ui.mainContainer.classList.add('minimized'); } // Respect explicit persisted visibility over default if (typeof savedState.visible === 'boolean') { state.uiVisible = savedState.visible; } else { state.uiVisible = !!CONFIG.DEFAULT_VISIBLE; } ui.mainContainer.style.display = state.uiVisible ? 'block' : 'none'; // Restore persisted log history try { const hist = GM_getValue(STORAGE_KEYS.logHistory, []); if (Array.isArray(hist) && hist.length && ui.logContainer) { hist.slice(-200).forEach((h) => { const div = document.createElement('div'); div.className = `log-entry log-${h.type || 'info'}`; div.textContent = h.msg; ui.logContainer.appendChild(div); }); ui.logContainer.scrollTop = ui.logContainer.scrollHeight; } } catch {} // Bind events bindEvents(); // Initialize auto-scroll button state const autoScrollBtn = document.getElementById('toggle-auto-scroll-btn'); if (autoScrollBtn && typeof state.autoScrollLogs === 'boolean') { autoScrollBtn.style.opacity = state.autoScrollLogs ? '1' : '0.5'; autoScrollBtn.title = state.autoScrollLogs ? 'Auto-scroll: ON' : 'Auto-scroll: OFF'; } // Watch for theme changes const observer = new MutationObserver(() => { const newDarkMode = utils.detectDarkMode(); if (newDarkMode !== state.isDarkMode) { state.isDarkMode = newDarkMode; ui.mainContainer.className = state.isDarkMode ? 'dark-mode' : 'light-mode'; if (state.isMinimized) ui.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(); utils.log('UI initialized successfully'); }; // 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 panel = document.getElementById('chatgpt-automation-ui'); if (!panel) { createUI(); panel = document.getElementById('chatgpt-automation-ui'); } if (!panel) return; const show = panel.style.display === 'none'; panel.style.display = show ? 'block' : 'none'; ui.mainContainer = panel; state.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 = uiState.load(); const shouldShow = savedState.visible === true || (savedState.visible == null && CONFIG.DEFAULT_VISIBLE); if (shouldShow && !document.getElementById('chatgpt-automation-ui')) { createUI(); } return true; }; const startHeaderObserver = () => { if (state.headerObserverStarted) return; state.headerObserverStarted = true; const ensure = () => { try { // Recreate launcher if missing mountHeaderLauncher(); // Ensure UI matches persisted visibility const savedState = uiState.load(); const panel = document.getElementById('chatgpt-automation-ui'); const shouldShow = savedState.visible === true || (savedState.visible == null && CONFIG.DEFAULT_VISIBLE); if (panel) { panel.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 (!ui.statusIndicator) return; const statusTexts = { idle: 'Ready', processing: 'Typing...', waiting: 'Waiting for response...', complete: 'Complete', error: 'Error', }; ui.statusIndicator.className = `status-indicator status-${status}`; const textEl = ui.statusIndicator.querySelector('.status-text'); if (textEl) textEl.textContent = statusTexts[status] || 'Unknown'; }; const updateProgress = (done, total) => { // Use header mini progress as single source of truth. Keep in-panel progress hidden. if (!ui.miniProgress || !ui.miniFill || !ui.miniLabel) return; const show = total > 0; // Hide the in-panel progress container entirely (not used) try { if (ui.progressBar) ui.progressBar.style.display = 'none'; } catch {} ui.miniProgress.style.display = show ? 'flex' : 'none'; if (!show) { ui.miniFill.style.width = '0%'; ui.miniLabel.textContent = '0/0'; return; } const pct = total ? Math.round((done / total) * 100) : 0; ui.miniFill.style.width = pct + '%'; ui.miniLabel.textContent = `${done}/${total}`; }; const updateSubProgress = (done, total) => { // Use header mini sub-progress as single source of truth. Keep in-panel sub progress hidden. if (!ui.miniSubProgress || !ui.miniSubFill || !ui.miniSubLabel) return; const show = total > 0; try { if (ui.progressBarSub) ui.progressBarSub.style.display = 'none'; const subText = document.getElementById('progress-text-sub'); if (subText) subText.style.display = 'none'; } catch {} ui.miniSubProgress.style.display = show ? 'flex' : 'none'; if (!show) { ui.miniSubFill.style.width = '0%'; ui.miniSubLabel.textContent = '0/0'; return; } const pct = total ? Math.round((done / total) * 100) : 0; ui.miniSubFill.style.width = pct + '%'; ui.miniSubLabel.textContent = `${done}/${total}`; }; // Unified progress helper that clamps values and drives header mini bars const refreshBatchProgress = (doneLike, totalLike) => { const total = Math.max(0, Number(totalLike || 0)); const done = Math.max(0, Math.min(Number(doneLike || 0), total)); updateProgress(done, total); return { done, total }; }; // Safely remove N items from the head of dynamicElements, keep JSON/textarea in sync const removeHeadItems = (count = 1) => { if (!Array.isArray(state.dynamicElements) || count <= 0) return; try { state.dynamicElements.splice(0, count); } catch {} try { if (!state.chainDefinition) { const txt = document.getElementById('chain-json-input')?.value || '{}'; state.chainDefinition = JSON.parse(txt); } state.chainDefinition.dynamicElements = state.dynamicElements; const chainInput = document.getElementById('chain-json-input'); if (chainInput) chainInput.value = JSON.stringify(state.chainDefinition, null, 2); } catch {} try { const dynEl = document.getElementById('dynamic-elements-input'); if (dynEl) dynEl.value = JSON.stringify(state.dynamicElements, null, 2); } catch {} }; // Allow canceling long runs const stopBatchProcessing = () => { state.cancelRequested = true; utils.log('Stop requested'); }; // Function to start a new chat (language independent, uses data-testid) const startNewChat = async () => { utils.log('Starting new chat...'); const btn = document.querySelector('a[data-testid="create-new-chat-button"]'); if (btn) { utils.log('Using new chat button...'); btn.click(); await utils.sleep(1000); return true; } const homeLink = document.querySelector('a[href="/"]'); if (homeLink && (homeLink.textContent || '').trim() !== '') { utils.log('Using home link...'); homeLink.click(); await utils.sleep(1000); return true; } utils.log('Failed to start a new chat', 'warning'); return false; }; 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'); // Persist active tab utils.saveToStorage(STORAGE_KEYS.activeTab, tabName); }); }); // 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) => { state.autoRemoveProcessed = e.target.checked; utils.log( `Auto-remove processed items: ${state.autoRemoveProcessed ? 'enabled' : 'disabled'}` ); utils.saveToStorage(STORAGE_KEYS.autoRemove, state.autoRemoveProcessed); }); // Wait time input document.getElementById('wait-time-input').addEventListener('change', (e) => { const value = parseInt(e.target.value); if (value >= 0 && value <= 30000) { state.batchWaitTime = value; utils.log(`Wait time between items set to ${value}ms`); utils.saveToStorage(STORAGE_KEYS.waitTime, state.batchWaitTime); } else { e.target.value = state.batchWaitTime; utils.log('Invalid wait time, keeping current value', 'warning'); } }); // Per-step wait time input const stepWaitEl = document.getElementById('step-wait-input'); if (stepWaitEl) { stepWaitEl.addEventListener('change', (e) => { const value = parseInt(e.target.value); if (value >= 0 && value <= 30000) { utils.saveToStorage(STORAGE_KEYS.stepWaitTime, value); utils.log(`Wait time between steps set to ${value}ms`); } else { const saved = parseInt(GM_getValue(STORAGE_KEYS.stepWaitTime, 0)); e.target.value = String(!Number.isNaN(saved) ? saved : 0); utils.log('Invalid per-step wait time, keeping current value', 'warning'); } }); } const toggleLogVisibility = () => { const logWrap = document.getElementById('log-container'); if (!logWrap) return; const currentlyHidden = logWrap.style.display === 'none'; const willShow = currentlyHidden; logWrap.style.display = willShow ? 'flex' : 'none'; // Toggle class on main container so CSS can adapt minimized height if (ui.mainContainer) { ui.mainContainer.classList.toggle('log-open', willShow); } utils.saveToStorage(STORAGE_KEYS.logVisible, willShow); }; document.getElementById('header-log-toggle').addEventListener('click', toggleLogVisibility); // Clear log button document.getElementById('clear-log-btn').addEventListener('click', () => { if (ui.logContainer) ui.logContainer.innerHTML = ''; utils.saveToStorage(STORAGE_KEYS.logHistory, []); utils.log('Log cleared'); }); // Stop button in minimized header document.getElementById('stop-mini-btn').addEventListener('click', () => { stopBatchProcessing(); const stopRunBtn = document.getElementById('stop-run-btn'); if (stopRunBtn) stopRunBtn.style.display = 'none'; const stopBtn = document.getElementById('stop-batch-btn'); if (stopBtn) stopBtn.style.display = 'none'; const stopMini = document.getElementById('stop-mini-btn'); if (stopMini) stopMini.style.display = 'none'; }); // Toggle auto-scroll button document.getElementById('toggle-auto-scroll-btn').addEventListener('click', () => { state.autoScrollLogs = !state.autoScrollLogs; const btn = document.getElementById('toggle-auto-scroll-btn'); btn.style.opacity = state.autoScrollLogs ? '1' : '0.5'; btn.title = state.autoScrollLogs ? 'Auto-scroll: ON' : 'Auto-scroll: OFF'; utils.log(`Auto-scroll logs: ${state.autoScrollLogs ? 'enabled' : 'disabled'}`); if (state.autoScrollLogs && ui.logContainer) ui.logContainer.scrollTop = ui.logContainer.scrollHeight; utils.saveToStorage(STORAGE_KEYS.autoScroll, state.autoScrollLogs); }); document.getElementById('minimize-btn').addEventListener('click', () => { state.isMinimized = !state.isMinimized; if (state.isMinimized) { // Save previous explicit height if present ui.mainContainer.classList.add('minimized'); } else { ui.mainContainer.classList.remove('minimized'); ui.mainContainer.style.height = ''; // after finishing resize and saving state isResizing = false; // allow CSS/auto layout to reclaim sizing by removing inline size overrides if (ui.mainContainer) { ui.mainContainer.style.removeProperty('width'); ui.mainContainer.style.removeProperty('height'); } saveUIState(true); } saveUIState(true); // Immediate save for user action }); // Close button document.getElementById('close-btn').addEventListener('click', () => { ui.mainContainer.style.display = 'none'; state.uiVisible = false; saveUIState(true); // Immediate save for user action utils.log('UI closed'); }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { // Ctrl/Cmd + Enter to send if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { const sendBtn = document.getElementById('send-btn'); if (sendBtn) sendBtn.click(); e.preventDefault(); } // Escape to minimize if (e.key === 'Escape') { 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 = ui.mainContainer.getBoundingClientRect(); dragOffset.x = e.clientX - rect.left; dragOffset.y = e.clientY - rect.top; header.style.userSelect = 'none'; e.preventDefault(); }); ui.resizeHandle.addEventListener('mousedown', (e) => { isResizing = true; resizeStartX = e.clientX; resizeStartY = e.clientY; resizeStartWidth = ui.mainContainer.offsetWidth; resizeStartHeight = ui.mainContainer.offsetHeight; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (isDragging) { const x = e.clientX - dragOffset.x; const y = e.clientY - dragOffset.y; ui.mainContainer.style.left = `${Math.max(0, Math.min(x, window.innerWidth - ui.mainContainer.offsetWidth))}px`; ui.mainContainer.style.top = `${Math.max(0, Math.min(y, window.innerHeight - ui.mainContainer.offsetHeight))}px`; ui.mainContainer.style.right = 'auto'; saveUIState(); // Debounced for drag operations } else if (isResizing) { // Clamp resizing to reasonable window bounds (sizes are automatic) const rawWidth = resizeStartWidth + (e.clientX - resizeStartX); const rawHeight = resizeStartHeight + (e.clientY - resizeStartY); const newWidth = Math.max(200, Math.min(window.innerWidth, rawWidth)); const newHeight = Math.max(120, Math.min(window.innerHeight, rawHeight)); ui.mainContainer.style.width = `${newWidth}px`; ui.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; } }); // Persist loop checkbox when used const loopEl = document.getElementById('loop-checkbox'); loopEl.addEventListener('change', (e) => { state.isLooping = e.target.checked; utils.saveToStorage(STORAGE_KEYS.loop, state.isLooping); }); // Settings: Debug mode const debugEl = document.getElementById('debug-mode-checkbox'); if (debugEl) { debugEl.addEventListener('change', (e) => { CONFIG.DEBUG_MODE = !!e.target.checked; utils.saveToStorage(STORAGE_KEYS.configDebug, CONFIG.DEBUG_MODE); utils.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; utils.saveToStorage(STORAGE_KEYS.configTimeout, v); utils.log(`Response timeout set to ${v}ms`); } else { e.target.value = String(CONFIG.RESPONSE_TIMEOUT); utils.log('Invalid response timeout', 'warning'); } }); } // 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 {} // If user disables default visibility and UI wasn't explicitly opened, keep current visibility but don't force-open later utils.log(`Default visibility ${CONFIG.DEFAULT_VISIBLE ? 'ON' : 'OFF'}`); }); // Restore log visibility try { const logWrap = document.getElementById('log-container'); const vis = GM_getValue(STORAGE_KEYS.logVisible, false); if (logWrap) { logWrap.style.display = vis ? 'flex' : 'none'; } if (ui.mainContainer) { ui.mainContainer.classList.toggle('log-open', !!vis); } } catch {} // Chain UI: basic actions const chainInput = document.getElementById('chain-json-input'); const sampleItemsEl = document.getElementById('dynamic-elements-input'); const chainCards = document.getElementById('chain-cards'); const refreshChainCards = () => { if (!chainCards) return; chainCards.innerHTML = ''; let chain; try { chain = JSON.parse(chainInput.value || '{}'); // Update global state.chainDefinition when parsing JSON state.chainDefinition = chain; } catch { chain = null; state.chainDefinition = null; } // Reflect dynamicElements in the dedicated textarea if ( chain && (Array.isArray(chain.dynamicElements) || typeof chain.dynamicElements === 'string') && sampleItemsEl ) { try { sampleItemsEl.value = JSON.stringify(chain.dynamicElements, null, 2); } catch { // if dynamicElements is a function string, show raw try { sampleItemsEl.value = String(chain.dynamicElements); } catch {} } } if (!chain || !Array.isArray(chain.steps) || chain.steps.length === 0) { // Chain cards will show empty state due to CSS :empty selector return; } chain.steps.forEach((step) => { const card = document.createElement('div'); card.className = 'chain-card'; card.dataset.stepId = step.id; const typeDisplay = step.type === 'template' ? 'Template (Batch)' : step.type === 'js' ? 'JavaScript' : step.type === 'prompt' ? 'Prompt' : step.type === 'http' ? 'HTTP Request' : step.type; card.innerHTML = ` <div class="title">${step.title || step.id || '(untitled)'}</div> <div class="meta">type: ${typeDisplay}${step.next ? ` → ${step.next}` : ''}</div> <div class="actions"> <button class="btn btn-secondary btn-sm" data-action="edit" title="Edit step"><svg width="14" height="14" viewBox="0 0 640 640" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M100.4 417.2C104.5 402.6 112.2 389.3 123 378.5L304.2 197.3L338.1 163.4C354.7 180 389.4 214.7 442.1 267.4L476 301.3L442.1 335.2L260.9 516.4C250.2 527.1 236.8 534.9 222.2 539L94.4 574.6C86.1 576.9 77.1 574.6 71 568.4C64.9 562.2 62.6 553.3 64.9 545L100.4 417.2zM156 413.5C151.6 418.2 148.4 423.9 146.7 430.1L122.6 517L209.5 492.9C215.9 491.1 221.7 487.8 226.5 483.2L155.9 413.5zM510 267.4C493.4 250.8 458.7 216.1 406 163.4L372 129.5C398.5 103 413.4 88.1 416.9 84.6C430.4 71 448.8 63.4 468 63.4C487.2 63.4 505.6 71 519.1 84.6L554.8 120.3C568.4 133.9 576 152.3 576 171.4C576 190.5 568.4 209 554.8 222.5C551.3 226 536.4 240.9 509.9 267.4z"/></svg></button> <button class="btn btn-danger btn-sm" data-action="delete" title="Delete step"><svg width="14" height="14" viewBox="0 0 448 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M136.7 5.9C141.1-7.2 153.3-16 167.1-16l113.9 0c13.8 0 26 8.8 30.4 21.9L320 32 416 32c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 96C14.3 96 0 81.7 0 64S14.3 32 32 32l96 0 8.7-26.1zM32 144l384 0 0 304c0 35.3-28.7 64-64 64L96 512c-35.3 0-64-28.7-64-64l0-304zm88 64c-13.3 0-24 10.7-24 24l0 192c0 13.3 10.7 24 24 24s24-10.7 24-24l0-192c0-13.3-10.7-24-24-24zm104 0c-13.3 0-24 10.7-24 24l0 192c0 13.3 10.7 24 24 24s24-10.7 24-24l0-192c0-13.3-10.7-24-24-24zm104 0c-13.3 0-24 10.7-24 24l0 192c0 13.3 10.7 24 24 24s24-10.7 24-24l0-192c0-13.3-10.7-24-24-24z"/></svg></button> </div> `; card .querySelector('[data-action="edit"]') .addEventListener('click', () => openStepEditor(step.id)); card.querySelector('[data-action="delete"]').addEventListener('click', () => { if (confirm(`Delete step "${step.title || step.id}"?`)) { chain.steps = chain.steps.filter((s) => s.id !== step.id); // Remove references to this step chain.steps.forEach((s) => { if (s.next === step.id) s.next = ''; }); // Update entry point if needed if (chain.entryId === step.id) { chain.entryId = chain.steps.length > 0 ? chain.steps[0].id : ''; } chainInput.value = JSON.stringify(chain, null, 2); utils.saveToStorage(STORAGE_KEYS.chainDef, chainInput.value); refreshChainCards(); utils.log(`Step "${step.title || step.id}" deleted`); } }); chainCards.appendChild(card); }); }; const openStepEditor = (stepId) => { let chain; try { chain = JSON.parse(chainInput.value || '{}'); } catch { chain = { steps: [] }; } if (!Array.isArray(chain.steps)) chain.steps = []; let step = chain.steps.find((s) => s.id === stepId); if (!step) { step = { id: stepId || `step-${Date.now()}`, type: 'prompt', title: '', template: '' }; chain.steps.push(step); } const modal = document.getElementById('chain-step-modal'); modal.style.display = 'block'; modal.setAttribute('aria-hidden', 'false'); // Populate fields document.getElementById('step-id-input').value = step.id || ''; document.getElementById('step-title-input').value = step.title || ''; document.getElementById('step-type-select').value = step.type || 'prompt'; // Per-step options const respTypeSel = document.getElementById('step-response-type'); if (respTypeSel) respTypeSel.value = step.responseType || 'text'; const newChatCb = document.getElementById('step-newchat-checkbox'); if (newChatCb) newChatCb.checked = !!step.newChat; // Prompt content const promptEl = document.getElementById('step-prompt-template'); if (promptEl) promptEl.value = step.template || step.content || step.message || ''; // Template fields document.getElementById('step-template-input').value = step.template || ''; const stepElementsEl = document.getElementById('step-template-elements'); stepElementsEl.value = step.elements || ''; const useSamplesCb = document.getElementById('step-use-dynamicelements-checkbox'); // Override-but-restore: when checked, populate the step elements from chain.dynamicElements // and disable editing; when unchecked, restore the previous per-step value. if (useSamplesCb) { useSamplesCb.checked = !!step.useDynamicElements; // Replace any existing handler to avoid duplicates useSamplesCb.onchange = (e) => { try { if (e.target.checked) { // Backup current step value so it can be restored later try { modal.dataset.backupStepElements = stepElementsEl.value || ''; } catch {} // Populate from chain.dynamicElements (prefer chain parsed from editor) try { if ( chain && (Array.isArray(chain.dynamicElements) || typeof chain.dynamicElements === 'string') ) { try { stepElementsEl.value = JSON.stringify(chain.dynamicElements, null, 2); } catch { stepElementsEl.value = String(chain.dynamicElements); } } else { stepElementsEl.value = ''; } } catch {} stepElementsEl.disabled = true; } else { // Restore backed-up value (if any) and re-enable editing try { const bak = modal.dataset.backupStepElements; stepElementsEl.value = bak != null ? bak : step.elements || ''; delete modal.dataset.backupStepElements; } catch { stepElementsEl.value = step.elements || ''; } stepElementsEl.disabled = false; } } catch (err) { utils.log('Failed to toggle useDynamicElements: ' + err.message, 'error'); } }; // Initialize UI state according to the checkbox if (useSamplesCb.checked) { // Trigger handler to populate from chain useSamplesCb.dispatchEvent(new Event('change')); } else { stepElementsEl.disabled = false; } } // Prompt document.getElementById('step-prompt-template').value = step.template || ''; // HTTP fields document.getElementById('step-http-url').value = step.url || ''; document.getElementById('step-http-method').value = (step.method || 'GET').toUpperCase(); document.getElementById('step-http-headers').value = step.headers ? JSON.stringify(step.headers) : ''; document.getElementById('step-http-body').value = step.bodyTemplate || ''; // JavaScript document.getElementById('step-js-code').value = step.code || ''; // Populate next step selector with auto-suggestion const nextSel = document.getElementById('step-next-select'); nextSel.innerHTML = '<option value="">(end)</option>'; const currentIndex = chain.steps.findIndex((s) => s.id === step.id); chain.steps.forEach((s, index) => { if (s.id !== step.id) { // Don't include self const opt = document.createElement('option'); opt.value = s.id; const labelParts = [s.id]; if (s.title) labelParts.push('— ' + s.title); if (s.type) labelParts.push('(' + s.type + ')'); opt.textContent = labelParts.join(' '); if (step.next === s.id) { opt.selected = true; } else if (!step.next && index === currentIndex + 1) { // Auto-suggest next sequential step opt.selected = true; step.next = s.id; } nextSel.appendChild(opt); } }); const onTypeChange = () => { const type = document.getElementById('step-type-select').value; // Clear all fields first when type changes to prevent contamination if (step.type && step.type !== type) { // Clear previous type's fields from the step object and UI delete step.template; delete step.elements; delete step.code; delete step.url; delete step.method; delete step.headers; delete step.bodyTemplate; delete step.message; // Clear form inputs const clear = (id) => { const el = document.getElementById(id); if (el) el.value = id === 'step-http-method' ? 'GET' : ''; }; [ 'step-prompt-template', 'step-template-input', 'step-template-elements', 'step-js-code', 'step-http-url', 'step-http-headers', 'step-http-body', 'step-http-method', ].forEach(clear); // Clear form inputs document.getElementById('step-prompt-template').value = ''; document.getElementById('step-template-input').value = ''; document.getElementById('step-template-elements').value = ''; document.getElementById('step-js-code').value = ''; document.getElementById('step-http-url').value = ''; document.getElementById('step-http-method').value = 'GET'; document.getElementById('step-http-headers').value = ''; document.getElementById('step-http-body').value = ''; } // Update step type step.type = type; // Toggle field groups based on step type modal .querySelectorAll('[data-field="prompt"]') .forEach((el) => (el.style.display = type === 'prompt' ? 'block' : 'none')); modal .querySelectorAll('[data-field="template"]') .forEach((el) => (el.style.display = type === 'template' ? 'block' : 'none')); modal .querySelectorAll('[data-field="http"]') .forEach((el) => (el.style.display = type === 'http' ? 'block' : 'none')); modal .querySelectorAll('[data-field="js"]') .forEach((el) => (el.style.display = type === 'js' ? 'block' : 'none')); }; document.getElementById('step-type-select').onchange = onTypeChange; onTypeChange(); const saveBtn = document.getElementById('save-step-btn'); const deleteBtn = document.getElementById('delete-step-btn'); const closeBtn = document.getElementById('close-step-modal-btn'); const closeModal = () => { modal.style.display = 'none'; modal.setAttribute('aria-hidden', 'true'); }; closeBtn.onclick = closeModal; deleteBtn.onclick = () => { if (confirm(`Delete step "${step.title || step.id}"?`)) { chain.steps = chain.steps.filter((s) => s.id !== step.id); // Remove references chain.steps.forEach((s) => { if (s.next === step.id) s.next = ''; }); // Update entry point if needed if (chain.entryId === step.id) { chain.entryId = chain.steps.length > 0 ? chain.steps[0].id : ''; } chainInput.value = JSON.stringify(chain, null, 2); utils.saveToStorage(STORAGE_KEYS.chainDef, chainInput.value); refreshChainCards(); closeModal(); utils.log(`Step "${step.title || step.id}" deleted`); } }; saveBtn.onclick = () => { const newId = document.getElementById('step-id-input').value.trim() || step.id; const oldId = step.id; step.id = newId; step.title = document.getElementById('step-title-input').value.trim(); step.type = document.getElementById('step-type-select').value; step.next = document.getElementById('step-next-select').value; // Clear all type-specific fields first delete step.template; delete step.elements; delete step.code; delete step.url; delete step.method; delete step.headers; delete step.bodyTemplate; delete step.message; delete step.responseType; delete step.newChat; delete step.useDynamicElements; // Save type-specific fields based on current type if (step.type === 'template') { step.template = document.getElementById('step-template-input').value; step.elements = document.getElementById('step-template-elements').value; step.useDynamicElements = !!document.getElementById('step-use-dynamicelements-checkbox') ?.checked; } else if (step.type === 'prompt') { step.template = document.getElementById('step-prompt-template').value; step.responseType = document.getElementById('step-response-type')?.value || 'text'; step.newChat = !!document.getElementById('step-newchat-checkbox')?.checked; } else if (step.type === 'http') { step.url = document.getElementById('step-http-url').value.trim(); step.method = document.getElementById('step-http-method').value.trim(); try { const headerText = document.getElementById('step-http-headers').value.trim(); step.headers = headerText ? JSON.parse(headerText) : {}; } catch { step.headers = {}; } step.bodyTemplate = document.getElementById('step-http-body').value; } else if (step.type === 'js') { step.code = document.getElementById('step-js-code').value; } // If ID changed, update references if (oldId !== newId) { chain.steps.forEach((s) => { if (s.next === oldId) s.next = newId; }); if (chain.entryId === oldId) chain.entryId = newId; } chainInput.value = JSON.stringify(chain, null, 2); utils.saveToStorage(STORAGE_KEYS.chainDef, chainInput.value); refreshChainCards(); closeModal(); utils.log(`Step "${step.title || step.id}" saved`); // Note: preset save is handled by the dedicated icon in the popup }; }; const addStepBtn = document.getElementById('add-step-btn'); if (addStepBtn) addStepBtn.addEventListener('click', () => { let chain; try { chain = JSON.parse(chainInput.value || '{}'); } catch { chain = {}; } if (!chain.steps) chain.steps = []; const id = `step-${(chain.steps.length || 0) + 1}`; const newStep = { id, title: `Step ${chain.steps.length + 1}`, type: 'prompt', template: '', }; // Auto-link the previous step if it doesn't have a next if (chain.steps.length > 0) { const lastStep = chain.steps[chain.steps.length - 1]; if (!lastStep.next) { lastStep.next = id; } } chain.steps.push(newStep); if (!chain.entryId) chain.entryId = id; chainInput.value = JSON.stringify(chain, null, 2); utils.saveToStorage(STORAGE_KEYS.chainDef, chainInput.value); refreshChainCards(); // Open editor and default to "Select preset" openStepEditor(id); // Reset the preset selector to show "Select preset..." setTimeout(() => { const presetSelect = document.getElementById('step-preset-select'); if (presetSelect) presetSelect.value = ''; }, 100); }); const validateChainBtn = document.getElementById('validate-chain-btn'); if (validateChainBtn) validateChainBtn.addEventListener('click', () => { // Ensure log is visible when validating for better feedback try { const logWrap = document.getElementById('log-container'); if (logWrap && logWrap.style.display === 'none') { logWrap.style.display = 'flex'; if (ui.mainContainer) ui.mainContainer.classList.add('log-open'); utils.saveToStorage(STORAGE_KEYS.logVisible, true); } } catch {} try { const c = JSON.parse(chainInput.value || '{}'); if (!c.entryId) throw new Error('Missing entryId'); if (!Array.isArray(c.steps) || !c.steps.length) throw new Error('No steps'); const ids = new Set(c.steps.map((s) => s.id)); if (!ids.has(c.entryId)) throw new Error('entryId not found among steps'); c.steps.forEach((s) => { if (s.next && !ids.has(s.next)) throw new Error(`Step ${s.id} next '${s.next}' not found`); }); utils.log('Chain valid'); } catch (e) { utils.log('Chain invalid: ' + e.message, 'error'); } }); const runChainBtn = document.getElementById('run-chain-btn'); const stopRunBtn = document.getElementById('stop-run-btn'); if (stopRunBtn) { stopRunBtn.addEventListener('click', () => { stopBatchProcessing(); stopRunBtn.style.display = 'none'; }); } if (runChainBtn) runChainBtn.addEventListener('click', async () => { // When running, load whatever is currently in the dynamic elements textarea try { const dynEl = document.getElementById('dynamic-elements-input'); let items = []; if (dynEl) { const raw = (dynEl.value || '').trim(); if (raw) { if (raw.startsWith('[') || raw.startsWith('{')) { try { const parsed = JSON.parse(raw); items = Array.isArray(parsed) ? parsed : [parsed]; } catch (e) { // fallback to processor for function-style inputs try { const parsed = await processors.parseDynamicElements(raw); items = Array.isArray(parsed) ? parsed : [parsed]; } catch {} } } else { try { const parsed = await processors.parseDynamicElements(raw); items = Array.isArray(parsed) ? parsed : [parsed]; } catch {} } } } state.dynamicElements = items; // If the new list is shorter than what we've already processed, clamp the current index try { if ( typeof state.currentBatchIndex === 'number' && state.currentBatchIndex > items.length ) { state.currentBatchIndex = Math.max(0, items.length); } } catch {} // Keep chainDefinition in sync so the JSON reflects the runtime items try { if (!state.chainDefinition) { state.chainDefinition = JSON.parse( document.getElementById('chain-json-input').value || '{}' ); } state.chainDefinition.dynamicElements = items; const chainInput = document.getElementById('chain-json-input'); if (chainInput) chainInput.value = JSON.stringify(state.chainDefinition, null, 2); } catch {} } catch (e) { utils.log('Failed to read dynamic elements before run: ' + e.message, 'warning'); } if (stopRunBtn) stopRunBtn.style.display = 'inline-flex'; await runChainWithBatch(); if (stopRunBtn) stopRunBtn.style.display = 'none'; }); // Generic JSON formatter for overlay buttons const registerJsonFormatter = (btnId, inputId, opts = {}) => { const btn = document.getElementById(btnId); if (!btn) return; btn.addEventListener('click', async () => { try { const src = document.getElementById(inputId); if (!src) return; const val = (src.value || '').trim(); if (!val) return; let parsed; if (!opts.allowFunction && (val.startsWith('[') || val.startsWith('{'))) parsed = JSON.parse(val); else parsed = await processors.parseDynamicElements(val); src.value = JSON.stringify(parsed, null, 2); utils.log(`${opts.label || 'JSON'} formatted`); } catch (e) { utils.log(`Invalid ${opts.label || 'value'}: ${e.message}`, 'error'); } }); }; registerJsonFormatter('format-chain-json-btn', 'chain-json-input', { label: 'Chain JSON', allowFunction: false, }); registerJsonFormatter('format-dyn-elements-btn', 'dynamic-elements-input', { label: 'Dynamic elements', allowFunction: true, }); registerJsonFormatter('format-step-elements-btn', 'step-template-elements', { label: 'Step elements', allowFunction: true, }); registerJsonFormatter('format-http-headers-btn', 'step-http-headers', { label: 'HTTP headers', allowFunction: false, }); registerJsonFormatter('format-http-body-btn', 'step-http-body', { label: 'HTTP body', allowFunction: false, }); // Change events to keep cards in sync and persist data if (chainInput) { chainInput.addEventListener('input', () => { let parsed = null; try { parsed = JSON.parse(chainInput.value || '{}'); } catch { /* ignore parse errors during typing */ } if (parsed) { state.chainDefinition = parsed; refreshChainCards(); } else { // if invalid, still clear cards to reflect invalid state refreshChainCards(); } utils.saveToStorage(STORAGE_KEYS.chainDef, chainInput.value); }); } // Stop auto-syncing dynamic elements on input; apply explicitly via button const applyDynBtn = document.getElementById('apply-dyn-elements-btn'); if (applyDynBtn) { applyDynBtn.addEventListener('click', async () => { try { const src = document.getElementById('dynamic-elements-input'); const raw = (src?.value || '').trim(); if (!raw) { state.dynamicElements = []; utils.log('Dynamic elements cleared'); try { if (!state.chainDefinition) { const txt = document.getElementById('chain-json-input')?.value || '{}'; state.chainDefinition = JSON.parse(txt); } state.chainDefinition.dynamicElements = []; const chainInput = document.getElementById('chain-json-input'); if (chainInput) chainInput.value = JSON.stringify(state.chainDefinition, null, 2); } catch {} refreshBatchProgress(0, 0); return; } let items; if (raw.startsWith('[') || raw.startsWith('{')) items = JSON.parse(raw); else items = await processors.parseDynamicElements(raw); if (!Array.isArray(items)) items = [items]; state.dynamicElements = items; utils.log(`Applied ${items.length} dynamic element(s) to runtime`); try { if (!state.chainDefinition) { const txt = document.getElementById('chain-json-input')?.value || '{}'; state.chainDefinition = JSON.parse(txt); } state.chainDefinition.dynamicElements = items; const chainInput = document.getElementById('chain-json-input'); if (chainInput) chainInput.value = JSON.stringify(state.chainDefinition, null, 2); } catch {} if (!state.isProcessing) refreshBatchProgress(0, items.length); } catch (e) { utils.log('Invalid dynamic elements: ' + e.message, 'error'); } }); } // Live-sync dynamic elements while running: when user edits the textarea during a run, // parse and update state.dynamicElements and the chain JSON so the running batch reflects changes. const dynInputEl = document.getElementById('dynamic-elements-input'); if (dynInputEl) { dynInputEl.addEventListener('input', async (e) => { // If not processing, do nothing — user must press Apply to change runtime by default. if (!state.isProcessing) return; try { const raw = (e.target.value || '').trim(); let items = []; if (raw) { if (raw.startsWith('[') || raw.startsWith('{')) { try { const parsed = JSON.parse(raw); items = Array.isArray(parsed) ? parsed : [parsed]; } catch { try { const parsed = await processors.parseDynamicElements(raw); items = Array.isArray(parsed) ? parsed : [parsed]; } catch {} } } else { try { const parsed = await processors.parseDynamicElements(raw); items = Array.isArray(parsed) ? parsed : [parsed]; } catch {} } } // Replace live items but preserve already-processed count by removing leading items // that were already processed when appropriate. Simpler approach: replace full list. state.dynamicElements = items; // Update chain JSON representation for visibility try { if (!state.chainDefinition) state.chainDefinition = JSON.parse( document.getElementById('chain-json-input').value || '{}' ); state.chainDefinition.dynamicElements = items; const chainInput = document.getElementById('chain-json-input'); if (chainInput) chainInput.value = JSON.stringify(state.chainDefinition, null, 2); } catch {} utils.log(`Runtime dynamic elements updated (${items.length} items) while running`); // Refresh header progress: denominator = processed so far + remaining items const done = Math.max(0, Number(state.processedCount || 0)); refreshBatchProgress(Math.min(done, done + items.length), done + items.length); } catch (err) { utils.log('Failed to live-apply dynamic elements: ' + err.message, 'error'); } }); } refreshChainCards(); // Presets: populate selects and wire buttons (normalized storage) const loadPresetSelects = () => { // Steps presets let stepsMapRaw = GM_getValue(STORAGE_KEYS.presetsSteps, {}); let stepsMap = {}; try { stepsMap = typeof stepsMapRaw === 'string' ? JSON.parse(stepsMapRaw) : stepsMapRaw || {}; } catch { stepsMap = {}; } const defaultSteps = { 'Get Weather': { type: 'http', url: 'https://wttr.in/{item}?format=j1', method: 'GET', headers: { 'Content-Type': 'application/json' }, }, 'Extract Data': { type: 'js', code: 'const raw = steps.weather?.rawText ?? steps.weather?.data;\nconst data = typeof raw === "string" ? JSON.parse(raw) : raw;\nconst tempC = Number(data?.current_condition?.[0]?.temp_C);\nutils.log("Temperature °C:", tempC);\nreturn isNaN(tempC) ? null : tempC;', }, 'Ask ChatGPT': { type: 'prompt', template: 'Explain the implications of the temperature {steps.extractData.response} K.', }, 'Basic Prompt': { type: 'prompt', template: 'Please analyze {item} and provide 3 key insights.', }, 'API Call': { type: 'http', url: 'https://jsonplaceholder.typicode.com/posts/{item}', method: 'GET', headers: { 'Content-Type': 'application/json' }, }, 'Reddit .json': { type: 'http', url: 'https://www.reddit.com/.json', method: 'GET', headers: { 'Content-Type': 'application/json' }, }, 'Process JSON': { type: 'js', code: 'const raw = steps.apiCall?.rawText ?? steps.apiCall?.data;\nconst data = typeof raw === "string" ? JSON.parse(raw) : raw;\nutils.log("Post title:", data?.title);\nreturn data?.title;', }, }; Object.entries(defaultSteps).forEach(([name, preset]) => { if (!Object.prototype.hasOwnProperty.call(stepsMap, name)) stepsMap[name] = preset; }); try { GM_setValue(STORAGE_KEYS.presetsSteps, stepsMap); } catch {} // Chains presets let chainsMapRaw = GM_getValue(STORAGE_KEYS.presetsChains, {}); let chainsMap = {}; try { chainsMap = typeof chainsMapRaw === 'string' ? JSON.parse(chainsMapRaw) : chainsMapRaw || {}; } catch { chainsMap = {}; } const defaultChains = { 'Weather Analysis': JSON.stringify( { dynamicElements: ['London', 'Tokyo', 'New York'], entryId: 'weather', steps: [ { id: 'weather', type: 'http', url: 'https://wttr.in/{item}?format=j1', method: 'GET', next: 'extract', }, { id: 'extract', type: 'js', code: 'const raw = steps.weather?.rawText ?? steps.weather?.data;\nconst data = typeof raw === "string" ? JSON.parse(raw) : raw;\nconst tempC = Number(data?.current_condition?.[0]?.temp_C);\nutils.log(`Weather for {item}: ${isNaN(tempC)?"n/a":tempC+"°C"}`);\nreturn isNaN(tempC) ? "Unknown" : tempC + "°C";', next: 'chat', }, { id: 'chat', type: 'prompt', template: 'In {item}, the current temperature is {steps.extract.response}. Share a fun fact about this city.', }, ], }, null, 2 ), 'Content Research': JSON.stringify( { dynamicElements: ['JavaScript', 'TypeScript', 'WebAssembly'], entryId: 'search', steps: [ { id: 'search', type: 'prompt', template: 'Research {item} and provide 3 key facts', next: 'summarize', }, { id: 'summarize', type: 'js', code: 'const text = steps.search.response || "";\nreturn text.slice(0,200) + (text.length>200?"...":"");', next: 'expand', }, { id: 'expand', type: 'prompt', template: 'Using this summary: {steps.summarize.response}, write a short article about {item}', }, ], }, null, 2 ), 'Simple Chain': JSON.stringify( { dynamicElements: ['London', 'Tokyo', 'New York'], entryId: 'step1', steps: [ { id: 'step1', type: 'prompt', template: 'Tell me about {item}', next: 'step2' }, { id: 'step2', type: 'template', template: 'Summary: {steps.step1.response}' }, ], }, null, 2 ), 'Reddit JSON': JSON.stringify( { dynamicElements: ['javascript'], entryId: 'redditGet', steps: [ { id: 'redditGet', type: 'http', url: 'https://www.reddit.com/.json', method: 'GET', next: 'logJson', }, { id: 'logJson', type: 'js', code: 'const raw = steps.redditGet?.rawText ?? steps.redditGet?.data;\n' + 'const data = typeof raw === "string" ? (function(){ try { return JSON.parse(raw); } catch(e){ return raw; } })() : raw;\n' + 'const children = Array.isArray(data?.data?.children) ? data.data.children : [];\n' + 'const posts = children.slice(0,10).map(c => { const d = c.data || {}; return { title: d.title, author: d.author, subreddit: d.subreddit, score: d.score, num_comments: d.num_comments, id: d.id, url: d.url }; });\n' + 'const summary = { kind: data?.kind || "Listing", topPosts: posts };\n' + 'log(`Prepared reddit summary with ${posts.length} posts`);\n' + 'return JSON.stringify(summary);', next: 'summarize', }, { id: 'summarize', type: 'prompt', template: 'I have a compact reddit summary: {steps.logJson.response}\n\nBased on this summary, what interesting insights or patterns do you observe about trending topics, engagement (score vs comments), or subreddit activity?', }, ], }, null, 2 ), 'Kanji Mnemonics': JSON.stringify( { dynamicElements: [ { index: 1, kanji: '一', keyword: 'One', kanji_id: '40' }, { index: 2, kanji: '二', keyword: 'Two', kanji_id: '41' }, { index: 3, kanji: '三', keyword: 'Three', kanji_id: '42' }, { index: 4, kanji: '口', keyword: 'Mouth, Entrance', kanji_id: '83' }, { index: 6, kanji: '四', keyword: 'Four', components: ['legs', 'Mouth, Entrance'], kanji_id: '43', }, ], entryId: 'mnemonic', steps: [ { id: 'mnemonic', type: 'prompt', template: 'Create a vivid mnemonic story for the kanji {item.kanji} meaning {item.keyword}. Components (if any): {item.components}. Respond in 1-2 lines.', newChat: true, next: 'imgPrompt', }, { id: 'imgPrompt', type: 'prompt', template: 'Based on this mnemonic: {steps.mnemonic.response}\\nWrite a concise visual image prompt (no prefatory text).', newChat: true, next: 'genImage', }, { id: 'genImage', type: 'prompt', template: 'Generate an image for this prompt: {steps.imgPrompt.response}. Return the image here in chat.', responseType: 'image', newChat: true, next: 'sendToServer', }, { id: 'sendToServer', type: 'http', method: 'POST', url: 'https://postman-echo.com/post', headers: { 'Content-Type': 'application/json' }, bodyTemplate: '{"kanjiId": "{item.kanji_id}", "kanji": "{item.kanji}", "mnemonic": "{steps.mnemonic.response}", "imagePrompt": "{steps.imgPrompt.response}", "imageUrl": "{steps.genImage.images[0]}"}', }, ], }, null, 2 ), }; Object.entries(defaultChains).forEach(([name, preset]) => { if (!Object.prototype.hasOwnProperty.call(chainsMap, name)) chainsMap[name] = typeof preset === 'string' ? preset : JSON.stringify(preset, null, 2); }); try { GM_setValue(STORAGE_KEYS.presetsChains, chainsMap); } catch {} const fill = (id, map) => { const sel = document.getElementById(id); if (!sel) return; sel.innerHTML = '<option value="">Select preset...</option>'; Object.keys(map || {}) .sort() .forEach((name) => { const o = document.createElement('option'); o.value = name; o.textContent = name; sel.appendChild(o); }); }; fill('composer-preset-select', chainsMap); fill('step-preset-select', stepsMap); }; const getComposerPresetName = () => (document.getElementById('composer-preset-name-input')?.value || '').trim(); const savePreset = (storeKey, name, value) => { if (!name) return utils.log('Enter a preset name', 'warning'); try { const raw = GM_getValue(storeKey, {}) || {}; const map = typeof raw === 'string' ? JSON.parse(raw) : raw; map[name] = value; GM_setValue(storeKey, map); loadPresetSelects(); utils.log(`Preset "${name}" saved`); } catch (e) { utils.log('Save failed: ' + e.message, 'error'); } }; const deletePreset = (storeKey, selId) => { try { const sel = document.getElementById(selId); if (!sel || !sel.value) return utils.log('Select a preset to delete', 'warning'); const name = sel.value; // Confirm with the user before deleting the selected preset/chain if (!confirm(`Delete preset/chain "${name}"? This action cannot be undone.`)) { utils.log(`Delete cancelled for "${name}"`, 'info'); return; } const raw = GM_getValue(storeKey, {}) || {}; const map = typeof raw === 'string' ? JSON.parse(raw) : raw; delete map[name]; GM_setValue(storeKey, map); loadPresetSelects(); utils.log(`Preset "${name}" deleted`); } catch (e) { utils.log('Delete failed: ' + e.message, 'error'); } }; const loadPreset = (storeKey, selId, apply) => { try { const sel = document.getElementById(selId); if (!sel || !sel.value) return utils.log('Select a preset to load', 'warning'); const raw = GM_getValue(storeKey, {}) || {}; const map = typeof raw === 'string' ? JSON.parse(raw) : raw; const v = map[sel.value]; if (v == null) return utils.log('Preset not found', 'warning'); apply(v); utils.log(`Preset "${sel.value}" loaded`); } catch (e) { utils.log('Load failed: ' + e.message, 'error'); } }; loadPresetSelects(); // Composer preset handlers document.getElementById('save-composer-preset-btn')?.addEventListener('click', () => { const name = getComposerPresetName(); const chainValue = document.getElementById('chain-json-input')?.value || ''; savePreset(STORAGE_KEYS.presetsChains, name, chainValue); }); document.getElementById('load-composer-preset-btn')?.addEventListener('click', () => { const sel = document.getElementById('composer-preset-select'); if (sel && (!sel.value || sel.value.trim() === '')) { const chainInput = document.getElementById('chain-json-input'); if (chainInput) { chainInput.value = ''; state.chainDefinition = null; utils.saveToStorage(STORAGE_KEYS.chainDef, ''); refreshChainCards(); utils.log('Cleared chain definition'); } return; } loadPreset(STORAGE_KEYS.presetsChains, 'composer-preset-select', (v) => { const chainInput = document.getElementById('chain-json-input'); if (chainInput) { const str = typeof v === 'string' ? v : JSON.stringify(v, null, 2); chainInput.value = str; try { state.chainDefinition = JSON.parse(str); } catch { state.chainDefinition = null; } utils.saveToStorage(STORAGE_KEYS.chainDef, str); // Clear dynamic items when switching presets state.dynamicElements = []; const dynEl = document.getElementById('dynamic-elements-input'); if (dynEl) dynEl.value = ''; // Refresh chain cards to show the loaded chain refreshChainCards(); } }); }); document.getElementById('delete-composer-preset-btn')?.addEventListener('click', () => { deletePreset(STORAGE_KEYS.presetsChains, 'composer-preset-select'); }); // Step modal preset handlers document.getElementById('save-step-preset-btn')?.addEventListener('click', () => { const modal = document.getElementById('chain-step-modal'); if (!modal || modal.style.display === 'none') return; // Collect current step data const stepData = { type: document.getElementById('step-type-select')?.value || '', title: document.getElementById('step-title-input')?.value || '', template: document.getElementById('step-template-input')?.value || document.getElementById('step-prompt-template')?.value || '', elements: document.getElementById('step-template-elements')?.value || '', code: document.getElementById('step-js-code')?.value || '', url: document.getElementById('step-http-url')?.value || '', method: document.getElementById('step-http-method')?.value || 'GET', headers: document.getElementById('step-http-headers')?.value || '', bodyTemplate: document.getElementById('step-http-body')?.value || '', }; const name = prompt('Enter preset name:'); if (name) { try { const raw = GM_getValue(STORAGE_KEYS.presetsSteps, {}) || {}; const map = typeof raw === 'string' ? JSON.parse(raw) : raw; map[name] = stepData; GM_setValue(STORAGE_KEYS.presetsSteps, map); } catch (e) { utils.log('Failed saving step preset: ' + e.message, 'error'); } loadPresetSelects(); } }); document.getElementById('step-preset-select')?.addEventListener('change', (e) => { if (!e.target.value) return; try { const raw = GM_getValue(STORAGE_KEYS.presetsSteps, {}) || {}; const map = typeof raw === 'string' ? JSON.parse(raw) : raw; let stepData = map[e.target.value]; if (typeof stepData === 'string') { try { stepData = JSON.parse(stepData); } catch {} } if (!stepData) return; if (stepData.type) { const typeSel = document.getElementById('step-type-select'); typeSel.value = stepData.type; typeSel.dispatchEvent(new Event('change')); } if (stepData.title) document.getElementById('step-title-input').value = stepData.title; if (stepData.template) { // Apply to both prompt/template fields as applicable const promptEl = document.getElementById('step-prompt-template'); const tmplEl = document.getElementById('step-template-input'); if (promptEl) promptEl.value = stepData.template; if (tmplEl) tmplEl.value = stepData.template; } if (stepData.elements) document.getElementById('step-template-elements').value = stepData.elements; if (stepData.responseType) { const r = document.getElementById('step-response-type'); if (r) r.value = stepData.responseType; } if (typeof stepData.newChat === 'boolean') { const nc = document.getElementById('step-newchat-checkbox'); if (nc) nc.checked = !!stepData.newChat; } if (stepData.code) document.getElementById('step-js-code').value = stepData.code; if (stepData.url) document.getElementById('step-http-url').value = stepData.url; if (stepData.method) document.getElementById('step-http-method').value = stepData.method; if (stepData.headers) document.getElementById('step-http-headers').value = typeof stepData.headers === 'string' ? stepData.headers : JSON.stringify(stepData.headers); if (stepData.bodyTemplate) document.getElementById('step-http-body').value = stepData.bodyTemplate; } catch (err) { utils.log('Failed to load step preset: ' + err.message, 'error'); } }); // Add delete step preset button handler document.getElementById('delete-step-preset-btn')?.addEventListener('click', () => { const select = document.getElementById('step-preset-select'); if (!select || !select.value) { utils.log('Select a preset to delete', 'warning'); return; } if (confirm(`Delete preset "${select.value}"?`)) { try { const raw = GM_getValue(STORAGE_KEYS.presetsSteps, {}) || {}; const map = typeof raw === 'string' ? JSON.parse(raw) : raw; delete map[select.value]; GM_setValue(STORAGE_KEYS.presetsSteps, map); loadPresetSelects(); utils.log(`Preset "${select.value}" deleted`); } catch (e) { utils.log('Delete failed: ' + e.message, 'error'); } } }); }; // Run-lock utilities to avoid cross-tab collisions const acquireRunLock = () => { try { const key = STORAGE_KEYS.runLockKey; const now = Date.now(); const existing = localStorage.getItem(key); const selfId = state.runLockId || (state.runLockId = `${now}-${Math.random().toString(36).slice(2)}`); if (existing) { try { const obj = JSON.parse(existing); if (obj && obj.id && obj.ts && now - obj.ts < CONFIG.RUN_LOCK_TTL_MS) { return false; // another tab active } } catch { /* treat as stale */ } } localStorage.setItem(key, JSON.stringify({ id: selfId, ts: now })); // heartbeat clearInterval(state.runLockTimer); state.runLockTimer = setInterval(() => { try { localStorage.setItem(key, JSON.stringify({ id: selfId, ts: Date.now() })); } catch (e) { /* ignore */ } }, CONFIG.RUN_LOCK_RENEW_MS); window.addEventListener('beforeunload', releaseRunLock); return true; } catch { return true; } }; const releaseRunLock = () => { try { clearInterval(state.runLockTimer); state.runLockTimer = null; const key = STORAGE_KEYS.runLockKey; const existing = localStorage.getItem(key); if (existing) { const obj = JSON.parse(existing); if (!obj || obj.id === state.runLockId) localStorage.removeItem(key); } } catch (e) { /* ignore */ } }; const runChainWithBatch = async () => { if (!state.chainDefinition) { try { state.chainDefinition = JSON.parse( document.getElementById('chain-json-input').value || '{}' ); } catch { state.chainDefinition = null; } } if (!state.chainDefinition) { utils.log('No chain defined', 'warning'); return; } if (!acquireRunLock()) { utils.log('Another tab is running automation - aborting to prevent collision', 'error'); return; } state.isProcessing = true; updateStatus('processing'); try { // Prefer runtime batch; if none, allow chain to provide sample items let items = Array.isArray(state.dynamicElements) ? state.dynamicElements : []; if ( (!items || items.length === 0) && (Array.isArray(state.chainDefinition?.dynamicElements) || typeof state.chainDefinition?.dynamicElements === 'string') ) { items = state.chainDefinition.dynamicElements; // If dynamicElements is a string (JSON/function), attempt to parse/execute if (typeof items === 'string') { try { const parsed = await processors.parseDynamicElements(items); if (Array.isArray(parsed)) items = parsed; } catch {} } } // Fallback: if still empty but chain references {item}, seed with sample cities if (!items || items.length === 0) { const usesItem = Array.isArray(state.chainDefinition?.steps) ? state.chainDefinition.steps.some((s) => ['url', 'template', 'bodyTemplate'].some( (k) => typeof s?.[k] === 'string' && s[k].includes('{item') ) ) : false; if (usesItem) { utils.log('No dynamic elements provided; using sample items for this chain.', 'warning'); items = ['London', 'Tokyo', 'New York']; } } // Use live state.dynamicElements so runtime edits affect the remaining items. const stopBtn = document.getElementById('stop-batch-btn'); if (stopBtn) stopBtn.style.display = 'inline-flex'; const stopRunBtn = document.getElementById('stop-run-btn'); if (stopRunBtn) stopRunBtn.style.display = 'inline-flex'; const stopMini = document.getElementById('stop-mini-btn'); if (stopMini) stopMini.style.display = 'inline-flex'; state.cancelRequested = false; state.currentBatchIndex = 0; state.processedCount = 0; updateSubProgress(0, 0); // If there are no dynamic elements, allow a single run with null item const liveItems = Array.isArray(state.dynamicElements) ? state.dynamicElements : []; if (!liveItems || liveItems.length === 0) { // Single run with empty item refreshBatchProgress(0, 0); await processChain(state.chainDefinition, { item: null, index: 1, total: 1 }); } else { let processed = 0; // Loop until we've processed all available items or cancel is requested while (true) { if (state.cancelRequested) { utils.log('Run canceled'); break; } const itemsNow = Array.isArray(state.dynamicElements) ? state.dynamicElements : []; const totalNow = Math.max(0, itemsNow.length); // If no items remain, we're done if (totalNow === 0) { break; } // Update progress using processed count and dynamic total (processed + remaining) // Ensure currentBatchIndex reflects what we've processed so far for live updates state.currentBatchIndex = processed; state.processedCount = processed; refreshBatchProgress(processed, processed + totalNow); // Determine the next item to process let itemToProcess; if (state.autoRemoveProcessed) { // Always take the first item itemToProcess = itemsNow[0]; if (typeof state.dynamicElements.shift === 'function') { // We'll remove after processing to avoid racing with input handlers } } else { // Use processed as index; if out of range, break (may happen if list shrank) if (processed >= itemsNow.length) break; itemToProcess = itemsNow[processed]; } utils.log(`🔗 Chain run for item ${processed + 1}/${processed + totalNow}`); await processChain(state.chainDefinition, { item: itemToProcess, index: processed + 1, total: totalNow, }); if (state.cancelRequested) { utils.log('Run canceled'); break; } // After processing, update the runtime list according to auto-remove if (state.autoRemoveProcessed) { removeHeadItems(1); processed += 1; state.currentBatchIndex = processed; state.processedCount = processed; } else { processed += 1; state.processedCount = processed; } // Sync chain JSON so edits are reflected try { if (!state.chainDefinition) state.chainDefinition = JSON.parse( document.getElementById('chain-json-input').value || '{}' ); state.chainDefinition.dynamicElements = state.dynamicElements; const chainInput = document.getElementById('chain-json-input'); if (chainInput) chainInput.value = JSON.stringify(state.chainDefinition, null, 2); } catch {} // Update progress after completion of this item; denominator = processed + remaining const remainingNow = Array.isArray(state.dynamicElements) ? state.dynamicElements.length : 0; refreshBatchProgress(processed, processed + remainingNow); // Wait between items when there are still items left const remaining = Array.isArray(state.dynamicElements) ? state.dynamicElements.length : 0; if (remaining > 0) { utils.log(`⏱️ Waiting ${state.batchWaitTime}ms before next item…`); await utils.sleep(state.batchWaitTime); continue; } break; } } utils.log('🏁 Chain batch completed'); } catch (e) { utils.log('Chain error: ' + e.message, 'error'); } finally { releaseRunLock(); state.isProcessing = false; updateStatus('idle'); refreshBatchProgress(0, 0); updateSubProgress(0, 0); const stopBtn = document.getElementById('stop-batch-btn'); if (stopBtn) stopBtn.style.display = 'none'; const stopRunBtn = document.getElementById('stop-run-btn'); if (stopRunBtn) stopRunBtn.style.display = 'none'; const stopMini = document.getElementById('stop-mini-btn'); if (stopMini) stopMini.style.display = 'none'; } }; const resolveEntryStep = (chain) => { if (!chain) return null; if (chain.entryId) return (chain.steps || []).find((s) => s.id === chain.entryId) || null; const steps = chain.steps || []; if (!steps.length) return null; const referenced = new Set(steps.map((s) => s.next).filter(Boolean)); const first = steps.find((s) => !referenced.has(s.id)); return first || steps[0]; }; // Helper: create a per-step context that exposes previous steps and chain data const createStepContext = (context) => ({ ...context, item: context.item, index: context.index, total: context.total, steps: context.steps, chain: context.chain, }); const handlePromptStep = async (step, stepContext, context) => { const msg = processors.processDynamicTemplate(step.template || '', stepContext); // Per-step new chat option if (step.newChat) { await startNewChat(); } const expect = step.responseType === 'image' ? 'image' : 'text'; if (expect === 'image') { const { el: respEl, images } = await chatGPT.askWith(msg, { expect: 'image' }); const imgs = images || []; context.lastResponseText = imgs[0] || ''; context.chain[step.id] = { images: imgs }; context.steps[step.id] = { type: 'prompt', responseType: 'image', images: imgs }; utils.log(`🖼️ Step ${step.id} returned ${imgs.length} image(s)`); utils.log(`💡 Access first image: {steps.${step.id}.images[0]}`); } else { const { el: respEl, text: resp } = await chatGPT.ask(msg); context.lastResponseText = resp; context.chain[step.id] = { response: resp }; context.steps[step.id] = { type: 'prompt', response: resp, responseText: resp }; utils.log(`📩 Step ${step.id} response (${resp.length} chars)`); utils.log( `💡 Access this data in next steps with: {steps.${step.id}.response} or {steps.${step.id}.responseText}` ); } }; const handleHttpStep = async (step, stepContext, context) => { const url = processors.processDynamicTemplate(step.url || '', stepContext); const method = (step.method || 'GET').toUpperCase(); let headers = step.headers || {}; try { if (typeof headers === 'string') headers = JSON.parse(headers); } catch {} const body = step.bodyTemplate ? processors.processDynamicTemplate(step.bodyTemplate, stepContext) : undefined; // Basic retry for transient failures let res; let attempt = 0; let lastErr = null; while (attempt < 3) { try { res = await http.request({ method, url, headers, data: body }); break; } catch (e) { lastErr = e; attempt++; if (attempt < 3) { utils.log(`HTTP attempt ${attempt} failed (${e?.message || e}). Retrying...`, 'warning'); await utils.sleep(500 * attempt); } } } if (!res) throw lastErr || new Error('Network error'); const payload = res.responseText || res.response || ''; let parsedData = payload; try { parsedData = JSON.parse(payload); } catch {} const httpData = { status: res.status, statusText: res.statusText || '', data: parsedData, rawText: payload, headers: res.responseHeaders || {}, url, method, }; context.chain[step.id] = { http: httpData }; context.steps[step.id] = { type: 'http', ...httpData }; utils.log(`🌐 HTTP ${method} ${url} → ${res.status}`); utils.log( `💡 Access this data with: {steps.${step.id}.data} or {steps.${step.id}.rawText} or {steps.${step.id}.status}` ); }; const handleJsStep = async (step, stepContext, context) => { const jsContext = { elementData: context.item, index: context.index, total: context.total, steps: context.steps, lastResponse: context.lastResponseText, }; const ret = await processors.executeCustomCode( step.code || '', context.lastResponseText || '', jsContext ); context.steps[step.id] = { type: 'js', executed: true, response: ret }; }; const handleTemplateStep = async (step, stepContext, context) => { let arr = []; try { // Allow templating inside elements definition const elemsSrc = processors.processDynamicTemplate(step.elements || '[]', stepContext); arr = await processors.parseDynamicElements(elemsSrc || '[]'); } catch { arr = []; } // Optionally use chain-level dynamicElements for nested batching if ( (!arr || arr.length === 0) && step.useDynamicElements && (Array.isArray(context.chain?.dynamicElements) || typeof context.chain?.dynamicElements === 'string') ) { arr = context.chain.dynamicElements; } if (!Array.isArray(arr) || arr.length === 0) { utils.log('Template step has no elements; sending one prompt with current context'); const msg = processors.processDynamicTemplate(step.template || '', stepContext); const { text: resp } = await chatGPT.ask(msg); context.lastResponseText = resp; context.chain[step.id] = { response: resp }; context.steps[step.id] = { type: 'template', response: resp, responseText: resp, itemCount: 0, }; utils.log( `💡 Access template data with: {steps.${step.id}.responses} or {steps.${step.id}.lastResponse}` ); return; } utils.log(`🧩 Template step expanding ${arr.length} items`); updateSubProgress(0, arr.length); const responses = []; for (let i = 0; i < arr.length; i++) { updateSubProgress(i + 1, arr.length); if (state.cancelRequested) { utils.log('Run canceled'); break; } const child = arr[i]; const itemContext = { ...stepContext, item: child, index: i + 1, total: arr.length }; const msg = processors.processDynamicTemplate(step.template || '', itemContext); utils.log(`📝 Template item ${i + 1}/${arr.length}: ${utils.clip(msg, 200)}`); if (step.newChat) { await startNewChat(); } const expect = step.responseType === 'image' ? 'image' : 'text'; if (expect === 'image') { const { images } = await chatGPT.askWith(msg, { expect: 'image' }); responses.push({ item: child, images: images || [] }); context.lastResponseText = (images && images[0]) || ''; } else { const { text: resp } = await chatGPT.ask(msg); responses.push({ item: child, response: resp }); context.lastResponseText = resp; } if (state.cancelRequested) { utils.log('Run canceled'); break; } if (i < arr.length - 1) { utils.log(`⏱️ Waiting ${state.batchWaitTime}ms before next template item…`); await utils.sleep(state.batchWaitTime); } } context.chain[step.id] = { responses }; context.steps[step.id] = { type: 'template', responses, itemCount: responses.length, lastResponse: responses[responses.length - 1]?.response || '', }; updateSubProgress(0, 0); utils.log( `💡 Access template data with: {steps.${step.id}.responses} or {steps.${step.id}.lastResponse}` ); }; const processChain = async (chain, baseContext) => { const entry = resolveEntryStep(chain); if (!entry) throw new Error('Empty chain'); let step = entry; let context = { ...baseContext, lastResponseText: '', chain: { dynamicElements: chain.dynamicElements || [] }, steps: {}, }; const perStepWait = parseInt(document.getElementById('step-wait-input')?.value || '0') || 0; while (step) { utils.log(`➡️ Step ${step.id} (${step.type})`); const stepContext = createStepContext(context); try { if (step.type === 'prompt') await handlePromptStep(step, stepContext, context); else if (step.type === 'http') await handleHttpStep(step, stepContext, context); else if (step.type === 'js') await handleJsStep(step, stepContext, context); else if (step.type === 'template') await handleTemplateStep(step, stepContext, context); else utils.log(`Unknown step type: ${step.type}`, 'warning'); } catch (err) { const msg = err?.message || String(err || 'Unknown error'); utils.log(`Step ${step.id} error: ${msg}`, 'error'); throw new Error(msg); } step = step.next ? (chain.steps || []).find((s) => s.id === step.next) : null; if (step && perStepWait > 0) { utils.log(`⏱️ Waiting ${perStepWait}ms before next step…`); await utils.sleep(perStepWait); } } }; // Initialize the script const init = () => { if (document.getElementById('chatgpt-automation-ui')) { return; // Already initialized } utils.log('Initializing ChatGPT Automation Pro...'); // Wait for page to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createUI); } else { createUI(); } }; // Auto-start init(); })();