您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A highly customizable AI rates tweets 1-10 and removes all the slop, saving your braincells!
- // ==UserScript==
- // @name TweetFilter AI
- // @namespace http://tampermonkey.net/
- // @version Version 1.5
- // @description A highly customizable AI rates tweets 1-10 and removes all the slop, saving your braincells!
- // @author Obsxrver(3than)
- // @match *://twitter.com/*
- // @match *://x.com/*
- // @grant GM_addStyle
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_xmlhttpRequest
- // @grant GM_getResourceText
- // @connect openrouter.ai
- // @run-at document-idle
- // @license MIT
- // ==/UserScript==
- (function() {
- 'use strict';
- // Embedded Menu.html
- const MENU = `<div id="tweetfilter-root-container"><button id="filter-toggle" class="toggle-button" style="display: none;">Filter Slider</button><div id="tweet-filter-container"><button class="close-button" data-action="close-filter">×</button><label for="tweet-filter-slider">SlopScore:</label><div class="filter-controls"><input type="range" id="tweet-filter-slider" min="0" max="10" step="1"><input type="number" id="tweet-filter-value" min="0" max="10" step="1" value="5"></div></div><button id="settings-toggle" class="toggle-button" data-action="toggle-settings"><span style="font-size: 14px;">⚙️</span> Settings</button><div id="settings-container" class="hidden"><div class="settings-header"><div class="settings-title">Twitter De-Sloppifier</div><button class="close-button" data-action="toggle-settings">×</button></div><div class="settings-content"><div class="tab-navigation"><button class="tab-button active" data-tab="general">General</button><button class="tab-button" data-tab="models">Models</button><button class="tab-button" data-tab="instructions">Instructions</button></div><div id="general-tab" class="tab-content active"><div class="section-title"><span style="font-size: 14px;">🔑</span> OpenRouter API Key <a href="https://openrouter.ai/settings/keys" target="_blank">Get one here</a></div><input id="openrouter-api-key" placeholder="Enter your OpenRouter API key"><button class="settings-button" data-action="save-api-key">Save API Key</button><div class="section-title" style="margin-top: 20px;"><span style="font-size: 14px;">🗄️</span> Cache Statistics</div><div class="stats-container"><div class="stats-row"><div class="stats-label">Cached Tweet Ratings</div><div class="stats-value" id="cached-ratings-count">0</div></div><div class="stats-row"><div class="stats-label">Whitelisted Handles</div><div class="stats-value" id="whitelisted-handles-count">0</div></div></div><button id="clear-cache" class="settings-button danger" data-action="clear-cache">Clear Rating Cache</button><div class="section-title" style="margin-top: 20px;"><span style="font-size: 14px;">💾</span> Backup & Restore</div><div class="section-description">Export your settings and cached ratings to a file for backup, or import previously saved settings.</div><button class="settings-button" data-action="export-cache">Export Cache</button><button class="settings-button danger" style="margin-top: 15px;" data-action="reset-settings">Reset to Defaults</button><div id="version-info" style="margin-top: 20px; font-size: 11px; opacity: 0.6; text-align: center;">Twitter De-Sloppifier v?.?</div></div><div id="models-tab" class="tab-content"><div class="section-title"><span style="font-size: 14px;">🧠</span> Tweet Rating Model</div><div class="section-description">The rating model is responsible for reviewing each tweet. <br>It will process images directly if you select an <strong>image-capable (🖼️)</strong> model.</div><div class="select-container" id="model-select-container"></div><div class="advanced-options"><div class="advanced-toggle" data-toggle="model-options-content"><div class="advanced-toggle-title">Options</div><div class="advanced-toggle-icon">▼</div></div><div class="advanced-content" id="model-options-content"><div class="sort-container"><label for="model-sort-order">Sort models by: </label><div class="controls-group"><select id="model-sort-order" data-setting="modelSortOrder"><option value="pricing-low-to-high">Price</option><option value="latency-low-to-high">Latency</option><option value="throughput-high-to-low">Throughput</option><option value="top-weekly">Popularity</option><option value="">Age</option></select><button id="sort-direction" class="sort-toggle" data-setting="sortDirection" data-value="default">High-Low</button></div></div><div class="sort-container"><label for="provider-sort">API Endpoint Priority: </label><select id="provider-sort" data-setting="providerSort"><option value="">Default (load-balanced)</option><option value="throughput">Throughput</option><option value="latency">Latency</option><option value="price">Price</option></select></div><div class="sort-container"><label><input type="checkbox" id="show-free-models" data-setting="showFreeModels" checked>Show Free Models</label></div><div class="parameter-row" data-param-name="modelTemperature"><div class="parameter-label" title="How random the model responses should be (0.0-1.0)">Temperature</div><div class="parameter-control"><input type="range" class="parameter-slider" min="0" max="2" step="0.1"><input type="number" class="parameter-value" min="0" max="2" step="0.01" style="width: 60px;"></div></div><div class="parameter-row" data-param-name="modelTopP"><div class="parameter-label" title="Nucleus sampling parameter (0.0-1.0)">Top-p</div><div class="parameter-control"><input type="range" class="parameter-slider" min="0" max="1" step="0.1"><input type="number" class="parameter-value" min="0" max="1" step="0.01" style="width: 60px;"></div></div><div class="parameter-row" data-param-name="maxTokens"><div class="parameter-label" title="Maximum number of tokens for the response (0 means no limit)">Max Tokens</div><div class="parameter-control"><input type="range" class="parameter-slider" min="0" max="2000" step="100"><input type="number" class="parameter-value" min="0" max="2000" step="100" style="width: 60px;"></div></div><div class="toggle-row"><div class="toggle-label" title="Stream API responses as they're generated for live updates">Enable Live Streaming</div><label class="toggle-switch"><input type="checkbox" data-setting="enableStreaming"><span class="toggle-slider"></span></label></div></div></div><div class="section-title" style="margin-top: 25px;"><span style="font-size: 14px;">🖼️</span> Image Processing Model</div><div class="section-description">This model generates <strong>text descriptions</strong> of images for the rating model.<br> Hint: If you selected an image-capable model (🖼️) as your <strong>main rating model</strong>, it will process images directly.</div><div class="toggle-row"><div class="toggle-label">Enable Image Descriptions</div><label class="toggle-switch"><input type="checkbox" data-setting="enableImageDescriptions"><span class="toggle-slider"></span></label></div><div id="image-model-container" style="display: none;"><div class="select-container" id="image-model-select-container"></div><div class="advanced-options" id="image-advanced-options"><div class="advanced-toggle" data-toggle="image-advanced-content"><div class="advanced-toggle-title">Options</div><div class="advanced-toggle-icon">▼</div></div><div class="advanced-content" id="image-advanced-content"><div class="parameter-row" data-param-name="imageModelTemperature"><div class="parameter-label" title="Randomness for image descriptions (0.0-1.0)">Temperature</div><div class="parameter-control"><input type="range" class="parameter-slider" min="0" max="2" step="0.1"><input type="number" class="parameter-value" min="0" max="2" step="0.1" style="width: 60px;"></div></div><div class="parameter-row" data-param-name="imageModelTopP"><div class="parameter-label" title="Nucleus sampling for image model (0.0-1.0)">Top-p</div><div class="parameter-control"><input type="range" class="parameter-slider" min="0" max="1" step="0.1"><input type="number" class="parameter-value" min="0" max="1" step="0.1" style="width: 60px;"></div></div></div></div></div></div><div id="instructions-tab" class="tab-content"><div class="section-title">Custom Instructions</div><div class="section-description">Add custom instructions for how the model should score tweets:</div><textarea id="user-instructions" placeholder="Examples:- Give high scores to tweets about technology- Penalize clickbait-style tweets- Rate educational content higher" data-setting="userDefinedInstructions" value=""></textarea><button class="settings-button" data-action="save-instructions">Save Instructions</button><div class="advanced-options" id="instructions-history"><div class="advanced-toggle" data-toggle="instructions-history-content"><div class="advanced-toggle-title">Custom Instructions History</div><div class="advanced-toggle-icon">▼</div></div><div class="advanced-content" id="instructions-history-content"><div class="instructions-list" id="instructions-list"><!-- Instructions entries will be added here dynamically --></div><button class="settings-button danger" style="margin-top: 10px;" data-action="clear-instructions-history">Clear All History</button></div></div><div class="section-title" style="margin-top: 20px;">Auto-Rate Handles as 10/10</div><div class="section-description">Add Twitter handles to automatically rate as 10/10:</div><div class="handle-input-container"><input id="handle-input" type="text" placeholder="Twitter handle (without @)"><button class="add-handle-btn" data-action="add-handle">Add</button></div><div class="handle-list" id="handle-list"></div></div></div><div id="status-indicator" class=""></div></div><div id="tweet-filter-stats-badge" class="tweet-filter-stats-badge"></div></div>`;
- // Embedded style.css
- const STYLE = `.refreshing {animation: spin 1s infinite linear;}@keyframes spin {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}}.score-highlight {display: inline-block;background-color: #1d9bf0;/* Twitter blue */color: white;padding: 3px 10px;border-radius: 9999px;margin: 8px 0;font-weight: bold;font-size: 0.9em;}.mobile-tooltip {/* Add specific mobile tooltip styles if needed */max-width: 90vw;/* Example */}.score-description.streaming-tooltip {scroll-behavior: smooth;border-left: 3px solid #1d9bf0;background-color: rgba(25, 30, 35, 0.98);}.score-description.streaming-tooltip::before {content: 'Live';position: absolute;top: 10px;right: 10px;background-color: #1d9bf0;color: white;font-size: 11px;padding: 2px 6px;border-radius: 10px;font-weight: bold;}.score-description::-webkit-scrollbar {width: 6px;}.score-description::-webkit-scrollbar-track {background: rgba(255, 255, 255, 0.05);border-radius: 3px;}.score-description::-webkit-scrollbar-thumb {background: rgba(255, 255, 255, 0.2);border-radius: 3px;}.score-description::-webkit-scrollbar-thumb:hover {background: rgba(255, 255, 255, 0.3);}.score-description.streaming-tooltip p::after {content: '|';display: inline-block;color: #1d9bf0;animation: blink 0.7s infinite;font-weight: bold;margin-left: 2px;}@keyframes blink {0%,100% {opacity: 0;}50% {opacity: 1;}}.streaming-rating {background-color: rgba(33, 150, 243, 0.9) !important;color: white !important;animation: pulse 1.5s infinite alternate;position: relative;}.streaming-rating::after {content: '';position: absolute;top: -2px;right: -2px;width: 6px;height: 6px;background-color: #1d9bf0;border-radius: 50%;animation: blink 0.7s infinite;box-shadow: 0 0 4px #1d9bf0;}.cached-rating {background-color: rgba(76, 175, 80, 0.9) !important;color: white !important;}.rated-rating {background-color: rgba(33, 33, 33, 0.9) !important;color: white !important;}.blacklisted-rating {background-color: rgba(255, 193, 7, 0.9) !important;color: black !important;}.pending-rating {background-color: rgba(255, 152, 0, 0.9) !important;color: white !important;}@keyframes pulse {0% {opacity: 0.8;}100% {opacity: 1;}}.error-rating {background-color: rgba(244, 67, 54, 0.9) !important;color: white !important;}#status-indicator {position: fixed;bottom: 20px;right: 20px;background-color: rgba(22, 24, 28, 0.95);color: #e7e9ea;padding: 10px 15px;border-radius: 8px;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;font-size: 12px;z-index: 9999;display: none;border: 1px solid rgba(255, 255, 255, 0.1);box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);transform: translateY(100px);transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);}#status-indicator.active {display: block;transform: translateY(0);}.toggle-switch {position: relative;display: inline-block;width: 36px;height: 20px;}.toggle-switch input {opacity: 0;width: 0;height: 0;}.toggle-slider {position: absolute;cursor: pointer;top: 0;left: 0;right: 0;bottom: 0;background-color: rgba(255, 255, 255, 0.2);transition: .3s;border-radius: 34px;}.toggle-slider:before {position: absolute;content: "";height: 16px;width: 16px;left: 2px;bottom: 2px;background-color: white;transition: .3s;border-radius: 50%;}input:checked+.toggle-slider {background-color: #1d9bf0;}input:checked+.toggle-slider:before {transform: translateX(16px);}.toggle-row {display: flex;align-items: center;justify-content: space-between;padding: 8px 10px;margin-bottom: 12px;background-color: rgba(255, 255, 255, 0.05);border-radius: 8px;transition: background-color 0.2s;}.toggle-row:hover {background-color: rgba(255, 255, 255, 0.08);}.toggle-label {font-size: 13px;color: #e7e9ea;}#tweet-filter-container {position: fixed;top: 70px;right: 15px;background-color: rgba(22, 24, 28, 0.95);color: #e7e9ea;padding: 10px 12px;border-radius: 12px;z-index: 9999;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;font-size: 13px;box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);display: flex;align-items: center;gap: 10px;border: 1px solid rgba(255, 255, 255, 0.1);transform-origin: top right;transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.5s ease-in-out;opacity: 1;transform: scale(1) translateX(0);visibility: visible;}#tweet-filter-container.hidden {opacity: 0;transform: scale(0.8) translateX(50px);visibility: hidden;}.close-button {background: none;border: none;color: #e7e9ea;font-size: 16px;cursor: pointer;padding: 0;width: 28px;height: 28px;display: flex;align-items: center;justify-content: center;opacity: 0.8;transition: opacity 0.2s;border-radius: 50%;min-width: 28px;min-height: 28px;-webkit-tap-highlight-color: transparent;touch-action: manipulation;user-select: none;z-index: 30;}.close-button:hover {opacity: 1;background-color: rgba(255, 255, 255, 0.1);}.hidden {display: none !important;}/* Only override hidden for our specific containers */#tweet-filter-container.hidden,#settings-container.hidden {display: flex !important;}.toggle-button {position: fixed;right: 15px;background-color: rgba(22, 24, 28, 0.95);color: #e7e9ea;padding: 8px 12px;border-radius: 8px;cursor: pointer;font-size: 12px;z-index: 9999;border: 1px solid rgba(255, 255, 255, 0.1);box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;display: flex;align-items: center;gap: 6px;transition: all 0.2s ease;}.toggle-button:hover {background-color: rgba(29, 155, 240, 0.2);}#filter-toggle {top: 70px;}#settings-toggle {top: 120px;}#tweet-filter-container label {margin: 0;font-weight: bold;}.tweet-filter-stats-badge {position: fixed;bottom: 50px;right: 20px;background-color: rgba(29, 155, 240, 0.9);color: white;padding: 5px 10px;border-radius: 15px;font-size: 12px;z-index: 9999;box-shadow: 0 2px 5px rgba(0,0,0,0.2);transition: opacity 0.3s;cursor: pointer;display: flex;align-items: center;}#tweet-filter-slider {cursor: pointer;width: 120px;vertical-align: middle;-webkit-appearance: none;appearance: none;height: 6px;border-radius: 3px;background: linear-gradient(to right,#FF0000 0%,#FF8800 calc(var(--slider-percent, 50%) * 0.166),#FFFF00 calc(var(--slider-percent, 50%) * 0.333),#00FF00 calc(var(--slider-percent, 50%) * 0.5),#00FFFF calc(var(--slider-percent, 50%) * 0.666),#0000FF calc(var(--slider-percent, 50%) * 0.833),#800080 var(--slider-percent, 50%),#DEE2E6 var(--slider-percent, 50%),#DEE2E6 100%);}#tweet-filter-slider::-webkit-slider-thumb {-webkit-appearance: none;appearance: none;width: 16px;height: 16px;border-radius: 50%;background: #1d9bf0;cursor: pointer;border: 2px solid white;box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);transition: transform 0.1s;}#tweet-filter-slider::-webkit-slider-thumb:hover {transform: scale(1.2);}#tweet-filter-slider::-moz-range-thumb {width: 16px;height: 16px;border-radius: 50%;background: #1d9bf0;cursor: pointer;border: 2px solid white;box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);transition: transform 0.1s;}#tweet-filter-slider::-moz-range-thumb:hover {transform: scale(1.2);}#tweet-filter-value {min-width: 20px;text-align: center;font-weight: bold;background-color: rgba(255, 255, 255, 0.1);padding: 2px 5px;border-radius: 4px;}#settings-container {position: fixed;top: 70px;right: 15px;background-color: rgba(22, 24, 28, 0.95);color: #e7e9ea;padding: 0;border-radius: 16px;z-index: 9999;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;font-size: 13px;box-shadow: 0 2px 18px rgba(0, 0, 0, 0.6);display: flex;flex-direction: column;width: 90vw;max-width: 380px;max-height: 85vh;overflow: hidden;border: 1px solid rgba(255, 255, 255, 0.1);line-height: 1.3;transform-origin: top right;transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55),opacity 0.5s ease-in-out;opacity: 1;transform: scale(1) translateX(0);visibility: visible;}#settings-container.hidden {opacity: 0;transform: scale(0.8) translateX(50px);visibility: hidden;}.settings-header {padding: 12px 15px;border-bottom: 1px solid rgba(255, 255, 255, 0.1);display: flex;justify-content: space-between;align-items: center;position: sticky;top: 0;background-color: rgba(22, 24, 28, 0.98);z-index: 20;border-radius: 16px 16px 0 0;}.settings-title {font-weight: bold;font-size: 16px;}.settings-content {overflow-y: auto;max-height: calc(85vh - 110px);padding: 0;}.settings-content::-webkit-scrollbar {width: 6px;}.settings-content::-webkit-scrollbar-track {background: rgba(255, 255, 255, 0.05);border-radius: 3px;}.settings-content::-webkit-scrollbar-thumb {background: rgba(255, 255, 255, 0.2);border-radius: 3px;}.settings-content::-webkit-scrollbar-thumb:hover {background: rgba(255, 255, 255, 0.3);}.tab-navigation {display: flex;border-bottom: 1px solid rgba(255, 255, 255, 0.1);position: sticky;top: 0;background-color: rgba(22, 24, 28, 0.98);z-index: 10;padding: 10px 15px;gap: 8px;}.tab-button {padding: 6px 10px;background: none;border: none;color: #e7e9ea;font-weight: bold;cursor: pointer;border-radius: 8px;transition: all 0.2s ease;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;font-size: 13px;flex: 1;text-align: center;}.tab-button:hover {background-color: rgba(255, 255, 255, 0.1);}.tab-button.active {color: #1d9bf0;background-color: rgba(29, 155, 240, 0.1);border-bottom: 2px solid #1d9bf0;}.tab-content {display: none;animation: fadeIn 0.3s ease;padding: 15px;}@keyframes fadeIn {from {opacity: 0;}to {opacity: 1;}}.tab-content.active {display: block;}.select-container {position: relative;margin-bottom: 15px;}.select-container .search-field {position: sticky;top: 0;background-color: rgba(39, 44, 48, 0.95);padding: 8px;border-bottom: 1px solid rgba(255, 255, 255, 0.1);z-index: 1;}.select-container .search-input {width: 100%;padding: 8px 10px;border-radius: 8px;border: 1px solid rgba(255, 255, 255, 0.2);background-color: rgba(39, 44, 48, 0.9);color: #e7e9ea;font-size: 12px;transition: border-color 0.2s;}.select-container .search-input:focus {border-color: #1d9bf0;outline: none;}.custom-select {position: relative;display: inline-block;width: 100%;}.select-selected {background-color: rgba(39, 44, 48, 0.95);color: #e7e9ea;padding: 10px 12px;border: 1px solid rgba(255, 255, 255, 0.2);border-radius: 8px;cursor: pointer;user-select: none;display: flex;justify-content: space-between;align-items: center;font-size: 13px;transition: border-color 0.2s;}.select-selected:hover {border-color: rgba(255, 255, 255, 0.4);}.select-selected:after {content: "";width: 8px;height: 8px;border: 2px solid #e7e9ea;border-width: 0 2px 2px 0;display: inline-block;transform: rotate(45deg);margin-left: 10px;transition: transform 0.2s;}.select-selected.select-arrow-active:after {transform: rotate(-135deg);}.select-items {position: absolute;background-color: rgba(39, 44, 48, 0.98);top: 100%;left: 0;right: 0;z-index: 99;max-height: 300px;overflow-y: auto;border: 1px solid rgba(255, 255, 255, 0.2);border-radius: 8px;margin-top: 5px;box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);display: none;}.select-items div {color: #e7e9ea;padding: 10px 12px;cursor: pointer;user-select: none;transition: background-color 0.2s;border-bottom: 1px solid rgba(255, 255, 255, 0.05);}.select-items div:hover {background-color: rgba(29, 155, 240, 0.1);}.select-items div.same-as-selected {background-color: rgba(29, 155, 240, 0.2);}.select-items::-webkit-scrollbar {width: 6px;}.select-items::-webkit-scrollbar-track {background: rgba(255, 255, 255, 0.05);}.select-items::-webkit-scrollbar-thumb {background: rgba(255, 255, 255, 0.2);border-radius: 3px;}.select-items::-webkit-scrollbar-thumb:hover {background: rgba(255, 255, 255, 0.3);}#openrouter-api-key,#user-instructions {width: 100%;padding: 10px 12px;border-radius: 8px;border: 1px solid rgba(255, 255, 255, 0.2);margin-bottom: 12px;background-color: rgba(39, 44, 48, 0.95);color: #e7e9ea;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;font-size: 13px;transition: border-color 0.2s;}#openrouter-api-key:focus,#user-instructions:focus {border-color: #1d9bf0;outline: none;}#user-instructions {height: 120px;resize: vertical;}.parameter-row {display: flex;align-items: center;margin-bottom: 12px;gap: 8px;padding: 6px;border-radius: 8px;transition: background-color 0.2s;}.parameter-row:hover {background-color: rgba(255, 255, 255, 0.05);}.parameter-label {flex: 1;font-size: 13px;color: #e7e9ea;}.parameter-control {flex: 1.5;display: flex;align-items: center;gap: 8px;}.parameter-value {min-width: 28px;text-align: center;background-color: rgba(255, 255, 255, 0.1);padding: 3px 5px;border-radius: 4px;font-size: 12px;}.parameter-slider {flex: 1;-webkit-appearance: none;height: 4px;border-radius: 4px;background: rgba(255, 255, 255, 0.2);outline: none;cursor: pointer;}.parameter-slider::-webkit-slider-thumb {-webkit-appearance: none;appearance: none;width: 14px;height: 14px;border-radius: 50%;background: #1d9bf0;cursor: pointer;transition: transform 0.1s;}.parameter-slider::-webkit-slider-thumb:hover {transform: scale(1.2);}.section-title {font-weight: bold;margin-top: 20px;margin-bottom: 8px;color: #e7e9ea;display: flex;align-items: center;gap: 6px;font-size: 14px;}.section-title:first-child {margin-top: 0;}.section-description {font-size: 12px;margin-bottom: 8px;opacity: 0.8;line-height: 1.4;}.section-title a {color: #1d9bf0;text-decoration: none;background-color: rgba(255, 255, 255, 0.1);padding: 3px 6px;border-radius: 6px;transition: all 0.2s ease;}.section-title a:hover {background-color: rgba(29, 155, 240, 0.2);text-decoration: underline;}.advanced-options {margin-top: 5px;margin-bottom: 15px;border: 1px solid rgba(255, 255, 255, 0.1);border-radius: 8px;padding: 12px;background-color: rgba(255, 255, 255, 0.03);overflow: hidden;}.advanced-toggle {display: flex;justify-content: space-between;align-items: center;cursor: pointer;margin-bottom: 5px;}.advanced-toggle-title {font-weight: bold;font-size: 13px;color: #e7e9ea;}.advanced-toggle-icon {transition: transform 0.3s;}.advanced-toggle-icon.expanded {transform: rotate(180deg);}.advanced-content {max-height: 0;overflow: hidden;transition: max-height 0.3s ease-in-out;}.advanced-content.expanded {max-height: none;}#instructions-history-content.expanded {max-height: none !important;}#instructions-history .instructions-list {max-height: 400px;overflow-y: auto;margin-bottom: 10px;}.handle-list {margin-top: 10px;max-height: 120px;overflow-y: auto;border: 1px solid rgba(255, 255, 255, 0.1);border-radius: 8px;padding: 5px;}.handle-item {display: flex;align-items: center;justify-content: space-between;padding: 6px 10px;border-bottom: 1px solid rgba(255, 255, 255, 0.05);border-radius: 4px;transition: background-color 0.2s;}.handle-item:hover {background-color: rgba(255, 255, 255, 0.05);}.handle-item:last-child {border-bottom: none;}.handle-text {font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;font-size: 12px;}.remove-handle {background: none;border: none;color: #ff5c5c;cursor: pointer;font-size: 14px;padding: 0 3px;opacity: 0.7;transition: opacity 0.2s;}.remove-handle:hover {opacity: 1;}.add-handle-btn {background-color: #1d9bf0;color: white;border: none;border-radius: 6px;padding: 7px 10px;cursor: pointer;font-weight: bold;font-size: 12px;margin-left: 5px;transition: background-color 0.2s;}.add-handle-btn:hover {background-color: #1a8cd8;}.settings-button {background-color: #1d9bf0;color: white;border: none;border-radius: 8px;padding: 10px 14px;cursor: pointer;font-weight: bold;transition: background-color 0.2s;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;margin-top: 8px;width: 100%;font-size: 13px;}.settings-button:hover {background-color: #1a8cd8;}.settings-button.secondary {background-color: rgba(255, 255, 255, 0.1);}.settings-button.secondary:hover {background-color: rgba(255, 255, 255, 0.15);}.settings-button.danger {background-color: #ff5c5c;}.settings-button.danger:hover {background-color: #e53935;}.button-row {display: flex;gap: 8px;margin-top: 10px;}.button-row .settings-button {margin-top: 0;}.stats-container {background-color: rgba(255, 255, 255, 0.05);padding: 10px;border-radius: 8px;margin-bottom: 15px;}.stats-row {display: flex;justify-content: space-between;padding: 5px 0;border-bottom: 1px solid rgba(255, 255, 255, 0.1);}.stats-row:last-child {border-bottom: none;}.stats-label {font-size: 12px;opacity: 0.8;}.stats-value {font-weight: bold;}.score-indicator {position: absolute;top: 10px;right: 10.5%;background-color: rgba(22, 24, 28, 0.9);color: #e7e9ea;padding: 4px 10px;border-radius: 8px;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;font-size: 14px;font-weight: bold;z-index: 100;cursor: pointer;border: 1px solid rgba(255, 255, 255, 0.1);min-width: 20px;text-align: center;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);transition: transform 0.15s ease;}.score-indicator:hover {transform: scale(1.05);}.score-indicator.mobile-indicator {position: absolute !important;bottom: 3% !important;right: 10px !important;top: auto !important;}.score-description {display: none;background-color: rgba(22, 24, 28, 0.95);color: #e7e9ea;padding: 0 20px 16px 20px;border-radius: 12px;box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;font-size: 16px;line-height: 1.5;z-index: 99999999;position: absolute;width: 550px !important;max-width: 80vw !important;max-height: 60vh;overflow-y: auto;border: 1px solid rgba(255, 255, 255, 0.1);word-wrap: break-word;box-sizing: border-box !important;max-width: 500px;padding-bottom: 35px;}.score-description.pinned {border: 2px solid #1d9bf0 !important;}.tooltip-controls {display: flex !important;justify-content: flex-end !important;position: relative !important;margin: 0 -20px 15px -20px !important;top: 0 !important;background-color: rgba(39, 44, 48, 0.95) !important;padding: 12px 15px !important;z-index: 2 !important;border-top-left-radius: 12px !important;border-top-right-radius: 12px !important;border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;backdrop-filter: blur(5px) !important;}.tooltip-pin-button,.tooltip-copy-button {background: none !important;border: none !important;color: #8899a6 !important;cursor: pointer !important;font-size: 16px !important;padding: 4px 8px !important;margin-left: 8px !important;border-radius: 4px !important;transition: all 0.2s !important;}.tooltip-pin-button:hover,.tooltip-copy-button:hover {background-color: rgba(29, 155, 240, 0.1) !important;color: #1d9bf0 !important;}.tooltip-pin-button:active,.tooltip-copy-button:active {transform: scale(0.95) !important;}.description-text {margin: 0 0 25px 0 !important;font-size: 15px !important;line-height: 1.6 !important;max-width: 100% !important;overflow-wrap: break-word !important;padding: 5px 0 !important;color: #e1e8ed !important;letter-spacing: 0.2px !important;}/* Style different elements within description text */.description-text p {margin: 0 0 12px 0 !important;}.description-text p:last-child {margin-bottom: 0 !important;}.description-text strong {color: #fff !important;font-weight: 600 !important;}.description-text em {color: #8899a6 !important;font-style: italic !important;}.description-text code {background: rgba(255, 255, 255, 0.1) !important;padding: 2px 6px !important;border-radius: 4px !important;font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;font-size: 0.9em !important;color: #1d9bf0 !important;}.description-text a {color: #1d9bf0 !important;text-decoration: none !important;transition: color 0.2s ease !important;border-bottom: 1px solid rgba(29, 155, 240, 0.3) !important;}.description-text a:hover {color: #ffffff !important;border-bottom-color: #ffffff !important;}/* Add subtle highlight for important sections */.description-text blockquote {margin: 12px 0 !important;padding: 8px 16px !important;border-left: 3px solid #1d9bf0 !important;background: rgba(29, 155, 240, 0.1) !important;border-radius: 0 4px 4px 0 !important;}/* Style lists within description */.description-text ul, .description-text ol {margin: 8px 0 12px 20px !important;padding: 0 !important;}.description-text li {margin: 4px 0 !important;line-height: 1.5 !important;}/* Style headings if present */.description-text h1,.description-text h2,.description-text h3,.description-text h4 {color: #ffffff !important;margin: 16px 0 8px 0 !important;font-weight: 600 !important;line-height: 1.3 !important;}.description-text h1 { font-size: 1.4em !important; }.description-text h2 { font-size: 1.3em !important; }.description-text h3 { font-size: 1.2em !important; }.description-text h4 { font-size: 1.1em !important; }/* Add subtle animation for content changes */.description-text {transition: opacity 0.2s ease !important;}/* Style tables if present */.description-text table {width: 100% !important;border-collapse: collapse !important;margin: 12px 0 !important;background: rgba(255, 255, 255, 0.03) !important;border-radius: 4px !important;}.description-text th,.description-text td {padding: 8px 12px !important;border: 1px solid rgba(255, 255, 255, 0.1) !important;text-align: left !important;}.description-text th {background: rgba(255, 255, 255, 0.05) !important;font-weight: 600 !important;color: #ffffff !important;}/* Add a subtle separator between major sections */.description-text hr {border: none !important;border-top: 1px solid rgba(255, 255, 255, 0.1) !important;margin: 20px 0 !important;}/* Style pre blocks for code snippets */.description-text pre {background: rgba(0, 0, 0, 0.2) !important;padding: 12px !important;border-radius: 4px !important;overflow-x: auto !important;border: 1px solid rgba(255, 255, 255, 0.1) !important;margin: 12px 0 !important;}.description-text pre code {background: none !important;padding: 0 !important;border-radius: 0 !important;font-size: 0.9em !important;color: #e1e8ed !important;}/* Add a subtle hover effect for interactive elements */.description-text *[role="button"],.description-text .interactive {transition: all 0.2s ease !important;}.description-text *[role="button"]:hover,.description-text .interactive:hover {background-color: rgba(255, 255, 255, 0.1) !important;}/* Style keyboard shortcuts or commands */.description-text kbd {background: rgba(255, 255, 255, 0.1) !important;border: 1px solid rgba(255, 255, 255, 0.2) !important;border-radius: 3px !important;padding: 2px 6px !important;font-size: 0.9em !important;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important;box-shadow: 0 1px 1px rgba(0,0,0,0.2) !important;}.tooltip-bottom-spacer {height: 10px;}.reasoning-dropdown {margin-top: 15px !important;border-top: 1px solid rgba(255, 255, 255, 0.1) !important;padding-top: 10px !important;}.reasoning-toggle {display: flex !important;align-items: center !important;color: #1d9bf0 !important;cursor: pointer !important;font-weight: bold !important;padding: 5px !important;user-select: none !important;}.reasoning-toggle:hover {background-color: rgba(29, 155, 240, 0.1) !important;border-radius: 4px !important;}.reasoning-arrow {display: inline-block !important;margin-right: 5px !important;transition: transform 0.2s ease !important;}.reasoning-content {max-height: 0 !important;overflow: hidden !important;transition: max-height 0.3s ease-out, padding 0.3s ease-out !important;background-color: rgba(0, 0, 0, 0.15) !important;border-radius: 5px !important;margin-top: 5px !important;padding: 0 !important;}.reasoning-dropdown.expanded .reasoning-content {max-height: 350px !important;overflow-y: auto !important;padding: 10px !important;}.reasoning-dropdown.expanded .reasoning-arrow {transform: rotate(90deg) !important;}.reasoning-text {font-size: 14px !important;line-height: 1.4 !important;color: #ccc !important;margin: 0 !important;padding: 5px !important;}.scroll-to-bottom-button {position: sticky;bottom: 0;width: 100%;background-color: rgba(29, 155, 240, 0.9);color: white;text-align: center;padding: 8px 0;cursor: pointer;font-weight: bold;border-top: 1px solid rgba(255, 255, 255, 0.2);margin-top: 10px;z-index: 100;transition: background-color 0.2s;}.scroll-to-bottom-button:hover {background-color: rgba(29, 155, 240, 1);}@media (max-width: 600px) {.score-indicator {position: absolute !important;bottom: 3% !important;right: 10px !important;top: auto !important;}.score-description {position: fixed !important;width: 100% !important;max-width: 100% !important;top: 5vh !important;bottom: 5vh !important;left: 0 !important;right: 0 !important;margin: 0 !important;padding: 12px !important;box-sizing: border-box !important;overflow-y: auto !important;overflow-x: hidden !important;-webkit-overflow-scrolling: touch !important;overscroll-behavior: contain !important;transform: translateZ(0) !important;border-radius: 16px 16px 0 0 !important;}.reasoning-dropdown.expanded .reasoning-content {max-height: 200px !important;}.close-button {width: 32px;height: 32px;min-width: 32px;min-height: 32px;font-size: 18px;padding: 8px;margin: -4px;}.settings-header .close-button {position: relative;right: 0;}.tooltip-close-button {font-size: 22px !important;width: 32px !important;height: 32px !important;}.tooltip-controls {padding-right: 40px !important;}}.sort-container {margin: 10px 0;display: flex;align-items: center;gap: 10px;justify-content: space-between;}.sort-container label {font-size: 14px;color: var(--text-color);white-space: nowrap;}.sort-container .controls-group {display: flex;gap: 8px;align-items: center;}.sort-container select {padding: 5px 10px;border-radius: 4px;border: 1px solid rgba(255, 255, 255, 0.2);background-color: rgba(39, 44, 48, 0.95);color: #e7e9ea;font-size: 14px;cursor: pointer;min-width: 120px;}.sort-container select:hover {border-color: #1d9bf0;}.sort-container select:focus {outline: none;border-color: #1d9bf0;box-shadow: 0 0 0 2px rgba(29, 155, 240, 0.2);}.sort-toggle {padding: 5px 10px;border-radius: 4px;border: 1px solid rgba(255, 255, 255, 0.2);background-color: rgba(39, 44, 48, 0.95);color: #e7e9ea;font-size: 14px;cursor: pointer;transition: all 0.2s ease;}.sort-toggle:hover {border-color: #1d9bf0;background-color: rgba(29, 155, 240, 0.1);}.sort-toggle.active {background-color: rgba(29, 155, 240, 0.2);border-color: #1d9bf0;}.sort-container select option {background-color: rgba(39, 44, 48, 0.95);color: #e7e9ea;}@media (min-width: 601px) {#settings-container {width: 480px;max-width: 480px;}}#handle-input {flex: 1;padding: 8px 12px;border-radius: 8px;border: 1px solid rgba(255, 255, 255, 0.2);background-color: rgba(39, 44, 48, 0.95);color: #e7e9ea;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;font-size: 14px;transition: border-color 0.2s;min-width: 200px;}#handle-input:focus {outline: none;border-color: #1d9bf0;box-shadow: 0 0 0 2px rgba(29, 155, 240, 0.2);}#handle-input::placeholder {color: rgba(231, 233, 234, 0.5);}.handle-input-container {display: flex;gap: 8px;align-items: center;margin-bottom: 10px;padding: 5px;border-radius: 8px;background-color: rgba(255, 255, 255, 0.03);}.add-handle-btn {background-color: #1d9bf0;color: white;border: none;border-radius: 8px;padding: 8px 16px;cursor: pointer;font-weight: bold;font-size: 14px;transition: background-color 0.2s;white-space: nowrap;}.add-handle-btn:hover {background-color: #1a8cd8;}.instructions-list {margin-top: 10px;max-height: 200px;overflow-y: auto;border: 1px solid rgba(255, 255, 255, 0.1);border-radius: 8px;padding: 5px;}.instruction-item {display: flex;align-items: center;justify-content: space-between;padding: 8px 10px;border-bottom: 1px solid rgba(255, 255, 255, 0.05);border-radius: 4px;transition: background-color 0.2s;}.instruction-item:hover {background-color: rgba(255, 255, 255, 0.05);}.instruction-item:last-child {border-bottom: none;}.instruction-text {font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;font-size: 12px;flex: 1;margin-right: 10px;}.instruction-buttons {display: flex;gap: 5px;}.use-instruction {background: none;border: none;color: #1d9bf0;cursor: pointer;font-size: 12px;padding: 3px 8px;border-radius: 4px;transition: all 0.2s;}.use-instruction:hover {background-color: rgba(29, 155, 240, 0.1);}.remove-instruction {background: none;border: none;color: #ff5c5c;cursor: pointer;font-size: 14px;padding: 0 3px;opacity: 0.7;transition: opacity 0.2s;border-radius: 4px;}.remove-instruction:hover {opacity: 1;background-color: rgba(255, 92, 92, 0.1);}.tweet-filtered {display: none !important;visibility: hidden !important;opacity: 0 !important;pointer-events: none !important;/* Ensure it stays hidden even if Twitter tries to show it */position: absolute !important;z-index: -9999 !important;height: 0 !important;width: 0 !important;margin: 0 !important;padding: 0 !important;overflow: hidden !important;}.filter-controls {display: flex;align-items: center;gap: 10px;margin: 5px 0;}.filter-controls input[type="range"] {flex: 1;min-width: 100px;}.filter-controls input[type="number"] {width: 50px;padding: 2px 5px;border: 1px solid #ccc;border-radius: 4px;text-align: center;}/* Hide number input spinners */.filter-controls input[type="number"]::-webkit-inner-spin-button,.filter-controls input[type="number"]::-webkit-outer-spin-button {-webkit-appearance: none;margin: 0;}.filter-controls input[type="number"] {-moz-appearance: textfield;}/* --- Metadata Specific Styling --- */.tooltip-metadata {font-size: 0.8em;opacity: 0.7;margin-top: 8px;padding-top: 8px;border-top: 1px solid rgba(255, 255, 255, 0.2);display: block;line-height: 1.5;}.metadata-line {white-space: nowrap;overflow: hidden;text-overflow: ellipsis;margin-bottom: 2px;}.metadata-separator {display: none;}/* --- Specific Indicator Styles --- */.score-indicator.pending-rating {}/* --- Tooltip Styles --- */.score-description {/* ... existing styles ... */max-width: 500px;padding-bottom: 35px; /* Add padding for scroll button *//* ... existing styles ... */}.score-description.streaming-tooltip {border-color: #ffa500; /* Orange border for streaming */}/* ... existing .tooltip-controls, .tooltip-pin-button, .tooltip-copy-button styles ... *//* --- Reasoning Dropdown --- */.reasoning-dropdown {/* ... existing styles ... */}.reasoning-toggle {/* ... existing styles ... */}.reasoning-arrow {/* ... existing styles ... */}.reasoning-content {/* ... existing styles ... */}.reasoning-text {/* ... existing styles ... */}.description-text {/* ... existing styles ... */}/* --- Last Answer Area --- */.tooltip-last-answer {margin-top: 10px;padding: 10px;background-color: rgba(255, 255, 255, 0.05); /* Slightly different background */border-radius: 4px;font-size: 0.9em;line-height: 1.4;}.answer-separator {border: none;border-top: 1px dashed rgba(255, 255, 255, 0.2);margin: 10px 0;}/* --- Follow-Up Questions Area --- */.tooltip-follow-up-questions {margin-top: 10px;display: flex;flex-direction: column;gap: 6px; /* Spacing between buttons */}.follow-up-question-button {background-color: rgba(60, 160, 240, 0.2); /* Light blue background */border: 1px solid rgba(60, 160, 240, 0.5);color: #e1e8ed; /* Light text */padding: 8px 12px;border-radius: 15px; /* Pill shape */cursor: pointer;font-size: 0.85em;text-align: left;transition: background-color 0.2s ease, border-color 0.2s ease;white-space: normal; /* Allow wrapping */line-height: 1.3;}.follow-up-question-button:hover {background-color: rgba(60, 160, 240, 0.35);border-color: rgba(60, 160, 240, 0.8);}.follow-up-question-button:active {background-color: rgba(60, 160, 240, 0.5);}/* --- Metadata Area --- */.tooltip-metadata {margin-top: 12px;padding-top: 8px;font-size: 0.8em;color: #8899a6; /* Muted color */border-top: 1px solid rgba(255, 255, 255, 0.1);}.metadata-separator {border: none;border-top: 1px dashed rgba(255, 255, 255, 0.2);margin: 8px 0;}.metadata-line {margin-bottom: 4px;}.metadata-line:last-child {margin-bottom: 0;}/* --- Score Highlight --- */.score-highlight {/* ... existing styles ... */}/* --- Scroll Button --- */.scroll-to-bottom-button {/* ... existing styles ... */}.tooltip-bottom-spacer {/* ... existing styles ... */}/* --- Custom Question Input Area --- */.tooltip-custom-question-container {margin-top: 15px;display: flex;gap: 8px;padding-top: 10px;border-top: 1px solid rgba(255, 255, 255, 0.1); /* Separator */}.tooltip-custom-question-input {flex-grow: 1; /* Take available space */padding: 8px 10px;border-radius: 6px;border: 1px solid rgba(255, 255, 255, 0.2);background-color: rgba(39, 44, 48, 0.9);color: #e7e9ea;font-size: 0.9em;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; /* Ensure font consistency */line-height: 1.4; /* Adjust line height for better text readability */resize: none; /* Prevent manual resizing by the user */overflow-y: hidden; /* Hide scrollbar initially, height adjustment will manage overflow */min-height: calc(0.9em * 1.4 + 16px + 2px); /* Approx height for 1 row: (font-size * line-height) + padding + border */box-sizing: border-box; /* Ensure padding and border are included in height calculation */}.tooltip-custom-question-input:focus {border-color: #1d9bf0;outline: none;}.tooltip-custom-question-button {background-color: #1d9bf0;color: white;border: none;border-radius: 6px;padding: 8px 12px;cursor: pointer;font-weight: bold;font-size: 0.9em;transition: background-color 0.2s;}.tooltip-custom-question-button:hover {background-color: #1a8cd8;}.tooltip-custom-question-button:disabled,.tooltip-custom-question-input:disabled {opacity: 0.6;cursor: not-allowed;}/* --- Conversation History Styling --- */.tooltip-conversation-history {margin-top: 15px;padding-top: 10px;border-top: 1px solid rgba(255, 255, 255, 0.1);display: flex;flex-direction: column;gap: 12px; /* Space between conversation turns */}.conversation-turn {background-color: rgba(255, 255, 255, 0.04);padding: 10px;border-radius: 6px;line-height: 1.4;}.conversation-question {font-size: 0.9em;color: #b0bec5; /* Lighter grey for user question */margin-bottom: 6px;}.conversation-question strong {color: #cfd8dc; /* Slightly brighter for "You:" */}.conversation-answer {font-size: 0.95em;color: #e1e8ed; /* Main text color for AI answer */}.conversation-answer strong {color: #1d9bf0; /* Twitter blue for "AI:" */}.conversation-separator {border: none;border-top: 1px dashed rgba(255, 255, 255, 0.15);margin: 0; /* Reset margin, gap handles spacing */}.pending-answer {color: #ffa726; /* Orange for pending state */font-style: italic;}/* Blinking cursor for streaming answers */.pending-cursor {display: inline-block;color: #1d9bf0; /* Twitter blue */animation: blink 0.7s infinite;font-weight: bold;margin-left: 2px;font-style: normal; /* Override italic from pending-answer if nested */}@keyframes blink {0%, 100% { opacity: 0; }50% { opacity: 1; }}/* Styling for links generated from markdown in AI answers */.ai-generated-link {color: #1d9bf0; /* Twitter blue */text-decoration: underline;transition: color 0.2s ease;}.ai-generated-link:hover {color: #1a8cd8; /* Slightly darker blue on hover */text-decoration: underline;}/* Style for the new tooltip close button */.tooltip-close-button {/* Reuse general close button styles */background: none !important;border: none !important;color: #8899a6 !important; /* Match other control button colors */cursor: pointer !important;font-size: 20px !important; /* Slightly larger for easier tapping */line-height: 1 !important;padding: 4px 8px !important;margin-left: 8px !important; /* Space from other buttons */border-radius: 50% !important; /* Make it round */width: 28px !important; /* Explicit size */height: 28px !important; /* Explicit size */display: flex !important;align-items: center !important;justify-content: center !important;transition: all 0.2s !important;order: 3; /* Ensure it comes after pin and copy */}.tooltip-close-button:hover {background-color: rgba(255, 92, 92, 0.1) !important; /* Reddish background on hover */color: #ff5c5c !important; /* Red color on hover */}.tooltip-close-button:active {transform: scale(0.95) !important;}/* Adjust mobile close button specifically if needed */@media (max-width: 600px) {.tooltip-close-button {font-size: 22px !important; /* Even larger on mobile */width: 32px !important;height: 32px !important;}/* Ensure controls container accommodates button */.tooltip-controls {padding-right: 40px !important; /* Add padding to prevent overlap if button was absolute */}}/* --- Styling for Reasoning Dropdown within Conversation Turn --- */.conversation-turn .reasoning-dropdown {margin-top: 8px; /* Space above the dropdown */margin-bottom: 8px; /* Space below the dropdown, before the answer */border-radius: 4px;background-color: rgba(255, 255, 255, 0.02); /* Slightly different background from turn itself */border: 1px solid rgba(255, 255, 255, 0.08);}.conversation-turn .reasoning-toggle {display: flex;align-items: center;color: #b0bec5; /* Muted color for toggle text */cursor: pointer;font-weight: normal; /* Less prominent than main reasoning toggle */font-size: 0.85em;padding: 6px 8px;user-select: none;transition: background-color 0.2s;}.conversation-turn .reasoning-toggle:hover {background-color: rgba(255, 255, 255, 0.05);}.conversation-turn .reasoning-arrow {display: inline-block;margin-right: 4px;font-size: 0.9em;transition: transform 0.2s ease;}.conversation-turn .reasoning-content {max-height: 0;overflow: hidden;transition: max-height 0.3s ease-out, padding 0.3s ease-out;background-color: rgba(0, 0, 0, 0.1);border-radius: 0 0 4px 4px;padding: 0 8px; /* Horizontal padding only when collapsed */}.conversation-turn .reasoning-dropdown.expanded .reasoning-content {max-height: 200px; /* Adjust as needed */overflow-y: auto;padding: 8px; /* Full padding when expanded */}.conversation-turn .reasoning-dropdown.expanded .reasoning-arrow {transform: rotate(90deg);}.conversation-turn .reasoning-text {font-size: 0.85em; /* Smaller text for reasoning */line-height: 1.4;color: #ccc; /* Similar to main reasoning text */margin: 0;padding: 0; /* Padding is on the content container */}/* Ensure scrollbars look consistent */.conversation-turn .reasoning-content::-webkit-scrollbar {width: 5px;}.conversation-turn .reasoning-content::-webkit-scrollbar-track {background: rgba(255, 255, 255, 0.05);border-radius: 3px;}.conversation-turn .reasoning-content::-webkit-scrollbar-thumb {background: rgba(255, 255, 255, 0.2);border-radius: 3px;}.conversation-turn .reasoning-content::-webkit-scrollbar-thumb:hover {background: rgba(255, 255, 255, 0.3);}/* --- Styling for Image Upload in Follow-up --- */.tooltip-attach-image-button {background: none;border: none;color: #8899a6; /* Muted color, similar to other controls */font-size: 1.2em; /* Slightly larger for icon visibility */cursor: pointer;padding: 6px 8px; /* Adjust padding to align with input/button height */margin: 0 4px; /* Space around the icon */border-radius: 4px;transition: all 0.2s ease;align-self: center; /* Vertically align with input and Ask button */}.tooltip-attach-image-button:hover {background-color: rgba(29, 155, 240, 0.1);color: #1d9bf0;}.tooltip-follow-up-image-preview-container {margin-top: 10px;padding-top: 10px;border-top: 1px solid rgba(255, 255, 255, 0.1);display: flex; /* Changed to flex for easier alignment of preview and button */flex-direction: row; /* Lay out previews in a row */flex-wrap: wrap; /* Allow previews to wrap to the next line */gap: 10px; /* Spacing between preview items */align-items: flex-start;}.follow-up-image-preview-item {position: relative; /* For positioning the remove button */display: flex;flex-direction: column;align-items: center;border: 1px solid rgba(255, 255, 255, 0.2);border-radius: 6px;padding: 5px;background-color: rgba(255, 255, 255, 0.05);}.follow-up-image-preview-thumbnail {max-width: 80px; /* Smaller thumbnails for multiple previews */max-height: 80px;border-radius: 4px;object-fit: cover; /* Or contain, depending on desired look */margin-bottom: 5px; /* Space between image and potential future captions */}.follow-up-image-remove-btn {position: absolute;top: -5px;right: -5px;background-color: rgba(40, 40, 40, 0.8);color: white;border: 1px solid rgba(255,255,255,0.3);border-radius: 50%; /* Circular button */width: 20px;height: 20px;font-size: 12px;font-weight: bold;line-height: 18px; /* Adjust for vertical centering of X */text-align: center;cursor: pointer;padding: 0;transition: background-color 0.2s ease, transform 0.2s ease;}.follow-up-image-remove-btn:hover {background-color: rgba(255, 92, 92, 0.9);transform: scale(1.1);}/* Adjust custom question container to be flex for alignment */.tooltip-custom-question-container {/* ... existing styles ... */display: flex; /* Ensures items are in a row */align-items: center; /* Vertically aligns items in the middle */}.tooltip-custom-question-input {/* ... existing styles ... */margin-right: 0; /* Remove right margin if any, gap handles spacing */}/* --- Styling for Uploaded Image in Conversation History --- */.conversation-image-container {margin-top: 8px; /* Space above the image */margin-bottom: 8px; /* Space below the image, before reasoning/answer */display: flex; /* Use flex for multiple images */flex-wrap: wrap; /* Allow images to wrap */gap: 8px; /* Space between images */}.conversation-uploaded-image {max-width: 80%; /* Limit width to not dominate the tooltip */max-height: 120px; /* Slightly larger than preview, but still constrained */border-radius: 6px;border: 1px solid rgba(255, 255, 255, 0.2);object-fit: contain; /* Maintain aspect ratio */display: block; /* Ensure it takes its own line if needed */cursor: pointer; /* Indicate it can be clicked (e.g., for lightbox in future) */transition: transform 0.2s ease;}.conversation-uploaded-image:hover {transform: scale(1.02); /* Slight zoom on hover */}`;
- // Apply CSS
- GM_addStyle(STYLE);
- // Set menu HTML
- GM_setValue('menuHTML', MENU);
- // ----- helpers/browserStorage.js -----
- function browserGet(key, defaultValue = null) {
- try {
- return GM_getValue(key, defaultValue);
- } catch (error) {
- return defaultValue;
- }
- }
- function browserSet(key, value) {
- try {
- GM_setValue(key, value);
- } catch (error) {
- }
- }
- // ----- helpers/cache.js -----
- function updateCacheStatsUI() {
- const cachedCountEl = document.getElementById('cached-ratings-count');
- const whitelistedCountEl = document.getElementById('whitelisted-handles-count');
- const cachedCount = tweetCache.size;
- const wlCount = blacklistedHandles.length;
- if (cachedCountEl) cachedCountEl.textContent = cachedCount;
- if (whitelistedCountEl) whitelistedCountEl.textContent = wlCount;
- const statsBadge = document.getElementById("tweet-filter-stats-badge");
- if (statsBadge) statsBadge.innerHTML = `
- <span style="margin-right: 5px;">🧠</span>
- <span data-cached-count>${cachedCount} rated</span>
- <span data-pending-count> | ${pendingRequests} pending</span>
- ${wlCount > 0 ? `<span style="margin-left: 5px;"> | ${wlCount} whitelisted</span>` : ''}
- `;
- }
- // ----- backends/TweetCache.js -----
- function debounce(func, wait) {
- let timeout;
- return function executedFunction(...args) {
- const later = () => {
- clearTimeout(timeout);
- func.apply(this, args);
- };
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- };
- };
- class TweetCache {
- static DEBOUNCE_DELAY = 1500;
- constructor() {
- this.cache = {};
- this.loadFromStorage();
- this.debouncedSaveToStorage = debounce(this.#saveToStorageInternal.bind(this), TweetCache.DEBOUNCE_DELAY);
- }
- loadFromStorage() {
- try {
- const storedCache = browserGet('tweetRatings', '{}');
- this.cache = JSON.parse(storedCache);
- for (const tweetId in this.cache) {
- this.cache[tweetId].fromStorage = true;
- }
- } catch (error) {
- this.cache = {};
- }
- }
- #saveToStorageInternal() {
- try {
- browserSet('tweetRatings', JSON.stringify(this.cache));
- updateCacheStatsUI();
- } catch (error) {
- }
- }
- get(tweetId) {
- return this.cache[tweetId] || null;
- }
- set(tweetId, rating, saveImmediately = true) {
- this.cache[tweetId] = {
- score: rating.score,
- fullContext: rating.fullContext || '',
- description: rating.description || '',
- reasoning: rating.reasoning || '',
- questions: rating.questions || [],
- lastAnswer: rating.lastAnswer || '',
- mediaUrls: rating.mediaUrls || [],
- timestamp: rating.timestamp || Date.now(),
- streaming: rating.streaming || false,
- blacklisted: rating.blacklisted || false,
- fromStorage: rating.fromStorage || false,
- metadata: {
- model: rating.metadata?.model || null,
- promptTokens: rating.metadata?.promptTokens || null,
- completionTokens: rating.metadata?.completionTokens || null,
- latency: rating.metadata?.latency || null,
- mediaInputs: rating.metadata?.mediaInputs || null,
- price: rating.metadata?.price || null
- },
- qaConversationHistory: rating.qaConversationHistory || []
- };
- if(!saveImmediately) {
- this.debouncedSaveToStorage();
- } else {
- this.#saveToStorageInternal();
- }
- }
- has(tweetId) {
- return this.cache[tweetId] !== undefined;
- }
- delete(tweetId, saveImmediately = true) {
- if (this.has(tweetId)) {
- delete this.cache[tweetId];
- this.debouncedSaveToStorage();
- }
- }
- clear(saveImmediately = false) {
- this.cache = {};
- if (saveImmediately) {
- this.#saveToStorageInternal();
- } else {
- this.debouncedSaveToStorage();
- }
- }
- get size() {
- return Object.keys(this.cache).length;
- }
- cleanup(saveImmediately = true) {
- const beforeCount = this.size;
- let deletedCount = 0;
- let streamingDeletedCount = 0;
- let undefinedScoreCount = 0;
- let missingQaHistoryCount = 0;
- for (const tweetId in this.cache) {
- const entry = this.cache[tweetId];
- let shouldDelete = false;
- if (entry.score === undefined || entry.score === null) {
- if (entry.streaming === true) {
- streamingDeletedCount++;
- } else {
- undefinedScoreCount++;
- }
- shouldDelete = true;
- }
- if (!entry.streaming && entry.score !== undefined && entry.score !== null && !entry.blacklisted &&
- (!entry.qaConversationHistory || !Array.isArray(entry.qaConversationHistory) || entry.qaConversationHistory.length < 3)) {
- missingQaHistoryCount++;
- shouldDelete = true;
- }
- if (shouldDelete) {
- delete this.cache[tweetId];
- deletedCount++;
- }
- }
- if (deletedCount > 0) {
- this.debouncedSaveToStorage();
- }
- return {
- beforeCount,
- afterCount: this.size,
- deletedCount,
- streamingDeletedCount,
- undefinedScoreCount,
- missingQaHistoryCount
- };
- }
- }
- const tweetCache = new TweetCache();
- // ----- backends/InstructionsHistory.js -----
- class InstructionsHistory {
- constructor() {
- if (InstructionsHistory.instance) {
- return InstructionsHistory.instance;
- }
- InstructionsHistory.instance = this;
- this.history = [];
- this.maxEntries = 10;
- this.loadFromStorage();
- }
- #hashString(str) {
- let hash = 0;
- for (let i = 0; i < str.length; i++) {
- const char = str.charCodeAt(i);
- hash = ((hash << 5) - hash) + char;
- hash = hash & hash;
- }
- return hash.toString(36);
- }
- loadFromStorage() {
- try {
- const stored = browserGet('instructionsHistory', '[]');
- this.history = JSON.parse(stored);
- if (!Array.isArray(this.history)) {
- throw new Error('Stored history is not an array');
- }
- this.history = this.history.map(entry => ({
- ...entry,
- hash: entry.hash || this.#hashString(entry.instructions)
- }));
- } catch (e) {
- this.history = [];
- }
- }
- #saveToStorage() {
- try {
- browserSet('instructionsHistory', JSON.stringify(this.history));
- } catch (e) {
- throw new Error('Failed to save instructions history');
- }
- }
- async add(instructions, summary) {
- try {
- if (!instructions?.trim() || !summary?.trim()) {
- throw new Error('Invalid instructions or summary');
- }
- const hash = this.#hashString(instructions.trim());
- const existingIndex = this.history.findIndex(entry => entry.hash === hash);
- if (existingIndex !== -1) {
- this.history[existingIndex].timestamp = Date.now();
- this.history[existingIndex].summary = summary;
- const entry = this.history.splice(existingIndex, 1)[0];
- this.history.unshift(entry);
- } else {
- this.history.unshift({
- instructions: instructions.trim(),
- summary: summary.trim(),
- timestamp: Date.now(),
- hash
- });
- if (this.history.length > this.maxEntries) {
- this.history = this.history.slice(0, this.maxEntries);
- }
- }
- this.#saveToStorage();
- return true;
- } catch (e) {
- return false;
- }
- }
- remove(index) {
- try {
- if (index < 0 || index >= this.history.length) {
- throw new Error('Invalid history index');
- }
- this.history.splice(index, 1);
- this.#saveToStorage();
- return true;
- } catch (e) {
- return false;
- }
- }
- getAll() {
- return [...this.history];
- }
- get(index) {
- try {
- if (index < 0 || index >= this.history.length) {
- return null;
- }
- return { ...this.history[index] };
- } catch (e) {
- return null;
- }
- }
- clear() {
- try {
- this.history = [];
- this.#saveToStorage();
- } catch (e) {
- throw new Error('Failed to clear instructions history');
- }
- }
- get size() {
- return this.history.length;
- }
- }
- // ----- backends/InstructionsManager.js -----
- class InstructionsManager {
- constructor() {
- if (InstructionsManager.instance) {
- return InstructionsManager.instance;
- }
- InstructionsManager.instance = this;
- this.history = new InstructionsHistory();
- this.currentInstructions = browserGet('userDefinedInstructions', '');
- }
- async saveInstructions(instructions) {
- if (!instructions?.trim()) {
- return { success: false, message: 'Instructions cannot be empty' };
- }
- instructions = instructions.trim();
- this.currentInstructions = instructions;
- browserSet('userDefinedInstructions', instructions);
- if (typeof USER_DEFINED_INSTRUCTIONS !== 'undefined') {
- USER_DEFINED_INSTRUCTIONS = instructions;
- }
- const summary = await getCustomInstructionsDescription(instructions);
- if (!summary.error) {
- await this.history.add(instructions, summary.content);
- }
- return {
- success: true,
- message: 'Scoring instructions saved! New tweets will use these instructions.',
- shouldClearCache: true
- };
- }
- getCurrentInstructions() {
- return this.currentInstructions;
- }
- getHistory() {
- return this.history.getAll();
- }
- removeFromHistory(index) {
- return this.history.remove(index);
- }
- clearHistory() {
- this.history.clear();
- }
- }
- const instructionsManager = new InstructionsManager();
- // ----- config.js -----
- const processedTweets = new Set();
- const adAuthorCache = new Set();
- const PROCESSING_DELAY_MS = 150;
- const API_CALL_DELAY_MS = 25;
- let USER_DEFINED_INSTRUCTIONS = instructionsManager.getCurrentInstructions() || 'Rate the tweet on a scale from 1 to 10 based on its clarity, insight, creativity, and overall quality.';
- let currentFilterThreshold = parseInt(browserGet('filterThreshold', '5'));
- let observedTargetNode = null;
- let lastAPICallTime = 0;
- let pendingRequests = 0;
- const MAX_RETRIES = 5;
- let availableModels = [];
- let listedModels = [];
- let selectedModel = browserGet('selectedModel', 'openai/gpt-4.1-nano');
- let selectedImageModel = browserGet('selectedImageModel', 'openai/gpt-4.1-nano');
- let showFreeModels = browserGet('showFreeModels', true);
- let providerSort = browserGet('providerSort', '');
- let blacklistedHandles = browserGet('blacklistedHandles', '').split('\n').filter(h => h.trim() !== '');
- let storedRatings = browserGet('tweetRatings', '{}');
- let threadHist = "";
- let enableImageDescriptions = browserGet('enableImageDescriptions', false);
- let enableStreaming = browserGet('enableStreaming', true);
- const REVIEW_SYSTEM_PROMPT=`
- You are **TweetFilter-AI**.
- When given a tweet, do these three steps **in order**:
- 1. **ANALYZE** - Judge how closely the tweet matches the user's instructions.
- 2. **SCORE** - Assign an integer from 0'10 (inclusive) based *only* on that alignment.
- 3. **ASK** - Write **exactly three** open-ended follow-up questions the user might ask next.
- • Questions must be answerable from the tweet itself or general knowledge.
- • Do **not** ask for information that requires unavailable context (e.g., the author's other tweets).
- **Important constraints**
- • You do **not** have up-to-the-minute knowledge of current events.
- → If a tweet makes a factual claim you cannot verify, **do not down-score it** for "fake news"; instead, evaluate it solely on the user's criteria and note any uncertainty in your analysis.
- → Only down-score when the tweet contradicts widely-known, stable facts or directly violates the user's instructions.
- ⚠️ Output must match **exactly** the EXPECTED_RESPONSE_FORMAT" - no extra text, no missing tags - or the pipeline crashes.
- EXPECTED_RESPONSE_FORMAT: (begin with <ANALYSIS> and end with </FOLLOW_UP_QUESTIONS>)
- <ANALYSIS>
- (Your analysis goes here.)
- </ANALYSIS>
- <SCORE>
- SCORE_X
- </SCORE>
- <FOLLOW_UP_QUESTIONS>
- Q_1. …
- Q_2. …
- Q_3. …
- </FOLLOW_UP_QUESTIONS>
- End of EXPECTED_RESPONSE_FORMAT
- `;
- const FOLLOW_UP_SYSTEM_PROMPT = `
- You are TweetFilter-AI, continuing a conversation about a tweet.
- The user has asked a follow-up question.
- Your entire conversation history up to this point is provided in the message list.
- Please provide an answer and then generate 3 new, relevant follow-up questions.
- Adhere strictly to the following response format:
- <ANSWER>
- (Your answer here)
- </ANSWER>
- <FOLLOW_UP_QUESTIONS>
- Q_1. (New Question 1 here)
- Q_2. (New Question 2 here)
- Q_3. (New Question 3 here)
- </FOLLOW_UP_QUESTIONS>
- `;
- let modelTemperature = parseFloat(browserGet('modelTemperature', '0.5'));
- let modelTopP = parseFloat(browserGet('modelTopP', '0.9'));
- let imageModelTemperature = parseFloat(browserGet('imageModelTemperature', '0.5'));
- let imageModelTopP = parseFloat(browserGet('imageModelTopP', '0.9'));
- let maxTokens = parseInt(browserGet('maxTokens', '0'));
- const TWEET_ARTICLE_SELECTOR = 'article[data-testid="tweet"]';
- const QUOTE_CONTAINER_SELECTOR = 'div[role="link"][tabindex="0"]';
- const USER_HANDLE_SELECTOR = 'div[data-testid="User-Name"] a[role="link"]';
- const TWEET_TEXT_SELECTOR = 'div[data-testid="tweetText"]';
- const MEDIA_IMG_SELECTOR = 'div[data-testid="tweetPhoto"] img, img[src*="pbs.twimg.com/media"]';
- const MEDIA_VIDEO_SELECTOR = 'video[poster*="pbs.twimg.com"], video';
- const PERMALINK_SELECTOR = 'a[href*="/status/"] time';
- function modelSupportsImages(modelId) {
- if (!availableModels || availableModels.length === 0) {
- return false;
- }
- const model = availableModels.find(m => m.slug === modelId);
- if (!model) {
- return false;
- }
- return model.input_modalities &&
- model.input_modalities.includes('image');
- }
- // ----- domScraper.js -----
- function getElementText(elements) {
- if (!elements) return '';
- const elementList = elements instanceof NodeList ? Array.from(elements) : [elements];
- for (const element of elementList) {
- const text = element?.textContent?.trim();
- if (text) return text;
- }
- return '';
- }
- function getTweetID(tweetArticle) {
- const timeEl = tweetArticle.querySelector(PERMALINK_SELECTOR);
- let tweetId = timeEl?.parentElement?.href;
- if (tweetId && tweetId.includes('/status/')) {
- const match = tweetId.match(/\/status\/(\d+)/);
- if (match && match[1]) {
- return match[1];
- }
- return tweetId.substring(tweetId.indexOf('/status/') + 1);
- }
- return `tweet-${Math.random().toString(36).substring(2, 15)}-${Date.now()}`;
- }
- function getUserHandles(tweetArticle) {
- let handles = [];
- const handleElement = tweetArticle.querySelector(USER_HANDLE_SELECTOR);
- if (handleElement) {
- const href = handleElement.getAttribute('href');
- if (href && href.startsWith('/')) {
- handles.push(href.slice(1));
- }
- }
- if (handles.length > 0) {
- const quoteContainer = tweetArticle.querySelector('div[role="link"][tabindex="0"]');
- if (quoteContainer) {
- const userAvatarDiv = quoteContainer.querySelector('div[data-testid^="UserAvatar-Container-"]');
- if (userAvatarDiv) {
- const testId = userAvatarDiv.getAttribute('data-testid');
- const lastDashIndex = testId.lastIndexOf('-');
- if (lastDashIndex >= 0 && lastDashIndex < testId.length - 1) {
- const quotedHandle = testId.substring(lastDashIndex + 1);
- if (quotedHandle && quotedHandle !== handles[0]) {
- handles.push(quotedHandle);
- }
- }
- const quotedLink = quoteContainer.querySelector('a[href*="/status/"]');
- if (quotedLink) {
- const href = quotedLink.getAttribute('href');
- const match = href.match(/^\/([^/]+)\/status\/\d+/);
- if (match && match[1] && match[1] !== handles[0]) {
- handles.push(match[1]);
- }
- }
- }
- }
- }
- return handles.length > 0 ? handles : [''];
- }
- async function extractMediaLinks(scopeElement) {
- if (!scopeElement) return [];
- const mediaLinks = new Set();
- const imgSelector = `${MEDIA_IMG_SELECTOR}, [data-testid="tweetPhoto"] img, img[src*="pbs.twimg.com/media"]`;
- const videoSelector = `${MEDIA_VIDEO_SELECTOR}, video[poster*="pbs.twimg.com"], video`;
- const combinedSelector = `${imgSelector}, ${videoSelector}`;
- let mediaElements = scopeElement.querySelectorAll(combinedSelector);
- const RETRY_DELAY = 100;
- let retries = 0;
- while (mediaElements.length === 0 && retries < MAX_RETRIES) {
- retries++;
- await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
- mediaElements = scopeElement.querySelectorAll(combinedSelector);
- }
- if (mediaElements.length === 0 && scopeElement.matches(QUOTE_CONTAINER_SELECTOR)) {
- mediaElements = scopeElement.querySelectorAll('img[src*="pbs.twimg.com"], video[poster*="pbs.twimg.com"]');
- }
- mediaElements.forEach(mediaEl => {
- const sourceUrl = mediaEl.tagName === 'IMG' ? mediaEl.src : mediaEl.poster;
- if (!sourceUrl ||
- !(sourceUrl.includes('pbs.twimg.com/')) ||
- sourceUrl.includes('profile_images')) {
- return;
- }
- try {
- const url = new URL(sourceUrl);
- const name = url.searchParams.get('name');
- let finalUrl = sourceUrl;
- if (name && name !== 'orig') {
- finalUrl = sourceUrl.replace(`name=${name}`, 'name=small');
- }
- mediaLinks.add(finalUrl);
- } catch (error) {
- mediaLinks.add(sourceUrl);
- }
- });
- return Array.from(mediaLinks);
- }
- function isOriginalTweet(tweetArticle) {
- let sibling = tweetArticle.nextElementSibling;
- while (sibling) {
- if (sibling.matches && sibling.matches('div[data-testid="inline_reply_offscreen"]')) {
- return true;
- }
- sibling = sibling.nextElementSibling;
- }
- return false;
- }
- function handleMutations(mutationsList) {
- let tweetsAdded = false;
- let needsCleanup = false;
- const shouldSkipProcessing = (element) => {
- if (!element) return true;
- if (element.dataset?.filtered === 'true' || element.dataset?.isAd === 'true') {
- return true;
- }
- const cell = element.closest('div[data-testid="cellInnerDiv"]');
- if (cell?.dataset?.filtered === 'true' || cell?.dataset?.isAd === 'true') {
- return true;
- }
- if (isAd(element)) {
- if (cell) {
- cell.dataset.isAd = 'true';
- cell.classList.add('tweet-filtered');
- }
- element.dataset.isAd = 'true';
- return true;
- }
- const tweetId = getTweetID(element);
- if (processedTweets.has(tweetId)) {
- const indicator = ScoreIndicatorRegistry.get(tweetId);
- if (indicator && indicator.status !== 'error') {
- return true;
- }
- }
- return false;
- };
- for (const mutation of mutationsList) {
- if (mutation.type === 'childList') {
- if (mutation.addedNodes.length > 0) {
- mutation.addedNodes.forEach(node => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- let conversationTimeline = null;
- if (node.matches && node.matches('div[aria-label^="Timeline: Conversation"]')) {
- conversationTimeline = node;
- } else if (node.querySelector) {
- conversationTimeline = node.querySelector('div[aria-label^="Timeline: Conversation"]');
- }
- if (conversationTimeline) {
- setTimeout(handleThreads, 50);
- }
- if (node.matches && node.matches(TWEET_ARTICLE_SELECTOR)) {
- if (!shouldSkipProcessing(node)) {
- scheduleTweetProcessing(node);
- tweetsAdded = true;
- }
- }
- else if (node.querySelector) {
- const tweetsInside = node.querySelectorAll(TWEET_ARTICLE_SELECTOR);
- tweetsInside.forEach(tweet => {
- if (!shouldSkipProcessing(tweet)) {
- scheduleTweetProcessing(tweet);
- tweetsAdded = true;
- }
- });
- }
- }
- });
- }
- if (mutation.removedNodes.length > 0) {
- mutation.removedNodes.forEach(node => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- if (node.dataset?.filtered === 'true' || node.dataset?.isAd === 'true') {
- return;
- }
- if (node.matches && node.matches(TWEET_ARTICLE_SELECTOR)) {
- const tweetId = getTweetID(node);
- if (tweetId) {
- ScoreIndicatorRegistry.get(tweetId)?.destroy();
- needsCleanup = true;
- }
- }
- else if (node.querySelectorAll) {
- const removedTweets = node.querySelectorAll(TWEET_ARTICLE_SELECTOR);
- removedTweets.forEach(tweet => {
- if (tweet.dataset?.filtered === 'true' || tweet.dataset?.isAd === 'true') {
- return;
- }
- const tweetId = getTweetID(tweet);
- if (tweetId) {
- ScoreIndicatorRegistry.get(tweetId)?.destroy();
- needsCleanup = true;
- }
- });
- }
- }
- });
- }
- }
- }
- if (tweetsAdded) {
- setTimeout(() => {
- applyFilteringToAll();
- }, 100);
- }
- if (needsCleanup) {
- ScoreIndicatorRegistry.cleanupOrphaned();
- }
- }
- function isAd(tweetArticle) {
- if (!tweetArticle) return false;
- const spans = tweetArticle.querySelectorAll('div[dir="ltr"] span');
- for (const span of spans) {
- if (span.textContent.trim() === 'Ad' && !span.children.length) {
- return true;
- }
- }
- return false;
- }
- // ----- ui/utils.js -----
- function isMobileDevice() {
- return (window.innerWidth <= 600 ||
- /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent));
- }
- function showStatus(message, type = 'info') {
- const indicator = document.getElementById('status-indicator');
- if (!indicator) {
- return;
- }
- indicator.textContent = message;
- indicator.className = 'active ' + type;
- setTimeout(() => { indicator.classList.remove('active', type); }, 3000);
- }
- function resizeImage(file, maxDimPx) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = (event) => {
- const img = new Image();
- img.onload = () => {
- let { width, height } = img;
- let newWidth, newHeight;
- if (width > height) {
- if (width > maxDimPx) {
- newWidth = maxDimPx;
- newHeight = height * (maxDimPx / width);
- } else {
- newWidth = width;
- newHeight = height;
- }
- } else {
- if (height > maxDimPx) {
- newHeight = maxDimPx;
- newWidth = width * (maxDimPx / height);
- } else {
- newWidth = width;
- newHeight = height;
- }
- }
- const canvas = document.createElement('canvas');
- canvas.width = newWidth;
- canvas.height = newHeight;
- const ctx = canvas.getContext('2d');
- ctx.drawImage(img, 0, 0, newWidth, newHeight);
- const dataUrl = canvas.toDataURL('image/jpeg', 0.9);
- resolve(dataUrl);
- };
- img.onerror = (error) => {
- reject(new Error("Could not load image for resizing."));
- };
- img.src = event.target.result;
- };
- reader.onerror = (error) => {
- reject(new Error("Could not read file."));
- };
- reader.readAsDataURL(file);
- });
- }
- // ----- ui/InstructionsUI.js -----
- async function saveInstructions() {
- const instructionsTextarea = document.getElementById('user-instructions');
- const result = await instructionsManager.saveInstructions(instructionsTextarea.value);
- showStatus(result.message);
- if (result.success && result.shouldClearCache) {
- if (isMobileDevice() || confirm('Do you want to clear the rating cache to apply these instructions to all tweets?')) {
- clearTweetRatingsAndRefreshUI();
- }
- }
- if (result.success) {
- refreshInstructionsHistory();
- }
- }
- function refreshInstructionsHistory() {
- const listElement = document.getElementById('instructions-list');
- if (!listElement) return;
- const history = instructionsManager.getHistory();
- listElement.innerHTML = '';
- if (history.length === 0) {
- const emptyMsg = document.createElement('div');
- emptyMsg.style.cssText = 'padding: 8px; opacity: 0.7; font-style: italic;';
- emptyMsg.textContent = 'No saved instructions yet';
- listElement.appendChild(emptyMsg);
- return;
- }
- history.forEach((entry, index) => {
- const item = createHistoryItem(entry, index);
- listElement.appendChild(item);
- });
- }
- function createHistoryItem(entry, index) {
- const item = document.createElement('div');
- item.className = 'instruction-item';
- item.dataset.index = index;
- const text = document.createElement('div');
- text.className = 'instruction-text';
- text.textContent = entry.summary;
- text.title = entry.instructions;
- item.appendChild(text);
- const buttons = document.createElement('div');
- buttons.className = 'instruction-buttons';
- const useBtn = document.createElement('button');
- useBtn.className = 'use-instruction';
- useBtn.textContent = 'Use';
- useBtn.title = 'Use these instructions';
- useBtn.onclick = () => useInstructions(entry.instructions);
- buttons.appendChild(useBtn);
- const removeBtn = document.createElement('button');
- removeBtn.className = 'remove-instruction';
- removeBtn.textContent = '×';
- removeBtn.title = 'Remove from history';
- removeBtn.onclick = () => removeInstructions(index);
- buttons.appendChild(removeBtn);
- item.appendChild(buttons);
- return item;
- }
- function useInstructions(instructions) {
- const textarea = document.getElementById('user-instructions');
- if (textarea) {
- textarea.value = instructions;
- saveInstructions();
- }
- }
- function removeInstructions(index) {
- if (instructionsManager.removeFromHistory(index)) {
- refreshInstructionsHistory();
- showStatus('Instructions removed from history');
- } else {
- showStatus('Error removing instructions');
- }
- }
- function clearInstructionsHistory() {
- if (isMobileDevice() || confirm('Are you sure you want to clear all instruction history?')) {
- instructionsManager.clearHistory();
- refreshInstructionsHistory();
- showStatus('Instructions history cleared');
- }
- }
- // ----- ui/ScoreIndicator.js -----
- class ScoreIndicator {
- constructor(tweetArticle) {
- if (!tweetArticle || !tweetArticle.nodeType || tweetArticle.nodeType !== Node.ELEMENT_NODE) {
- throw new Error("ScoreIndicator requires a valid tweet article DOM element.");
- }
- this.tweetArticle = tweetArticle;
- this.tweetId = getTweetID(this.tweetArticle);
- this.indicatorElement = null;
- this.tooltipElement = null;
- this.tooltipControls = null;
- this.pinButton = null;
- this.copyButton = null;
- this.tooltipCloseButton = null;
- this.reasoningDropdown = null;
- this.reasoningToggle = null;
- this.reasoningArrow = null;
- this.reasoningContent = null;
- this.reasoningTextElement = null;
- this.descriptionElement = null;
- this.scoreTextElement = null;
- this.followUpQuestionsTextElement = null;
- this.scrollButton = null;
- this.metadataElement = null;
- this.conversationContainerElement = null;
- this.followUpQuestionsElement = null;
- this.customQuestionContainer = null;
- this.customQuestionInput = null;
- this.customQuestionButton = null;
- this.attachImageButton = null;
- this.followUpImageContainer = null;
- this.followUpImageInput = null;
- this.followUpImagePreview = null;
- this.followUpRemoveImageButton = null;
- this.uploadedImageDataUrls = [];
- this.status = 'pending';
- this.score = null;
- this.description = '';
- this.reasoning = '';
- this.metadata = null;
- this.conversationHistory = [];
- this.questions = [];
- this.isPinned = false;
- this.isVisible = false;
- this.autoScroll = true;
- this.userInitiatedScroll = false;
- this.uploadedImageDataUrls = [];
- this.qaConversationHistory = [];
- try {
- this._createElements(tweetArticle);
- this._addEventListeners();
- ScoreIndicatorRegistry.add(this.tweetId, this);
- this.updateDatasetAttributes(tweetArticle);
- } catch (error) {
- this.destroy();
- throw error;
- }
- }
- _createElements(initialTweetArticle) {
- this.indicatorElement = document.createElement('div');
- this.indicatorElement.className = 'score-indicator';
- this.indicatorElement.dataset.tweetId = this.tweetId;
- const currentPosition = window.getComputedStyle(initialTweetArticle).position;
- if (currentPosition !== 'relative' && currentPosition !== 'absolute' && currentPosition !== 'fixed' && currentPosition !== 'sticky') {
- initialTweetArticle.style.position = 'relative';
- }
- initialTweetArticle.appendChild(this.indicatorElement);
- this.tooltipElement = document.createElement('div');
- this.tooltipElement.className = 'score-description';
- this.tooltipElement.style.display = 'none';
- this.tooltipElement.dataset.tweetId = this.tweetId;
- this.tooltipElement.dataset.autoScroll = this.autoScroll ? 'true' : 'false';
- this.tooltipControls = document.createElement('div');
- this.tooltipControls.className = 'tooltip-controls';
- this.tooltipCloseButton = document.createElement('button');
- this.tooltipCloseButton.className = 'close-button tooltip-close-button';
- this.tooltipCloseButton.innerHTML = '×';
- this.tooltipCloseButton.title = 'Close tooltip';
- this.pinButton = document.createElement('button');
- this.pinButton.className = 'tooltip-pin-button';
- this.pinButton.innerHTML = '📌';
- this.pinButton.title = 'Pin tooltip (prevents auto-closing)';
- this.copyButton = document.createElement('button');
- this.copyButton.className = 'tooltip-copy-button';
- this.copyButton.innerHTML = '📋';
- this.copyButton.title = 'Copy content to clipboard';
- this.tooltipControls.appendChild(this.pinButton);
- this.tooltipControls.appendChild(this.copyButton);
- this.tooltipControls.appendChild(this.tooltipCloseButton);
- this.tooltipElement.appendChild(this.tooltipControls);
- this.reasoningDropdown = document.createElement('div');
- this.reasoningDropdown.className = 'reasoning-dropdown';
- this.reasoningDropdown.style.display = 'none';
- this.reasoningToggle = document.createElement('div');
- this.reasoningToggle.className = 'reasoning-toggle';
- this.reasoningArrow = document.createElement('span');
- this.reasoningArrow.className = 'reasoning-arrow';
- this.reasoningArrow.textContent = '▶';
- this.reasoningToggle.appendChild(this.reasoningArrow);
- this.reasoningToggle.appendChild(document.createTextNode(' Show Reasoning Trace'));
- this.reasoningContent = document.createElement('div');
- this.reasoningContent.className = 'reasoning-content';
- this.reasoningTextElement = document.createElement('p');
- this.reasoningTextElement.className = 'reasoning-text';
- this.reasoningContent.appendChild(this.reasoningTextElement);
- this.reasoningDropdown.appendChild(this.reasoningToggle);
- this.reasoningDropdown.appendChild(this.reasoningContent);
- this.tooltipElement.appendChild(this.reasoningDropdown);
- this.descriptionElement = document.createElement('div');
- this.descriptionElement.className = 'description-text';
- this.tooltipElement.appendChild(this.descriptionElement);
- this.scoreTextElement = document.createElement('div');
- this.scoreTextElement.className = 'score-text-from-description';
- this.scoreTextElement.style.display = 'none';
- this.tooltipElement.appendChild(this.scoreTextElement);
- this.followUpQuestionsTextElement = document.createElement('div');
- this.followUpQuestionsTextElement.className = 'follow-up-questions-text-from-description';
- this.followUpQuestionsTextElement.style.display = 'none';
- this.tooltipElement.appendChild(this.followUpQuestionsTextElement);
- this.conversationContainerElement = document.createElement('div');
- this.conversationContainerElement.className = 'tooltip-conversation-history';
- this.tooltipElement.appendChild(this.conversationContainerElement);
- this.followUpQuestionsElement = document.createElement('div');
- this.followUpQuestionsElement.className = 'tooltip-follow-up-questions';
- this.followUpQuestionsElement.style.display = 'none';
- this.tooltipElement.appendChild(this.followUpQuestionsElement);
- this.customQuestionContainer = document.createElement('div');
- this.customQuestionContainer.className = 'tooltip-custom-question-container';
- this.customQuestionInput = document.createElement('textarea');
- this.customQuestionInput.placeholder = 'Ask your own question...';
- this.customQuestionInput.className = 'tooltip-custom-question-input';
- this.customQuestionInput.rows = 1;
- this.customQuestionInput.addEventListener('input', function() {
- this.style.height = 'auto';
- this.style.height = (this.scrollHeight) + 'px';
- });
- const currentSelectedModel = browserGet('selectedModel', 'openai/gpt-4.1-nano');
- const supportsImages = typeof modelSupportsImages === 'function' && modelSupportsImages(currentSelectedModel);
- if (supportsImages) {
- this.attachImageButton = document.createElement('button');
- this.attachImageButton.textContent = '📎';
- this.attachImageButton.className = 'tooltip-attach-image-button';
- this.attachImageButton.title = 'Attach image(s)';
- this.followUpImageInput = document.createElement('input');
- this.followUpImageInput.type = 'file';
- this.followUpImageInput.accept = 'image/' + '*';
- this.followUpImageInput.multiple = true;
- this.followUpImageInput.style.display = 'none';
- }
- this.customQuestionButton = document.createElement('button');
- this.customQuestionButton.textContent = 'Ask';
- this.customQuestionButton.className = 'tooltip-custom-question-button';
- this.customQuestionContainer.appendChild(this.customQuestionInput);
- if (this.attachImageButton) {
- this.customQuestionContainer.appendChild(this.attachImageButton);
- if (this.followUpImageInput) {
- this.customQuestionContainer.appendChild(this.followUpImageInput);
- }
- }
- this.customQuestionContainer.appendChild(this.customQuestionButton);
- this.tooltipElement.appendChild(this.customQuestionContainer);
- if (supportsImages) {
- this.followUpImageContainer = document.createElement('div');
- this.followUpImageContainer.className = 'tooltip-follow-up-image-preview-container';
- this.tooltipElement.insertBefore(this.followUpImageContainer, this.metadataElement);
- }
- this.metadataElement = document.createElement('div');
- this.metadataElement.className = 'tooltip-metadata';
- this.metadataElement.style.display = 'none';
- this.tooltipElement.appendChild(this.metadataElement);
- this.scrollButton = document.createElement('div');
- this.scrollButton.className = 'scroll-to-bottom-button';
- this.scrollButton.innerHTML = '⬇ Scroll to bottom';
- this.scrollButton.style.display = 'none';
- this.tooltipElement.appendChild(this.scrollButton);
- const bottomSpacer = document.createElement('div');
- bottomSpacer.className = 'tooltip-bottom-spacer';
- this.tooltipElement.appendChild(bottomSpacer);
- document.body.appendChild(this.tooltipElement);
- if (isMobileDevice()) {
- this.indicatorElement?.classList.add('mobile-indicator');
- this.tooltipElement?.classList.add('mobile-tooltip');
- }
- this._updateIndicatorUI();
- this._updateTooltipUI();
- }
- _addEventListeners() {
- if (!this.indicatorElement || !this.tooltipElement) return;
- this.indicatorElement.addEventListener('mouseenter', this._handleMouseEnter.bind(this));
- this.indicatorElement.addEventListener('mouseleave', this._handleMouseLeave.bind(this));
- this.indicatorElement.addEventListener('click', this._handleIndicatorClick.bind(this));
- this.tooltipElement.addEventListener('mouseenter', this._handleTooltipMouseEnter.bind(this));
- this.tooltipElement.addEventListener('mouseleave', this._handleTooltipMouseLeave.bind(this));
- this.tooltipElement.addEventListener('scroll', this._handleTooltipScroll.bind(this));
- this.pinButton?.addEventListener('click', this._handlePinClick.bind(this));
- this.copyButton?.addEventListener('click', this._handleCopyClick.bind(this));
- this.tooltipCloseButton?.addEventListener('click', this._handleCloseClick.bind(this));
- this.reasoningToggle?.addEventListener('click', this._handleReasoningToggleClick.bind(this));
- this.scrollButton?.addEventListener('click', this._handleScrollButtonClick.bind(this));
- this.followUpQuestionsElement?.addEventListener('click', this._handleFollowUpQuestionClick.bind(this));
- this.customQuestionButton?.addEventListener('click', this._handleCustomQuestionClick.bind(this));
- this.customQuestionInput?.addEventListener('keydown', (event) => {
- if (event.key === 'Enter') {
- event.preventDefault();
- this._handleCustomQuestionClick();
- }
- });
- if (this.attachImageButton && this.followUpImageInput) {
- this.attachImageButton.addEventListener('click', () => this.followUpImageInput.click());
- this.followUpImageInput.addEventListener('change', this._handleFollowUpImageSelect.bind(this));
- }
- }
- updateDatasetAttributes(currentTweetArticle) {
- const article = currentTweetArticle || this.findCurrentArticleElement();
- if (!article) {
- return;
- }
- article.dataset.ratingStatus = this.status;
- article.dataset.sloppinessScore = this.score !== null ? String(this.score) : '';
- article.dataset.ratingDescription = this.description;
- article.dataset.ratingReasoning = this.reasoning;
- article.dataset.blacklisted = String(this.status === 'blacklisted');
- article.dataset.cachedRating = String(this.status === 'cached');
- }
- _updateIndicatorUI() {
- if (!this.indicatorElement) return;
- const classList = this.indicatorElement.classList;
- classList.remove(
- 'pending-rating', 'rated-rating', 'error-rating',
- 'cached-rating', 'blacklisted-rating', 'streaming-rating'
- );
- let indicatorText = '';
- let indicatorClass = '';
- switch (this.status) {
- case 'pending':
- indicatorClass = 'pending-rating';
- indicatorText = '⏳';
- break;
- case 'streaming':
- indicatorClass = 'streaming-rating';
- indicatorText = (this.score !== null && this.score !== undefined) ? String(this.score) : '🔄';
- break;
- case 'error':
- indicatorClass = 'error-rating';
- indicatorText = '⚠️';
- break;
- case 'cached':
- indicatorClass = 'cached-rating';
- indicatorText = String(this.score);
- break;
- case 'blacklisted':
- indicatorClass = 'blacklisted-rating';
- indicatorText = String(this.score);
- break;
- case 'rated':
- default:
- indicatorClass = 'rated-rating';
- indicatorText = String(this.score);
- break;
- }
- if (indicatorClass) {
- classList.add(indicatorClass);
- }
- this.indicatorElement.textContent = indicatorText;
- }
- _updateTooltipUI() {
- if (!this.tooltipElement || !this.descriptionElement || !this.scoreTextElement || !this.followUpQuestionsTextElement || !this.reasoningTextElement || !this.reasoningDropdown || !this.conversationContainerElement || !this.followUpQuestionsElement || !this.metadataElement) {
- return;
- }
- const wasNearBottom = this.tooltipElement.scrollHeight - this.tooltipElement.scrollTop - this.tooltipElement.clientHeight < (isMobileDevice() ? 40 : 55);
- const previousScrollTop = this.tooltipElement.scrollTop;
- const previousScrollHeight = this.tooltipElement.scrollHeight;
- const fullDescription = this.description || "";
- const analysisMatch = fullDescription.match(/<ANALYSIS>([^<]+)<\/ANALYSIS>/);
- const scoreMatch = fullDescription.match(/<SCORE>([^<]+)<\/SCORE>/);
- const questionsMatch = fullDescription.match(/<FOLLOW_UP_QUESTIONS>([^<]+)<\/FOLLOW_UP_QUESTIONS>/);
- let analysisContent = "";
- let scoreContent = "";
- let questionsContent = "";
- if (analysisMatch && analysisMatch[1] !== undefined) {
- analysisContent = analysisMatch[1].trim();
- } else if (!scoreMatch && !questionsMatch) {
- analysisContent = fullDescription;
- } else {
- analysisContent = "*Waiting for analysis...*";
- }
- if (scoreMatch && scoreMatch[1] !== undefined) {
- scoreContent = scoreMatch[1].trim();
- }
- if (questionsMatch && questionsMatch[1] !== undefined) {
- questionsContent = questionsMatch[1].trim();
- }
- let contentChanged = false;
- const formattedAnalysis = formatTooltipDescription(analysisContent).description;
- if (this.descriptionElement.innerHTML !== formattedAnalysis) {
- this.descriptionElement.innerHTML = formattedAnalysis;
- contentChanged = true;
- }
- if (scoreContent) {
- const formattedScoreText = scoreContent
- .replace(/</g, '<').replace(/>/g, '>') // Basic escaping
- .replace(/SCORE_(\d+)/g, '<span class="score-highlight">SCORE: $1</span>') // Apply highlighting
- .replace(/\n/g, '<br>');
- if (this.scoreTextElement.innerHTML !== formattedScoreText) {
- this.scoreTextElement.innerHTML = formattedScoreText;
- contentChanged = true;
- }
- this.scoreTextElement.style.display = 'block';
- } else {
- if (this.scoreTextElement.style.display !== 'none') {
- this.scoreTextElement.style.display = 'none';
- this.scoreTextElement.innerHTML = '';
- contentChanged = true;
- }
- }
- if (questionsContent) {
- const formattedQuestionsText = questionsContent.replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');
- if (this.followUpQuestionsTextElement.innerHTML !== formattedQuestionsText) {
- this.followUpQuestionsTextElement.innerHTML = formattedQuestionsText;
- }
- } else {
- if (this.followUpQuestionsTextElement.innerHTML !== '') {
- this.followUpQuestionsTextElement.innerHTML = '';
- }
- }
- this.followUpQuestionsTextElement.style.display = 'none';
- const formattedReasoning = formatTooltipDescription("", this.reasoning).reasoning;
- if (this.reasoningTextElement.innerHTML !== formattedReasoning) {
- this.reasoningTextElement.innerHTML = formattedReasoning;
- contentChanged = true;
- }
- const showReasoning = !!formattedReasoning;
- if ((this.reasoningDropdown.style.display === 'none') === showReasoning) {
- this.reasoningDropdown.style.display = showReasoning ? 'block' : 'none';
- contentChanged = true;
- }
- const renderedHistory = this._renderConversationHistory();
- if (this.conversationContainerElement.innerHTML !== renderedHistory) {
- this.conversationContainerElement.innerHTML = renderedHistory;
- this.conversationContainerElement.style.display = this.conversationHistory.length > 0 ? 'block' : 'none';
- contentChanged = true;
- }
- let questionsButtonsChanged = false;
- if (this.followUpQuestionsElement.children.length !== (this.questions?.length || 0)) {
- questionsButtonsChanged = true;
- } else {
- this.questions?.forEach((q, i) => {
- const button = this.followUpQuestionsElement.children[i];
- if (!button || button.dataset.questionText !== q) {
- questionsButtonsChanged = true;
- }
- });
- }
- if (questionsButtonsChanged) {
- this.followUpQuestionsElement.innerHTML = '';
- if (this.questions && this.questions.length > 0) {
- this.questions.forEach((question, index) => {
- const questionButton = document.createElement('button');
- questionButton.className = 'follow-up-question-button';
- questionButton.textContent = `🤔 ${question}`;
- questionButton.dataset.questionIndex = index;
- questionButton.dataset.questionText = question;
- this.followUpQuestionsElement.appendChild(questionButton);
- });
- this.followUpQuestionsElement.style.display = 'block';
- } else {
- this.followUpQuestionsElement.style.display = 'none';
- }
- contentChanged = true;
- }
- let metadataHTML = '';
- let showMetadata = false;
- const hasFullMetadata = this.metadata && Object.keys(this.metadata).length > 1 && this.metadata.model;
- const hasOnlyGenId = this.metadata && this.metadata.generationId && Object.keys(this.metadata).length === 1;
- if (hasFullMetadata) {
- metadataHTML += '<hr class="metadata-separator">';
- metadataHTML += `<div class="metadata-line">Model: ${this.metadata.model}</div>`;
- metadataHTML += `<div class="metadata-line">Tokens: prompt: ${this.metadata.promptTokens} / completion: ${this.metadata.completionTokens}</div>`;
- if (this.metadata.reasoningTokens > 0) {
- metadataHTML += `<div class="metadata-line">Reasoning Tokens: ${this.metadata.reasoningTokens}</div>`;
- }
- metadataHTML += `<div class="metadata-line">Latency: ${this.metadata.latency}</div>`;
- if (this.metadata.mediaInputs > 0) {
- metadataHTML += `<div class="metadata-line">Media: ${this.metadata.mediaInputs}</div>`;
- }
- metadataHTML += `<div class="metadata-line">Price: ${this.metadata.price}</div>`;
- showMetadata = true;
- } else if (hasOnlyGenId) {
- metadataHTML += '<hr class="metadata-separator">';
- metadataHTML += `<div class="metadata-line">Generation ID: ${this.metadata.generationId} (fetching details...)</div>`;
- showMetadata = true;
- }
- if (this.metadataElement.innerHTML !== metadataHTML) {
- this.metadataElement.innerHTML = metadataHTML;
- contentChanged = true;
- }
- if ((this.metadataElement.style.display === 'none') === showMetadata) {
- this.metadataElement.style.display = showMetadata ? 'block' : 'none';
- contentChanged = true;
- }
- const isStreaming = this.status === 'streaming';
- if (this.tooltipElement.classList.contains('streaming-tooltip') !== isStreaming) {
- this.tooltipElement.classList.toggle('streaming-tooltip', isStreaming);
- contentChanged = true;
- }
- if (contentChanged) {
- requestAnimationFrame(() => {
- if (this.autoScroll && (wasNearBottom || !previousScrollHeight)) {
- this._performAutoScroll();
- } else if (!this.autoScroll && previousScrollHeight > 0) {
- const newScrollHeight = this.tooltipElement.scrollHeight;
- const scrollDiff = newScrollHeight - previousScrollHeight;
- this.tooltipElement.scrollTop = previousScrollTop + scrollDiff;
- }
- this._updateScrollButtonVisibility();
- });
- } else {
- this._updateScrollButtonVisibility();
- }
- }
- _renderConversationHistory() {
- if (!this.conversationHistory || this.conversationHistory.length === 0) {
- return '';
- }
- const expandedStates = new Map();
- if (this.conversationContainerElement) {
- this.conversationContainerElement.querySelectorAll('.conversation-reasoning').forEach((dropdown, index) => {
- expandedStates.set(index, dropdown.classList.contains('expanded'));
- });
- }
- let historyHtml = '';
- this.conversationHistory.forEach((turn, index) => {
- const formattedQuestion = turn.question
- .replace(/</g, '<').replace(/>/g, '>');
- let uploadedImageHtml = '';
- if (turn.uploadedImages && turn.uploadedImages.length > 0) {
- uploadedImageHtml = `
- <div class="conversation-image-container">
- ${turn.uploadedImages.map(imageUrl => `
- <img src="${imageUrl}" alt="User uploaded image" class="conversation-uploaded-image">
- `).join('')}
- </div>
- `;
- }
- let formattedAnswer;
- if (turn.answer === 'pending') {
- formattedAnswer = '<em class="pending-answer">Answering...</em>';
- } else {
- formattedAnswer = turn.answer
- .replace(/</g, '<').replace(/>/g, '>') // Escape potential raw HTML first
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" class="ai-generated-link">$1</a>') // Added class
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
- .replace(/`([^`]+)`/g, '<code>$1</code>')
- .replace(/\n/g, '<br>');
- }
- if (index > 0) {
- historyHtml += '<hr class="conversation-separator">';
- }
- let reasoningHtml = '';
- if (turn.reasoning && turn.reasoning.trim() !== '') {
- const formattedReasoning = formatTooltipDescription("", turn.reasoning).reasoning;
- const wasExpanded = expandedStates.get(index);
- const expandedClass = wasExpanded ? ' expanded' : '';
- const arrowChar = wasExpanded ? '▼' : '▶';
- const contentStyle = wasExpanded ? 'style="max-height: 200px; padding: 8px;"' : 'style="max-height: 0; padding: 0 8px;"';
- reasoningHtml = `
- <div class="reasoning-dropdown conversation-reasoning${expandedClass}" data-index="${index}">
- <div class="reasoning-toggle" role="button" tabindex="0" aria-expanded="${wasExpanded ? 'true' : 'false'}">
- <span class="reasoning-arrow">${arrowChar}</span> Show Reasoning Trace
- </div>
- <div class="reasoning-content" ${contentStyle}>
- <p class="reasoning-text">${formattedReasoning}</p>
- </div>
- </div>
- `;
- }
- historyHtml += `
- <div class="conversation-turn">
- <div class="conversation-question"><strong>You:</strong> ${formattedQuestion}</div>
- ${uploadedImageHtml}
- ${reasoningHtml}
- <div class="conversation-answer"><strong>AI:</strong> ${formattedAnswer}</div>
- </div>
- `;
- });
- if (this.conversationContainerElement) {
- this.conversationContainerElement.innerHTML = historyHtml;
- this._attachConversationReasoningListeners();
- }
- return historyHtml;
- }
- _attachConversationReasoningListeners() {
- if (!this.conversationContainerElement) return;
- this.conversationContainerElement.removeEventListener('click', this._handleConversationReasoningToggle);
- this.conversationContainerElement.addEventListener('click', (e) => {
- const toggleButton = e.target.closest('.conversation-reasoning .reasoning-toggle');
- if (!toggleButton) return;
- e.stopPropagation();
- const dropdown = toggleButton.closest('.reasoning-dropdown');
- const content = dropdown?.querySelector('.reasoning-content');
- const arrow = dropdown?.querySelector('.reasoning-arrow');
- if (!dropdown || !content || !arrow) return;
- const isExpanded = dropdown.classList.toggle('expanded');
- arrow.textContent = isExpanded ? '▼' : '▶';
- toggleButton.setAttribute('aria-expanded', isExpanded);
- content.style.maxHeight = isExpanded ? '200px' : '0';
- content.style.padding = isExpanded ? '8px' : '0 8px';
- });
- }
- _performAutoScroll() {
- if (!this.tooltipElement || !this.autoScroll) return;
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
- if (this.tooltipElement && this.autoScroll && this.isVisible) {
- const targetScroll = this.tooltipElement.scrollHeight;
- this.tooltipElement.scrollTo({
- top: targetScroll,
- behavior: 'instant'
- });
- setTimeout(() => {
- if (this.tooltipElement && this.autoScroll && this.isVisible) {
- const isNearBottom = this.tooltipElement.scrollHeight - this.tooltipElement.scrollTop - this.tooltipElement.clientHeight < 5;
- if (!isNearBottom) {
- this.tooltipElement.scrollTop = this.tooltipElement.scrollHeight;
- }
- }
- }, 50);
- }
- });
- });
- }
- _setPosition() {
- if (!this.isVisible || !this.indicatorElement || !this.tooltipElement) return;
- const indicatorRect = this.indicatorElement.getBoundingClientRect();
- const tooltip = this.tooltipElement;
- const margin = 10;
- const isMobile = isMobileDevice();
- const viewportHeight = window.innerHeight;
- const viewportWidth = window.innerWidth;
- const safeAreaHeight = viewportHeight - margin;
- const safeAreaWidth = viewportWidth - margin;
- tooltip.style.maxHeight = '';
- tooltip.style.overflowY = '';
- tooltip.style.visibility = 'hidden';
- tooltip.style.display = 'block';
- const computedStyle = window.getComputedStyle(tooltip);
- const tooltipWidth = parseFloat(computedStyle.width);
- let tooltipHeight = parseFloat(computedStyle.height);
- let left, top;
- let finalMaxHeight = '';
- let finalOverflowY = '';
- if (isMobile) {
- left = Math.max(margin, (viewportWidth - tooltipWidth) / 2);
- if (left + tooltipWidth > safeAreaWidth) {
- left = safeAreaWidth - tooltipWidth;
- }
- const maxTooltipHeight = viewportHeight * 0.8;
- if (tooltipHeight > maxTooltipHeight) {
- finalMaxHeight = `${maxTooltipHeight}px`;
- finalOverflowY = 'scroll';
- tooltipHeight = maxTooltipHeight;
- }
- top = Math.max(margin, (viewportHeight - tooltipHeight) / 2);
- if (top + tooltipHeight > safeAreaHeight) {
- top = safeAreaHeight - tooltipHeight;
- }
- } else {
- left = indicatorRect.right + margin;
- top = indicatorRect.top + (indicatorRect.height / 2) - (tooltipHeight / 2);
- if (left + tooltipWidth > safeAreaWidth) {
- left = indicatorRect.left - tooltipWidth - margin;
- if (left < margin) {
- left = Math.max(margin, (viewportWidth - tooltipWidth) / 2);
- if (indicatorRect.bottom + tooltipHeight + margin <= safeAreaHeight) {
- top = indicatorRect.bottom + margin;
- }
- else if (indicatorRect.top - tooltipHeight - margin >= margin) {
- top = indicatorRect.top - tooltipHeight - margin;
- }
- else {
- top = margin;
- finalMaxHeight = `${safeAreaHeight - margin}px`;
- finalOverflowY = 'scroll';
- tooltipHeight = safeAreaHeight - margin;
- }
- }
- }
- if (top < margin) {
- top = margin;
- }
- if (top + tooltipHeight > safeAreaHeight) {
- if (tooltipHeight > safeAreaHeight - margin) {
- top = margin;
- finalMaxHeight = `${safeAreaHeight - margin}px`;
- finalOverflowY = 'scroll';
- } else {
- top = safeAreaHeight - tooltipHeight;
- }
- }
- }
- tooltip.style.position = 'fixed';
- tooltip.style.left = `${left}px`;
- tooltip.style.top = `${top}px`;
- tooltip.style.zIndex = '99999999';
- tooltip.style.maxHeight = finalMaxHeight;
- tooltip.style.overflowY = finalOverflowY;
- if (finalOverflowY === 'scroll') {
- tooltip.style.webkitOverflowScrolling = 'touch';
- }
- tooltip.style.visibility = 'visible';
- }
- _updateScrollButtonVisibility() {
- if (!this.tooltipElement || !this.scrollButton) return;
- const isStreaming = this.status === 'streaming';
- if (!isStreaming) {
- this.scrollButton.style.display = 'none';
- return;
- }
- const isNearBottom = this.tooltipElement.scrollHeight - this.tooltipElement.scrollTop - this.tooltipElement.clientHeight < (isMobileDevice() ? 40 : 55);
- this.scrollButton.style.display = isNearBottom ? 'none' : 'block';
- }
- _handleMouseEnter(event) {
- if (isMobileDevice()) return;
- this.show();
- }
- _handleMouseLeave(event) {
- if (isMobileDevice()) return;
- setTimeout(() => {
- if (this.tooltipElement && !this.tooltipElement.matches(':hover') &&
- this.indicatorElement && !this.indicatorElement.matches(':hover')) {
- this.hide();
- }
- }, 100);
- }
- _handleIndicatorClick(event) {
- event.stopPropagation();
- event.preventDefault();
- this.toggle();
- }
- _handleTooltipMouseEnter() {
- if (!this.isPinned) {
- this.show();
- }
- }
- _handleTooltipMouseLeave() {
- if (!this.isPinned) {
- this.hide();
- }
- }
- _handleTooltipClick(event) {
- }
- _handleTooltipScroll() {
- if (!this.tooltipElement) return;
- const isNearBottom = this.tooltipElement.scrollHeight - this.tooltipElement.scrollTop - this.tooltipElement.clientHeight < (isMobileDevice() ? 40 : 55);
- if (!isNearBottom) {
- if (this.autoScroll) {
- this.autoScroll = false;
- this.tooltipElement.dataset.autoScroll = 'false';
- this.userInitiatedScroll = true;
- }
- } else {
- if (this.userInitiatedScroll) {
- this.autoScroll = true;
- this.tooltipElement.dataset.autoScroll = 'true';
- this.userInitiatedScroll = false;
- }
- }
- this._updateScrollButtonVisibility();
- }
- _handlePinClick(e) {
- e.stopPropagation();
- if (this.isPinned) {
- this.unpin();
- } else {
- this.pin();
- }
- }
- _handleCopyClick(e) {
- e.stopPropagation();
- if (!this.descriptionElement || !this.reasoningTextElement || !this.copyButton) return;
- let textToCopy = this.descriptionElement.textContent || '';
- const reasoningContent = this.reasoningTextElement.textContent || '';
- if (reasoningContent) {
- textToCopy += '\n\nReasoning:\n' + reasoningContent;
- }
- navigator.clipboard.writeText(textToCopy).then(() => {
- const originalText = this.copyButton.innerHTML;
- this.copyButton.innerHTML = '✓';
- this.copyButton.disabled = true;
- setTimeout(() => {
- this.copyButton.innerHTML = originalText;
- this.copyButton.disabled = false;
- }, 1500);
- }).catch(err => {
- });
- }
- _handleReasoningToggleClick(e) {
- e.stopPropagation();
- if (!this.reasoningDropdown || !this.reasoningContent || !this.reasoningArrow) return;
- const isExpanded = this.reasoningDropdown.classList.toggle('expanded');
- this.reasoningArrow.textContent = isExpanded ? '▼' : '▶';
- if (isExpanded) {
- this.reasoningContent.style.maxHeight = '300px';
- this.reasoningContent.style.padding = '10px';
- } else {
- this.reasoningContent.style.maxHeight = '0';
- this.reasoningContent.style.padding = '0 10px';
- }
- }
- _handleScrollButtonClick(e) {
- e.stopPropagation();
- if (!this.tooltipElement) return;
- this.autoScroll = true;
- this.tooltipElement.dataset.autoScroll = 'true';
- this._performAutoScroll();
- this._updateScrollButtonVisibility();
- }
- _handleFollowUpQuestionClick(event) {
- const isMockEvent = event.target && event.target.dataset && event.target.dataset.questionText && typeof event.target.closest !== 'function';
- const button = isMockEvent ? event.target : event.target.closest('.follow-up-question-button');
- if (!button) return;
- event.stopPropagation();
- const questionText = button.dataset.questionText;
- const apiKey = browserGet('openrouter-api-key', '');
- if (!isMockEvent) {
- button.disabled = true;
- button.textContent = `🤔 Asking: ${questionText}...`;
- this.followUpQuestionsElement.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = true);
- } else {
- if (this.customQuestionInput) this.customQuestionInput.disabled = true;
- if (this.customQuestionButton) {
- this.customQuestionButton.disabled = true;
- this.customQuestionButton.textContent = 'Asking...';
- }
- }
- this.conversationHistory.push({
- question: questionText,
- answer: 'pending',
- uploadedImages: [...this.uploadedImageDataUrls], // Store a copy of the image URLs array
- reasoning: '' // Initialize reasoning for this turn
- });
- this._updateTooltipUI();
- this.questions = [];
- this._updateTooltipUI();
- const userMessageContent = [{ type: "text", text: questionText }];
- if (this.uploadedImageDataUrls && this.uploadedImageDataUrls.length > 0) {
- this.uploadedImageDataUrls.forEach(url => {
- userMessageContent.push({ type: "image_url", image_url: { "url": url } });
- });
- }
- const userApiMessage = { role: "user", content: userMessageContent };
- const historyForApiCall = [...this.qaConversationHistory, userApiMessage];
- if (!apiKey) {
- showStatus('API key missing. Cannot answer question.', 'error');
- this._updateConversationHistory(questionText, "Error: API Key missing.", "");
- if (!isMockEvent) {
- button.disabled = false;
- this.followUpQuestionsElement.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = false);
- }
- if (this.customQuestionInput) this.customQuestionInput.disabled = false;
- if (this.customQuestionButton) {
- this.customQuestionButton.disabled = false;
- this.customQuestionButton.textContent = 'Ask';
- }
- this._clearFollowUpImage();
- return;
- }
- if (!questionText) {
- this._updateConversationHistory(questionText || "Error: Empty Question", "Error: Could not identify question.", "");
- if (!isMockEvent) {
- button.disabled = false;
- this.followUpQuestionsElement.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = false);
- }
- if (this.customQuestionInput) this.customQuestionInput.disabled = false;
- if (this.customQuestionButton) {
- this.customQuestionButton.disabled = false;
- this.customQuestionButton.textContent = 'Ask';
- }
- this._clearFollowUpImage();
- return;
- }
- const currentArticle = this.findCurrentArticleElement();
- try {
- answerFollowUpQuestion(this.tweetId, historyForApiCall, apiKey, currentArticle, this);
- } finally {
- setTimeout(() => {
- if (this.followUpQuestionsElement) {
- this.followUpQuestionsElement.querySelectorAll('.follow-up-question-button').forEach(btn => {
- btn.disabled = false;
- });
- }
- if (this.customQuestionInput) this.customQuestionInput.disabled = false;
- if (this.customQuestionButton) {
- this.customQuestionButton.disabled = false;
- this.customQuestionButton.textContent = 'Ask';
- }
- this._clearFollowUpImage();
- }, 100);
- }
- }
- _handleCustomQuestionClick() {
- if (!this.customQuestionInput || !this.customQuestionButton) return;
- const questionText = this.customQuestionInput.value.trim();
- if (!questionText) {
- showStatus("Please enter a question.", "warning");
- this.customQuestionInput.focus();
- return;
- }
- const mockButton = {
- dataset: { questionText: questionText },
- disabled: false,
- textContent: ''
- };
- this.followUpQuestionsElement?.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = true);
- this._handleFollowUpQuestionClick({ target: mockButton, stopPropagation: () => {} });
- if (this.customQuestionInput) {
- this.customQuestionInput.value = '';
- }
- }
- _handleFollowUpImageSelect(event) {
- const files = event.target.files;
- if (!files || files.length === 0) return;
- if (this.followUpImageContainer && files.length > 0) {
- this.followUpImageContainer.style.display = 'flex';
- }
- Array.from(files).forEach(file => {
- if (file && file.type.startsWith('image/')) {
- resizeImage(file, 512) // Resize to max 512px
- .then(resizedDataUrl => {
- this.uploadedImageDataUrls.push(resizedDataUrl);
- this._addPreviewToContainer(resizedDataUrl);
- })
- .catch(error => {
- showStatus(`Could not process image ${file.name}: ${error.message}`, "error");
- });
- } else if (file) {
- showStatus(`Skipping non-image file: ${file.name}`, "warning");
- }
- });
- event.target.value = null;
- }
- _addPreviewToContainer(imageDataUrl) {
- if (!this.followUpImageContainer) return;
- const previewItem = document.createElement('div');
- previewItem.className = 'follow-up-image-preview-item';
- previewItem.dataset.imageDataUrl = imageDataUrl;
- const img = document.createElement('img');
- img.src = imageDataUrl;
- img.className = 'follow-up-image-preview-thumbnail';
- const removeBtn = document.createElement('button');
- removeBtn.textContent = '×';
- removeBtn.className = 'follow-up-image-remove-btn';
- removeBtn.title = 'Remove this image';
- removeBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- this._removeSpecificUploadedImage(imageDataUrl);
- });
- previewItem.appendChild(img);
- previewItem.appendChild(removeBtn);
- this.followUpImageContainer.appendChild(previewItem);
- }
- _removeSpecificUploadedImage(imageDataUrl) {
- this.uploadedImageDataUrls = this.uploadedImageDataUrls.filter(url => url !== imageDataUrl);
- if (this.followUpImageContainer) {
- const previewItemToRemove = this.followUpImageContainer.querySelector(`div.follow-up-image-preview-item[data-image-data-url="${CSS.escape(imageDataUrl)}"]`);
- if (previewItemToRemove) {
- previewItemToRemove.remove();
- }
- if (this.uploadedImageDataUrls.length === 0) {
- this.followUpImageContainer.style.display = 'none';
- }
- }
- }
- _clearFollowUpImage() {
- this.uploadedImageDataUrls = [];
- if (this.followUpImageContainer) {
- this.followUpImageContainer.innerHTML = '';
- this.followUpImageContainer.style.display = 'none';
- }
- if (this.followUpImageInput) {
- this.followUpImageInput.value = null;
- }
- }
- _updateConversationHistory(question, answer, reasoning = '') {
- const entryIndex = this.conversationHistory.findIndex(turn => turn.question === question && turn.answer === 'pending');
- if (entryIndex !== -1) {
- this.conversationHistory[entryIndex].answer = answer;
- this.conversationHistory[entryIndex].reasoning = reasoning;
- this._updateTooltipUI();
- } else {
- }
- }
- _renderStreamingAnswer(streamingText, reasoningText = '') {
- if (!this.conversationContainerElement) return;
- const conversationTurns = this.conversationContainerElement.querySelectorAll('.conversation-turn');
- const lastTurnElement = conversationTurns.length > 0 ? conversationTurns[conversationTurns.length - 1] : null;
- if (!lastTurnElement) {
- return;
- }
- const lastHistoryEntry = this.conversationHistory.length > 0 ? this.conversationHistory[this.conversationHistory.length -1] : null;
- if (!(lastHistoryEntry && lastHistoryEntry.answer === 'pending')) {
- return;
- }
- let reasoningDropdown = lastTurnElement.querySelector('.reasoning-dropdown');
- const hasReasoning = reasoningText && reasoningText.trim() !== '';
- if (hasReasoning && !reasoningDropdown) {
- reasoningDropdown = document.createElement('div');
- reasoningDropdown.className = 'reasoning-dropdown conversation-reasoning';
- const reasoningToggle = document.createElement('div');
- reasoningToggle.className = 'reasoning-toggle';
- const reasoningArrow = document.createElement('span');
- reasoningArrow.className = 'reasoning-arrow';
- reasoningArrow.textContent = '▶';
- reasoningToggle.appendChild(reasoningArrow);
- reasoningToggle.appendChild(document.createTextNode(' Show Reasoning Trace'));
- const reasoningContent = document.createElement('div');
- reasoningContent.className = 'reasoning-content';
- const reasoningTextElement = document.createElement('p');
- reasoningTextElement.className = 'reasoning-text';
- reasoningContent.appendChild(reasoningTextElement);
- reasoningDropdown.appendChild(reasoningToggle);
- reasoningDropdown.appendChild(reasoningContent);
- const answerElement = lastTurnElement.querySelector('.conversation-answer');
- if (answerElement) {
- lastTurnElement.insertBefore(reasoningDropdown, answerElement);
- } else {
- lastTurnElement.appendChild(reasoningDropdown);
- }
- reasoningToggle.addEventListener('click', (e) => {
- e.stopPropagation();
- const dropdown = e.target.closest('.reasoning-dropdown');
- const content = dropdown?.querySelector('.reasoning-content');
- const arrow = dropdown?.querySelector('.reasoning-arrow');
- if (!dropdown || !content || !arrow) return;
- const isExpanded = dropdown.classList.toggle('expanded');
- arrow.textContent = isExpanded ? '▼' : '▶';
- content.style.maxHeight = isExpanded ? '200px' : '0';
- content.style.padding = isExpanded ? '8px' : '0 8px';
- });
- }
- if (reasoningDropdown && hasReasoning) {
- const reasoningTextElement = reasoningDropdown.querySelector('.reasoning-text');
- if (reasoningTextElement) {
- const formattedReasoning = formatTooltipDescription("", reasoningText).reasoning;
- if (reasoningTextElement.innerHTML !== formattedReasoning) {
- reasoningTextElement.innerHTML = formattedReasoning;
- }
- }
- reasoningDropdown.style.display = 'block';
- } else if (reasoningDropdown) {
- reasoningDropdown.style.display = 'none';
- }
- const lastAnswerElement = lastTurnElement.querySelector('.conversation-answer');
- if (lastAnswerElement) {
- const formattedStreamingAnswer = streamingText
- .replace(/</g, '<').replace(/>/g, '>') // Escape potential raw HTML first
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" class="ai-generated-link">$1</a>')
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
- .replace(/`([^`]+)`/g, '<code>$1</code>')
- .replace(/\n/g, '<br>');
- lastAnswerElement.innerHTML = `<strong>AI:</strong> ${formattedStreamingAnswer}<em class="pending-cursor">|</em>`;
- } else {
- }
- if (this.autoScroll) {
- this._performAutoScroll();
- }
- }
- update({ status, score = null, description = '', reasoning = '', metadata = null, questions = undefined }) {
- const statusChanged = status !== undefined && this.status !== status;
- const scoreChanged = score !== null && this.score !== score;
- const descriptionChanged = description !== '' && this.description !== description;
- const reasoningChanged = reasoning !== '' && this.reasoning !== reasoning;
- const metadataChanged = metadata !== null && JSON.stringify(this.metadata) !== JSON.stringify(metadata);
- const questionsChanged = questions !== undefined && JSON.stringify(this.questions) !== JSON.stringify(questions);
- if (!statusChanged && !scoreChanged && !descriptionChanged && !reasoningChanged && !metadataChanged && !questionsChanged) {
- return;
- }
- if (statusChanged) this.status = status;
- if (scoreChanged || statusChanged) {
- this.score = (this.status === 'pending' || this.status === 'error') ? score : // Allow score display for error state if provided
- (this.status === 'streaming' && score === null) ? this.score : // Keep existing score during streaming if new one is null
- score;
- }
- if (descriptionChanged) this.description = description;
- if (reasoningChanged) this.reasoning = reasoning;
- if (metadataChanged) this.metadata = metadata;
- if (questionsChanged) this.questions = questions;
- if (statusChanged) {
- const shouldAutoScroll = (this.status === 'pending' || this.status === 'streaming');
- if (this.autoScroll !== shouldAutoScroll) {
- this.autoScroll = shouldAutoScroll;
- if (this.tooltipElement) {
- this.tooltipElement.dataset.autoScroll = this.autoScroll ? 'true' : 'false';
- }
- }
- }
- if (statusChanged || scoreChanged) {
- this._updateIndicatorUI();
- }
- if (descriptionChanged || reasoningChanged || statusChanged || metadataChanged || questionsChanged) {
- this._updateTooltipUI();
- } else {
- this._updateScrollButtonVisibility();
- }
- this.updateDatasetAttributes();
- }
- show() {
- if (!this.tooltipElement) return;
- this.isVisible = true;
- this.tooltipElement.style.display = 'block';
- this._setPosition();
- if (this.autoScroll && (this.status === 'streaming' || this.status === 'pending')) {
- this._performAutoScroll();
- }
- this._updateScrollButtonVisibility();
- }
- hide() {
- if (!this.isPinned && this.tooltipElement) {
- this.isVisible = false;
- this.tooltipElement.style.display = 'none';
- } else if (this.isPinned) {
- }
- }
- toggle() {
- if (this.isVisible && !this.isPinned) {
- this.hide();
- } else {
- this.show();
- }
- }
- pin() {
- if (!this.tooltipElement || !this.pinButton) return;
- this.isPinned = true;
- this.tooltipElement.classList.add('pinned');
- this.pinButton.innerHTML = '📍';
- this.pinButton.title = 'Unpin tooltip';
- }
- unpin() {
- if (!this.tooltipElement || !this.pinButton) return;
- this.isPinned = false;
- this.tooltipElement.classList.remove('pinned');
- this.pinButton.innerHTML = '📌';
- this.pinButton.title = 'Pin tooltip (prevents auto-closing)';
- setTimeout(() => {
- if (this.tooltipElement && !this.tooltipElement.matches(':hover') &&
- this.indicatorElement && !this.indicatorElement.matches(':hover')) {
- this.hide();
- }
- }, 0);
- }
- _handleCloseClick(e) {
- e.stopPropagation();
- this.hide();
- }
- destroy() {
- if (window.activeStreamingRequests && window.activeStreamingRequests[this.tweetId]) {
- window.activeStreamingRequests[this.tweetId].abort();
- delete window.activeStreamingRequests[this.tweetId];
- }
- this.indicatorElement?.removeEventListener('mouseenter', this._handleMouseEnter);
- this.indicatorElement?.removeEventListener('mouseleave', this._handleMouseLeave);
- this.indicatorElement?.removeEventListener('click', this._handleIndicatorClick);
- this.tooltipElement?.removeEventListener('mouseenter', this._handleTooltipMouseEnter);
- this.tooltipElement?.removeEventListener('mouseleave', this._handleTooltipMouseLeave);
- this.tooltipElement?.removeEventListener('scroll', this._handleTooltipScroll);
- this.pinButton?.removeEventListener('click', this._handlePinClick);
- this.copyButton?.removeEventListener('click', this._handleCopyClick);
- this.tooltipCloseButton?.removeEventListener('click', this._handleCloseClick);
- this.reasoningToggle?.removeEventListener('click', this._handleReasoningToggleClick);
- this.scrollButton?.removeEventListener('click', this._handleScrollButtonClick);
- this.followUpQuestionsElement?.removeEventListener('click', this._handleFollowUpQuestionClick);
- this.customQuestionButton?.removeEventListener('click', this._handleCustomQuestionClick);
- this.customQuestionInput?.removeEventListener('keydown', (event) => {
- if (event.key === 'Enter') {
- event.preventDefault();
- this._handleCustomQuestionClick();
- }
- });
- this.indicatorElement?.remove();
- this.tooltipElement?.remove();
- ScoreIndicatorRegistry.remove(this.tweetId);
- const currentArticle = this.findCurrentArticleElement();
- if (currentArticle) {
- delete currentArticle.dataset.hasScoreIndicator;
- }
- this.tweetArticle = null;
- this.indicatorElement = null;
- this.tooltipElement = null;
- this.pinButton = null;
- this.copyButton = null;
- this.tooltipCloseButton = null;
- this.reasoningToggle = null;
- this.scrollButton = null;
- this.conversationContainerElement = null;
- this.followUpQuestionsElement = null;
- this.customQuestionContainer = null;
- this.customQuestionInput = null;
- this.customQuestionButton = null;
- this.followUpImageContainer = null;
- this.followUpImageInput = null;
- this.followUpImagePreview = null;
- this.followUpRemoveImageButton = null;
- this.attachImageButton = null;
- this.uploadedImageDataUrls = [];
- }
- ensureIndicatorAttached() {
- if (!this.indicatorElement) return;
- const currentArticle = this.findCurrentArticleElement();
- if (!currentArticle) {
- return;
- }
- if (this.indicatorElement.parentElement !== currentArticle) {
- const currentPosition = window.getComputedStyle(currentArticle).position;
- if (currentPosition !== 'relative' && currentPosition !== 'absolute' && currentPosition !== 'fixed' && currentPosition !== 'sticky') {
- currentArticle.style.position = 'relative';
- }
- currentArticle.appendChild(this.indicatorElement);
- }
- this.updateDatasetAttributes(currentArticle);
- }
- findCurrentArticleElement() {
- const timeline = document.querySelector('main') || document.querySelector('div[data-testid="primaryColumn"]');
- if (!timeline) return null;
- const linkSelector = `a[href*="/status/${this.tweetId}"]`;
- const linkElement = timeline.querySelector(linkSelector);
- const article = linkElement?.closest('article[data-testid="tweet"]');
- if (article) {
- if (getTweetID(article) === this.tweetId) {
- return article;
- }
- }
- const articles = timeline.querySelectorAll('article[data-testid="tweet"]');
- for (const art of articles) {
- if (getTweetID(art) === this.tweetId) {
- return art;
- }
- }
- return null;
- }
- updateInitialReviewAndBuildHistory({ fullContext, mediaUrls, apiResponseContent, reviewSystemPrompt, followUpSystemPrompt }) {
- const analysisMatch = apiResponseContent.match(/<ANALYSIS>([\s\S]*?)<\/ANALYSIS>/);
- const scoreMatch = apiResponseContent.match(/<SCORE>\s*SCORE_(\d+)\s*<\/SCORE>/);
- const initialQuestions = extractFollowUpQuestions(apiResponseContent);
- this.score = scoreMatch ? parseInt(scoreMatch[1], 10) : null;
- this.description = analysisMatch ? analysisMatch[1].trim() : apiResponseContent;
- this.questions = initialQuestions;
- this.status = this.score !== null ? 'rated' : 'error';
- const userMessageContent = [{ type: "text", text: fullContext }];
- mediaUrls.forEach(url => {
- userMessageContent.push({ type: "image_url", image_url: { "url": url } });
- });
- this.qaConversationHistory = [
- { role: "system", content: [{ type: "text", text: reviewSystemPrompt }] },
- { role: "user", content: userMessageContent },
- { role: "assistant", content: [{ type: "text", text: apiResponseContent }] },
- { role: "system", content: [{ type: "text", text: followUpSystemPrompt }] }
- ];
- this._updateIndicatorUI();
- this._updateTooltipUI();
- this.updateDatasetAttributes();
- }
- updateAfterFollowUp({ assistantResponseContent, updatedQaHistory }) {
- this.qaConversationHistory = updatedQaHistory;
- const answerMatch = assistantResponseContent.match(/<ANSWER>([\s\S]*?)<\/ANSWER>/);
- const newFollowUpQuestions = extractFollowUpQuestions(assistantResponseContent);
- const answerText = answerMatch ? answerMatch[1].trim() : assistantResponseContent;
- this.questions = newFollowUpQuestions;
- if (this.conversationHistory.length > 0) {
- const lastTurn = this.conversationHistory[this.conversationHistory.length - 1];
- if (lastTurn.answer === 'pending') {
- lastTurn.answer = answerText;
- }
- }
- this._updateTooltipUI();
- this.updateDatasetAttributes();
- }
- rehydrateFromCache(cachedData) {
- this.score = cachedData.score;
- this.description = cachedData.description;
- this.reasoning = cachedData.reasoning;
- this.questions = cachedData.questions || [];
- this.status = cachedData.status || (cachedData.score !== null ? (cachedData.fromStorage ? 'cached' : 'rated') : 'error');
- this.metadata = cachedData.metadata || null;
- this.qaConversationHistory = cachedData.qaConversationHistory || [];
- this.isPinned = cachedData.isPinned || false;
- this.conversationHistory = [];
- if (this.qaConversationHistory.length > 0) {
- let currentQuestion = null;
- let currentUploadedImages = [];
- let startIndex = 0;
- for(let i=0; i < this.qaConversationHistory.length; i++) {
- if (this.qaConversationHistory[i].role === 'system' && this.qaConversationHistory[i].content[0].text.includes('FOLLOW_UP_SYSTEM_PROMPT')) {
- startIndex = i + 1;
- break;
- }
- if (i === 3 && this.qaConversationHistory[i].role === 'system') {
- startIndex = i + 1;
- }
- }
- for (let i = startIndex; i < this.qaConversationHistory.length; i++) {
- const message = this.qaConversationHistory[i];
- if (message.role === 'user') {
- const textContent = message.content.find(c => c.type === 'text');
- currentQuestion = textContent ? textContent.text : "[Question not found]";
- currentUploadedImages = message.content
- .filter(c => c.type === 'image_url' && c.image_url && c.image_url.url.startsWith('data:image'))
- .map(c => c.image_url.url);
- } else if (message.role === 'assistant' && currentQuestion) {
- const assistantTextContent = message.content.find(c => c.type === 'text');
- const assistantAnswer = assistantTextContent ? assistantTextContent.text : "[Answer not found]";
- const answerMatch = assistantAnswer.match(/<ANSWER>([\s\S]*?)<\/ANSWER>/);
- const uiAnswer = answerMatch ? answerMatch[1].trim() : assistantAnswer;
- this.conversationHistory.push({
- question: currentQuestion,
- answer: uiAnswer,
- uploadedImages: currentUploadedImages,
- reasoning: '' // Reasoning extraction from assistant's full response for UI needs more logic
- });
- currentQuestion = null;
- currentUploadedImages = [];
- }
- }
- }
- if (this.isPinned) {
- this.pinButton.innerHTML = '📍';
- this.tooltipElement?.classList.add('pinned');
- } else {
- this.pinButton.innerHTML = '📌';
- this.tooltipElement?.classList.remove('pinned');
- }
- this._updateIndicatorUI();
- this._updateTooltipUI();
- this.updateDatasetAttributes();
- }
- }
- const ScoreIndicatorRegistry = {
- managers: new Map(),
- get(tweetId, tweetArticle = null) {
- if (!tweetId) {
- return null;
- }
- if (this.managers.has(tweetId)) {
- const existingManager = this.managers.get(tweetId);
- if (tweetArticle && existingManager.tweetArticle !== tweetArticle) {
- }
- return existingManager;
- } else if (tweetArticle) {
- try {
- const existingIndicator = tweetArticle.querySelector(`.score-indicator[data-tweet-id="${tweetId}"]`);
- const existingTooltip = document.querySelector(`.score-description[data-tweet-id="${tweetId}"]`);
- if (existingIndicator || existingTooltip) {
- existingIndicator?.remove();
- existingTooltip?.remove();
- }
- return new ScoreIndicator(tweetArticle);
- } catch (e) {
- return null;
- }
- }
- return null;
- },
- add(tweetId, instance) {
- if (this.managers.has(tweetId)) {
- }
- this.managers.set(tweetId, instance);
- },
- remove(tweetId) {
- if (this.managers.has(tweetId)) {
- this.managers.delete(tweetId);
- }
- },
- cleanupOrphaned() {
- let removedCount = 0;
- const observedTimeline = document.querySelector('main') || document.querySelector('div[data-testid="primaryColumn"]');
- if (!observedTimeline) return;
- const visibleTweetIds = new Set();
- observedTimeline.querySelectorAll('article[data-testid="tweet"]').forEach(article => {
- const id = getTweetID(article);
- if (id) visibleTweetIds.add(id);
- });
- for (const [tweetId, manager] of this.managers.entries()) {
- const isConnected = manager.indicatorElement?.isConnected;
- const isVisible = visibleTweetIds.has(tweetId);
- if (!isConnected || !isVisible) {
- manager.destroy();
- removedCount++;
- }
- }
- },
- destroyAll() {
- [...this.managers.values()].forEach(manager => manager.destroy());
- this.managers.clear();
- }
- };
- function formatTooltipDescription(description = "", reasoning = "") {
- let formattedDescription = description === "*Waiting for analysis...*" ? description :
- (description || "*waiting for content...*")
- .replace(/</g, '<').replace(/>/g, '>') // Escape HTML tags first
- .replace(/^# (.*$)/gm, '<h1>$1</h1>')
- .replace(/^## (.*$)/gm, '<h2>$1</h2>')
- .replace(/^### (.*$)/gm, '<h3>$1</h3>')
- .replace(/^#### (.*$)/gm, '<h4>$1</h4>')
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // Bold
- .replace(/\*(.*?)\*/g, '<em>$1</em>') // Italic
- .replace(/`([^`]+)`/g, '<code>$1</code>') // Inline code
- .replace(/SCORE_(\d+)/g, '<span class="score-highlight">SCORE: $1</span>') // Score highlight class
- .replace(/\n\n/g, '<br><br>') // Paragraph breaks
- .replace(/\n/g, '<br>');
- let formattedReasoning = '';
- if (reasoning && reasoning.trim()) {
- formattedReasoning = reasoning
- .replace(/</g, '<').replace(/>/g, '>')
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
- .replace(/`([^`]+)`/g, '<code>$1</code>')
- .replace(/\n\n/g, '<br><br>')
- .replace(/\n/g, '<br>');
- }
- return { description: formattedDescription, reasoning: formattedReasoning };
- }
- // ----- ui/ui.js -----
- function toggleElementVisibility(element, toggleButton, openText, closedText) {
- if (!element || !toggleButton) return;
- const isCurrentlyHidden = element.classList.contains('hidden');
- toggleButton.innerHTML = isCurrentlyHidden ? openText : closedText;
- if (isCurrentlyHidden) {
- element.style.display = 'flex';
- element.offsetHeight;
- element.classList.remove('hidden');
- } else {
- element.classList.add('hidden');
- setTimeout(() => {
- if (element.classList.contains('hidden')) {
- element.style.display = 'none';
- }
- }, 500);
- }
- if (element.id === 'tweet-filter-container') {
- const filterToggle = document.getElementById('filter-toggle');
- if (filterToggle) {
- if (!isCurrentlyHidden) {
- setTimeout(() => {
- filterToggle.style.display = 'block';
- }, 500);
- } else {
- filterToggle.style.display = 'none';
- }
- }
- }
- }
- function injectUI() {
- let menuHTML;
- if (MENU) {
- menuHTML = MENU;
- } else {
- menuHTML = browserGet('menuHTML');
- }
- if (!menuHTML) {
- showStatus('Error: Could not load UI components.');
- return null;
- }
- const containerId = 'tweetfilter-root-container';
- let uiContainer = document.getElementById(containerId);
- if (uiContainer) {
- return uiContainer;
- }
- uiContainer = document.createElement('div');
- uiContainer.id = containerId;
- uiContainer.innerHTML = menuHTML;
- document.body.appendChild(uiContainer);
- const versionInfo = uiContainer.querySelector('#version-info');
- if (versionInfo) {
- versionInfo.textContent = `Twitter De-Sloppifier v${VERSION}`;
- }
- return uiContainer;
- }
- function initializeEventListeners(uiContainer) {
- if (!uiContainer) {
- return;
- }
- const settingsContainer = uiContainer.querySelector('#settings-container');
- const filterContainer = uiContainer.querySelector('#tweet-filter-container');
- const settingsToggleBtn = uiContainer.querySelector('#settings-toggle');
- const filterToggleBtn = uiContainer.querySelector('#filter-toggle');
- uiContainer.addEventListener('click', (event) => {
- const target = event.target;
- const actionElement = target.closest('[data-action]');
- const action = actionElement?.dataset.action;
- const setting = target.dataset.setting;
- const paramName = target.closest('.parameter-row')?.dataset.paramName;
- const tab = target.dataset.tab;
- const toggleTargetId = target.closest('[data-toggle]')?.dataset.toggle;
- if (action) {
- switch (action) {
- case 'close-filter':
- toggleElementVisibility(filterContainer, filterToggleBtn, 'Filter Slider', 'Filter Slider');
- break;
- case 'toggle-settings':
- case 'close-settings':
- toggleElementVisibility(settingsContainer, settingsToggleBtn, '<span style="font-size: 14px;">✕</span> Close', '<span style="font-size: 14px;">⚙️</span> Settings');
- break;
- case 'save-api-key':
- saveApiKey();
- break;
- case 'clear-cache':
- clearTweetRatingsAndRefreshUI();
- break;
- case 'reset-settings':
- resetSettings(isMobileDevice());
- break;
- case 'save-instructions':
- saveInstructions();
- break;
- case 'add-handle':
- addHandleFromInput();
- break;
- case 'clear-instructions-history':
- clearInstructionsHistory();
- break;
- case 'export-cache':
- exportCacheToJson();
- break;
- }
- }
- if (target.classList.contains('remove-handle')) {
- const handleItem = target.closest('.handle-item');
- const handleTextElement = handleItem?.querySelector('.handle-text');
- if (handleTextElement) {
- const handle = handleTextElement.textContent.substring(1);
- removeHandleFromBlacklist(handle);
- }
- }
- if (tab) {
- switchTab(tab);
- }
- if (toggleTargetId) {
- toggleAdvancedOptions(toggleTargetId);
- }
- });
- uiContainer.addEventListener('input', (event) => {
- const target = event.target;
- const setting = target.dataset.setting;
- const paramName = target.closest('.parameter-row')?.dataset.paramName;
- if (setting) {
- handleSettingChange(target, setting);
- }
- if (paramName) {
- handleParameterChange(target, paramName);
- }
- if (target.id === 'tweet-filter-slider') {
- handleFilterSliderChange(target);
- }
- if (target.id === 'tweet-filter-value') {
- handleFilterValueInput(target);
- }
- });
- uiContainer.addEventListener('change', (event) => {
- const target = event.target;
- const setting = target.dataset.setting;
- if (setting === 'modelSortOrder') {
- handleSettingChange(target, setting);
- fetchAvailableModels();
- }
- if (setting === 'enableImageDescriptions') {
- handleSettingChange(target, setting);
- }
- });
- if (filterToggleBtn) {
- filterToggleBtn.onclick = () => {
- if (filterContainer) {
- filterContainer.style.display = 'flex';
- filterContainer.offsetHeight;
- filterContainer.classList.remove('hidden');
- }
- filterToggleBtn.style.display = 'none';
- };
- }
- document.addEventListener('click', closeAllSelectBoxes);
- const showFreeModelsCheckbox = uiContainer.querySelector('#show-free-models');
- if (showFreeModelsCheckbox) {
- showFreeModelsCheckbox.addEventListener('change', function () {
- showFreeModels = this.checked;
- browserSet('showFreeModels', showFreeModels);
- refreshModelsUI();
- });
- }
- const sortDirectionBtn = uiContainer.querySelector('#sort-direction');
- if (sortDirectionBtn) {
- sortDirectionBtn.addEventListener('click', function () {
- const currentDirection = browserGet('sortDirection', 'default');
- const newDirection = currentDirection === 'default' ? 'reverse' : 'default';
- browserSet('sortDirection', newDirection);
- this.dataset.value = newDirection;
- refreshModelsUI();
- });
- }
- const modelSortSelect = uiContainer.querySelector('#model-sort-order');
- if (modelSortSelect) {
- modelSortSelect.addEventListener('change', function () {
- browserSet('modelSortOrder', this.value);
- if (this.value === 'latency-low-to-high') {
- browserSet('sortDirection', 'default');
- } else if (this.value === '') {
- browserSet('sortDirection', 'default');
- }
- refreshModelsUI();
- });
- }
- const providerSortSelect = uiContainer.querySelector('#provider-sort');
- if (providerSortSelect) {
- providerSortSelect.addEventListener('change', function () {
- providerSort = this.value;
- browserSet('providerSort', providerSort);
- });
- }
- }
- function saveApiKey() {
- const apiKeyInput = document.getElementById('openrouter-api-key');
- const apiKey = apiKeyInput.value.trim();
- let previousAPIKey = browserGet('openrouter-api-key', '').length > 0 ? true : false;
- if (apiKey) {
- if (!previousAPIKey) {
- resetSettings(true);
- }
- browserSet('openrouter-api-key', apiKey);
- showStatus('API key saved successfully!');
- fetchAvailableModels();
- location.reload();
- } else {
- showStatus('Please enter a valid API key');
- }
- }
- function exportCacheToJson() {
- if (!tweetCache) {
- showStatus('Error: Tweet cache not found.', 'error');
- return;
- }
- try {
- const cacheData = tweetCache.cache;
- if (!cacheData || Object.keys(cacheData).length === 0) {
- showStatus('Cache is empty. Nothing to export.', 'warning');
- return;
- }
- const jsonString = JSON.stringify(cacheData, null, 2);
- const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8;' });
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.setAttribute('href', url);
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
- link.setAttribute('download', `tweet-filter-cache-${timestamp}.json`);
- link.style.visibility = 'hidden';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- URL.revokeObjectURL(url);
- showStatus(`Cache exported successfully (${Object.keys(cacheData).length} items).`);
- } catch (error) {
- showStatus('Error exporting cache. Check console for details.', 'error');
- }
- }
- function clearTweetRatingsAndRefreshUI() {
- if (isMobileDevice() || confirm('Are you sure you want to clear all cached tweet ratings?')) {
- tweetCache.clear(true);
- pendingRequests = 0;
- if (window.threadRelationships) {
- window.threadRelationships = {};
- browserSet('threadRelationships', '{}');
- }
- showStatus('All cached ratings and thread relationships cleared!');
- if (observedTargetNode) {
- observedTargetNode.querySelectorAll('article[data-testid="tweet"]').forEach(tweet => {
- tweet.removeAttribute('data-sloppiness-score');
- tweet.removeAttribute('data-rating-status');
- tweet.removeAttribute('data-rating-description');
- tweet.removeAttribute('data-cached-rating');
- const indicator = tweet.querySelector('.score-indicator');
- if (indicator) {
- indicator.remove();
- }
- const tweetId = getTweetID(tweet);
- if (tweetId) {
- processedTweets.delete(tweetId);
- const indicatorInstance = ScoreIndicatorRegistry.get(tweetId);
- if (indicatorInstance) {
- indicatorInstance.destroy();
- }
- scheduleTweetProcessing(tweet);
- }
- });
- }
- document.querySelectorAll('div[aria-label="Timeline: Conversation"], div[aria-label^="Timeline: Conversation"]').forEach(conversation => {
- delete conversation.dataset.threadMapping;
- delete conversation.dataset.threadMappedAt;
- delete conversation.dataset.threadMappingInProgress;
- delete conversation.dataset.threadHist;
- delete conversation.dataset.threadMediaUrls;
- });
- }
- }
- function addHandleFromInput() {
- const handleInput = document.getElementById('handle-input');
- const handle = handleInput.value.trim();
- if (handle) {
- addHandleToBlacklist(handle);
- handleInput.value = '';
- }
- }
- function handleSettingChange(target, settingName) {
- let value;
- if (target.type === 'checkbox') {
- value = target.checked;
- } else {
- value = target.value;
- }
- if (window[settingName] !== undefined) {
- window[settingName] = value;
- }
- browserSet(settingName, value);
- if (settingName === 'enableImageDescriptions') {
- const imageModelContainer = document.getElementById('image-model-container');
- if (imageModelContainer) {
- imageModelContainer.style.display = value ? 'block' : 'none';
- }
- showStatus('Image descriptions ' + (value ? 'enabled' : 'disabled'));
- }
- }
- function handleParameterChange(target, paramName) {
- const row = target.closest('.parameter-row');
- if (!row) return;
- const slider = row.querySelector('.parameter-slider');
- const valueInput = row.querySelector('.parameter-value');
- const min = parseFloat(slider.min);
- const max = parseFloat(slider.max);
- let newValue = parseFloat(target.value);
- if (target.type === 'number' && !isNaN(newValue)) {
- newValue = Math.max(min, Math.min(max, newValue));
- }
- if (slider && valueInput) {
- slider.value = newValue;
- valueInput.value = newValue;
- }
- if (window[paramName] !== undefined) {
- window[paramName] = newValue;
- }
- browserSet(paramName, newValue);
- }
- function handleFilterSliderChange(slider) {
- const valueInput = document.getElementById('tweet-filter-value');
- currentFilterThreshold = parseInt(slider.value, 10);
- if (valueInput) {
- valueInput.value = currentFilterThreshold.toString();
- }
- const percentage = (currentFilterThreshold / 10) * 100;
- slider.style.setProperty('--slider-percent', `${percentage}%`);
- browserSet('filterThreshold', currentFilterThreshold);
- applyFilteringToAll();
- }
- function handleFilterValueInput(input) {
- let value = parseInt(input.value, 10);
- value = Math.max(0, Math.min(10, value));
- input.value = value.toString();
- const slider = document.getElementById('tweet-filter-slider');
- if (slider) {
- slider.value = value.toString();
- const percentage = (value / 10) * 100;
- slider.style.setProperty('--slider-percent', `${percentage}%`);
- }
- currentFilterThreshold = value;
- browserSet('filterThreshold', currentFilterThreshold);
- applyFilteringToAll();
- }
- function switchTab(tabName) {
- const settingsContent = document.querySelector('#settings-container .settings-content');
- if (!settingsContent) return;
- const tabs = settingsContent.querySelectorAll('.tab-content');
- const buttons = settingsContent.querySelectorAll('.tab-navigation .tab-button');
- tabs.forEach(tab => tab.classList.remove('active'));
- buttons.forEach(btn => btn.classList.remove('active'));
- const tabToShow = settingsContent.querySelector(`#${tabName}-tab`);
- const buttonToActivate = settingsContent.querySelector(`.tab-navigation .tab-button[data-tab="${tabName}"]`);
- if (tabToShow) tabToShow.classList.add('active');
- if (buttonToActivate) buttonToActivate.classList.add('active');
- }
- function toggleAdvancedOptions(contentId) {
- const content = document.getElementById(contentId);
- const toggle = document.querySelector(`[data-toggle="${contentId}"]`);
- if (!content || !toggle) return;
- const icon = toggle.querySelector('.advanced-toggle-icon');
- const isExpanded = content.classList.toggle('expanded');
- if (icon) {
- icon.classList.toggle('expanded', isExpanded);
- }
- if (isExpanded) {
- content.style.maxHeight = content.scrollHeight + 'px';
- } else {
- content.style.maxHeight = '0';
- }
- }
- function refreshSettingsUI() {
- document.querySelectorAll('[data-setting]').forEach(input => {
- const settingName = input.dataset.setting;
- const value = browserGet(settingName, window[settingName]);
- if (input.type === 'checkbox') {
- input.checked = value;
- handleSettingChange(input, settingName);
- } else {
- input.value = value;
- }
- });
- document.querySelectorAll('.parameter-row[data-param-name]').forEach(row => {
- const paramName = row.dataset.paramName;
- const slider = row.querySelector('.parameter-slider');
- const valueInput = row.querySelector('.parameter-value');
- const value = browserGet(paramName, window[paramName]);
- if (slider) slider.value = value;
- if (valueInput) valueInput.value = value;
- });
- const filterSlider = document.getElementById('tweet-filter-slider');
- const filterValueInput = document.getElementById('tweet-filter-value');
- const currentThreshold = browserGet('filterThreshold', '5');
- if (filterSlider && filterValueInput) {
- filterSlider.value = currentThreshold;
- filterValueInput.value = currentThreshold;
- const percentage = (parseInt(currentThreshold, 10) / 10) * 100;
- filterSlider.style.setProperty('--slider-percent', `${percentage}%`);
- }
- refreshHandleList(document.getElementById('handle-list'));
- refreshModelsUI();
- document.querySelectorAll('.advanced-content').forEach(content => {
- if (!content.classList.contains('expanded')) {
- content.style.maxHeight = '0';
- }
- });
- document.querySelectorAll('.advanced-toggle-icon.expanded').forEach(icon => {
- if (!icon.closest('.advanced-toggle')?.nextElementSibling?.classList.contains('expanded')) {
- icon.classList.remove('expanded');
- }
- });
- refreshInstructionsHistory();
- }
- function refreshHandleList(listElement) {
- if (!listElement) return;
- listElement.innerHTML = '';
- if (blacklistedHandles.length === 0) {
- const emptyMsg = document.createElement('div');
- emptyMsg.style.cssText = 'padding: 8px; opacity: 0.7; font-style: italic;';
- emptyMsg.textContent = 'No handles added yet';
- listElement.appendChild(emptyMsg);
- return;
- }
- blacklistedHandles.forEach(handle => {
- const item = document.createElement('div');
- item.className = 'handle-item';
- const handleText = document.createElement('div');
- handleText.className = 'handle-text';
- handleText.textContent = '@' + handle;
- item.appendChild(handleText);
- const removeBtn = document.createElement('button');
- removeBtn.className = 'remove-handle';
- removeBtn.textContent = '×';
- removeBtn.title = 'Remove from list';
- item.appendChild(removeBtn);
- listElement.appendChild(item);
- });
- }
- function refreshModelsUI() {
- const modelSelectContainer = document.getElementById('model-select-container');
- const imageModelSelectContainer = document.getElementById('image-model-select-container');
- listedModels = [...availableModels];
- if (!showFreeModels) {
- listedModels = listedModels.filter(model => !model.slug.endsWith(':free'));
- }
- const sortDirection = browserGet('sortDirection', 'default');
- const sortOrder = browserGet('modelSortOrder', 'throughput-high-to-low');
- const toggleBtn = document.getElementById('sort-direction');
- if (toggleBtn) {
- switch (sortOrder) {
- case 'latency-low-to-high':
- toggleBtn.textContent = sortDirection === 'default' ? 'High-Low' : 'Low-High';
- if (sortDirection === 'reverse') listedModels.reverse();
- break;
- case '': // Age
- toggleBtn.textContent = sortDirection === 'default' ? 'New-Old' : 'Old-New';
- if (sortDirection === 'reverse') listedModels.reverse();
- break;
- case 'top-weekly':
- toggleBtn.textContent = sortDirection === 'default' ? 'Most Popular' : 'Least Popular';
- if (sortDirection === 'reverse') listedModels.reverse();
- break;
- default:
- toggleBtn.textContent = sortDirection === 'default' ? 'High-Low' : 'Low-High';
- if (sortDirection === 'reverse') listedModels.reverse();
- }
- }
- if (modelSelectContainer) {
- modelSelectContainer.innerHTML = '';
- createCustomSelect(
- modelSelectContainer,
- 'model-selector',
- listedModels.map(model => ({ value: model.slug || model.id, label: formatModelLabel(model) })),
- selectedModel,
- (newValue) => {
- selectedModel = newValue;
- browserSet('selectedModel', selectedModel);
- showStatus('Rating model updated');
- },
- 'Search rating models...'
- );
- }
- if (imageModelSelectContainer) {
- const visionModels = listedModels.filter(model =>
- model.input_modalities?.includes('image') ||
- model.architecture?.input_modalities?.includes('image') ||
- model.architecture?.modality?.includes('image')
- );
- imageModelSelectContainer.innerHTML = '';
- createCustomSelect(
- imageModelSelectContainer,
- 'image-model-selector',
- visionModels.map(model => ({ value: model.slug || model.id, label: formatModelLabel(model) })),
- selectedImageModel,
- (newValue) => {
- selectedImageModel = newValue;
- browserSet('selectedImageModel', selectedImageModel);
- showStatus('Image model updated');
- },
- 'Search vision models...'
- );
- }
- }
- function formatModelLabel(model) {
- let label = model.slug || model.id || model.name || 'Unknown Model';
- let pricingInfo = '';
- const pricing = model.endpoint?.pricing || model.pricing;
- if (pricing) {
- const promptPrice = parseFloat(pricing.prompt);
- const completionPrice = parseFloat(pricing.completion);
- if (!isNaN(promptPrice)) {
- pricingInfo += ` - $${(promptPrice * 1e6).toFixed(4)}/mil. tok.-in`;
- if (!isNaN(completionPrice) && completionPrice !== promptPrice) {
- pricingInfo += ` $${(completionPrice * 1e6).toFixed(4)}/mil. tok.-out`;
- }
- } else if (!isNaN(completionPrice)) {
- pricingInfo += ` - $${(completionPrice * 1e6).toFixed(4)}/mil. tok.-out`;
- }
- }
- const isVision = model.input_modalities?.includes('image') ||
- model.architecture?.input_modalities?.includes('image') ||
- model.architecture?.modality?.includes('image');
- if (isVision) {
- label = '🖼️ ' + label;
- }
- return label + pricingInfo;
- }
- function createCustomSelect(container, id, options, initialSelectedValue, onChange, searchPlaceholder) {
- let currentSelectedValue = initialSelectedValue;
- const customSelect = document.createElement('div');
- customSelect.className = 'custom-select';
- customSelect.id = id;
- const selectSelected = document.createElement('div');
- selectSelected.className = 'select-selected';
- const selectItems = document.createElement('div');
- selectItems.className = 'select-items';
- selectItems.style.display = 'none';
- const searchField = document.createElement('div');
- searchField.className = 'search-field';
- const searchInput = document.createElement('input');
- searchInput.type = 'text';
- searchInput.className = 'search-input';
- searchInput.placeholder = searchPlaceholder || 'Search...';
- searchField.appendChild(searchInput);
- selectItems.appendChild(searchField);
- function renderOptions(filter = '') {
- while (selectItems.childNodes.length > 1) {
- selectItems.removeChild(selectItems.lastChild);
- }
- const filteredOptions = options.filter(opt =>
- opt.label.toLowerCase().includes(filter.toLowerCase())
- );
- if (filteredOptions.length === 0) {
- const noResults = document.createElement('div');
- noResults.textContent = 'No matches found';
- noResults.style.cssText = 'opacity: 0.7; font-style: italic; padding: 10px; text-align: center; cursor: default;';
- selectItems.appendChild(noResults);
- }
- filteredOptions.forEach(option => {
- const optionDiv = document.createElement('div');
- optionDiv.textContent = option.label;
- optionDiv.dataset.value = option.value;
- if (option.value === currentSelectedValue) {
- optionDiv.classList.add('same-as-selected');
- }
- optionDiv.addEventListener('click', (e) => {
- e.stopPropagation();
- currentSelectedValue = option.value;
- selectSelected.textContent = option.label;
- selectItems.style.display = 'none';
- selectSelected.classList.remove('select-arrow-active');
- selectItems.querySelectorAll('div[data-value]').forEach(div => {
- div.classList.toggle('same-as-selected', div.dataset.value === currentSelectedValue);
- });
- onChange(currentSelectedValue);
- });
- selectItems.appendChild(optionDiv);
- });
- }
- const initialOption = options.find(opt => opt.value === currentSelectedValue);
- selectSelected.textContent = initialOption ? initialOption.label : 'Select an option';
- customSelect.appendChild(selectSelected);
- customSelect.appendChild(selectItems);
- container.appendChild(customSelect);
- renderOptions();
- searchInput.addEventListener('input', () => renderOptions(searchInput.value));
- searchInput.addEventListener('click', e => e.stopPropagation());
- selectSelected.addEventListener('click', (e) => {
- e.stopPropagation();
- closeAllSelectBoxes(customSelect);
- const isHidden = selectItems.style.display === 'none';
- selectItems.style.display = isHidden ? 'block' : 'none';
- selectSelected.classList.toggle('select-arrow-active', isHidden);
- if (isHidden) {
- searchInput.focus();
- searchInput.select();
- renderOptions();
- }
- });
- }
- function closeAllSelectBoxes(exceptThisOne = null) {
- document.querySelectorAll('.custom-select').forEach(select => {
- if (select === exceptThisOne) return;
- const items = select.querySelector('.select-items');
- const selected = select.querySelector('.select-selected');
- if (items) items.style.display = 'none';
- if (selected) selected.classList.remove('select-arrow-active');
- });
- }
- function resetSettings(noconfirm = false) {
- if (noconfirm || confirm('Are you sure you want to reset all settings to their default values? This will not clear your cached ratings, blacklisted handles, or instruction history.')) {
- tweetCache.clear();
- const defaults = {
- selectedModel: 'openai/gpt-4.1-nano',
- selectedImageModel: 'openai/gpt-4.1-nano',
- enableImageDescriptions: false,
- enableStreaming: true,
- modelTemperature: 0.5,
- modelTopP: 0.9,
- imageModelTemperature: 0.5,
- imageModelTopP: 0.9,
- maxTokens: 0,
- filterThreshold: 5,
- userDefinedInstructions: 'Rate the tweet on a scale from 1 to 10 based on its clarity, insight, creativity, and overall quality.',
- modelSortOrder: 'throughput-high-to-low'
- };
- for (const key in defaults) {
- if (window[key] !== undefined) {
- window[key] = defaults[key];
- }
- browserSet(key, defaults[key]);
- }
- refreshSettingsUI();
- fetchAvailableModels();
- showStatus('Settings reset to defaults');
- }
- }
- function addHandleToBlacklist(handle) {
- handle = handle.trim().replace(/^@/, '');
- if (handle === '' || blacklistedHandles.includes(handle)) {
- showStatus(handle === '' ? 'Handle cannot be empty.' : `@${handle} is already on the list.`);
- return;
- }
- blacklistedHandles.push(handle);
- browserSet('blacklistedHandles', blacklistedHandles.join('\n'));
- refreshHandleList(document.getElementById('handle-list'));
- showStatus(`Added @${handle} to auto-rate list.`);
- }
- function removeHandleFromBlacklist(handle) {
- const index = blacklistedHandles.indexOf(handle);
- if (index > -1) {
- blacklistedHandles.splice(index, 1);
- browserSet('blacklistedHandles', blacklistedHandles.join('\n'));
- refreshHandleList(document.getElementById('handle-list'));
- showStatus(`Removed @${handle} from auto-rate list.`);
- } else console.warn(`Attempted to remove non-existent handle: ${handle}`);
- }
- function initialiseUI() {
- const uiContainer = injectUI();
- if (!uiContainer) return;
- initializeEventListeners(uiContainer);
- refreshSettingsUI();
- fetchAvailableModels();
- initializeFloatingCacheStats();
- setInterval(updateCacheStatsUI, 3000);
- if (!window.activeStreamingRequests) window.activeStreamingRequests = {};
- }
- function initializeFloatingCacheStats() {
- const statsBadge = document.getElementById('tweet-filter-stats-badge');
- if (!statsBadge) return;
- statsBadge.title = 'Click to open settings';
- statsBadge.addEventListener('click', () => {
- const settingsToggle = document.getElementById('settings-toggle');
- if (settingsToggle) {
- settingsToggle.click();
- }
- });
- let fadeTimeout;
- const resetFadeTimeout = () => {
- clearTimeout(fadeTimeout);
- statsBadge.style.opacity = '1';
- fadeTimeout = setTimeout(() => {
- statsBadge.style.opacity = '0.3';
- }, 5000);
- };
- statsBadge.addEventListener('mouseenter', () => {
- statsBadge.style.opacity = '1';
- clearTimeout(fadeTimeout);
- });
- statsBadge.addEventListener('mouseleave', resetFadeTimeout);
- resetFadeTimeout();
- updateCacheStatsUI();
- }
- // ----- ratingEngine.js -----
- function filterSingleTweet(tweetArticle) {
- const cell = tweetArticle.closest('div[data-testid="cellInnerDiv"]');
- if (!cell) {
- return;
- }
- const handles = getUserHandles(tweetArticle);
- const authorHandle = handles.length > 0 ? handles[0] : '';
- if (authorHandle && adAuthorCache.has(authorHandle)) {
- const tweetId = getTweetID(tweetArticle);
- if (tweetId) {
- ScoreIndicatorRegistry.get(tweetId)?.destroy();
- }
- cell.innerHTML = '';
- cell.dataset.filtered = 'true';
- cell.dataset.isAd = 'true';
- return;
- }
- const score = parseInt(tweetArticle.dataset.sloppinessScore || '9', 10);
- const tweetId = getTweetID(tweetArticle);
- const indicatorInstance = ScoreIndicatorRegistry.get(tweetId);
- indicatorInstance?.ensureIndicatorAttached();
- const currentFilterThreshold = parseInt(browserGet('filterThreshold', '1'));
- const ratingStatus = tweetArticle.dataset.ratingStatus;
- if (ratingStatus === 'pending' || ratingStatus === 'streaming') {
- delete cell.dataset.filtered;
- } else if (isNaN(score) || score < currentFilterThreshold) {
- if (tweetId) {
- ScoreIndicatorRegistry.get(tweetId)?.destroy();
- }
- cell.innerHTML = '';
- cell.dataset.filtered = 'true';
- }
- }
- async function applyTweetCachedRating(tweetArticle) {
- const tweetId = getTweetID(tweetArticle);
- const handles = getUserHandles(tweetArticle);
- const userHandle = handles.length > 0 ? handles[0] : '';
- if (userHandle && isUserBlacklisted(userHandle)) {
- const indicatorInstance = ScoreIndicatorRegistry.get(tweetId, tweetArticle);
- if (indicatorInstance) {
- const tweetText = getElementText(tweetArticle.querySelector(TWEET_TEXT_SELECTOR)) || "[Tweet text not found]";
- const mediaUrls = await extractMediaLinks(tweetArticle);
- const blacklistResponse = `<ANALYSIS>
- This user is on the blacklist. Tweets from this user are not rated by the AI and are always shown.
- </ANALYSIS>
- <SCORE>
- SCORE_10
- </SCORE>
- <FOLLOW_UP_QUESTIONS>
- Q_1. Rate this tweet anyway.
- Q_2. N/A
- Q_3. N/A
- </FOLLOW_UP_QUESTIONS>`;
- indicatorInstance.updateInitialReviewAndBuildHistory({
- fullContext: tweetText,
- mediaUrls: mediaUrls,
- apiResponseContent: blacklistResponse,
- reviewSystemPrompt: REVIEW_SYSTEM_PROMPT, // Assumed global
- followUpSystemPrompt: FOLLOW_UP_SYSTEM_PROMPT // Assumed global
- });
- tweetCache.set(tweetId, {
- score: 10,
- description: indicatorInstance.description,
- reasoning: "",
- questions: indicatorInstance.questions,
- lastAnswer: "",
- tweetContent: tweetText,
- mediaUrls: mediaUrls,
- streaming: false,
- blacklisted: true,
- timestamp: Date.now(),
- qaConversationHistory: indicatorInstance.qaConversationHistory
- });
- } else {
- tweetArticle.dataset.sloppinessScore = '10';
- tweetArticle.dataset.blacklisted = 'true';
- tweetArticle.dataset.ratingStatus = 'blacklisted';
- tweetArticle.dataset.ratingDescription = 'User is blacklisted';
- }
- filterSingleTweet(tweetArticle);
- return true;
- }
- const cachedRating = tweetCache.get(tweetId);
- if (cachedRating) {
- if (cachedRating.streaming === true &&
- (cachedRating.score === undefined || cachedRating.score === null)) {
- return false;
- }
- if (cachedRating.score !== undefined && cachedRating.score !== null) {
- const indicatorInstance = ScoreIndicatorRegistry.get(tweetId, tweetArticle);
- if (indicatorInstance) {
- indicatorInstance.rehydrateFromCache(cachedRating);
- } else {
- return false;
- }
- filterSingleTweet(tweetArticle);
- return true;
- } else if (!cachedRating.streaming) {
- tweetCache.delete(tweetId);
- return false;
- }
- }
- return false;
- }
- function isUserBlacklisted(handle) {
- if (!handle) return false;
- handle = handle.toLowerCase().trim();
- return blacklistedHandles.some(h => h.toLowerCase().trim() === handle);
- }
- const VALID_FINAL_STATES = ['rated', 'cached', 'blacklisted'];
- const VALID_INTERIM_STATES = ['pending', 'streaming'];
- function isValidFinalState(status) {
- return VALID_FINAL_STATES.includes(status);
- }
- function isValidInterimState(status) {
- return VALID_INTERIM_STATES.includes(status);
- }
- async function delayedProcessTweet(tweetArticle, tweetId, authorHandle) {
- let processingSuccessful = false;
- try {
- const apiKey = browserGet('openrouter-api-key', '');
- if (!apiKey) {
- tweetArticle.dataset.ratingStatus = 'error';
- tweetArticle.dataset.ratingDescription = "No API key";
- ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
- status: 'error',
- score: 9,
- description: "No API key",
- questions: [],
- lastAnswer: ""
- });
- filterSingleTweet(tweetArticle);
- return;
- }
- if (authorHandle && adAuthorCache.has(authorHandle)) {
- tweetArticle.dataset.ratingStatus = 'rated';
- tweetArticle.dataset.ratingDescription = "Advertisement";
- tweetArticle.dataset.sloppinessScore = '0';
- ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
- status: 'rated',
- score: 0,
- description: "Advertisement from known ad author",
- questions: [],
- lastAnswer: ""
- });
- filterSingleTweet(tweetArticle);
- processingSuccessful = true;
- return;
- }
- if (isAd(tweetArticle)) {
- if (authorHandle) {
- adAuthorCache.add(authorHandle);
- }
- tweetArticle.dataset.ratingStatus = 'rated';
- tweetArticle.dataset.ratingDescription = "Advertisement";
- tweetArticle.dataset.sloppinessScore = '0';
- ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
- status: 'rated',
- score: 0,
- description: "Advertisement",
- questions: [],
- lastAnswer: ""
- });
- filterSingleTweet(tweetArticle);
- processingSuccessful = true;
- return;
- }
- let score = 5;
- let description = "";
- let reasoning = "";
- let questions = [];
- let lastAnswer = "";
- try {
- const cachedRating = tweetCache.get(tweetId);
- if (cachedRating) {
- if (cachedRating.streaming === true &&
- (cachedRating.score === undefined || cachedRating.score === null)) {
- }
- else if (!cachedRating.streaming && (cachedRating.score === undefined || cachedRating.score === null)) {
- tweetCache.delete(tweetId);
- }
- }
- const fullContextWithImageDescription = await getFullContext(tweetArticle, tweetId, apiKey);
- if (!fullContextWithImageDescription) {
- throw new Error("Failed to get tweet context");
- }
- let mediaURLs = [];
- if (document.querySelector('div[aria-label="Timeline: Conversation"]')) {
- const replyInfo = getTweetReplyInfo(tweetId);
- if (replyInfo && replyInfo.replyTo) {
- if (!tweetCache.has(tweetId)) {
- tweetCache.set(tweetId, {});
- }
- if (!tweetCache.get(tweetId).threadContext) {
- tweetCache.get(tweetId).threadContext = {
- replyTo: replyInfo.to,
- replyToId: replyInfo.replyTo,
- isRoot: false
- };
- }
- }
- }
- const mediaMatches1 = fullContextWithImageDescription.matchAll(/(?:\[MEDIA_URLS\]:\s*\n)(.*?)(?:\n|$)/g);
- const mediaMatches2 = fullContextWithImageDescription.matchAll(/(?:\[QUOTED_TWEET_MEDIA_URLS\]:\s*\n)(.*?)(?:\n|$)/g);
- for (const match of mediaMatches1) {
- if (match[1]) {
- mediaURLs.push(...match[1].split(', ').filter(url => url.trim()));
- }
- }
- for (const match of mediaMatches2) {
- if (match[1]) {
- mediaURLs.push(...match[1].split(', ').filter(url => url.trim()));
- }
- }
- mediaURLs = [...new Set(mediaURLs.filter(url => url.trim()))];
- if (fullContextWithImageDescription) {
- try {
- const currentCache = tweetCache.get(tweetId);
- const isCached = currentCache &&
- !currentCache.streaming &&
- currentCache.score !== undefined &&
- currentCache.score !== null;
- if (isCached) {
- score = currentCache.score;
- description = currentCache.description || "";
- reasoning = currentCache.reasoning || "";
- questions = currentCache.questions || [];
- lastAnswer = currentCache.lastAnswer || "";
- const mediaUrls = currentCache.mediaUrls || [];
- processingSuccessful = true;
- ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
- status: currentCache.fromStorage ? 'cached' : 'rated',
- score: score,
- description: description,
- reasoning: reasoning,
- questions: questions,
- lastAnswer: lastAnswer,
- metadata: currentCache.metadata || null,
- mediaUrls: mediaUrls // Pass mediaUrls to indicator
- });
- filterSingleTweet(tweetArticle);
- return;
- }
- const rating = await rateTweetWithOpenRouter(fullContextWithImageDescription, tweetId, apiKey, mediaURLs, 3, tweetArticle, authorHandle);
- score = rating.score;
- description = rating.content;
- reasoning = rating.reasoning || '';
- questions = rating.questions || [];
- lastAnswer = "";
- let finalStatus = rating.error ? 'error' : 'rated';
- if (!rating.error) {
- const cacheEntry = tweetCache.get(tweetId);
- if (cacheEntry && cacheEntry.fromStorage) {
- finalStatus = 'cached';
- } else if (rating.cached) {
- finalStatus = 'cached';
- }
- }
- tweetArticle.dataset.ratingStatus = finalStatus;
- tweetArticle.dataset.ratingDescription = description || "not available";
- tweetArticle.dataset.sloppinessScore = score?.toString() || '';
- tweetArticle.dataset.ratingReasoning = reasoning;
- ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
- status: finalStatus,
- score: score,
- description: description,
- reasoning: reasoning,
- questions: questions,
- lastAnswer: lastAnswer,
- metadata: rating.data?.id ? { generationId: rating.data.id } : null, // Pass metadata
- mediaUrls: mediaURLs // Pass mediaUrls to indicator
- });
- processingSuccessful = !rating.error;
- filterSingleTweet(tweetArticle);
- return;
- } catch (apiError) {
- score = 5;
- description = `API Error: ${apiError.message}`;
- reasoning = '';
- questions = [];
- lastAnswer = '';
- processingSuccessful = false;
- ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
- status: 'error',
- score: score,
- description: description,
- questions: [],
- lastAnswer: ""
- });
- const errorCacheEntry = tweetCache.get(tweetId) || {};
- errorCacheEntry.score = score;
- errorCacheEntry.description = description;
- errorCacheEntry.reasoning = reasoning;
- errorCacheEntry.questions = questions;
- errorCacheEntry.lastAnswer = lastAnswer;
- errorCacheEntry.error = true;
- errorCacheEntry.streaming = false;
- tweetCache.set(tweetId, errorCacheEntry);
- filterSingleTweet(tweetArticle);
- return;
- }
- }
- filterSingleTweet(tweetArticle);
- } catch (error) {
- ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
- status: 'error',
- score: 5,
- description: "Error during processing: " + error.message,
- questions: [],
- lastAnswer: ""
- });
- filterSingleTweet(tweetArticle);
- processingSuccessful = false;
- } finally {
- if (!processingSuccessful) {
- processedTweets.delete(tweetId);
- }
- }
- } catch (error) {
- const indicatorInstance = ScoreIndicatorRegistry.get(tweetId);
- if (indicatorInstance) {
- indicatorInstance.update({
- status: 'error',
- score: 5,
- description: "Error during processing: " + error.message,
- questions: [],
- lastAnswer: ""
- });
- }
- filterSingleTweet(tweetArticle);
- processingSuccessful = false;
- } finally {
- if (!processingSuccessful) {
- processedTweets.delete(tweetId);
- const indicatorInstance = ScoreIndicatorRegistry.get(tweetId);
- if (indicatorInstance && !isValidFinalState(indicatorInstance.status)) {
- setTimeout(() => {
- if (!isValidFinalState(ScoreIndicatorRegistry.get(tweetId)?.status)) {
- scheduleTweetProcessing(tweetArticle);
- }
- }, PROCESSING_DELAY_MS * 2);
- }
- }
- }
- }
- async function scheduleTweetProcessing(tweetArticle) {
- const tweetId = getTweetID(tweetArticle);
- if (!tweetId) {
- return;
- }
- if (window.activeStreamingRequests && window.activeStreamingRequests[tweetId]) {
- return;
- }
- const handles = getUserHandles(tweetArticle);
- const authorHandle = handles.length > 0 ? handles[0] : '';
- if (authorHandle && adAuthorCache.has(authorHandle)) {
- filterSingleTweet(tweetArticle);
- return;
- }
- if (isAd(tweetArticle)) {
- if (authorHandle) {
- adAuthorCache.add(authorHandle);
- }
- filterSingleTweet(tweetArticle);
- return;
- }
- const existingInstance = ScoreIndicatorRegistry.get(tweetId);
- if (existingInstance) {
- existingInstance.ensureIndicatorAttached();
- if (isValidFinalState(existingInstance.status)) {
- filterSingleTweet(tweetArticle);
- return;
- }
- if (isValidInterimState(existingInstance.status) && processedTweets.has(tweetId)) {
- filterSingleTweet(tweetArticle);
- return;
- }
- processedTweets.delete(tweetId);
- }
- if (authorHandle && isUserBlacklisted(authorHandle)) {
- const indicatorInstance = ScoreIndicatorRegistry.get(tweetId, tweetArticle);
- if (indicatorInstance) {
- const tweetText = getElementText(tweetArticle.querySelector(TWEET_TEXT_SELECTOR)) || "[Tweet text not found]";
- const mediaUrls = await extractMediaLinks(tweetArticle);
- const blacklistResponse = `<ANALYSIS>
- This user is on the blacklist. Tweets from this user are not rated by the AI and are always shown.
- </ANALYSIS>
- <SCORE>
- SCORE_10
- </SCORE>
- <FOLLOW_UP_QUESTIONS>
- Q_1. Rate this tweet anyway.
- Q_2. N/A
- Q_3. N/A
- </FOLLOW_UP_QUESTIONS>`;
- indicatorInstance.updateInitialReviewAndBuildHistory({
- fullContext: tweetText,
- mediaUrls: mediaUrls,
- apiResponseContent: blacklistResponse,
- reviewSystemPrompt: REVIEW_SYSTEM_PROMPT, // Assumed global
- followUpSystemPrompt: FOLLOW_UP_SYSTEM_PROMPT // Assumed global
- });
- tweetCache.set(tweetId, {
- score: 10,
- description: indicatorInstance.description,
- reasoning: "",
- questions: indicatorInstance.questions,
- lastAnswer: "",
- tweetContent: tweetText,
- mediaUrls: mediaUrls,
- streaming: false,
- blacklisted: true,
- timestamp: Date.now(),
- qaConversationHistory: indicatorInstance.qaConversationHistory
- });
- } else {
- tweetArticle.dataset.sloppinessScore = '10';
- tweetArticle.dataset.blacklisted = 'true';
- tweetArticle.dataset.ratingStatus = 'blacklisted';
- tweetArticle.dataset.ratingDescription = 'User is blacklisted';
- }
- filterSingleTweet(tweetArticle);
- return;
- }
- if (tweetCache.has(tweetId)) {
- const isIncompleteStreaming =
- tweetCache.get(tweetId).streaming === true &&
- (tweetCache.get(tweetId).score === undefined || tweetCache.get(tweetId).score === null);
- if (!isIncompleteStreaming) {
- const wasApplied = await applyTweetCachedRating(tweetArticle);
- if (wasApplied) {
- return;
- }
- }
- }
- if (processedTweets.has(tweetId)) {
- const instance = ScoreIndicatorRegistry.get(tweetId);
- if (instance) {
- instance.ensureIndicatorAttached();
- if (instance.status === 'pending' || instance.status === 'streaming') {
- filterSingleTweet(tweetArticle);
- return;
- }
- }
- processedTweets.delete(tweetId);
- }
- const indicatorInstance = ScoreIndicatorRegistry.get(tweetId, tweetArticle);
- if (indicatorInstance) {
- if (indicatorInstance.status !== 'blacklisted' &&
- indicatorInstance.status !== 'cached' &&
- indicatorInstance.status !== 'rated') {
- indicatorInstance.update({ status: 'pending', score: null, description: 'Rating scheduled...', questions: [], lastAnswer: "" });
- } else {
- indicatorInstance.ensureIndicatorAttached();
- filterSingleTweet(tweetArticle);
- return;
- }
- } else {
- }
- if (!processedTweets.has(tweetId)) {
- processedTweets.add(tweetId);
- }
- setTimeout(() => {
- try {
- delayedProcessTweet(tweetArticle, tweetId, authorHandle);
- } catch (e) {
- processedTweets.delete(tweetId);
- }
- }, PROCESSING_DELAY_MS);
- }
- let threadRelationships = {};
- const THREAD_CHECK_INTERVAL = 2500;
- const SWEEP_INTERVAL = 1000;
- let threadMappingInProgress = false;
- function loadThreadRelationships() {
- try {
- const savedRelationships = browserGet('threadRelationships', '{}');
- threadRelationships = JSON.parse(savedRelationships);
- } catch (e) {
- threadRelationships = {};
- }
- }
- function saveThreadRelationships() {
- try {
- const relationshipCount = Object.keys(threadRelationships).length;
- if (relationshipCount > 1000) {
- const entries = Object.entries(threadRelationships);
- entries.sort((a, b) => (b[1].timestamp || 0) - (a[1].timestamp || 0));
- const recent = entries.slice(0, 500);
- threadRelationships = Object.fromEntries(recent);
- }
- browserSet('threadRelationships', JSON.stringify(threadRelationships));
- } catch (e) {
- }
- }
- loadThreadRelationships();
- async function buildReplyChain(tweetId, maxDepth = 5) {
- if (!tweetId || maxDepth <= 0) return [];
- const chain = [];
- let currentId = tweetId;
- let depth = 0;
- while (currentId && depth < maxDepth) {
- const replyInfo = threadRelationships[currentId];
- if (!replyInfo || !replyInfo.replyTo) break;
- chain.push({
- fromId: currentId,
- toId: replyInfo.replyTo,
- from: replyInfo.from,
- to: replyInfo.to
- });
- currentId = replyInfo.replyTo;
- depth++;
- }
- return chain;
- }
- async function getFullContext(tweetArticle, tweetId, apiKey) {
- const handles = getUserHandles(tweetArticle);
- const userHandle = handles.length > 0 ? handles[0] : '';
- const quotedHandle = handles.length > 1 ? handles[1] : '';
- const mainText = getElementText(tweetArticle.querySelector(TWEET_TEXT_SELECTOR));
- let allMediaLinks = await extractMediaLinks(tweetArticle);
- let quotedText = "";
- let quotedMediaLinks = [];
- let quotedTweetId = null;
- const quoteContainer = tweetArticle.querySelector(QUOTE_CONTAINER_SELECTOR);
- if (quoteContainer) {
- const quotedLink = quoteContainer.querySelector('a[href*="/status/"]');
- if (quotedLink) {
- const href = quotedLink.getAttribute('href');
- const match = href.match(/\/status\/(\d+)/);
- if (match && match[1]) {
- quotedTweetId = match[1];
- }
- }
- quotedText = getElementText(quoteContainer.querySelector(TWEET_TEXT_SELECTOR)) || "";
- quotedMediaLinks = await extractMediaLinks(quoteContainer);
- }
- const conversation = document.querySelector('div[aria-label="Timeline: Conversation"]') ||
- document.querySelector('div[aria-label^="Timeline: Conversation"]');
- let threadMediaUrls = [];
- if (conversation && conversation.dataset.threadMapping && tweetCache.has(tweetId) && tweetCache.get(tweetId).threadContext?.threadMediaUrls) {
- threadMediaUrls = tweetCache.get(tweetId).threadContext.threadMediaUrls || [];
- } else if (conversation && conversation.dataset.threadMediaUrls) {
- try {
- const allMediaUrls = JSON.parse(conversation.dataset.threadMediaUrls);
- threadMediaUrls = Array.isArray(allMediaUrls) ? allMediaUrls : [];
- } catch (e) {
- }
- }
- let allAvailableMediaLinks = [...allMediaLinks];
- let mainMediaLinks = allAvailableMediaLinks.filter(link => !quotedMediaLinks.includes(link));
- let engagementStats = "";
- const engagementDiv = tweetArticle.querySelector('div[role="group"][aria-label$=" views"]');
- if (engagementDiv) {
- engagementStats = engagementDiv.getAttribute('aria-label')?.trim() || "";
- }
- let fullContextWithImageDescription = `[TWEET ${tweetId}]
- Author:@${userHandle}:
- ` + mainText;
- if (mainMediaLinks.length > 0) {
- if (enableImageDescriptions = browserGet('enableImageDescriptions', false)) {
- let mainMediaLinksDescription = await getImageDescription(mainMediaLinks, apiKey, tweetId, userHandle);
- fullContextWithImageDescription += `
- [MEDIA_DESCRIPTION]:
- ${mainMediaLinksDescription}`;
- }
- fullContextWithImageDescription += `
- [MEDIA_URLS]:
- ${mainMediaLinks.join(", ")}`;
- }
- if (engagementStats) {
- fullContextWithImageDescription += `
- [ENGAGEMENT_STATS]:
- ${engagementStats}`;
- }
- if (!isOriginalTweet(tweetArticle) && threadMediaUrls.length > 0) {
- const uniqueThreadMediaUrls = threadMediaUrls.filter(url =>
- !mainMediaLinks.includes(url) && !quotedMediaLinks.includes(url));
- if (uniqueThreadMediaUrls.length > 0) {
- fullContextWithImageDescription += `
- [THREAD_MEDIA_URLS]:
- ${uniqueThreadMediaUrls.join(", ")}`;
- }
- }
- if (quotedText || quotedMediaLinks.length > 0) {
- fullContextWithImageDescription += `
- [QUOTED_TWEET${quotedTweetId ? ' ' + quotedTweetId : ''}]:
- Author:@${quotedHandle}:
- ${quotedText}`;
- if (quotedMediaLinks.length > 0) {
- if (enableImageDescriptions) {
- let quotedMediaLinksDescription = await getImageDescription(quotedMediaLinks, apiKey, tweetId, userHandle);
- fullContextWithImageDescription += `
- [QUOTED_TWEET_MEDIA_DESCRIPTION]:
- ${quotedMediaLinksDescription}`;
- }
- fullContextWithImageDescription += `
- [QUOTED_TWEET_MEDIA_URLS]:
- ${quotedMediaLinks.join(", ")}`;
- }
- }
- if (document.querySelector('div[aria-label="Timeline: Conversation"]', 'div[aria-label^="Timeline: Conversation"]')) {
- const replyChain = await buildReplyChain(tweetId);
- let threadHistoryIncluded = false;
- if (conversation && conversation.dataset.threadHist) {
- if (!isOriginalTweet(tweetArticle)) {
- fullContextWithImageDescription = conversation.dataset.threadHist + `
- [REPLY]
- ` + fullContextWithImageDescription;
- threadHistoryIncluded = true;
- }
- }
- if (replyChain.length > 0 && !threadHistoryIncluded) {
- let parentContexts = "";
- for (let i = replyChain.length - 1; i >= 0; i--) {
- const link = replyChain[i];
- const parentId = link.toId;
- const parentCache = tweetCache.get(parentId);
- const parentContent = parentCache?.tweetContent;
- if (parentContent) {
- parentContexts = parentContent + "\n[REPLY]\n" + parentContexts;
- } else {
- parentContexts = `[CONTEXT UNAVAILABLE FOR TWEET ${parentId} @${link.to || 'unknown'}]\n[REPLY]\n` + parentContexts;
- }
- }
- fullContextWithImageDescription = parentContexts + fullContextWithImageDescription;
- }
- const replyInfo = getTweetReplyInfo(tweetId);
- if (replyInfo && replyInfo.replyTo && !threadHistoryIncluded && replyChain.length === 0) {
- fullContextWithImageDescription = `[REPLY TO TWEET ${replyInfo.replyTo}]\n` + fullContextWithImageDescription;
- }
- }
- tweetArticle.dataset.fullContext = fullContextWithImageDescription;
- return fullContextWithImageDescription;
- }
- function applyFilteringToAll() {
- if (!observedTargetNode) return;
- const tweets = observedTargetNode.querySelectorAll(TWEET_ARTICLE_SELECTOR);
- tweets.forEach(filterSingleTweet);
- }
- function ensureAllTweetsRated() {
- if (!observedTargetNode) return;
- const tweets = observedTargetNode.querySelectorAll(TWEET_ARTICLE_SELECTOR);
- if (tweets.length > 0) {
- tweets.forEach(tweet => {
- const tweetId = getTweetID(tweet);
- if (!tweetId) return;
- const indicatorInstance = ScoreIndicatorRegistry.get(tweetId);
- const needsProcessing = !indicatorInstance ||
- !indicatorInstance.status ||
- indicatorInstance.status === 'error' ||
- (!isValidFinalState(indicatorInstance.status) && !isValidInterimState(indicatorInstance.status)) ||
- (processedTweets.has(tweetId) && !isValidFinalState(indicatorInstance.status) && !isValidInterimState(indicatorInstance.status));
- if (needsProcessing) {
- if (processedTweets.has(tweetId)) {
- processedTweets.delete(tweetId);
- }
- scheduleTweetProcessing(tweet);
- } else if (indicatorInstance && !isValidInterimState(indicatorInstance.status)) {
- filterSingleTweet(tweet);
- }
- });
- }
- }
- async function handleThreads() {
- try {
- let conversation = document.querySelector('div[aria-label="Timeline: Conversation"]');
- if (!conversation) {
- conversation = document.querySelector('div[aria-label^="Timeline: Conversation"]');
- }
- if (!conversation) return;
- if (threadMappingInProgress || conversation.dataset.threadHist === "pending") {
- return;
- }
- if (conversation.dataset.threadMappedAt) {
- const lastMappedTime = parseInt(conversation.dataset.threadMappedAt, 10);
- if (Date.now() - lastMappedTime < 10000) {
- return;
- }
- }
- const match = location.pathname.match(/status\/(\d+)/);
- const pageTweetId = match ? match[1] : null;
- if (!pageTweetId) return;
- let rootTweetId = pageTweetId;
- while (threadRelationships[rootTweetId] && threadRelationships[rootTweetId].replyTo) {
- rootTweetId = threadRelationships[rootTweetId].replyTo;
- }
- if (conversation.dataset.threadHist === undefined) {
- threadHist = "";
- const rootArticle = Array.from(conversation.querySelectorAll('article[data-testid="tweet"]'))
- .find(el => getTweetID(el) === rootTweetId)
- || document.querySelector('article[data-testid="tweet"]');
- if (rootArticle) {
- conversation.dataset.threadHist = 'pending';
- threadMappingInProgress = true;
- try {
- const tweetId = getTweetID(rootArticle);
- if (!tweetId) {
- throw new Error("Failed to get tweet ID from first article");
- }
- const apiKey = browserGet('openrouter-api-key', '');
- const fullcxt = await getFullContext(rootArticle, tweetId, apiKey);
- if (!fullcxt) {
- throw new Error("Failed to get full context for root tweet");
- }
- threadHist = fullcxt;
- conversation.dataset.threadHist = threadHist;
- if (conversation.firstChild) {
- conversation.firstChild.dataset.canary = "true";
- }
- if (!processedTweets.has(tweetId)) {
- scheduleTweetProcessing(rootArticle);
- }
- setTimeout(() => {
- mapThreadStructure(conversation, rootTweetId);
- }, 10);
- } catch (error) {
- threadMappingInProgress = false;
- delete conversation.dataset.threadHist;
- }
- return;
- }
- } else if (conversation.dataset.threadHist !== "pending" &&
- conversation.firstChild &&
- conversation.firstChild.dataset.canary === undefined) {
- if (conversation.firstChild) {
- conversation.firstChild.dataset.canary = "pending";
- }
- threadMappingInProgress = true;
- try {
- const nextArticle = document.querySelector('article[data-testid="tweet"]:has(~ div[data-testid="inline_reply_offscreen"])');
- if (nextArticle) {
- const tweetId = getTweetID(nextArticle);
- if (!tweetId) {
- throw new Error("Failed to get tweet ID from next article");
- }
- if (tweetCache.has(tweetId) && tweetCache.get(tweetId).tweetContent) {
- threadHist = threadHist + "\n[REPLY]\n" + tweetCache.get(tweetId).tweetContent;
- } else {
- const apiKey = browserGet('openrouter-api-key', '');
- await new Promise(resolve => setTimeout(resolve, 10));
- const newContext = await getFullContext(nextArticle, tweetId, apiKey);
- if (!newContext) {
- throw new Error("Failed to get context for next article");
- }
- threadHist = threadHist + "\n[REPLY]\n" + newContext;
- }
- conversation.dataset.threadHist = threadHist;
- }
- setTimeout(() => {
- mapThreadStructure(conversation, rootTweetId);
- }, 500);
- } catch (error) {
- threadMappingInProgress = false;
- if (conversation.firstChild) {
- delete conversation.firstChild.dataset.canary;
- }
- }
- } else if (!threadMappingInProgress && !conversation.dataset.threadMappingInProgress) {
- threadMappingInProgress = true;
- setTimeout(() => {
- mapThreadStructure(conversation, rootTweetId);
- }, 250);
- }
- } catch (error) {
- threadMappingInProgress = false;
- }
- }
- async function mapThreadStructure(conversation, localRootTweetId) {
- conversation.dataset.threadMappingInProgress = "true";
- conversation.dataset.threadMappedAt = Date.now().toString();
- threadMappingInProgress = true;
- try {
- const timeout = new Promise((_, reject) =>
- setTimeout(() => reject(new Error('Thread mapping timed out')), 5000)
- );
- const mapping = async () => {
- let cellDivs = Array.from(document.querySelectorAll('div[data-testid="cellInnerDiv"]'));
- if (!cellDivs.length) {
- delete conversation.dataset.threadMappingInProgress;
- threadMappingInProgress = false;
- return;
- }
- let tweetCells = [];
- let processedCount = 0;
- for (let idx = 0; idx < cellDivs.length; idx++) {
- const cell = cellDivs[idx];
- const article = cell.querySelector('article[data-testid="tweet"]');
- if (!article) continue;
- try {
- let tweetId = getTweetID(article);
- if (!tweetId) {
- let tweetLink = article.querySelector('a[href*="/status/"]');
- if (tweetLink) {
- let match = tweetLink.href.match(/status\/(\d+)/);
- if (match) tweetId = match[1];
- }
- }
- if (!tweetId) continue;
- const handles = getUserHandles(article);
- let username = handles.length > 0 ? handles[0] : null;
- if (!username) continue;
- let tweetTextSpan = article.querySelector('[data-testid="tweetText"]');
- let text = tweetTextSpan ? tweetTextSpan.innerText.trim().replace(/\n+/g, ' ⏎ ') : '';
- let mediaLinks = await extractMediaLinks(article);
- let quotedMediaLinks = [];
- const quoteContainer = article.querySelector(QUOTE_CONTAINER_SELECTOR);
- if (quoteContainer) {
- quotedMediaLinks = await extractMediaLinks(quoteContainer);
- }
- let prevCell = cellDivs[idx - 1] || null;
- let isReplyToRoot = false;
- if (prevCell && prevCell.childElementCount === 1) {
- let onlyChild = prevCell.children[0];
- if (onlyChild && onlyChild.children.length === 0 && onlyChild.innerHTML.trim() === '') {
- isReplyToRoot = true;
- }
- }
- tweetCells.push({
- tweetNode: article,
- username,
- tweetId,
- text,
- mediaLinks,
- quotedMediaLinks,
- cellIndex: idx,
- isReplyToRoot,
- cellDiv: cell,
- index: processedCount++
- });
- if (!processedTweets.has(tweetId)) {
- scheduleTweetProcessing(article);
- }
- } catch (err) {
- continue;
- }
- }
- if (tweetCells.length === 0) {
- delete conversation.dataset.threadMappingInProgress;
- threadMappingInProgress = false;
- return;
- }
- for (let i = 0; i < tweetCells.length; ++i) {
- let tw = tweetCells[i];
- const persistentRelation = threadRelationships[tw.tweetId];
- if (tw.tweetId === localRootTweetId) {
- tw.replyTo = null;
- tw.replyToId = null;
- tw.isRoot = true;
- } else if (persistentRelation && persistentRelation.replyTo) {
- tw.replyTo = persistentRelation.to;
- tw.replyToId = persistentRelation.replyTo;
- tw.isRoot = false;
- } else if (tw.isReplyToRoot) {
- let root = tweetCells.find(tk => tk.tweetId === localRootTweetId);
- tw.replyTo = root ? root.username : null;
- tw.replyToId = root ? root.tweetId : null;
- tw.isRoot = false;
- } else if (i > 0) {
- tw.replyTo = tweetCells[i - 1].username;
- tw.replyToId = tweetCells[i - 1].tweetId;
- tw.isRoot = false;
- } else {
- tw.replyTo = null;
- tw.replyToId = null;
- tw.isRoot = false;
- }
- }
- const replyDocs = tweetCells.map(tw => ({
- from: tw.username,
- tweetId: tw.tweetId,
- to: tw.replyTo,
- toId: tw.replyToId,
- isRoot: tw.isRoot === true,
- text: tw.text,
- mediaLinks: tw.mediaLinks || [],
- quotedMediaLinks: tw.quotedMediaLinks || []
- }));
- for (let tw of tweetCells) {
- if (!tw.replyToId && !tw.isRoot && threadRelationships[tw.tweetId]?.replyTo) {
- tw.replyToId = threadRelationships[tw.tweetId].replyTo;
- tw.replyTo = threadRelationships[tw.tweetId].to;
- const doc = replyDocs.find(d => d.tweetId === tw.tweetId);
- if (doc) {
- doc.toId = tw.replyToId;
- doc.to = tw.replyTo;
- }
- }
- }
- conversation.dataset.threadMapping = JSON.stringify(replyDocs);
- const timestamp = Date.now();
- replyDocs.forEach(doc => {
- if (doc.tweetId && doc.toId) {
- threadRelationships[doc.tweetId] = {
- replyTo: doc.toId,
- from: doc.from,
- to: doc.to,
- isRoot: false,
- timestamp
- };
- } else if (doc.tweetId && doc.isRoot) {
- threadRelationships[doc.tweetId] = {
- replyTo: null,
- from: doc.from,
- isRoot: true,
- timestamp
- };
- }
- });
- saveThreadRelationships();
- let completeThreadHistory = "";
- const rootTweet = replyDocs.find(t => t.isRoot === true);
- if (rootTweet && rootTweet.tweetId) {
- const rootTweetElement = tweetCells.find(t => t.tweetId === rootTweet.tweetId)?.tweetNode;
- if (rootTweetElement) {
- try {
- const apiKey = browserGet('openrouter-api-key', '');
- const rootContext = await getFullContext(rootTweetElement, rootTweet.tweetId, apiKey);
- if (rootContext) {
- completeThreadHistory = rootContext;
- conversation.dataset.threadHist = completeThreadHistory;
- const allMediaUrls = [];
- replyDocs.forEach(doc => {
- if (doc.mediaLinks && doc.mediaLinks.length) {
- allMediaUrls.push(...doc.mediaLinks);
- }
- if (doc.quotedMediaLinks && doc.quotedMediaLinks.length) {
- allMediaUrls.push(...doc.quotedMediaLinks);
- }
- });
- if (allMediaUrls.length > 0) {
- conversation.dataset.threadMediaUrls = JSON.stringify(allMediaUrls);
- }
- }
- } catch (error) {
- }
- }
- }
- const batchSize = 10;
- for (let i = 0; i < replyDocs.length; i += batchSize) {
- const batch = replyDocs.slice(i, i + batchSize);
- batch.forEach(doc => {
- if (doc.tweetId && tweetCache.has(doc.tweetId)) {
- tweetCache.get(doc.tweetId).threadContext = {
- replyTo: doc.to,
- replyToId: doc.toId,
- isRoot: doc.isRoot,
- threadMediaUrls: doc.isRoot ? [] : getAllPreviousMediaUrls(doc.tweetId, replyDocs)
- };
- if (doc.tweetId && processedTweets.has(doc.tweetId)) {
- const tweetCell = tweetCells.find(tc => tc.tweetId === doc.tweetId);
- if (tweetCell && tweetCell.tweetNode) {
- const isStreaming = tweetCell.tweetNode.dataset.ratingStatus === 'streaming' ||
- (tweetCache.has(doc.tweetId) && tweetCache.get(doc.tweetId).streaming === true);
- if (!isStreaming) {
- processedTweets.delete(doc.tweetId);
- scheduleTweetProcessing(tweetCell.tweetNode);
- }
- }
- }
- }
- });
- if (i + batchSize < replyDocs.length) {
- await new Promise(resolve => setTimeout(resolve, 0));
- }
- }
- delete conversation.dataset.threadMappingInProgress;
- threadMappingInProgress = false;
- };
- function getAllPreviousMediaUrls(tweetId, replyDocs) {
- const allMediaUrls = [];
- const index = replyDocs.findIndex(doc => doc.tweetId === tweetId);
- if (index > 0) {
- for (let i = 0; i < index; i++) {
- if (replyDocs[i].mediaLinks && replyDocs[i].mediaLinks.length) {
- allMediaUrls.push(...replyDocs[i].mediaLinks);
- }
- if (replyDocs[i].quotedMediaLinks && replyDocs[i].quotedMediaLinks.length) {
- allMediaUrls.push(...replyDocs[i].quotedMediaLinks);
- }
- }
- }
- return allMediaUrls;
- }
- await Promise.race([mapping(), timeout]);
- } catch (error) {
- delete conversation.dataset.threadMappedAt;
- delete conversation.dataset.threadMappingInProgress;
- threadMappingInProgress = false;
- }
- }
- function getTweetReplyInfo(tweetId) {
- if (threadRelationships[tweetId]) {
- return threadRelationships[tweetId];
- }
- return null;
- }
- setInterval(handleThreads, THREAD_CHECK_INTERVAL);
- setInterval(ensureAllTweetsRated, SWEEP_INTERVAL);
- setInterval(applyFilteringToAll, SWEEP_INTERVAL);
- // ----- api/api_requests.js -----
- async function getCompletion(request, apiKey, timeout = 30000) {
- return new Promise((resolve) => {
- GM_xmlhttpRequest({
- method: "POST",
- url: "https://openrouter.ai/api/v1/chat/completions",
- headers: {
- "Content-Type": "application/json",
- "Authorization": `Bearer ${apiKey}`,
- "HTTP-Referer": "https://greasyfork.org/en/scripts/532459-tweetfilter-ai",
- "X-Title": "TweetFilter-AI"
- },
- data: JSON.stringify(request),
- timeout: timeout,
- onload: function (response) {
- if (response.status >= 200 && response.status < 300) {
- try {
- const data = JSON.parse(response.responseText);
- if (data.content==="") {
- resolve({
- error: true,
- message: `No content returned${data.choices[0].native_finish_reason=="SAFETY"?" (SAFETY FILTER)":""}`,
- data: data
- });
- }
- resolve({
- error: false,
- message: "Request successful",
- data: data
- });
- } catch (error) {
- resolve({
- error: true,
- message: `Failed to parse response: ${error.message}`,
- data: null
- });
- }
- } else {
- resolve({
- error: true,
- message: `Request failed with status ${response.status}: ${response.responseText}`,
- data: null
- });
- }
- },
- onerror: function (error) {
- resolve({
- error: true,
- message: `Request error: ${error.toString()}`,
- data: null
- });
- },
- ontimeout: function () {
- resolve({
- error: true,
- message: `Request timed out after ${timeout}ms`,
- data: null
- });
- }
- });
- });
- }
- function getCompletionStreaming(request, apiKey, onChunk, onComplete, onError, timeout = 90000, tweetId = null) {
- const streamingRequest = {
- ...request,
- stream: true
- };
- let fullResponse = "";
- let content = "";
- let reasoning = "";
- let responseObj = null;
- let streamComplete = false;
- const reqObj = GM_xmlhttpRequest({
- method: "POST",
- url: "https://openrouter.ai/api/v1/chat/completions",
- headers: {
- "Content-Type": "application/json",
- "Authorization": `Bearer ${apiKey}`,
- "HTTP-Referer": "https://greasyfork.org/en/scripts/532459-tweetfilter-ai",
- "X-Title": "TweetFilter-AI"
- },
- data: JSON.stringify(streamingRequest),
- timeout: timeout,
- responseType: "stream",
- onloadstart: function(response) {
- const reader = response.response.getReader();
- const resetStreamTimeout = () => {
- if (streamTimeout) clearTimeout(streamTimeout);
- streamTimeout = setTimeout(() => {
- if (!streamComplete) {
- streamComplete = true;
- onComplete({
- content: content,
- reasoning: reasoning,
- fullResponse: fullResponse,
- data: responseObj,
- timedOut: true
- });
- }
- }, 30000);
- };
- let streamTimeout = null;
- const processStream = async () => {
- try {
- resetStreamTimeout()
- let isDone = false;
- let emptyChunksCount = 0;
- while (!isDone && !streamComplete) {
- const { done, value } = await reader.read();
- if (done) {
- isDone = true;
- break;
- }
- const chunk = new TextDecoder().decode(value);
- clearTimeout(streamTimeout);
- resetStreamTimeout();
- if (chunk.trim() === '') {
- emptyChunksCount++;
- if (emptyChunksCount >= 3) {
- isDone = true;
- break;
- }
- continue;
- }
- emptyChunksCount = 0;
- fullResponse += chunk;
- const lines = chunk.split("\n");
- for (const line of lines) {
- if (line.startsWith("data: ")) {
- const data = line.substring(6);
- if (data === "[DONE]") {
- isDone = true;
- break;
- }
- try {
- const parsed = JSON.parse(data);
- responseObj = parsed;
- if (parsed.choices && parsed.choices[0]) {
- if (parsed.choices[0].delta && parsed.choices[0].delta.content !== undefined) {
- const delta = parsed.choices[0].delta.content || "";
- content += delta;
- }
- if (parsed.choices[0].delta && parsed.choices[0].delta.reasoning !== undefined) {
- const reasoningDelta = parsed.choices[0].delta.reasoning || "";
- reasoning += reasoningDelta;
- }
- onChunk({
- chunk: parsed.choices[0].delta?.content || "",
- reasoningChunk: parsed.choices[0].delta?.reasoning || "",
- content: content,
- reasoning: reasoning,
- data: parsed
- });
- }
- } catch (e) {
- }
- }
- }
- }
- if (!streamComplete) {
- streamComplete = true;
- if (streamTimeout) clearTimeout(streamTimeout);
- if (tweetId && window.activeStreamingRequests) {
- delete window.activeStreamingRequests[tweetId];
- }
- onComplete({
- content: content,
- reasoning: reasoning,
- fullResponse: fullResponse,
- data: responseObj
- });
- }
- } catch (error) {
- if (streamTimeout) clearTimeout(streamTimeout);
- if (!streamComplete) {
- streamComplete = true;
- if (tweetId && window.activeStreamingRequests) {
- delete window.activeStreamingRequests[tweetId];
- }
- onError({
- error: true,
- message: `Stream processing error: ${error.toString()}`,
- data: null
- });
- }
- }
- };
- processStream().catch(error => {
- if (streamTimeout) clearTimeout(streamTimeout);
- if (!streamComplete) {
- streamComplete = true;
- if (tweetId && window.activeStreamingRequests) {
- delete window.activeStreamingRequests[tweetId];
- }
- onError({
- error: true,
- message: `Unhandled stream error: ${error.toString()}`,
- data: null
- });
- }
- });
- },
- onerror: function(error) {
- if (tweetId && window.activeStreamingRequests) {
- delete window.activeStreamingRequests[tweetId];
- }
- onError({
- error: true,
- message: `Request error: ${error.toString()}`,
- data: null
- });
- },
- ontimeout: function() {
- if (tweetId && window.activeStreamingRequests) {
- delete window.activeStreamingRequests[tweetId];
- }
- onError({
- error: true,
- message: `Request timed out after ${timeout}ms`,
- data: null
- });
- }
- });
- const streamingRequestObj = {
- abort: function() {
- streamComplete = true;
- pendingRequests--;
- try {
- reqObj.abort();
- } catch (e) {
- }
- if (tweetId && window.activeStreamingRequests) {
- delete window.activeStreamingRequests[tweetId];
- }
- if (tweetId && tweetCache.has(tweetId)) {
- const entry = tweetCache.get(tweetId);
- if (entry.streaming && (entry.score === undefined || entry.score === null)) {
- tweetCache.delete(tweetId);
- }
- }
- }
- };
- if (tweetId && window.activeStreamingRequests) {
- window.activeStreamingRequests[tweetId] = streamingRequestObj;
- }
- return streamingRequestObj;
- }
- function fetchAvailableModels() {
- const apiKey = browserGet('openrouter-api-key', '');
- if (!apiKey) {
- showStatus('Please enter your OpenRouter API key');
- return;
- }
- showStatus('Fetching available models...');
- const sortOrder = browserGet('modelSortOrder', 'throughput-high-to-low');
- GM_xmlhttpRequest({
- method: "GET",
- url: `https://openrouter.ai/api/frontend/models/find?order=${sortOrder}`,
- headers: {
- "Authorization": `Bearer ${apiKey}`,
- "HTTP-Referer": "https://greasyfork.org/en/scripts/532182-twitter-x-ai-tweet-filter",
- "X-Title": "Tweet Rating Tool"
- },
- onload: function (response) {
- try {
- const data = JSON.parse(response.responseText);
- if (data.data && data.data.models) {
- let filteredModels = data.data.models.filter(model => model.endpoint && model.endpoint !== null);
- filteredModels.forEach(model => {
- const endpointPricing = model.endpoint?.pricing;
- const isFree = !endpointPricing || (
- (endpointPricing.completion == null || parseFloat(endpointPricing.completion) === 0) &&
- (endpointPricing.prompt == null || parseFloat(endpointPricing.prompt) === 0)
- );
- if (isFree && model.slug && !model.slug.endsWith(':free')) {
- model.slug += ':free';
- }
- });
- if (sortOrder === 'latency-low-to-high'|| sortOrder === 'pricing-low-to-high') {
- filteredModels.reverse();
- }
- availableModels = filteredModels || [];
- listedModels = [...availableModels];
- refreshModelsUI();
- showStatus('Models updated!');
- }
- } catch (error) {
- showStatus('Error parsing models list');
- }
- },
- onerror: function (error) {
- showStatus('Error fetching models!');
- }
- });
- }
- async function getImageDescription(urls, apiKey, tweetId, userHandle) {
- if (!urls?.length || !enableImageDescriptions) {
- return !enableImageDescriptions ? '[Image descriptions disabled]' : '';
- }
- let descriptions = [];
- for (const url of urls) {
- const request = {
- model: selectedImageModel,
- messages: [{
- role: "user",
- content: [
- {
- type: "text",
- text: "Describe what you see in this image in a concise way, focusing on the main elements and any text visible. Keep the description under 100 words."
- },
- {
- type: "image_url",
- image_url: { url }
- }
- ]
- }],
- temperature: imageModelTemperature,
- top_p: imageModelTopP,
- max_tokens: maxTokens,
- };
- if (selectedImageModel.includes('gemini')) {
- request.config = {
- safetySettings: safetySettings,
- }
- }
- if (providerSort) {
- request.provider = {
- sort: providerSort,
- allow_fallbacks: true
- };
- }
- const result = await getCompletion(request, apiKey);
- if (!result.error && result.data?.choices?.[0]?.message?.content) {
- descriptions.push(result.data.choices[0].message.content);
- } else {
- descriptions.push('[Error getting image description]');
- }
- }
- return descriptions.map((desc, i) => `[IMAGE ${i + 1}]: ${desc}`).join('\n');
- }
- async function getGenerationMetadata(generationId, apiKey, timeout = 10000) {
- return new Promise((resolve) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: `https://openrouter.ai/api/v1/generation?id=${generationId}`,
- headers: {
- "Authorization": `Bearer ${apiKey}`,
- "HTTP-Referer": "https://greasyfork.org/en/scripts/532459-tweetfilter-ai", // Use your script's URL
- "X-Title": "TweetFilter-AI" // Replace with your script's name
- },
- timeout: timeout,
- onload: function(response) {
- if (response.status >= 200 && response.status < 300) {
- try {
- const data = JSON.parse(response.responseText);
- resolve({
- error: false,
- message: "Metadata fetched successfully",
- data: data // The structure is { data: { ...metadata... } }
- });
- } catch (error) {
- resolve({
- error: true,
- message: `Failed to parse metadata response: ${error.message}`,
- data: null
- });
- }
- } else if (response.status === 404) {
- resolve({
- error: true,
- status: 404, // Indicate not found specifically for retry logic
- message: `Generation metadata not found (404): ${response.responseText}`,
- data: null
- });
- } else {
- resolve({
- error: true,
- status: response.status,
- message: `Metadata request failed with status ${response.status}: ${response.responseText}`,
- data: null
- });
- }
- },
- onerror: function(error) {
- resolve({
- error: true,
- message: `Metadata request error: ${error.toString()}`,
- data: null
- });
- },
- ontimeout: function() {
- resolve({
- error: true,
- message: `Metadata request timed out after ${timeout}ms`,
- data: null
- });
- }
- });
- });
- }
- // ----- api/api.js -----
- const safetySettings = [
- {
- category: "HARM_CATEGORY_HARASSMENT",
- threshold: "BLOCK_NONE",
- },
- {
- category: "HARM_CATEGORY_HATE_SPEECH",
- threshold: "BLOCK_NONE",
- },
- {
- category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
- threshold: "BLOCK_NONE",
- },
- {
- category: "HARM_CATEGORY_DANGEROUS_CONTENT",
- threshold: "BLOCK_NONE",
- },
- {
- category: "HARM_CATEGORY_CIVIC_INTEGRITY",
- threshold: "BLOCK_NONE",
- },
- ];
- function extractFollowUpQuestions(content) {
- if (!content) return [];
- const questions = [];
- const q1Marker = "Q_1.";
- const q2Marker = "Q_2.";
- const q3Marker = "Q_3.";
- const q1Start = content.indexOf(q1Marker);
- const q2Start = content.indexOf(q2Marker);
- const q3Start = content.indexOf(q3Marker);
- if (q1Start !== -1 && q2Start > q1Start && q3Start > q2Start) {
- const q1Text = content.substring(q1Start + q1Marker.length, q2Start).trim();
- questions.push(q1Text);
- const q2Text = content.substring(q2Start + q2Marker.length, q3Start).trim();
- questions.push(q2Text);
- let q3Text = content.substring(q3Start + q3Marker.length).trim();
- const endMarker = "</FOLLOW_UP_QUESTIONS>";
- if (q3Text.endsWith(endMarker)) {
- q3Text = q3Text.substring(0, q3Text.length - endMarker.length).trim();
- }
- questions.push(q3Text);
- if (questions.every(q => q.length > 0)) {
- return questions;
- }
- }
- return [];
- }
- async function rateTweetWithOpenRouter(tweetText, tweetId, apiKey, mediaUrls, maxRetries = 3, tweetArticle = null, authorHandle="") {
- const cleanupRequest = () => {
- pendingRequests = Math.max(0, pendingRequests - 1);
- showStatus(`Rating tweet... (${pendingRequests} pending)`);
- };
- const indicatorInstance = ScoreIndicatorRegistry.get(tweetId, tweetArticle);
- if (!indicatorInstance) {
- return {
- score: 5, // Default error score
- content: "Failed to initialize UI components for rating.",
- reasoning: "",
- questions: [],
- lastAnswer: "",
- error: true,
- cached: false,
- data: null,
- qaConversationHistory: [] // Empty history
- };
- }
- if (adAuthorCache.has(authorHandle)) {
- indicatorInstance.updateInitialReviewAndBuildHistory({
- fullContext: tweetText, // or a specific ad message
- mediaUrls: [],
- apiResponseContent: "<ANALYSIS>This tweet is from an ad author.</ANALYSIS><SCORE>SCORE_0</SCORE><FOLLOW_UP_QUESTIONS>Q_1. N/A\\nQ_2. N/A\\nQ_3. N/A</FOLLOW_UP_QUESTIONS>",
- reviewSystemPrompt: REVIEW_SYSTEM_PROMPT, // Globally available from config.js
- followUpSystemPrompt: FOLLOW_UP_SYSTEM_PROMPT // Globally available from config.js
- });
- return {
- score: 0,
- content: indicatorInstance.description,
- reasoning: "",
- error: false,
- cached: false,
- questions: indicatorInstance.questions,
- qaConversationHistory: indicatorInstance.qaConversationHistory
- };
- }
- const currentInstructions = instructionsManager.getCurrentInstructions();
- const requestBody = {
- model: selectedModel,
- messages: [
- {
- role: "system",
- content: [{ type: "text", text: REVIEW_SYSTEM_PROMPT}]
- },
- {
- role: "user",
- content: [
- {
- type: "text",
- text: `<TARGET_TWEET_ID>[${tweetId}]</TARGET_TWEET_ID>
- <USER_INSTRUCTIONS>[${currentInstructions}]</USER_INSTRUCTIONS>
- <TWEET>[${tweetText}]</TWEET>
- Follow this expected response format exactly, or you break the UI:
- <EXPECTED_RESPONSE_FORMAT>
- <ANALYSIS>
- (Your analysis according to the user instructions.)
- </ANALYSIS>
- <SCORE>
- SCORE_X (Where X is a number between 0 and 10)
- </SCORE>
- <FOLLOW_UP_QUESTIONS>
- Q_1. …
- Q_2. …
- Q_3. …
- </FOLLOW_UP_QUESTIONS>
- </EXPECTED_RESPONSE_FORMAT>
- `
- }
- ]
- }
- ],
- temperature: modelTemperature,
- top_p: modelTopP,
- max_tokens: maxTokens
- };
- if (selectedModel.includes('gemini')) {
- requestBody.config = { safetySettings: safetySettings };
- }
- if (mediaUrls?.length > 0 && modelSupportsImages(selectedModel)) {
- mediaUrls.forEach(url => {
- requestBody.messages[1].content.push({
- type: "image_url",
- image_url: { "url": url }
- });
- });
- }
- if (providerSort) {
- requestBody.provider = { sort: providerSort, allow_fallbacks: true };
- }
- const useStreaming = browserGet('enableStreaming', false);
- tweetCache.set(tweetId, {
- streaming: true,
- timestamp: Date.now(),
- tweetContent: tweetText, // Store original tweet text for context
- mediaUrls: mediaUrls // Store original media URLs
- });
- let attempt = 0;
- while (attempt < maxRetries) {
- attempt++;
- const now = Date.now();
- const timeElapsed = now - lastAPICallTime;
- if (timeElapsed < API_CALL_DELAY_MS) {
- await new Promise(resolve => setTimeout(resolve, API_CALL_DELAY_MS - timeElapsed));
- }
- lastAPICallTime = now;
- pendingRequests++;
- showStatus(`Rating tweet... (${pendingRequests} pending)`);
- try {
- let result;
- if (useStreaming) {
- result = await rateTweetStreaming(requestBody, apiKey, tweetId, tweetText, tweetArticle);
- } else {
- result = await rateTweet(requestBody, apiKey);
- }
- cleanupRequest();
- if (!result.error && result.content) {
- indicatorInstance.updateInitialReviewAndBuildHistory({
- fullContext: tweetText, // The full text of the tweet that was rated
- mediaUrls: mediaUrls, // The media URLs associated with that tweet
- apiResponseContent: result.content,
- reviewSystemPrompt: REVIEW_SYSTEM_PROMPT,
- followUpSystemPrompt: FOLLOW_UP_SYSTEM_PROMPT
- });
- const finalScore = indicatorInstance.score;
- const finalQuestions = indicatorInstance.questions;
- const finalDescription = indicatorInstance.description;
- const finalQaHistory = indicatorInstance.qaConversationHistory;
- tweetCache.set(tweetId, {
- score: finalScore,
- description: finalDescription, // Analysis
- reasoning: result.reasoning || "", // If rateTweet/Streaming provide it separately
- questions: finalQuestions,
- lastAnswer: "",
- tweetContent: tweetText,
- mediaUrls: mediaUrls,
- streaming: false,
- timestamp: Date.now(),
- metadata: result.data?.id ? { generationId: result.data.id } : null,
- qaConversationHistory: finalQaHistory // Store the history
- });
- return {
- score: finalScore,
- content: result.content, // Keep raw content for direct use if needed
- reasoning: result.reasoning || "",
- questions: finalQuestions,
- error: false,
- cached: false,
- data: result.data,
- qaConversationHistory: finalQaHistory
- };
- }
- if (attempt < maxRetries && (result.error || !result.content)) {
- const backoffDelay = Math.pow(attempt, 2) * 1000;
- await new Promise(resolve => setTimeout(resolve, backoffDelay));
- } else if (result.error || !result.content) {
- throw new Error(result.content || "Failed to get valid rating content after multiple attempts");
- }
- } catch (error) {
- cleanupRequest();
- if (attempt < maxRetries) {
- const backoffDelay = Math.pow(attempt, 2) * 1000;
- await new Promise(resolve => setTimeout(resolve, backoffDelay));
- } else {
- const errorContent = `Failed to get valid rating after multiple attempts: ${error.message}`;
- indicatorInstance.updateInitialReviewAndBuildHistory({
- fullContext: tweetText,
- mediaUrls: mediaUrls,
- apiResponseContent: `<ANALYSIS>${errorContent}</ANALYSIS><SCORE>SCORE_5</SCORE><FOLLOW_UP_QUESTIONS>Q_1. N/A\\nQ_2. N/A\\nQ_3. N/A</FOLLOW_UP_QUESTIONS>`,
- reviewSystemPrompt: REVIEW_SYSTEM_PROMPT,
- followUpSystemPrompt: FOLLOW_UP_SYSTEM_PROMPT
- });
- tweetCache.set(tweetId, {
- score: 5,
- description: errorContent,
- reasoning: "",
- questions: [],
- lastAnswer: "",
- error: true,
- tweetContent: tweetText,
- mediaUrls: mediaUrls,
- streaming: false,
- timestamp: Date.now(),
- qaConversationHistory: indicatorInstance.qaConversationHistory
- });
- return {
- score: 5,
- content: errorContent,
- reasoning: "",
- questions: [],
- lastAnswer: "",
- error: true,
- data: null,
- qaConversationHistory: indicatorInstance.qaConversationHistory
- };
- }
- }
- }
- cleanupRequest();
- const fallbackError = "Unexpected failure in rating process.";
- indicatorInstance.updateInitialReviewAndBuildHistory({
- fullContext: tweetText,
- mediaUrls: mediaUrls,
- apiResponseContent: `<ANALYSIS>${fallbackError}</ANALYSIS><SCORE>SCORE_5</SCORE><FOLLOW_UP_QUESTIONS>Q_1. N/A\\nQ_2. N/A\\nQ_3. N/A</FOLLOW_UP_QUESTIONS>`,
- reviewSystemPrompt: REVIEW_SYSTEM_PROMPT,
- followUpSystemPrompt: FOLLOW_UP_SYSTEM_PROMPT
- });
- return {
- score: 5,
- content: fallbackError,
- reasoning: "",
- questions: [],
- lastAnswer: "",
- error: true,
- data: null,
- qaConversationHistory: indicatorInstance.qaConversationHistory
- };
- }
- async function getCustomInstructionsDescription(instructions) {
- const INSTRUCTION_SUMMARY_MODEL = "google/gemini-2.5-flash-preview";
- const request={
- model: INSTRUCTION_SUMMARY_MODEL,
- messages: [{
- role: "system",
- content: [{
- type: "text",
- text: `
- Please come up with a 5-word summary of the following instructions.
- `
- }]
- },
- {
- role: "user",
- content: [{
- type: "text",
- text: `Please come up with a 5-word summary of the following instructions:
- ${instructions}
- `
- }]
- }]
- }
- let key = browserGet('openrouter-api-key');
- const result = await getCompletion(request,key);
- if (!result.error && result.data?.choices?.[0]?.message) {
- const content = result.data.choices[0].message.content || "";
- return {
- content,
- error: false,
- };
- }
- return {
- error: true,
- content: result.error || "Unknown error"
- };
- }
- async function rateTweet(request, apiKey) {
- const tweetId = request.tweetId;
- const existingScore = tweetCache.get(tweetId)?.score;
- const result = await getCompletion(request, apiKey);
- if (!result.error && result.data?.choices?.[0]?.message) {
- const content = result.data.choices[0].message.content || "";
- const reasoning = result.data.choices[0].message.reasoning || "";
- const scoreMatches = content.match(/SCORE_(\d+)/g);
- const score = existingScore || (scoreMatches && scoreMatches.length > 0
- ? parseInt(scoreMatches[scoreMatches.length - 1].match(/SCORE_(\d+)/)[1], 10)
- : null);
- tweetCache.set(tweetId, {
- score: score,
- description: content,
- tweetContent: request.tweetText,
- streaming: false
- });
- return {
- content,
- reasoning
- };
- }
- return {
- error: true,
- content: result.error || "Unknown error",
- reasoning: "",
- data: null
- };
- }
- async function rateTweetStreaming(request, apiKey, tweetId, tweetText, tweetArticle) {
- if (window.activeStreamingRequests && window.activeStreamingRequests[tweetId]) {
- window.activeStreamingRequests[tweetId].abort();
- delete window.activeStreamingRequests[tweetId];
- }
- const existingCache = tweetCache.get(tweetId);
- if (!existingCache || existingCache.score === undefined || existingCache.score === null) {
- tweetCache.set(tweetId, {
- streaming: true,
- timestamp: Date.now(),
- tweetContent: tweetText,
- description: "",
- reasoning: "",
- questions: [],
- lastAnswer: "",
- score: null
- });
- }
- return new Promise((resolve, reject) => {
- const indicatorInstance = ScoreIndicatorRegistry.get(tweetId, tweetArticle);
- if (!indicatorInstance) {
- if (tweetCache.has(tweetId)) {
- tweetCache.get(tweetId).streaming = false;
- tweetCache.get(tweetId).error = "Indicator initialization failed";
- }
- return reject(new Error(`ScoreIndicator instance could not be initialized for tweet ${tweetId}`));
- }
- let aggregatedContent = existingCache?.description || "";
- let aggregatedReasoning = existingCache?.reasoning || "";
- let aggregatedQuestions = existingCache?.questions || [];
- let finalData = null;
- let score = existingCache?.score || null;
- getCompletionStreaming(
- request,
- apiKey,
- (chunkData) => {
- aggregatedContent = chunkData.content || aggregatedContent;
- aggregatedReasoning = chunkData.reasoning || aggregatedReasoning;
- const scoreMatches = aggregatedContent.match(/SCORE_(\d+)/g);
- if (scoreMatches && scoreMatches.length > 0) {
- const lastScore = scoreMatches[scoreMatches.length - 1];
- score = parseInt(lastScore.match(/SCORE_(\d+)/)[1], 10);
- }
- indicatorInstance.update({
- status: 'streaming',
- score: score,
- description: aggregatedContent || "Rating in progress...",
- reasoning: aggregatedReasoning,
- questions: [],
- lastAnswer: ""
- });
- if (tweetCache.has(tweetId)) {
- const entry = tweetCache.get(tweetId);
- entry.description = aggregatedContent;
- entry.reasoning = aggregatedReasoning;
- entry.score = score;
- entry.streaming = true;
- }
- },
- (finalResult) => {
- aggregatedContent = finalResult.content || aggregatedContent;
- aggregatedReasoning = finalResult.reasoning || aggregatedReasoning;
- finalData = finalResult.data;
- const scoreMatches = aggregatedContent.match(/SCORE_(\d+)/g);
- if (scoreMatches && scoreMatches.length > 0) {
- const lastScore = scoreMatches[scoreMatches.length - 1];
- score = parseInt(lastScore.match(/SCORE_(\d+)/)[1], 10);
- }
- let finalStatus = 'rated';
- if (score === null || score === undefined) {
- finalStatus = 'error';
- score = 5;
- aggregatedContent += "\n[No score detected - Error]";
- }
- const finalCacheData = {
- tweetContent: tweetText,
- score: score,
- description: aggregatedContent,
- reasoning: aggregatedReasoning,
- streaming: false,
- timestamp: Date.now(),
- error: finalStatus === 'error' ? "No score detected" : undefined,
- metadata: finalData?.id ? { generationId: finalData.id } : null
- };
- tweetCache.set(tweetId, finalCacheData);
- indicatorInstance.update({
- status: finalStatus,
- score: score,
- description: aggregatedContent,
- reasoning: aggregatedReasoning,
- questions: extractFollowUpQuestions(aggregatedContent),
- lastAnswer: "",
- metadata: finalData?.id ? { generationId: finalData.id } : null
- });
- if (tweetArticle) {
- filterSingleTweet(tweetArticle);
- }
- const generationId = finalData?.id;
- if (generationId && apiKey) {
- fetchAndStoreGenerationMetadata(tweetId, generationId, apiKey, indicatorInstance);
- }
- resolve({
- score: score,
- content: aggregatedContent,
- reasoning: aggregatedReasoning,
- error: finalStatus === 'error',
- cached: false,
- data: finalData
- });
- },
- (errorData) => {
- indicatorInstance.update({
- status: 'error',
- score: 5,
- description: `Stream Error: ${errorData.message}`,
- reasoning: '',
- questions: [],
- lastAnswer: ''
- });
- if (tweetCache.has(tweetId)) {
- const entry = tweetCache.get(tweetId);
- entry.streaming = false;
- entry.error = errorData.message;
- entry.score = 5;
- entry.description = `Stream Error: ${errorData.message}`;
- }
- reject(new Error(errorData.message));
- },
- 30000,
- tweetId // Pass the tweet ID to associate with this request
- );
- });
- }
- async function fetchAndStoreGenerationMetadata(tweetId, generationId, apiKey, indicatorInstance, attempt = 0, delays = [1000, 500, 2000, 4000, 8000]) {
- if (attempt >= delays.length) {
- return;
- }
- const delay = delays[attempt];
- await new Promise(resolve => setTimeout(resolve, delay));
- try {
- const metadataResult = await getGenerationMetadata(generationId, apiKey);
- if (!metadataResult.error && metadataResult.data?.data) {
- const meta = metadataResult.data.data;
- const extractedMetadata = {
- model: meta.model || 'N/A',
- promptTokens: meta.tokens_prompt || 0,
- completionTokens: meta.tokens_completion || 0, // Use this for total completion output
- reasoningTokens: meta.native_tokens_reasoning || 0, // Specific reasoning tokens if available
- latency: meta.latency !== undefined ? (meta.latency / 1000).toFixed(2) + 's' : 'N/A', // Convert ms to s
- mediaInputs: meta.num_media_prompt || 0,
- price: meta.total_cost !== undefined ? `$${meta.total_cost.toFixed(6)}` : 'N/A' // Add total cost
- };
- const currentCache = tweetCache.get(tweetId);
- if (currentCache) {
- currentCache.metadata = extractedMetadata;
- tweetCache.set(tweetId, currentCache);
- indicatorInstance.update({ metadata: extractedMetadata });
- } else {
- }
- return;
- } else if (metadataResult.status === 404) {
- fetchAndStoreGenerationMetadata(tweetId, generationId, apiKey, indicatorInstance, attempt + 1, delays);
- } else {
- fetchAndStoreGenerationMetadata(tweetId, generationId, apiKey, indicatorInstance, attempt + 1, delays);
- }
- } catch (error) {
- fetchAndStoreGenerationMetadata(tweetId, generationId, apiKey, indicatorInstance, attempt + 1, delays);
- }
- }
- async function answerFollowUpQuestion(tweetId, qaHistoryForApiCall, apiKey, tweetArticle, indicatorInstance) {
- const questionText = qaHistoryForApiCall.find(m => m.role === 'user' && m === qaHistoryForApiCall[qaHistoryForApiCall.length - 1])?.content.find(c => c.type === 'text')?.text || "User's question";
- const useStreaming = browserGet('enableStreaming', false);
- const request = {
- model: selectedModel,//was using :online but the search results are irrelevant
- messages: qaHistoryForApiCall, // The entire history IS the messages array
- temperature: modelTemperature,
- top_p: modelTopP,
- max_tokens: maxTokens,
- stream: useStreaming
- };
- if (selectedModel.includes('gemini')) {
- request.config = { safetySettings: safetySettings };
- }
- if (providerSort) {
- request.provider = { sort: providerSort, allow_fallbacks: true };
- }
- try {
- let finalAnswerContent = "*Processing...*";
- let finalQaHistory = [...qaHistoryForApiCall];
- if (useStreaming) {
- await new Promise((resolve, reject) => {
- let aggregatedContent = "";
- getCompletionStreaming(
- request, apiKey,
- (chunkData) => {
- aggregatedContent = chunkData.content || aggregatedContent;
- indicatorInstance._renderStreamingAnswer(aggregatedContent, "");
- },
- (result) => {
- finalAnswerContent = result.content || aggregatedContent;
- const assistantMessage = { role: "assistant", content: [{ type: "text", text: finalAnswerContent }] };
- finalQaHistory.push(assistantMessage);
- indicatorInstance.updateAfterFollowUp({
- assistantResponseContent: finalAnswerContent,
- updatedQaHistory: finalQaHistory
- });
- const currentCache = tweetCache.get(tweetId) || {};
- currentCache.qaConversationHistory = finalQaHistory;
- const parsedAnswer = finalAnswerContent.match(/<ANSWER>([\s\S]*?)<\/ANSWER>/);
- currentCache.lastAnswer = parsedAnswer ? parsedAnswer[1].trim() : finalAnswerContent;
- currentCache.questions = extractFollowUpQuestions(finalAnswerContent);
- currentCache.timestamp = Date.now();
- tweetCache.set(tweetId, currentCache);
- resolve();
- },
- (error) => {
- const errorMessage = `Error generating answer: ${error.message}`;
- indicatorInstance._updateConversationHistory(questionText, errorMessage);
- indicatorInstance.questions = tweetCache.get(tweetId)?.questions || [];
- indicatorInstance._updateTooltipUI();
- const currentCache = tweetCache.get(tweetId) || {};
- currentCache.lastAnswer = errorMessage;
- currentCache.timestamp = Date.now();
- tweetCache.set(tweetId, currentCache);
- reject(new Error(error.message));
- },
- 60000,
- `followup-${tweetId}`
- );
- });
- } else {
- const result = await getCompletion(request, apiKey, 60000);
- if (result.error || !result.data?.choices?.[0]?.message?.content) {
- throw new Error(result.message || "Failed to get follow-up answer.");
- }
- finalAnswerContent = result.data.choices[0].message.content;
- const assistantMessage = { role: "assistant", content: [{ type: "text", text: finalAnswerContent }] };
- finalQaHistory.push(assistantMessage);
- indicatorInstance.updateAfterFollowUp({
- assistantResponseContent: finalAnswerContent,
- updatedQaHistory: finalQaHistory
- });
- const currentCache = tweetCache.get(tweetId) || {};
- currentCache.qaConversationHistory = finalQaHistory;
- const parsedAnswer = finalAnswerContent.match(/<ANSWER>([\s\S]*?)<\/ANSWER>/);
- currentCache.lastAnswer = parsedAnswer ? parsedAnswer[1].trim() : finalAnswerContent;
- currentCache.questions = extractFollowUpQuestions(finalAnswerContent);
- currentCache.timestamp = Date.now();
- tweetCache.set(tweetId, currentCache);
- }
- } catch (error) {
- const errorMessage = `Error answering question: ${error.message}`;
- indicatorInstance._updateConversationHistory(questionText, errorMessage);
- indicatorInstance.questions = tweetCache.get(tweetId)?.questions || [];
- indicatorInstance._updateTooltipUI();
- const currentCache = tweetCache.get(tweetId) || {};
- currentCache.lastAnswer = errorMessage;
- currentCache.timestamp = Date.now();
- tweetCache.set(tweetId, currentCache);
- }
- }
- // ----- twitter-desloppifier.js -----
- const VERSION = '1.5';
- (function () {
- 'use strict';
- let menuhtml = GM_getResourceText("MENU_HTML");
- browserSet('menuHTML', menuhtml);
- let firstRun = browserGet('firstRun', true);
- function initializeObserver() {
- const target = document.querySelector('main') || document.querySelector('div[data-testid="primaryColumn"]');
- if (target) {
- observedTargetNode = target;
- initialiseUI();
- if (firstRun) {
- resetSettings(true);
- browserSet('firstRun', false);
- }
- let apiKey = browserGet('openrouter-api-key', '');
- if(!apiKey){
- alert("No API Key found. Please enter your API Key in Settings > General.")
- }
- if (apiKey) {
- browserSet('openrouter-api-key', apiKey);
- showStatus(`Loaded ${tweetCache.size} cached ratings. Starting to rate visible tweets...`);
- fetchAvailableModels();
- }
- observedTargetNode.querySelectorAll(TWEET_ARTICLE_SELECTOR).forEach(scheduleTweetProcessing);
- applyFilteringToAll();
- const observer = new MutationObserver(handleMutations);
- observer.observe(observedTargetNode, { childList: true, subtree: true });
- window.addEventListener('beforeunload', () => {
- observer.disconnect();
- const sliderUI = document.getElementById('tweet-filter-container');
- if (sliderUI) sliderUI.remove();
- const settingsUI = document.getElementById('settings-container');
- if (settingsUI) settingsUI.remove();
- const statusIndicator = document.getElementById('status-indicator');
- if (statusIndicator) statusIndicator.remove();
- ScoreIndicatorRegistry.destroyAll();
- });
- } else {
- setTimeout(initializeObserver, 1000);
- }
- }
- initializeObserver();
- })();
- })();