// ==UserScript== // @name Arena++ // @name:en Arena++ // @name:ja Arena++ // @name:ru Arena++ // @namespace http://tampermonkey.net/ // @version 1.2.1 // @description 提取 Arena.ai 对话(支持公式/MD/选区复制) // @description:en Extract the Arena.ai dialog (supports formula/MD/selection copying) // @description:ja Arena.ai ダイアログを抽出します (式/MD/選択のコピーをサポート) // @description:ru Распакуйте диалоговое окно Arena.ai (поддерживает копирование формул/MD/выделений). // @author Panda is cat // @match https://arena.ai/* // @grant GM_setClipboard // @grant GM_addStyle // @run-at document-end // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/565333/Arena%2B%2B.user.js // @updateURL https://update.greasyfork.icu/scripts/565333/Arena%2B%2B.meta.js // ==/UserScript== (function() { 'use strict'; // ================= I18N (多语言配置) ================= const userLang = navigator.language.startsWith('zh') ? 'zh' : 'en'; const I18N = { zh: { mode_on: "🖱️ 选择模式: 开", mode_off: "🖱️ 选择模式: 关", copy_btn: "📋 提取并复制", copy_done: "✅ 已复制!", alert_empty: "❌ 未找到消息或未选中任何内容!", role_user: "User", role_ai_default: "AI" }, en: { mode_on: "🖱️ Select Mode: ON", mode_off: "🖱️ Select Mode: OFF", copy_btn: "📋 Extract & Copy", copy_done: "✅ Copied!", alert_empty: "❌ No messages found or selected!", role_user: "User", role_ai_default: "AI" } }; const TEXT = I18N[userLang] || I18N.en; // ================= 样式配置 ================= const CONFIG = { highlightColor: 'rgba(121, 219, 143, 0.3)', borderColor: '#10a37f', buttonColor: '#3b82f6' }; GM_addStyle(` #ace-floating-panel { position: fixed; bottom: 20px; right: 20px; z-index: 9999; background: #fff; padding: 10px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); font-family: sans-serif; display: flex; gap: 8px; border: 1px solid #e5e7eb; } .ace-btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 600; transition: all 0.2s; color: white; } #ace-toggle-select { background-color: #6b7280; } #ace-toggle-select.active { background-color: ${CONFIG.borderColor}; } #ace-copy-btn { background-color: ${CONFIG.buttonColor}; } #ace-copy-btn:hover { opacity: 0.9; } .ace-selectable { cursor: pointer !important; position: relative; transition: all 0.2s; } .ace-selectable:hover { box-shadow: 0 0 0 2px rgba(0,0,0,0.1); } .ace-selected { background-color: ${CONFIG.highlightColor} !important; box-shadow: 0 0 0 2px ${CONFIG.borderColor} !important; border-radius: 6px; } .ace-selected * { pointer-events: none; } `); // ================= 核心类 ================= class ChatExporter { constructor() { this.selectionMode = false; this.messages = []; this.selectedSet = new Set(); this.initUI(); } initUI() { const panel = document.createElement('div'); panel.id = 'ace-floating-panel'; panel.innerHTML = ` `; document.body.appendChild(panel); this.btnToggle = panel.querySelector('#ace-toggle-select'); this.btnCopy = panel.querySelector('#ace-copy-btn'); this.btnToggle.addEventListener('click', () => this.toggleSelectionMode()); this.btnCopy.addEventListener('click', () => this.extractAndCopy()); } async toggleSelectionMode() { this.selectionMode = !this.selectionMode; this.btnToggle.textContent = this.selectionMode ? TEXT.mode_on : TEXT.mode_off; this.btnToggle.classList.toggle('active', this.selectionMode); await this.scanMessages(); if (this.selectionMode) { this.messages.forEach((msg) => { msg.element.classList.add('ace-selectable'); msg.element.onclick = (e) => { e.preventDefault(); e.stopPropagation(); this.toggleMessageSelection(msg.element); }; }); if(this.messages.length > 0) return; } else { this.messages.forEach(msg => { msg.element.classList.remove('ace-selectable', 'ace-selected'); msg.element.onclick = null; }); this.selectedSet.clear(); } } toggleMessageSelection(element) { if (element.classList.contains('ace-selected')) { element.classList.remove('ace-selected'); this.selectedSet.delete(element); } else { element.classList.add('ace-selected'); this.selectedSet.add(element); } } /** * 核心重构:消息扫描逻辑 * 不再依赖双栏坐标,而是根据 DOM 特征(bg-surface-raised)和局部查找 */ async scanMessages() { // 1. 获取所有可能是消息内容的块 // .prose 是 markdown 内容的标准容器,.message-content 是某些版本的容器 const rawElements = Array.from(document.querySelectorAll('.prose, .message-content, [data-testid="chatbot"] .markdown')); if (rawElements.length === 0) return; this.messages = rawElements.map(el => { const rect = el.getBoundingClientRect(); let role = "Unknown"; let displayName = TEXT.role_ai_default; // --- 步骤 1: 判定是否为用户 (基于你提供的 bg-surface-raised 类) --- // 用户对话框特征: