// ==UserScript==
// @name TweetFilter AI
// @namespace http://tampermonkey.net/
// @version Version 1.4.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;}.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);}`;
// 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
}
};
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;
for (const tweetId in this.cache) {
const entry = this.cache[tweetId];
if (entry.score === undefined || entry.score === null) {
if (entry.streaming === true) {
streamingDeletedCount++;
} else {
undefinedScoreCount++;
}
delete this.cache[tweetId];
deletedCount++;
}
}
if (deletedCount > 0) {
this.debouncedSaveToStorage();
}
return {
beforeCount,
afterCount: this.size,
deletedCount,
streamingDeletedCount,
undefinedScoreCount
};
}
}
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 SYSTEM_PROMPT=`
<SYSTEM_INSTRUCTIONS>
<TASK_DESCRIPTION>
You are a tweet filtering AI. In short, you will be given a tweet to process. Your task has 3-parts.
First, carefully analyze the tweet. And determine how well it aligns with the user's instructions. Your analysis should align with and precisely follow the user's instructions.
Second, rate the tweet ONLY according to the extent that it aligns with the user's defined instructions.
Third, provide 3 follow up questions designed to spark further discussion about the tweet. Which the user may ask the AI. The questions should be discursory in nature.
Do NOT ask questions which you cannot answer, such as context related to the tweet author's other tweets. Your internet search function will be enabled when providing follow up questions.
Remember to ALWAYS follow the EXPECTED_RESPONSE_FORMAT EXACTLY. If you fail to respond with this format, the upstream processing pipeline will be unable to parse your response, leading to a crash.
</TASK_DESCRIPTION>
<EXPECTED_RESPONSE_FORMAT>
(Do not include (text enclosed in parenthesis) in your response. Parenthesisized text serves as guidelines. DO include everything else.)
[ANALYSIS]
(Your analysis of the tweet according to the user defined instructions)
[/ANALYSIS]
[SCORE]
SCORE_X (where X is a number from 0 to 10 for example: SCORE_0, SCORE_1, SCORE_2, SCORE_3, etc)
[/SCORE]
[FOLLOW_UP_QUESTIONS]
Q_1. (Question 1)
Q_2. (Question 2)
Q_3. (Question 3)
[/FOLLOW_UP_QUESTIONS]
</EXPECTED_RESPONSE_FORMAT>
</SYSTEM_INSTRUCTIONS>
`
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) {
const indicator = document.getElementById('status-indicator');
if (!indicator) {
return;
}
indicator.textContent = message;
indicator.classList.add('active');
setTimeout(() => { indicator.classList.remove('active'); }, 3000);
}
// ----- 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.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;
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('input');
this.customQuestionInput.type = 'text';
this.customQuestionInput.placeholder = 'Ask your own question...';
this.customQuestionInput.className = 'tooltip-custom-question-input';
this.customQuestionButton = document.createElement('button');
this.customQuestionButton.textContent = 'Ask';
this.customQuestionButton.className = 'tooltip-custom-question-button';
this.customQuestionContainer.appendChild(this.customQuestionInput);
this.customQuestionContainer.appendChild(this.customQuestionButton);
this.tooltipElement.appendChild(this.customQuestionContainer);
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();
}
});
}
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 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>
${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 button = event.target.closest('.follow-up-question-button');
if (!button) return;
event.stopPropagation();
const questionText = button.dataset.questionText;
const apiKey = browserGet('openrouter-api-key', '');
button.disabled = true;
button.textContent = `🤔 Asking: ${questionText}...`;
this.followUpQuestionsElement.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = true);
this.conversationHistory.push({ question: questionText, answer: 'pending' });
this._updateTooltipUI();
this.questions = [];
this._updateTooltipUI();
if (!apiKey) {
showStatus('API key missing. Cannot answer question.', 'error');
this._updateConversationHistory(questionText, "Error: API Key missing.", "");
button.disabled = false;
this.followUpQuestionsElement.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = false);
return;
}
if (!questionText) {
this._updateConversationHistory(questionText || "Error: Empty Question", "Error: Could not identify question.", "");
button.disabled = false;
this.followUpQuestionsElement.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = false);
return;
}
const cachedData = tweetCache.get(this.tweetId);
const mediaUrls = cachedData?.mediaUrls || [];
const currentArticle = this.findCurrentArticleElement();
try {
answerFollowUpQuestion(this.tweetId, questionText, apiKey, currentArticle, this, mediaUrls);
} finally {
setTimeout(() => {
if (this.followUpQuestionsElement) {
this.followUpQuestionsElement.querySelectorAll('.follow-up-question-button').forEach(btn => {
btn.disabled = false;
});
}
}, 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 apiKey = browserGet('openrouter-api-key', '');
this.customQuestionInput.disabled = true;
this.customQuestionButton.disabled = true;
this.customQuestionButton.textContent = 'Asking...';
this.followUpQuestionsElement?.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = true);
this.conversationHistory.push({ question: questionText, answer: 'pending' });
this.questions = [];
this._updateTooltipUI();
if (!apiKey) {
showStatus('API key missing. Cannot answer question.', 'error');
this._updateConversationHistory(questionText, "Error: API Key missing.", "");
this.customQuestionInput.disabled = false;
this.customQuestionButton.disabled = false;
this.customQuestionButton.textContent = 'Ask';
this.followUpQuestionsElement?.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = false);
return;
}
this.customQuestionInput.value = '';
const cachedData = tweetCache.get(this.tweetId);
const mediaUrls = cachedData?.mediaUrls || [];
const currentArticle = this.findCurrentArticleElement();
try {
answerFollowUpQuestion(this.tweetId, questionText, apiKey, currentArticle, this, mediaUrls);
} finally {
setTimeout(() => {
if (this.customQuestionInput) this.customQuestionInput.disabled = false;
if (this.customQuestionButton) {
this.customQuestionButton.disabled = false;
this.customQuestionButton.textContent = 'Ask';
}
if (this.followUpQuestionsElement) {
this.followUpQuestionsElement.querySelectorAll('.follow-up-question-button').forEach(btn => btn.disabled = false);
}
}, 100);
}
}
_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;
}
if (!(this.conversationHistory.length > 0 && this.conversationHistory[this.conversationHistory.length - 1].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;
}
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;
}
}
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';
}
}
function applyTweetCachedRating(tweetArticle) {
const tweetId = getTweetID(tweetArticle);
const handles = getUserHandles(tweetArticle);
const userHandle = handles.length > 0 ? handles[0] : '';
if (userHandle && isUserBlacklisted(userHandle)) {
tweetArticle.dataset.sloppinessScore = '10';
tweetArticle.dataset.blacklisted = 'true';
tweetArticle.dataset.ratingStatus = 'blacklisted';
tweetArticle.dataset.ratingDescription = 'Whitelisted user';
ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
status: 'blacklisted',
score: 10,
description: "User is whitelisted",
questions: [], // No questions for blacklist
lastAnswer: "" // No answer for blacklist
});
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 score = cachedRating.score;
const desc = cachedRating.description;
const reasoning = cachedRating.reasoning || "";
const questions = cachedRating.questions || [];
const lastAnswer = cachedRating.lastAnswer || "";
const mediaUrls = cachedRating.mediaUrls || [];
tweetArticle.dataset.sloppinessScore = score.toString();
tweetArticle.dataset.cachedRating = 'true';
if (reasoning) {
tweetArticle.dataset.ratingReasoning = reasoning;
}
let status = 'rated';
const isFromStorage = cachedRating.fromStorage === true;
status = isFromStorage ? 'cached' : 'rated';
const metadata = cachedRating.metadata || null;
ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
status: status,
score: score,
description: desc,
reasoning: reasoning,
questions: questions, // Pass questions to indicator
lastAnswer: lastAnswer, // Pass answer to indicator
metadata: metadata, // Pass metadata to indicator
mediaUrls: mediaUrls // Pass mediaUrls to indicator
});
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);
}
}
}
}
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)) {
tweetArticle.dataset.sloppinessScore = '10';
tweetArticle.dataset.blacklisted = 'true';
tweetArticle.dataset.ratingStatus = 'blacklisted';
tweetArticle.dataset.ratingDescription = "Whitelisted user";
ScoreIndicatorRegistry.get(tweetId, tweetArticle)?.update({
status: 'blacklisted',
score: 10,
description: "User is whitelisted",
questions: [],
lastAnswer: ""
});
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 = 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 localRootTweetId = match ? match[1] : null;
if (!localRootTweetId) return;
if (conversation.dataset.threadHist === undefined) {
threadHist = "";
const firstArticle = document.querySelector('article[data-testid="tweet"]');
if (firstArticle) {
conversation.dataset.threadHist = 'pending';
threadMappingInProgress = true;
try {
const tweetId = getTweetID(firstArticle);
if (!tweetId) {
throw new Error("Failed to get tweet ID from first article");
}
const apiKey = browserGet('openrouter-api-key', '');
const fullcxt = await getFullContext(firstArticle, 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(firstArticle);
}
setTimeout(() => {
mapThreadStructure(conversation, localRootTweetId);
}, 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, localRootTweetId);
}, 500);
} catch (error) {
threadMappingInProgress = false;
if (conversation.firstChild) {
delete conversation.firstChild.dataset.canary;
}
}
} else if (!threadMappingInProgress && !conversation.dataset.threadMappingInProgress) {
threadMappingInProgress = true;
setTimeout(() => {
mapThreadStructure(conversation, localRootTweetId);
}, 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://gf.qytechs.cn/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://gf.qytechs.cn/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://gf.qytechs.cn/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://gf.qytechs.cn/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)`);
};
if (adAuthorCache.has(authorHandle)) {
return {
score: 0,
content: "This tweet is from an ad author.",
reasoning: "",
error: false,
cached: false,
};
}
const currentInstructions = instructionsManager.getCurrentInstructions();
const request = {
model: selectedModel,
messages: [{
role: "system",
content: [{
type: "text",
text: `
${SYSTEM_PROMPT}`
},]
},
{
role: "user",
content: [{
type: "text",
text:
`<TARGET_TWEET_ID_TO_RATE>
[${tweetId}].
</TARGET_TWEET_ID_TO_RATE>
<USER_INSTRUCTIONS:>
[${currentInstructions}]
</USER INSTURCTIONS>
<TWEET>
_______BEGIN TWEET CONTEXT_______
[${tweetText}]
</TWEET>
<EXPECTED_RESPONSE_FORMAT>
(Do not include (text enclosed in parenthesis) in your response. Parenthesisized text serves as guidelines. DO include everything else.)
[ANALYSIS]
(Your analysis of the tweet according to the user defined instructions)
[/ANALYSIS]
[SCORE]
SCORE_X (where X is a number from 0 to 10 for example: SCORE_0, SCORE_1, SCORE_2, SCORE_3, etc)
[/SCORE]
[FOLLOW_UP_QUESTIONS]
Q_1. (Question 1)
Q_2. (Question 2)
Q_3. (Question 3)
[/FOLLOW_UP_QUESTIONS]
</EXPECTED_RESPONSE_FORMAT>
`
}]
}]
};
if (selectedModel.includes('gemini')) {
request.config = {
safetySettings: safetySettings,
};
}
if (mediaUrls?.length > 0 && modelSupportsImages(selectedModel)) {
for (const url of mediaUrls) {
request.messages[1].content.push({
type: "image_url",
image_url: { "url": url }
});
}
}
request.temperature = modelTemperature;
request.top_p = modelTopP;
request.max_tokens = maxTokens;
if (providerSort) {
request.provider = {
sort: providerSort,
allow_fallbacks: true
};
}
const useStreaming = browserGet('enableStreaming', false);
tweetCache.set(tweetId, {
streaming: true,
timestamp: Date.now()
});
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(request, apiKey, tweetId, tweetText, tweetArticle);
} else {
result = await rateTweet(request, apiKey);
}
cleanupRequest();
if (!result.error && result.content) {
const scoreMatches = result.content.match(/SCORE_(\d+)/g);
if (scoreMatches && scoreMatches.length > 0) {
const lastScore = scoreMatches[scoreMatches.length - 1];
const score = parseInt(lastScore.match(/SCORE_(\d+)/)[1], 10);
tweetCache.set(tweetId, {
score: score,
description: result.content,
reasoning: result.reasoning,
questions: extractFollowUpQuestions(result.content),
lastAnswer: "",
tweetContent: tweetText,
mediaUrls: mediaUrls,
streaming: false,
timestamp: Date.now(),
metadata: result.data?.id ? { generationId: result.data.id } : null
});
return {
score,
content: result.content,
reasoning: result.reasoning,
questions: extractFollowUpQuestions(result.content),
lastAnswer: "",
error: false,
cached: false,
data: result.data
};
}
}
if (attempt < maxRetries) {
const backoffDelay = Math.pow(attempt, 2) * 1000;
await new Promise(resolve => setTimeout(resolve, backoffDelay));
}
} catch (error) {
cleanupRequest();
if (attempt < maxRetries) {
const backoffDelay = Math.pow(attempt, 2) * 1000;
await new Promise(resolve => setTimeout(resolve, backoffDelay));
}
}
}
cleanupRequest();
return {
score: 5,
content: "Failed to get valid rating after multiple attempts",
reasoning: "",
questions: [],
lastAnswer: "",
error: true,
data: null
};
}
async function getCustomInstructionsDescription(instructions) {
const request={
model: selectedModel,
messages: [{
role: "system",
content: [{
type: "text",
text: `
Please come up with a 5-word summary of the following instructions:
${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, questionText, apiKey, tweetArticle, indicatorInstance, mediaUrls = []) {
const useStreaming = browserGet('enableStreaming', false);
const cachedData = tweetCache.get(tweetId);
let originalContext = cachedData?.tweetContent || null;
const originalScore = cachedData?.score;
const originalDescription = cachedData?.description;
if (!originalContext && tweetArticle) {
try {
originalContext = await getFullContext(tweetArticle, tweetId, apiKey);
if (originalContext && cachedData) {
cachedData.tweetContent = originalContext;
tweetCache.set(tweetId, cachedData);
} else if (originalContext && !cachedData) {
tweetCache.set(tweetId, {
tweetContent: originalContext,
timestamp: Date.now()
});
}
} catch (scrapeError) {
indicatorInstance.update({
lastAnswer: `Error: Could not retrieve original tweet context to answer question.`
});
return;
}
}
if (!originalContext) {
indicatorInstance.update({
lastAnswer: `Error: Original tweet context unavailable.`
});
return;
}
let conversationHistory = '';
const tooltip = document.querySelector(`.score-description[data-tweet-id="${tweetId}"]`);
if (tooltip) {
const historyContainer = tooltip.querySelector('.tooltip-conversation-history');
if (historyContainer) {
const turns = historyContainer.querySelectorAll('.conversation-turn');
conversationHistory = Array.from(turns).map((turn, index) => {
const question = turn.querySelector('.conversation-question')?.textContent?.replace('You:', '').trim() || '';
const answer = turn.querySelector('.conversation-answer')?.textContent?.replace('AI:', '').trim() || '';
return `Q${index + 1}: ${question}\nA${index + 1}: ${answer}`;
}).join('\n\n');
}
}
const followUpPrompt = `
You are answering a follow-up question about a specific tweet.
Here is the original tweet context:
_______BEGIN TWEET CONTEXT_______
${originalContext}
_______END TWEET CONTEXT_______
${originalScore !== undefined && originalDescription ? `
Here is the original AI rating and description for the tweet:
SCORE: ${originalScore}
DESCRIPTION:
${originalDescription}
---------------------------------
` : ''}
${conversationHistory ? `CONVERSATION HISTORY:
${conversationHistory}
---------------------------------
` : ''}Current follow-up question: "${questionText}"
Please consider the ENTIRE conversation history above when formulating your response. Your answer should build upon and be consistent with previous answers, and acknowledge any relevant information that was discussed in earlier exchanges.
Answer the user's question concisely and accurately.
After answering, provide 3 new, relevant follow-up questions the user might have based on your answer or the ongoing conversation context.
Follow this format exactly:
[ANSWER]
[Your answer here]
[3 FOLLOW UP Questions]
Q_1. [New Question 1]
Q_2. [New Question 2]
Q_3. [New Question 3]
`;
const request = {
model: `${selectedModel}:online`, // Use the same model as the initial rating
messages: [
{
role: "user",
content: [{ type: "text", text: followUpPrompt }]
}
],
temperature: modelTemperature,
top_p: modelTopP,
max_tokens: maxTokens,
stream: useStreaming // Set stream based on user setting
};
if (mediaUrls?.length > 0 && modelSupportsImages(selectedModel)) {
for (const url of mediaUrls) {
request.messages[0].content.push({
type: "image_url",
image_url: { "url": url }
});
}
}
if (selectedModel.includes('gemini')) {
request.config = { safetySettings: safetySettings };
}
if (providerSort) {
request.provider = { sort: providerSort, allow_fallbacks: true };
}
indicatorInstance.update({
lastAnswer: `*Answering "${questionText}"...*`,
questions: [] // Temporarily hide old questions
});
try {
let finalAnswer = "*Processing...*";
let finalQuestions = [];
if (useStreaming) {
await new Promise((resolve, reject) => {
let aggregatedContent = "";
let currentAnswer = "";
let currentQuestions = [];
let aggregatedReasoning = "";
getCompletionStreaming(
request, apiKey,
(chunkData) => {
aggregatedContent = chunkData.content || aggregatedContent;
aggregatedReasoning = chunkData.reasoning || aggregatedReasoning;
indicatorInstance._renderStreamingAnswer(aggregatedContent, aggregatedReasoning);
},
(result) => {
aggregatedContent = result.content || aggregatedContent;
const answerMatch = aggregatedContent.match(/\[ANSWER\]\s*([\s\S]*?)(?=\[3 FOLLOW UP Questions\]|$)/);
finalAnswer = answerMatch ? answerMatch[1].trim() : "[No answer found in response]";
finalQuestions = extractFollowUpQuestions(aggregatedContent);
const finalReasoning = aggregatedReasoning;
const currentCache = tweetCache.get(tweetId) || {};
currentCache.questions = finalQuestions;
currentCache.timestamp = Date.now();
tweetCache.set(tweetId, currentCache);
indicatorInstance._updateConversationHistory(questionText, finalAnswer, finalReasoning);
indicatorInstance.questions = finalQuestions;
indicatorInstance._updateTooltipUI();
resolve();
},
(error) => {
finalAnswer = `Error generating answer: ${error.message}`;
indicatorInstance._updateConversationHistory(questionText, finalAnswer);
indicatorInstance.questions = [];
indicatorInstance._updateTooltipUI();
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.");
}
const content = result.data.choices[0].message.content;
const answerMatch = content.match(/\[ANSWER\]\s*([\s\S]*?)(?=\[3 FOLLOW UP Questions\]|$)/);
finalAnswer = answerMatch ? answerMatch[1].trim() : "[No answer found in response]";
finalQuestions = extractFollowUpQuestions(content);
const currentCache = tweetCache.get(tweetId) || {};
currentCache.lastAnswer = finalAnswer;
currentCache.questions = finalQuestions;
currentCache.timestamp = Date.now();
tweetCache.set(tweetId, currentCache);
indicatorInstance._updateConversationHistory(questionText, finalAnswer);
indicatorInstance.questions = finalQuestions;
indicatorInstance._updateTooltipUI();
}
} catch (error) {
const errorMessage = `Error answering question: ${error.message}`;
indicatorInstance._updateConversationHistory(questionText, errorMessage);
indicatorInstance.questions = cachedData?.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.4.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();
})();
})();